Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Building Packages from Source

InterGenOS is built from source. Every package that ships in the system is compiled from upstream code in a controlled environment, recorded with declared file paths, and verified before it ever reaches an image. This page explains how that build works, and how you can author a new package recipe and integrate it into the build.

This is the workflow behind the distribution itself. If you only want to install pre-built packages on a running system, see The Package Manager (pkm). If you want to understand how the whole system is assembled and signed, see Reproducibility.

Building from source is part of that posture: the goal is a machine you understand, can modify, and can trust, with no opaque binaries arriving from elsewhere.

How the system is built

InterGenOS (currently 1.0-dev, build id v1.0-dev1) is assembled in a sequence of build phases that run in order, each consuming the output of the ones before it. The phases are, in order:

  1. validate
  2. verify-sources
  3. setup
  4. toolchain
  5. chroot-prep
  6. chroot-tools
  7. core
  8. config
  9. core-extra
  10. base
  11. kernel
  12. desktop
  13. extra
  14. compute
  15. ai
  16. bootloader
  17. image
  18. manifest
  19. squashfs
  20. ukis-verity
  21. iso

An optional publish step follows when the resulting packages are being pushed to the repository.

The early phases build a clean toolchain and a temporary set of tools, enter an isolated build root, then build the system tier by tier. The later phases assemble the image, generate an integrity manifest, build the live filesystem (squashfs), sign the unified kernel image and set up dm-verity integrity, and produce the final ISO.

Packages and tiers

A package is a recipe directory. Recipes are grouped into seven tiers, each with a distinct role in the build:

TierRole
toolchainThe compiler and core build tools used to build everything else
coreFoundational system libraries and utilities
baseThe base userland on top of core
desktopThe graphical desktop and its applications
extraServers, developer tooling, browsers, and additional system utilities
computeOpt-in GPU-compute SDKs (ROCm stack) — entirely mirror-only
aiThe local AI runtime and the model-tooling stack

The number of packages in each tier drifts as the system grows. The build spans over 1,100 package definitions across the seven tiers, of which roughly three quarters ship on the installed image and the rest are mirror-only. Treat these as a live count, not a fixed figure; derive the current numbers from the package tree rather than relying on a number written here.

The desktop that ships today is GNOME 49 on Wayland (GNOME Shell 49.4).

The ai tier holds over 50 package definitions, but only two of them ship on the installed image: InterGen — the tiered, hardware-detected, offline-first local assistant built on an on-device model, with zero telemetry — and llama.cpp, the inference engine it runs on. The rest are the mirror-only model-tooling stack (PyTorch, Transformers, and their dependencies), which you can install from the repository when you want it and which is absent from a default installation.

InterGen includes a security-scanner subsystem, InterGen Sentinel, that defaults to Local-Rules plus a local Qwen model and offers six named opt-in cloud providers — Claude (Anthropic), Gemini (Google), Copilot (Microsoft), ChatGPT (OpenAI), Grok (xAI), and DeepSeek — plus a seventh option that accepts any custom OpenAI-compatible endpoint. Escalating a check to one of those providers is the “Phone-A-Friend” (Frontier/Cloud Escalation) path, and it is always opt-in.

Two builders

Which tool builds a package depends on its tier. A package is reachable by exactly one of the two builders, never both:

  • Core and base are built from static lists in shell scripts. Each of these phases enumerates the exact packages it builds.
  • Desktop, ai, compute, and extra are built by the Python tier driver. It walks the tier’s recipe directory, sorts packages by their declared dependencies, and builds everything reachable.

This split matters when you wire a new package in: the tier you choose determines where (and whether) you need to register it.

Anatomy of a package recipe

A recipe lives in a directory named for its tier and package name and contains:

package.yml      required: metadata + verify_paths
build.sh         required for custom builds; optional for autotools/meson/cmake
patches/         optional, applied by build.sh
<name>.1         optional manpage

package.yml

The metadata file declares what the package is, where its source comes from, what it depends on, and which files it must install. Both tier: and build_style: are closed sets that the parser validates and rejects on mismatch, so the values below are the complete list, not examples. A representative shape:

name: <name>
version: "<version>"
release: 1
description: <one-line description, no trailing period>
license: <SPDX-identifier>
homepage: https://<upstream-homepage>
tier: <toolchain|core|base|desktop|extra|compute|ai>
build_style: <custom|autotools|cmake|meson|make>
source:
  - url: https://<mirror-or-upstream>/<name>-<version>.tar.<ext>
    sha256: <expected-sha256-of-tarball>
dependencies:
  build: []
  host: []
  runtime:
    - <dep1>
    - <dep2>
verify_paths:
  - /usr/bin/<name>
  - /usr/lib/lib<name>.so
  - /etc/<name>/<name>.conf

The verify_paths block is the package’s promise about what it installs. Pick two or three paths that prove the package actually landed: the primary binary (/usr/bin/<name> or /usr/sbin/<name>), the primary library (/usr/lib/lib<name>.so*) for library-only packages, or a canonical data or config directory for data packages. Each path must be absolute and have at least three segments (for example /usr/bin/x). After the build, an audit checks that every declared path is present before the image is assembled. If a path is missing, the build halts there rather than shipping a broken package.

build.sh

For a custom build style, build.sh defines three functions that the build runs in sequence inside the build root: configure, build, and do_install. An optional post_install runs after the package is registered, for tasks like rebuilding a library cache. A typical skeleton:

#!/bin/bash
# <name> <version> — <one-line description>

configure() {
    set -e
    ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var
}

build() {
    set -e
    make -j"$(nproc)"
}

do_install() {
    set -e
    make DESTDIR="${DESTDIR}" install
    # Post-install fixups go here if needed.
}

Stubs are not allowed. A configure that does nothing and a do_install that installs nothing is forbidden. If the package has source to compile, the recipe must compile it. The only exception is a deliberate metapackage with no source, which must say so explicitly in a header comment and only write configuration in do_install.

Pin a specific version rather than tracking “latest”, and add a short comment justifying the pin.

Patches

There are two patch paths and they are not interchangeable. A downstream patch carried in the recipe’s own patches/ directory is applied by hand inside build.sh. The separate patches: key in package.yml is for patch files staged alongside a fetched upstream tarball and is auto-applied. Do not list the same patch in both places, and dry-run any patch against a fresh extraction before committing it.

Adding a new package

The end-to-end flow for landing a new recipe:

  1. Create the recipe directory and package.yml. Choose the tier by role: foundational utilities go to core or base; graphical apps to desktop; servers, developer tooling, and browsers to extra; the AI runtime to ai.

  2. Author build.sh if the build style is custom, following the skeleton above and the no-stub rule.

  3. Wire it into the right builder. For core or base, add the package’s exact name-version literal to the static list in the matching phase script. Use the exact literal, not a greedy prefix, or the list can silently swallow an unrelated package whose name shares the prefix. For desktop, extra, ai, or compute, no script edit is needed: the tier: field is the entry point, and the Python builder picks it up automatically.

  4. Confirm reachability. Run the builder-coverage check. It reports any recipe that exists in the tree but is reachable by neither builder, and it separately reports a tier mismatch — a package living under packages/A/ while declaring tier: B. Such an orphan will never build, and the failure surfaces late. Fix an orphan by registering it with the correct builder. The toolchain tier is excluded from this check and from the declared-path audit, because it is built outside the build root before either builder runs.

  5. Dry-run the build. Build just the new package inside the build root before committing to a full tier rebuild. The toolchain lives only inside the build root, so the build must run there, not on the host.

  6. Commit and push. A pre-push check refuses a new package.yml that lacks both verify_paths: and a pending_acquisition: declaration. If it blocks, add the verify_paths block. Use a conventional commit message describing what the package is and why the system ships it.

  7. Verify with a full rebuild. A complete rebuild is the definitive proof. The package builds as part of its phase, the pre-squashfs audit confirms its declared paths are present, and its archive appears in the post-build archive manifest.

When a package can’t be acquired yet

If a package legitimately cannot be obtained yet because it is blocked on an external dependency, replace the verify_paths block with a pending_acquisition: field stating the reason. The audit skips packages marked this way. This is only for genuinely blocked-on-external cases, not a workaround for not wanting to write the recipe.

From source to installed: the handoff to pkm

The from-source build is the factory; the package manager is the consumer.

The build compiles source and produces a binary archive (.igos.tar.gz), generating the file list and SHA-256 hashes during a tracking phase. The archive itself carries no signature; its integrity is committed by signature one level up — the signed repository index on the mirror, and the signed archive manifest on install media. On a running system, pkm is the tool that downloads and installs those archives. It records every file and its hash in a database at /var/lib/igos/pkm.db, alongside human-readable text manifests under /var/lib/igos/packages/ for inspection.

That recorded state is what makes integrity verification possible. pkm verify recalculates the hash of every installed file and compares it against the stored value, detecting both accidental corruption and unauthorized modification. Combined with the signed Secure Boot chain, dm-verity integrity, and UKI signing established during the build, the result is a system whose contents you can check, all the way from upstream source to the files on disk.

See also