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:
validateverify-sourcessetuptoolchainchroot-prepchroot-toolscoreconfigcore-extrabasekerneldesktopaiextrabootloaderimagemanifestsquashfsukis-verityiso
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 six tiers, each with a distinct role in the build:
| Tier | Role |
|---|---|
toolchain | The compiler and core build tools used to build everything else |
core | Foundational system libraries and utilities |
base | The base userland on top of core |
desktop | The graphical desktop and its applications |
ai | The local AI runtime |
extra | Servers, developer tooling, browsers, and additional system utilities |
The number of packages in each tier drifts as the system grows. As of 2026-06-15, the build spans roughly 857 packages across the six tiers (toolchain 28, core 272, base 23, desktop 420, extra 112, ai 2). 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.
The ai tier holds two packages: InterGen, the tiered, hardware-detected, offline-first local assistant built on Qwen models, with zero telemetry; and llama.cpp, the inference engine it runs on. InterGen includes a security-scanner subsystem, InterGen Sentinel, that defaults to Local-Rules plus a local Qwen model and offers six opt-in cloud providers (Claude (Anthropic), Gemini (Google), Copilot (Microsoft), ChatGPT (OpenAI), Grok (xAI), and DeepSeek). 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, 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. A representative shape:
name: <name>
version: "<version>"
release: 1
description: <one-line description, no trailing period>
license: <SPDX-identifier>
homepage: https://<upstream-homepage>
tier: <core|base|desktop|extra|ai>
build_style: <custom|autotools|meson|cmake|cargo|python>
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:
-
Create the recipe directory and
package.yml. Choose the tier by role: foundational utilities go tocoreorbase; graphical apps todesktop; servers, developer tooling, and browsers toextra; the AI runtime toai. -
Author
build.shif the build style is custom, following the skeleton above and the no-stub rule. -
Wire it into the right builder. For
coreorbase, add the package’s exactname-versionliteral 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. Fordesktop,extra, orai, no script edit is needed: thetier:field is the entry point, and the Python builder picks it up automatically. -
Confirm reachability. Run the builder-coverage check. It reports any recipe that exists in the tree but is reachable by neither builder. Such an orphan will never build, and the failure surfaces late. Fix an orphan by registering it with the correct builder.
-
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.
-
Commit and push. A pre-push check refuses a new
package.ymlthat lacks bothverify_paths:and apending_acquisition:declaration. If it blocks, add theverify_pathsblock. Use a conventional commit message describing what the package is and why the system ships it. -
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 signed binary archive (.igos.tar.gz), generating the file list and SHA-256 hashes during a tracking phase. 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
- The Package Manager (pkm) — installing and verifying packages on a running system
- Reproducibility — how the build produces verifiable, repeatable results
- Repositories — where built packages are published and fetched from
- Package Reference — the package catalog
- Forge: the installer — installing InterGenOS to disk
- Verified Boot — the signed boot chain and dm-verity integrity