SPS · log · files · refs · tags · releases · people · languages · GitHub · file
PACKAGE-MAINTAINER.md
17630 bytes at 2909919 · GitHub
SPS package maintainer guide
An SPS package directory contains a line-oriented file named recipe. It may also contain files/, patches/, and hooks/. Recipes are data, not shell files: repository indexing parses them without executing build commands.
The same recipe format works anywhere SPS runs, including Linux From Scratch and custom roots. linux-desktop, Plasma profiles, and setup defaults are SPS Linux (Splux) distro pieces; an LFS project can ignore them.
Recipe records
Each logical line is a key, whitespace, and a non-empty value. Blank lines and lines beginning with # are ignored. A trailing backslash continues a value on the next physical line. Inline comments are not recognized.
The complete key set is:
| Key | Count | Meaning |
|---|---|---|
name | one | Package name |
version | one | Upstream source version |
release | one | SPS recipe release |
arch | zero or one | Target architecture |
description | zero or one | Short package description |
source | repeatable | Upstream URL or local source path |
hash | one per source | Source SHA-256 digest |
depend | repeatable | Runtime dependency |
builddep | repeatable | Build dependency |
optional | repeatable | Informational optional dependency |
conflict | repeatable | Package names that cannot be co-installed |
prepare | repeatable | Source preparation command |
configure | repeatable | Configuration command |
build | repeatable | Build command |
install | one or more | Staged installation command |
Unknown keys are errors. SPS 1.0 has no recipe keys for homepage, license, maintainer, replacements, provides, or package options. Keep that information in repository documentation when needed; do not invent fields.
conflict is supported: sget will not install a package next to a named conflict. Use it for init systems (systemd vs openrc) and for udev implementations (systemd vs eudev).
name, version, release, and at least one install record are required. The install phase must produce a non-empty package. name must begin with an alphanumeric character and may then contain letters, digits, +, _, ., or -. Versions and releases also allow ~.
${name}, ${version}, ${release}, and ${arch} are expanded as recipe data. No other recipe variables are defined by the parser.
Sources and hashes
Every source has one hash in the same declaration order:
source https://example.org/project/project-${version}.tar.xz
hash sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefThe digest must contain exactly 64 hexadecimal digits. There is no skip value or insecure checksum mode. Download the canonical release archive and calculate its digest before committing the recipe.
HTTP and HTTPS URLs are downloaded with curl or wget. A bare relative path is resolved from the recipe directory. Use a bare relative path for a checked-in source file, or an absolute file:///path URL; relative file:// paths depend on the caller's working directory and should be avoided.
mkpkg automatically extracts .tar, .tar.gz, .tgz, .tar.xz, .txz, .tar.zst, .tzst, .tar.bz2, and .tbz2 sources. When extraction produces one top-level directory, that directory becomes $WORK; otherwise the extraction root is $WORK. Plain files remain below $SRC.
Source extraction rejects absolute member names and .. path components, but it is not a sandbox for arbitrary hostile archives. Use immutable, reviewed upstream releases with verified checksums. In particular, current SPS 1.0 does not independently validate every tar link target before extraction.
Dependencies
Use the singular keys depend, builddep, optional, and conflict. A record may contain one or more whitespace- or comma-separated specifications. Version constraints have no spaces around the operator:
depend zlib
depend openssl>=3.0
builddep pkgconf
builddep meson ninja
optional doxygenSupported operators are =, <, <=, >, and >=. Constraints apply to the upstream version, not the package release. sget install adds a package's build dependencies only when that package will be compiled. Well-known tools already on PATH (cmake, ninja, meson, patch, pkgconf, python, ...) satisfy a builddep without installing the package. optional is informational and is not installed automatically. Low-level mkpkg and pkin do not resolve dependencies.
Declare everything needed in a controlled build root. A successful build on a maintainer workstation is insufficient if undeclared host tools or libraries made it succeed.
New packages that are not ready for extra belong in sps-community. That tree is opt-in. Do not reuse a name from core or extra.
Architecture and release
Omit arch for compiled packages. It then defaults to the exact configured SPS_ARCH, normally uname -m. SPS performs no architecture aliasing or ELF inspection.
Use:
arch anyonly for architecture-independent scripts, data, documentation, or fonts. noarch is accepted as a compatibility spelling, but official recipes use any consistently.
Keep the upstream version and SPS release separate. Begin a new upstream version at release 1. Increment release whenever a recipe, source choice, patch, local file, hook, or build option changes without an upstream version change. Binary cache identity is name, version, release, and architecture; a release bump is therefore required to prevent reuse of an older artifact.
Build phases
mkpkg runs prepare, configure, build, and install in that order. Each phase is a separate /bin/sh -eu script starting in $WORK. Shell variables, functions, and directory changes do not carry between phases.
The phase environment provides:
| Variable | Value |
|---|---|
WORK | Extracted source root and phase working directory |
SRC | Downloaded/copied sources and support trees |
PKG | Package staging root |
MAKEJOBS | Positive parallel-job count |
SPS_TARGET_LIBRARY_PATH | Target lib64/lib dirs when SPS_ROOT is not / |
Typical build-system records look like:
configure ./configure --prefix=/usr
build make -j"$MAKEJOBS"
install make DESTDIR="$PKG" installconfigure cmake -S . -B build -G Ninja -DCMAKE_INSTALL_PREFIX=/usr
build cmake --build build --parallel "$MAKEJOBS"
install DESTDIR="$PKG" cmake --install buildconfigure meson setup build --prefix=/usr --libdir=lib64 --buildtype=release
build meson compile -C build -j "$MAKEJOBS"
install DESTDIR="$PKG" meson install -C buildMeson feature options take enabled, disabled, or auto, not true/false. Passing -Dpython=false to libxml2 2.15 is a meson error, not a compiler error.
Use the upstream project's intended build system and supported options. Build commands run on the host; mkpkg is not a chroot and cannot prevent a bad command from modifying paths outside $PKG.
When SPS_ROOT is a mounted disk (live setup), mkpkg puts that root on LD_LIBRARY_PATH so a just-built interpreter can import its own extensions against libraries already unpacked into the target. CPython's check_extension_modules will otherwise load the live disc's older libsqlite3, rename _sqlite3 to _sqlite3_failed, and then make install dies on sharedinstall. Host gcc, python3, meson, and git wrappers restore the previous LD_LIBRARY_PATH so those tools do not load a half-installed libc from the disk. Shared libraries belong in lib64 on this architecture; pass --libdir=/usr/lib64 (or Meson's --libdir=lib64) instead of relying on Autoconf's /usr/lib default. CPython also needs --with-platlibdir=lib64 so lib-dynload matches getpath; otherwise a build under SPS_ROOT dies with No module named '_posixsubprocess'.
Local files and patches
A real files/ directory is copied to $SRC/files; a real patches/ directory is copied to $SRC/patches. Entries must be ordinary directories or regular files. Symlinks, special files, and paths containing tabs or newlines are rejected. The repository definition digest includes support paths, contents, and copied modes, so run src update after any of them changes. src update batches recipe-only hashes; the digest is the same as hashing one package at a time. Nothing is applied or installed automatically.
For example:
builddep patch
prepare patch -Np1 -i "$SRC/patches/fix-build.patch"
install mkdir -p "$PKG/etc/example"
install cp "$SRC/files/example.conf" "$PKG/etc/example/example.conf"Document why each patch exists beside the patch or in the package's repository history. Remove patches that upstream no longer needs.
Hooks
The only lifecycle files accepted below hooks/ are:
pre-install
post-install
pre-remove
post-removeThey must be regular files, not symlinks, and their names may not contain tabs or newlines. Hook contents are part of the repository definition digest. mkpkg stores them as mode 0644, and package tools invoke them with /bin/sh -eu; an executable bit and shebang are unnecessary.
Install and upgrade hooks receive SPS_ACTION, SPS_PACKAGE, SPS_VERSION, SPS_RELEASE, and SPS_PACKAGE_ARCH. Remove hooks receive SPS_ACTION, SPS_PACKAGE, and SPS_VERSION. Normal SPS configuration variables, including SPS_ROOT, are also exported. Hooks run on the host rather than in a chroot and must prefix target paths with $SPS_ROOT.
A failed pre-hook stops the payload action. A failed post-hook returns status 10 after the package database change has committed. Use hooks only for visible, idempotent lifecycle work; ordinary build and install commands belong in the recipe.
Complete local example
examples/hello/recipe is a complete, offline recipe:
name hello-sps
version 1.0
release 1
arch any
description Minimal hello-world package used by the SPS examples
source hello.sh
hash sha256:25576de85c3f0b4dc3e7847956daf896c450318a00bb1b818869accf8fdc7268
install mkdir -p \
"$PKG/usr/bin"
install cp "$SRC/hello.sh" "$PKG/usr/bin/hello-sps"
install chmod 755 "$PKG/usr/bin/hello-sps"Build it with:
cd examples/hello
mkpkg --no-download --compression nonePackage verification
Build, inspect, install, verify, query, smoke-test, and remove every completed package. Keep SPS state outside the disposable filesystem root so leftover payload is easy to see:
tmp=$(mktemp -d)
root=$tmp/root
mkdir -p "$root" "$tmp/db" "$tmp/cache" "$tmp/build" "$tmp/out"
run_sps()
{
SPS_ROOT="$root" \
SPS_DB="$tmp/db" \
SPS_CACHE="$tmp/cache" \
SPS_BUILD="$tmp/build" \
SPS_CONFIG=/dev/null \
SPS_REPOS_CONFIG=/dev/null \
"$@"
}
result=$(mktemp)
run_sps mkpkg --artifact-file "$result" --output "$tmp/out" ./recipe
artifact=$(cat "$result")
tar -tf "$artifact"
run_sps pkin "$artifact"
run_sps pkcheck package-name
run_sps pkcheck --database
run_sps pkstat package-name
run_sps pkstat --files package-name
# Run an appropriate executable, link, or data smoke test here.
run_sps pkdel package-name
run_sps pkcheck --database
find "$root" -mindepth 1 -printRead the archive path from --artifact-file after mkpkg exits 0. Compression depends on the local configuration. Use a fresh output directory because mkpkg does not overwrite an existing artifact. --keep-build retains work and staging paths for failure inspection; redirect command output to a log outside Git when preserving a build log.
SPS_ROOT redirects installation but does not chroot a smoke test. Running a dynamically linked executable under $root/usr/bin may still use host runtime libraries. Use a controlled chroot or container when validating declared dependencies and runtime linkage.
pkcheck verifies installed presence, regular-file hashes, and database relationships. Also inspect modes, ownership, symlink targets, installed paths, and the resulting archive manually. After removal, account for intentionally retained modified protected files and any state created by lifecycle hooks.
Init scripts for daemons
Mapped daemons live in lib/setup/services. After pkin, SPS enables the service for the init named in /etc/sps/init (or inferred from the landed init-* package). Supported inits are systemd, openrc, s6, runit, dinit, shepherd, and sysvinit.
A daemon recipe ships a file for every init into that init's directory:
$PKG/usr/lib/systemd/system/NAME.service
$PKG/etc/init.d/NAME
$PKG/etc/sv/NAME/run
$PKG/etc/s6/sv/NAME/run
$PKG/etc/dinit.d/NAME
$PKG/etc/shepherd.d/NAME.scm
$PKG/etc/rc.d/init.d/NAMEpkin enables only the detected target init. Stock copies in lib/setup/sv/<init>/ fill a gap when an older package omitted a file. Add a stock file for every supported init whenever you introduce a new daemon:
lib/setup/sv/systemd/NAME.service
lib/setup/sv/openrc/NAME
lib/setup/sv/runit/NAME/run
lib/setup/sv/s6/NAME/run
lib/setup/sv/dinit/NAME
lib/setup/sv/shepherd/NAME.scm
lib/setup/sv/sysvinit/NAMEtools/write-stock-services.py regenerates those stock files from its service table and installs copies next to core and extra recipes.
Rolling upgrades (tools/upgrade-recipes)
Splux is a rolling distro. Maintainers bump every recipe with a real upstream tarball by running one command against the core and extra trees:
python3 tools/upgrade-recipes \
--trees /usr/src/sps/core:/usr/src/sps/extraThat prints a plan. It does not write files. To apply it:
python3 tools/upgrade-recipes \
--trees /usr/src/sps/core:/usr/src/sps/extra \
--apply--apply downloads each newer tarball, writes a real SHA-256, sets release to 1, and rewrites source URLs. It then unpacks the new source and scans Meson, CMake, Autoconf, and pkg-config files for libraries the recipe does not declare. Names that already exist in the trees are appended as depend. Names that do not exist are listed as missing. Pass --scaffold-missing to draft a conservative extra recipe when a source URL can be resolved.
Other useful switches:
--only gtk4,qemu,llvm limit the run
--jobs 12 parallel upstream probes
--scan-deps download and scan on a dry-run
--no-scan-deps skip tarball work (version plan only)
--include-kernel allow linux and firmware recipes
--json machine-readable plan
--selftest offline checksQt, KDE Frameworks, Plasma, and GStreamer modules are pinned together so one sibling cannot race ahead of the rest. GNOME even-minor libraries (gtk, glib, libadwaita, ...) skip odd development series. Tags containing alpha, beta, rc, or git are ignored. Kernel and firmware recipes stay pinned unless you pass --include-kernel.
Set SPS_RECIPE_TREES instead of --trees if you prefer. Set GITHUB_TOKEN if GitHub rate-limits the probe. Downloaded tarballs are cached under ~/.cache/sps-upgrade (override with SPS_UPGRADE_CACHE).
Run src update in the recipe trees after a successful apply, then build the packages that moved.
Auditing the trees (tools/audit-recipes)
src update validates recipe syntax, same-repository uniqueness, and the dependency graph. It never downloads anything, so it cannot tell you that an upstream tarball moved, was re-rolled, or now answers with an HTML sign-in page. That failure only surfaces when a user's mkpkg aborts on a checksum, which is the worst possible place to find out.
audit-recipes covers the rest:
python3 tools/audit-recipes --trees /usr/src/sps/core:/usr/src/sps/extraOffline it reports unknown recipe keys, missing required fields, a name that disagrees with its directory, mismatched source/hash counts, malformed or placeholder digests, one digest reused for two different URLs, self and duplicate dependencies, a dependency no recipe in the trees provides, runtime dependency cycles, duplicate names inside one tree, names shadowed across trees, plaintext http:// sources, a remote URL that never mentions the version, and a build phase that escalates privilege or installs outside $PKG.
Add --net to download every declared source and verify its SHA-256:
python3 tools/audit-recipes --trees ... --net --jobs 16Sources are streamed through the hasher and never written to disk, so the whole tree costs bandwidth and time but no storage. Run it on a schedule rather than on every push.
Useful switches:
--only foo,bar audit a few packages (the rest of the tree still
resolves their dependencies)
--fail-level warn make warnings fail the command too
--json machine-readable findings
--jobs N parallel downloads for --net
--timeout N per-source download timeoutBoth audits are also Make targets:
make audit-recipes RECIPE_TREES=/usr/src/sps/core:/usr/src/sps/extra
make audit-recipes-net RECIPE_TREES=/usr/src/sps/core:/usr/src/sps/extraOne caution when writing your own checks against upstream hosts: do not send a browser User-Agent. SourceForge and several other hosts answer browser agents with an HTML interstitial and give curl/wget the real archive, so a spoofed agent turns every SourceForge recipe into a false hash mismatch. mkpkg uses a plain curl -fL, and audit-recipes matches it.