Chapter 24: Building and distributing
This chapter covers getting a native Loup program from a project directory onto someone else's machine: the project manifest, the delivery targets loup build can produce, the native build options and backends, which platforms can build what, testing, and what to ship. Everything here is the 1.25.1 command line; where the specification describes more, the text says so. The normative references are the Language Reference §3-2.1 to §3-2.8 and §1-13.3 (the manifest).
Projects and the manifest
loup build always works on a project: a directory with a manifest named <name>.lppkg (or package.lppkg). loup-bootstrap init creates one:
loup-bootstrap init inventory
cd inventory
This writes inventory.lppkg, src/main.loup (a host.dual "hello" program) and an empty tests/ directory. The manifest is TOML. The part that matters for a native program:
name = "inventory"
version = "0.1.0"
loup = "1.20.0"
authors = []
[build]
entry = "src/main.lphc"
delivery = "native"
profile = "release"
[dependencies]
[modes]
Listing 24-1: The manifest of a native project
| Key | Meaning |
|---|---|
name, version |
the package identity (required) |
loup |
the minimum toolchain version |
description, authors, license |
metadata |
[build] entry |
the file that holds main() |
[build] delivery |
what loup build produces by default (below); target is accepted as the older spelling |
[build] profile |
release or debug |
[build] mode |
the mode for files that do not declare one (every file should, ruling MODE-2) |
[build] arch |
the default --arch |
[dependencies], [dev_dependencies] |
other packages, by local path (chapter 13) |
[modes] |
per-file mode overrides |
[options] |
the package's own build options, set with --option NAME=VALUE |
[exports] modules |
the modules other packages may import |
The scaffold writes a few more tables ([test], [stdlib], [linker], [freestanding], [const_eval]); leave them as they are. loup.toml and loup.pkg are not read.
To switch the scaffold to host.native, rename src/main.loup to src/main.lphc, change its first line to @mode(host.native), and set entry and delivery as in Listing 24-1 (chapter 1 walks through it).
Delivery targets
delivery in the manifest, or --target on the command line (also spelled --delivery), chooses what a build produces:
| Target | Produces | Runs with |
|---|---|---|
native |
a platform executable | itself |
cli (the scaffold's default) |
build/<name>.wasm |
loup-launcher --run-bare build/<name>.wasm |
desktop |
an application directory: bin/main (the launcher), lib/main.wasm, loup-package.json |
./<dir>/bin/main |
service |
a WebAssembly module for a long-running service | loup-launcher |
browser |
a bare build/<name>.wasm; no HTML page or JavaScript loader yet |
— |
freestanding |
a bare-metal image (The Loup Freestanding Book) | a board or QEMU |
native needs a native toolchain (next section). The WebAssembly targets need nothing but the Loup archive, on every platform, and they accept host.dual programs. That makes desktop the practical way to hand a host.dual program to someone on Windows or Linux arm64 today:
loup-bootstrap build --target desktop -o inventory-app
./inventory-app/bin/main
hello, inventory
The directory is self-contained: copy it anywhere and run bin/main. loup-package.json records the toolchain version, the build profile and a SHA-256 hash of each file. Despite the name, desktop does not mean a graphical program; the planned GUI library is chapter 22.
The WebAssembly targets are designed for host.dual programs. Stage0 1.25.1 will also build a host.native file to WebAssembly without complaint, even one that declares a unique class or an extern native function, although the specification makes host.native a native-only mode. Keep code that you deliver both ways in host.dual, where the compiler does refuse the native-only features (chapter 2).
Native builds
loup build --target native -o inventory
./inventory
loup build --help lists every option. The ones for native builds:
| Option | Meaning |
|---|---|
-o PATH |
where to write the executable |
--release, --debug |
the release or debug profile (--profile release|debug) |
--backend llvm|cranelift|llbe |
the code generator (below); llvm is the default |
--lto thin|full |
link-time optimization (LLVM only) |
--arch ARCH, --os OS |
target architecture and OS (see "Platforms") |
--foundation libc|libos |
the runtime the program links against; libc is the default, libos is the Helios kernel's runtime |
--error-traces, --no-error-traces |
record where ? propagated an error (on by default with --debug) |
--option NAME=VALUE |
set one of the package's [options] |
--no-cache, --clean |
rebuild everything / delete the .loup-cache directory |
-v, -q |
more or less progress output |
The executable contains the Loup runtime (reference counting, strings, collections, exceptions, threads). It links dynamically against the system C library and nothing else, so it runs on any machine of the same OS and architecture with a compatible C library.
Backends
llvm(the default) writes LLVM IR and compiles and links it withclangat-O2with ThinLTO. It is the only backend for macOS, and the one to ship with.craneliftgenerates code in-process and is faster to compile. It writes ELF objects only, so it works on Linux and is refused elsewhere.llbeis Loup's own clang-free backend. It emits machine code and ELF files itself, so it needs no C toolchain at all. With the Stage0 compiler it builds freestanding images only (E-BUILD-BACKEND:--backend llbeis only supported for freestanding targets); the self-hosted toolchain on Linux x86-64 also uses it for hosted builds.
Mistake: Cranelift on macOS
loup build --target native --backend cranelift -o inventory
error[E-BUILD-NATIVE]: --backend cranelift writes ELF objects, which a macOS host cannot link; use the default LLVM backend (`--backend llvm`)
Drop the option. The other native-build refusal you will meet is a construct the native code generator does not lower yet. It names every offending declaration, for example:
error[E-BUILD-NATIVE]: unsupported in managed native (libc foundation): async function `main` (native builds have no task scheduler: `async`/`await` run on the interpreters only); …
Chapter 2 lists these constructs (async, template fences, for concurrent, enums with tuple or Result payloads, and a few others). The build stops instead of producing a binary that behaves differently from loup run.
Platforms
| Build machine | Native executables | Notes |
|---|---|---|
| Linux x86-64 | supported | needs clang; all three backends |
| macOS 11+ (Apple silicon, Intel) | preview | LLVM backend only; needs the Xcode Command Line Tools; --arch arm64 or --arch x86_64 selects either Apple architecture |
| Windows 10+ x86-64 | not available | build WebAssembly targets instead |
| Linux arm64 | not available | the Stage0 compiler still emits x86-64 code |
There is no cross-OS native build: the executable is always for the build machine's OS. --os is accepted but only names the output, and on macOS other architectures are refused (--arch riscv64: a macOS host builds hosted native programs for arm64 and x86_64 only). To ship Linux and macOS binaries, build on each. A macOS build is signed ad hoc by the linker, which is enough to run it locally; distributing it to other Macs requires your own codesign and notarization.
Libraries
⚠ Planned.
--lib sharedand--lib staticare accepted, but in 1.25.1 the file they write (lib<name>.so,.dylib,.a, …) carries the module's WebAssembly bytes, not native code, so it cannot be linked into a C or Rust program (§3-2.1). Calling Loup from other languages, the stable ABI of §3-2.6 (stable_abi,abi_version) and the@cexportannotation are not implemented yet; see chapter 19 for the header tools.
Linking C libraries
An extern native "name" declaration (chapter 19) names the library its functions come from.
⚠ Planned. The specification also lets the manifest add libraries and search paths for the linker (
link,link_path,framework, §3-2.8). In 1.25.1 these directives cannot be written: the specification's line form (link sqlite3) is not valid TOML, and the TOML form is rejected (invalid library name '= ["sqlite3"]' in a 'link' directive in the native delivery section).
Testing
loup test finds the test files in a project (by default tests/**/*_test.loup, set by the manifest's [test] pattern) and runs every function named test_… that takes no arguments:
@mode(host.dual)
from debug.assert import (expectEq);
func checksum(data: string): int {
var h = 0;
for (i in 0..data.length()) {
h = h + data.length();
}
return h;
}
func test_empty(): void {
expectEq(checksum(""), 0, "empty input");
}
func test_three(): void {
expectEq(checksum("abc"), 9, "three characters");
}
Listing 24-2: tests/checksum_test.loup
loup test
Found 2 tests in 1 files (0 compile failures)
✓ tests/checksum_test.loup::test_empty (18ms)
✓ tests/checksum_test.loup::test_three (18ms)
Tests: 2 total, 2 passed, 0 failed, 0 skipped (19ms)
By default tests run on the interpreter. loup test --native builds each test through the native pipeline and runs the binary, which is how to catch the differences between the two (overflow, exception output, finalization timing). --filter, --jobs, --timeout and --format pretty|json|junit do what their names say. On Linux x86-64, where loup is the self-hosted toolchain, the runner is loup-test and is invoked as loup test --strict.
What is not in the command line
Earlier editions of this chapter described options that 1.25.1 does not have. Each of these is invalid argument '…': unknown flag or unknown command: --strip, --opt-for-size, --crate-type, --emit-header, --emit-symbols, --dynamic-runtime, --reproducible, --lockfile, --sanitize, loup bundle, loup run --inspect, and the [[bin]]/[[lib]] manifest tables. One project builds one entry point. There is no package registry yet, so dependencies are local paths (chapter 13).
Summary
- A project is a directory with a TOML manifest,
<name>.lppkg;[build] entryanddeliverydecide whatloup buildmakes. --target nativebuilds a platform executable that contains the Loup runtime;cli,serviceanddesktopbuild WebAssembly that runs underloup-launcheron every platform, anddesktoppackages it as a self-contained directory.llvmis the default and the shipping backend;craneliftis Linux-only;llbeis clang-free and, with Stage0, freestanding-only.- Native executables can be built on Linux x86-64 and (as a preview) on macOS, for the build machine's OS only. Windows and Linux arm64 cannot build them yet.
--liboutput, manifest link directives, a stable C ABI and the size/strip/reproducibility flags are planned or absent.loup testrunstest_…functions on the interpreter;loup test --nativeruns them as native binaries.- Chapter 25 brings the safety features of the previous chapters together.