The Loup Native Book — contents
  1. Chapter 1: Getting started
  2. Chapter 2: Native mode overview
  3. Chapter 3: Variables and numerics
  4. Chapter 4: Functions
  5. Chapter 5: Control flow
  6. Chapter 6: Collections
  7. Chapter 7: Structs
  8. Chapter 8: Enums and pattern matching
  9. Chapter 9: Classes and ARC
  10. Chapter 10: Interfaces
  11. Chapter 11: Generics
  12. Chapter 12: Error handling
  13. Chapter 13: Modules and packages
  14. Chapter 14: Template fences
  15. Chapter 15: Ownership verbs
  16. Chapter 16: borrow, excl borrow and noescape
  17. Chapter 17: unique ownership
  18. Chapter 18: Lifetimes and regions
  19. Chapter 19: @unsafe and calling C
  20. Chapter 20: Concurrency with OS threads
  21. Chapter 21: Async on native
  22. Chapter 22: Desktop UI
  23. Chapter 23: SIMD and GPU
  24. Chapter 24: Building and distributing
  25. Chapter 25: Memory safety patterns

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 with clang at -O2 with ThinLTO. It is the only backend for macOS, and the one to ship with.
  • cranelift generates code in-process and is faster to compile. It writes ELF objects only, so it works on Linux and is refused elsewhere.
  • llbe is 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 llbe is 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 shared and --lib static are 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 @cexport annotation 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] entry and delivery decide what loup build makes.
  • --target native builds a platform executable that contains the Loup runtime; cli, service and desktop build WebAssembly that runs under loup-launcher on every platform, and desktop packages it as a self-contained directory.
  • llvm is the default and the shipping backend; cranelift is Linux-only; llbe is 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.
  • --lib output, manifest link directives, a stable C ABI and the size/strip/reproducibility flags are planned or absent.
  • loup test runs test_… functions on the interpreter; loup test --native runs them as native binaries.
  • Chapter 25 brings the safety features of the previous chapters together.