lumi's code spelunking adventures

AerynOS Adventures: Moss

A little adventure into the inner workings of Moss, the package manager of AerynOS


Introduction

I have recently switched from using NixOS to using AerynOS, and also started contributing to the project. The package manager used in AerynOS is called Moss. It works in a different way from most package managers you may have seen.

Personally, I really like how it works, which is a part of why I've switched to it and started contributing to it. The tooling is also written in Rust, and, while I have some mixed feelings about the Rust project and ecosystem, it is still my favorite programming language.

The project having a full ban on GenAI was another reason influencing my decision to switch to it, as I do not think generative models can be ethical.

While spelunking into the codebase, trying to learn how everything fits together, I thought to myself, "what if I wrote a post about it?". After that thought, this blog post materialized, which now makes it your problem. You're welcome.

Jokes aside, I hope this will be an interesting read for you. :)

There are links to source code sprinkled all over this post, these all point to code in the AerynOS/os-tools repository at commit 83f08d46dc27fc34e64d054da480e0c83170495e. This commit was created on July 13th 2026. If you are reading this post significantly after that date, keep that in mind.

Overview

In this post, I'll go through everything that happens when you run moss sync -u, from retrieving package indexes, to downloading and unpacking packages, to building the new filesystem root and switching to it.

On a high level, when you run moss sync -u, it will:

Note: "VFS" here has nothing to do with the Linux VFS. They both mean "Virtual FileSystem", however.

The rest of this article goes into more detail about all of these steps, and some notes around them.

System Structure

AerynOS uses an FHS-compliant filesystem, like most Linux distributions, and unlike distributions such as NixOS, GuixSD and GoboLinux.

There are a few peculiarities, however.

Moss directory

The directory /.moss is used by Moss to store the data necessary to its functioning, this includes caches, databases, roots and repos.

Inside this directory, you will find:

Immutable usr

In AerynOS, /usr is atomic (not immutable!). All packages and their default configurations are installed under /usr, and system-scope triggers are used to do all the rest. The default configurations live under /usr/share/defaults, and packages are patched such that they will look there after looking in /etc.

For anyone that likes doing the ./configure, make, sudo make install ritual, you'll be pleased to hear that /usr/local is a symlink to /var/local. Though, please just package it using boulder. It's not difficult, I promise!

Background

Before I get started explaining the whole procedure, I'll go through some background topics that may be useful, like the stone format, content-addressable stores and the SQLite databases that are being used.

Stone Format

Packages and package indexes use a format called stone, a stone file is referred to as a stone. It contains a header followed by a list of payloads. Every payload consists of a list of records of the same type. Payloads can optionally be compressed using the zstandard compression scheme.

The stone format is manipulated using the stone crate.

There are 4 types of records types in use:

There is also an attribute record type, but as far as I can tell, this is currently not being used anywhere.

Package indexes (stone.index) consist of all of the meta records copied from all of the packages contained inside of a moss-format .stone repository.

Here is a visualization of both a package .stone archive and a stone.index .stone archive:

Visualization of the stone format

Content-Addressable Stores

Moss uses content-addressable stores everywhere. These are directories where the names of the files are determined by their hashes. The specific hash function used and the organization of these stores varies a bit between them.

To prevent having one directory with a ridiculous amount of files, files are categorized into directories based on the prefixes of their hashes. This is done both for performance reasons and to ensure we're not hitting into any inode limits. This is also known as a "trie" data structure.

One of the content-addressable stores is the asset store, which stores all of the unpacked stone content. It uses XXH3_128bit hashes and is located at /.moss/assets/v2. It has a 4-level directory structure. Taking 2 hex characters of the hash on every level until the last, where the content is stored as the full hash.

For example: /usr/bin/env has XXH3_128bit hash b9c764ff4842d6da1e731bddd246b17e, which means it gets stored in b9/c7/64/b9c764ff4842d6da1e731bddd246b17e.

More visually:

Visualization of the asset store paths

The download store is another one of these stores, it is located at /.moss/cache/downloads/v1 and uses a 3-level directory structure, taking 5 characters, then another 5, and then storing the file as the full hash. It (currently) uses SHA256 hashes.

For example: 009b6/d35de/009b6a147cddc394325446b9b62ef8605126263c1d5d540db62b8fbcaebd35de

The download and asset content store paths are constructed in client/cache.rs.

Just Living In The Database, Wow Wow

Moss uses SQLite for storing metadata about packages and system states.

These can be found in /.moss/db. The code for manipulating these databases is in db.

Moss sync procedure, for realsies

Now, let's run through what happens when you run moss sync -u!

Fetching indexes

When fetching indexes, there are two places where the URLs can be found:

These URLs point to stone.index files, which can be obtained locally or via HTTP, depending on whether the scheme is "file" or "http".

As mentioned in the stone format section, these are moss-format .stone archives with many meta records corresponding to the packages in the repository.

Every repository also has a priority: this is how Moss determines which package to fetch, in case multiple repositories have the same package.

Fetching indexes is handled in:

Fetching and unpacking packages

After this, the packages are selected, respecting dependencies, conflicts, etc.

Every meta record has a package URI field, this is the URI where the package will be fetched from.

In parallel, all of the packages are now downloaded and unpacked:

As a cleanup operation, the file in the content cache is removed.

The unpacking code can be found in cache.rs.

Blitting

The term VFS refers to "Virtual FileSystem". It is an in-memory representation of a filesystem tree and it is used for building up the new root. The VFS is implemented in the vfs crate.

Every entry in the VFS stores a Layout Record, the same one as in stones. This is sufficient because it contains the hash, and the hash can be used to find the file content in the asset store.

All of the layouts for the current root are collected into one VFS, and this VFS is then traversed in parallel, spawning one job for the root and on every directory firing off more jobs to run in a thread pool. On every node visit, the appropriate filesystem operations are done inside of the root to build it up. This process is called "blitting".

How each layout entry is blitted depends on its type:

The root is installed in /.moss/root/<name of root> and then /.moss/root/<name of root>/usr is atomically swapped with the system /usr, using the renameat2 syscall with the RENAME_EXCHANGE flag set.

To prevent having one file hash with a ridiculous amount of hard-links, empty files are not hard-linked.

Triggers

A trigger is a script that will run to finalize the installation of packages and/or finalize the installation of the system. Triggers are yaml files which are provided by packages by placing these files into sub-directories of /usr/share/moss/triggers.

There are two kinds of triggers:

The trigger-related logic is in:

System activation

When the system is activated, /usr is atomically swapped with the /usr in the selected root, then transaction-scope triggers are run, and then system-scope triggers are run.

An atomic swap means that there is no "in-between" state, the whole /usr directory tree is swapped at once.

Outro

Spelunking into source code to see what you can find can be a lot of fun!

I would encourage anyone to do that. Often the best way to find out how something works is to read the code.

If you think AerynOS would be something for you, give it a try! It's currently in alpha, but it has been pretty comfy for me. Join the Zulip and tell us what you think! :3

This is the first post on my blog, and hopefully many more will follow. There is an RSS feed if you're interested!