Build the COSMIC desktop environment
as .deb packages on Debian testing, and install them locally.
Everything is built inside an OCI container (podman or docker), so the build
host only needs git and a container engine; the produced .deb files are
extracted to the project directory and installed with apt.
Licensed under the MIT license.
Host:
- Debian testing (the install scripts use apt/dpkg)
- git
- podman or docker (podman is preferred if both are present;
override with e.g.
CONTAINER_ENGINE=docker)
Privileges:
build.shandclear.shrun as a regular userinstall.shanduninstall.shrequire root (run via sudo)
sudo apt-get update && sudo apt-get install -y git podman # or: docker.io
git clone https://github.com/nimenko/cosmic-debian-testing.git
cd cosmic-debian-testing
./build.sh # clones/updates sources, builds all .deb packages
sudo ./install.sh # installs them
sudo systemctl restart gdm # or sddm/lightdm, or rebootLog out and choose the COSMIC session at the greeter.
install.sh needs the .deb files from a successful build, so run it after
build.sh and before clear.sh. It also holds the installed packages with
apt-mark, so apt upgrade will not replace them with other versions (their
dependencies are not held). Uninstalling, or reinstalling, clears the holds.
build.sh builds the current upstream master of every source repository.
Nothing is pinned (see Caveats). A rebuild reuses the engine's
layer and cargo caches.
sudo ./uninstall.sh # unhold + purge, then apt-get autoremove --purge
./clear.sh # .deb files, cloned sources, generated ContainerfileRun uninstall first. It purges the packages listed in .installed_packages, or
the .deb files if that log is gone. clear.sh deletes those .deb files.
Both scripts leave container data behind, and that is the bulky part:
podman rmi cosmic-epoch cosmic-deps && podman image prune -f --build-cache
docker rmi cosmic-epoch cosmic-deps && docker builder prune -fThe prune commands affect all local images and build cache, not just this
project. --build-cache needs podman 5. Look before you prune with
podman system df.
Per-user state also survives a purge. To remove it, delete
~/.config/cosmic*, ~/.cache/cosmic*, ~/.local/state/cosmic* and the
matching pop-launcher paths. Check the result with
dpkg-query -W 'cosmic-*' 'pop-launcher*' and apt-mark showhold.
The build is a two-stage OCI build ("Containerfile") per component group,
ending in a FROM scratch stage that contains only the .deb files, which
are extracted with --output type=local. Package sources are cloned into
the build context with git (depth 1) on every build.
cosmic/Containerfile is generated by cosmic/gen-containerfile.sh on
every build and must not be edited by hand. The generator:
- takes the module list from
cosmic-epoch/.gitmodules, keeping every module that has Debian packaging (so new upstream modules are picked up automatically; modules without packaging, e.g.cosmic-sound-theme, are skipped with a notice, as issimple-wrapper, which upstream's own build does not install either), - derives the build order from the
Build-Dependsfields of the modules'debian/controlfiles: if one module build-depends on a package that another module in the set provides (currently onlycosmic-osdoncosmic-randr), the providing module is built first and its freshly built.debis installed into the build container right after its build, - emits one
COPY+RUNblock per module so each package keeps its own layer and the build cache stays fine-grained.
Rust modules are built without vendoring: the container sets VENDOR=0 and
CLEAN=0, an empty vendor.tar is created to satisfy the upstream
packaging's tar pxf vendor.tar step, and cargo fetch fills a persistent
cache mount so the --offline build works. A small
/usr/local/bin/ischroot shim (always "yes, this is a chroot") is installed
because some modules' rules use the old-style ischroot || (clean && vendor)
guard, which buildah containers misdetect otherwise — their clean step would
then try to delete the shared, cache-mounted target directory and fail.
Modules that generate desktop files via a just xdgen recipe instead of a
build script get that recipe run explicitly, since upstream only runs it as
a side effect of the skipped vendoring step. Cargo's registry, git and
target directory caches are BuildKit cache mounts, shared across all
modules, so a rebuild only recompiles what changed.
Install/uninstall keep a log (.installed_packages) of what was installed.
On uninstall it falls back to the .deb files still present in the
directory, and only as a last resort to a dpkg query (which could, in
principle, also match future distro-provided packages with those names).
- Nothing is pinned: sources are always upstream master, and the build
base image is the current
debian:testing. Two builds at different times produce different package sets. Debian testing also moves, so a latersudo apt upgradecan pull in incompatible library versions (the packages themselves are held, their dependencies are not). - The built binaries report a fake git SHA and commit date (the source
.gitdirectories are excluded from the build context). - The Containerfiles build unsigned packages (
-uc -us) and run without build-dep verification (-d); this is a local build tool, not a distribution pipeline.
MIT © 2026 Maksym Nimenko