kdos

Decisions

This chapter records the choices in KDOS where a reasonable engineer could have gone the other way: the compositor, the application catalogue, the C library, the base userland and PID 1, where upstream sources are kept, how the ports tree, KDOS’s own code and the build phases are laid out, how CPU optimisation is chosen, and about twenty smaller ones. It is written for readers who want to know why the system has the shape it has, and for contributors about to propose a different approach, whose objection may already have an answer here. Read Why KDOS, How KDOS differs and Principles first; the entries assume the goals and rules set out there.

Each entry gives the conclusion first. Most then give the question it answers, the alternatives and why they were not taken, and what the choice costs; an entry leaves a part out where there is nothing more to say about it than the conclusion already does. The entries link to the chapters that describe the mechanism as built. The final section, Narrowings, collects smaller decisions that look like missing features.

Index

Decision Conclusion
The compositor A frozen hard fork of labwc 0.20.0, never merged from again
Application delivery The medium carries native ports; podman builds catalogue applications on request
Application packaging One artefact per application over shared runtimes, never one image
The base distribution in boxes Debian trixie, with Alpine carried as a scratch base
The host C library musl, which forecloses runtime CPU dispatch
The base userland GNU coreutils in full, grep, sed, gawk, findutils, diffutils, patch and tar; no toybox, BusyBox or uutils
PID 1 sysvinit 3.18 with runlevels; util-linux agetty and sulogin
The host desktop A desktop written for this system; KDE’s and GNOME’s applications, never their shells
Where upstream sources live Numbered volumes of shelves, each file under <shelf>--<file> and checked by its sha256, fetched by make fetch
CPU optimisation Measured per machine by kdos march, never a shipped feature level
Compiler flags -O2 with hardening for every port; -O3 per port on precedent; LTO only for a listed set of hot libraries
The vulnerability database A vendored, pruned copy of Alpine’s secdb, answered offline
Recipe format Two files: parsed metadata, plus ordinary bash
The ports tree 102 subject shelves under ports/core; a port is its bare name, unique across the tree
KDOS’s own code Six areas under src/, every port exactly two levels deep
Build phases Thirteen named phases in bands of ten, split by the dependency graph; each list names exactly what its phase installs
Binhost signing One signature over the index; a signature file holds several
Updates Every image carries its recipe tree; a newer one arrives signed over the network and is built on the machine; no binhost
The ASCII demo A frozen hard fork of the AA-project’s bb 1.3rc1
The terminal state machine libkvt, a hard fork of libtsm 4.7.1
The file chooser One program at 64×22 for portal and native callers alike
Narrowings Small decisions that read like gaps

The compositor is a frozen fork of labwc

KDOS runs a frozen hard fork of the labwc 0.20.0 Wayland compositor. src/desktop/kdos-comp is upstream’s source, imported whole, renamed, and never merged from again. KDOS-FORK at its root records the upstream tarball and its sha256. KDOS’s additions live in twenty-three files named src/kdos-*.c, and the upstream files carry small hooks marked /* KDOS */ (or # KDOS in a meson.build), so grep finds every point where the fork touches upstream code.

The question was what to do about a compositor that needs things no existing one offers: the phosphor shader (the pass that renders the screen as a CRT in the current accent), a wallpaper the compositor owns, a frame-timing channel, an idle policy and per-box client identity. The choice was to write one on wlroots or to take an existing one.

A compositor written from scratch was rejected because window management, session lock, screen capture, the clipboard, the input-method relay, Xwayland integration and security-context filtering are not what this project is about, and each is a protocol whose semantics only show under real clients. Taking labwc’s implementation brings parts that people outside this project have exercised for years — both generations of the capture protocols (wlr-screencopy and ext-image-copy-capture), both data-control managers, the text-input and input-method relay — and leaves the effort for the parts that are specific to KDOS.

A fork was chosen over a patch series because a .patch file against a compositor becomes a merge conflict at the next upstream release, and these additions would not be accepted upstream: they are one distribution’s opinions. Freezing means the source in the tree is the source that builds, readable in a pager, with no patch step between the two.

The cost is that upstream fixes do not arrive. A security fix in labwc has to be read and applied by hand. That is accepted; the alternative is maintaining a compositor outright.

See kdos-comp for the fork as built.

Native applications on the medium, a store that builds the rest

The installation medium carries native ports of the applications a machine needs with no network: a browser, an office suite, media and graphics tools, maps, an offline library, and specialist tools for CAD, electronics, software radio, science and amateur radio. A machine that never sees a network has only what its media carry, so these are compiled like every other port and ship in the root filesystem; The ports catalogue lists them by shelf. For everything else, the medium carries a catalogue, and podman builds what somebody asks for. A catalogue row is a parent chain of apt packages: installing a row builds a podman image per row in its chain, each FROM the one below, and creates a box (a rootless podman container one application runs in) over the top one. The catalogue file installs to /usr/share/kdos/appstore/catalogue, and it is what the store, kinstall (the installer) and kdos app all read, so adding an application to KDOS is one line in a text file. kdos-store, the panel’s graphical catalogue, and kinstall both offer the catalogue by group — seven of them, from one application each in essential, creative, make and games to six in science — so choosing the six science applications is one choice rather than six.

Carrying every catalogue application prebuilt on the ISO, as packs (the application images described below), was rejected: it puts every application on every medium whether or not it is ever launched, costs an hour of building to add one row, and needs a release channel to push the whole set through.

The choice has three costs.

  1. A store install is unsigned. It pulls its base image from a public registry and its packages from Debian’s archive, and nothing in /etc/kdos/keys vouches for either. kdos-box create prints exactly that about an OCI base, and it is true of every application built this way. The claim that everything is verified holds for an imported set and for nothing else.
  2. Installing needs a network, and minutes of apt.
  3. A live session keeps less of what it installs. $HOME is on the boot overlay there, so the container store runs through fuse-overlayfs, and what a store install builds is written under $HOME, held in memory and lost at power-off unless the session has a persistence store (see Getting started). A pack box’s writable layer is always a tmpfs there, persistence store or not, because the kernel refuses an overlay upper directory on overlayfs, so whatever a box writes outside the home directory is gone at shutdown. See Known gaps.

That is why import and the pack format exist. kdos-appbox export <file.ktar> <id|group>... writes the built images as packs with an index, signed when KDOS_PACK_KEY names a readable key and saying plainly that the index is unsigned otherwise. `kdos-appbox import

[...]` stages each pack through `kdos-packd`, the root daemon that mounts packs, which hashes and signature-checks it where it mounts it; with no names it imports the whole set the archive carries. An imported application is better verified than a store-installed one and needs no network. kinstall imports a set it finds on a stick, but into the live session rather than onto the new disk (see [Installation](/docs/kdos/02-user-guide/installation.html#8-applications)). The pack format is how a set is carried, not how software is distributed. ## One pack per application, not one image Each application is its own artefact over a small set of shared [runtimes](/docs/kdos/06-reference/glossary.html) (layers holding a toolkit or a family of libraries that many applications sit on): a podman image per catalogue row when the store builds it, and a [pack](/docs/kdos/06-reference/glossary.html) per row when a set is exported. Installing an application disturbs nothing else, and a shared runtime's layers are stored once however many applications sit on it. The question was how the catalogue's 73 applications (the `app` rows in `src/system/kdos-appbox/catalogue`) reach a machine: as one container image, or as separate artefacts. A single image was rejected because it puts every application on every install whether or not it is ever launched, and it turns adding one application into a rebuild of the whole image. The alternative also hits two limits that have nothing to do with size. Overlayfs cannot add a layer to a mounted overlay, so installing an application into a shared box would mean restarting a container other applications are running in. And a hundred lower directories do not fit in the 4096 bytes `mount(2)` allows for its option string. Per application, a stack is two to four layers — the base, none, one or two runtimes, and the application — and the pages of a shared runtime are shared between boxes because each pack is mounted once. The cost is one `conmon`, podman's container monitor, per running application rather than one for all of them, and only for applications that are open. See [Packs and boxes](/docs/kdos/03-architecture/packs-and-boxes.html) for the stack as built. ## Debian inside boxes, not Alpine The application catalogue is built on `debian:trixie-slim`, with every row installed from a pinned Debian snapshot archive. The question was which base distribution the catalogue should use. KDOS itself uses musl as its C library and would pair naturally with Alpine. The catalogue decided it. Its purpose is to offer the strongest free application in each segment it covers — office, creative work, development, science, CAD and 3D printing, games — and Debian packages and patches nearly all of them, where Alpine's repositories lack a number of them. The one application the catalogue carries that Debian does not package, VSCodium, is fetched as a `.deb` from its own releases by a `deb` line in the catalogue, and apt resolves its dependencies. An all-musl system would be more consistent and its images far smaller, and neither buys anything for a user who wants to open a CAD file. The catalogue's `snapshot = auto` line pins the Debian archive to the date the base image itself was built from, so the packages installed on top cannot disagree with the root filesystem under them. A literal timestamp pins it explicitly, and `off` reads the live archive and gives up reproducibility. Alpine is present all the same, as a second base pinned to `alpine:3.24.1`: about 3 MB of busybox and musl, with no packages added. A clean scratch userland that needs no network is worth one row, and Alpine is the one non-Debian root filesystem small enough that carrying it costs little. The cost is size, and it is accepted: a Debian box is much larger than the same program would be on Alpine. Every generated `apt-get install` passes `--no-install-recommends` to keep it from growing further. ## musl as the host C library The host C library is musl. The question was which C library the host is compiled against, and the alternative was glibc, which the Debian base of the boxes uses. musl was chosen because it is small, readable and close to the standards, which suits a system meant to be understood in full. musl closes some doors, and they are stated here rather than discovered later. It has no `glibc-hwcaps` mechanism, so runtime dispatch for CPU optimisation — building several variants of a library and letting the loader pick one — is not available, which is one reason [`kdos march`](/docs/kdos/04-programs/kdos-command.html#kdos-march) measures per machine instead. Some upstream code assumes glibc extensions and needs a build flag or a patch. And a header warning that glibc does not emit can turn an upstream `-Werror` build fatal. The application catalogue is glibc, inside boxes built on the Debian base. That is the purpose of the [ring](/docs/kdos/06-reference/glossary.html) boundary: the host and the boxes need not share a C library. ## GNU at every name, no multi-call userland The base userland is GNU's: coreutils in full, as separate programs, with grep, sed, gawk, findutils, diffutils, patch and tar, cross-built in `10_bootstrap` and rebuilt as ports in `30_foundation`. util-linux, procps-ng and iproute2 supply the system tools. The question was whether a compact multi-call userland could serve as the base, as BusyBox does for Alpine. The alternatives were toybox, BusyBox, uutils, chimerautils and sbase. The deciding property is how a tool fails. A compact tool that differs from GNU tends to differ silently: an `install -m644` that writes 0600, an `awk` that prints nothing, a `grep` that matches nothing and reports no error. On a system that compiles every package, each such difference is a build that goes wrong without failing, and the wrong output reaches the image. BusyBox on musl gives different answers from GNU on about a third of a probe set, several of them wrong with no error, and its current releases are labelled unstable; uutils needs Rust, which the build has only after its compilers phase. GNU coreutils passes its own test suite on musl. What this costs is a longer first build stage and a few megabytes on the image. What it buys is that every configure script meets the tools upstream tested against, and that a build which passes produced what it says it did. See [Packaging](/docs/kdos/03-architecture/packaging.html#the-base-userland). ## sysvinit as PID 1 PID 1 is sysvinit 3.18: `init`, `telinit`, `halt`, `poweroff`, `reboot`, `shutdown`, `runlevel` and `killall5`, and nothing else from that package. Every console is util-linux's `agetty`, and single-user mode is util-linux's `sulogin`. The question was which init runs the numbered shell scripts, with systemd ruled out by [principle](/docs/kdos/01-philosophy/principles.html#no-systemd). sysvinit gives runlevels, so single-user mode, power-off and reboot are states of `/etc/inittab` rather than code paths of their own; it builds on musl and against utmps with no patch, so it writes the boot and runlevel records `who -b` and `who -r` read; and its shutdown order is a list of `wait` entries anyone can read. It does not supervise services: that stays `ksvc`'s job, so the choice of PID 1 changes nothing about how a daemon is run. The constraint it brings is that `telinit` is the only way to ask it for a runlevel. It ignores `SIGTERM`, and `SIGUSR2` closes its control FIFO for the rest of the boot, so a program that wants the machine off runs `telinit 0` and never signals PID 1. See [Boot and init](/docs/kdos/03-architecture/boot-and-init.html#rcs-and-the-service-scripts). ## No KDE, GNOME or any existing desktop on the host The host runs a desktop written for it, in which every surface KDOS paints is a character-cell grid: the panel and all its surfaces, the file chooser, the resource monitor, the terminal, the lock screen, the installer, the boot splash and `tty1`. The compositor is the one place with pixels of its own. It links `cairo` and `pangocairo` and draws titlebars, the root menu and the window-switcher display with pango, at a size matched to the grid. An application, native or in a box, draws whatever its toolkit draws. The question was why KDOS does not run one of the complete desktops that exist. It has two reasons: the cell grid is the project's identity, and keeping it keeps every large toolkit out of the desktop itself, which is what makes compiling and reading the desktop in one sitting tractable. KDE Plasma on the host was the serious alternative. It was rejected because it would put Qt and KDE Frameworks, a body of code larger than the rest of the host combined, under the desktop's own surfaces, and bring a second session with its own daemons, portals and lock screen. This does not reject KDE's or GNOME's applications. Dolphin, Kate, Okular, Kdenlive, GIMP and others are ported natively with their toolkits and run as ordinary clients of `kdos-comp`, and the catalogue carries more on the shared `rt-kde` and `rt-gtk` runtimes. None of them needs Plasma or GNOME Shell running. See [Principles](/docs/kdos/01-philosophy/principles.html#toolkits-are-for-applications-not-the-desktop) and [Packs and boxes](/docs/kdos/03-architecture/packs-and-boxes.html). ## Upstream archives are hash-checked release assets Upstream source archives are not kept in git. Every source file a recipe names by a `sha256 =` line is a release asset in the GitHub repository `kunaldawn/kdos`. The exception is a small set of upstream files that git does carry, such as bash's and readline's patch levels, the IANA registries, `certdata.txt` and a Tesseract language model; those are never archived. The archive is a run of numbered volumes, pre-releases tagged `sources-1`, `sources-2` and on, and each shelf of `ports/core` has its volume, the second field of its line in `ports/shelves`. A file goes into the volume of the first shelf, in the tree's sorted order, whose recipe names it, under the name `--`; a file only old history names is kept as `attic--` in the highest volume. The asset carries the shelf and the file's own name, so a file's address reads like what it is: ``` https://github.com/kunaldawn/kdos/releases/download/sources-1/archiver--zstd-1.5.7.tar.gz ``` The committed file `ports/sources.idx` records the release and the asset for each hash. The current recipes name 2,487 distinct files, 38.6 GiB in total: 39 that git carries, and 2,448 that belong in the archive, spread over 102 shelves and packed into four volumes of 583 to 646 assets each, under GitHub's 1,000 assets per release. Sixty of them exceed the 100 MiB a push to github.com refuses; they appear 74 times across the port directories, because a file such as the LLVM source tarball serves several ports. The largest, `texlive`'s texmf tree, is 4.96 GB, over GitHub's 2 GiB limit per asset, so any file larger than 1,900 MiB is stored as `.part01` onwards and joined by `ports/fetch`. `KDOS_SOURCES_REPO` names a different archive repository, and `KDOS_SOURCES_BASE` a different download base; setting `KDOS_SOURCES_BASE` empty makes `make fetch` skip the archive and go from the local cache straight to upstream. The hash is the identity and the name is advisory. A recipe names contents, not a location, so a file that verifies is the file the recipe meant whether it came from the archive, from upstream or from a mirror added in ten years, and none of those changes a commit. A readable name is what lets a person browsing a release, or reading a URL in a log, see which file it is; the hash already guarantees which bytes it is. Two upstream files of different bytes under one filename on one shelf would meet in one volume, so the second is stored as `----`, and a third as `------`. GitHub rewrites every character outside `[A-Za-z0-9._-]` in an asset name to a dot, so the index records the name GitHub returned rather than the one asked for, and each asset's label keeps the true name for the release page. Volumes keep the release page short: four archive releases rather than one per shelf, each a readable run of neighbouring shelves in sorted order, its title naming the first and the last. A volume is planned to about 650 assets, so it has room for the versions to come before GitHub's limit of 1,000. The mapping is committed and never changes for a shelf that has one: `ports/publish --plan` gives a volume only to a shelf without one, packing those into the highest volume and then new ones, so no planning run moves a file. A file whose volume is full goes to the lowest-numbered volume with room, or opens the next volume, and the index records where it went; a later `--rehome` brings it home once its volume has room. The index is what the naming costs: one committed line per file, ` /`, with `parts=:

,…` for a split file, which `ports/publish` writes only after GitHub reports every uploaded asset's digest equal to the bytes it sent. Its first line, `# kdos-sources-index 2`, is its format; an index without it is not read at all, so a checkout with an index of another format fetches from upstream rather than misreading an address. The newest index says where every archived file is now, and an old checkout can be fetched with it (`ports/fetch --tree `). The archive is append-only by default. A verified asset is never replaced, because replacing one would silently change what an old commit builds; a checkout from five years ago finds the bytes it was written against after upstream has moved or gone. Two maintainer commands remove assets, and both say so by name. `ports/publish --rehome` moves a file whose port has changed shelf into the new shelf's volume and name, and a file that spilled back into its own volume, and deletes the old copy only once the new one is verified and both the working and the pushed index place the file elsewhere. `ports/publish --orphans --prune=yes-delete` deletes files no current recipe names, sparing every file a recipe at any `v*` tag names or a freeze list under `build/freeze/` holds; a checkout older than the pruning fetches those files from upstream. For each KDOS release, `ports/publish --freeze ` attaches `sources.sha256`, the list of every hash that tag's recipes name, to the release of that tag, creating it as a draft when it does not exist. The list is a convenience, one file that says what the release needs. What pins the hashes is the tag itself: its recipes carry them, and git cannot change those without changing the tag. The archive's releases share the repository's release page with the KDOS releases. Each volume's notes hold a section per shelf, with the shelf's description and every file of it the volume holds, with version, size and hash, and each is a pre-release created with `make_latest` off, so "latest" always means a KDOS system release. GitHub's immutable releases stay off on the repository: the setting applies to every release in it, and it would freeze an archive release at its first publication, after which no source could be added to it. The first cost is that a clone alone does not build. `make fetch` has to run once after a clone and again after a recipe changes, and it is the only build step that uses the network. It takes each file from the local cache or the archive, and falls back to the upstream URL in the recipe's `source =` line, so a file missing from the archive, or an archive that cannot be reached, costs a download from upstream rather than a failed build, as long as upstream still has the file. See [Where sources come from](/docs/kdos/05-developer/developing.html#where-sources-come-from) for the order it looks in, and [How KDOS is built](/docs/kdos/05-developer/how-kdos-is-built.html) for where `make fetch` sits in the build as a whole. The second cost is on the publishing side. A new or bumped source has to reach the archive, by `ports/publish`, before the commit naming it is pushed, or that commit builds on the machine that wrote it and nowhere else. The pre-push hook in `script/hooks/` enforces this by refusing a push whose recipes name a hash the archive cannot be shown to hold. See [Writing ports](/docs/kdos/05-developer/writing-ports.html#publishing-sources) for the procedure. Git LFS would make a clone the whole input to a build, and it is not used because the sources do not fit in it. A free account has 10 GiB of LFS storage and 10 GiB of monthly bandwidth, shared across every repository the account owns. The current sources take 38.6 GiB, nearly four times that storage, on their own, before any older version the history names, and a single clone uses most of a month's bandwidth. Past the allowance LFS reads are blocked outright, not slowed, so a repository that depends on LFS stops checking out. Release assets carry no total-size or bandwidth limit and allow 2 GiB per asset, and the one larger file is stored in parts. Plain git objects are not possible at all, since sixty of the files exceed the push limit. ## `-march` measured per machine, not chosen for a population CPU optimisation is measured on the machine that will run the result. [`kdos march`](/docs/kdos/04-programs/kdos-command.html#kdos-march) builds a port twice, once with the highest x86-64 feature level the CPU's `/proc/cpuinfo` flags allow, runs that port's own benchmark against both builds, and keeps the flags only where the median win exceeds 3 per cent plus the machine's own measured noise. The verdicts go to a ledger, `/var/lib/kdos/march.ledger`, which `kdos march report` prints: kept, reverted and unmeasurable. A port with no `bench =` line in its recipe is unmeasurable, never a winner; six ports under `ports/core` carry one. Each measurement takes the median of five runs by default, because a single run measures the scheduler and a mean measures the worst outlier. The noise floor is the larger spread of the two builds' own runs, so a busy or thermally throttled machine has to see a larger win before it keeps the flags. Shipping a feature level was rejected because published benchmarks of `x86-64-v3` show both gains and regressions: some programs run faster and others slower, and some draw more power for the same work. A distribution that shipped v3 everywhere would ship those regressions and never learn of them. Runtime dispatch, which would choose between variants at load time, is not available on musl (see [musl as the host C library](#musl-as-the-host-c-library)). Because KDOS rebuilds itself on the machine it runs on, rebuilding per machine is available here in a way it is not for a binary distribution. That turns "which flags" into "did they help on this machine", which is a question with a measurable answer. ## One release flag set, raised per port Every port from `20_selfhost` on builds with one set of flags, from `script/env/chroot.env`: `-O2 -pipe -fPIC -fno-semantic-interposition -fstack-clash-protection -D_FORTIFY_SOURCE=2`, with `-D_GLIBCXX_ASSERTIONS` for C++, linked with `-Wl,-O1,--sort-common,--as-needed,-z,now,-z,pack-relative-relocs`. Go builds with `-buildmode=pie`. The whole set, with the defaults for CMake, cargo and Go, is in [Writing ports](/docs/kdos/05-developer/writing-ports.html#the-release-flags). `-O2` is the level Arch, Fedora, Debian and Gentoo build with. Alpine's base is `-Os` and T2 SDE's is `-Os` with `-O2` for hot code; both raise single packages rather than the whole tree. `-O3` across every port means larger code and more exposure to undefined behaviour in code nobody here has tested at that level, for a gain that is mostly vectorisation, which on the baseline SSE2 target is small. So `-O3`, LTO and PGO are per port: a recipe raises its own level where upstream's default or Alpine's recipe does, and LTO is for a short list of hot code only: the media codecs and ffmpeg, zstd and lz4, pipewire, pixman, cairo and harfbuzz, python and Pillow, and cmake's own build. The list is in [Writing ports](/docs/kdos/05-developer/writing-ports.html#every-port). CMake's Release `-O3` is left in place, because it is the configuration upstream tests. The one level below `-O2` is in the bare-metal cross compilers: the `libgcc` and `libstdc++` they build for a microcontroller are compiled `-Os`, as Alpine's are, because that code is linked into flash measured in kilobytes. The global `-fPIC` keeps static archives linkable into shared libraries, but under it GCC may not inline an exported function into a caller in the same file, since a preloaded library could replace it. `-fno-semantic-interposition` gives that inlining back; the cost is that `LD_PRELOAD` cannot replace a library's call to a function defined in the same source file. Calls between files, and exported data, still go through the PLT and GOT. What is ruled out, and why: - **No feature level above x86-64 v1**, in any language: that is [`kdos march`'s](#-march-measured-per-machine-not-chosen-for-a-population) question, per machine. - **No `-Ofast` or `-ffast-math` from the tree**: they break IEEE and `errno` semantics that numerical, database and audio code depends on. Where upstream chose one for its own code, as goxel, Hydrogen, Sauerbraten and Routino do, the recipe keeps upstream's choice after the exported flags: that is the configuration upstream tests. - **No global `-DNDEBUG`**: some headers change a structure's layout under it, so a library and its consumer built with different settings would disagree. CMake's Release sets it for its own project; a meson port keeps its assertions unless it is one of the few hot ones listed in [Writing ports](/docs/kdos/05-developer/writing-ports.html#meson). - **No global `RUSTFLAGS`**: it replaces a project's own `.cargo/config` flags. No global `panic=abort`, which changes what `catch_unwind` does. - **No global `-Werror`** of any kind: it gains nothing at run time and fails ports hours into a build. - **Hardening is not traded for speed**: default PIE and SSP from the compiler, stack-clash probes, a read-only GOT, fortified C library calls through the `fortify-headers` port (musl has no `_FORTIFY_SOURCE` of its own) and libstdc++'s assertions stay on every C and C++ port, and Go executables are PIE. Where the tree's flags do not reach is measured, not assumed: Go has no stack protector or fortification, GHC links its compiler and programs with `-z,now` but without PIE (its static libraries are not position-independent, so a `-pie` link fails, and only a `-dynamic` program links as PIE), Rust has no stack protector by design, yices2, wine, valgrind, LuaJIT (texlive's `luajittex`) and mednafen turn the stack protector off in their own builds, and what `00_cross` and `10_bootstrap` build, `kpkg` among it, is not fortified. The table is in [Writing ports](/docs/kdos/05-developer/writing-ports.html#the-release-flags). - **No control-flow protection** (`-fcf-protection`, Intel CET): the instructions cost nothing on an old processor, but the shadow stack needs the C library to enable it per process and to handle it across `setjmp`, signals and threads, and musl does neither. Indirect-branch tracking alone protects nothing without a kernel that enforces it for user space. Revisit when musl enables user shadow stacks. - **`00_cross` and `10_bootstrap` keep their own plain flags**, from `script/env/host.env`: a flag the cross toolchain mishandled would break the compiler every later phase is built with. The exception is `kpkg` and `kinstall`. No package phase rebuilds them, so the binaries `10_bootstrap` compiles are the ones that ship, and their compile lines carry the tree's hardening themselves: `-fstack-protector-strong -fstack-clash-protection` and `-Wl,-O1,--as-needed,-z,now,-z,pack-relative-relocs -Wl,--build-id=sha1`. ## Alpine's security database, not NVD or OSV [`kdos cve`](/docs/kdos/04-programs/kdos-command.html#kdos-cve) answers from a vendored, pruned copy of Alpine's security database, `src/system/kdos-tools/secdb/secdb.txt`, installed to `/usr/share/kdos/secdb.txt`. It is about 260 KiB: 4,099 fix records for 798 packages, merged from twelve Alpine branches (`main` and `community` for v3.19 to v3.24) by `secdb/vendor.py`. It is committed and diffable, so the answer needs no network. The question it answers is narrow and exact: is the version KDOS pins older than the version Alpine records as fixing a CVE. That is a version comparison with libkpkg's `kp_vercmp`, the same comparator `kdos-portup`, the port-version checker, uses, not a scan of binaries. NVD was rejected because its data feeds are retired in favour of an API, which needs a network; OSV because it carries the same facts in a far larger download. Alpine is the closest distribution to KDOS that publishes machine-readable security data: musl, the same upstream tarballs, comparable version pins. The limit is stated with every run. The database is keyed by Alpine package names, so a port named differently needs a `secdb = ` line in its recipe, and a CVE fixed in a version Alpine never shipped is invisible. A package with no entry is reported unknown, never clean. The data ages, so `kdos cve` warns once it is more than 180 days old, and `ports/update --cve` is the separate online cross-check. ## The build shell lives beside the recipe A port is two files. `kpkgbuild` is declarative metadata that is parsed and never sourced; `build.sh` beside it is ordinary bash. `bash -n`, shellcheck, syntax highlighting and `git diff` all work on it, and no parser has to understand shell. An argument-vector list with no shell at all would fit most ports, but a minority need heredocs, loops, redirects, globs and command substitution, and a format that cannot express those cannot build them. Embedding a shell interpreter in `kpkg`, the host package manager, was rejected because there is no embeddable evaluator: the candidates are either parsers that do not execute, or programs with their own `main()`, which would mean vendoring tens of thousands of lines of third-party C into a tree that vendors almost none. It would gain nothing either, since bash is in the sysroot (the tree the host is compiled into) before `kpkg` is compiled and ships on the target regardless. A single file with the shell carried inline under INI-style section headers was rejected on a concrete case. The `rust` port's `build.sh` writes a `config.toml` through a heredoc whose body contains a line reading exactly `[build]`, the start of a TOML section. A recipe format with `[build]` as one of its own section headers would read that line as the start of its own section. See [Writing ports](/docs/kdos/05-developer/writing-ports.html) for the format as built. ## Ports are shelved by subject; identity is the bare name The upstream ports, about 2,000, are filed on 102 **shelves**, one directory per subject: `ports/core///`, such as `ports/core/wl/wlroots/` or `ports/core/games-board/kpat/`. The shelf list is closed. It is the file `ports/shelves`, one line per shelf giving its id, the source-archive volume that keeps its files, and what belongs on it, and a port may sit only on a shelf that file lists. A port's identity is its bare name: the shelf is where the recipe is filed and nothing more. The question was how a person finds, reviews and places a port among two thousand. A single flat directory answers none of those: `ls` shows two thousand names in alphabetical order, and nothing says that `kpat` and `gnome-mines` are the same kind of thing or that a new card game belongs beside them. Subject directories answer all three, in the way T2 SDE's `package///` does. Several details follow from what the rest of the system already assumes: - **The shelves sit under `ports/core`, not directly under `ports/`.** `ports/` also holds the fetch, publish and update tools, the source index and the caches. `ports/core` is the repository root: every `PORT_REPO` value and the `/etc/kpkg.conf` default name it, as `/ports/core` inside the chroot's bind of `ports/`, and never a shelf inside it. - **The shelf comes from the path alone. There is no `shelf =` recipe key.** A recipe's bytes are its [recipe hash](/docs/kdos/03-architecture/packaging.html#e--the-recipe-hash), so a new key in every recipe would change every hash and, under strict recipe matching, rebuild the whole tree. A key would also be a second statement of the shelf that could disagree with the first. - **Everything names a port by its bare name.** `depends =` lines, the package lists, the package database, the binary host's index, delta names and `ports/sources.idx` all say `wlroots`, never `wl/wlroots`, so moving a port to another shelf is a `git mv` plus moving its name to that shelf's file in its phase's package list, and changes no hash and no package. The same holds for prose: a comment or a page that means a port names the port, not its shelved path, since a path goes stale the day the port moves; a path appears only to show the layout itself. - **A name is unique across the whole tree, and a duplicate is an error, not a choice.** `kpkg` looks a name up flat and then on every shelf of a repository, and a second match inside one repository stops it with both paths named, as T2's tools abort on a name found in two trees. Picking the first would build whichever copy sorts earlier, with nothing saying so. The pre-push hook and preflight refuse a duplicate across `ports/core` and the `src/` areas before it is pushed. - **`kpkg` descends into the shelves itself; they are never listed on `PORT_REPO`.** That list holds at most eight repositories, and a hundred shelves would not fit on it. - **A shelf's name is constrained.** It is lowercase letters, digits and `-`. It is never `libs`: a source-less port hashes `../../libs` from its directory, which for a core port is `ports/core/libs`, and a shelf by that name would be hashed whole into `containers-common` and `musl-ldd`. It is never `core`, and never the name of a port, because a shelf named `perl` or `llvm` could not be told from a port of that name at the top of `ports/core`. - **A `group =` family shares a shelf,** so the version checker's joint bump touches one directory. - **KDOS's own ports are not shelved.** They keep their own areas under `src/`, for the reason in the next entry. The shelves are chosen by rules applied in order, the first that matches deciding: a `group =` family stays together, a name-prefix family (`python3-*`, `qt6-*`, `libretro-*`, `font-*`) decides, KDE Frameworks go to `kf6`, data and plug-ins follow the program they exist for, an application is filed by what a person does with it and never by its toolkit or desktop project, and a library goes to its domain shelf when it has one. [Writing ports](/docs/kdos/05-developer/writing-ports.html) gives them in full. A single recipe key naming a category, read by the tools, was rejected for the hash reason above. Listing each shelf on `PORT_REPO` as a repository of its own was rejected because of its eight-entry limit, and because the first-wins rule between repositories would turn a duplicate into a silent choice. Filing applications under their toolkit or desktop project (a `kde-apps` or `gnome-apps` shelf) was rejected because a person looks for a file manager or a card game, not for the library it links. The package lists are grouped by the same shelves, so there is one classification of the ports rather than two that drift apart. See [Packaging](/docs/kdos/03-architecture/packaging.html#phases-package-lists-and-shelves). The cost is judgement. Some ports fit two shelves, and a rule has to pick one; where the rules do not settle it, the shelf of the port's only main consumer wins, or failing that the shelf a person would open first. A name also has to be unique across all two thousand ports, which one flat directory would enforce by construction and which, across shelves, the tools check. And every tool that walks the tree descends exactly one level: a walker that did not would see no ports at all and report success, which is why `kpkg` warns about a repository that holds no port at either depth. ## KDOS's own code is divided by what each program is KDOS's own software lives in six areas under `src/`, each one kind of thing: | Area | Holds | Recipes | |---|---|---| | `src/desktop` | Programs that draw the session or serve it over Wayland or D-Bus | 8 | | `src/daemons` | Root daemons whose client is the desktop account | 5 | | `src/system` | The package manager, the `kdos` command and its services, packs and boxes, and the installer | 5, and `kdos-kpkg`, built by script | | `src/art` | Themes, pictures and the programs that generate them | 6 | | `src/libs` | The 17 `libk*` libraries | none | | `src/devtools` | Programs that run on the build machine and are never installed: `kdosbuild` and `kdos-portup` | none | A new program goes to the first area that fits: a library to `src/libs`, a program not installed on the target to `src/devtools`, one that draws or speaks the session's protocols to `src/desktop`, a root daemon for the desktop account to `src/daemons`, a picture, theme or generator to `src/art`, and anything else to `src/system`. Every port sits exactly two levels below `src/`, at `src///`. That depth is not a style: seventeen recipes find the libraries at `$PORT_SRC/../../libs`, the recipe hash of every source-less port walks the same path, and `kdos-installer` compiles a file from its sibling `kdos-appbox`. A third level would break all three. The areas also carry the build's phase split. `40_lang` to `44_apps` and `60_kernel` search `src/system` and `src/art`, and only `50_desktop` adds `src/desktop` and `src/daemons`, so no userland port can come to depend on the compositor or a desktop daemon: the phase building it could not resolve the name. That makes five repositories in the desktop phase, under the eight `PORT_REPO` holds. Flattening every program into `src//` was rejected: `../../libs` would stop resolving, the libraries would sit among the programs, and the phase split would have nothing to hang on. Grouping the libraries by kind (drawing apart from system) was rejected because their `libk` prefix already namespaces them, and moving them would change every `-I` flag in seventeen build scripts and every path the recipe hash walks for no gain. Shelving KDOS's own ports under `ports/core` with the upstream ones was rejected for the depth reason: a shelved port's `../../libs` is inside `ports/core`. ## Build phases are named, banded and closed The build's phases live in `script/phases/`, one directory each, named for what they build and numbered in bands of ten: `00_cross`, `10_bootstrap`, `20_selfhost`, `30_foundation`, `31_compilers`, `40_lang`, `41_system`, `42_graphics`, `43_toolkits`, `44_apps`, `50_desktop`, `60_kernel` and `70_image`. Each directory holds its steps or its package list and its own environment, `phase.env`. Steps inside a phase are numbered with gaps too, such as `10_bootstrap/060_gzip.sh`, `061_tar.sh` and `062_coreutils.sh`, so their order is the number and never an accident of the alphabet. The numbering with gaps means a phase can be added between two others without renaming either, and so without renaming every snapshot, log directory and page that names the later one. The names say what a phase builds, so `--phases 43_toolkits` reads as what it does. **The userland is split by the dependency graph, not by subject.** Each phase's boundary is a property of every port's dependency closure: `31_compilers` holds the large compilers (LLVM and Clang with their runtimes, Rust, Go, GHC, Zig, Node.js and Ruby) and a handful of tools built with them, and every later phase builds on them; `42_graphics` the ports that reach the graphics stack but no toolkit; `43_toolkits` those that reach a toolkit and that something depends on; `44_apps` the leaf applications over them. Every port's dependencies sit in its own phase or an earlier one. The split puts a restore point after `30_foundation`, about the first hour of native building, in front of the compilers that take most of the hours before `40_lang`, and another after each layer of the userland. `41_system`, 967 ports, is one phase because the graph does not support a finer cut. Any split of it by subject pushes a hundred or more ports past their subject's phase, because a system library filed on a hardware shelf sits under `openssh`, `gnutls` and `cups`; a split by dependency depth is valid but puts `zsh` beside `libpng`. Its `packages.d/` gives it its grouping instead. **From `30_foundation` on, a phase's list names exactly the ports the phase installs.** A list that names only what it wants, and lets `depends =` pull in the rest, lets a port move silently into an earlier phase when something there starts depending on it, and lets a list reach forward into a later phase's ports. `testing/phaseclosure.py`, run by preflight, requires each list's closure, less what earlier phases installed, to equal the list, and names each port that breaks it with both phases. **A large list is a directory of shelf files.** The five userland phases, `40_lang` to `44_apps`, each read `packages.d/*.txt` in byte order as one list: one `.txt` per shelf the phase draws on, one `src-.txt` for the ports it builds from an area under `src/`, and, where a phase has a run whose order a comment pins (`41_system`), an `00-order.txt` that sorts first and holds it. A phase with one short list keeps a single `packages.txt`: its pinned run first and, where the list holds more, the rest grouped by shelf under comment banners. The lists of `20_selfhost` and `60_kernel` are a pinned run alone. **Shared settings are written once.** `script/env/common.env` holds what every phase sets (the reproducibility variables, `MAKEFLAGS`, strict recipe matching), `script/env/host.env` what the two host phases share, and `script/env/chroot.env` what every chroot phase shares, such as the pinned compiler. A `phase.env` sources one of them and sets only what differs. The keys the orchestrator reads, the title, the description, the snapshot paths and exclusions, and `CHROOT`, stay in the `phase.env` text itself, because the orchestrator reads that file's text and follows no `source`: set in a shared file instead, `CHROOT=1` would run every chroot phase on the host. `PORT_REPO` stays there too, because `testing/phaseclosure.py` reads it from that text in the same way; a phase that sets none searches kpkg's default, `/ports/core`. The pre-push hook sits in `script/hooks/` and nowhere else, because every clone has `core.hooksPath` pointing at that directory, and a hook at any other path would silently not run on any of them. The cost is more phases and more snapshots. Every phase up to `50_desktop` saves a compressed layer of what it changed in the tree when it finishes, on top of the whole tree the first phase saved, so each restore point is paid for in disk; a phase opts out by leaving `KDOS_SNAPSHOT_PATHS` empty, as `60_kernel` and `70_image` do. A list that names every port it installs is also longer than one that names only what it wants, and a new dependency that crosses a phase boundary fails preflight until a list moves. Some real edges in the graph become visible this way: `podman`, `distrobox`, `qemu`, `libvirt` and `sane-backends` build in `43_toolkits`, because `gpgme` reaches Qt through `gnupg` and `pinentry`, and `kdos-tools` builds in `42_graphics`, because `kbd` reaches Wayland through `libxkbcommon`. See [The build system](/docs/kdos/05-developer/build-system.html#phases). ## Signing the index, not every package A [binhost](/docs/kdos/06-reference/glossary.html) (a directory of prebuilt host packages with a signed index) is protected by one Ed25519 signature over its index, `PACKAGES.sig` beside `PACKAGES`. The index carries every package's SHA-256, so one signature covers all of them transitively. A per-package `.sig` sidecar exists for the separate case of a package travelling on a USB stick with no index beside it. Signing every artefact as the primary mechanism was rejected because it multiplies the work and the number of things that can be individually wrong without improving what is proven. A signature file holds any number of signatures, one per line, and verifies when at least one line verifies against a key in the local keyring. During a key rollover both keys sign and a client trusting either keeps working. Adding that to a format that holds only one signature would be a format change every client has to understand. The key id inside a signature is a label, not a selector: every line is tried against every key in the local keyring and against nothing outside it, so the id can never supply a key. See [Packaging](/docs/kdos/03-architecture/packaging.html). ## Updates build a signed tree from source Every image carries its recipe-only ports tree at `/ports`. `kdos update fetch` replaces it with the tree of the latest release, `kdos-ports.tar.xz`, once its Ed25519 signature verifies against `/etc/kdos/keys/tree`, which holds the public half of the release key the image was built with. `kdos update apply` builds every package that tree pins newer, on the machine, fetching each source from the `sources-` archive and then upstream and checking it against the recipe's `sha256 =`. No binhost takes part, and `kdos update` has no option or setting that names one. A binhost was rejected for updates because it answers a question a recipe tree already answers. A prebuilt package is usable only when its recipe hash and build-config hash equal this machine's, so it saves compiling and says nothing about what is newer; it also adds a second thing to publish, sign and keep in step with every tree. One signed tree is the whole trust root: every source an update builds is checked against a hash the tree names. The cost is in time and in the network. An update needs the network for the tree and for every source, and compiles on the machine, which is hours for a large set: a new compiler, a browser or Qt. A machine offline cannot update, and says so in the panel's badge rather than looking current. See [Packaging](/docs/kdos/03-architecture/packaging.html#updating-a-machine). ## Freezing a demo rather than writing one The ASCII-art demo, `kdos-bb`, is a frozen hard fork of the AA-project's `bb` 1.3rc1, recorded in `src/art/kdos-bb/KDOS-FORK`. Of the 81 files upstream ships, it keeps the sources and headers the binary is built from, the three `.s3m` tracker modules, and the authors' own credits scroll; the autotools apparatus is replaced by a static `aconfig.h` and a `build.sh` that calls the compiler. The fork's `src/` holds 44 C sources, one of them `kdostux.c`, an image generated by `genimg.py`, and 16 headers, one of them `aconfig.h`. `KDOS-FORK` lists every change the fork makes, and they fall into two groups. Some changes are needed for the demo to run here. `clear_zbuff()` clears exactly the `sizeof(int)` per cell it allocates; clearing `sizeof(long)`, double that on x86-64, corrupts the heap. The message scroll moves overlapping ranges with `memmove`, because `memcpy` on them is undefined and musl does not tolerate it. `REGISTERS(n)` expands to nothing, because the x86-32-only `regparm` attribute warns on every declaration here. The drawing is paced on a deadline grid of one frame per 16 ms, the mixer runs on its own thread so the music does not starve while a frame is drawn, and the scene clock follows the music player so the two stay in step. Each frame is wrapped in synchronized output (DECSET 2026), so a terminal that supports it, `kdos-term` included, draws whole frames rather than halves of two. The rest change what the demo shows and how it starts. It starts by itself, with `-nosound` and `-mixer` as flags in place of upstream's two start-up questions; the flash words, the closing text and two logo beats carry KDOS's mark; and the closing text turns its own pages in time with the last module. Every contributor line and every Special Thanks in the credits is upstream's, unchanged. A demo written from scratch was the alternative. `bb` is a set of scenes paced against the three tracker modules it ships with, and reaching that from nothing would be a project of its own, where the fork needed only the changes listed above. See [kdos-bb](/docs/kdos/04-programs/kdos-bb.html). ## Forking libtsm rather than writing a terminal `libkvt` is a hard fork of libtsm 4.7.1, kmscon's VT100–VT520 state machine, with its `tsm_` and `shl_` prefixes renamed to `kvt_` and `kvt_shl_`. A terminal emulator is a decade of edge cases — character sets, the alternate screen, DEC private modes, wrapping rules that differ between terminals that both claim VT100 — and none of it is a place to be original. What is original here is the boundary to the toolkit, not the parser. Like the compositor, the fork is pinned and does not rebase onto upstream. A *cell* is the record for one character position on the screen: the character, its colours and its attributes. `libkvt` keeps libtsm's own cell record internally and produces the toolkit's shared cell type — `KtuiCell`, defined in `libktui`'s `ktui.h` — only when the screen is drawn. `libkcell` paints `KtuiCell`s directly rather than defining a cell type of its own, but that is about the toolkit's libraries agreeing with each other, not about a terminal's private screen buffer, which nothing outside the library sees. Upstream's cell is kept because it carries 24-bit colour, a per-cell age that drives damage tracking, and a symbol-table handle that makes combining characters possible. The conversion to a `KtuiCell` happens at one render boundary, once per frame, over the runs the screen reports as changed, so none of that is lost before the screen is drawn. The library reaches the toolkit only through narrow calls: key codes, character width, mouse selection, and libkbase's base64 decoder for OSC 52 clipboard writes. libtsm borrows an LGPL hash table from CCAN, and the fork replaces it with one written here, because `libkvt` is MIT and a library that put relinking obligations on every binary linking it could not be carried. The terminal's colours reduce onto the palette's [slots](/docs/kdos/06-reference/glossary.html) (named colour roles) by the same rule as every other surface, so `kdos theme` recolours a terminal too; the rule is described with the library in [The C libraries](/docs/kdos/05-developer/c-libraries.html#libkvt). The cost is the compositor's: upstream fixes do not arrive, and are read and applied by hand. See [The C libraries](/docs/kdos/05-developer/c-libraries.html#libkvt) for the library as built. ## One file chooser at one width The chooser a boxed application reaches through the FileChooser portal and the chooser this desktop's own programs open are one program, `kdos-pick`, at one size: 64 columns by 22 rows. The portal does not get a wider one to hold a sidebar column. The width is not a free parameter. Laid out inside a classic 80-column by 24-row terminal, a 64-column dialog leaves eight cells of ground either side of it and two rows below it for the taskbar. A sidebar wide enough to read a place name is about sixteen more columns, and a chooser needing 80 columns would be a chooser with no frame, no ground and no room for the taskbar at that size. The chooser's [goldens](/docs/kdos/06-reference/glossary.html) (committed reference frames of a surface, compared byte for byte by the tests) are also taken at 56 by 24, where the dialog shrinks to the screen. A third column is what a sidebar would cost. When the selected row is an image, the chooser already carves a preview pane from the right-hand third of the list, so a sidebar would take its width from the names: about thirty cells for a filename, in the window whose purpose is showing filenames. So the places are an overlay and not a column. `Ctrl+P` opens them over the file list, and `Esc` closes them. The list starts with the places `kxdg_places()` gives the Start menu and adds the recently visited directories that `zoxide` reports, every place at full width, and nothing is taken from the names while it is closed. A boxed application's Open and Save get exactly what a native one gets, which is the other half of the decision: two dialogs of two widths would be two layouts to maintain, two sets of reference frames, and two answers to how wide a chooser is. ## Narrowings A narrowing is a decision that reads like a gap. The thing is not there, and it is not there because a smaller answer was chosen over a larger one. Each of these is too small for a full entry of its own, and each has the same shape: what was asked for, what is built instead, and what the larger answer would have cost. ### Drops reach only the trash A drop on the desktop reaches the trash and nothing else. Dropping a file onto a folder icon would be a move, and a move across filesystems is a copy and an unlink that can half-succeed. A desktop offering the gesture has to say what it did with the file when the second half failed, on a surface with nowhere to say it. The trash is the one target whose failure mode is that nothing happened. ### Screens are ordered, not placed Screens are placed edge to edge in list order, not at coordinates. `kdos-display` lays the outputs out from `x = 0` with their tops aligned, in the order of its list, and `[` and `]` move a screen along it. A vertical arrangement, an overlap and a deliberate gap cannot be expressed. What people reach a screen tool for is which screen is left of which, and an order answers that in a list; a geometry answers it with a canvas, a drag, a snapping rule and a validity check for the arrangements that leave a hole. ### Tabs stack; they do not tile The compositor's `AddToTabGroup` action folds the focused window onto the one behind it, and there are no tile groups — two windows side by side that move, resize and minimise together. A window's `tiled` state is a set of edges resolved against the work area, never against a neighbour, and `libkwm`, which computes window rectangles, has no notion of a neighbour at all. A group is a relation between windows, which is the one thing the window model does not hold, so a tile group would reuse none of the machinery a tab stack reuses. See [The window model](/docs/kdos/03-architecture/window-model.html). ### One font for every output The desktop uses one font for every output. `~/.config/kdos/comp.conf` holds `chrome_font` and `panel_font`, and each is one value for every output, so they are right on a machine with one screen and wrong on two of different densities. `kdos-style`'s font page offers fontconfig's monospace families and writes those two keys, which the desktop reads at the next login; setting the face on every output keeps two screens agreeing rather than letting each be right. A per-output font is a different design — a font per connector, a picker that asks which, and a cell size that changes under a window as it is dragged across the boundary — not a missing call. ### No ReGIS or Tektronix The terminal implements neither ReGIS nor Tektronix graphics. They are vector graphics protocols from DEC and Tektronix hardware, and nothing in the catalogue emits either. The three raster protocols the terminal implements (sixel, the kitty graphics protocol and iTerm2's inline images) are what current programs use. See [kdos-term](/docs/kdos/04-programs/kdos-term.html). ### Braille and speech do not read the desktop Braille and speech are ports, and neither reads the desktop. `brltty` reads `/dev/vcsa` by default, so it covers `tty1` and the installer; its other screen drivers read a `tmux` session or a terminal run under `brltty-pty`, and none of them reads anything the compositor draws. It carries a driver for nearly every braille display, loaded by name from `/etc/brltty.conf`. `speech-dispatcher` is the interface a screen reader speaks to and `espeak-ng` is the voice behind it. No KDOS program links BrlAPI; the API server stays on for BRLTTY's own clients, such as `brltty-clip`. No KDOS surface talks to `brltty`, `speech-dispatcher` or `espeak-ng`. They are on the image because they are useful to somebody at a terminal, not because the desktop is readable; see [Accessibility](/docs/kdos/02-user-guide/accessibility.html). ### A tray menu is read once A tray icon's menu is read when it opens and not watched while it is open. `kdos-shell`'s tray menu listens to neither `LayoutUpdated` nor `ItemsPropertiesUpdated`, the two signals an application sends when its menu changes (`src/desktop/kdos-shell/traymenu.c`). A menu is open for as long as somebody is looking at it, and a menu whose rows move under the pointer activates the wrong row. `AboutToShow` is sent first, which is where an application fills a submenu in; the layout is read once its reply arrives, and that read is the only one taken. Following the signals would mean a policy for every row that moves, appears or disappears under a pointer that is already on its way to it. See [kdos-shell](/docs/kdos/04-programs/kdos-shell.html). ### Invitations are read, not answered An invitation in a message is read and never answered. The shipped `aerc` configuration sends `text/calendar` parts through `kdos-part ics`, which runs aerc's own calendar filter: it prints the event — summary, times, location, who was asked — and writes nothing anywhere. A filter runs every time a message scrolls past, so one that imported would accept every meeting it was scrolled over, and no `text/calendar` handler is registered for the same reason. Filing an invitation is manual and works: `:save` the part out of the message and `khal import` it, and the day carries a mark in the panel's calendar. ### No raw-block-device applications Applications that need raw block devices are not in the catalogue: partitioners, drive-health tools, recovery tools. A rootless container cannot do anything useful with a block device, and a launcher that opens onto a permission error teaches somebody that the machine is broken. Those jobs are native tools on the host, where the privilege is: `parted`, `gptfdisk`, `smartmontools`, `testdisk` and `ddrescue` are ports. ### No compositor-private protocols Applications requiring a particular compositor's private protocols are out. Spectacle, KDE's screenshot tool, asks for KWin's own screenshot interface on Wayland and opens an error dialog on any other compositor, so it is not in the catalogue. Screenshots are the host's `kdos-shot`, which runs where the capture protocols are. ### Nothing downloads at run time Nothing downloads a component at run time. `winetricks` is not in the catalogue, because it fetches Windows runtimes from the network when it runs, and for the same reason Wine gets no Microsoft core fonts: a Windows program that wants a specific proprietary font gets a substitute. Once installed, nothing may depend on a download an offline machine cannot make. ### A library edit rebuilds every in-tree port Editing a library rebuilds every port of KDOS's own software that builds from the tree, not only its consumers. A port under `src/` with no `source =` line builds from the tree, and seventeen of the 24 ports there compile `libk*` libraries into themselves, each `build.sh` naming the ones it uses as a glob under `$LIBS`. Working out which ports an edit reaches would need a shell parser inside the package manager, so `kp_hash.c` puts the whole of `src/libs` into the recipe hash of every such port instead. The test is the recipe and where it sits: the hash walks `src/libs` for every port with no source (no `source =` line, or an empty one) whose directory has a `../../libs` beside it, which is every port under `src/`, since each sits at `src///`. The two source-less ports under `ports/core`, `musl-ldd` and `containers-common`, sit on a shelf, where `../../libs` is `ports/core/libs`, a name no shelf may take, and hash their own directory alone, and every other port under `ports/core` is unaffected. Each rebuild takes seconds, while a parser would be a second build system whose mistakes show only as missed rebuilds. ### util-linux switch_root in the initramfs The initramfs carries util-linux's `switch_root`, which moves the new root onto the root of the mount namespace before it `chroot()`s. A `switch_root` that only `chroot()`s leaves every process on the booted system chrooted. The kernel refuses a chrooted caller a new user namespace, so every rootless container would be refused, and the machine boots normally either way. The initramfs step refuses to build an initramfs whose `switch_root`, `mount`, `umount`, `losetup`, `dmesg` or `blkid` does not name util-linux in its `--version`. See [Boot and init](/docs/kdos/03-architecture/boot-and-init.html#switch_root). ## See also - [Why KDOS](/docs/kdos/01-philosophy/why-kdos.html) — the properties these decisions serve - [How KDOS differs](/docs/kdos/01-philosophy/how-kdos-differs.html) — how other distributions answer the same questions - [Principles](/docs/kdos/01-philosophy/principles.html) — the rules that follow from them - [Packs and boxes](/docs/kdos/03-architecture/packs-and-boxes.html) — the pack and store decisions as built - [kdos-comp](/docs/kdos/04-programs/kdos-comp.html) — the compositor fork as built - [Writing ports](/docs/kdos/05-developer/writing-ports.html) — the recipe format and publishing sources - [How KDOS is built](/docs/kdos/05-developer/how-kdos-is-built.html) — the build from a clone to an ISO, where the source archive and the recipe hash come into play - [The ports catalogue](/docs/kdos/06-reference/ports-catalogue.html) — every port the build and security decisions apply to, by shelf - [Known gaps](/docs/kdos/06-reference/known-gaps.html) — what these decisions leave undone - [Glossary](/docs/kdos/06-reference/glossary.html) — the terms these entries use --- *Part I — Introduction, chapter 4.* Previous: [3. Principles](/docs/kdos/01-philosophy/principles.html) · [Contents](/docs/kdos/) · Next: [5. Getting started](/docs/kdos/02-user-guide/getting-started.html)