WHEN: Creating or modifying system extensions (sysexts). WHEN NOT: Working on base images or desktop profiles without sysext involvement.
Guide to creating and modifying systemd system extensions for frostyard OS images.
A system extension (sysext) is an overlay image that extends /usr on an immutable OS. Sysexts are managed by systemd-sysupdate and applied at boot via systemd-sysext.
/usr/etc or /var at runtime/etc, use the factory pattern (see below)Minimal sysext config (copy from existing like 1password-cli):
[Config]
Dependencies=base
[Output]
ImageId=<name>
Output=<name>
Overlay=yes
ManifestFormat=json
Format=sysext
[Content]
Bootable=no
BaseTrees=%O/base
PostOutputScripts=%D/shared/sysext/postoutput/sysext-postoutput.sh
Environment=KEYPACKAGE=<primary-package>
Packages=<package-list>
Key fields:
Overlay=yes — only includes files that differ from the baseFormat=sysext — produces a sysext imageKEYPACKAGE — the primary package whose version is used for sysext namingPostOutputScripts — MUST point to the shared postoutput scriptEvery sysext needs two files in mkosi.images/base/mkosi.extra/usr/lib/sysupdate.d/:
<name>.transfer)Tells systemd-sysupdate how to download the sysext:
[Transfer]
Features=<name>
Verify=false
[Source]
Type=url-file
Path=https://repository.frostyard.org/ext/<name>/
MatchPattern=<name>_@[email protected] \
<name>_@[email protected] \
<name>_@[email protected] \
<name>_@[email protected]
[Target]
Type=regular-file
Path=/var/lib/extensions.d/
MatchPattern=<name>_@[email protected] \
<name>_@[email protected] \
<name>_@[email protected] \
<name>_@[email protected]
CurrentSymlink=<name>.raw
<name>.feature)Provides metadata and default enable state:
[Feature]
Description=<Human-readable description>
Documentation=https://frostyard.org
Enabled=false
Enabled=false is the default — users opt-in to sysexts.
The script at shared/sysext/postoutput/sysext-postoutput.sh runs after image creation:
KEYPACKAGE env var is setKEYPACKAGE from the manifest<name>_<version>_<arch>.raw[.zst|.xz|.gz]Do not modify this script per-sysext. If custom post-processing is needed, add a separate script.
Package versions come from the APT repositories configured in mkosi. To update a sysext's version:
KEYPACKAGE version in the repositoryshared/download/checksums.jsonsudo mkosi -f -i <name> — the postoutput script extracts and applies the new versionWhen a sysext needs files in /etc at runtime:
In the build, capture configs to /usr/share/factory/etc/:
# In mkosi.finalize or postinstall script
mkdir -p /usr/share/factory/etc/myapp
cp /etc/myapp/config.toml /usr/share/factory/etc/myapp/
Create a tmpfiles rule to inject at boot:
# /usr/lib/tmpfiles.d/myapp.conf
C /etc/myapp - - - - /usr/share/factory/etc/myapp
This works because systemd-tmpfiles runs early in boot and copies factory defaults to /etc if they don't already exist.
Sysexts cannot run systemctl enable at build time. To enable a systemd service shipped by a sysext:
System services: Use a systemd preset file:
# /usr/lib/systemd/system-preset/50-<name>.preset
enable <service-name>.service
User services: Use the factory pattern to create the enablement symlink:
# In postinstall script
mkdir -p /usr/share/factory/etc/systemd/user/<target>.wants
ln -sf /usr/lib/systemd/user/<service> /usr/share/factory/etc/systemd/user/<target>.wants/<service>
Then add a tmpfiles rule:
# /usr/lib/tmpfiles.d/<name>-user-service.conf
C /etc/systemd/user/<target>.wants/<service> - - - - /usr/share/factory/etc/systemd/user/<target>.wants/<service>
Create the mkosi.conf:
mkdir mkosi.images/<name>
# Copy and adapt from mkosi.images/1password-cli/mkosi.conf
Set the config fields: ImageId, Output, KEYPACKAGE, Packages
Add to root dependencies:
Edit mkosi.conf and add <name> to the Dependencies= list
Create sysupdate.d files:
# In mkosi.images/base/mkosi.extra/usr/lib/sysupdate.d/
# Copy and adapt <name>.transfer and <name>.feature from an existing sysext
Handle /opt packages (if applicable):
If the package installs to /opt, add a postinstall script to relocate:
# mkosi.images/<name>/mkosi.postinst.chroot
set -euo pipefail
mv /opt/<package> /usr/lib/<package>
ln -sf /usr/lib/<package>/bin/<binary> /usr/bin/<binary>
Handle /etc configs (if applicable): Use the factory pattern described above
Test the build:
sudo mkosi -f -i <name>
After building with sudo mkosi -f -i <name>:
.raw output to /var/lib/extensions.d/sudo systemd-sysext refreshsystemd-sysext statusls /usr/bin/<expected-binary>.raw file and run sudo systemd-sysext refreshmkosi.images/<name>/ directory, remove from root Dependencies=, and delete its .transfer and .feature files from mkosi.images/base/mkosi.extra/usr/lib/sysupdate.d/.