diff --git a/.dockerignore b/.dockerignore index 2a1a7ec..a1d76ef 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,10 +1,2 @@ -.git/ -cache/ -out/ -aports/ -extra/ -screenshots/ -Dockerfile -.dockerignore -docker-env.sh -profile/repo/ +# The checkout is mounted at run time, never copied: send no build context. +* diff --git a/.github/actions/container.yml b/.github/actions/container.yml deleted file mode 100644 index 3917e98..0000000 --- a/.github/actions/container.yml +++ /dev/null @@ -1,5 +0,0 @@ -name: 'Container' -description: 'Runs the Dockerfile for Sigma Linux' -runs: - using: 'docker' - image: 'Dockerfile' diff --git a/.github/boot-test.sh b/.github/boot-test.sh new file mode 100644 index 0000000..12616eb --- /dev/null +++ b/.github/boot-test.sh @@ -0,0 +1,62 @@ +#!/bin/sh + +# Boots the freshly built ISO in QEMU and waits until OpenRC reaches the last +# service of the default runlevel ("Starting local"): the kernel, initrdbo's +# live boot from the squashfs and the whole init sequence all worked. +# +# The ISO's own menu has no serial console, so its kernel and initramfs are +# booted directly, with the ISO's command line plus console=ttyS0 and the ISO +# attached as a CD-ROM, which is where initrdbo finds the squashfs. +# +# usage: sh .github/boot-test.sh (from the checkout, after `./sigma build`) + +set -eu + +iso=out/sigma-linux.iso +boot=cache/boot +menu=cache/iso/boot/grub/grub.cfg +log=cache/logs/boot-test.log +marker="Starting local" +limit=${BOOT_TIMEOUT:-600} + +for file in "$iso" "$boot/vmlinuz-sigma" "$boot/initramfs-sigma" "$menu"; do + [ -r "$file" ] || { echo "boot-test: $file is missing; build the ISO first" >&2; exit 1; } +done + +# The first entry's parameters, exactly as the ISO boots them. +cmdline="$(sed -n 's|^[[:space:]]*linux /boot/vmlinuz-sigma ||p' "$menu" | head -n 1)" +[ -n "$cmdline" ] || { echo "boot-test: no vmlinuz-sigma entry in $menu" >&2; exit 1; } + +accel="" +[ -w /dev/kvm ] && accel="-enable-kvm -cpu host" + +mkdir -p "${log%/*}" +: > "$log" +# shellcheck disable=SC2086 # $accel is a list of options +qemu-system-x86_64 $accel -m 4096 -smp 2 -display none -no-reboot \ + -cdrom "$iso" -kernel "$boot/vmlinuz-sigma" -initrd "$boot/initramfs-sigma" \ + -append "$cmdline console=ttyS0" -serial "file:$log" & +qemu=$! + +fail() { + kill "$qemu" 2>/dev/null || true + echo "boot-test: $*" >&2 + echo "--- last lines of the serial console ($log):" >&2 + tail -n 60 "$log" >&2 + exit 1 +} + +echo "Booting ${iso} ($([ -n "$accel" ] && echo KVM || echo 'no KVM, slow')); waiting up to ${limit}s for \"$marker\"" +waited=0 +until grep -q "$marker" "$log"; do + grep -q "Kernel panic" "$log" && fail "kernel panic" + kill -0 "$qemu" 2>/dev/null || fail "QEMU exited before the system finished booting" + [ "$waited" -lt "$limit" ] || fail "no \"$marker\" after ${limit}s" + sleep 5 + waited=$((waited + 5)) +done + +kill "$qemu" 2>/dev/null || true +echo "Booted in about ${waited}s:" +grep -m 1 "Linux version" "$log" || true +grep -m 1 "OpenRC" "$log" || true diff --git a/.github/workflows/build-iso.yml b/.github/workflows/build-iso.yml index 9e80fa6..333edba 100644 --- a/.github/workflows/build-iso.yml +++ b/.github/workflows/build-iso.yml @@ -1,19 +1,155 @@ -name: Build ISO +name: CI -on: [workflow_dispatch] +# Every push and pull request runs the tests. The full ISO build (hours: the +# kernel alone is most of it) runs for master, for version tags, and on +# demand: Actions → CI → Run workflow, or `gh workflow run build-iso.yml --ref `. +# A version tag (git tag v1.2 && git push --tags) also publishes the ISO as a +# GitHub release, once it has booted in the boot test. +on: + push: + branches: ["**"] + tags: ["v*"] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +# A newer push to the same branch supersedes a build in progress. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} jobs: - container-job: - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v4 - - name: Container Build Step - uses: ./ - id: container - - name: Upload Build Artifacts - uses: actions/upload-artifact@v4 - with: - name: sigma-linux - path: ${{ github.workspace }}/cache/sigma-linux.iso - compression-level: 0 + test: + name: Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + # In Alpine, like the build: the APKBUILD reader runs them through its sh. + - name: Unit tests and build plan + run: > + docker run --rm -v "$PWD:/app" -w /app alpine:edge + sh -c 'apk add --no-cache ruby ruby-minitest >/dev/null && ruby test/run.rb && ./sigma build --plan' + + iso: + name: ISO + needs: test + if: > + github.event_name == 'workflow_dispatch' || + github.ref == 'refs/heads/master' || + startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + timeout-minutes: 355 # the hosted runner's hard limit is 360 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + # The kernel's build tree peaks at ~20 GB and the image adds ~10 GB; the + # runner starts with less free than that. + - name: Free disk space + run: | + df -h / + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup \ + /opt/hostedtoolcache/CodeQL /usr/local/share/boost /usr/share/swift \ + /usr/local/share/powershell /usr/local/share/chromium /usr/lib/jvm + sudo docker image prune --all --force > /dev/null + df -h / + + - name: Ruby + run: command -v ruby || { sudo apt-get update -q && sudo apt-get install -y -q ruby; } + + # Compiled objects from earlier runs, the kernel's especially. Saved + # under a new key every run; the restore takes the most recent one. + - name: Restore ccache + uses: actions/cache/restore@v6 + with: + path: cache/ccache + key: ccache-${{ github.run_id }}-${{ github.run_attempt }} + restore-keys: ccache- + + # Same command as on a workstation: sigma builds its container image and + # runs the pipeline in it. All cores are ours here, and the ccache cap + # keeps the saved cache within the repository's 10 GB of Actions cache. + # The signing key is made fresh for this run and never leaves it. + - name: Build + id: build + timeout-minutes: 320 + run: ./sigma build --set jobs=$(nproc) --set packages.ccache_size=5G + + # On a timeout the step's process dies but its container may not. + - name: Stop the build container + if: always() + run: docker ps -q | xargs -r docker kill + + # Also after a failure or a timeout: the next run picks up where this one stopped. + - name: Save ccache + if: always() && steps.build.outcome != 'skipped' + uses: actions/cache/save@v6 + with: + path: cache/ccache + key: ccache-${{ github.run_id }}-${{ github.run_attempt }} + + - name: Checksum + run: cd out && sha256sum sigma-linux.iso | tee sigma-linux.iso.sha256 + + - name: Upload ISO + uses: actions/upload-artifact@v7 + with: + name: sigma-linux + path: | + out/sigma-linux.iso + out/sigma-linux.iso.sha256 + compression-level: 0 + retention-days: 14 + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + sudo apt-get update -q && sudo apt-get install -y -q --no-install-recommends qemu-system-x86 + + - name: Boot test + timeout-minutes: 15 + run: sh .github/boot-test.sh + + - name: Upload logs + if: failure() || cancelled() + uses: actions/upload-artifact@v7 + with: + name: logs + path: | + cache/logs/ + !cache/logs/latest + if-no-files-found: ignore + retention-days: 7 + + release: + name: Release + needs: iso + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v8 + with: + name: sigma-linux + path: out + + - name: Publish + env: + GH_TOKEN: ${{ github.token }} + run: | + size=$(stat -c %s out/sigma-linux.iso) + if [ "$size" -ge 2147483648 ]; then + echo "::error::The ISO is $((size / 1048576)) MiB; GitHub release assets must be under 2 GiB." + exit 1 + fi + gh release create "$GITHUB_REF_NAME" out/sigma-linux.iso out/sigma-linux.iso.sha256 \ + --repo "$GITHUB_REPOSITORY" --title "Sigma Linux $GITHUB_REF_NAME" --generate-notes diff --git a/.gitignore b/.gitignore index b1ec6d5..a9ccdfe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,4 @@ *.swp -aports/ cache*/ out/ -profile/repo -profile/apk/sigma-rootfs/rootfs.tar.gz -src/config.override.sh +config/local.yml diff --git a/Dockerfile b/Dockerfile index 71f26dd..405c29e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,32 @@ +# Sigma Linux build environment. +# +# Nothing is copied in: `./sigma` mounts the checkout at /app and runs itself +# inside, and everything worth keeping (downloads, ccache, signing key, the +# local repository) lives in ./cache on the host, so this image stays a pure +# toolbox that is cheap to rebuild. FROM alpine:edge -# Setup packages -RUN printf "http://dl-cdn.alpinelinux.org/alpine/edge/main\nhttp://dl-cdn.alpinelinux.org/alpine/edge/community\nhttp://dl-cdn.alpinelinux.org/alpine/edge/testing\n" > /etc/apk/repositories -RUN apk update -RUN apk add gcc openssl-dev elfutils-dev git gpg make musl-dev flex bison linux-headers perl mtools netpbm netpbm-extras bc diffutils findutils installkernel python3 sed xz gmp-dev mpc1-dev mpfr-dev # Kernel dependencies -RUN apk add doas alpine-sdk # Alpine SDK and build tools -RUN apk add squashfs-tools xorriso grub grub-efi grub-bios # ISO dependencies -RUN apk add shadow -RUN apk add ruby tar -RUN apk add umount -# RUN apk add clang llvm lld # For kernel built with clang -RUN apk add libx11-dev libxft-dev harfbuzz-dev fontconfig-dev freetype-dev gd-dev glib-dev # xorg/suckless dependencies -RUN apk add libxkbcommon-dev glib-dev wayland-dev pango-dev cairo-dev # wvkbd dependencies -RUN apk add ruby-dev ruby-full ruby-rdoc ruby-bundler +RUN printf '%s\n' \ + https://dl-cdn.alpinelinux.org/alpine/edge/main \ + https://dl-cdn.alpinelinux.org/alpine/edge/community \ + https://dl-cdn.alpinelinux.org/alpine/edge/testing \ + > /etc/apk/repositories -# Create build user -RUN adduser -D build -G abuild -RUN addgroup build wheel +# Package builds pull their own makedepends (abuild -r) from the APKBUILDs, +# so only the tools around them are listed here. +RUN apk add --no-cache \ + ruby \ + alpine-sdk ccache doas openssl \ + coreutils tar shadow umount \ + squashfs-tools xorriso mtools grub grub-bios grub-efi -# Setup abuild for build user -RUN echo "permit nopass keepenv :wheel" > /etc/doas.conf -USER build -RUN abuild-keygen -i -a -n -# Fix for building helix -RUN git config --global http.postBuffer 1048576000 -RUN git config --global http.version HTTP/1.1 -USER root +# The unprivileged user abuild runs as. Matching the host user's UID keeps +# files it writes into ./cache owned by that user on the host. +ARG BUILD_UID=1000 +RUN adduser -D -u "$BUILD_UID" -G abuild build \ + && addgroup build wheel \ + && printf '%s\n' 'permit nopass root as build' 'permit nopass :wheel' > /etc/doas.conf -# Fix for building helix -RUN git config --global http.postBuffer 1048576000 -RUN git config --global http.version HTTP/1.1 - -CMD ["/bin/sh", "./build.sh"] +ENV SIGMA_CONTAINER=1 +WORKDIR /app +CMD ["./sigma", "build"] diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..189878e --- /dev/null +++ b/PLAN.md @@ -0,0 +1,360 @@ +# Build system rework + +Rework of the Sigma Linux build system: Bash → Ruby, monolithic script → staged +pipeline, plus a fix for GRUB losing its theme on kernel updates. + +Legend: `[x]` done and verified · `[~]` done, verification pending · `[ ]` to do + +--- + +## 1. Where we start + +What the current build does, end to end (`build.sh`, run as root inside Docker): + +| Step | Script | Notes | +|---|---|---| +| Stage APKs | `src/setup_apks.sh` | One hand-written block per package; special cases tar `rootfs/` and `sent/` into source tarballs. Skips a package if its staging dir exists, so edits to an APKBUILD are **never** picked up until `cache/` is wiped. | +| Build repo | `src/build_repo.sh` | Skips a package if *any* `.apk` with its name prefix is in the repo, so version bumps are ignored too. `abuild -kK` keeps every build tree: **15 GB** in `cache/apk` today. | +| Rootfs + squashfs | `src/make_squashfs.sh` | Installs the whole profile with `--allow-untrusted --no-cache` (no signature checks on *Alpine's* packages, and re-downloads ~1-2 GB on every build). Skips installing if `/etc/apk` exists, so a rebuilt kernel never lands in the image. Config files written via heredocs. | +| ISO | `src/make_iso.sh` | Writes `grub.cfg` from a heredoc; theme values (gfxmode, wallpaper, VT colours) live only here. | + +Problems worth fixing beyond the language: + +- **No way to do part of the work.** You can't build one package and stop, rebuild the image without + rebuilding packages, or re-run just the ISO step, short of commenting code out. +- **Staleness everywhere.** Every "skip if exists" check is a stale-cache trap (see the table). +- **Nothing persists between containers.** abuild downloads (the 150 MB kernel tarball), the apk + package cache and the abuild signing key all live inside the container and vanish with it. +- **Root-owned files** in `cache/` on the host (hence the `chmod 777`). +- **CI is broken.** The workflow does `uses: ./`, which needs an `action.yml` at the repo root (there is + none), and Docker actions aren't privileged, so `mount --rbind` / `chroot` couldn't work anyway. +- Junk committed by in-place abuild runs (`src/apk/sigma-vt323/src/*.tar.xz` symlinks to `/app/...`). + +### The GRUB bug (root cause found) + +Alpine's grub package ships this trigger: + +```sh +if grep -q '^disable_trigger=1' /etc/update-grub.conf 2>/dev/null; then exit 0; fi +exec /usr/sbin/update-grub +``` + +The build writes **`/etc/upgrade-grub.conf`**: wrong file name, so the guard never applied, and every +kernel update ran a stock `grub-mkconfig` over the hand-made config. (This machine also has +`disable_trigger=yes`, which wouldn't match either.) + +Just fixing the file name would stop the overwrite, but it would also stop GRUB from ever picking up kernel +changes (for example a second kernel installed next to `linux-sigma`). Running stock +`grub-mkconfig` isn't an option either, because its output can't boot with initrdbo: + +- initrdbo **requires** `rootfstype=` on the command line, or it drops to a troubleshooting shell; +- on encrypted installs `root=` must name the **LUKS container** (initrdbo unlocks it itself), while + `grub-mkconfig` emits the UUID of the filesystem *inside* the container; +- the theme (640x480 gfxterm, wallpaper, `vt.*` colours) isn't in `/etc/default/grub` at all. + +**Fix:** make `grub-mkconfig` produce the Sigma config instead of fighting it (Phase 5). + +--- + +## 2. Decisions + +### Ruby, stdlib only +No gems. The tool must run on the host (to dispatch into Docker), inside the Alpine container, and in CI, +and Ruby's stdlib already covers it all: `optparse`, `yaml`, `json`, `digest`, `open3`, `erb`, `fileutils`. +Nothing to `bundle install`, nothing to break. + +### A purpose-built CLI, not Rake / Make / Thor +I weighed the obvious alternatives: + +- **Rake / Make**: give a dependency graph for free, but their freshness model is file mtimes. Our inputs + are directories, remote repositories and config values, so the stamp logic has to be custom anyway. + "Run just this stage" fights them (they always walk prerequisites), task arguments read badly + (`rake 'pkg[linux-sigma]'`), and the output can't be made pleasant. +- **Thor**: a gem dependency in exchange for option parsing we get from `optparse`. + +The pipeline itself is small (four stages, linear), so a purpose-built runner is less code than adapting a +general one, and it lets the output be designed rather than inherited. + +### Stages, steps and selections +The build is a linear pipeline of **stages**, each made of named **steps**: + +``` +packages ──▶ rootfs ──▶ squashfs ──▶ iso +``` + +| Stage | Produces | Steps | +|---|---|---| +| `packages` | signed local repo `cache/repo/` | one per local package (built on demand, in dependency order) | +| `rootfs` | `cache/rootfs/`, `cache/boot/` | `prepare`, `install`, `configure` | +| `squashfs` | `cache/rootfs.squashfs` | `compress` | +| `iso` | `out/sigma-linux.iso` | `stage`, `grub`, `image` | + +Selection syntax (one grammar for everything): + +``` +sigma build # whole pipeline, skipping stages that are up to date +sigma build rootfs # just that stage (named ⇒ always runs) +sigma build rootfs.. # rootfs and everything after it +sigma build ..squashfs # everything up to squashfs +sigma build packages:linux-sigma # a subset of a stage: one package… +sigma build rootfs:configure # …or one step +sigma pkg linux-sigma initrdbo # shortcut for packages: +``` + +**Freshness model.** Each stage has a fingerprint (hash of its inputs: source files, relevant config, and +the fingerprint of the stage before it) stored in `cache/state/`. A bare `sigma build` skips a stage whose +fingerprint matches and whose outputs exist. Naming a stage means "do it" and it always runs. Packages +are tracked individually: a package rebuilds when its directory content changes or its `.apk` files are +missing. That fixes the "edited the APKBUILD, nothing happened" trap. Remote Alpine updates are not +fingerprinted; `--force` or naming the stage covers that. + +**Local packages are built on demand.** The packages stage builds the local packages the selected profile +needs (plus their local dependencies), not everything under `packages/`. Unused recipes (`sigma-dwl`, +`sigma-st`, `simplex-chat`) stop costing build time but stay buildable by name. + +### Configuration: YAML, layered +`config/sigma.yml` (committed defaults) ← `config/local.yml` (git-ignored, replaces +`config.override.sh`) ← `--set key=value` on the command line. `sigma config` prints the merged result. + +### Docker by default, native when it makes sense +`./sigma …` on the host builds the image if needed and re-runs itself inside the container with the same +arguments. Inside the container (or with `--native`) it runs directly. `sigma shell` replaces +`docker-env.sh`. Building a single package natively on a Sigma host works too, since abuild doesn't need root. + +### Layout + +``` +sigma CLI entry point +lib/sigma/ the build system +config/sigma.yml build configuration +config/repositories Alpine repositories for the image (was src/repositories) +packages// local APKBUILDs (was src/apk/) +profiles/*.list package profiles with @include (were src/pkglist.*) +image/overlay/ files copied verbatim onto the rootfs (were heredocs) +image/grub.cfg.erb live ISO boot menu template +cache/ intermediate state (safe to delete) +out/ final artifacts (the ISO) +``` + +--- + +## 3. Phases + +### Phase 1: Core toolkit ✅ +The Ruby foundation everything else stands on: no build logic yet. + +- [x] `sigma` entry point and `lib/sigma.rb` loader +- [x] `UI`: palette (Sigma teal, truecolor → 256 → none), `NO_COLOR`, TTY detection, headers, + step lines with spinner + elapsed time + live last-output-line, failure box with log tail, summary +- [x] `Shell`: run commands with output captured to per-step log files, live or verbose streaming, + `as_builder` (drop to the abuild user when root), `chroot`, clear errors +- [x] `Config`: YAML defaults + local + `--set` overrides, deep merge, typed accessors, derived paths +- [x] `CLI`: subcommands (`build`, `pkg`, `status`, `clean`, `config`, `shell`, `run`), help text +- [x] **Check:** `sigma --help`, `sigma config`, `sigma config --set squashfs.compression=zstd`, and a UI + demo exercising success / failure / non-TTY output + +### Phase 2: Layout and data ✅ +Move sources into their new homes and turn heredocs into data. Content stays identical. + +- [x] `src/apk/` → `packages/` (git mv), drop committed abuild junk (`sigma-vt323/src/`) +- [x] Profiles: `minimal` ⊂ `standard` ⊂ `full` rewritten with `@include` and `-pkg` removals +- [x] **Check:** each new profile resolves to *exactly* the same package set as the old list + (minimal 155, standard 237, full 243 packages: identical) +- [x] Heredoc files → `image/overlay/` (`issue`, `motd`, `fstab`, `network/interfaces`, `doas.conf`, + `conf.d/zram-init`, `modules`); services, user and skel dirs → `config/sigma.yml` +- [x] **Check:** overlay files byte-identical to what the heredocs produced (shell-rendered and diffed) +- [x] The GRUB wallpaper moved into `sigma-conf` (`/usr/share/sigma/grub/`), ready for Phase 5 + +### Phase 3: The pipeline ✅ +Port the four stages onto the toolkit, with the selection grammar and freshness model. + +- [x] `Pipeline`: stage registry, selection parsing (`a`, `a..`, `..b`, `a..b`, `a:x,y`), fingerprints, + stamps in `cache/state/`, skip-if-fresh, end-of-run summary +- [x] `APKBUILD` reader (sources the file in `sh`, never `eval`s it in Ruby) and local package graph + (profile → needed packages via names / subpackages / provides, topological order) +- [x] Stage `packages`: generic staging (a `foo.tar.gz` source with a sibling `foo/` dir is tarred + reproducibly; no per-package code), `abuild checksum`, build, prune superseded `.apk`s, re-index, + remove build trees on success +- [~] Stage `rootfs`: fresh root each run, signed install (Alpine keys + our key, **no + `--allow-untrusted`**), persistent apk cache, overlay (before install for triggers, after to win), + services, user, merge-usr; mounts always torn down, even on failure or Ctrl-C +- [~] Stage `squashfs`, stage `iso` (boot entry per installed kernel, rendered from template) +- [x] `status` and `clean` commands +- [x] **Check:** selection grammar unit-tested; APKBUILD reader and package graph tested against the real + `packages/`; dry run (`--plan`) prints the right stages / steps for each selection. + `ruby test/run.rb`: 23 tests, including freshness (inputs / upstream token / partial runs) and the + ISO menu rendering byte-identical to the old `make_iso.sh` output. Stages marked `[~]` run for real in + Phase 4 (packages) and Phase 6 (rootfs, squashfs, iso). + +### Phase 4: Container and caching ✅ +- [x] Dockerfile: one documented package layer, `ccache`, build user with host UID, no keygen baked + into the image, `SIGMA_CONTAINER=1`, minimal build context +- [x] Persistent caches under `cache/`: abuild sources (`downloads/sources/`), apk packages (`downloads/apk/`), ccache, + abuild signing keys (`keys/`) +- [x] Docker dispatch from the host (`build` / `shell` / everything), cross-arch binfmt setup kept +- [x] Host-owned outputs (no more root-owned `cache/` / `out/` on the host) +- [x] CI workflow rewritten to run the same `./sigma build` in a privileged container; grown into the full + CI/CD of Phase 7 +- [x] **Check:** image builds; `sigma shell` works; `sigma pkg initrdbo` builds a package end to end + in the container; a second run is a no-op; editing the APKBUILD triggers a rebuild. + Also verified: a `pkgrel` bump builds `-r1` and prunes `-r0` from the signed index (and back); + `sigma-conf` / `sigma-sent` sources packed reproducibly by the generic rule; makedepends served from + the shared apk cache; outputs, logs, stamps and the (0600) key owned by the host user. + +### Phase 5: GRUB that survives kernel updates +- [x] `sigma-conf` ships the theme as data: `/usr/share/sigma/grub/grub.conf` (shell-sourceable + `GRUB_*` vars: gfxmode, gfxterm, background, font, VT colours, timeout, top-level kernel) and + `wallpaper.png`; the *same file* drives the ISO menu, so the theme lives in one place +- [x] `sigma-grub-setup` (installed system): writes `/etc/default/grub` that sources the theme and pins + `root=` / `rootfstype=` for initrdbo (LUKS-aware), installs the wallpaper on `/boot`, removes the + legacy guard files, runs `update-grub`. Re-runnable, so it also migrates existing installs. +- [x] `setup-sigma` uses it instead of copying and `sed`-patching the ISO's `grub.cfg`; it now + bind-mounts the live `/dev` into the chroot (grub-mkconfig must resolve `/dev/mapper/…` on LUKS) + and mounts `/sys` +- [x] Build: trigger correctly disabled *during* the image build only, removed before shipping +- [x] Menu layout matches the old one: the Sigma kernel is the plain "Sigma Linux" entry + (`GRUB_TOP_LEVEL`), other kernels go under "Advanced options for Sigma Linux" +- [~] **Check:** container + loop-device test, three scenarios (plain, LUKS, migrating an old-style install): + 48/48 checks passed, including apk's real grub trigger regenerating the menu on a kernel install + with theme, `root=UUID=`, `rootfstype` and VT colours intact, overrides + kept across re-runs, and grub's post-upgrade leaving the cmdline alone. After the last tweak (submenu + layout) the setup checks passed again, but the run was cut off by the crash below, before the + kernel-update part. **Remaining verification moves into the Phase 6 VM test; no more loop / dm + devices on the host kernel.** + +### Phase 6: Cut-over and end-to-end +- [x] Remove the Bash build (`build.sh`, `docker-env.sh`, `src/*.sh`, `start*.sh`, `sensible_clean.sh`); every + feature checked off against the Ruby port first (one deliberate difference: empty `/dev`, `/proc`, + `/boot` mountpoints stay in the image instead of being deleted; initrdbo and `setup-sigma` `mkdir -p` + them anyway) +- [x] README: build documentation +- [x] `sigma run --disk FILE [--no-iso]` to try the installer in QEMU (replaces `start*.sh`) +- [~] **Check:** the runbook below. Steps 0-5 done on the workstation; the full build and a boot moved to + CI (Phase 7), so the workstation only runs the light steps and, when it can be spared, the VM test. + +#### Runbook + +You run these; I don't. Nothing heavy runs in parallel. **Stop at the first step that doesn't look as +described** and send me the error box (plus `cache/logs/latest//.log` if it names one). + +0. ✅ **Protect the work.** Committed and pushed (`claude-rework`); SSD health checked: no media errors, + 5% used. TRIM enabled through dm-crypt (Phase 7). + +1. ✅ **No Docker yet.** Tests pass; `--plan` and `status` work. + +2. ✅ **Leftovers from the crash** removed (`cache/grubtest`, the interrupted kernel tree). + +3. ✅ **Small packages.** `initrdbo`, `sigma-conf`, `biblegen`, `sigma-sent` built. + +4. **The rest except the kernel.** Covered by the CI build (step 6); locally only needed before a local + `sigma build`: `./sigma pkg rdbo-archives sigma-firacode-nerd sigma-flat-remix-gtk + sigma-material-symbols sigma-vt323 sigma-wvkbd` → six ✓. + +5. ✅ **The kernel, alone.** `linux-sigma` 7.3.1 built (two fixes on the way, Phase 7), installed on the + workstation and booted. + +6. **Full build, in CI.** Push, then run the workflow on the branch: + `gh workflow run build-iso.yml --ref claude-rework` (or Actions → CI → Run workflow). Expect a few hours, + mostly the kernel on 4 cores. Green means: every package, rootfs, squashfs and ISO built, and the ISO + booted in QEMU to the end of the default runlevel (`.github/boot-test.sh`). The ISO is the run's + `sigma-linux` artifact; on failure the `logs` artifact has every step's log. + +7. **Second CI run** (re-run the workflow): the `linux-sigma` line in the Build step should show a high + `ccache hit …%`, and the run should be much shorter. That verifies the ccache fix. + +8. **Boot it with a screen** (when the workstation can spare a VM; needs QEMU: + `doas apk add qemu-system-x86_64 qemu-img qemu-ui-gtk ovmf`). Put the CI artifact's ISO at + `out/sigma-linux.iso`, then `./sigma run` (BIOS) and `./sigma run --uefi`: the Sigma menu (wallpaper, + 640x480), boots to the live desktop. Log in as `user` / `pass`. + +9. **Install test, in the VM only.** `./sigma run --uefi --disk cache/vm.qcow2` → in the live system: + `doas setup-sigma`, disk `/dev/nvme0n1` (the VM disk is NVMe, like your machine), full-disk encryption + **y**, random fill **n**, `YES`. Power off, then + `./sigma run --uefi --disk cache/vm.qcow2 --no-iso`: + - the menu shows the wallpaper and a plain **Sigma Linux** entry; the LUKS passphrase prompt appears; + the desktop comes up; + - `cat /proc/cmdline` has `root=UUID=…`, `rootfstype=ext4` and the `vt.default_*` colours; + - **the kernel-update test:** `doas apk add linux-lts`; the output shows the grub trigger running. + `doas reboot`: same themed menu, *Advanced options for Sigma Linux* now lists the lts kernel, and + the default entry still boots. + - Optional: repeat with encryption **n** on a fresh disk (`rm cache/vm.qcow2`). + +10. **Incremental behaviour** (after a local full build only). `./sigma build` → "Everything is up to + date". `./sigma build iso` → only the iso stage runs. + +11. **Migrate your own machine's GRUB** (light: no build beyond `sigma-conf`, seconds). The installed + `sigma-conf` predates the theme files, so the new one goes in first. This touches your real + bootloader, so dry-run first: + 1. `./sigma pkg sigma-conf` (Docker running). The key is already trusted from the kernel install. + 2. `doas apk add -u --repository "$PWD/cache/repo/sigma" sigma-conf`, then `apk info -v sigma-conf` → + `sigma-conf-1.0-r2` and `command -v sigma-grub-setup` finds it. (The installed one was `1.0-r0`, the + same version as the first rework build, which apk wouldn't have replaced; hence the `pkgrel` bump.) + 3. Move kernel backups out of `/boot`'s top level: + `doas mkdir -p /boot/backup && doas mv /boot/vmlinuz-sigma.old_* /boot/backup/`. grub-mkconfig + counts every `/boot/vmlinuz-*` as a kernel (it skips only `.dpkg-*`, `.rpmsave`/`.rpmnew` and `.sig` + names), and that one has no matching initramfs (or modules), so its entry couldn't boot. initrdbo's + `initramfs-*.old_*` backups can stay; GRUB never lists them. + 4. `doas sigma-grub-setup --no-update`, then read `/etc/default/grub`: `GRUB_DEVICE_UUID` must equal the + `root=UUID=` in your `/proc/cmdline`. + 5. `doas grub-mkconfig -o /tmp/grub.cfg.new` and compare with `/boot/grub/grub.cfg`: same root UUID, + `rootfstype=ext4`, the colours, the wallpaper. + 6. Only then `doas update-grub` (it keeps the old file as `grub.cfg.backup`). From here on, kernel + updates regenerate the themed menu by themselves; `sigma-grub-setup` removed the + `disable_trigger` guard. + 7. The generated menu is "Sigma Linux" plus "Advanced options for Sigma Linux" (the other kernels). For + a flat list of every kernel instead, like the hand-made menu, add `GRUB_DISABLE_SUBMENU=true` below + the "Local overrides" line of `/etc/default/grub` and run `doas update-grub`. + +### Phase 7: After the first real builds +What building and installing the 7.3 kernel for real turned up, and the CI/CD that takes the heavy +builds off the workstation. + +- [x] Kernel 7.3 failed on musl in a build-time self-test (`insn_sanity`: a uapi header uses + `__attribute_const__`, which only glibc defines). `CONFIG_X86_DECODER_SELFTEST` off; it's a + decoder test for kernel developers, nothing in the shipped kernel depends on it +- [x] The error box shows the **first** error in the output, with GCC's include chain (parallel make + keeps going for thousands of lines after it), and drops the build-tree prefix from paths +- [~] ccache across kernel builds: the rebuild got ~3% hits. The kernel compiles its GCC plugin + (latent entropy) afresh in each build tree and loads it into nearly every compile; ccache keyed + the plugin by mtime. Now `compiler_check=content`, cap 10 GiB (`packages.ccache_size`), and each + package's line shows its hit rate. **Verify:** runbook step 7 +- [x] `sigma-grub-setup` on the workstation picked the UUID of the ext4 *inside* LUKS: there + `/dev/mapper/cryptroot` is a device node, not a link to `/dev/dm-N`, so the lookup of the partition + under it found nothing. It now finds `dm-N` by the mapping's name, refuses to leave a device-mapper + device as `root=`, and on a running system refuses a UUID other than the one it booted with + (nothing is written in that case). `sigma-conf` 1.0-r2 (r1 was the first with the theme files) +- [x] `smartmontools` and `nvme-cli` in the `minimal` profile +- [x] Workstation: TRIM enabled through dm-crypt (`cryptsetup refresh --allow-discards --persistent`), + first `fstrim` done; the GRUB guard moved to the file the trigger actually reads +- [~] CI/CD (`.github/workflows/build-iso.yml`): + - tests (in Alpine, plus `sigma build --plan`) on every push and pull request + - the full ISO build for master, version tags and on demand, with ccache carried between runs + (saved even when a build fails or times out, so a re-run continues) + - a boot test: the ISO's kernel and initramfs in QEMU with KVM and a serial console, the ISO as the + CD-ROM; passes when OpenRC reaches `Starting local` + - on a `v*` tag, a GitHub release with the ISO and its SHA-256, after the boot test passes + - the signing key is generated per run and never stored, cached or uploaded + + **Verify:** runbook steps 6-7 (nothing of it can run on the workstation) + +--- + +## 4. Risks and notes + +- **Incident during development: the build host became unresponsive** (I/O errors; a reboot recovered it + cleanly) while a kernel build using every core ran *alongside* a loop-device test. The cause couldn't be + proven; most likely the storage stack stalled under the combined load. Safeguards since: + - `jobs: auto` = CPUs − 1, capped at one job per 1.5 GiB of RAM; + - compiles and compression run under `nice`; + - anything that partitions, formats or opens LUKS runs inside a QEMU VM, never against the host kernel; + - one heavy job at a time. + +- **Disk space.** A kernel build peaks around 23 GB; the rootfs, squashfs and ISO add ~10 GB. +- **CI can't be exercised locally**; the workflow mirrors the local container invocation, and the first + runs (runbook steps 6-7) are its test. Hosted runners have 4 cores and a 6-hour limit: a cold kernel + build should fit, and if it ever doesn't, the saved ccache lets a re-run finish. +- **Release assets must be under 2 GiB**; the release job checks and fails clearly if the ISO grows past it. +- **initrdbo keeps every old initramfs** (`/boot/initramfs-.old_`, ~20 MB per kernel + update) and never deletes them; `/boot` (1 GB) fills up after a few dozen updates unless they're pruned. +- Kernel updates on an installed system: the initramfs name is per-flavor (`initramfs-sigma`), so the order + in which the initrdbo and grub triggers run doesn't matter for an existing kernel. diff --git a/README.md b/README.md index 6305e2c..b61b9f8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,71 @@ # Sigma Linux ## Elite operating system +# Building + +Everything goes through one command, `./sigma`. It needs Ruby (standard library only) and Docker: build commands run themselves inside the build container, so the host needs nothing else. + +```sh +./sigma build # build the ISO: out/sigma-linux.iso +./sigma run # boot it in QEMU (--uefi for UEFI firmware) +``` + +The build is a pipeline of four stages, and a bare `./sigma build` skips the stages that are already up to date: + +``` +packages ──▶ rootfs ──▶ squashfs ──▶ iso +``` + +| Stage | What it does | Output | +|---|---|---| +| `packages` | builds the local packages (`packages/`) the profile needs into a signed repository | `cache/repo/` | +| `rootfs` | installs the profile into a fresh root filesystem and configures it | `cache/rootfs/` | +| `squashfs` | compresses the root filesystem | `cache/rootfs.squashfs` | +| `iso` | assembles a bootable image with a menu entry per kernel | `out/sigma-linux.iso` | + +Naming a stage (or part of one) runs just that: + +```sh +./sigma pkg linux-sigma # build one package and stop +./sigma build rootfs.. # rootfs and everything after it +./sigma build ..squashfs # everything up to the squashfs +./sigma build rootfs:configure # one step of a stage +./sigma build --plan # show what would run +./sigma status # what is built, what is stale, and why +./sigma clean rootfs.. # delete outputs (downloads and built packages stay) +./sigma shell # a shell in the build container +``` + +Packages rebuild when anything in their `packages//` directory changes. To install a freshly built package on the machine you're on (a new kernel, say), `./sigma pkg` prints the `apk add` command. + +## Configuration + +`config/sigma.yml` holds the defaults: name, profile, users, services, compression and so on. Put personal overrides in `config/local.yml` (git-ignored, same shape), or pass them for one run with `--set key=value`. `./sigma config` prints the result. + +| Path | Contents | +|---|---| +| `packages/` | local APKBUILDs | +| `profiles/` | package lists: `minimal` ⊂ `standard` ⊂ `full` (`@include` composes them) | +| `image/overlay/` | files copied verbatim onto the root filesystem | +| `image/grub.cfg.erb` | the live ISO's boot menu | +| `cache/` | intermediate state, safe to delete; logs in `cache/logs/latest/` | + +The boot menu's look (resolution, wallpaper, console colours) lives in `packages/sigma-conf/rootfs/usr/share/sigma/grub/grub.conf`, shared by the ISO and installed systems. On an installed system, `sigma-grub-setup` hooks it into `/etc/default/grub`, so the menu keeps its theme when kernel updates regenerate it. Installs made before this fix can run it once to migrate: `doas sigma-grub-setup`. + +## CI + +GitHub Actions (`.github/workflows/build-iso.yml`) runs the tests on every push and pull request. The full ISO build runs for `master`, for version tags, and on demand (`gh workflow run build-iso.yml --ref `). After building, it boots the ISO in QEMU (`.github/boot-test.sh`) and uploads it as the run's `sigma-linux` artifact. Pushing a `v*` tag also publishes the ISO as a GitHub release. Builds carry ccache between runs, so a rebuild after a kernel bump only recompiles what changed. + +## Resources + +A full build needs about 35 GB of free disk space (the kernel alone takes 15-20 GB while it builds). `jobs: auto` uses one core less than the machine has, and at most one job per 1.5 GiB of RAM, so the system stays usable. Run the tests with `ruby test/run.rb`. + # License This project is licensed under the GNU AGPL-3.0 (no later versions allowed) All the files in this repository follow the license in the file `LICENSE` present in this directory, unless explicitly said otherwise (e.g through a CREDITS or another LICENSE file) -Note that packages under `src/apk` will have their license names noted at the `APKBUILD` file +Note that packages under `packages/` will have their license names noted at the `APKBUILD` file Packages installed from the package repository will have their own licenses as well diff --git a/build.sh b/build.sh deleted file mode 100755 index 405e507..0000000 --- a/build.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/sh - -set -e - -if [ "$(id -u)" != "0" ]; then - echo "[!] Run as root" - exit 1 -fi - -echo "[*] Sigma Linux Builder" - -echo "[*] Running config..." -. ./src/config.sh - -mkdir -p "$CACHE_DIR" -chmod 777 "$CACHE_DIR" # Allow read-write for build user - -# Setup APKs and build local repository -echo "[*] Setting up APKs..." -doas -u "$BUILD_USER" -- ./src/setup_apks.sh - -echo "[*] Building local repository..." -doas -u "$BUILD_USER" -- ./src/build_repo.sh - -# Skip making squashfs if rootfs.squashfs is found -if [ ! -e "$SQUASHFS_PATH" ]; then - # Make squashfs - echo "[*] Making squashfs..." - ./src/make_squashfs.sh -else - echo "[*] Skipped making squashfs, file '$SQUASHFS_PATH' exists" -fi - -# Build ISO -echo "[*] Making ISO..." -./src/make_iso.sh - -echo "[*] Build finished successfully" diff --git a/src/repositories b/config/repositories similarity index 100% rename from src/repositories rename to config/repositories diff --git a/config/sigma.yml b/config/sigma.yml new file mode 100644 index 0000000..e57a3ae --- /dev/null +++ b/config/sigma.yml @@ -0,0 +1,58 @@ +# Sigma Linux build configuration. +# +# Personal overrides go in config/local.yml (git-ignored, same shape as this +# file), and one-off ones on the command line: sigma build --set profile=full +# `sigma config` prints the merged result. + +name: Sigma Linux # shown in the boot menu +id: sigma # hostname and image file names +arch: x86_64 +profile: standard # profiles/.list + +jobs: auto # parallel build jobs; auto = CPUs - 1, capped at 1 per 1.5 GiB RAM + +# Where intermediate state (cache) and finished artifacts (out) are written, +# relative to the repository. Point cache somewhere roomy: a kernel build +# alone needs 15-20 GB. +paths: + cache: cache + out: out + +packages: + builder: build # unprivileged user abuild runs as when we are root + ccache: true # speeds up rebuilds, the kernel's especially + ccache_size: 10G # one kernel build fills ~3.5 GB; older entries are evicted beyond this + keep_build_trees: false # keep abuild's src/ and pkg/ dirs for debugging + +rootfs: + repositories: config/repositories + overlay: image/overlay # copied verbatim onto the root filesystem + + user: + name: user + password: pass + shell: /bin/bash + groups: [wheel, audio, input, video, seat, dialout, netdev] + + # Created in /etc/skel, so every new user starts with them. + skel_dirs: [Downloads, Documents, Pictures, Videos, Music] + + # OpenRC services, by runlevel. + # udev* set up /dev and device ownership (e.g. /dev/dri/cardN), which is + # what lets users reach the GPU and input devices without root + # local runs start scripts + # modloop is deliberately absent: Sigma Linux doesn't need it + services: + sysinit: [devfs, dmesg, mdev, hwdrivers, udev, udev-trigger, udev-settle] + boot: [hwclock, modules, sysctl, bootmisc, syslog, hostname, zram-init] + default: [udev-postmount, networking, earlyoom, polkit, networkmanager, dbus, seatd, bluetooth, local] + +squashfs: + compression: xz # xz is smallest; zstd builds much faster for testing + args: [] # extra mksquashfs arguments + +iso: + volume_id: sigma-linux-cdrom + +docker: + image: sigma-linux diff --git a/docker-env.sh b/docker-env.sh deleted file mode 100755 index c7dbd08..0000000 --- a/docker-env.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -set -e - -echo "[*] Loading config..." -. src/config.sh >& /dev/null - -set_docker_arch() { - local osarch="$1" - local dockerarch="" - case "$osarch" in - x86_64) dockerarch="linux/amd64";; - aarch64) dockerarch="linux/aarch64";; - *) echo "[!] Failed to start docker for cross compilation target: unknown OS arch"; exit 1;; - esac - - export DOCKER_DEFAULT_PLATFORM="$dockerarch" - echo "DOCKER_DEFAULT_PLATFORM=$DOCKER_DEFAULT_PLATFORM" -} - -if [ "$DISTRO_TARGET_ARCH" != "$(uname -m)" ]; then - if [ ! -e "/proc/sys/fs/binfmt_misc/qemu-$DISTRO_TARGET_ARCH" ]; then - # Install binfmt - echo "[*] Setting up binfmt for cross compilation..." - doas modprobe binfmt_misc - doas mount -t binfmt_misc binfmt_misc /proc/sys/fs/binfmt_misc/ - docker run --privileged --rm tonistiigi/binfmt --install all - fi - - echo "[*] Setting up cross target docker..." - set_docker_arch "$DISTRO_TARGET_ARCH" -fi - -echo "[*] Building Dockerfile..." -docker build -t sigma-linux . - -echo "[*] Starting Docker environment..." -docker run --privileged -v "$(pwd):/app" --workdir /app -it sigma-linux sh diff --git a/image/grub.cfg.erb b/image/grub.cfg.erb new file mode 100644 index 0000000..6b37c4d --- /dev/null +++ b/image/grub.cfg.erb @@ -0,0 +1,21 @@ +<%# Live ISO boot menu. Theme values come from sigma-conf's grub.conf, which installed systems share. -%> +set timeout=<%= theme.fetch("GRUB_TIMEOUT", "5") %> + +insmod all_video +loadfont /boot/grub/fonts/unicode.pf2 + +insmod gfxterm +set gfxmode=<%= theme.fetch("GRUB_GFXMODE", "auto") %> +terminal_output gfxterm + +insmod png +background_image /boot/grub/wallpaper.png +<% kernels.each do |kernel| -%> + +menuentry "<%= kernel.title %>" { + echo "Loading vmlinuz..." + linux /boot/<%= kernel.vmlinuz %> root=LABEL=<%= volume_id %> rootfstype=iso9660 rd.live=/rootfs.squashfs <%= cmdline %> + echo "Loading initrd..." + initrd /boot/<%= kernel.initramfs %> +} +<% end -%> diff --git a/image/overlay/etc/conf.d/zram-init b/image/overlay/etc/conf.d/zram-init new file mode 100644 index 0000000..ee12fd5 --- /dev/null +++ b/image/overlay/etc/conf.d/zram-init @@ -0,0 +1,10 @@ +load_on_start="yes" +unload_on_stop="yes" +num_devices="1" + +type0="swap" +flag0= +size0=`LC_ALL=C free -m | awk '/^Mem:/{print int($2/2)}'` # 50% of memory reserved for zram +maxs0=1 +algo0=zstd +labl0=zram_swap diff --git a/image/overlay/etc/doas.conf b/image/overlay/etc/doas.conf new file mode 100644 index 0000000..bb2ee27 --- /dev/null +++ b/image/overlay/etc/doas.conf @@ -0,0 +1,2 @@ +permit persist :wheel +permit nopass root diff --git a/image/overlay/etc/fstab b/image/overlay/etc/fstab new file mode 100644 index 0000000..e69de29 diff --git a/image/overlay/etc/issue b/image/overlay/etc/issue new file mode 100644 index 0000000..1ded3c5 --- /dev/null +++ b/image/overlay/etc/issue @@ -0,0 +1,2 @@ +Welcome to Sigma Linux (made by rdbo) +Kernel \r on an \m (\l) diff --git a/image/overlay/etc/modules b/image/overlay/etc/modules new file mode 100644 index 0000000..786156a --- /dev/null +++ b/image/overlay/etc/modules @@ -0,0 +1,14 @@ +ahci libahci +ata_generic libata +ata_piix +nvme_core nvme +usbcore +xhci_pci ehci_pci uhci_hcd +usb_common usb_storage uas + +scsi_mod sd_mod sr_mod + +hid hid_generic usbhid + +af_packet +ipv6 diff --git a/image/overlay/etc/motd b/image/overlay/etc/motd new file mode 100644 index 0000000..5f9d681 --- /dev/null +++ b/image/overlay/etc/motd @@ -0,0 +1,8 @@ +Welcome to Sigma Linux! + +To install the system, run the following command: setup-sigma + +For more information about the distribution, see: + - https://github.com/rdbo/sigma-linux + - https://wiki.alpinelinux.org + diff --git a/image/overlay/etc/network/interfaces b/image/overlay/etc/network/interfaces new file mode 100644 index 0000000..f1bd92e --- /dev/null +++ b/image/overlay/etc/network/interfaces @@ -0,0 +1,2 @@ +auto lo +iface lo inet loopback diff --git a/lib/sigma.rb b/lib/sigma.rb new file mode 100644 index 0000000..d86ba7f --- /dev/null +++ b/lib/sigma.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# Sigma Linux build system. See `./sigma --help` and PLAN.md. +module Sigma + ROOT = File.expand_path("..", __dir__) +end + +require_relative "sigma/errors" +require_relative "sigma/ui" +require_relative "sigma/shell" +require_relative "sigma/config" +require_relative "sigma/profile" +require_relative "sigma/fingerprint" +require_relative "sigma/state" +require_relative "sigma/mounts" +require_relative "sigma/signing" +require_relative "sigma/ownership" +require_relative "sigma/apkbuild" +require_relative "sigma/local_packages" +require_relative "sigma/context" +require_relative "sigma/selection" +require_relative "sigma/stage" +require_relative "sigma/stages/packages" +require_relative "sigma/stages/rootfs" +require_relative "sigma/stages/squashfs" +require_relative "sigma/stages/iso" +require_relative "sigma/pipeline" +require_relative "sigma/docker" +require_relative "sigma/qemu" +require_relative "sigma/cli" diff --git a/lib/sigma/apkbuild.rb b/lib/sigma/apkbuild.rb new file mode 100644 index 0000000..741dc95 --- /dev/null +++ b/lib/sigma/apkbuild.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Sigma + # Metadata of one APKBUILD. Read by sourcing the file in sh, as abuild does, + # so variables built from other variables come out right. APKBUILDs are + # shell scripts; we never interpret them ourselves. + class Apkbuild + FIELDS = %w[pkgname pkgver pkgrel arch depends makedepends checkdepends subpackages provides source].freeze + + attr_reader :dir, :name, :pkgver, :pkgrel, :arch, :depends, :makedepends, :checkdepends, + :subpackages, :provides, :sources + + def self.read(dir, shell:, carch:) + script = <<~SH + . ./APKBUILD || exit 1 + #{FIELDS.map { |f| %(printf '%s\\0' "$#{f}") }.join("\n")} + SH + env = { "CARCH" => carch, "CBUILD_ARCH" => carch, "CTARGET_ARCH" => carch, + "startdir" => dir, "srcdir" => File.join(dir, "src"), "pkgdir" => File.join(dir, "pkg") } + values = shell.capture("sh", "-c", script, env: env, chdir: dir).split("\0", -1) + new(dir, FIELDS.zip(values).to_h) + rescue CommandFailed => e + raise Error, "#{File.basename(dir)}/APKBUILD: could not be read (#{e.tail.last || e.message})" + end + + def initialize(dir, fields) + @dir = dir + @name = fields["pkgname"] + @pkgver = fields["pkgver"] + @pkgrel = fields["pkgrel"] + @arch = words(fields["arch"]) + @depends = names(fields["depends"]) + @makedepends = names(fields["makedepends"]) + @checkdepends = names(fields["checkdepends"]) + @subpackages = words(fields["subpackages"]).map { |s| s.split(":").first } + @provides = words(fields["provides"]).map { |p| p.split(/[<>=~]/).first } + @sources = words(fields["source"]) + raise Error, "#{File.basename(dir)}/APKBUILD: pkgname/pkgver/pkgrel missing" if [@name, @pkgver, @pkgrel].any?(&:empty?) + end + + def version = "#{pkgver}-r#{pkgrel}" + + # Every package name this APKBUILD produces. + def outputs = [name, *subpackages] + + # Names this APKBUILD answers to when something depends on it. + def aliases = [*outputs, *provides] + + # Local files abuild expects next to the APKBUILD (not URLs). + def local_sources + sources.reject { |s| s.include?("://") }.map { |s| s.split("::").last } + end + + private + + def words(value) = value.to_s.split + + # Dependency names without version constraints; conflicts (!foo) dropped. + def names(value) + words(value).reject { |d| d.start_with?("!") }.map { |d| d.split(/[<>=~]/).first } + end + end +end diff --git a/lib/sigma/cli.rb b/lib/sigma/cli.rb new file mode 100644 index 0000000..7907d09 --- /dev/null +++ b/lib/sigma/cli.rb @@ -0,0 +1,219 @@ +# frozen_string_literal: true + +require "optparse" + +module Sigma + # Command-line front end: parses arguments, decides whether to hop into the + # build container, and renders errors. + class CLI + # Commands that touch the build environment run inside the container + # unless we are already there or --native is given. + CONTAINERIZED = %w[build pkg clean shell].freeze + + COMMANDS = { + "build" => "Run the pipeline, skipping stages that are up to date", + "pkg" => "Build local packages (same as build packages:NAME,…)", + "status" => "Show what is built and what is stale", + "clean" => "Delete stage outputs (every stage by default)", + "shell" => "Open a shell in the build container", + "run" => "Boot the ISO (or an installed disk) in QEMU", + "config" => "Print the effective configuration" + }.freeze + + def self.start(argv) = new.start(argv) + + def initialize + @options = { verbose: false, native: false, overrides: [], color: nil, force: false, plan: false } + end + + def start(argv) + @original_argv = argv.dup + argv = argv.dup + command = argv.first&.start_with?("-") ? nil : argv.shift + parser = option_parser(command) + args = parser.parse(argv) + @ui = UI.new(verbose: @options[:verbose], color: @options[:color]) + + return help(parser) if command.nil? || command == "help" + raise Error.new("unknown command `#{command}`", hint: "See `sigma --help`.") unless COMMANDS.key?(command) + + @config = Config.load(ROOT, overrides: @options[:overrides]) + return Docker.new(@config, @ui).dispatch(@original_argv) if containerize?(command) + + public_send("cmd_#{command}", args) + 0 + rescue OptionParser::ParseError => e + render_error(Error.new(e.message, hint: "See `sigma #{command} --help`.")) + rescue CommandFailed, Error => e + render_error(e) + rescue Interrupt + ui = @ui || UI.new + ui.blank + ui.say " #{ui.paint("Interrupted.", :warn)}" + 130 + end + + # -- commands --------------------------------------------------------------- + + def cmd_build(selections) + Pipeline.new(context).build(selections, force: @options[:force], plan: @options[:plan]) + end + + def cmd_pkg(names) + raise Error.new("which packages?", hint: "e.g. `sigma pkg linux-sigma`") if names.empty? + + cmd_build(["packages:#{names.join(",")}"]) + end + + def cmd_status(_args) + Pipeline.new(context).status + end + + def cmd_clean(selections) + Pipeline.new(context).clean(selections, everything: @options[:everything]) + end + + def cmd_shell(_args) + # Only reached with --native or inside the container already. + exec(ENV.fetch("SHELL", "/bin/sh")) + end + + def cmd_run(extra) + Qemu.new(@config, @ui).boot(uefi: @options[:uefi], disk: @options[:disk], iso: @options.fetch(:iso, true), extra: extra) + end + + def cmd_config(keys) + if keys.empty? + @ui.banner("Configuration", @config.sources) + @ui.table(@config.flatten.map { |k, v| [k, format_value(v)] }) + @ui.blank + else + keys.each { |key| @ui.say format_value(@config.fetch(key)) } + end + end + + private + + def context + @context ||= Context.new(@config, @ui) + end + + # --plan only reads files, so it never needs the container. + def containerize?(command) + CONTAINERIZED.include?(command) && !@options[:native] && !@options[:plan] && !Docker.inside? + end + + def format_value(value) + case value + when Array then value.join(" ") + when nil then @ui.paint("(unset)", :muted) + else value.to_s + end + end + + def option_parser(command) + OptionParser.new do |o| + o.banner = command ? "Usage: sigma #{command} #{usage_args(command)}" : "Usage: sigma [options]" + o.on("-v", "--verbose", "Stream command output instead of a live summary") { @options[:verbose] = true } + o.on("-s", "--set KEY=VALUE", "Override a config value (repeatable)") { |v| @options[:overrides] << v } + o.on("--native", "Run here instead of in the build container") { @options[:native] = true } + o.on("--no-color", "Disable colour") { @options[:color] = false } + case command + when "build", "pkg" + o.on("-f", "--force", "Rebuild even what is up to date") { @options[:force] = true } + o.on("--plan", "Show what would run, and stop") { @options[:plan] = true } + when "clean" + o.on("--everything", "Also delete downloads, ccache, signing keys and out/") { @options[:everything] = true } + when "run" + o.on("--uefi", "Boot with UEFI firmware (OVMF) instead of BIOS") { @options[:uefi] = true } + o.on("--disk FILE", "Attach a virtual disk (qcow2, created if missing)") { |f| @options[:disk] = File.expand_path(f) } + o.on("--no-iso", "Boot only the disk (an installed system; needs --uefi)") { @options[:iso] = false } + end + o.on("-h", "--help", "Show this help") do + help(o, command) + exit 0 + end + end + end + + def usage_args(command) + { + "build" => "[SELECTION…] [options]", "pkg" => "NAME… [options]", "clean" => "[SELECTION…] [options]", + "run" => "[options] [-- QEMU ARGS]", "config" => "[KEY…]" + }.fetch(command, "[options]") + end + + def help(parser, command = nil) + ui = @ui || UI.new + if command + ui.say parser.help + ui.blank + selection_help(ui) if %w[build clean].include?(command) + return 0 + end + + ui.banner("Sigma Linux builder") + ui.say " #{ui.paint("Usage", :accent, bold: true)} sigma [options]" + ui.blank + width = COMMANDS.keys.map(&:length).max + COMMANDS.each { |name, text| ui.say " #{ui.paint(name.ljust(width), :text, bold: true)} #{ui.paint(text, :muted)}" } + ui.blank + selection_help(ui) + ui.say " #{ui.paint("Options", :accent, bold: true)}" + parser.summarize { |line| ui.say " #{line}" } + ui.blank + ui.say " #{ui.paint("`sigma --help` for command options.", :muted)}" + ui.blank + 0 + end + + def selection_help(ui) + ui.say " #{ui.paint("Selections", :accent, bold: true)} #{ui.paint("stages: #{Pipeline::STAGE_NAMES.join(" → ")}", :muted)}" + [ + ["rootfs", "one stage (always runs)"], + ["rootfs..", "rootfs and every stage after it"], + ["..squashfs", "every stage up to squashfs"], + ["packages:linux-sigma,initrdbo", "part of a stage: packages…"], + ["rootfs:configure", "…or steps"] + ].each { |sel, text| ui.say " #{sel.ljust(30)} #{ui.paint(text, :muted)}" } + ui.say " #{ui.paint("(none: the whole pipeline, skipping what is up to date)", :muted)}" + ui.blank + end + + def render_error(error) + ui = @ui || UI.new + room = ui.width - 12 + lines = ui.wrap(error.message, room).map { |l| ui.paint(l, :text, bold: true) } + if error.is_a?(CommandFailed) + unless error.tail.empty? + lines << "" + lines << ui.paint("first error in the output:", :text) if error.first_error + lines.concat(error.excerpt.map { |l| ui.paint(without_build_tree(l), :muted) }) + end + lines << "" << "#{ui.paint("log", :muted)} #{relative(error.log_path)}" if error.log_path + end + if error.hint + hint = ui.wrap(error.hint, room - 5) + lines << "" << "#{ui.paint("hint", :accent)} #{hint.first}" + lines.concat(hint.drop(1).map { |l| " #{l}" }) + end + ui.blank + ui.box(lines, title: ui.paint("error", :error, bold: true), role: :error) + ui.blank + 1 + end + + # Compiler messages name files by their full path in abuild's build tree + # (cache/build/sigma//src/...); without that prefix the message + # itself fits in the box. + def without_build_tree(line) + return line unless @config + + line.gsub(%r{#{Regexp.escape(@config.paths.staging)}/[^/\s]+/src/}, "") + end + + def relative(path) + path.to_s.delete_prefix("#{ROOT}/") + end + end +end diff --git a/lib/sigma/config.rb b/lib/sigma/config.rb new file mode 100644 index 0000000..cd11a3d --- /dev/null +++ b/lib/sigma/config.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require "yaml" +require "etc" + +module Sigma + # Build configuration, layered (later wins): + # + # config/sigma.yml committed defaults + # config/local.yml personal overrides (git-ignored) + # --set key=value one-off overrides from the command line + class Config + DEFAULTS = "config/sigma.yml" + LOCAL = "config/local.yml" + + # Where everything lives. Intermediate state goes under cache/ (always safe + # to delete); finished artifacts go under out/. + Paths = Struct.new( + :root, :cache, :out, + :state, :logs, :keys, :ccache, + :sources, # abuild source downloads (SRCDEST) + :apk_cache, # apk package downloads for the image + :staging, # abuild working copies of packages/ + :repo, # local repository (abuild REPODEST) + :rootfs, :boot, :iso_tree, :squashfs, :iso, + keyword_init: true + ) + + REPO_NAME = "sigma" # abuild names the repo after the staging dir's parent + + attr_reader :root, :data, :sources + + def self.load(root, overrides: []) + new(root, overrides) + end + + def initialize(root, overrides) + @root = root + @sources = [] + @data = {} + [DEFAULTS, LOCAL].each do |relative| + path = File.join(root, relative) + next unless File.exist?(path) + + @data = deep_merge(@data, read_yaml(path)) + @sources << relative + end + raise Error.new("missing #{DEFAULTS}", hint: "Run sigma from the repository checkout.") if @sources.empty? + + overrides.each { |assignment| apply_override(assignment) } + @sources << "--set" unless overrides.empty? + end + + # dig("squashfs", "compression") or dig("squashfs.compression") + def dig(*keys) + keys = keys.flat_map { |k| k.to_s.split(".") } + keys.reduce(@data) { |node, key| node.is_a?(Hash) ? node[key] : nil } + end + + def fetch(path) + value = dig(path) + raise Error.new("config: `#{path}` is not set", hint: "Add it to #{LOCAL} or pass --set #{path}=…") if value.nil? + + value + end + + def name = fetch("name") + def id = fetch("id") + def arch = fetch("arch") + def profile = fetch("profile") + def builder = fetch("packages.builder") + + # `auto` leaves headroom so a build can't starve the machine: one core + # stays free, and each job gets ~1.5 GiB of RAM (roughly what one kernel + # compiler process peaks at). Using every core once made a build host + # unresponsive. + def jobs + value = dig("jobs") + return Integer(value) unless value.nil? || value == "auto" + + by_cpu = Etc.nprocessors - 1 + by_memory = (memory_gib / GIB_PER_JOB).floor + [by_cpu, by_memory].min.clamp(1, Etc.nprocessors) + end + + GIB_PER_JOB = 1.5 + + def memory_gib + File.read("/proc/meminfo")[/^MemTotal:\s+(\d+) kB/, 1].to_i / 1024.0 / 1024 + rescue SystemCallError + GIB_PER_JOB * 2 + end + + def paths + @paths ||= begin + cache = File.expand_path(dig("paths.cache") || "cache", root) + out = File.expand_path(dig("paths.out") || "out", root) + Paths.new( + root: root, cache: cache, out: out, + state: File.join(cache, "state"), + logs: File.join(cache, "logs"), + keys: File.join(cache, "keys"), + ccache: File.join(cache, "ccache"), + sources: File.join(cache, "downloads", "sources"), + apk_cache: File.join(cache, "downloads", "apk"), + staging: File.join(cache, "build", REPO_NAME), + repo: File.join(cache, "repo"), + rootfs: File.join(cache, "rootfs"), + boot: File.join(cache, "boot"), + iso_tree: File.join(cache, "iso"), + squashfs: File.join(cache, "rootfs.squashfs"), + iso: File.join(out, "#{id}-linux.iso") + ) + end + end + + # The local repository as apk sees it (REPODEST/). + def repo_dir = File.join(paths.repo, REPO_NAME) + + # Flattened "a.b.c" => value pairs, for display. + def flatten(node = @data, prefix = nil) + node.flat_map do |key, value| + path = [prefix, key].compact.join(".") + value.is_a?(Hash) ? flatten(value, path) : [[path, value]] + end + end + + private + + def read_yaml(path) + YAML.safe_load_file(path) || {} + rescue Psych::SyntaxError => e + raise Error, "#{path}: #{e.message}" + end + + def apply_override(assignment) + key, value = assignment.split("=", 2) + raise Error.new("bad --set `#{assignment}`", hint: "Use --set key.path=value") if value.nil? || key.empty? + + *parents, leaf = key.split(".") + node = parents.reduce(@data) { |n, k| n[k] = n[k].is_a?(Hash) ? n[k] : {} } + node[leaf] = YAML.safe_load(value) + rescue Psych::SyntaxError + node[leaf] = value + end + + def deep_merge(base, other) + base.merge(other) do |_, a, b| + a.is_a?(Hash) && b.is_a?(Hash) ? deep_merge(a, b) : b + end + end + end +end diff --git a/lib/sigma/context.rb b/lib/sigma/context.rb new file mode 100644 index 0000000..56251d4 --- /dev/null +++ b/lib/sigma/context.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require "etc" +require "fileutils" + +module Sigma + # What a single invocation shares across stages: configuration, the UI, the + # command runner, and this run's log directory. + class Context + KEEP_LOGS = 10 # runs + + attr_reader :config, :ui, :shell, :run_id + + def initialize(config, ui) + @config = config + @ui = ui + @shell = Shell.new(builder: config.builder) + @run_id = Time.now.strftime("%Y-%m-%d_%H%M%S") + end + + def paths = config.paths + + # Opens the log for one task: cache/logs///.log, and + # points cache/logs/latest at this run. + def log(stage, name) + dir = File.join(paths.logs, run_id, stage.to_s) + FileUtils.mkdir_p(dir) + link_latest + File.open(File.join(dir, "#{name.to_s.gsub(/[^\w.-]+/, "_")}.log"), "a").tap { |f| f.sync = true } + end + + # Runs a UI task with its own log, closing the log afterwards. + def task(stage, name, label: name, &block) + log = log(stage, name) + ui.task(label, log: log, &block) + ensure + log&.close + end + + # As root, the unprivileged builder must own what abuild writes to. + def give_to_builder(path, recursive: false) + return unless shell.root? + + user = Etc.getpwnam(config.builder) + recursive ? FileUtils.chown_R(user.uid, user.gid, path) : File.chown(user.uid, user.gid, path) + rescue ArgumentError + raise Error.new("build user `#{config.builder}` doesn't exist", + hint: "The container creates it; natively, set packages.builder to an abuild user.") + end + + private + + # On the first log of a run: point cache/logs/latest here and drop the + # logs of all but the last KEEP_LOGS runs. + def link_latest + return if @linked + + latest = File.join(paths.logs, "latest") + FileUtils.rm_f(latest) + File.symlink(run_id, latest) + runs = Dir.children(paths.logs).reject { |d| d == "latest" }.sort + runs.first([runs.size - KEEP_LOGS, 0].max).each { |old| FileUtils.rm_rf(File.join(paths.logs, old)) } + @linked = true + end + end +end diff --git a/lib/sigma/docker.rb b/lib/sigma/docker.rb new file mode 100644 index 0000000..80a7b74 --- /dev/null +++ b/lib/sigma/docker.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require "digest" +require "etc" +require "fileutils" + +module Sigma + # Runs sigma inside the build container: `./sigma build` on the host builds + # the image if the Dockerfile changed, then re-runs itself in a container + # with the checkout mounted at /app and the same arguments. + class Docker + MOUNT = "/app" + LABEL = "org.sigma-linux.dockerfile" + PLATFORMS = { "x86_64" => "linux/amd64", "aarch64" => "linux/arm64" }.freeze + + def self.inside? = ENV["SIGMA_CONTAINER"] == "1" + + def initialize(config, ui) + @config = config + @ui = ui + @shell = Shell.new(builder: config.builder) + end + + def dispatch(argv) + check_docker + setup_binfmt if cross? + build_image + shell = argv.first == "shell" + exec(*run_command(shell ? ["/bin/sh"] : ["./sigma", *argv], stdin: shell)) + end + + private + + def image = @config.fetch("docker.image") + def dockerfile = File.join(@config.root, "Dockerfile") + def platform = PLATFORMS.fetch(@config.arch) { raise Error, "no container platform known for arch #{@config.arch}" } + def cross? = @config.arch != Etc.uname[:machine] + + # The UID the image's build user gets. Root on the host (some CI) has no + # meaningful UID to match, so fall back to the usual first user. + def build_uid = Process.uid.zero? ? 1000 : Process.uid + + def check_docker + unless @shell.available?("docker") + raise Error.new("docker is not installed", + hint: "Install Docker, or run natively inside an Alpine environment with --native.") + end + @shell.capture("docker", "info", "--format", "{{.ServerVersion}}") + rescue CommandFailed => e + raise Error.new("can't reach the Docker daemon (#{e.tail.last || "docker info failed"})", + hint: "Start it (e.g. `doas rc-service docker start`) and make sure you can use its socket.") + end + + # The image is rebuilt only when the Dockerfile (or the build UID) changes; + # the label records what it was built from. + def build_image + wanted = Digest::SHA256.hexdigest(File.read(dockerfile) + build_uid.to_s + platform)[0, 16] + current = begin + @shell.capture("docker", "image", "inspect", image, "--format", "{{index .Config.Labels \"#{LABEL}\"}}").strip + rescue CommandFailed + nil + end + return if current == wanted + + context = Context.new(@config, @ui) + @ui.blank + context.task(:container, "image", label: "build container image") do |task| + @shell.run(task, "docker", "build", "--platform", platform, "--build-arg", "BUILD_UID=#{build_uid}", + "--label", "#{LABEL}=#{wanted}", "-t", image, "-f", dockerfile, @config.root) + task.detail = image + end + end + + def setup_binfmt + return if File.exist?("/proc/sys/fs/binfmt_misc/qemu-#{@config.arch}") + unless File.exist?("/proc/sys/fs/binfmt_misc/register") + raise Error.new("building for #{@config.arch} needs binfmt_misc, which isn't mounted", + hint: "doas modprobe binfmt_misc && doas mount -t binfmt_misc binfmt_misc /proc/sys/fs/binfmt_misc") + end + + Context.new(@config, @ui).task(:container, "binfmt", label: "enable #{@config.arch} emulation") do |task| + @shell.run(task, "docker", "run", "--privileged", "--rm", "tonistiigi/binfmt", "--install", @config.arch) + end + end + + # cache/ and out/ may be configured to live outside the checkout (another + # disk, say); those are mounted at the same path so the config still works. + def outside_mounts + [@config.paths.cache, @config.paths.out].reject { |p| p.start_with?("#{@config.root}/") }.flat_map do |path| + FileUtils.mkdir_p(path) + ["-v", "#{path}:#{path}"] + end + end + + # --privileged: building the image mounts and chroots. --init: so signals + # (Ctrl-C) reach sigma, which then unmounts before exiting. + def run_command(command, stdin: false) + interactive = if $stdin.tty? then ["-it"] + elsif stdin then ["-i"] + else [] + end + env = { "SIGMA_HOST_UID" => Process.uid, "SIGMA_HOST_GID" => Process.gid } + %w[TERM COLORTERM NO_COLOR FORCE_COLOR].each { |name| env[name] = ENV[name] if ENV[name] } + ["docker", "run", "--rm", "--init", "--privileged", *interactive, "--platform", platform, + "-v", "#{@config.root}:#{MOUNT}", *outside_mounts, "-w", MOUNT, + *env.flat_map { |k, v| ["-e", "#{k}=#{v}"] }, image, *command] + end + end +end diff --git a/lib/sigma/errors.rb b/lib/sigma/errors.rb new file mode 100644 index 0000000..01ac44c --- /dev/null +++ b/lib/sigma/errors.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Sigma + # An error meant for the user: rendered as a box with an optional hint, + # never as a Ruby backtrace. + class Error < StandardError + attr_reader :hint + + def initialize(message, hint: nil) + super(message) + @hint = hint + end + end + + # A command exited non-zero. Carries what is needed to explain it: the last + # lines it printed, the first error among them (see Shell::ERROR_LINE) and + # where the full log is. + class CommandFailed < Error + attr_reader :command, :status, :log_path, :tail, :first_error + + # The lines worth showing: the first error when there was one. Otherwise + # the end of the output, which for abuild means up to its ">>> ERROR" + # line, since the cleanup it prints afterwards explains nothing. + def excerpt(count = 15) + return first_error.first(count) if first_error + + marker = tail.rindex { |line| line.start_with?(">>> ERROR") } + marker ? tail[[marker - count + 1, 0].max..marker] : tail.last(count) + end + + def initialize(command, status, log_path: nil, tail: [], first_error: nil, hint: nil) + @command = command + @status = status + @log_path = log_path + @tail = tail + @first_error = first_error + super("`#{command}` exited with status #{status}", hint: hint) + end + end +end diff --git a/lib/sigma/fingerprint.rb b/lib/sigma/fingerprint.rb new file mode 100644 index 0000000..a09b75d --- /dev/null +++ b/lib/sigma/fingerprint.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "digest" +require "json" + +module Sigma + # Content hashes that decide whether something needs rebuilding. + module Fingerprint + module_function + + # Hash of arbitrary values (strings, arrays, hashes, numbers). + def of(*parts) + Digest::SHA256.hexdigest(JSON.generate(parts)) + end + + # Hash of a file or directory tree: relative paths, contents, symlink + # targets and the executable bit. Timestamps are ignored on purpose, so a + # fresh checkout of the same content is "unchanged". + def tree(path) + digest = Digest::SHA256.new + return digest.hexdigest unless File.exist?(path) || File.symlink?(path) + return Digest::SHA256.file(path).hexdigest if File.file?(path) && !File.symlink?(path) + + Dir.glob("**/*", File::FNM_DOTMATCH, base: path).sort.each do |relative| + next if relative.end_with?("/.", "/..") || relative == "." + + full = File.join(path, relative) + if File.symlink?(full) + digest << "L #{relative} -> #{File.readlink(full)}\0" + elsif File.file?(full) + digest << "F #{relative} #{File.executable?(full) ? "x" : "-"} #{Digest::SHA256.file(full).hexdigest}\0" + elsif File.directory?(full) + digest << "D #{relative}\0" + end + end + digest.hexdigest + end + end +end diff --git a/lib/sigma/local_packages.rb b/lib/sigma/local_packages.rb new file mode 100644 index 0000000..acf544f --- /dev/null +++ b/lib/sigma/local_packages.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +module Sigma + # The recipes under packages/, and which of them a build needs. + # + # Local packages are built on demand: a profile pulls in the ones it names + # (directly, through a subpackage like linux-sigma-dev, or through a provided + # name like initramfs-generator) plus their local dependencies. Recipes no + # profile uses cost nothing, but can still be built by name. + class LocalPackages + include Enumerable + + def initialize(dir, shell:, carch:) + @dir = dir + @shell = shell + @carch = carch + end + + def each(&) = all.each(&) + + def all + @all ||= Dir[File.join(@dir, "*", "APKBUILD")].sort.map do |path| + Apkbuild.read(File.dirname(path), shell: @shell, carch: @carch) + end + end + + def names = all.map(&:name) + + # Finds the recipe answering to +name+ (a pkgname, subpackage or provide). + def [](name) = index[name] + + # Local packages the given profile packages need, in build order. + def for_profile(packages) + seeds = packages.filter_map { |p| self[p] } + order(closure(seeds) { |pkg| pkg.depends + pkg.makedepends + pkg.checkdepends }) + end + + # The named packages plus what they need to *build*, in build order. + def for_names(requested) + seeds = requested.map do |name| + self[name] or raise Error.new("no local package `#{name}`", + hint: "Local packages: #{names.join(", ")}") + end + order(closure(seeds) { |pkg| pkg.makedepends + pkg.checkdepends }) + end + + private + + def index + @index ||= all.each_with_object({}) do |pkg, map| + pkg.aliases.each { |alias_name| map[alias_name] ||= pkg } + end + end + + def closure(seeds) + seen = {} + queue = seeds.dup + until queue.empty? + pkg = queue.shift + next if seen[pkg.name] + + seen[pkg.name] = pkg + yield(pkg).each { |dep| (local = self[dep]) && queue << local } + end + seen.values + end + + # Topological order over local dependencies; ties keep alphabetical order + # so builds are predictable. + def order(packages) + members = packages.to_h { |p| [p.name, p] } + sorted = [] + state = {} + visit = lambda do |pkg, path| + return if state[pkg.name] == :done + raise Error, "dependency cycle between local packages: #{(path + [pkg.name]).join(" → ")}" if state[pkg.name] == :visiting + + state[pkg.name] = :visiting + (pkg.depends + pkg.makedepends + pkg.checkdepends).each do |dep| + local = self[dep] + visit.call(local, path + [pkg.name]) if local && members[local.name] && local != pkg + end + state[pkg.name] = :done + sorted << pkg + end + members.values.sort_by(&:name).each { |pkg| visit.call(pkg, []) } + sorted + end + end +end diff --git a/lib/sigma/mounts.rb b/lib/sigma/mounts.rb new file mode 100644 index 0000000..7f144a1 --- /dev/null +++ b/lib/sigma/mounts.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require "fileutils" + +module Sigma + # Bind mounts for chroot-style work, and the safety net around them. + # + # The image root gets the host's /dev and /proc bind-mounted into it while + # packages install. Deleting that root while anything is still mounted would + # recurse into the real /dev, so every removal goes through #remove_tree, + # which refuses to touch a path with live mounts underneath. + module Mounts + module_function + + # Mount points at or below +path+, deepest first. + def under(path) + path = File.expand_path(path) + File.readlines("/proc/self/mountinfo").filter_map do |line| + point = line.split(" ")[4].gsub(/\\([0-7]{3})/) { Regexp.last_match(1).to_i(8).chr } + point if point == path || point.start_with?("#{path}/") + end.uniq.sort_by { |p| -p.length } + rescue Errno::ENOENT + [] + end + + def mounted?(path) = under(path).include?(File.expand_path(path)) + + # Recursive bind mount, made a slave so unmounting it later can never + # propagate back to the source (the same thing arch-chroot does). + def bind(shell, task, source, target) + FileUtils.mkdir_p(target) + shell.run(task, "mount", "--rbind", source, target) + shell.run(task, "mount", "--make-rslave", target) + end + + # Unmounts everything below +path+, falling back to a lazy unmount for + # anything still busy. + def release(shell, task, path) + under(path).each do |point| + next unless mounted?(point) # a recursive unmount may already have taken it + + begin + shell.run(task, "umount", "-R", point) + rescue CommandFailed + shell.run(task, "umount", "-R", "-l", point) + end + end + return if under(path).empty? + + raise Error.new("could not unmount #{under(path).join(", ")}", + hint: "Unmount them by hand (umount -R -l ) before trying again.") + end + + def remove_tree(path) + live = under(path) + unless live.empty? + raise Error.new("refusing to delete #{path}: still mounted at #{live.join(", ")}", + hint: "Run `sigma clean rootfs`, which unmounts first, or unmount by hand.") + end + + FileUtils.rm_rf(path) + end + end +end diff --git a/lib/sigma/ownership.rb b/lib/sigma/ownership.rb new file mode 100644 index 0000000..61bc4d2 --- /dev/null +++ b/lib/sigma/ownership.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "fileutils" + +module Sigma + # The container runs as root, but the checkout belongs to someone on the + # host. After a run, what root wrote that the host user deals with (the ISO, + # logs, stamps, keys, downloads) is handed back to them, so out/ and friends + # never end up root-owned. The root filesystem itself must stay root's. + class Ownership + def initialize(context) + @context = context + end + + def restore + uid = Integer(ENV.fetch("SIGMA_HOST_UID", ""), exception: false) + gid = Integer(ENV.fetch("SIGMA_HOST_GID", ""), exception: false) + return unless uid && @context.shell.root? + + paths = @context.paths + [paths.out, paths.logs, paths.state, paths.keys, paths.apk_cache].each do |path| + FileUtils.chown_R(uid, gid, path) if File.exist?(path) + end + File.chown(uid, gid, paths.cache) if File.exist?(paths.cache) + end + end +end diff --git a/lib/sigma/pipeline.rb b/lib/sigma/pipeline.rb new file mode 100644 index 0000000..0e7f0b9 --- /dev/null +++ b/lib/sigma/pipeline.rb @@ -0,0 +1,186 @@ +# frozen_string_literal: true + +require "fileutils" + +module Sigma + # The staged build: packages → rootfs → squashfs → iso + class Pipeline + STAGES = [Stages::Packages, Stages::Rootfs, Stages::Squashfs, Stages::Iso].freeze + STAGE_NAMES = STAGES.map(&:stage_name).freeze + + STATUS_GLYPH = { fresh: [:ok, :ok], stale: [:warn, :warn], missing: [:skip, :muted] }.freeze + + attr_reader :state + + def initialize(context) + @context = context + @state = State.new(context.paths.state) + @stages = STAGES.map { |klass| klass.new(context, self) } + end + + def stage(name) = @stages.find { |s| s.name == name } + def before(stage) = (index = @stages.index(stage)).positive? ? @stages[index - 1] : nil + + def profile + @profile ||= Profile.new(File.join(@context.paths.root, "profiles"), config.profile) + end + + # -- sigma build ------------------------------------------------------------ + + def build(args, force: false, plan: false) + selection = Selection.parse(args, STAGE_NAMES) + selection.entries.each { |entry| stage(entry.stage).validate(entry.items) if entry.items } + banner(plan ? "Plan" : "Build") + return show_plan(selection, force) if plan + + started = ui.clock + ran = selection.entries.filter_map do |entry| + current = stage(entry.stage) + if selection.incremental? && !force && current.fresh? + ui.stage_skipped(current.name, "up to date") + next + end + + current.check_prerequisites + ui.stage(current.name, current.description) { current.run(entry.items, force: force) } + current + end + finish(ran, ui.clock - started) + ensure + Ownership.new(@context).restore + end + + # -- sigma status ----------------------------------------------------------- + + def status + banner("Status") + width = STAGE_NAMES.map(&:length).max + 2 + @stages.each do |current| + state_now, reason = current.status + glyph, role = STATUS_GLYPH.fetch(state_now) + stamp = current.stamp + built = stamp ? "built #{ago(stamp["finished_at"])} in #{ui.duration(stamp["duration"].to_f)}" : nil + text = state_now == :fresh ? "up to date" : reason + ui.say " #{ui.paint(UI::GLYPHS[glyph], role)} #{ui.paint(current.name.ljust(width), :accent)}" \ + "#{ui.paint(text.ljust(22), role)}#{ui.paint(built.to_s, :muted)}" + end + ui.blank + package_status + artifact_status + end + + # -- sigma clean ------------------------------------------------------------ + + def clean(args, everything: false) + banner("Clean") + return clean_everything if everything + + selection = Selection.parse(args, STAGE_NAMES) + selection.entries.reverse_each do |entry| + current = stage(entry.stage) + freed = current.outputs.sum { |path| disk_usage(path) } + freed += disk_usage(@context.paths.repo) + disk_usage(File.dirname(@context.paths.staging)) if current.name == "packages" && entry.items.nil? + current.clean(entry.items) + what = entry.items ? "#{current.name}:#{entry.items.join(",")}" : current.name + ui.line(:ok, what, freed.positive? ? "freed #{ui.bytes(freed)}" : "nothing to remove", role: :ok) + end + ui.blank + ui.say " #{ui.paint("Kept: downloads, ccache and the signing key (`sigma clean --everything` removes them).", :muted)}" + ui.blank + ensure + Ownership.new(@context).restore + end + + private + + def config = @context.config + def ui = @context.ui + + def banner(action) + runner = Docker.inside? ? "container" : "native" + ui.banner("#{config.name} · #{action.downcase}", + ["profile #{config.profile}", config.arch, "#{config.jobs} jobs", runner]) + end + + def show_plan(selection, force) + width = STAGE_NAMES.map(&:length).max + 2 + selection.entries.each do |entry| + current = stage(entry.stage) + if selection.incremental? && !force && current.fresh? + ui.say " #{ui.paint(UI::GLYPHS[:skip], :muted)} #{ui.paint(current.name.ljust(width), :muted)}#{ui.paint("up to date, skipped", :muted)}" + else + ui.say " #{ui.paint(UI::GLYPHS[:run], :accent)} #{ui.paint(current.name.ljust(width), :accent, bold: true)}#{current.plan(entry.items)}" + end + end + ui.blank + end + + def finish(ran, elapsed) + if ran.empty? + ui.box(["#{ui.paint(UI::GLYPHS[:ok], :ok, bold: true)} Everything is up to date."], role: :muted) + else + lines = ["#{ui.paint(UI::GLYPHS[:ok], :ok, bold: true)} #{ui.paint("Done in #{ui.duration(elapsed)}", :text, bold: true)}" \ + "#{ui.paint(" #{ran.map(&:name).join(" → ")}", :muted)}"] + iso = @context.paths.iso + lines << " #{relative(iso)} #{ui.paint(ui.bytes(File.size(iso)), :muted)}" if ran.any? { |s| s.name == "iso" } + lines << " #{ui.paint("try it:", :muted)} ./sigma run" if ran.any? { |s| s.name == "iso" } + ui.box(lines, role: :ok) + end + ui.blank + end + + def package_status + packages = stage("packages") + list = packages.selected + return if list.empty? + + ui.say " #{ui.paint("packages", :accent, bold: true)} #{ui.paint("needed by profile #{config.profile}", :muted)}" + list.each do |pkg| + state_now, reason = packages.package_status(pkg) + glyph, role = STATUS_GLYPH.fetch(state_now) + ui.say " #{ui.paint(UI::GLYPHS[glyph], role)} #{ui.pad("#{pkg.name} #{pkg.version}", 36)}#{ui.paint(reason || "up to date", role == :ok ? :muted : role)}" + end + others = packages.local.names - list.map(&:name) + ui.say " #{ui.paint("not in profile: #{others.join(", ")}", :muted)}" unless others.empty? + ui.blank + end + + def artifact_status + iso = @context.paths.iso + return unless File.exist?(iso) + + ui.say " #{ui.paint("image", :accent, bold: true)} #{relative(iso)} #{ui.paint("#{ui.bytes(File.size(iso))}, #{ago(File.mtime(iso).to_i)}", :muted)}" + ui.blank + end + + def clean_everything + paths = @context.paths + Mounts.under(paths.rootfs).empty? || stage("rootfs").clean + [paths.cache, paths.out].each do |dir| + freed = disk_usage(dir) + Mounts.remove_tree(dir) + ui.line(:ok, relative(dir), freed.positive? ? "freed #{ui.bytes(freed)}" : "nothing to remove", role: :ok) + end + ui.blank + end + + def disk_usage(path) + return 0 unless File.exist?(path) + + @context.shell.capture("du", "-sk", path).split.first.to_i * 1024 + rescue CommandFailed + 0 + end + + def ago(epoch) + seconds = Time.now.to_i - epoch.to_i + return "just now" if seconds < 60 + return "#{seconds / 60}m ago" if seconds < 3600 + return "#{seconds / 3600}h ago" if seconds < 86_400 + + "#{seconds / 86_400}d ago" + end + + def relative(path) = path.to_s.delete_prefix("#{@context.paths.root}/") + end +end diff --git a/lib/sigma/profile.rb b/lib/sigma/profile.rb new file mode 100644 index 0000000..dc44ee1 --- /dev/null +++ b/lib/sigma/profile.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Sigma + # A package profile: profiles/.list. + # + # # comment ignored, also after packages on a line + # foo bar packages to install + # @include minimal everything another profile resolves to, in place + # -foo -linux-fw-* remove packages added so far (globs allowed) + # + # Lines are applied in order, so a removal only affects what came before it. + class Profile + attr_reader :name, :packages, :files + + def self.names(dir) + Dir[File.join(dir, "*.list")].map { |f| File.basename(f, ".list") }.sort + end + + def initialize(dir, name) + @dir = dir + @name = name + @files = [] + @packages = resolve(name, []) + end + + def include?(package) = @packages.include?(package) + + private + + def resolve(name, stack) + path = File.join(@dir, "#{name}.list") + unless File.exist?(path) + raise Error.new("no profile `#{name}`", + hint: "Profiles: #{Profile.names(@dir).join(", ")} (#{relative(@dir)}/.list)") + end + raise Error, "profile include cycle: #{(stack + [name]).join(" → ")}" if stack.include?(name) + + @files << path + File.readlines(path, chomp: true).each_with_index.reduce([]) do |packages, (line, index)| + line = line.sub(/#.*/, "").strip + next packages if line.empty? + + if line.start_with?("@") + directive(packages, line, stack + [name], "#{relative(path)}:#{index + 1}") + else + line.split.reduce(packages) do |acc, token| + token.start_with?("-") ? acc.reject { |p| File.fnmatch?(token[1..], p) } : acc | [token] + end + end + end + end + + def directive(packages, line, stack, where) + keyword, argument, *rest = line.split + unless keyword == "@include" && argument && rest.empty? + raise Error.new("#{where}: don't understand `#{line}`", hint: "The only directive is `@include `.") + end + + packages | resolve(argument, stack) + end + + def relative(path) = path.delete_prefix("#{ROOT}/") + end +end diff --git a/lib/sigma/qemu.rb b/lib/sigma/qemu.rb new file mode 100644 index 0000000..3ad5cdb --- /dev/null +++ b/lib/sigma/qemu.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require "open3" + +module Sigma + # Boots the built ISO in a VM, for a quick look at the result. + class Qemu + OVMF = %w[ + /usr/share/OVMF/OVMF.fd + /usr/share/ovmf/OVMF.fd + /usr/share/ovmf/x64/OVMF.fd + /usr/share/edk2/x64/OVMF.fd + /usr/share/qemu/OVMF.fd + ].freeze + + def initialize(config, ui) + @config = config + @ui = ui + end + + DISK_SIZE = "20G" + + # Boots the ISO, optionally with a virtual disk attached (created on first + # use) to try the installer on. With iso: false only the disk boots, + # which needs UEFI: setup-sigma installs GRUB for EFI only. + def boot(uefi: false, disk: nil, iso: true, extra: []) + raise Error.new("nothing to boot", hint: "--no-iso needs --disk.") unless iso || disk + raise Error.new("an installed system boots with UEFI only", hint: "Add --uefi.") if !iso && !uefi + + cmd = ["qemu-system-#{@config.arch}", "-smp", "4", "-m", "2048"] + if iso + image = @config.paths.iso + raise Error.new("no ISO at #{relative(image)}", hint: "Build one with `sigma build`.") unless File.exist?(image) + + cmd += ["-cdrom", image] + end + if disk + create_disk(disk) + # NVMe, like real hardware: virtio_blk isn't in the initramfs's + # module list, so an installed system on a virtio disk can't find /. + cmd += ["-drive", "file=#{disk},format=qcow2,if=none,id=sigmadisk", "-device", "nvme,serial=sigma,drive=sigmadisk"] + end + cmd += ["-enable-kvm"] if File.writable?("/dev/kvm") + cmd += ["-bios", firmware] if uefi + cmd += extra + + @ui.banner("Booting #{iso ? File.basename(@config.paths.iso) : File.basename(disk)}", + [uefi ? "UEFI" : "BIOS", File.writable?("/dev/kvm") ? "KVM" : "no KVM (slow)", + disk && iso ? "disk #{relative(disk)}" : nil].compact) + exec(*cmd) + rescue Errno::ENOENT => e + raise Error.new("#{e.message.split(" - ").last} not found", hint: "Install QEMU on this machine (apk add qemu-system-#{@config.arch} qemu-img).") + end + + private + + def firmware + OVMF.find { |path| File.exist?(path) } or + raise Error.new("no UEFI firmware found", hint: "Install OVMF (apk add ovmf), looked in: #{OVMF.join(", ")}") + end + + # A sparse qcow2 image: it only takes the space the guest writes. + def create_disk(path) + return if File.exist?(path) + + out, status = Open3.capture2e("qemu-img", "create", "-f", "qcow2", path, DISK_SIZE) + raise Error, "could not create #{path}: #{out.strip}" unless status.success? + + @ui.say " #{@ui.paint("created #{relative(path)} (#{DISK_SIZE}, sparse)", :muted)}" + end + + def relative(path) = path.delete_prefix("#{@config.root}/") + end +end diff --git a/lib/sigma/selection.rb b/lib/sigma/selection.rb new file mode 100644 index 0000000..7e92f7a --- /dev/null +++ b/lib/sigma/selection.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require "did_you_mean" + +module Sigma + # Parses what the user asked the pipeline to do. + # + # (nothing) every stage, incrementally (up-to-date ones skipped) + # rootfs one stage + # rootfs.. ..squashfs open ranges + # rootfs..iso closed range + # packages:a,b part of a stage (packages, or steps) + # + # Arguments combine; the result is always in pipeline order, and a stage + # asked for both whole and in part runs whole. + class Selection + Entry = Struct.new(:stage, :items) # items: nil means the whole stage + + attr_reader :entries + + def self.parse(args, stages) = new(args, stages) + + def initialize(args, stages) + @stages = stages + @incremental = args.empty? + wanted = {} + (args.empty? ? stages : []).each { |s| wanted[s] = nil } + args.each { |arg| add(wanted, arg) } + @entries = stages.filter_map { |s| Entry.new(s, wanted[s]) if wanted.key?(s) } + end + + # True for a bare `sigma build`: skip stages that are up to date. + def incremental? = @incremental + + def stages = entries.map(&:stage) + + private + + def add(wanted, arg) + if arg.include?(":") && arg.include?("..") + raise Error.new("`#{arg}` mixes a range with parts of a stage", hint: "Use separate arguments, e.g. `rootfs:configure squashfs..`.") + end + + case arg + when /\A([\w-]*)\.\.([\w-]*)\z/ + from = Regexp.last_match(1).empty? ? @stages.first : stage(Regexp.last_match(1)) + to = Regexp.last_match(2).empty? ? @stages.last : stage(Regexp.last_match(2)) + range = @stages[@stages.index(from)..@stages.index(to)] + raise Error.new("`#{arg}` is backwards", hint: "Stages run #{@stages.join(" → ")}.") if range.nil? || range.empty? + + range.each { |s| wanted[s] = nil } + when /\A([\w-]+):(.+)\z/ + name = stage(Regexp.last_match(1)) + items = Regexp.last_match(2).split(",").map(&:strip).reject(&:empty?) + raise Error, "`#{arg}`: nothing after the colon" if items.empty? + return if wanted.key?(name) && wanted[name].nil? # already whole + + wanted[name] = ((wanted[name] || []) + items).uniq + when /\A[\w-]+\z/ + wanted[stage(arg)] = nil + else + raise Error.new("can't read selection `#{arg}`", hint: "See `sigma build --help`.") + end + end + + def stage(name) + return name if @stages.include?(name) + + guess = DidYouMean::SpellChecker.new(dictionary: @stages).correct(name).first + raise Error.new("no stage `#{name}`#{guess ? ", did you mean `#{guess}`?" : ""}", + hint: "Stages: #{@stages.join(" → ")}") + end + end +end diff --git a/lib/sigma/shell.rb b/lib/sigma/shell.rb new file mode 100644 index 0000000..b0d2855 --- /dev/null +++ b/lib/sigma/shell.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +require "open3" + +module Sigma + # Runs external commands. Output never goes straight to the terminal: it is + # fed line by line to the running UI task, which writes it to the task's log + # and shows the latest line (or every line, in verbose mode). + class Shell + TAIL_LINES = 80 + + # A compiler-style diagnostic ("file.c:48:15: error: …", "error[E0425]: …") + # or a tool's "ERROR: …". Parallel make keeps building after one, so the + # real error can be thousands of lines above the end of the output. + ERROR_LINE = /\berror(?:\[\w+\])?: |\AERROR: / + # Lines GCC prints above a diagnostic, naming where it came from. + ERROR_CONTEXT = /\AIn file included from |\A\s+from |: In (?:member )?function |: At top level:/ + FIRST_ERROR_LINES = 15 + + def initialize(builder:) + @builder = builder + end + + def root? = Process.uid.zero? + + # Runs +cmd+ as part of +task+. Raises CommandFailed on a non-zero exit. + # + # as: :builder run as the unprivileged abuild user (only matters as root) + # input: string written to the command's stdin + # heavy: long CPU-bound work (compiling, compressing); runs at + # lower CPU priority so the machine stays usable + def run(task, *cmd, env: {}, chdir: nil, as: nil, input: nil, heavy: false) + cmd = cmd.flatten.map(&:to_s) + argv, spawn_env = wrap(heavy ? ["nice", "-n", "10", *cmd] : cmd, env, as) + task.log&.puts("$ #{display(cmd, env)}") + tail = [] + first_error = nil + + status = Open3.popen2e(spawn_env, *argv, **spawn_options(chdir)) do |stdin, out, wait| + stdin.write(input) if input + stdin.close + each_line(out) do |line| + task.output(line) + if first_error + first_error << line if first_error.size < FIRST_ERROR_LINES + elsif line.match?(ERROR_LINE) + context = tail.reverse.take_while { |l| l.match?(ERROR_CONTEXT) }.reverse + first_error = context.last(FIRST_ERROR_LINES - 1) << line + end + tail << line + tail.shift if tail.size > TAIL_LINES + end + wait.value + end + + return if status.success? + + # The log has the full command with its environment; the error keeps + # to the command itself. + raise CommandFailed.new(display(cmd, {}), status.exitstatus || status.termsig, + log_path: task.log&.path, tail: tail, first_error: first_error) + rescue Errno::ENOENT + raise Error.new("command not found: #{cmd.first}", + hint: "Is it installed? Building inside the container (the default) provides every tool.") + end + + # Runs a query and returns its stdout. For short commands whose output we + # parse; failures raise with stderr attached. + def capture(*cmd, env: {}, chdir: nil, as: nil) + cmd = cmd.flatten.map(&:to_s) + argv, spawn_env = wrap(cmd, env, as) + out, err, status = Open3.capture3(spawn_env, *argv, **spawn_options(chdir)) + return out if status.success? + + raise CommandFailed.new(display(cmd, env), status.exitstatus, tail: err.lines.last(TAIL_LINES).map(&:chomp)) + rescue Errno::ENOENT + raise Error, "command not found: #{cmd.first}" + end + + def available?(command) + ENV.fetch("PATH", "").split(":").any? { |dir| File.executable?(File.join(dir, command)) } + end + + private + + # Dropping privileges goes through doas (configured in the Dockerfile). + # doas resets the environment to the target user's, so ours is passed + # explicitly via env(1). + def wrap(cmd, env, as) + if as == :builder && root? + pairs = env.map { |k, v| "#{k}=#{v}" } + [["doas", "-u", @builder, "--", "env", *pairs, *cmd], {}] + else + [cmd, env.transform_keys(&:to_s).transform_values(&:to_s)] + end + end + + def spawn_options(chdir) + chdir ? { chdir: chdir.to_s } : {} + end + + # A copy-pasteable rendering of a command, quoted for humans. + def display(cmd, env) + (env.map { |k, v| "#{k}=#{quote(v.to_s)}" } + cmd.map { |c| quote(c) }).join(" ") + end + + def quote(arg) + return arg if arg.match?(%r{\A[\w./:=@%+,-]+\z}) + + "'#{arg.gsub("'") { %q('\\'') }}'" + end + + # Splits on both \n and \r so progress bars (curl, wget) become lines. + def each_line(io) + buffer = +"" + loop do + buffer << io.readpartial(65_536).force_encoding(Encoding::UTF_8).scrub + while (match = buffer.match(/\r\n|\r|\n/)) + # A trailing lone \r may be the first half of a \r\n still in flight. + break if match[0] == "\r" && match.end(0) == buffer.length + + yield buffer.slice!(0, match.end(0)).chomp + end + end + rescue EOFError + yield buffer unless buffer.empty? + end + end +end diff --git a/lib/sigma/signing.rb b/lib/sigma/signing.rb new file mode 100644 index 0000000..8e0b442 --- /dev/null +++ b/lib/sigma/signing.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "fileutils" +require "securerandom" + +module Sigma + # The key that signs the local repository. It lives in cache/keys/ so it + # survives container rebuilds, which means the image can verify our packages + # like any other instead of installing them --allow-untrusted. + class Signing + def initialize(context) + @context = context + @dir = context.paths.keys + end + + def private_key = Dir[File.join(@dir, "*.rsa")].min + def public_key = private_key && "#{private_key}.pub" + + # Creates the key pair on first use. apk finds a public key by the file + # name abuild-sign embeds, so the name must stay stable once generated. + def ensure(task) + return private_key if private_key + + FileUtils.mkdir_p(@dir) + @context.give_to_builder(@dir) + key = File.join(@dir, "#{@context.config.id}-#{SecureRandom.hex(4)}.rsa") + # Generated as the builder, who signs with it; private stays 0600. + @context.shell.run(task, "openssl", "genrsa", "-out", key, "4096", as: :builder) + @context.shell.run(task, "openssl", "rsa", "-in", key, "-pubout", "-out", "#{key}.pub", as: :builder) + File.chmod(0o600, key) + key + end + + # Lets this machine's apk (abuild -r installing local makedepends) trust + # the repository. Only possible, and only needed, as root in the container. + def trust_here + return unless @context.shell.root? && public_key + + FileUtils.mkdir_p("/etc/apk/keys") + FileUtils.cp(public_key, "/etc/apk/keys/") + end + end +end diff --git a/lib/sigma/stage.rb b/lib/sigma/stage.rb new file mode 100644 index 0000000..08d7ceb --- /dev/null +++ b/lib/sigma/stage.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +module Sigma + # A pipeline stage: a named list of steps, the inputs that decide whether it + # is up to date, and the outputs it leaves behind. + # + # A stage is fresh when its stamp matches its current inputs, it was built + # against the current output of the stage before it, and its outputs exist. + class Stage + class << self + attr_reader :stage_name, :description, :steps + + def stage(name, description, steps: []) + @stage_name = name + @description = description + @steps = steps + end + end + + attr_reader :context + + def initialize(context, pipeline) + @context = context + @pipeline = pipeline + end + + def name = self.class.stage_name + def description = self.class.description + def steps = self.class.steps + def config = context.config + def paths = context.paths + def ui = context.ui + def shell = context.shell + def state = @pipeline.state + + # -- freshness ------------------------------------------------------------ + + def key = "stage/#{name}" + def stamp = state.read(key) + def upstream = @pipeline.before(self) + + # What downstream stages record to notice that this one ran again. + def token = stamp&.fetch("token", nil) + + # Fingerprint of everything this stage reads, besides the stage before it. + def inputs = raise(NotImplementedError) + + # Paths that must exist for the stage to count as built. + def outputs = [] + + # [:fresh | :stale | :missing, reason] + def status + current = stamp + return [:missing, "never built"] unless current + return [:stale, "output missing"] unless outputs.all? { |p| File.exist?(p) } + return [:stale, "inputs changed"] if current["inputs"] != inputs + return [:stale, "#{upstream.name} changed"] if upstream && current["upstream"] != upstream.token + + [:fresh, nil] + end + + def fresh? = status.first == :fresh + + # -- running ---------------------------------------------------------------- + + # Raises unless +items+ name things this stage can do. + def validate(items) + unknown = items - steps.map(&:to_s) + return if unknown.empty? + + raise Error.new("#{name} has no step `#{unknown.first}`", hint: "Steps: #{steps.join(", ")}") + end + + # Short description of what +items+ will do, for --plan. + def plan(items) = (items || steps).join(", ") + + # Raises if what this stage consumes isn't there. + def check_prerequisites; end + + # Runs the selected steps (all by default) and returns a summary line. + def run(items, force: false) # rubocop:disable Lint/UnusedMethodArgument -- used by overrides + selected = (items || steps.map(&:to_s)).map(&:to_sym) + prior = stamp + started = ui.clock + around_steps(selected) do + selected.each { |step| context.task(name, step, label: step_label(step)) { |t| send("step_#{step}", t) } } + end + record(items.nil?, prior, ui.clock - started) + summary + end + + # Deletes what the stage produced, and its stamp. + def clean(_items = nil) + outputs.each { |path| Mounts.remove_tree(path) } + state.delete(key) + end + + protected + + def around_steps(_selected) = yield + def step_label(step) = step.to_s + def summary = nil + + # A whole run records the inputs it was built from. A partial run leaves + # the recorded inputs alone (the stage as a whole wasn't redone) but still + # takes a new token, so downstream stages see that something changed. + def record(whole, prior, duration) + if whole + state.write(key, "inputs" => inputs, "upstream" => upstream&.token, "duration" => duration) + elsif prior + state.write(key, prior.merge("token" => nil, "duration" => duration)) + end + end + + def relative(path) = path.to_s.delete_prefix("#{paths.root}/") + + def require_output(path, from) + return if File.exist?(path) + + raise Error.new("#{relative(path)} is missing", hint: "Build it first: `sigma build #{from}`.") + end + end +end diff --git a/lib/sigma/stages/iso.rb b/lib/sigma/stages/iso.rb new file mode 100644 index 0000000..85d28d7 --- /dev/null +++ b/lib/sigma/stages/iso.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require "erb" +require "fileutils" + +module Sigma + module Stages + # Assembles the bootable ISO in out/: the kernels and initramfs collected + # in cache/boot, the squashfs, and a GRUB menu with one entry per kernel. + class Iso < Stage + stage "iso", "bootable ISO", steps: %i[tree grub image] + + TEMPLATE = "image/grub.cfg.erb" + + # The boot menu theme ships in sigma-conf, so the live ISO and installed + # systems (through /etc/default/grub) share one source of truth. + THEME_DIR = "packages/sigma-conf/rootfs/usr/share/sigma/grub" + + Kernel = Struct.new(:flavor, :vmlinuz, :initramfs, :title, keyword_init: true) + + def tree = paths.iso_tree + def theme_dir = File.join(paths.root, THEME_DIR) + + def inputs + Fingerprint.of(config.name, config.dig("iso"), File.read(File.join(paths.root, TEMPLATE)), + Fingerprint.tree(theme_dir)) + end + + def outputs = [paths.iso] + + def clean(_items = nil) + Mounts.remove_tree(tree) + super + end + + def check_prerequisites + require_output(paths.squashfs, "squashfs") + return unless kernels.empty? + + raise Error.new("no kernel in #{relative(paths.boot)}", + hint: "The profile must install a kernel (linux-sigma); rebuild with `sigma build rootfs..`.") + end + + # Boot entries: the distribution's own kernel first, then any others. + def kernels + Dir[File.join(paths.boot, "vmlinuz-*")].map { |path| File.basename(path).delete_prefix("vmlinuz-") } + .select { |flavor| File.exist?(File.join(paths.boot, "initramfs-#{flavor}")) } + .sort_by { |flavor| [flavor == config.id ? 0 : 1, flavor] } + .then { |flavors| entries(flavors) } + end + + # Theme values from sigma-conf's grub.conf: plain KEY="value" lines. + def theme + File.readlines(File.join(theme_dir, "grub.conf"), chomp: true).each_with_object({}) do |line, vars| + next unless (match = line.match(/\A([A-Z_][A-Z0-9_]*)=(?:"([^"]*)"|'([^']*)'|(\S*))\s*(?:#.*)?\z/)) + + vars[match[1]] = match[2] || match[3] || match[4] + end + end + + protected + + def step_label(step) = { tree: "stage files", grub: "boot menu", image: "write image" }.fetch(step) + def summary = "#{relative(paths.iso)} · #{ui.bytes(File.size(paths.iso))}" + + def step_tree(task) + Mounts.remove_tree(tree) + FileUtils.mkdir_p(File.join(tree, "boot", "grub")) + shell.run(task, "cp", "-a", "#{paths.boot}/.", File.join(tree, "boot/")) + squashfs = File.join(tree, "rootfs.squashfs") + begin + File.link(paths.squashfs, squashfs) # same filesystem: no 2 GB copy + rescue SystemCallError + FileUtils.cp(paths.squashfs, squashfs) + end + task.detail = ui.plural(kernels.size, "kernel") + end + + def step_grub(task) + grub = File.join(tree, "boot", "grub") + FileUtils.mkdir_p(grub) + FileUtils.cp(File.join(theme_dir, "wallpaper.png"), File.join(grub, "wallpaper.png")) + File.write(File.join(grub, "grub.cfg"), render) + task.detail = kernels.map(&:flavor).join(", ") + end + + def step_image(task) + FileUtils.mkdir_p(File.dirname(paths.iso)) + shell.run(task, "grub-mkrescue", tree, "-o", paths.iso, "-volid", config.fetch("iso.volume_id"), heavy: true) + task.detail = ui.bytes(File.size(paths.iso)) + end + + private + + def entries(flavors) + flavors.map do |flavor| + Kernel.new(flavor: flavor, vmlinuz: "vmlinuz-#{flavor}", initramfs: "initramfs-#{flavor}", + title: flavors.size == 1 ? config.name : "#{config.name} (#{flavor})") + end + end + + def render + values = { name: config.name, volume_id: config.fetch("iso.volume_id"), kernels: kernels, theme: theme, + cmdline: theme.fetch("GRUB_CMDLINE_LINUX_DEFAULT", "") } + ERB.new(File.read(File.join(paths.root, TEMPLATE)), trim_mode: "-").result_with_hash(values) + end + end + end +end diff --git a/lib/sigma/stages/packages.rb b/lib/sigma/stages/packages.rb new file mode 100644 index 0000000..9f3b25e --- /dev/null +++ b/lib/sigma/stages/packages.rb @@ -0,0 +1,263 @@ +# frozen_string_literal: true + +require "fileutils" + +module Sigma + module Stages + # Builds the local packages into a signed repository at cache/repo/. + # + # Each package is tracked on its own: it rebuilds when anything in its + # packages// directory changes, or when its .apk files are missing. + # Packages build in a disposable copy (cache/build/), so abuild never + # writes into the source tree. + class Packages < Stage + stage "packages", "local APK repository" + + TARBALL = /\.(tar\.(gz|xz|zst|bz2)|tgz)\z/ + + def local + @local ||= LocalPackages.new(File.join(paths.root, "packages"), shell: shell, carch: config.arch) + end + + # The packages a run covers: the named ones, or what the profile needs. + def selected(items = nil) + items ? local.for_names(items) : local.for_profile(@pipeline.profile.packages) + end + + def validate(items) = local.for_names(items) + + def plan(items) + list = selected(items) + pending = list.reject { |pkg| package_status(pkg).first == :fresh } + "#{ui.plural(list.size, "package")} (#{pending.empty? ? "all up to date" : "to build: #{pending.map(&:name).join(", ")}"})" + end + + def inputs = Fingerprint.of(selected.map { |pkg| [pkg.name, fingerprint(pkg)] }) + + # Changes whenever a package the profile uses is rebuilt. + def token = Fingerprint.of(selected.map { |pkg| state.read(package_key(pkg))&.fetch("token", nil) }) + + def status + pending = selected.map { |pkg| [pkg, package_status(pkg)] }.reject { |_, (s, _)| s == :fresh } + return [:fresh, nil] if pending.empty? + return [:missing, "never built"] if pending.all? { |_, (s, _)| s == :missing } && pending.size == selected.size + + [:stale, "#{ui.plural(pending.size, "package")} to build"] + end + + def package_status(pkg) + stamp = state.read(package_key(pkg)) + return [:missing, "never built"] unless stamp + return [:stale, "#{stamp["version"]} → #{pkg.version}"] if stamp["version"] != pkg.version + return [:stale, "recipe changed"] if stamp["fingerprint"] != fingerprint(pkg) + return [:stale, "APKs missing"] unless stamp["apks"].all? { |apk| File.exist?(File.join(arch_dir, apk)) } + + [:fresh, nil] + end + + def run(items, force: false) + list = selected(items) + signing = Signing.new(context) + prepare_directories + context.task(name, "signing-key", label: "signing key") do |t| + had_key = signing.private_key + signing.ensure(t) + signing.trust_here + t.detail = had_key ? File.basename(signing.private_key) : "generated #{File.basename(signing.private_key)}" + end + + built = list.count do |pkg| + state_now, reason = package_status(pkg) + if state_now == :fresh && !force + ui.line(:skip, label(pkg), "up to date") + next false + end + + build(pkg, signing.private_key, reason: force && state_now == :fresh ? "forced" : reason) + true + end + + @summary = [built.positive? ? "#{built} built" : nil, "#{list.size - built} up to date"].compact.join(" · ") + @summary = ui.plural(list.size, "package") if built == list.size + install_hint(list) if items + @summary + end + + def clean(items = nil) + if items + validate(items) + items.map { |item| local[item] }.uniq.each do |pkg| + (state.read(package_key(pkg))&.fetch("apks", []) || []).each { |apk| FileUtils.rm_f(File.join(arch_dir, apk)) } + state.delete(package_key(pkg)) + end + else + [paths.repo, File.dirname(paths.staging)].each { |dir| FileUtils.rm_rf(dir) } + state.keys("package").each { |key| state.delete(key) } + state.delete(key) + end + end + + protected + + def summary = @summary + + private + + def label(pkg) = "#{pkg.name} #{pkg.version}" + def package_key(pkg) = "package/#{pkg.name}" + def arch_dir = File.join(config.repo_dir, config.arch) + + def fingerprint(pkg) = Fingerprint.of(Fingerprint.tree(pkg.dir), config.arch) + + def build(pkg, key, reason:) + context.task(name, pkg.name, label: label(pkg)) do |task| + started = ui.clock + task.detail = reason + dir = stage_copy(pkg, task) + task.detail = "checksum" + abuild(task, dir, key, "checksum") + task.detail = reason + before = ccache_counters + abuild(task, dir, key, "-r", "-f", *(config.dig("packages.keep_build_trees") ? ["-K"] : [])) + hits, calls = ccache_delta(before, ccache_counters) + apks = prune(pkg, task, dir, key) + state.write(package_key(pkg), "version" => pkg.version, "fingerprint" => fingerprint(pkg), + "apks" => apks, "duration" => ui.clock - started, + "ccache" => calls && { "hits" => hits, "calls" => calls }) + parts = ["built #{ui.bytes(apks.sum { |apk| File.size(File.join(arch_dir, apk)) })}"] + parts << ccache_rate(hits, calls) if calls&.positive? + task.detail = parts.join(" · ") + end + end + + # ccache's lifetime counters (`ccache --print-stats`), or nil when ccache + # is off or missing. Builds run one at a time, so the difference across a + # build is that build's. + def ccache_counters + return unless config.dig("packages.ccache") && shell.available?("ccache") + + out = shell.capture("ccache", "--print-stats", env: { "CCACHE_DIR" => paths.ccache }, as: :builder) + out.lines.to_h do |line| + name, value = line.chomp.split("\t", 2) + [name, value.to_i] + end + rescue CommandFailed + nil + end + + # [hits, cacheable compiles] between two snapshots; nil when unknown. + def ccache_delta(before, after) + return unless before && after + + change = ->(name) { after.fetch(name, 0) - before.fetch(name, 0) } + hits = change["direct_cache_hit"] + change["preprocessed_cache_hit"] + [hits, hits + change["cache_miss"]] + end + + def ccache_rate(hits, calls) + "ccache hit #{(100.0 * hits / calls).floor}% of #{ui.count(calls)}" + end + + # A fresh working copy in cache/build/sigma/. A source named + # foo.tar.gz with a sibling foo/ directory is packed here, reproducibly, + # which is how sigma-conf ships rootfs/ without per-package code. + def stage_copy(pkg, task) + dest = File.join(paths.staging, pkg.name) + FileUtils.rm_rf(dest) + FileUtils.copy_entry(pkg.dir, dest, true) + pkg.local_sources.each do |source| + next if File.exist?(File.join(dest, source)) || !source.match?(TARBALL) + + stem = source.sub(TARBALL, "") + next unless File.directory?(File.join(dest, stem)) + + compress = source.end_with?("gz") ? ["-I", "gzip -n"] : ["-a"] + shell.run(task, "tar", "--sort=name", "--mtime=@0", "--owner=0", "--group=0", "--numeric-owner", + *compress, "-cf", source, stem, chdir: dest) + end + context.give_to_builder(dest, recursive: true) + dest + end + + def abuild(task, dir, key, *args) + shell.run(task, "abuild", "-P", paths.repo, *args, chdir: dir, as: :builder, env: abuild_env(key), heavy: true) + end + + def abuild_env(key) + jobs = config.jobs + env = { + "SRCDEST" => paths.sources, + "REPODEST" => paths.repo, + "PACKAGER" => "#{config.name} builder", + "PACKAGER_PRIVKEY" => key, + "JOBS" => jobs, + "MAKEFLAGS" => "-j#{jobs}" + } + env.merge!(ccache_env) if config.dig("packages.ccache") + env + end + + # compiler_check=content: ccache tells compilers and GCC plugins apart by + # their contents, not their timestamps. The kernel compiles its plugins + # afresh in every build tree and loads them into nearly every compile, + # so with the default (mtime) every one of those compiles missed. + def ccache_env + { + "USE_CCACHE" => "1", + "CCACHE_DIR" => paths.ccache, + "CCACHE_COMPILERCHECK" => "content", + "CCACHE_MAXSIZE" => config.dig("packages.ccache_size") || "10G" + } + end + + # Keeps only this version's .apk files for the package's outputs and + # returns them; re-indexes when older versions were removed. + def prune(pkg, task, dir, key) + files = Dir.children(arch_dir).select { |f| f.end_with?(".apk") } + mine = files.select { |f| pkg.outputs.any? { |o| f.match?(/\A#{Regexp.escape(o)}-\d[^-]*-r\d+\.apk\z/) } } + current = mine.select { |f| pkg.outputs.any? { |o| f == "#{o}-#{pkg.version}.apk" } } + stale = mine - current + raise Error, "abuild finished but produced no APKs for #{pkg.name}" if current.empty? + return current if stale.empty? + + stale.each { |f| FileUtils.rm_f(File.join(arch_dir, f)) } + abuild(task, dir, key, "index") + current + end + + def prepare_directories + [paths.staging, paths.repo, paths.sources, paths.ccache].each do |dir| + FileUtils.mkdir_p(dir) + context.give_to_builder(dir) + end + context.give_to_builder(File.dirname(paths.staging)) + context.give_to_builder(File.dirname(paths.sources)) + share_apk_cache + end + + # In the container, point apk's cache at cache/downloads/apk: the + # makedepends `abuild -r` installs for each build then come from disk. + def share_apk_cache + return unless Docker.inside? && shell.root? + + FileUtils.mkdir_p(paths.apk_cache) + return if File.symlink?("/etc/apk/cache") && File.readlink("/etc/apk/cache") == paths.apk_cache + + FileUtils.rm_rf("/etc/apk/cache") + File.symlink(paths.apk_cache, "/etc/apk/cache") + end + + # After `sigma pkg`, say where the result is and how to use it here. + # The hint runs on the host, from the checkout: "$PWD" is expanded there + # (inside the container the checkout is /app). -u upgrades a package + # that is already installed instead of leaving the old version. + def install_hint(list) + names = list.map(&:name).join(" ") + repo = "\"$PWD/#{relative(config.repo_dir)}\"" + ui.note "repository #{relative(config.repo_dir)}/#{config.arch}/" + ui.note "install here, from the checkout: doas apk add -u --repository #{repo} #{names}" + ui.note "(trust the key once first: doas cp #{relative(paths.keys)}/*.rsa.pub /etc/apk/keys/)" + end + end + end +end diff --git a/lib/sigma/stages/rootfs.rb b/lib/sigma/stages/rootfs.rb new file mode 100644 index 0000000..33df07d --- /dev/null +++ b/lib/sigma/stages/rootfs.rb @@ -0,0 +1,180 @@ +# frozen_string_literal: true + +require "fileutils" + +module Sigma + module Stages + # Builds the root filesystem in cache/rootfs/, and collects what packages + # install into /boot (kernel, initramfs) in cache/boot/. + # + # Every run starts from an empty root: the configure step isn't idempotent + # (users, services), and with the apk download cache a fresh install takes + # about a minute. + class Rootfs < Stage + stage "rootfs", "root filesystem", steps: %i[prepare install configure] + + # Alpine's grub trigger regenerates grub.cfg whenever a kernel is + # installed. Inside the image build that's wasted work at best, so it is + # disabled while we build, and the guard is removed before shipping so + # installed systems keep regenerating their (themed) config. + GRUB_GUARD = "etc/update-grub.conf" + + def root = paths.rootfs + def profile = @pipeline.profile + def overlay = File.expand_path(config.fetch("rootfs.overlay"), paths.root) + def repositories = File.expand_path(config.fetch("rootfs.repositories"), paths.root) + + def inputs + Fingerprint.of(profile.packages, profile.files.map { |f| File.read(f) }, Fingerprint.tree(overlay), + File.read(repositories), config.dig("rootfs"), config.arch, config.id) + end + + def outputs = [root, paths.boot] + + def plan(items) + selected = (items || steps).map(&:to_s) + detail = selected.include?("install") ? " (#{profile.packages.size} packages, profile #{profile.name})" : "" + "#{selected.join(", ")}#{detail}" + end + + def check_prerequisites + return if @pipeline.stage("packages").selected.empty? + + require_output(File.join(config.repo_dir, config.arch, "APKINDEX.tar.gz"), "packages") + end + + def run(items, force: false) + unless shell.root? + raise Error.new("building the root filesystem needs root (mounts, chroot)", + hint: "Run it in the container (drop --native), or as root.") + end + if items && !items.include?("prepare") && !File.directory?(File.join(root, "etc")) + raise Error.new("there is no root filesystem to #{items.join(" and ")}", hint: "Run the whole stage: `sigma build rootfs`.") + end + + super + end + + def clean(_items = nil) + unless Mounts.under(root).empty? + context.task(name, "unmount", label: "unmount leftovers") { |t| Mounts.release(shell, t, root) } + end + super + end + + protected + + def step_label(step) + { prepare: "prepare", install: "install #{profile.packages.size} packages", configure: "configure" }.fetch(step) + end + + def summary = "#{ui.bytes(disk_usage(root))} root" + + def step_prepare(task) + Mounts.release(shell, task, root) # leftovers of an interrupted run + [root, paths.boot].each { |dir| Mounts.remove_tree(dir) } + FileUtils.mkdir_p([File.join(root, "etc/apk/keys"), paths.boot]) + + # Trust Alpine's keys (from this machine) and our repository's key, so + # every package is verified: no --allow-untrusted. + keys = Dir["/etc/apk/keys/*.pub"] + [Signing.new(context).public_key].compact + FileUtils.cp(keys, File.join(root, "etc/apk/keys")) + File.write(File.join(root, GRUB_GUARD), "disable_trigger=1\n") + + # Overlay files go in before packages too, so install-time triggers + # see them (the initramfs generator reads /etc/modules). + copy_overlay + task.detail = "#{ui.plural(keys.size, "trusted key")}" + end + + def step_install(task) + FileUtils.mkdir_p(paths.apk_cache) + chroot_mounts(task) do + shell.run(task, "apk", "add", "--root", root, "--initdb", "--arch", config.arch, "--update-cache", + "--repositories-file", repositories, "--repository", config.repo_dir, + "--cache-dir", paths.apk_cache, *profile.packages) + end + task.detail = task.last_line.to_s[/\AOK: (.*)/, 1] + end + + def step_configure(task) + chroot_mounts(task) do + copy_overlay(drop_apk_new: true) + File.write(File.join(root, "etc/hostname"), "#{config.id}\n") + FileUtils.cp(repositories, File.join(root, "etc/apk/repositories")) + + config.fetch("rootfs.skel_dirs").each { |dir| FileUtils.mkdir_p(File.join(root, "etc/skel", dir)) } + # Root gets a desktop-ready home too (logging in as root on the live ISO). + shell.run(task, "cp", "-a", "#{root}/etc/skel/.", "#{root}/root/") + + enable_services(task) + create_user(task) + + # https://alpinelinux.org/posts/2025-10-01-usr-merge.html + shell.run(task, "chroot", root, "merge-usr") + FileUtils.rm_f(File.join(root, GRUB_GUARD)) + end + end + + private + + def enable_services(task) + config.fetch("rootfs.services").each do |level, services| + services.each { |service| shell.run(task, "chroot", root, "rc-update", "add", service, level) } + end + end + + def create_user(task) + user = config.fetch("rootfs.user") + shell.run(task, "useradd", "-R", root, "-s", user.fetch("shell"), "-m", + "-G", user.fetch("groups").join(","), user.fetch("name")) + shell.run(task, "chroot", root, "chpasswd", input: "#{user.fetch("name")}:#{user.fetch("password")}\n") + end + + # Packages write to /dev and /proc while installing (e.g. /dev/null), and + # the kernel and initramfs land in /boot, which is cache/boot here. + def chroot_mounts(task) + Mounts.bind(shell, task, "/dev", File.join(root, "dev")) + Mounts.bind(shell, task, "/proc", File.join(root, "proc")) + Mounts.bind(shell, task, paths.boot, File.join(root, "boot")) + yield + ensure + begin + Mounts.release(shell, task, root) + rescue Error => e + ui.warn(e.message) + end + end + + # Files are written by us (so owned by root in the image, whatever owns + # the checkout) with their modes kept. With drop_apk_new, the .apk-new + # copies apk left next to them while installing are removed: the + # overlay wins. + def copy_overlay(drop_apk_new: false) + Dir.glob("**/*", File::FNM_DOTMATCH, base: overlay).sort.each do |relative| + next if relative.end_with?("/.", "/..") || relative == "." + + source = File.join(overlay, relative) + target = File.join(root, relative) + if File.symlink?(source) + FileUtils.rm_f(target) + File.symlink(File.readlink(source), target) + elsif File.directory?(source) + FileUtils.mkdir_p(target) + else + FileUtils.mkdir_p(File.dirname(target)) + FileUtils.rm_f(target) + FileUtils.install(source, target, mode: File.stat(source).mode & 0o7777) + FileUtils.rm_f("#{target}.apk-new") if drop_apk_new + end + end + end + + def disk_usage(path) + shell.capture("du", "-sk", path).split.first.to_i * 1024 + rescue CommandFailed + 0 + end + end + end +end diff --git a/lib/sigma/stages/squashfs.rb b/lib/sigma/stages/squashfs.rb new file mode 100644 index 0000000..f768467 --- /dev/null +++ b/lib/sigma/stages/squashfs.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "fileutils" + +module Sigma + module Stages + # Compresses the root filesystem into cache/rootfs.squashfs, which the + # live ISO boots from. + class Squashfs < Stage + stage "squashfs", "compressed root image", steps: %i[compress] + + def inputs = Fingerprint.of(config.dig("squashfs")) + def outputs = [paths.squashfs] + + def check_prerequisites + require_output(File.join(paths.rootfs, "etc", "apk", "world"), "rootfs") + return if Mounts.under(paths.rootfs).empty? + + raise Error.new("#{relative(paths.rootfs)} still has things mounted in it", + hint: "An earlier run was interrupted; `sigma build rootfs` cleans it up.") + end + + protected + + def step_label(_step) = "compress (#{config.fetch("squashfs.compression")})" + def summary = ui.bytes(File.size(paths.squashfs)) + + def step_compress(task) + FileUtils.mkdir_p(File.dirname(paths.squashfs)) + shell.run(task, "mksquashfs", paths.rootfs, paths.squashfs, "-noappend", + "-comp", config.fetch("squashfs.compression"), "-processors", config.jobs, + *Array(config.dig("squashfs.args")), heavy: true) + task.detail = ui.bytes(File.size(paths.squashfs)) + end + end + end +end diff --git a/lib/sigma/state.rb b/lib/sigma/state.rb new file mode 100644 index 0000000..33e4f02 --- /dev/null +++ b/lib/sigma/state.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "json" +require "fileutils" +require "securerandom" + +module Sigma + # Stamps recording what was built from what, as small JSON files under + # cache/state/. Keys look like "stage/rootfs" or "package/linux-sigma". + # + # Every stamp carries a `token` that changes whenever its producer runs; + # downstream stages record the token they were built against, which is how + # "rootfs ran again" makes squashfs stale even if rootfs's inputs didn't change. + class State + def initialize(dir) + @dir = dir + end + + def read(key) + path = path_for(key) + File.exist?(path) ? JSON.parse(File.read(path)) : nil + rescue JSON::ParserError + nil + end + + def write(key, data) + path = path_for(key) + FileUtils.mkdir_p(File.dirname(path)) + stamp = data.merge("token" => data["token"] || SecureRandom.hex(8), "finished_at" => Time.now.to_i) + File.write("#{path}.tmp", JSON.pretty_generate(stamp)) + File.rename("#{path}.tmp", path) + stamp + end + + def delete(key) + FileUtils.rm_f(path_for(key)) + end + + def keys(prefix) + Dir[File.join(@dir, prefix, "*.json")].map { |f| "#{prefix}/#{File.basename(f, ".json")}" } + end + + private + + def path_for(key) = File.join(@dir, "#{key}.json") + end +end diff --git a/lib/sigma/ui.rb b/lib/sigma/ui.rb new file mode 100644 index 0000000..84c5d9d --- /dev/null +++ b/lib/sigma/ui.rb @@ -0,0 +1,360 @@ +# frozen_string_literal: true + +require "io/console" +require "monitor" + +module Sigma + # Everything the user sees goes through here: colours, glyphs, stage frames, + # live task lines and boxes. It adapts to where it is writing: + # + # * an interactive terminal gets colour, a spinner and a live tail of the + # running command's output on a single line; + # * verbose mode streams every output line under its task instead; + # * pipes and CI logs get plain, append-only lines plus a heartbeat so long + # steps don't look hung. + class UI + SPINNER = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze + HEARTBEAT = 60 # seconds between "still running" lines when not on a TTY + LABEL_WIDTH = 30 + + # [truecolor RGB, 256-colour index] + PALETTE = { + accent: [[0, 255, 200], 49], # Sigma teal, colour 14 of the VT palette + ok: [[80, 250, 160], 85], + warn: [[255, 200, 87], 221], + error: [[255, 95, 95], 203], + muted: [[112, 122, 135], 244], + text: [[222, 226, 232], 254] + }.freeze + + GLYPHS = { + ok: "✓", fail: "✗", skip: "○", warn: "!", run: "▸", info: "·", + frame_top: "┌", frame_mid: "│", frame_end: "└" + }.freeze + + attr_reader :verbose + + def initialize(out: $stdout, verbose: false, color: nil) + @out = out + @tty = out.tty? + @verbose = verbose + @depth = detect_color_depth(color) + @lock = Monitor.new + @in_stage = false + end + + def tty? = @tty + def color? = @depth.positive? + + # -- primitives ----------------------------------------------------------- + + def paint(text, role = nil, bold: false, dim: false) + return text.to_s if @depth.zero? || (role.nil? && !bold && !dim) + + codes = [] + codes << "1" if bold + codes << "2" if dim + if role + rgb, index = PALETTE.fetch(role) + codes << (@depth == 24 ? "38;2;#{rgb.join(";")}" : "38;5;#{index}") + end + "\e[#{codes.join(";")}m#{text}\e[0m" + end + + def say(line = "") + @lock.synchronize { @out.puts(line) } + end + + def blank = say + + def width + return 100 unless @tty + + IO.console&.winsize&.last.then { |w| w.nil? || w.zero? ? 100 : w } + rescue StandardError + 100 + end + + # -- page furniture --------------------------------------------------------- + + def banner(title, details = []) + blank + say " #{paint("Σ", :accent, bold: true)} #{paint(title, :text, bold: true)}" + say " #{details.map { |d| paint(d, :muted) }.join(paint(" · ", :muted))}" unless details.empty? + blank + end + + # A framed group of tasks, e.g. one pipeline stage: + # + # ┌ packages · local APK repository + # │ ✓ initrdbo 0.5.3-r0 up to date + # └ 1 package · 3s + def stage(name, description = nil) + say " #{frame(:frame_top)} #{paint(name, :accent, bold: true)}" \ + "#{description ? paint(" · #{description}", :muted) : ""}" + @in_stage = true + started = clock + summary = yield + @in_stage = false + say " #{frame(:frame_end)} #{paint([summary, duration(clock - started)].compact.join(" · "), :muted)}" + blank + rescue Exception # rubocop:disable Lint/RescueException -- close the frame on Ctrl-C too + @in_stage = false + say " #{frame(:frame_end)} #{paint("failed", :error)}" + blank + raise + end + + def stage_skipped(name, reason) + say " #{paint(GLYPHS[:skip], :muted)} #{paint(name, :accent)}#{paint(" · #{reason}", :muted)}" + blank + end + + # A single line inside a stage frame, for things that are not tasks. + def line(glyph, label, note = nil, role: :muted) + say "#{prefix}#{paint(GLYPHS.fetch(glyph), role)} #{pad(label)}#{note ? paint(note, :muted) : ""}" + end + + def note(text) = say("#{prefix} #{paint(text, :muted)}") + def warn(text) = say("#{prefix}#{paint(GLYPHS[:warn], :warn, bold: true)} #{paint(text, :warn)}") + + # Key/value listing, used by `sigma config` and `sigma status`. + def table(rows, indent: 2) + key_width = rows.map { |k, _| k.to_s.length }.max || 0 + rows.each do |key, value| + say "#{" " * indent}#{paint(key.to_s.ljust(key_width), :muted)} #{value}" + end + end + + # A rounded box. Lines are truncated to the terminal width. + def box(lines, title: nil, role: :accent) + inner = [width - 6, 40].max + content = lines.flat_map { |l| l.to_s.empty? ? [""] : l.to_s.split("\n", -1) } + body_width = [content.map { |l| visible_length(l) }.max.to_i, visible_length(title.to_s) + 2].max + body_width = body_width.clamp(20, inner) + + top = title ? "╭─ #{title} #{"─" * [body_width - visible_length(title) - 1, 0].max}╮" : "╭#{"─" * (body_width + 2)}╮" + say " #{paint(top, role)}" + content.each do |l| + l = truncate(l, body_width) + say " #{paint("│", role)} #{l}#{" " * (body_width - visible_length(l))} #{paint("│", role)}" + end + say " #{paint("╰#{"─" * (body_width + 2)}╯", role)}" + end + + # -- tasks ------------------------------------------------------------------ + + # Runs the block as a named task with a live status line, and returns the + # block's value. The block receives the Task, which commands write their + # output through (see Shell). Exceptions mark the task failed and re-raise. + def task(label, log: nil) + task = Task.new(self, label, log) + task.start + result = yield task + task.finish + result + rescue Exception => e # rubocop:disable Lint/RescueException -- show the ✗ on Ctrl-C too + task&.fail(e) + raise + end + + # -- formatting helpers (public: stages use them for notes) ------------------ + + def duration(seconds) + seconds = seconds.to_f + return format("%.1fs", seconds) if seconds < 10 + return "#{seconds.round}s" if seconds < 60 + + minutes, secs = seconds.round.divmod(60) + return format("%dm %02ds", minutes, secs) if minutes < 60 + + hours, minutes = minutes.divmod(60) + format("%dh %02dm", hours, minutes) + end + + def bytes(count) + units = %w[B KB MB GB TB] + value = count.to_f + unit = units.shift + while value >= 1024 && !units.empty? + value /= 1024 + unit = units.shift + end + unit == "B" ? "#{count} B" : format("%.1f %s", value, unit) + end + + # 950, 18.2k, 1.3M + def count(number) + return number.to_s if number < 1000 + return format("%.1fk", number / 1000.0) if number < 999_950 + + format("%.1fM", number / 1_000_000.0) + end + + def plural(count, word, plural_word = "#{word}s") + "#{count} #{count == 1 ? word : plural_word}" + end + + # -- internals used by Task ------------------------------------------------- + + def prefix + @in_stage ? " #{frame(:frame_mid)} " : " " + end + + def pad(label, width = LABEL_WIDTH) + label = label.to_s + visible_length(label) >= width ? "#{label} " : label + " " * (width - visible_length(label)) + end + + def visible_length(text) + text.to_s.gsub(/\e\[[0-9;?]*[A-Za-z]/, "").length + end + + # Word-wraps plain text (no escape codes) to +max+ columns. + def wrap(text, max) + text.to_s.split("\n").flat_map do |paragraph| + paragraph.split(/(?<=\s)/).each_with_object([+""]) do |word, lines| + lines << +"" if !lines.last.empty? && (lines.last + word.rstrip).length > max + lines.last << word + end.map(&:rstrip) + end + end + + def truncate(text, max) + return text if visible_length(text) <= max + + plain = text.gsub(/\e\[[0-9;?]*[A-Za-z]/, "") + "#{plain[0, [max - 1, 0].max]}…" + end + + def synchronize(&) = @lock.synchronize(&) + def raw(text) = @out.print(text) + def flush = @out.flush + + def clock = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + private + + def frame(part) = paint(GLYPHS.fetch(part), :accent) + + def detect_color_depth(requested) + return 0 if requested == false + return 0 if ENV.key?("NO_COLOR") && !ENV["NO_COLOR"].empty? + return 0 unless @tty || ENV["FORCE_COLOR"] || requested + return 24 if ENV["COLORTERM"].to_s.match?(/truecolor|24bit/i) + + 256 + end + + # One running task: owns its status line and, when given, its log file. + class Task + attr_reader :label, :log + attr_accessor :detail + + def initialize(ui, label, log) + @ui = ui + @label = label + @log = log + @tail = nil + @detail = nil + @skipped = nil + end + + def start + @started = @ui.clock + if live? + @ui.synchronize { @ui.raw("\e[?25l") } # hide cursor while spinning + @spinner = Thread.new { spin } + else + @ui.say "#{@ui.prefix}#{@ui.paint(GLYPHS[:run], :accent)} #{@label}" + @heartbeat = Thread.new { beat } unless @ui.verbose + end + end + + # Called by Shell for every line a command prints. + def output(line) + @log&.puts(line) + clean = line.gsub(/\e\[[0-9;?]*[A-Za-z]/, "").strip + return if clean.empty? + + @tail = clean + @ui.say("#{@ui.prefix} #{@ui.paint("│", :muted)} #{@ui.paint(clean, :muted)}") if @ui.verbose + end + + # Mark the task as intentionally not done, with a reason ("up to date"). + def skip(reason) + @skipped = reason + end + + def elapsed = @ui.clock - @started + + # The most recent non-empty line of output, e.g. a command's summary. + def last_line = @tail + + def finish + stop_threads + if @skipped + render_final(@ui.paint(GLYPHS[:skip], :muted), @ui.paint(@skipped, :muted)) + else + note = [@detail, @ui.duration(elapsed)].compact.join(" ") + render_final(@ui.paint(GLYPHS[:ok], :ok, bold: true), @ui.paint(note, :muted)) + end + end + + def fail(error) + stop_threads + what = error.is_a?(Interrupt) ? "interrupted" : "failed" + render_final(@ui.paint(GLYPHS[:fail], :error, bold: true), + @ui.paint("#{what} after #{@ui.duration(elapsed)}", :error)) + end + + private + + def live? = @ui.tty? && !@ui.verbose + + def render_final(glyph, note) + @ui.synchronize do + @ui.raw("\r\e[2K\e[?25h") if live? + @ui.raw("#{@ui.prefix}#{glyph} #{@ui.pad(@label)}#{note}\n") + @ui.flush + end + end + + def spin + frame = 0 + loop do + render_live(SPINNER[frame % SPINNER.size]) + frame += 1 + sleep 0.08 + end + end + + def render_live(glyph) + head = "#{@ui.prefix}#{@ui.paint(glyph, :accent)} #{@ui.pad(@label)}" + meta = [@detail, @ui.duration(elapsed)].compact.join(" ") + line = "#{head}#{@ui.paint(meta, :muted)}" + room = @ui.width - @ui.visible_length(line) - 4 + line += " #{@ui.paint(@ui.truncate(@tail, room), :muted, dim: true)}" if @tail && room > 8 + @ui.synchronize do + @ui.raw("\r\e[2K#{line}") + @ui.flush + end + end + + def beat + loop do + sleep HEARTBEAT + @ui.say "#{@ui.prefix} #{@ui.paint("… #{@label}: still running (#{@ui.duration(elapsed)})", :muted)}" + end + end + + def stop_threads + [@spinner, @heartbeat].compact.each do |thread| + thread.kill + thread.join + end + @spinner = @heartbeat = nil + end + end + end +end diff --git a/src/apk/biblegen/APKBUILD b/packages/biblegen/APKBUILD similarity index 98% rename from src/apk/biblegen/APKBUILD rename to packages/biblegen/APKBUILD index 36d43dc..cfc254e 100644 --- a/src/apk/biblegen/APKBUILD +++ b/packages/biblegen/APKBUILD @@ -10,6 +10,7 @@ makedepends=" ruby ruby-dev ruby-bundler + openssl-dev " source="$pkgname-$pkgver.tar.gz::https://github.com/rdbo/biblegen/archive/refs/tags/$pkgver.tar.gz" diff --git a/src/apk/initrdbo/APKBUILD b/packages/initrdbo/APKBUILD similarity index 100% rename from src/apk/initrdbo/APKBUILD rename to packages/initrdbo/APKBUILD diff --git a/src/apk/initrdbo/initrdbo.trigger b/packages/initrdbo/initrdbo.trigger similarity index 100% rename from src/apk/initrdbo/initrdbo.trigger rename to packages/initrdbo/initrdbo.trigger diff --git a/src/apk/linux-sigma/APKBUILD b/packages/linux-sigma/APKBUILD similarity index 100% rename from src/apk/linux-sigma/APKBUILD rename to packages/linux-sigma/APKBUILD diff --git a/src/apk/linux-sigma/sigma.x86_64.config b/packages/linux-sigma/sigma.x86_64.config similarity index 99% rename from src/apk/linux-sigma/sigma.x86_64.config rename to packages/linux-sigma/sigma.x86_64.config index 65c852b..95809ab 100644 --- a/src/apk/linux-sigma/sigma.x86_64.config +++ b/packages/linux-sigma/sigma.x86_64.config @@ -3204,5 +3204,5 @@ CONFIG_FTRACE_SYSCALLS=y CONFIG_IO_STRICT_DEVMEM=y # CONFIG_X86_VERBOSE_BOOTUP is not set # CONFIG_EARLY_PRINTK is not set -CONFIG_X86_DECODER_SELFTEST=y +# CONFIG_X86_DECODER_SELFTEST is not set # CONFIG_RUNTIME_TESTING_MENU is not set diff --git a/src/apk/rdbo-archives/APKBUILD b/packages/rdbo-archives/APKBUILD similarity index 100% rename from src/apk/rdbo-archives/APKBUILD rename to packages/rdbo-archives/APKBUILD diff --git a/src/apk/rdbo-archives/archiver.rb b/packages/rdbo-archives/archiver.rb similarity index 100% rename from src/apk/rdbo-archives/archiver.rb rename to packages/rdbo-archives/archiver.rb diff --git a/src/apk/rdbo-archives/src/archive.rb b/packages/rdbo-archives/src/archive.rb similarity index 100% rename from src/apk/rdbo-archives/src/archive.rb rename to packages/rdbo-archives/src/archive.rb diff --git a/src/apk/rdbo-archives/src/archiver.rb b/packages/rdbo-archives/src/archiver.rb similarity index 100% rename from src/apk/rdbo-archives/src/archiver.rb rename to packages/rdbo-archives/src/archiver.rb diff --git a/src/apk/sigma-conf/APKBUILD b/packages/sigma-conf/APKBUILD similarity index 97% rename from src/apk/sigma-conf/APKBUILD rename to packages/sigma-conf/APKBUILD index 46e1fb0..49c0663 100644 --- a/src/apk/sigma-conf/APKBUILD +++ b/packages/sigma-conf/APKBUILD @@ -3,7 +3,7 @@ pkgname="sigma-conf" pkgver="1.0" -pkgrel=0 +pkgrel=2 pkgdesc="Sigma Linux configuration files" url="https://github.com/rdbo/sigma-linux" arch="noarch" diff --git a/src/apk/sigma-conf/rootfs/etc/modules-load.d/sigma.conf b/packages/sigma-conf/rootfs/etc/modules-load.d/sigma.conf similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/modules-load.d/sigma.conf rename to packages/sigma-conf/rootfs/etc/modules-load.d/sigma.conf diff --git a/src/apk/sigma-conf/rootfs/etc/polkit-1/rules.d/99-network.rules b/packages/sigma-conf/rootfs/etc/polkit-1/rules.d/99-network.rules similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/polkit-1/rules.d/99-network.rules rename to packages/sigma-conf/rootfs/etc/polkit-1/rules.d/99-network.rules diff --git a/src/apk/sigma-conf/rootfs/etc/profile.d/xdg.sh b/packages/sigma-conf/rootfs/etc/profile.d/xdg.sh similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/profile.d/xdg.sh rename to packages/sigma-conf/rootfs/etc/profile.d/xdg.sh diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.Xresources b/packages/sigma-conf/rootfs/etc/skel/.Xresources similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.Xresources rename to packages/sigma-conf/rootfs/etc/skel/.Xresources diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.bashrc b/packages/sigma-conf/rootfs/etc/skel/.bashrc similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.bashrc rename to packages/sigma-conf/rootfs/etc/skel/.bashrc diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/autostart/sigma.desktop b/packages/sigma-conf/rootfs/etc/skel/.config/autostart/sigma.desktop similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/autostart/sigma.desktop rename to packages/sigma-conf/rootfs/etc/skel/.config/autostart/sigma.desktop diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicAppletTime/v1/first_day_of_week b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicAppletTime/v1/first_day_of_week similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicAppletTime/v1/first_day_of_week rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicAppletTime/v1/first_day_of_week diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicAppletTime/v1/military_time b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicAppletTime/v1/military_time similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicAppletTime/v1/military_time rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicAppletTime/v1/military_time diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicAudio/v1/amplification_sink b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicAudio/v1/amplification_sink similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicAudio/v1/amplification_sink rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicAudio/v1/amplification_sink diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicAudio/v1/amplification_source b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicAudio/v1/amplification_source similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicAudio/v1/amplification_source rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicAudio/v1/amplification_source diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicBackground/v1/all b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicBackground/v1/all similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicBackground/v1/all rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicBackground/v1/all diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicBackground/v1/same-on-all b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicBackground/v1/same-on-all similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicBackground/v1/same-on-all rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicBackground/v1/same-on-all diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/autotile b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/autotile similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/autotile rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/autotile diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/autotile_behavior b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/autotile_behavior similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/autotile_behavior rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/autotile_behavior diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/focus_follows_cursor b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/focus_follows_cursor similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/focus_follows_cursor rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/focus_follows_cursor diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/focus_follows_cursor_delay b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/focus_follows_cursor_delay similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/focus_follows_cursor_delay rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/focus_follows_cursor_delay diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/xkb_config b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/xkb_config similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/xkb_config rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicComp/v1/xkb_config diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/anchor b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/anchor similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/anchor rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/anchor diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/anchor_gap b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/anchor_gap similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/anchor_gap rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/anchor_gap diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/autohide b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/autohide similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/autohide rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/autohide diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/autohover_delay_ms b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/autohover_delay_ms similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/autohover_delay_ms rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/autohover_delay_ms diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/background b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/background similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/background rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/background diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/border_radius b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/border_radius similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/border_radius rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/border_radius diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/exclusive_zone b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/exclusive_zone similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/exclusive_zone rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/exclusive_zone diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/expand_to_edges b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/expand_to_edges similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/expand_to_edges rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/expand_to_edges diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/keyboard_interactivity b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/keyboard_interactivity similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/keyboard_interactivity rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/keyboard_interactivity diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/layer b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/layer similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/layer rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/layer diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/margin b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/margin similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/margin rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/margin diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/name b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/name similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/name rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/name diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/opacity b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/opacity similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/opacity rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/opacity diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/output b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/output similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/output rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/output diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/padding b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/padding similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/padding rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/padding diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/padding_overlap b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/padding_overlap similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/padding_overlap rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/padding_overlap diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/plugins_center b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/plugins_center similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/plugins_center rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/plugins_center diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/plugins_wings b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/plugins_wings similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/plugins_wings rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/plugins_wings diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/size b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/size similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/size rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/size diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/size_center b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/size_center similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/size_center rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/size_center diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/size_wings b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/size_wings similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/size_wings rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/size_wings diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/spacing b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/spacing similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/spacing rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Dock/v1/spacing diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/anchor b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/anchor similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/anchor rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/anchor diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/anchor_gap b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/anchor_gap similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/anchor_gap rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/anchor_gap diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/autohide b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/autohide similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/autohide rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/autohide diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/autohover_delay_ms b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/autohover_delay_ms similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/autohover_delay_ms rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/autohover_delay_ms diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/background b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/background similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/background rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/background diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/border_radius b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/border_radius similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/border_radius rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/border_radius diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/exclusive_zone b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/exclusive_zone similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/exclusive_zone rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/exclusive_zone diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/expand_to_edges b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/expand_to_edges similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/expand_to_edges rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/expand_to_edges diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/keyboard_interactivity b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/keyboard_interactivity similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/keyboard_interactivity rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/keyboard_interactivity diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/layer b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/layer similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/layer rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/layer diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/margin b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/margin similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/margin rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/margin diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/name b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/name similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/name rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/name diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/opacity b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/opacity similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/opacity rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/opacity diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/output b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/output similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/output rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/output diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/padding b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/padding similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/padding rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/padding diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/padding_overlap b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/padding_overlap similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/padding_overlap rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/padding_overlap diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/plugins_center b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/plugins_center similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/plugins_center rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/plugins_center diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/plugins_wings b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/plugins_wings similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/plugins_wings rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/plugins_wings diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/size b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/size similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/size rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/size diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/size_center b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/size_center similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/size_center rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/size_center diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/size_wings b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/size_wings similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/size_wings rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/size_wings diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/spacing b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/spacing similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/spacing rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel.Panel/v1/spacing diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel/v1/entries b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel/v1/entries similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel/v1/entries rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPanel/v1/entries diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPortal/v1/screenshot b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPortal/v1/screenshot similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPortal/v1/screenshot rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicPortal/v1/screenshot diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicSettings.Shortcuts/v1/custom b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicSettings.Shortcuts/v1/custom similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicSettings.Shortcuts/v1/custom rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicSettings.Shortcuts/v1/custom diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicSettings.Shortcuts/v1/system_actions b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicSettings.Shortcuts/v1/system_actions similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicSettings.Shortcuts/v1/system_actions rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicSettings.Shortcuts/v1/system_actions diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicSettings.Wallpaper/v1/custom-images b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicSettings.Wallpaper/v1/custom-images similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicSettings.Wallpaper/v1/custom-images rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicSettings.Wallpaper/v1/custom-images diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark.Builder/v1/accent b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark.Builder/v1/accent similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark.Builder/v1/accent rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark.Builder/v1/accent diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark.Builder/v1/bg_color b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark.Builder/v1/bg_color similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark.Builder/v1/bg_color rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark.Builder/v1/bg_color diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark.Builder/v1/text_tint b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark.Builder/v1/text_tint similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark.Builder/v1/text_tint rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark.Builder/v1/text_tint diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/accent b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/accent similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/accent rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/accent diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/accent_button b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/accent_button similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/accent_button rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/accent_button diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/background b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/background similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/background rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/background diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/button b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/button similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/button rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/button diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/destructive b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/destructive similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/destructive rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/destructive diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/destructive_button b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/destructive_button similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/destructive_button rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/destructive_button diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/icon_button b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/icon_button similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/icon_button rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/icon_button diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/link_button b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/link_button similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/link_button rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/link_button diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/palette b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/palette similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/palette rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/palette diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/primary b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/primary similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/primary rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/primary diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/secondary b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/secondary similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/secondary rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/secondary diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/success b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/success similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/success rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/success diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/text_button b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/text_button similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/text_button rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/text_button diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/warning b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/warning similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/warning rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/warning diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/warning_button b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/warning_button similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/warning_button rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTheme.Dark/v1/warning_button diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTk/v1/interface_font b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTk/v1/interface_font similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTk/v1/interface_font rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTk/v1/interface_font diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTk/v1/monospace_font b/packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTk/v1/monospace_font similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTk/v1/monospace_font rename to packages/sigma-conf/rootfs/etc/skel/.config/cosmic/com.system76.CosmicTk/v1/monospace_font diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/foot/foot.ini b/packages/sigma-conf/rootfs/etc/skel/.config/foot/foot.ini similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/foot/foot.ini rename to packages/sigma-conf/rootfs/etc/skel/.config/foot/foot.ini diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/fuzzel/fuzzel.ini b/packages/sigma-conf/rootfs/etc/skel/.config/fuzzel/fuzzel.ini similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/fuzzel/fuzzel.ini rename to packages/sigma-conf/rootfs/etc/skel/.config/fuzzel/fuzzel.ini diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/gtk-3.0/settings.ini b/packages/sigma-conf/rootfs/etc/skel/.config/gtk-3.0/settings.ini similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/gtk-3.0/settings.ini rename to packages/sigma-conf/rootfs/etc/skel/.config/gtk-3.0/settings.ini diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/helix/config.toml b/packages/sigma-conf/rootfs/etc/skel/.config/helix/config.toml similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/helix/config.toml rename to packages/sigma-conf/rootfs/etc/skel/.config/helix/config.toml diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/helix/languages.toml b/packages/sigma-conf/rootfs/etc/skel/.config/helix/languages.toml similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/helix/languages.toml rename to packages/sigma-conf/rootfs/etc/skel/.config/helix/languages.toml diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/helix/runtime b/packages/sigma-conf/rootfs/etc/skel/.config/helix/runtime similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/helix/runtime rename to packages/sigma-conf/rootfs/etc/skel/.config/helix/runtime diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/helix/themes/sigma.toml b/packages/sigma-conf/rootfs/etc/skel/.config/helix/themes/sigma.toml similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/helix/themes/sigma.toml rename to packages/sigma-conf/rootfs/etc/skel/.config/helix/themes/sigma.toml diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/hypr/hyprland.conf b/packages/sigma-conf/rootfs/etc/skel/.config/hypr/hyprland.conf similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/hypr/hyprland.conf rename to packages/sigma-conf/rootfs/etc/skel/.config/hypr/hyprland.conf diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/keepassxc/keepassxc.ini b/packages/sigma-conf/rootfs/etc/skel/.config/keepassxc/keepassxc.ini similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/keepassxc/keepassxc.ini rename to packages/sigma-conf/rootfs/etc/skel/.config/keepassxc/keepassxc.ini diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/kitty/kitty.conf b/packages/sigma-conf/rootfs/etc/skel/.config/kitty/kitty.conf similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/kitty/kitty.conf rename to packages/sigma-conf/rootfs/etc/skel/.config/kitty/kitty.conf diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/luakit/userconf.lua b/packages/sigma-conf/rootfs/etc/skel/.config/luakit/userconf.lua similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/luakit/userconf.lua rename to packages/sigma-conf/rootfs/etc/skel/.config/luakit/userconf.lua diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/mako/config b/packages/sigma-conf/rootfs/etc/skel/.config/mako/config similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/mako/config rename to packages/sigma-conf/rootfs/etc/skel/.config/mako/config diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/mango/autostart.sh b/packages/sigma-conf/rootfs/etc/skel/.config/mango/autostart.sh similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/mango/autostart.sh rename to packages/sigma-conf/rootfs/etc/skel/.config/mango/autostart.sh diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/mango/config.conf b/packages/sigma-conf/rootfs/etc/skel/.config/mango/config.conf similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/mango/config.conf rename to packages/sigma-conf/rootfs/etc/skel/.config/mango/config.conf diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/neofetch/config.conf b/packages/sigma-conf/rootfs/etc/skel/.config/neofetch/config.conf similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/neofetch/config.conf rename to packages/sigma-conf/rootfs/etc/skel/.config/neofetch/config.conf diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/niri/config.kdl b/packages/sigma-conf/rootfs/etc/skel/.config/niri/config.kdl similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/niri/config.kdl rename to packages/sigma-conf/rootfs/etc/skel/.config/niri/config.kdl diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/rio/config.toml b/packages/sigma-conf/rootfs/etc/skel/.config/rio/config.toml similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/rio/config.toml rename to packages/sigma-conf/rootfs/etc/skel/.config/rio/config.toml diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/river/init b/packages/sigma-conf/rootfs/etc/skel/.config/river/init similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/river/init rename to packages/sigma-conf/rootfs/etc/skel/.config/river/init diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/sc-im/scimrc b/packages/sigma-conf/rootfs/etc/skel/.config/sc-im/scimrc similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/sc-im/scimrc rename to packages/sigma-conf/rootfs/etc/skel/.config/sc-im/scimrc diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/sigma/background b/packages/sigma-conf/rootfs/etc/skel/.config/sigma/background similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/sigma/background rename to packages/sigma-conf/rootfs/etc/skel/.config/sigma/background diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/sigma/startup.sh b/packages/sigma-conf/rootfs/etc/skel/.config/sigma/startup.sh similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/sigma/startup.sh rename to packages/sigma-conf/rootfs/etc/skel/.config/sigma/startup.sh diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/starship.toml b/packages/sigma-conf/rootfs/etc/skel/.config/starship.toml similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/starship.toml rename to packages/sigma-conf/rootfs/etc/skel/.config/starship.toml diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/swaylock/config b/packages/sigma-conf/rootfs/etc/skel/.config/swaylock/config similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/swaylock/config rename to packages/sigma-conf/rootfs/etc/skel/.config/swaylock/config diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/waybar/config.jsonc b/packages/sigma-conf/rootfs/etc/skel/.config/waybar/config.jsonc similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/waybar/config.jsonc rename to packages/sigma-conf/rootfs/etc/skel/.config/waybar/config.jsonc diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/waybar/style.css b/packages/sigma-conf/rootfs/etc/skel/.config/waybar/style.css similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/waybar/style.css rename to packages/sigma-conf/rootfs/etc/skel/.config/waybar/style.css diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/xfe/xferc b/packages/sigma-conf/rootfs/etc/skel/.config/xfe/xferc similarity index 99% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/xfe/xferc rename to packages/sigma-conf/rootfs/etc/skel/.config/xfe/xferc index faca848..4e16b80 100644 --- a/src/apk/sigma-conf/rootfs/etc/skel/.config/xfe/xferc +++ b/packages/sigma-conf/rootfs/etc/skel/.config/xfe/xferc @@ -127,7 +127,6 @@ ignore_case=1 sort_func=ascendingCase showthumbnails=0 name_size=200 -lastdir=/home/user hiddenfiles=0 [PROGS] @@ -155,7 +154,6 @@ ignore_case=1 sort_func=ascendingCase showthumbnails=0 name_size=19163 -lastdir=/home/user/Repos/typemaster hiddenfiles=0 [SETTINGS] diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.config/yambar/config.yml b/packages/sigma-conf/rootfs/etc/skel/.config/yambar/config.yml similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.config/yambar/config.yml rename to packages/sigma-conf/rootfs/etc/skel/.config/yambar/config.yml diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.gtkrc-2.0 b/packages/sigma-conf/rootfs/etc/skel/.gtkrc-2.0 similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.gtkrc-2.0 rename to packages/sigma-conf/rootfs/etc/skel/.gtkrc-2.0 diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.inputrc b/packages/sigma-conf/rootfs/etc/skel/.inputrc similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.inputrc rename to packages/sigma-conf/rootfs/etc/skel/.inputrc diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.mozilla/firefox/installs.ini b/packages/sigma-conf/rootfs/etc/skel/.mozilla/firefox/installs.ini similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.mozilla/firefox/installs.ini rename to packages/sigma-conf/rootfs/etc/skel/.mozilla/firefox/installs.ini diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.mozilla/firefox/profiles.ini b/packages/sigma-conf/rootfs/etc/skel/.mozilla/firefox/profiles.ini similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.mozilla/firefox/profiles.ini rename to packages/sigma-conf/rootfs/etc/skel/.mozilla/firefox/profiles.ini diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.mozilla/firefox/sigma.default-release/user.js b/packages/sigma-conf/rootfs/etc/skel/.mozilla/firefox/sigma.default-release/user.js similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.mozilla/firefox/sigma.default-release/user.js rename to packages/sigma-conf/rootfs/etc/skel/.mozilla/firefox/sigma.default-release/user.js diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.newsboat/config b/packages/sigma-conf/rootfs/etc/skel/.newsboat/config similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.newsboat/config rename to packages/sigma-conf/rootfs/etc/skel/.newsboat/config diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.profile b/packages/sigma-conf/rootfs/etc/skel/.profile similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.profile rename to packages/sigma-conf/rootfs/etc/skel/.profile diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.tmux.conf b/packages/sigma-conf/rootfs/etc/skel/.tmux.conf similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.tmux.conf rename to packages/sigma-conf/rootfs/etc/skel/.tmux.conf diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.vim/after/syntax/c.vim b/packages/sigma-conf/rootfs/etc/skel/.vim/after/syntax/c.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.vim/after/syntax/c.vim rename to packages/sigma-conf/rootfs/etc/skel/.vim/after/syntax/c.vim diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.vim/after/syntax/cpp.vim b/packages/sigma-conf/rootfs/etc/skel/.vim/after/syntax/cpp.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.vim/after/syntax/cpp.vim rename to packages/sigma-conf/rootfs/etc/skel/.vim/after/syntax/cpp.vim diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.vimrc b/packages/sigma-conf/rootfs/etc/skel/.vimrc similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.vimrc rename to packages/sigma-conf/rootfs/etc/skel/.vimrc diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.zlogin b/packages/sigma-conf/rootfs/etc/skel/.zlogin similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.zlogin rename to packages/sigma-conf/rootfs/etc/skel/.zlogin diff --git a/src/apk/sigma-conf/rootfs/etc/skel/.zshrc b/packages/sigma-conf/rootfs/etc/skel/.zshrc similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/skel/.zshrc rename to packages/sigma-conf/rootfs/etc/skel/.zshrc diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/themes/start/sigma/LICENSE b/packages/sigma-conf/rootfs/etc/vim/pack/themes/start/sigma/LICENSE similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/themes/start/sigma/LICENSE rename to packages/sigma-conf/rootfs/etc/vim/pack/themes/start/sigma/LICENSE diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/themes/start/sigma/README.md b/packages/sigma-conf/rootfs/etc/vim/pack/themes/start/sigma/README.md similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/themes/start/sigma/README.md rename to packages/sigma-conf/rootfs/etc/vim/pack/themes/start/sigma/README.md diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/themes/start/sigma/colors/sigma.vim b/packages/sigma-conf/rootfs/etc/vim/pack/themes/start/sigma/colors/sigma.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/themes/start/sigma/colors/sigma.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/themes/start/sigma/colors/sigma.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/.gitignore b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/.gitignore similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/.gitignore rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/.gitignore diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/.vintrc.yaml b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/.vintrc.yaml similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/.vintrc.yaml rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/.vintrc.yaml diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/CHANGELOG.md b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/CHANGELOG.md similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/CHANGELOG.md rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/CHANGELOG.md diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/LICENCE b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/LICENCE similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/LICENCE rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/LICENCE diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/README.markdown b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/README.markdown similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/README.markdown rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/README.markdown diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/_config.yml b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/_config.yml similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/_config.yml rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/_config.yml diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/autoload/nerdtree.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/autoload/nerdtree.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/autoload/nerdtree.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/autoload/nerdtree.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/autoload/nerdtree/ui_glue.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/autoload/nerdtree/ui_glue.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/autoload/nerdtree/ui_glue.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/autoload/nerdtree/ui_glue.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/doc/NERDTree.txt b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/doc/NERDTree.txt similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/doc/NERDTree.txt rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/doc/NERDTree.txt diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/bookmark.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/bookmark.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/bookmark.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/bookmark.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/creator.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/creator.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/creator.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/creator.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/event.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/event.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/event.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/event.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/flag_set.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/flag_set.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/flag_set.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/flag_set.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/key_map.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/key_map.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/key_map.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/key_map.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/menu_controller.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/menu_controller.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/menu_controller.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/menu_controller.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/menu_item.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/menu_item.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/menu_item.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/menu_item.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/nerdtree.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/nerdtree.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/nerdtree.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/nerdtree.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/notifier.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/notifier.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/notifier.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/notifier.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/opener.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/opener.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/opener.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/opener.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/path.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/path.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/path.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/path.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/tree_dir_node.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/tree_dir_node.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/tree_dir_node.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/tree_dir_node.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/tree_file_node.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/tree_file_node.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/tree_file_node.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/tree_file_node.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/ui.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/ui.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/ui.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/lib/nerdtree/ui.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/nerdtree_plugin/exec_menuitem.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/nerdtree_plugin/exec_menuitem.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/nerdtree_plugin/exec_menuitem.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/nerdtree_plugin/exec_menuitem.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/nerdtree_plugin/fs_menu.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/nerdtree_plugin/fs_menu.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/nerdtree_plugin/fs_menu.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/nerdtree_plugin/fs_menu.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/nerdtree_plugin/vcs.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/nerdtree_plugin/vcs.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/nerdtree_plugin/vcs.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/nerdtree_plugin/vcs.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/plugin/NERD_tree.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/plugin/NERD_tree.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/plugin/NERD_tree.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/plugin/NERD_tree.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/screenshot.png b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/screenshot.png similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/screenshot.png rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/screenshot.png diff --git a/src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/syntax/nerdtree.vim b/packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/syntax/nerdtree.vim similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/syntax/nerdtree.vim rename to packages/sigma-conf/rootfs/etc/vim/pack/vendor/start/nerdtree/syntax/nerdtree.vim diff --git a/src/apk/sigma-conf/rootfs/etc/vim/vimrc.local b/packages/sigma-conf/rootfs/etc/vim/vimrc.local similarity index 100% rename from src/apk/sigma-conf/rootfs/etc/vim/vimrc.local rename to packages/sigma-conf/rootfs/etc/vim/vimrc.local diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-audio b/packages/sigma-conf/rootfs/usr/bin/sigma-audio similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-audio rename to packages/sigma-conf/rootfs/usr/bin/sigma-audio diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-battery b/packages/sigma-conf/rootfs/usr/bin/sigma-battery similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-battery rename to packages/sigma-conf/rootfs/usr/bin/sigma-battery diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-change-kbdlayout b/packages/sigma-conf/rootfs/usr/bin/sigma-change-kbdlayout similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-change-kbdlayout rename to packages/sigma-conf/rootfs/usr/bin/sigma-change-kbdlayout diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-change-resolution b/packages/sigma-conf/rootfs/usr/bin/sigma-change-resolution similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-change-resolution rename to packages/sigma-conf/rootfs/usr/bin/sigma-change-resolution diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-cipher b/packages/sigma-conf/rootfs/usr/bin/sigma-cipher similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-cipher rename to packages/sigma-conf/rootfs/usr/bin/sigma-cipher diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-common b/packages/sigma-conf/rootfs/usr/bin/sigma-common similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-common rename to packages/sigma-conf/rootfs/usr/bin/sigma-common diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-cpu b/packages/sigma-conf/rootfs/usr/bin/sigma-cpu similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-cpu rename to packages/sigma-conf/rootfs/usr/bin/sigma-cpu diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-disk b/packages/sigma-conf/rootfs/usr/bin/sigma-disk similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-disk rename to packages/sigma-conf/rootfs/usr/bin/sigma-disk diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-dmenu b/packages/sigma-conf/rootfs/usr/bin/sigma-dmenu similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-dmenu rename to packages/sigma-conf/rootfs/usr/bin/sigma-dmenu diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-filemgr b/packages/sigma-conf/rootfs/usr/bin/sigma-filemgr similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-filemgr rename to packages/sigma-conf/rootfs/usr/bin/sigma-filemgr diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-launcher b/packages/sigma-conf/rootfs/usr/bin/sigma-launcher similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-launcher rename to packages/sigma-conf/rootfs/usr/bin/sigma-launcher diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-lock-screen b/packages/sigma-conf/rootfs/usr/bin/sigma-lock-screen similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-lock-screen rename to packages/sigma-conf/rootfs/usr/bin/sigma-lock-screen diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-memory b/packages/sigma-conf/rootfs/usr/bin/sigma-memory similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-memory rename to packages/sigma-conf/rootfs/usr/bin/sigma-memory diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-nwif b/packages/sigma-conf/rootfs/usr/bin/sigma-nwif similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-nwif rename to packages/sigma-conf/rootfs/usr/bin/sigma-nwif diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-nwusage b/packages/sigma-conf/rootfs/usr/bin/sigma-nwusage similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-nwusage rename to packages/sigma-conf/rootfs/usr/bin/sigma-nwusage diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-power-menu b/packages/sigma-conf/rootfs/usr/bin/sigma-power-menu similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-power-menu rename to packages/sigma-conf/rootfs/usr/bin/sigma-power-menu diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-screenshot b/packages/sigma-conf/rootfs/usr/bin/sigma-screenshot similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-screenshot rename to packages/sigma-conf/rootfs/usr/bin/sigma-screenshot diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-startup b/packages/sigma-conf/rootfs/usr/bin/sigma-startup similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-startup rename to packages/sigma-conf/rootfs/usr/bin/sigma-startup diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-term b/packages/sigma-conf/rootfs/usr/bin/sigma-term similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-term rename to packages/sigma-conf/rootfs/usr/bin/sigma-term diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-toggle-vkbd b/packages/sigma-conf/rootfs/usr/bin/sigma-toggle-vkbd similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-toggle-vkbd rename to packages/sigma-conf/rootfs/usr/bin/sigma-toggle-vkbd diff --git a/src/apk/sigma-conf/rootfs/usr/bin/sigma-vkbd b/packages/sigma-conf/rootfs/usr/bin/sigma-vkbd similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/bin/sigma-vkbd rename to packages/sigma-conf/rootfs/usr/bin/sigma-vkbd diff --git a/src/apk/sigma-conf/rootfs/usr/sbin/setup-sigma b/packages/sigma-conf/rootfs/usr/sbin/setup-sigma similarity index 86% rename from src/apk/sigma-conf/rootfs/usr/sbin/setup-sigma rename to packages/sigma-conf/rootfs/usr/sbin/setup-sigma index 89d9ae1..c3e9d8f 100755 --- a/src/apk/sigma-conf/rootfs/usr/sbin/setup-sigma +++ b/packages/sigma-conf/rootfs/usr/sbin/setup-sigma @@ -132,21 +132,19 @@ echo "Copying files..." cp -r -p /live/. "$newroot/" echo "Mounting pseudo filesystems on new root..." -mkdir -p "$newroot/dev" "$newroot/proc" -mount -t devtmpfs none "$newroot/dev" +mkdir -p "$newroot/dev" "$newroot/proc" "$newroot/sys" +# The live /dev itself, not a fresh devtmpfs: grub-mkconfig resolves the root +# device, which on encrypted installs is the /dev/mapper node cryptsetup made here. +mount --rbind /dev "$newroot/dev" mount -t proc proc "$newroot/proc" +mount -t sysfs sys "$newroot/sys" # grub-probe looks through device-mapper (LUKS) here echo "Setting up boot..." -arch="$(uname -m)" -chroot "$newroot" grub-install --target=x86_64-efi --boot-directory=/boot --efi-directory=/boot/efi --removable "$disk" -cp /cdrom/boot/grub/grub.cfg /cdrom/boot/grub/wallpaper.png "$newroot/boot/grub/" -# Patch GRUB config -sed -i -E 's|/boot||g' "$newroot/boot/grub/grub.cfg" -root_findfs="$(get_dev_id "$root_partition")" -sed -i -E "s|root=[^[:space:]]+|root=$root_findfs|" "$newroot/boot/grub/grub.cfg" -sed -i -E "s|rootfstype=[^[:space:]]+|rootfstype=ext4|" "$newroot/boot/grub/grub.cfg" -sed -i -E 's|[[:space:]]*rd\.live=[^[:space:]]+||' "$newroot/boot/grub/grub.cfg" cp /cdrom/boot/vmlinuz* /cdrom/boot/config* /cdrom/boot/initramfs* /cdrom/boot/System.map* "$newroot/boot/" +chroot "$newroot" grub-install --target=x86_64-efi --boot-directory=/boot --efi-directory=/boot/efi --removable "$disk" +# The menu is generated by grub-mkconfig and themed through /etc/default/grub, +# so the regeneration apk triggers on every kernel update keeps it intact. +chroot "$newroot" sigma-grub-setup --root "$root_partition" --fstype ext4 echo "Setting up /etc/fstab..." boot_id="$(get_dev_id "$boot_partition")" @@ -159,8 +157,9 @@ $efi_id /boot/efi vfat defaults 0 1 EOF echo "Unmounting partitions..." +umount "$newroot/sys" umount "$newroot/proc" -umount "$newroot/dev" +umount -R "$newroot/dev" umount "$newroot/boot/efi" umount "$newroot/boot" umount "$newroot" diff --git a/packages/sigma-conf/rootfs/usr/sbin/sigma-grub-setup b/packages/sigma-conf/rootfs/usr/sbin/sigma-grub-setup new file mode 100755 index 0000000..7966c70 --- /dev/null +++ b/packages/sigma-conf/rootfs/usr/sbin/sigma-grub-setup @@ -0,0 +1,159 @@ +#!/bin/sh + +# sigma-grub-setup: make grub-mkconfig produce the Sigma Linux boot menu. +# +# Writes /etc/default/grub so that every grub-mkconfig run, including the one +# apk triggers after each kernel update, keeps the Sigma theme and the kernel +# parameters initrdbo needs. Safe to re-run. It also migrates older installs, +# whose grub.cfg was copied from the ISO and got overwritten on kernel updates. +# +# usage: sigma-grub-setup [--root DEVICE --fstype TYPE] [--no-update] +# +# --root DEVICE partition holding the root filesystem (the LUKS container +# on encrypted installs); detected from / when omitted +# --fstype TYPE filesystem of the root (default: detected from /) +# --no-update write the configuration but don't run update-grub + +set -e + +THEME=/usr/share/sigma/grub +DEFAULTS=/etc/default/grub +MARKER="# Managed by sigma-grub-setup." +OVERRIDES="# Local overrides (kept when sigma-grub-setup runs again):" + +die() { + echo "sigma-grub-setup: $*" >&2 + exit 1 +} + +usage() { + sed -n '/^# usage:/,/^$/s/^# \{0,1\}//p' "$0" +} + +# The device and filesystem type mounted at /. The last entry wins: the +# initramfs's rootfs can be listed before the real root. +mounted_root() { + awk '$2 == "/" { dev = $1; fs = $3 } END { print dev, fs }' /proc/mounts +} + +# initrdbo opens LUKS containers itself, so root= must name the partition +# under a device-mapper device, not the mapped filesystem. +backing_device() { + dev="$(readlink -f "$1")" + name="${dev##*/}" + # With udev, /dev/mapper/ links to /dev/dm-N; without it, it's a + # device node of its own. Either way, find dm-N by the mapping's name. + if [ ! -e "/sys/class/block/$name" ]; then + for dm in /sys/class/block/dm-*; do + if [ "$(cat "$dm/dm/name" 2>/dev/null)" = "$name" ]; then + name="${dm##*/}" + break + fi + done + fi + for slave in "/sys/class/block/$name/slaves/"*; do + if [ -e "$slave" ]; then + echo "/dev/${slave##*/}" + return + fi + done + echo "$dev" +} + +# Busybox blkid has no -s/-o, so pick UUID="..." out of its output. +device_uuid() { + blkid "$1" | sed -n 's/.* UUID="\([^"]*\)".*/\1/p' +} + +root_dev="" +fstype="" +update=yes +while [ $# -gt 0 ]; do + case "$1" in + --root) root_dev="$2"; shift 2 ;; + --fstype) fstype="$2"; shift 2 ;; + --no-update) update=""; shift ;; + -h|--help) usage; exit 0 ;; + *) die "unknown option: $1 (see --help)" ;; + esac +done + +[ "$(id -u)" = 0 ] || die "run as root" +[ -r "$THEME/grub.conf" ] || die "$THEME/grub.conf is missing; is sigma-conf installed?" + +detected="" +if [ -z "$root_dev" ]; then + set -- $(mounted_root) + root_dev="$1" + [ -n "$fstype" ] || fstype="$2" + detected=yes +fi +[ -n "$fstype" ] || die "--fstype is required with --root" +mapped="$root_dev" +root_dev="$(backing_device "$root_dev")" +[ -b "$root_dev" ] || die "$root_dev is not a block device" +case "$root_dev" in + /dev/mapper/*|/dev/dm-*) + die "could not find the partition under $root_dev; pass it with --root" ;; +esac +uuid="$(device_uuid "$root_dev")" +[ -n "$uuid" ] || die "could not read the UUID of $root_dev" + +# On a running system, the root= it booted with is known to work: refuse to +# write a different one unless told which device to use. +booted="$(sed -n 's/.*root=UUID=\([^ ]*\).*/\1/p' /proc/cmdline)" +if [ -n "$detected" ] && [ -n "$booted" ] && [ "$booted" != "$uuid" ]; then + die "this system booted with root=UUID=$booted, but $root_dev (under $mapped) has UUID $uuid; nothing was changed. Pass --root DEVICE to choose." +fi + +# A hand-written /etc/default/grub is kept aside, once. +if [ -f "$DEFAULTS" ] && ! grep -qF "$MARKER" "$DEFAULTS"; then + cp "$DEFAULTS" "$DEFAULTS.pre-sigma" + echo "Saved the previous $DEFAULTS as $DEFAULTS.pre-sigma" +fi + +# Whatever was added below the overrides line survives a rewrite. +overrides="" +if [ -f "$DEFAULTS" ] && grep -qF "$OVERRIDES" "$DEFAULTS"; then + overrides="$(sed "1,/^$OVERRIDES\$/d" "$DEFAULTS")" +fi + +mkdir -p "${DEFAULTS%/*}" +cat > "$DEFAULTS.new" <<- EOF + $MARKER Running it again rewrites everything + # above the "Local overrides" line and keeps everything below it. + + # The Sigma Linux theme and kernel parameters, shared with the live ISO. + [ -r $THEME/grub.conf ] && . $THEME/grub.conf + + # initrdbo mounts the root filesystem itself: root= must name the partition + # (on encrypted installs, the LUKS container it unlocks), and rootfstype= + # must say how to mount it. + GRUB_DEVICE_UUID="$uuid" + GRUB_CMDLINE_LINUX="rootfstype=$fstype" + + $OVERRIDES +EOF +[ -z "$overrides" ] || printf '%s\n' "$overrides" >> "$DEFAULTS.new" +mv "$DEFAULTS.new" "$DEFAULTS" + +# GRUB reads the wallpaper before any kernel runs, so it has to live on /boot +# (which may be its own unencrypted partition). +mkdir -p /boot/grub +cp "$THEME/wallpaper.png" /boot/grub/wallpaper.png + +# Older installs carry files meant to stop apk's grub trigger: one under a +# name the trigger never reads (upgrade-grub.conf), so it never worked. The +# trigger is wanted now; it is what keeps the menu current. +if [ -f /etc/upgrade-grub.conf ] && ! grep -qv '^disable_trigger=' /etc/upgrade-grub.conf; then + rm -f /etc/upgrade-grub.conf +fi +if [ -f /etc/update-grub.conf ]; then + sed -i '/^disable_trigger=/d' /etc/update-grub.conf + [ -s /etc/update-grub.conf ] || rm -f /etc/update-grub.conf +fi + +echo "Configured GRUB: root=UUID=$uuid ($root_dev), rootfstype=$fstype" +if [ -n "$update" ]; then + update-grub +fi diff --git a/src/apk/sigma-conf/rootfs/usr/share/icons/default/index.theme b/packages/sigma-conf/rootfs/usr/share/icons/default/index.theme similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/share/icons/default/index.theme rename to packages/sigma-conf/rootfs/usr/share/icons/default/index.theme diff --git a/src/apk/sigma-conf/rootfs/usr/share/plymouth/themes/sigma/image.png b/packages/sigma-conf/rootfs/usr/share/plymouth/themes/sigma/image.png similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/share/plymouth/themes/sigma/image.png rename to packages/sigma-conf/rootfs/usr/share/plymouth/themes/sigma/image.png diff --git a/src/apk/sigma-conf/rootfs/usr/share/plymouth/themes/sigma/sigma.plymouth b/packages/sigma-conf/rootfs/usr/share/plymouth/themes/sigma/sigma.plymouth similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/share/plymouth/themes/sigma/sigma.plymouth rename to packages/sigma-conf/rootfs/usr/share/plymouth/themes/sigma/sigma.plymouth diff --git a/src/apk/sigma-conf/rootfs/usr/share/plymouth/themes/sigma/sigma.script b/packages/sigma-conf/rootfs/usr/share/plymouth/themes/sigma/sigma.script similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/share/plymouth/themes/sigma/sigma.script rename to packages/sigma-conf/rootfs/usr/share/plymouth/themes/sigma/sigma.script diff --git a/src/apk/sigma-conf/rootfs/usr/share/sigma/ascii/sigma b/packages/sigma-conf/rootfs/usr/share/sigma/ascii/sigma similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/share/sigma/ascii/sigma rename to packages/sigma-conf/rootfs/usr/share/sigma/ascii/sigma diff --git a/src/apk/sigma-conf/rootfs/usr/share/sigma/ascii/sigma-linux-logo b/packages/sigma-conf/rootfs/usr/share/sigma/ascii/sigma-linux-logo similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/share/sigma/ascii/sigma-linux-logo rename to packages/sigma-conf/rootfs/usr/share/sigma/ascii/sigma-linux-logo diff --git a/src/apk/sigma-conf/rootfs/usr/share/sigma/backgrounds/wallpaper.png b/packages/sigma-conf/rootfs/usr/share/sigma/backgrounds/wallpaper.png similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/share/sigma/backgrounds/wallpaper.png rename to packages/sigma-conf/rootfs/usr/share/sigma/backgrounds/wallpaper.png diff --git a/packages/sigma-conf/rootfs/usr/share/sigma/grub/grub.conf b/packages/sigma-conf/rootfs/usr/share/sigma/grub/grub.conf new file mode 100644 index 0000000..61456d8 --- /dev/null +++ b/packages/sigma-conf/rootfs/usr/share/sigma/grub/grub.conf @@ -0,0 +1,34 @@ +# Sigma Linux boot menu: theme and kernel parameters. +# +# One source of truth for both boot menus: +# * the live ISO's grub.cfg is rendered from it at build time; +# * installed systems source it from /etc/default/grub (written by +# sigma-grub-setup), so every grub-mkconfig run, including the one apk +# triggers on kernel updates, keeps the Sigma look. +# +# Keep to plain KEY="value" lines: the ISO build reads this file without a +# shell. Per-machine overrides belong in /etc/default/grub, after the line +# that sources this file. + +GRUB_DISTRIBUTOR="Sigma Linux" +GRUB_TIMEOUT=5 +GRUB_GFXMODE=640x480 +GRUB_TERMINAL_OUTPUT=gfxterm +GRUB_FONT=/boot/grub/fonts/unicode.pf2 +GRUB_BACKGROUND=/boot/grub/wallpaper.png +GRUB_DISABLE_RECOVERY=true + +# The Sigma kernel is the plain "Sigma Linux" entry; any other installed +# kernels go under "Advanced options for Sigma Linux". +GRUB_TOP_LEVEL=/boot/vmlinuz-sigma + +# vt.*: the console colour palette, one value per colour 0-15. +# Linux's defaults, for reference: +# red: 0x00,0xaa,0x00,0xaa,0x00,0xaa,0x00,0xaa,0x55,0xff,0x55,0xff,0x55,0xff,0x55,0xff +# grn: 0x00,0x00,0xaa,0x55,0x00,0x00,0xaa,0xaa,0x55,0x55,0xff,0xff,0x55,0x55,0xff,0xff +# blu: 0x00,0x00,0x00,0x00,0xaa,0xaa,0xaa,0xaa,0x55,0x55,0x55,0x55,0xff,0xff,0xff,0xff +# The table is not in ANSI order. From drivers/tty/vt/vt.c: +# const unsigned char color_table[] = { 0, 4, 2, 6, 1, 5, 3, 7, +# 8,12,10,14, 9,13,11,15 }; +# so colour "3" takes the RGB values at index 6, for example. +GRUB_CMDLINE_LINUX_DEFAULT="vt.color=0x0B vt.default_red=0x10,0xaa,0x00,0xaa,0x00,0xaa,0x00,0xaa,0x55,0xff,0x55,0xff,0x55,0xff,0x00,0xff vt.default_grn=0x1a,0x00,0xaa,0x55,0x00,0x00,0xaa,0xaa,0x55,0x55,0xff,0xff,0x55,0x55,0xff,0xff vt.default_blu=0x20,0x00,0x00,0x00,0xaa,0xaa,0xaa,0xaa,0x55,0x55,0x55,0x55,0xff,0xff,0xc8,0xff splash" diff --git a/src/grub_wallpaper.png b/packages/sigma-conf/rootfs/usr/share/sigma/grub/wallpaper.png similarity index 100% rename from src/grub_wallpaper.png rename to packages/sigma-conf/rootfs/usr/share/sigma/grub/wallpaper.png diff --git a/src/apk/sigma-conf/rootfs/usr/share/sigma/lore.cipher b/packages/sigma-conf/rootfs/usr/share/sigma/lore.cipher similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/share/sigma/lore.cipher rename to packages/sigma-conf/rootfs/usr/share/sigma/lore.cipher diff --git a/src/apk/sigma-conf/rootfs/usr/share/sigma/luakit-welcome/index.html b/packages/sigma-conf/rootfs/usr/share/sigma/luakit-welcome/index.html similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/share/sigma/luakit-welcome/index.html rename to packages/sigma-conf/rootfs/usr/share/sigma/luakit-welcome/index.html diff --git a/src/apk/sigma-conf/rootfs/usr/share/sigma/luakit-welcome/sigma-logo-min.svg b/packages/sigma-conf/rootfs/usr/share/sigma/luakit-welcome/sigma-logo-min.svg similarity index 100% rename from src/apk/sigma-conf/rootfs/usr/share/sigma/luakit-welcome/sigma-logo-min.svg rename to packages/sigma-conf/rootfs/usr/share/sigma/luakit-welcome/sigma-logo-min.svg diff --git a/src/apk/sigma-dwl/APKBUILD b/packages/sigma-dwl/APKBUILD similarity index 100% rename from src/apk/sigma-dwl/APKBUILD rename to packages/sigma-dwl/APKBUILD diff --git a/src/apk/sigma-dwl/config.h b/packages/sigma-dwl/config.h similarity index 100% rename from src/apk/sigma-dwl/config.h rename to packages/sigma-dwl/config.h diff --git a/src/apk/sigma-firacode-nerd/APKBUILD b/packages/sigma-firacode-nerd/APKBUILD similarity index 100% rename from src/apk/sigma-firacode-nerd/APKBUILD rename to packages/sigma-firacode-nerd/APKBUILD diff --git a/src/apk/sigma-firacode-nerd/firacode.tar.xz b/packages/sigma-firacode-nerd/firacode.tar.xz similarity index 100% rename from src/apk/sigma-firacode-nerd/firacode.tar.xz rename to packages/sigma-firacode-nerd/firacode.tar.xz diff --git a/src/apk/sigma-flat-remix-gtk/APKBUILD b/packages/sigma-flat-remix-gtk/APKBUILD similarity index 100% rename from src/apk/sigma-flat-remix-gtk/APKBUILD rename to packages/sigma-flat-remix-gtk/APKBUILD diff --git a/src/apk/sigma-flat-remix-gtk/flat-remix-gtk-green-dark.tar.xz b/packages/sigma-flat-remix-gtk/flat-remix-gtk-green-dark.tar.xz similarity index 100% rename from src/apk/sigma-flat-remix-gtk/flat-remix-gtk-green-dark.tar.xz rename to packages/sigma-flat-remix-gtk/flat-remix-gtk-green-dark.tar.xz diff --git a/src/apk/sigma-material-symbols/APKBUILD b/packages/sigma-material-symbols/APKBUILD similarity index 100% rename from src/apk/sigma-material-symbols/APKBUILD rename to packages/sigma-material-symbols/APKBUILD diff --git a/src/apk/sigma-material-symbols/material-symbols.tar.xz b/packages/sigma-material-symbols/material-symbols.tar.xz similarity index 100% rename from src/apk/sigma-material-symbols/material-symbols.tar.xz rename to packages/sigma-material-symbols/material-symbols.tar.xz diff --git a/src/apk/sigma-sent/APKBUILD b/packages/sigma-sent/APKBUILD similarity index 100% rename from src/apk/sigma-sent/APKBUILD rename to packages/sigma-sent/APKBUILD diff --git a/src/apk/sigma-sent/sent/LICENSE b/packages/sigma-sent/sent/LICENSE similarity index 100% rename from src/apk/sigma-sent/sent/LICENSE rename to packages/sigma-sent/sent/LICENSE diff --git a/src/apk/sigma-sent/sent/Makefile b/packages/sigma-sent/sent/Makefile similarity index 100% rename from src/apk/sigma-sent/sent/Makefile rename to packages/sigma-sent/sent/Makefile diff --git a/src/apk/sigma-sent/sent/README.md b/packages/sigma-sent/sent/README.md similarity index 100% rename from src/apk/sigma-sent/sent/README.md rename to packages/sigma-sent/sent/README.md diff --git a/src/apk/sigma-sent/sent/arg.h b/packages/sigma-sent/sent/arg.h similarity index 100% rename from src/apk/sigma-sent/sent/arg.h rename to packages/sigma-sent/sent/arg.h diff --git a/src/apk/sigma-sent/sent/config.def.h b/packages/sigma-sent/sent/config.def.h similarity index 100% rename from src/apk/sigma-sent/sent/config.def.h rename to packages/sigma-sent/sent/config.def.h diff --git a/src/apk/sigma-sent/sent/config.h b/packages/sigma-sent/sent/config.h similarity index 100% rename from src/apk/sigma-sent/sent/config.h rename to packages/sigma-sent/sent/config.h diff --git a/src/apk/sigma-sent/sent/config.mk b/packages/sigma-sent/sent/config.mk similarity index 100% rename from src/apk/sigma-sent/sent/config.mk rename to packages/sigma-sent/sent/config.mk diff --git a/src/apk/sigma-sent/sent/drw.c b/packages/sigma-sent/sent/drw.c similarity index 100% rename from src/apk/sigma-sent/sent/drw.c rename to packages/sigma-sent/sent/drw.c diff --git a/src/apk/sigma-sent/sent/drw.h b/packages/sigma-sent/sent/drw.h similarity index 100% rename from src/apk/sigma-sent/sent/drw.h rename to packages/sigma-sent/sent/drw.h diff --git a/src/apk/sigma-sent/sent/example b/packages/sigma-sent/sent/example similarity index 100% rename from src/apk/sigma-sent/sent/example rename to packages/sigma-sent/sent/example diff --git a/src/apk/sigma-sent/sent/nyan.png b/packages/sigma-sent/sent/nyan.png similarity index 100% rename from src/apk/sigma-sent/sent/nyan.png rename to packages/sigma-sent/sent/nyan.png diff --git a/src/apk/sigma-sent/sent/sent.1 b/packages/sigma-sent/sent/sent.1 similarity index 100% rename from src/apk/sigma-sent/sent/sent.1 rename to packages/sigma-sent/sent/sent.1 diff --git a/src/apk/sigma-sent/sent/sent.c b/packages/sigma-sent/sent/sent.c similarity index 100% rename from src/apk/sigma-sent/sent/sent.c rename to packages/sigma-sent/sent/sent.c diff --git a/src/apk/sigma-sent/sent/transparent_test.ff b/packages/sigma-sent/sent/transparent_test.ff similarity index 100% rename from src/apk/sigma-sent/sent/transparent_test.ff rename to packages/sigma-sent/sent/transparent_test.ff diff --git a/src/apk/sigma-sent/sent/util.c b/packages/sigma-sent/sent/util.c similarity index 100% rename from src/apk/sigma-sent/sent/util.c rename to packages/sigma-sent/sent/util.c diff --git a/src/apk/sigma-sent/sent/util.h b/packages/sigma-sent/sent/util.h similarity index 100% rename from src/apk/sigma-sent/sent/util.h rename to packages/sigma-sent/sent/util.h diff --git a/src/apk/sigma-st/APKBUILD b/packages/sigma-st/APKBUILD similarity index 100% rename from src/apk/sigma-st/APKBUILD rename to packages/sigma-st/APKBUILD diff --git a/src/apk/sigma-vt323/APKBUILD b/packages/sigma-vt323/APKBUILD similarity index 100% rename from src/apk/sigma-vt323/APKBUILD rename to packages/sigma-vt323/APKBUILD diff --git a/src/apk/sigma-vt323/VT323.tar.xz b/packages/sigma-vt323/VT323.tar.xz similarity index 100% rename from src/apk/sigma-vt323/VT323.tar.xz rename to packages/sigma-vt323/VT323.tar.xz diff --git a/src/apk/sigma-wvkbd/APKBUILD b/packages/sigma-wvkbd/APKBUILD similarity index 100% rename from src/apk/sigma-wvkbd/APKBUILD rename to packages/sigma-wvkbd/APKBUILD diff --git a/src/apk/simplex-chat/APKBUILD b/packages/simplex-chat/APKBUILD similarity index 100% rename from src/apk/simplex-chat/APKBUILD rename to packages/simplex-chat/APKBUILD diff --git a/profiles/full.list b/profiles/full.list new file mode 100644 index 0000000..8b5a222 --- /dev/null +++ b/profiles/full.list @@ -0,0 +1,73 @@ +# full: standard plus office, creative, virtualization and security tools +# +# Syntax: one or more packages per line, # starts a comment, +# `@include ` pulls in another profile, `-pattern` removes +# packages (globs allowed) added before it. + +@include standard + +# All firmware instead of the curated subset +-linux-firmware-* +linux-firmware + +# Every tree-sitter grammar instead of helix's vendored set +-helix-tree-sitter-vendor + +# System + +## Administration Tools +yt-dlp +lighttpd + +#--------------------------------# + +# Desktop Environment + +## Text Editing +tree-sitter-grammars + +## Office +zathura zathura-pdf-mupdf +libreoffice libreoffice-gtk + +## Mail +neomutt +thunderbird + +## RSS +newsboat + +## Multimedia +gimp +inkscape +shotcut +lmms + +## Chat +simplex-chat + +## Monitoring +wireshark tshark + +## Misc +electrum +qemu-ui-gtk qemu-system-x86_64 qemu-system-aarch64 ovmf + +#--------------------------------# + +# 1337 h@xx0r + +aircrack-ng +nmap nmap-ncat +rustscan +ghidra +zaproxy +mitmproxy +hashcat +john +hydra +radare2 +sqlmap +recon-ng +bettercap +jadx apktool diff --git a/src/pkglist.standard b/profiles/minimal.list similarity index 69% rename from src/pkglist.standard rename to profiles/minimal.list index 7273d15..4effd13 100644 --- a/src/pkglist.standard +++ b/profiles/minimal.list @@ -1,3 +1,9 @@ +# minimal: the base system and desktop, nothing extra +# +# Syntax: one or more packages per line, # starts a comment, +# `@include ` pulls in another profile, `-pattern` removes +# packages (globs allowed) added before it. + # Core Packages ## Core @@ -71,32 +77,11 @@ linux-firmware-mwlwifi ## Administration Tools bash starship doas -openssh -curl wget -gpg -patch entr -7zip xfsprogs e2fsprogs btrfs-progs dosfstools ntfs-3g -# yt-dlp tar xz zstd zip unzip gzip -# lighttpd -rsync -diffutils -tmux tmux-doc -jq -ripgrep wireless-tools -btop -bandwhich -timeshift -flatpak -bubblewrap -bc cpupower polkit -argon2 -delta -just ## Suspend/Hibernate acpid zzz @@ -108,7 +93,6 @@ earlyoom zram-init wpa_supplicant dhcpcd iwd networkmanager networkmanager-wifi networkmanager-wwan networkmanager-dnsmasq networkmanager-cli networkmanager-tui -openvpn wireguard-tools ## Audio pipewire pipewire-pulse pipewire-alsa pipewire-jack @@ -118,9 +102,6 @@ easyeffects calf-lv2 lsp-plugins-lv2 mda-lv2 zam-plugins-lv2 ## Bluetooth bluez -## Docs -mandoc man-pages - ## Configuration sigma-conf biblegen @@ -137,11 +118,10 @@ xwayland xwayland-satellite seatd eudev xrdb ## Fonts sigma-firacode-nerd sigma-vt323 sigma-material-symbols font-ubuntu-nerd -font-noto-all ## Desktop # river river-doc # compositor -# hyprland hyprland-doc +# hyprland # hyprland-doc mangowc # niri waybar waybar-doc # status bar @@ -156,12 +136,11 @@ swaybg # wallpaper daemon foot ## Text Editing -vim helix helix-tree-sitter-vendor +vim ## Browser # luakit luakit-doc # firefox -librewolf ## Notifications mako @@ -171,64 +150,25 @@ libnotify grim slurp -## File Managent -yazi dragon-drop - ## Office -sc-im sigma-sent -groff -poppler poppler-utils -# zathura zathura-pdf-mupdf -# libreoffice libreoffice-gtk - -## Mail -# neomutt -# thunderbird - -## RSS -# newsboat - -## Multimedia -mpv -ffmpeg -imagemagick -nsxiv -# gimp -# inkscape -wf-recorder -# shotcut -# lmms - -## Password Management -keepassxc pass - -## Chat -weechat -# simplex-chat - -## Monitoring -# wireshark tshark ## System Resources lm-sensors -hardinfo2 pciutils lscpu +## Disk Health +# smartctl -a /dev/nvme0, nvme smart-log /dev/nvme0 +smartmontools nvme-cli + ## Misc wl-clipboard wlr-randr dbus-x11 sigma-wvkbd neofetch -lxappearance -zenity setxkbmap -# electrum -cava -wev xev -# qemu-ui-gtk qemu-system-x86_64 qemu-system-aarch64 ovmf ## Themes sigma-flat-remix-gtk @@ -251,54 +191,12 @@ mesa-utils # Development -## Compilers/Interpreters -ruby ruby-doc ruby-rdoc ruby-full ruby-dev -python3 py3-pip py3-virtualenv -gcc g++ valgrind -perl -openjdk25 -rust cargo -go - -## Linkers -mold - ## LSP # clang-extra-tools # py3-lsp-server -## Build Systems -make cmake meson ninja-build -maven - -## Versioning -git git-doc - -## Debuggers -gdb - #--------------------------------# # 1337 h@xx0r -hexedit -# aircrack-ng -# nmap nmap-ncat -# rustscan -# ghidra -# zaproxy -# mitmproxy -# hashcat -# john -# hydra -# radare2 -exiftool -# sqlmap -# recon-ng -# bettercap -socat -arp-scan -tor -# jadx apktool -android-tools scrcpy rdbo-archives diff --git a/profiles/standard.list b/profiles/standard.list new file mode 100644 index 0000000..90ea86f --- /dev/null +++ b/profiles/standard.list @@ -0,0 +1,116 @@ +# standard: minimal plus everyday tools, media and development +# +# Syntax: one or more packages per line, # starts a comment, +# `@include ` pulls in another profile, `-pattern` removes +# packages (globs allowed) added before it. + +@include minimal + +# System + +## Administration Tools +openssh +curl wget +gpg +patch entr +7zip +rsync +diffutils +tmux tmux-doc +jq +ripgrep +btop +bandwhich +timeshift +flatpak +bubblewrap +bc +argon2 +delta +just + +## Network +openvpn wireguard-tools + +## Docs +mandoc man-pages + +#--------------------------------# + +# Desktop Environment + +## Fonts +font-noto-all + +## Text Editing +helix helix-tree-sitter-vendor + +## Browser +librewolf + +## File Managent +yazi dragon-drop + +## Office +sc-im +groff +poppler poppler-utils + +## Multimedia +mpv +ffmpeg +imagemagick +nsxiv +wf-recorder + +## Password Management +keepassxc pass + +## Chat +weechat + +## System Resources +hardinfo2 + +## Misc +lxappearance +zenity +cava +wev xev + +#--------------------------------# + +# Development + +## Compilers/Interpreters +ruby ruby-doc ruby-rdoc ruby-full ruby-dev +python3 py3-pip py3-virtualenv +gcc g++ valgrind +perl +openjdk25 +rust cargo +go + +## Linkers +mold + +## Build Systems +make cmake meson ninja-build +maven + +## Versioning +git git-doc + +## Debuggers +gdb + +#--------------------------------# + +# 1337 h@xx0r + +hexedit +exiftool +socat +arp-scan +tor +android-tools scrcpy diff --git a/sensible_clean.sh b/sensible_clean.sh deleted file mode 100755 index c79df25..0000000 --- a/sensible_clean.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -# Clean everything, except built packages - -rm -rf cache/boot cache/firmware cache/iso cache/rootfs.squashfs cache/sigma-linux.iso cache/squashfs/ diff --git a/sigma b/sigma new file mode 100755 index 0000000..02a7420 --- /dev/null +++ b/sigma @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Sigma Linux builder. Run `./sigma --help`. +require_relative "lib/sigma" + +exit Sigma::CLI.start(ARGV) || 0 diff --git a/src/apk/sigma-vt323/src/VT323.tar.xz b/src/apk/sigma-vt323/src/VT323.tar.xz deleted file mode 120000 index 98f29ea..0000000 --- a/src/apk/sigma-vt323/src/VT323.tar.xz +++ /dev/null @@ -1 +0,0 @@ -/app/src/apk/sigma-vt323/VT323.tar.xz \ No newline at end of file diff --git a/src/apk/sigma-vt323/src/vt323.tar.xz b/src/apk/sigma-vt323/src/vt323.tar.xz deleted file mode 120000 index 2c6dbe6..0000000 --- a/src/apk/sigma-vt323/src/vt323.tar.xz +++ /dev/null @@ -1 +0,0 @@ -/app/src/apk/sigma-vt323/vt323.tar.xz \ No newline at end of file diff --git a/src/build_repo.sh b/src/build_repo.sh deleted file mode 100755 index 1ae78a6..0000000 --- a/src/build_repo.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/sh - -# NOTE: Run as the build user! - -set -e - -mkdir -p "$REPO_DIR" "$APKTEMP_DIR" - -# Get existing APKs in repo (if any) -apklist="$(find cache/repo/ -name "*.apk" | sed 's|.*/||')" -is_apk_indexed() { - printf "%s" "$apklist" | tr ' ' '\n' | grep "^$1-" > /dev/null - return $? -} - -# Build APKs -for apk in $(ls "$APKTEMP_DIR"); do - echo "[*] Building ${apk}..." - if is_apk_indexed "$apk"; then - echo "[*] Skipped building APK '${apk}', already indexed" - continue - fi - - cd "$APKTEMP_DIR/$apk" - abuild -kK -rf -P "$REPO_DIR" -done - -# Return to root dir -cd "$ROOT_DIR" diff --git a/src/config.sh b/src/config.sh deleted file mode 100755 index 5326a31..0000000 --- a/src/config.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/sh - -set -e - -export PROFILENAME="sigma" -export SYSNAME="Sigma Linux" -export ROOT_DIR="$(dirname -- "$(readlink -f -- "$0")")" -export CACHE_DIR="$ROOT_DIR/cache" -export SRC_DIR="$ROOT_DIR/src" -export REPO_DIR="$CACHE_DIR/repo" -export APK_DIR="$SRC_DIR/apk" -export APKTEMP_DIR="$CACHE_DIR/apk" -export BUILD_USER="build" # User with abuild setup -export MAX_THREADS="$(nproc)" -export DISTRO_TARGET_ARCH="$(uname -m)" -export REPOS_FILE="$SRC_DIR/repositories" -export PKG_PROFILE="standard" -export BOOT_DIR="$CACHE_DIR/boot" -export SQUASHFS_DIR="$CACHE_DIR/squashfs" -export SQUASHFS_PATH="$CACHE_DIR/rootfs.squashfs" -export SQUASHFS_COMP="xz" -export SQUASHFS_EXTRA_ARGS="" -export FIRMWARE_DIR="$CACHE_DIR/firmware" -export ISO_DIR="$CACHE_DIR/iso" -export ISO_PATH="$CACHE_DIR/$PROFILENAME-linux.iso" -export ISO_VOLID="sigma-linux-cdrom" - -# Allow overriding the default variables through a -# separate script, which is added to gitignore -if [ -f "$SRC_DIR/config.override.sh" ]; then - . "$SRC_DIR/config.override.sh" - echo "[*] Loaded config overrides" -fi - -echo "Config:" -echo " - PROFILENAME: $PROFILENAME" -echo " - SYSNAME: $SYSNAME" -echo " - ROOT_DIR: $ROOT_DIR" -echo " - CACHE_DIR: $CACHE_DIR" -echo " - SRC_DIR: $SRC_DIR" -echo " - REPO_DIR: $REPO_DIR" -echo " - APK_DIR: $APK_DIR" -echo " - APKTEMP_DIR: $APKTEMP_DIR" -echo " - BUILD_USER: $BUILD_USER" -echo " - MAX_THREADS: $MAX_THREADS" -echo " - DISTRO_TARGET_ARCH: $DISTRO_TARGET_ARCH" -echo " - REPOS_FILE: $REPOS_FILE" -echo " - PKG_PROFILE: $PKG_PROFILE" -echo " - SQUASHFS_DIR: $SQUASHFS_DIR" -echo " - SQUASHFS_PATH: $SQUASHFS_PATH" -echo " - SQUASHFS_COMP: $SQUASHFS_COMP" -echo " - SQUASHFS_EXTRA_ARGS: $SQUASHFS_EXTRA_ARGS" -echo " - FIRMWARE_DIR: $FIRMWARE_DIR" -echo " - ISO_DIR: $ISO_DIR" -echo " - ISO_PATH: $ISO_PATH" -echo " - ISO_VOLID: $ISO_VOLID" diff --git a/src/make_iso.sh b/src/make_iso.sh deleted file mode 100755 index 797f533..0000000 --- a/src/make_iso.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/sh - -set -e - -# The "$BOOT_DIR" will contain kernel, initramfs, kernel config, modules, etc -# so we don't have to do any aditional setup here -mkdir -p "$ISO_DIR" "$ISO_DIR/boot" -cp -r "$BOOT_DIR/." "$ISO_DIR/boot/." - -# Copy squashfs to ISO dir -cp "$SQUASHFS_PATH" "$ISO_DIR/rootfs.squashfs" - -# Copy GRUB wallpaper to /boot/grub -cp "$SRC_DIR/grub_wallpaper.png" "$ISO_DIR/boot/grub/wallpaper.png" - -# Copy GRUB config to ISO dir -# NOTE: The vt.* kernel parameters are used for changing the virtual terminal (VT) theme -# Sane defaults: -# default_red: 0x00,0xaa,0x00,0xaa,0x00,0xaa,0x00,0xaa,0x55,0xff,0x55,0xff,0x55,0xff,0x55,0xff -# default_grn: 0x00,0x00,0xaa,0x55,0x00,0x00,0xaa,0xaa,0x55,0x55,0xff,0xff,0x55,0x55,0xff,0xff -# default_blu: 0x00,0x00,0x00,0x00,0xaa,0xaa,0xaa,0xaa,0x55,0x55,0x55,0x55,0xff,0xff,0xff,0xff -# NOTE: The VT color table is not in order! -# From 'drivers/tty/vt/vt.c': -# ``` -# const unsigned char color_table[] = { 0, 4, 2, 6, 1, 5, 3, 7, -# 8,12,10,14, 9,13,11,15 }; -# ``` -# This means that color "3" will actually be mapped to the RGB indices of color "6", for -# example. -vt_default_red=0x10,0xaa,0x00,0xaa,0x00,0xaa,0x00,0xaa,0x55,0xff,0x55,0xff,0x55,0xff,0x00,0xff -vt_default_grn=0x1a,0x00,0xaa,0x55,0x00,0x00,0xaa,0xaa,0x55,0x55,0xff,0xff,0x55,0x55,0xff,0xff -vt_default_blu=0x20,0x00,0x00,0x00,0xaa,0xaa,0xaa,0xaa,0x55,0x55,0x55,0x55,0xff,0xff,0xc8,0xff - -# Get kernel and initramfs names -vmlinuz="$(ls "$BOOT_DIR/" | grep "^vmlinuz" | tail -1)" -initramfs="$(ls "$BOOT_DIR" | grep "^initramfs" | tail -1)" - -cat <<- EOF > "$ISO_DIR/boot/grub/grub.cfg" -set timeout=5 - -insmod all_video -loadfont /boot/grub/fonts/unicode.pf2 - -insmod gfxterm -set gfxmode=640x480 -terminal_output gfxterm - -insmod png -background_image /boot/grub/wallpaper.png - -menuentry "$SYSNAME" { - echo "Loading vmlinuz..." - linux /boot/$vmlinuz root=LABEL=$ISO_VOLID rootfstype=iso9660 rd.live=/rootfs.squashfs vt.color=0x0B vt.default_red=$vt_default_red vt.default_grn=$vt_default_grn vt.default_blu=$vt_default_blu splash - echo "Loading initrd..." - initrd /boot/$initramfs -} -EOF - -# Build ISO -grub-mkrescue "$ISO_DIR" -o "$ISO_PATH" -volid "$ISO_VOLID" diff --git a/src/make_squashfs.sh b/src/make_squashfs.sh deleted file mode 100755 index 6a3484f..0000000 --- a/src/make_squashfs.sh +++ /dev/null @@ -1,213 +0,0 @@ -#!/bin/sh - -set -e - -mkdir -p "$SQUASHFS_DIR" - -# Mount filesystems because some packages write to them -# (e.g /dev/null) -umount -R "$SQUASHFS_DIR/dev" > /dev/null 2>&1 || true -rm -rf "$SQUASHFS_DIR/dev" > /dev/null 2>&1 || true -mkdir -p "$SQUASHFS_DIR/dev" -mount --rbind /dev "$SQUASHFS_DIR/dev" - -umount -R "$SQUASHFS_DIR/proc" > /dev/null 2>&1 || true -rm -rf "$SQUASHFS_DIR/proc" > /dev/null 2>&1 || true -mkdir -p "$SQUASHFS_DIR/proc" -mount --rbind /proc "$SQUASHFS_DIR/proc" - -# Mount temporary boot dir, which will be populated by the -# kernel and initramfs packages -umount -R "$SQUASHFS_DIR/boot" > /dev/null 2>&1 || true -rm -rf "$SQUASHFS_DIR/boot" > /dev/null 2>&1 || true -mkdir -p "$BOOT_DIR" "$SQUASHFS_DIR/boot" -mount --rbind "$BOOT_DIR" "$SQUASHFS_DIR/boot" - -# Disable GRUB triggers since they -# interfere with our GRUB config -mkdir -p "$SQUASHFS_DIR/etc" -echo 'disable_trigger=1' > "$SQUASHFS_DIR/etc/upgrade-grub.conf" - -pkgs="$(cat "$SRC_DIR/pkglist.$PKG_PROFILE" | sed 's/#.*//g' | tr '\n' ' ')" -echo "Packages: $pkgs" - -# Create /etc/modules for use in initrdbo -cat <<- EOF > "$SQUASHFS_DIR/etc/modules" -ahci libahci -ata_generic libata -ata_piix -nvme_core nvme -usbcore -xhci_pci ehci_pci uhci_hcd -usb_common usb_storage uas - -scsi_mod sd_mod sr_mod - -hid hid_generic usbhid - -af_packet -ipv6 -EOF - - -# Skip installing APKs if APK database is already set up -if [ ! -d "$SQUASHFS_DIR/etc/apk" ]; then - # Initialize APK database - apk add --arch "$DISTRO_TARGET_ARCH" --initdb -p "$SQUASHFS_DIR" - - # Install packages - apk add \ - -p "$SQUASHFS_DIR" \ - --arch "$DISTRO_TARGET_ARCH" \ - --allow-untrusted \ - --no-cache \ - --repositories-file="$REPOS_FILE" \ - -X "$REPO_DIR/apk" \ - $pkgs -else - echo "[*] Skipped installing APKs in SquashFS, '/etc/apk' exists" -fi - -# Add repositories file to squashfs -mkdir -p "$SQUASHFS_DIR/etc/apk" -cp "$REPOS_FILE" "$SQUASHFS_DIR/etc/apk/repositories" - -# Overwrite package owned files -cat <<- EOF > "$SQUASHFS_DIR/etc/issue" -Welcome to Sigma Linux (made by rdbo) -Kernel \r on an \m (\l) -EOF - -cat <<- EOF > "$SQUASHFS_DIR/etc/motd" -Welcome to Sigma Linux! - -To install the system, run the following command: setup-sigma - -For more information about the distribution, see: - - https://github.com/rdbo/sigma-linux - - https://wiki.alpinelinux.org - -EOF - -echo "$PROFILENAME" > "$SQUASHFS_DIR/etc/hostname" - -> "$SQUASHFS_DIR/etc/fstab" - -# Add default network interfaces configuration -mkdir -p "$SQUASHFS_DIR/etc/network" -cat <<- EOF > "$SQUASHFS_DIR/etc/network/interfaces" -auto lo -iface lo inet loopback -EOF - -# Create default doas config -cat <<- EOF > "$SQUASHFS_DIR/etc/doas.conf" -permit persist :wheel -permit nopass root -EOF - -# Modify zram-init config -cat <<- EOF > "$SQUASHFS_DIR/etc/conf.d/zram-init" -load_on_start="yes" -unload_on_stop="yes" -num_devices="1" - -type0="swap" -flag0= -size0=\`LC_ALL=C free -m | awk '/^Mem:/{print int(\$2/2)}'\` # 50% of memory reserved for zram -maxs0=1 -algo0=zstd -labl0=zram_swap -EOF - -# Create common user directories in /etc/skel -dirs="Downloads Documents Pictures Videos Music" -for dir in $dirs; do - mkdir -p "$SQUASHFS_DIR/etc/skel/$dir" -done - -# Copy /etc/skel to /root (allows for logging in to the desktop environment as root on live boot) -cp -r "$SQUASHFS_DIR/etc/skel/." "$SQUASHFS_DIR/root/." - -# Enable OpenRC services -rc_add() { - # $1: service name - # $2: run level - chroot "$SQUASHFS_DIR" rc-update add "$1" "$2" -} - -rc_add devfs sysinit -rc_add dmesg sysinit -rc_add mdev sysinit -rc_add hwdrivers sysinit -# rc_add modloop sysinit # NOTE: Not necessary on Sigma Linux - -rc_add hwclock boot -rc_add modules boot -rc_add sysctl boot -rc_add bootmisc boot -rc_add syslog boot - -# NOTE: The 'udev' services are used for setting up /dev and doing things -# such as changing the ownership of certain devices (e.g /dev/dri/cardN). -# That behavior allows us to access devices (such as the GPU and the input -# devices) without root access. -rc_add udev sysinit -rc_add udev-trigger sysinit -rc_add udev-settle sysinit -rc_add udev-postmount default -rc_add hostname boot -rc_add zram-init boot -rc_add networking default # Sets up interfaces based on /etc/network/interfaces -rc_add earlyoom default -rc_add polkit default -rc_add networkmanager default -rc_add dbus default -rc_add seatd default -rc_add bluetooth default - -rc_add local default # used for start scripts - -# Setup regular user -if [ ! -e "$SQUASHFS_DIR/home/user" ]; then - useradd -R "$SQUASHFS_DIR" -s /bin/bash -m -G wheel,audio,input,video,seat,dialout,netdev user -fi -# passwd -R "$SQUASHFS_DIR" -d user -chroot "$SQUASHFS_DIR" sh -c 'printf "user:pass" | chpasswd' - -# Merge user patches (https://alpinelinux.org/posts/2025-10-01-usr-merge.html) -chroot "$SQUASHFS_DIR" merge-usr - -# Unmount filesystems -umount -R "$SQUASHFS_DIR/proc" -rm -rf "$SQUASHFS_DIR/proc" - -umount -R "$SQUASHFS_DIR/dev" -rm -rf "$SQUASHFS_DIR/dev" - -umount -R "$SQUASHFS_DIR/boot" -rm -rf "$SQUASHFS_DIR/boot" - -# Cleanup firmware files that are not used by any module -# (they can be reinstalled through the `linux-firmware` pkg) -# if [ -e "$FIRMWARE_DIR" ]; then -# echo "[*] Skipped firmware cleanup, '$FIRMWARE_DIR' exists" -# else -# echo "[*] Cleaning up unused firmware for squashfs..." -# mv "$SQUASHFS_DIR/lib/firmware" "$FIRMWARE_DIR" -# mkdir -p "$SQUASHFS_DIR/lib/firmware" -# # TODO: Make sure that `modinfo` cannot fail. It mail fail due to -# # the host kernel not being the same as the installer kernel -# chroot "$FILESYSTEM_DIR" find /lib/modules -type f -name "*.ko*" | chroot "$FILESYSTEM_DIR" xargs -r -- modinfo -F firmware | sort -u | while read fw; do -# for fname in "$fw" "$fw.zst" "$fw.xz"; do -# if [ -e "${FIRMWARE_DIR}/$fname" ]; then -# install -pD "${FIRMWARE_DIR}/$fname" "$SQUASHFS_DIR/lib/firmware/$fname" -# break -# fi -# done -# done -# fi - -# Create squashfs -rm -f "$SQUASHFS_PATH" # Avoid appending to existing squashfs file -mksquashfs "$SQUASHFS_DIR" "$SQUASHFS_PATH" -comp "$SQUASHFS_COMP" $SQUASHFS_EXTRA_ARGS diff --git a/src/pkglist.full b/src/pkglist.full deleted file mode 100644 index ef4fe05..0000000 --- a/src/pkglist.full +++ /dev/null @@ -1,273 +0,0 @@ -# Core Packages - -## Core -alpine-base -alpine-baselayout -alpine-baselayout-data -alpine-keys -apk-tools -busybox busybox-doc -ca-certificates-bundle -musl -openrc -openssl -cryptsetup -sfdisk -lsblk -grub -grub-bios -grub-efi -xorriso -mtools -efibootmgr -initrdbo -linux-sigma linux-sigma-dev -# linux-stable linux-stable-dev -alpine-sdk -kmod -shadow -umount -merge-usr -losetup - -## Firmware -linux-firmware - -#--------------------------------# - -# System - -## Administration Tools -bash starship -doas -openssh -curl wget -gpg -patch entr -7zip -xfsprogs e2fsprogs btrfs-progs dosfstools ntfs-3g -yt-dlp -tar xz zstd zip unzip gzip -lighttpd -rsync -diffutils -tmux tmux-doc -jq -ripgrep -wireless-tools -btop -bandwhich -timeshift -flatpak -bubblewrap -bc -cpupower -polkit -argon2 -delta -just - -## Suspend/Hibernate -acpid zzz - -## Memory -earlyoom zram-init - -## Network -wpa_supplicant dhcpcd -iwd -networkmanager networkmanager-wifi networkmanager-wwan networkmanager-dnsmasq networkmanager-cli networkmanager-tui -openvpn wireguard-tools - -## Audio -pipewire pipewire-pulse pipewire-alsa pipewire-jack -pulsemixer pulseaudio-ctl pulseaudio-utils -easyeffects calf-lv2 lsp-plugins-lv2 mda-lv2 zam-plugins-lv2 - -## Bluetooth -bluez - -## Docs -mandoc man-pages - -## Configuration -sigma-conf -biblegen -ncurses-terminfo - -#--------------------------------# - -# Desktop Environment - -## Core -wayland wayland-libs-client wayland-libs-server -xwayland xwayland-satellite seatd eudev xrdb - -## Fonts -sigma-firacode-nerd sigma-vt323 sigma-material-symbols -font-ubuntu-nerd -font-noto-all - -## Desktop -# river river-doc # compositor -# hyprland hyprland-doc -mangowc -# niri -waybar waybar-doc # status bar -# yambar yambar-doc # status bar -fuzzel # dmenu-like -swaylock # screen locker -swaybg # wallpaper daemon -# cosmic-session cosmic-launcher cosmic-randr cosmic-panel cosmic-files cosmic-icons cosmic-comp cosmic-bg - -## Terminal -# rio -foot - -## Text Editing -vim helix tree-sitter-grammars - -## Browser -# luakit luakit-doc -# firefox -librewolf - -## Notifications -mako -libnotify - -## Screenshot -grim -slurp - -## File Managent -yazi dragon-drop - -## Office -sc-im -sigma-sent -groff -poppler poppler-utils -zathura zathura-pdf-mupdf -libreoffice libreoffice-gtk - -## Mail -neomutt -thunderbird - -## RSS -newsboat - -## Multimedia -mpv -ffmpeg -imagemagick -nsxiv -gimp -inkscape -wf-recorder -shotcut -lmms - -## Password Management -keepassxc pass - -## Chat -weechat -simplex-chat - -## Monitoring -wireshark tshark - -## System Resources -lm-sensors -hardinfo2 -pciutils -lscpu - -## Misc -wl-clipboard -wlr-randr -dbus-x11 -sigma-wvkbd -neofetch -lxappearance -zenity -setxkbmap -electrum -cava -wev xev -qemu-ui-gtk qemu-system-x86_64 qemu-system-aarch64 ovmf - -## Themes -sigma-flat-remix-gtk -adwaita-icon-theme -papirus-icon-theme - -## GL/Rendering -mesa -mesa-gbm -mesa-dri-gallium -mesa-egl -mesa-gl -mesa-gles -mesa-vulkan-ati -mesa-vulkan-intel -mesa-vulkan-swrast -mesa-utils - -#--------------------------------# - -# Development - -## Compilers/Interpreters -ruby ruby-doc ruby-rdoc ruby-full ruby-dev -python3 py3-pip py3-virtualenv -gcc g++ valgrind -perl -openjdk25 -rust cargo -go - -## Linkers -mold - -## LSP -# clang-extra-tools -# py3-lsp-server - -## Build Systems -make cmake meson ninja-build -maven - -## Versioning -git git-doc - -## Debuggers -gdb - -#--------------------------------# - -# 1337 h@xx0r - -hexedit -aircrack-ng -nmap nmap-ncat -rustscan -ghidra -zaproxy -mitmproxy -hashcat -john -hydra -radare2 -exiftool -sqlmap -recon-ng -bettercap -socat -arp-scan -tor -jadx apktool -android-tools scrcpy -rdbo-archives diff --git a/src/pkglist.minimal b/src/pkglist.minimal deleted file mode 100644 index 2593475..0000000 --- a/src/pkglist.minimal +++ /dev/null @@ -1,304 +0,0 @@ -# Core Packages - -## Core -alpine-base -alpine-baselayout -alpine-baselayout-data -alpine-keys -apk-tools -busybox busybox-doc -ca-certificates-bundle -musl -openrc -openssl -cryptsetup -sfdisk -lsblk -grub -grub-bios -grub-efi -xorriso -mtools -efibootmgr -initrdbo -linux-sigma linux-sigma-dev -# linux-stable linux-stable-dev -alpine-sdk -kmod -shadow -umount -merge-usr -losetup - -## Firmware -linux-firmware-amd -linux-firmware-amd-ucode -linux-firmware-amdgpu -linux-firmware-intel -linux-firmware-i915 -linux-firmware-nvidia -linux-firmware-brcm -linux-firmware-realtek -linux-firmware-rtl_bt -linux-firmware-rtw88 -linux-firmware-rtw89 -linux-firmware-rtlwifi -linux-firmware-qca -linux-firmware-ath10k -linux-firmware-ath11k -linux-firmware-ath12k -linux-firmware-bnx2 -linux-firmware-bnx2x -linux-firmware-mellanox -linux-firmware-microchip -linux-firmware-synaptics -linux-firmware-hp -linux-firmware-lenovo -linux-firmware-dell -linux-firmware-powervr -linux-firmware-amlogic -linux-firmware-meson -linux-firmware-rockchip -linux-firmware-mrvl -linux-firmware-qcom -linux-firmware-mediatek -linux-firmware-mwlwifi - -#--------------------------------# - -# System - -## Administration Tools -bash starship -doas -# openssh -# curl wget -# gpg -# patch entr -# 7zip -xfsprogs e2fsprogs btrfs-progs dosfstools ntfs-3g -# yt-dlp -tar xz zstd zip unzip gzip -# lighttpd -# rsync -# diffutils -# tmux tmux-doc -# jq -# ripgrep -wireless-tools -# btop -# bandwhich -# timeshift -# flatpak -# bubblewrap -# bc -cpupower -polkit -# argon2 -# delta -# just - -## Suspend/Hibernate -acpid zzz - -## Memory -earlyoom zram-init - -## Network -wpa_supplicant dhcpcd -iwd -networkmanager networkmanager-wifi networkmanager-wwan networkmanager-dnsmasq networkmanager-cli networkmanager-tui -# openvpn wireguard-tools - -## Audio -pipewire pipewire-pulse pipewire-alsa pipewire-jack -pulsemixer pulseaudio-ctl pulseaudio-utils -easyeffects calf-lv2 lsp-plugins-lv2 mda-lv2 zam-plugins-lv2 - -## Bluetooth -bluez - -## Docs -# mandoc man-pages - -## Configuration -sigma-conf -biblegen -ncurses-terminfo - -#--------------------------------# - -# Desktop Environment - -## Core -wayland wayland-libs-client wayland-libs-server -xwayland xwayland-satellite seatd eudev xrdb - -## Fonts -sigma-firacode-nerd sigma-vt323 sigma-material-symbols -font-ubuntu-nerd -# font-noto-all - -## Desktop -# river river-doc # compositor -# hyprland # hyprland-doc -mangowc -# niri -waybar waybar-doc # status bar -# yambar yambar-doc # status bar -fuzzel # dmenu-like -swaylock # screen locker -swaybg # wallpaper daemon -# cosmic-session cosmic-launcher cosmic-randr cosmic-panel cosmic-files cosmic-icons cosmic-comp cosmic-bg - -## Terminal -# rio -foot - -## Text Editing -vim # helix tree-sitter-grammars - -## Browser -# luakit luakit-doc -# firefox -# librewolf - -## Notifications -mako -libnotify - -## Screenshot -grim -slurp - -## File Managent -# yazi dragon-drop - -## Office -# sc-im -sigma-sent -# groff -# poppler poppler-utils -# zathura zathura-pdf-mupdf -# libreoffice libreoffice-gtk - -## Mail -# neomutt -# thunderbird - -## RSS -# newsboat - -## Multimedia -# mpv -# ffmpeg -# imagemagick -# nsxiv -# gimp -# inkscape -# wf-recorder -# shotcut -# lmms - -## Password Management -# keepassxc pass - -## Chat -# weechat -# simplex-chat - -## Monitoring -# wireshark tshark - -## System Resources -lm-sensors -# hardinfo2 -pciutils -lscpu - -## Misc -wl-clipboard -wlr-randr -dbus-x11 -sigma-wvkbd -neofetch -# lxappearance -# zenity -setxkbmap -# electrum -# cava -# wev xev -# qemu-ui-gtk qemu-system-x86_64 qemu-system-aarch64 ovmf - -## Themes -sigma-flat-remix-gtk -adwaita-icon-theme -papirus-icon-theme - -## GL/Rendering -mesa -mesa-gbm -mesa-dri-gallium -mesa-egl -mesa-gl -mesa-gles -mesa-vulkan-ati -mesa-vulkan-intel -mesa-vulkan-swrast -mesa-utils - -#--------------------------------# - -# Development - -## Compilers/Interpreters -# ruby ruby-doc ruby-rdoc ruby-full ruby-dev -# python3 py3-pip py3-virtualenv -# gcc g++ valgrind -# perl -# openjdk25 -# rust cargo -# go - -## Linkers -# mold - -## LSP -# clang-extra-tools -# py3-lsp-server - -## Build Systems -# make cmake meson ninja-build -# maven - -## Versioning -# git git-doc - -## Debuggers -# gdb - -#--------------------------------# - -# 1337 h@xx0r - -# hexedit -# aircrack-ng -# nmap nmap-ncat -# rustscan -# ghidra -# zaproxy -# mitmproxy -# hashcat -# john -# hydra -# radare2 -# exiftool -# sqlmap -# recon-ng -# bettercap -# socat -# arp-scan -# tor -# jadx apktool -# android-tools scrcpy -rdbo-archives diff --git a/src/setup_apks.sh b/src/setup_apks.sh deleted file mode 100755 index dee491f..0000000 --- a/src/setup_apks.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/bin/sh - -# linux-sigma -if [ ! -d "$APKTEMP_DIR/linux-sigma" ]; then - mkdir -p "$APKTEMP_DIR/linux-sigma" - cp -r "$APK_DIR/linux-sigma/." "$APKTEMP_DIR/linux-sigma/." - cd "$APKTEMP_DIR/linux-sigma" - abuild checksum -fi - -# initrdbo -if [ ! -d "$APKTEMP_DIR/initrdbo" ]; then - mkdir -p "$APKTEMP_DIR/initrdbo" - cp -r "$APK_DIR/initrdbo/." "$APKTEMP_DIR/initrdbo/." - cd "$APKTEMP_DIR/initrdbo" - abuild checksum -fi - -# sigma-conf -if [ ! -d "$APKTEMP_DIR/sigma-conf" ]; then - mkdir -p "$APKTEMP_DIR/sigma-conf/" - cp "$APK_DIR/sigma-conf/APKBUILD" "$APKTEMP_DIR/sigma-conf/" - cd "$APK_DIR/sigma-conf" - tar -czf "$APKTEMP_DIR/sigma-conf/rootfs.tar.gz" rootfs - cd "$APKTEMP_DIR/sigma-conf" - abuild checksum -fi - -# rdbo-archives -if [ ! -d "$APKTEMP_DIR/rdbo-archives" ]; then - mkdir -p "$APKTEMP_DIR/rdbo-archives" - cp -r "$APK_DIR/rdbo-archives/." "$APKTEMP_DIR/rdbo-archives/." - cd "$APKTEMP_DIR/rdbo-archives" - abuild checksum -fi - -# sigma-firacode-nerd -if [ ! -d "$APKTEMP_DIR/sigma-firacode-nerd" ]; then - cp -r "$APK_DIR/sigma-firacode-nerd/" "$APKTEMP_DIR/" -fi - -# sigma-material-symbols -if [ ! -d "$APKTEMP_DIR/sigma-material-symbols" ]; then - cp -r "$APK_DIR/sigma-material-symbols/" "$APKTEMP_DIR/" -fi - -# sigma-sent -if [ ! -d "$APKTEMP_DIR/sigma-sent" ]; then - mkdir -p "$APKTEMP_DIR/sigma-sent/" - cp "$APK_DIR/sigma-sent/APKBUILD" "$APKTEMP_DIR/sigma-sent/" - cd "$APK_DIR/sigma-sent" - tar -czf "$APKTEMP_DIR/sigma-sent/sent.tar.gz" sent - cd "$APKTEMP_DIR/sigma-sent" - abuild checksum -fi - -# sigma-st -if [ ! -d "$APKTEMP_DIR/sigma-st" ]; then - cp -r "$APK_DIR/sigma-st/" "$APKTEMP_DIR/" - cd "$APKTEMP_DIR/sigma-st" - abuild checksum -fi - -# sigma-vt323 -if [ ! -d "$APKTEMP_DIR/sigma-vt323" ]; then - cp -r "$APK_DIR/sigma-vt323/" "$APKTEMP_DIR/" -fi - -# sigma-wvkbd -if [ ! -d "$APKTEMP_DIR/sigma-wvkbd" ]; then - cp -r "$APK_DIR/sigma-wvkbd/" "$APKTEMP_DIR/" - cd "$APKTEMP_DIR/sigma-wvkbd" - abuild checksum -fi - -# sigma-flat-remix-gtk -if [ ! -d "$APKTEMP_DIR/sigma-flat-remix-gtk" ]; then - cp -r "$APK_DIR/sigma-flat-remix-gtk/" "$APKTEMP_DIR/" -fi - -# simplex-chat -# if [ ! -d "$APKTEMP_DIR/simplex-chat" ]; then -# cp -r "$APK_DIR/simplex-chat/" "$APKTEMP_DIR/" -# fi - -# biblegen -if [ ! -d "$APKTEMP_DIR/biblegen" ]; then - mkdir -p "$APKTEMP_DIR/biblegen" - cp -r "$APK_DIR/biblegen/." "$APKTEMP_DIR/biblegen/." - cd "$APKTEMP_DIR/biblegen" - abuild checksum -fi diff --git a/start.sh b/start.sh deleted file mode 100755 index 7e540a4..0000000 --- a/start.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -qemu-system-x86_64 \ - -enable-kvm \ - -smp 4 \ - -m 2048 \ - -cdrom cache/sigma-linux.iso \ - $@ diff --git a/start_uefi.sh b/start_uefi.sh deleted file mode 100755 index 57092e5..0000000 --- a/start_uefi.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -qemu-system-x86_64 \ - -bios /usr/share/OVMF/OVMF.fd \ - -enable-kvm \ - -smp 4 \ - -m 2048 \ - -cdrom cache/sigma-linux.iso \ - $@ diff --git a/test/iso_test.rb b/test/iso_test.rb new file mode 100644 index 0000000..e4951e7 --- /dev/null +++ b/test/iso_test.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class IsoTest < Minitest::Test + def setup + @dir = Dir.mktmpdir + @context = Sigma::Context.new(config("paths.cache" => @dir), quiet_ui) + pipeline = Sigma::Pipeline.new(@context) + @iso = pipeline.stage("iso") + FileUtils.mkdir_p(@context.paths.boot) + end + + def teardown = FileUtils.rm_rf(@dir) + + def boot(*files) = files.each { |f| FileUtils.touch(File.join(@context.paths.boot, f)) } + + # The menu the old make_iso.sh heredoc produced, verbatim. + OLD_MENU = <<~'CFG' + set timeout=5 + + insmod all_video + loadfont /boot/grub/fonts/unicode.pf2 + + insmod gfxterm + set gfxmode=640x480 + terminal_output gfxterm + + insmod png + background_image /boot/grub/wallpaper.png + + menuentry "Sigma Linux" { + echo "Loading vmlinuz..." + linux /boot/vmlinuz-sigma root=LABEL=sigma-linux-cdrom rootfstype=iso9660 rd.live=/rootfs.squashfs vt.color=0x0B vt.default_red=0x10,0xaa,0x00,0xaa,0x00,0xaa,0x00,0xaa,0x55,0xff,0x55,0xff,0x55,0xff,0x00,0xff vt.default_grn=0x1a,0x00,0xaa,0x55,0x00,0x00,0xaa,0xaa,0x55,0x55,0xff,0xff,0x55,0x55,0xff,0xff vt.default_blu=0x20,0x00,0x00,0x00,0xaa,0xaa,0xaa,0xaa,0x55,0x55,0x55,0x55,0xff,0xff,0xc8,0xff splash + echo "Loading initrd..." + initrd /boot/initramfs-sigma + } + CFG + + def test_single_kernel_menu_matches_the_old_one + boot("vmlinuz-sigma", "initramfs-sigma", "System.map-7.3.1-0-sigma") + assert_equal OLD_MENU, @iso.send(:render) + end + + def test_one_entry_per_kernel_own_kernel_first + boot("vmlinuz-stable", "initramfs-stable", "vmlinuz-sigma", "initramfs-sigma", "vmlinuz-noinitrd") + kernels = @iso.kernels + assert_equal %w[sigma stable], kernels.map(&:flavor) + assert_equal ["Sigma Linux (sigma)", "Sigma Linux (stable)"], kernels.map(&:title) + assert_equal 2, @iso.send(:render).scan("menuentry").size + end + + def test_theme_parses_the_shared_file + theme = @iso.theme + assert_equal "640x480", theme["GRUB_GFXMODE"] + assert_equal "Sigma Linux", theme["GRUB_DISTRIBUTOR"] + assert theme["GRUB_CMDLINE_LINUX_DEFAULT"].start_with?("vt.color=0x0B ") + end +end diff --git a/test/packages_test.rb b/test/packages_test.rb new file mode 100644 index 0000000..968d474 --- /dev/null +++ b/test/packages_test.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class PackagesTest < Minitest::Test + def local + @local ||= Sigma::LocalPackages.new(File.join(Sigma::ROOT, "packages"), + shell: Sigma::Shell.new(builder: "build"), carch: "x86_64") + end + + def test_reads_real_apkbuilds + kernel = local["linux-sigma"] + assert_equal %w[linux-sigma linux-sigma-dev linux-sigma-doc], kernel.outputs + assert_includes kernel.local_sources, "sigma.x86_64.config" + assert_equal ["initramfs-generator"], kernel.depends + end + + def test_resolves_subpackages_and_provides + assert_equal "linux-sigma", local["linux-sigma-dev"].name + assert_equal "initrdbo", local["initramfs-generator"].name + assert_nil local["busybox"] + end + + def test_profile_needs_only_what_it_names + needed = local.for_profile(Sigma::Profile.new(File.join(Sigma::ROOT, "profiles"), "standard").packages).map(&:name) + assert_equal 11, needed.size + %w[sigma-dwl sigma-st simplex-chat].each { |unused| refute_includes needed, unused } + end + + def test_build_order_puts_dependencies_first + order = local.for_profile(%w[linux-sigma]).map(&:name) + assert_equal %w[initrdbo linux-sigma], order, "linux-sigma depends on initramfs-generator (initrdbo)" + end + + def test_ccache_delta_counts_one_builds_hits + stage = Sigma::Stages::Packages.allocate + before = { "direct_cache_hit" => 614, "preprocessed_cache_hit" => 3, "cache_miss" => 22_172 } + after = { "direct_cache_hit" => 1085, "preprocessed_cache_hit" => 139, "cache_miss" => 40_147 } + assert_equal [607, 18_582], stage.send(:ccache_delta, before, after) + assert_nil stage.send(:ccache_delta, nil, after) + end + + def test_unknown_package + assert_raises(Sigma::Error) { local.for_names(%w[nope]) } + end +end diff --git a/test/profile_test.rb b/test/profile_test.rb new file mode 100644 index 0000000..dbc580e --- /dev/null +++ b/test/profile_test.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class ProfileTest < Minitest::Test + def with_profiles(files) + Dir.mktmpdir do |dir| + files.each { |name, body| File.write(File.join(dir, "#{name}.list"), body) } + yield dir + end + end + + def test_includes_removals_and_globs_apply_in_order + with_profiles("base" => "a b # trailing comment\nfw-x fw-y\n", + "top" => "@include base\n-fw-*\nfw\nc\n-a\na\n") do |dir| + assert_equal %w[b fw c a], Sigma::Profile.new(dir, "top").packages + end + end + + def test_cycles_and_unknown_directives_are_errors + with_profiles("a" => "@include b\n", "b" => "@include a\n", "c" => "@require x\n") do |dir| + assert_match(/cycle/, assert_raises(Sigma::Error) { Sigma::Profile.new(dir, "a") }.message) + assert_match(/@require/, assert_raises(Sigma::Error) { Sigma::Profile.new(dir, "c") }.message) + assert_raises(Sigma::Error) { Sigma::Profile.new(dir, "missing") } + end + end + + def test_shipped_profiles_layer + dir = File.join(Sigma::ROOT, "profiles") + minimal, standard, full = %w[minimal standard full].map { |n| Sigma::Profile.new(dir, n).packages } + assert_empty minimal - standard, "minimal ⊂ standard" + assert_includes full, "linux-firmware" + refute full.any? { |p| p.start_with?("linux-firmware-") }, "full swaps the firmware subset for linux-firmware" + %w[linux-sigma sigma-conf initrdbo].each { |p| assert_includes minimal, p } + end +end diff --git a/test/run.rb b/test/run.rb new file mode 100644 index 0000000..2f5fd3b --- /dev/null +++ b/test/run.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# Runs every test: ruby test/run.rb +Dir[File.join(__dir__, "*_test.rb")].each { |file| require file } diff --git a/test/selection_test.rb b/test/selection_test.rb new file mode 100644 index 0000000..87107c7 --- /dev/null +++ b/test/selection_test.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class SelectionTest < Minitest::Test + STAGES = %w[packages rootfs squashfs iso].freeze + + def parse(*args) = Sigma::Selection.parse(args, STAGES) + def pairs(*args) = parse(*args).entries.map { |e| [e.stage, e.items] } + + def test_nothing_selects_everything_incrementally + selection = parse + assert selection.incremental? + assert_equal STAGES, selection.stages + end + + def test_named_stage_is_explicit + selection = parse("rootfs") + refute selection.incremental? + assert_equal [["rootfs", nil]], pairs("rootfs") + end + + def test_ranges + assert_equal %w[rootfs squashfs iso], parse("rootfs..").stages + assert_equal %w[packages rootfs squashfs], parse("..squashfs").stages + assert_equal %w[rootfs squashfs], parse("rootfs..squashfs").stages + assert_equal STAGES, parse("..").stages + end + + def test_items_merge_and_whole_wins + assert_equal [["packages", %w[linux-sigma initrdbo]]], pairs("packages:linux-sigma", "packages:initrdbo,linux-sigma") + assert_equal [["packages", nil]], pairs("packages:linux-sigma", "packages") + assert_equal [["packages", nil]], pairs("packages", "packages:linux-sigma") + end + + def test_result_is_in_pipeline_order + assert_equal [["rootfs", %w[configure]], ["iso", nil]], pairs("iso", "rootfs:configure") + end + + def test_errors + error = assert_raises(Sigma::Error) { parse("rotfs") } + assert_match(/did you mean `rootfs`/, error.message) + assert_raises(Sigma::Error) { parse("iso..rootfs") } + assert_raises(Sigma::Error) { parse("packages:") } + assert_raises(Sigma::Error) { parse("rootfs:configure..iso") } + end +end diff --git a/test/shell_test.rb b/test/shell_test.rb new file mode 100644 index 0000000..b42001c --- /dev/null +++ b/test/shell_test.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class ShellTest < Minitest::Test + def shell = Sigma::Shell.new(builder: "build") + def task = Sigma::UI::Task.new(quiet_ui, "test", nil) + + def failure(script) + shell.run(task, "sh", "-c", script) + flunk "expected the command to fail" + rescue Sigma::CommandFailed => e + e + end + + # Parallel make: the real error, then a hundred lines of other jobs. + def test_excerpt_is_the_first_error_with_its_include_chain + error = failure(<<~SH) + echo " CC a.o" + echo "In file included from x.h:1," + echo " from y.c:2:" + echo "x.h:3:4: error: unknown type name 'foo'" + echo " 3 | foo bar;" + i=0; while [ $i -lt 100 ]; do echo " CC noise$i.o"; i=$((i+1)); done + echo "y.c:9:1: error: second" + echo ">>> ERROR: pkg: build failed" + exit 1 + SH + excerpt = error.excerpt + assert_equal "In file included from x.h:1,", excerpt.first + assert_equal "x.h:3:4: error: unknown type name 'foo'", excerpt[2] + assert_equal Sigma::Shell::FIRST_ERROR_LINES, excerpt.size + refute_includes excerpt, " CC a.o" + end + + def test_without_an_error_line_the_excerpt_ends_at_abuilds_marker + error = failure(%(echo one; echo ">>> ERROR: pkg: build failed"; echo cleanup; exit 1)) + assert_nil error.first_error + assert_equal ["one", ">>> ERROR: pkg: build failed"], error.excerpt + end + + def test_error_lines_in_a_successful_command_are_ignored + assert_nil shell.run(task, "sh", "-c", "echo 'x.c:1:1: error: harmless'") + end + + def test_error_line_pattern + ["swab.h:48:15: error: unknown type name", "error[E0425]: cannot find value", + "ERROR: modpost: missing symbol", "configure: error: no compiler"].each do |line| + assert_match Sigma::Shell::ERROR_LINE, line + end + [" CC drivers/base/error.o", "cc1: all warnings being treated as errors", + ">>> ERROR: pkg: build failed"].each do |line| + refute_match Sigma::Shell::ERROR_LINE, line + end + end +end diff --git a/test/stage_test.rb b/test/stage_test.rb new file mode 100644 index 0000000..26c5dee --- /dev/null +++ b/test/stage_test.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +# Freshness: inputs, outputs and the upstream token. +class StageTest < Minitest::Test + class Upstream < Sigma::Stage + stage "up", "upstream", steps: %i[make] + attr_accessor :input + + def inputs = input + def step_make(_task) = nil + end + + class Downstream < Sigma::Stage + stage "down", "downstream", steps: %i[one two] + attr_accessor :input, :output + + def inputs = input + def outputs = [output] + def step_one(_task) = File.write(output, "x") + def step_two(_task) = nil + end + + FakePipeline = Struct.new(:state, :stages) do + def before(stage) = stage.is_a?(Downstream) ? stages.first : nil + end + + def setup + @dir = Dir.mktmpdir + cfg = config("paths.cache" => File.join(@dir, "cache")) + @context = Sigma::Context.new(cfg, quiet_ui) + pipeline = FakePipeline.new(Sigma::State.new(File.join(@dir, "state")), []) + @up = Upstream.new(@context, pipeline).tap { |s| s.input = "u1" } + @down = Downstream.new(@context, pipeline).tap { |s| s.input = "d1"; s.output = File.join(@dir, "out") } + pipeline.stages.replace([@up, @down]) + end + + def teardown = FileUtils.rm_rf(@dir) + + def test_lifecycle + assert_equal :missing, @down.status.first + @up.run(nil) + @down.run(nil) + assert @down.fresh? + + @down.input = "d2" + assert_equal [:stale, "inputs changed"], @down.status + @down.run(nil) + assert @down.fresh? + + @up.run(nil) # upstream ran again: same inputs, new token + assert_equal [:stale, "up changed"], @down.status + @down.run(nil) + + File.delete(@down.output) + assert_equal [:stale, "output missing"], @down.status + end + + def test_partial_run_keeps_inputs_but_changes_token + @up.run(nil) + @down.run(nil) + token = @down.token + @down.run(["two"]) + refute_equal token, @down.token, "downstream of a partial run must notice" + assert @down.fresh?, "a partial run doesn't invalidate the stage itself" + end + + def test_partial_run_without_prior_whole_run_records_nothing + @down.run(["two"]) + assert_nil @down.stamp + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..1ec8d9e --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require "tmpdir" +require_relative "../lib/sigma" + +module TestHelpers + def config(**overrides) + sets = overrides.map { |k, v| "#{k}=#{v}" } + Sigma::Config.load(Sigma::ROOT, overrides: sets) + end + + def quiet_ui = Sigma::UI.new(out: StringIO.new, color: false) +end + +Minitest::Test.include(TestHelpers) diff --git a/test/ui_test.rb b/test/ui_test.rb new file mode 100644 index 0000000..22aca12 --- /dev/null +++ b/test/ui_test.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class UITest < Minitest::Test + def test_durations + ui = quiet_ui + assert_equal "0.4s", ui.duration(0.4) + assert_equal "42s", ui.duration(42) + assert_equal "4m 05s", ui.duration(245) + assert_equal "1h 03m", ui.duration(3780) + end + + def test_counts + ui = quiet_ui + assert_equal "950", ui.count(950) + assert_equal "18.2k", ui.count(18_193) + assert_equal "1.0M", ui.count(999_999) + end + + def test_wrap_and_truncate + ui = quiet_ui + assert_equal ["aaa bbb", "ccc"], ui.wrap("aaa bbb ccc", 8) + assert_equal "abcd…", ui.truncate("abcdefgh", 5) + end + + def test_no_color_when_not_a_tty + out = StringIO.new + ui = Sigma::UI.new(out: out) + ui.say ui.paint("x", :accent, bold: true) + assert_equal "x\n", out.string + end +end