Deployment
d6 build compiles a .d6 program into a standalone native executable:
no d6 interpreter, no .d6 source file, and no recompilation step at
run time. It exists for shipping a finished d6 program to a machine
that doesn't have (and shouldn't need) a d6 checkout, alongside the
ordinary d6 program.d6 interpreter path.
./build/d6 build app.d6 -o app
./app arg1 arg2
From a project directory, plain d6 build builds its entry file and names the binary
after the directory:
cd greeter # made by `facet new cli greeter`
d6 build # builds main.d6 into ./greeter
CLI contract
d6 build [SOURCE] [-o OUTPUT] [--cc=COMPILER] [--target=TRIPLE] [--lto]
SOURCEdefaults to the first ofmain.d6(whatfacet newgenerates),app.d6and legacyapp.diin the current directory; with none of them,d6 buildexits 64. It is compiled exactly the wayd6 program.d6compiles it -- identicalrequireresolution, identical embedded-prelude-template fast path -- so anything that runs correctly under the interpreter compiles identically underbuild.-o OUTPUTnames the produced binary. Defaults toSOURCE's own basename with its final extension stripped (app.d6->app, and legacyapp.di->app). A basename without an extension is unchanged. WhenSOURCEis left out too, the binary is named after the current directory (cd greeter && d6 build->greeter). An output path that is an existing directory exits 64.--cc=COMPILERoverrides the C compiler used to link the binary (gcc,clang, a full path, ...). Defaults to the compiler recorded in the installed AOT kit, or to whatever$(CC)resolves to for an ordinarymakeinvocation in a checkout.--target=TRIPLElinks for another platform from a cross kit built withmake aot-kit-cross(see "Cross-compilation" below). It never falls back to the host toolchain: a missing or mismatched kit exits 74.--ltolinks the kit's LTO runtime (see "Optional LTO runtime" below) instead of the default one. It needs an installed kit built withAOT_LTO=1; without one it exits 74 and says how to build it. It does not combine with the checkout'smakepath.- A compile error (a real syntax/type/name error in
SOURCEor anything itrequires) prints the same diagnosticd6 program.d6would and exits 65 -- the C compiler andmakeare never invoked in this case. - A
make/link failure (a missing toolchain or system library) exits 74. - On success, prints
diamond: built 'OUTPUT'and exits 0. - An installed
d6(see "Installed AOT kit" below) links with a single compiler invocation against its prebuilt kit and needs neither this checkout normake. Without a kit it uses the checkout, as follows. - The compiler finds its own build checkout from the d6 executable,
while
SOURCEand relativeOUTPUTpaths resolve from the caller's working directory. The checkout still supplies the C sources and Makefile used for linking. - The first build compiles the runtime into
build/aot-COMPILER/and archives it aslibdiamond-aot.a. Later builds reuse that archive and compile only the generated program data. Header/source changes and changes to compiler flags rebuild affected runtime objects. The cache stamp includes a SHA-256 fingerprint of the runtime sources, headers, Makefile, and embedded prelude, so replacing a checkout with files that have older timestamps still invalidates the archive safely.make cleanremoves the default cache.--ccuses a separate cache per compiler. SetAOT_CACHE_ROOTto an absolute path to keep the archive outside an ephemeral checkout; remove that directory explicitly when no longer needed. Container builds should key this path to their image/toolchain.
Installed AOT kit
make aot-kit collects everything d6 build needs to link into
build/aot-kit/: the runtime archive libdiamond-aot.a (compiled at -O2),
libreginold.a, and one-argument-per-line files recording the compiler,
compile flags, link flags, and d6 version. make install copies the
built tools to PREFIX/bin and the kit to PREFIX/lib/diamond/aot:
make release facet lsp dap
make install PREFIX="$HOME/.local"
An installed d6 build finds the kit at <bin>/../lib/diamond/aot
(DIAMOND_AOT_KIT overrides the location), refuses a kit recorded for a
different d6 version, and runs one cc command: the generated program
data plus the two archives and the recorded link flags. It still needs a C23
compiler with #embed support (GCC 15+ or Clang 19+) and the development
libraries named in the link flags, but not the d6 sources, headers, or
make. The kit is specific to the platform and library versions it was built
against.
Optional LTO runtime
The default runtime is compiled at -O2 without link-time optimization, so d6 build
links in a fraction of a second. make aot-kit AOT_LTO=1 (or make install AOT_LTO=1) adds a
second archive, libdiamond-aot-lto.a, holding the same sources at -O3 as fat LTO objects,
plus compile-lto.args; d6 build --lto links against that, so the compiler optimizes
the runtime and the program together. Measured on five bench/ programs against the default:
-O2 with LTO is about 2% slower and -O3 with LTO about 3% faster (instructions and cycles
both), and every --lto build recompiles the runtime in that link, about 38 s instead of
0.2 s. That is why it is opt-in at both ends. The compiler needs LTO support (GCC's works with
plain ar; Clang needs a linker plugin or lld).
JIT in standalone programs
Built executables use the same opt-in JIT settings as the interpreter:
DIAMOND_JIT=1 ./app
DIAMOND_JIT=1 DIAMOND_JIT_THRESHOLD=1 DIAMOND_TRACE_JIT=1 ./app
The default compilation threshold is 50 calls per eligible function; a function
containing a loop is compiled by its first call, since a loop in a function called
once would otherwise run interpreted to completion. The
JIT runs only on glibc x86-64; on other platforms the executable stays on
the interpreter path. DIAMOND_TRACE_JIT=1 prints compiled-function and
bailout counts when the program returns normally; a server terminated by a
signal may not emit that summary. The JIT remains off when DIAMOND_JIT is
unset, empty or 0, including for standalone binaries.
At the language level, the produced binary behaves exactly like
d6 program.d6: it sets ARGV from its own command-line arguments, prints only what
the program writes, and honours DIAMOND_PRINT_RESULT the same way
(running a program).
What this is not
- Not a fully static/hermetic binary by default. (A static musl kit
produces one; see "Static musl binaries" below.) The produced executable still
dynamically links whatever
d6itself links against on this platform -- OpenSSL, SQLite3,libpq, the MariaDB client library, zlib,libcrypt, and their own transitive dependencies (confirmed withlddagainst a real build). "No separate interpreter, source file, or recompilation step" is the actual guarantee; the binary is exactly as portable across machines asd6itself is, no more and no less. - Cross-compilation needs a prepared kit. Without
--targetthe binary targets the machined6 buildran on; see "Cross-compilation" below. - The d6 build checkout is still needed at build time.
diamond buildinvokes its checkout'smake aot-buildtarget to link the binary; there is no separate installed runtime library yet. The finished binary has no dependency on the checkout at run time and can run from any working directory, including on a machine with no d6 checkout.
See docs/roadmap.md for what's explicitly deferred here (static linking).
Cross-compilation
d6 build app.d6 --target=aarch64-linux-gnu produces a binary for another
platform. The compiled program is portable between targets with the same word size
and byte order (the program image records both and a mismatched image is refused when
it loads); what changes is the runtime, which must be compiled and linked for the
target together with the target's own OpenSSL, SQLite3, libpq, MariaDB client, zlib
and libcrypt. A cross kit holds that runtime, built once per target:
make release
make aot-kit-cross TARGET_TRIPLE=aarch64-linux-gnu SYSROOT=/opt/sysroots/aarch64 \
CROSS_FLAGS_EXTRA='--gcc-install-dir=/opt/sysroots/aarch64/usr/lib/gcc/aarch64-redhat-linux/16'
./build/d6 build app.d6 --target=aarch64-linux-gnu
- A cross build needs a C23 compiler that targets the triple (clang 19+ with
--target, defaultCROSS_CC=clang) and a sysroot: the target's C library, headers, and the libraries above. A distribution's own aarch64 packages extracted into a directory work;CROSS_FLAGS_EXTRApasses anything else the driver needs (--gcc-install-dir,-fuse-ld=lld). - The kit lands in
build/aot-targets/TRIPLE;make installcopies every such kit toPREFIX/lib/diamond/aot/targets/TRIPLE, which an installedd6searches first. The kit records the sysroot path, so the sysroot must still exist whered6 build --targetlinks. - The prelude snapshot is generated by the host build and reginold is rebuilt from a scratch copy for the target, so the host build is left alone.
- The JIT is x86-64/glibc only, so other targets run on the interpreter.
- Test a target with
tests/cross_build_test.sh(make test-cross-build); setDIAMOND_CROSS_SYSROOT,DIAMOND_CROSS_TARGET,DIAMOND_CROSS_FLAGS_EXTRAand optionallyDIAMOND_CROSS_RUN(an emulator such asqemu-aarch64-static -L SYSROOT) to build and run a real cross binary.
Unless the kit is static (below), the built binary links the target's shared libraries dynamically, so it is as portable across machines as that sysroot's libraries are.
Static musl binaries
A musl target plus STATIC=1 produces a fully static executable with no dependency on
the target machine's libc, OpenSSL, SQLite, libpq or zlib. It runs on any Linux of that
architecture, including a FROM scratch container:
make aot-kit-cross TARGET_TRIPLE=x86_64-alpine-linux-musl SYSROOT=/opt/sysroots/alpine \
NO_MYSQL=1 STATIC=1 \
CROSS_FLAGS_EXTRA='--gcc-install-dir=/opt/sysroots/alpine/usr/lib/gcc/x86_64-alpine-linux-musl/15.2.0 -rtlib=libgcc --unwindlib=libgcc'
./build/d6 build app.d6 --target=x86_64-alpine-linux-musl
- The sysroot needs static libraries:
libc.a,libssl.a/libcrypto.a,libsqlite3.a,libz.a,libpq.awithlibpgcommon_shlib.a/libpgport_shlib.a,libucontext.a(musl has noucontext), andlibgcc.a. An Alpine root withmusl-dev,openssl-libs-static,sqlite-static,zlib-static,libpq-dev,libucontext-dev,libgcc-static,linux-headersandgccprovides all of them.STATIC_LDLIBSoverrides the extra archives for a sysroot that names them differently. - MySQL is left out of the kit (
NO_MYSQL=1): MariaDB Connector/C has no static build in Alpine.MySQL.openraises aMySQLError("MySQL support is not compiled into this build") and-lmariadbis not linked. (The portabled6below builds Connector/C from source to include it.)NO_POSTGRES,NO_SQLITE,NO_ZLIB,NO_TLSandNO_CRYPTOdo the same for those libraries, andMINIMAL=1sets all six; they work for ordinary builds too. - The binary reads the host's CA certificates for TLS (
/etc/ssl), so a minimal image still needs a certificate bundle for outbound HTTPS. - Static glibc is not offered: its name-service lookups load shared libraries at run time. Use musl for static output.
The portable d6
An experimental build, not attached to releases, while its limits (below) are worked out.
tools/build_portable.sh produces d6-VERSION-linux-x86_64.tar.gz or
d6-VERSION-linux-aarch64.tar.gz, holding d6, facet, d6-lsp and d6-dap as fully
static musl executables that run on any Linux distribution of that architecture, with or
without a native package. Unpack the tarball and put its directory on PATH. On every pull
request, CI runs them in clean Ubuntu 26.04, Debian 12, Fedora 44, Arch, Alpine
and openSUSE Leap containers: the smoke script, and a facet new project installing its
test cut from the registry and passing its tests.
Ubuntu 22.04 is no longer a supported deployment target or part of the CI matrix.
tools/build_portable.sh # build/portable/d6-linux-$(uname -m)/ and the tarball
tools/test_portable_distros.sh # packaging/portable_tools_smoke.sh on each distro
tools/build_portable.shbuilds inside an Alpine container (podman or docker) from the checkout's tracked files, so the host needs neither musl nor the static libraries. libxcrypt (forBCrypt) and MariaDB Connector/C are built from pinned, checksummed source releases.- Connector/C carries its authentication plugins statically (
caching_sha2_password,sha256_password,client_ed25519,parsec,dialog, clear password), since a static binary cannot load them at run time. Kerberos (auth_gssapi) is left out. - Every built-in library works: SQLite, PostgreSQL, MySQL/MariaDB, TLS, zlib and the crypto calls. The Arel dialect tests pass against live MySQL 8, MariaDB 11 and PostgreSQL 16 servers.
- What the host still provides: CA certificates in
/etc/ssl(cert.pemorcerts/, from the distribution's CA package) for TLS, andcurl(plusgitfor git sources) forfacet. - Limits compared with the packages:
- The x86-64 build targets the baseline ISA (
-march=x86-64), not x86-64-v3. - There is no JIT, which needs glibc (
DIAMOND_JIT=1is ignored). d6 buildis unavailable, because the tarball carries no build kit. It stops with "cannot locate d6 build files"; use a package or a source checkout to build executables.
- The x86-64 build targets the baseline ISA (
Building for a different target distro
Because d6 build links against whatever OpenSSL/SQLite3/libpq/MariaDB
client/glibc versions the build machine has ("Not a fully static/hermetic
binary" above), a binary built on one distro isn't guaranteed to run on
another -- most concretely, glibc is backward- but not forward-compatible, so
a binary built on a newer glibc can reference symbol versions an older-glibc
target doesn't have.
Build on a machine with the target's distro and architecture. For Skindicate,
the production droplet has one CPU core and 1 GB of RAM, so never compile
on the droplet. Its deployment runbook
builds on a local Ubuntu 26.04 VM with build_vm.sh, then uploads the finished
binary with deploy_prebuilt.sh. The upload script updates tracked static
assets and restarts the service; it does not build on the droplet.
A container is another way to match the target distro for local development.
docker/Dockerfile.ubuntu-build is an example:
an ubuntu:26.04 base image with the dev packages in
CONTRIBUTING.md. It has no source baked in;
source comes from a bind mount at container-run time, so it reflects whatever is on disk
locally -- committed or not).
The container must never build against a live checkout in place via a
read-write bind mount: some build artifacts (e.g. this repo's own
reginold/libreginold.a) live as plain tracked files rather than under
BUILD_DIR, so a native build and a container build sharing one directory
risks make's mtime-based staleness tracking silently reusing a
wrong-platform object file left over from the other one. The safe shape is:
- Bind-mount the source read-only into the container.
cp -rit into the container's own writable layer at container start (an ephemeral copy, gone when the container exits).- Build entirely inside that copy, using the container's own freshly-built
d6(never the host's). - Copy only the finished binary back out, via a second, read-write bind mount pointing at a dedicated output directory -- the only thing that crosses the container boundary in either direction besides the initial read-only source copy.
skindicate.dia/build_ubuntu.sh implements this container build for local
use. Production uses its VM build script, which also records the source
revision required by deploy_prebuilt.sh.
The example keeps AOT runtime objects in skindicate.dia/dist/aot-cache/,
outside the disposable source copy. d6 fingerprints the runtime inputs,
so the cache remains safe when a new source copy has older timestamps. The
d6 CLI's make release step still rebuilds on each container run, while
the AOT runtime compilation is reused. Delete dist/aot-cache/ when that
cache is no longer wanted.
One more thing a cross-build must override: the Makefile's own release
target defaults CFLAGS_RELEASE to -march=native (right for the common
case -- no distro packaging exists yet, so whoever builds d6 is
building it for the machine they're going to run it on). A container only
isolates the OS/libraries, not the CPU, so building inside one still runs on
the host's actual CPU -- inheriting that default would bake the build
machine's exact CPU features into a binary meant for a different one.
build_vm.sh and build_ubuntu.sh pass make CFLAGS_RELEASE="-O3 -DNDEBUG -march=x86-64-v3" release instead: x86-64-v3 is a named, standardized ISA tier (AVX2/BMI2/
FMA/LZCNT/MOVBE), not one CPU's exact feature set -- supported by any real
x86_64 machine from roughly 2013 (Intel Haswell) or 2015 (AMD Excavator)
onward, recovering the same speedup -march=native would give a local
build (confirmed directly: bench/int_arithmetic.d6, ~3.9s/iter at the
portable x86-64 baseline vs ~0.5s/iter at both native and x86-64-v3 --
multi-precision arithmetic leans heavily on BMI2/ADX) without tying the
binary to whichever machine happened to compile it. Any cross-build script
needs the same explicit override -- verify the actual deploy target's CPU
flags first (ssh target grep flags /proc/cpuinfo, checking for avx2 bmi2 fma), don't just assume.
x86-64-v3 isn't an arbitrary choice of override for this one script --
it's d6's stated practical minimum supported configuration (see the
Makefile's own CFLAGS_RELEASE comment and the README's system
dependencies section). A deploy target older than that is unsupported, not
just slower.
Caveat: matching the distro and release does not guarantee identical
package versions. A local VM or container can drift from the target's
apt upgrade history. Inspect the produced binary with file and ldd,
then smoke-test it on the target after upload.