User wants full chained pipeline:
GRUB veilor-os → plymouth text → branded gum installer →
install progress → reboot → installed system text-clean.
Live ISO was missing pieces from the install ks polish. v0.5.24
brings live ks into parity:
- bootloader --append: add plymouth.enable=0 (kills fedora splash,
exposes tty1 with gum installer banner immediately)
- chroot %post: GRUB_DISTRIBUTOR="veilor-os" (menu title)
- chroot %post: GRUB_CMDLINE_LINUX_DEFAULT="" (drop rhgb quiet)
- chroot %post: plymouth-set-default-theme details (text scroll
fallback if plymouth.enable=0 ignored)
- grub2-mkconfig regen with new branding
Result on next ISO build:
- Boot from ISO → GRUB shows "veilor-os" entry
- Pick veilor-os → text scroll (no fedora splash)
- TTY1 lands on gum installer banner + menu (no plymouth swallow)
- Install completes → reboot → installed system already has the
same text-mode boot + LUKS prompt config from v0.5.22-23
v0.5.22 plymouth details theme works (text scroll boot visible). But
LUKS prompt still never fires — dracut spins on dev-disk-by-uuid for
2+ min then drops to emergency shell.
Hypothesis: anaconda --cmdline mode skips/breaks the bootloader auto-
add of rd.luks.uuid arg. Without it, cryptsetup-generator in
initramfs has no UUID to create unlock unit for → no prompt → no
unlock → dracut times out.
Fix: chroot %post detects LUKS partition via blkid TYPE=crypto_LUKS,
injects `rd.luks.uuid=luks-<uuid>` into GRUB_CMDLINE_LINUX if not
already present. Belt-and-braces — if anaconda DID add it, sed
checks first.
Followed by grub2-mkconfig regen (already in script) so installed
grub.cfg picks up the new cmdline arg.
v0.5.21 set plymouth.enable=0 — plymouth-start.service still ran +
ate LUKS keystrokes. Boot fell to dracut emergency shell.
Better path: plymouth IS running but in TEXT mode via built-in
`details` theme (scrolling boot log, no graphics, no fedora logo).
LUKS prompt renders as text "Please enter passphrase for...:".
Plymouth still owns the prompt → keystrokes go through.
Changes:
- Drop plymouth.enable=0 from cmdline (let plymouth run)
- chroot %post: plymouth-set-default-theme details
- Drop rhgb quiet from GRUB_CMDLINE_LINUX_DEFAULT (all kernel msgs visible)
- dracut --force --regenerate-all (new theme baked into initramfs)
Result: text scroll boot → text LUKS prompt → text scroll → SDDM.
Onyx aesthetic. Branded plymouth theme deferred to v0.6.
User wants onyx-style boot: pure text scroll → LUKS prompt → text scroll
→ SDDM. No fedora splash, no plymouth UI.
Solution: keep plymouth PACKAGE installed (Fedora's dracut module
ships LUKS-prompt machinery via plymouth), but disable plymouthd at
runtime via kernel cmdline `plymouth.enable=0`.
Effect:
- plymouthd starts → reads cmdline → exits
- systemd-ask-password sees no plymouth daemon → falls back to
systemd-tty-ask-password-agent on /dev/console
- LUKS prompt rendered as text "Please enter passphrase for /dev/dm-0: "
- All kernel/systemd messages visible
- SDDM still launches at graphical.target (real install)
Applied to both:
- LIVE ks bootloader --append (so live boot text-mode + installer
visible on tty1, no splash hiding it)
- Generated install ks bootloader --append (so installed system
text-boots with LUKS prompt)
v0.6 will rebrand plymouth theme + re-enable for branded splash. For
v0.5.0 ship: minimal/text aesthetic matches user's onyx daily driver.
v0.5.18 added crypt + systemd-cryptsetup to dracut.conf.d/99-veilor-
no-plymouth.conf. Boot test still failed: dracut-initqueue stuck
waiting on dev-disk-by-uuid → systemd-cryptsetup never fired.
Diagnosis: %post chroot used bash glob `for kver in /lib/modules/*/`.
In chroot, shell may be dash + nullglob unset → unmatched glob
expands literally to "/lib/modules/*/" → dracut --kver "/lib/modules/*/"
fails silently with `|| true`. Initramfs never regenerated → still
contains the v0.5.14 omit_dracutmodules-only config without crypt.
Fix: dracut --force --regenerate-all (walks /lib/modules internally,
no shell glob needed). One call regens all kernel initramfses with
the new dracut.conf.d in scope.
v0.5.17 boot stuck at dracut-initqueue waiting for LUKS device that
never unlocks. Plymouth removal also dropped the dracut machinery
that prompts user for LUKS passphrase. Pure-text systemd-tty-ask-
password-agent works in real root but isn't bundled into initramfs.
Fix: dracut.conf.d/99-veilor-no-plymouth.conf:
add_dracutmodules+=" crypt systemd-cryptsetup "
install_items+=" /usr/bin/systemd-tty-ask-password-agent "
Result: dracut bundles systemd-cryptsetup + ask-password binary into
initramfs. cryptsetup-generator creates unit at boot, ask-password-
agent prompts on tty1 in text mode "Please enter passphrase for...:".
sendkey-friendly + works on real hardware.
Per user: bottom-screen "fedora" logo + spinner during boot is fedora-
logos package + GRUB graphical theme. v0.5.17 strips it for now —
v0.6 will re-introduce plymouth with veilor-black theme + LUKS-prompt-
friendly config.
Changes:
1. Kernel cmdline: add `logo.nologo console=tty0`
- logo.nologo: suppress kernel boot logo (Tux/Fedora leaf)
- console=tty0: explicit text console output
- `quiet` already absent → all kernel msgs scroll
2. /etc/default/grub patches in chroot %post:
- GRUB_DISTRIBUTOR="veilor-os" (menu titles read "veilor-os" not
"Fedora Linux 43...")
- GRUB_THEME= (empty — no graphical theme)
- GRUB_TERMINAL_OUTPUT="console" (text-mode menu, no gfxterm)
- Drop GRUB_BACKGROUND
3. Regen grub.cfg + EFI grub.cfg with new branding
Result: pure text scroll boot, white-on-black, no fedora artifacts.
veilor-os menu title in GRUB picker. "Hackery dope" aesthetic.
For real users wanting splash → v0.6 ships plymouth + veilor theme.
v0.5.14 ships a working install but auto-install harness can't SSH-
validate post-reboot — admin user has no authorized_keys, hardened
sshd rejects all auth. SSH up + listening but no path to log in.
Fix: detect_seed_pubkey() searches /dev/sr* for a NoCloud cidata
volume (label "cidata"), parses ssh_authorized_keys: list from
user-data, returns first key. generate_ks() then embeds as
sshkey --username=admin "ssh-ed25519 AAAA... user@host"
right after the user= directive. Anaconda creates
/home/admin/.ssh/authorized_keys with right perms (700/600).
Real users: drop a NoCloud seed iso next to install media (or via
USB), pubkey lands automatically. auto-install.sh: existing run-vm.sh
seed logic already builds the cidata iso with host pubkey.
If no seed → directive line empty → anaconda treats as no-op → SSH
validation blocked but install otherwise unaffected.
v0.5.13 added omit_dracutmodules+=plymouth + dracut --force regen in
chroot %post. Boot test still showed plymouth-start.service running.
Theory: the chroot dracut --force --kver loop didn't fire (kver glob
may have been empty in chroot), or anaconda regenerated initramfs
AFTER our %post and ignored our config drop-in.
Simpler fix: don't ship plymouth at all. Add `-plymouth
-plymouth-plugin-label -plymouth-system-theme` to kickstart %packages.
With no plymouth package on disk, dracut can't bundle it into
initramfs regardless of dracut.conf state.
The /etc/dracut.conf.d snippet + /dev/null masks from v0.5.12-13 stay
as belt-and-braces — harmless once plymouth is absent.
v0.5.12 added /dev/null symlinks for plymouth services on real root.
Boot test confirmed plymouth STILL starts: it lives in initramfs
(dracut module 90plymouth) which has its own bundled service files,
unaffected by /etc/systemd/system/ masks on the installed btrfs.
Two-layer fix:
1. /etc/dracut.conf.d/99-veilor-no-plymouth.conf:
omit_dracutmodules+=" plymouth "
Then `dracut -f --kver $kver` to regenerate initramfs sans plymouth.
2. Keep /dev/null symlinks for post-pivot real-root masking.
Result: LUKS prompt rendered as text by systemd-tty-ask-password-agent
on tty1 — sendkey-friendly, hardware-realistic.
v0.5.11 used `systemctl mask plymouth-*.service` in generated kickstart
%post chroot block. systemctl needs systemd running, which it isn't in
anaconda chroot — calls failed silently (|| true).
Boot test confirmed: post-reboot showed both:
Started plymouth-start.service - Show Plymouth Boot Screen
Started systemd-ask-password-plymouth.path - Forward Password Requests
Fix: write /dev/null symlinks directly (`ln -sf /dev/null
/etc/systemd/system/<unit>`). Achieves what mask does without needing
systemd. Also adds the path-activated unit
(systemd-ask-password-plymouth.path) which actually pulls plymouth in
during dracut-initqueue.
v0.5.10 added plymouth.enable=0 + rd.plymouth=0 to kernel cmdline,
but `plymouth-start.service` still registered and ran in real-root
boot. LUKS prompt remained invisible — dracut-initqueue stuck
waiting for /dev/disk/by-uuid/<luks> to materialize.
Belt-and-suspenders: mask plymouth-{start,quit,quit-wait,read-write,
switch-root}.service in the generated kickstart's %post chroot so the
units can never start.
Effect: systemd-tty-ask-password-agent handles LUKS prompt directly
on tty1 with text "Please enter passphrase for disk ...:" — sendkey-
friendly + works on real hardware too.
v0.5.9 GRUB-installs cleanly. Disk boots, dracut reaches
cryptsetup.target, systemd-ask-password-plymouth.path armed. But
plymouth never switches from boot-splash mode to password-prompt mode
— sendkey'd passphrases bounce, dracut waits forever on
dev-disk-by-uuid.
Workaround: pass `plymouth.enable=0 rd.plymouth=0` to kernel cmdline.
Eliminates plymouth-ask-password-plugin as a layer; LUKS prompt
appears as plain text on tty1 ("Please enter passphrase for disk... :").
Bonus: aligns with hardening posture. Plymouth is graphical eye-candy
running in pid 1's namespace during early boot. Fewer moving parts =
smaller attack surface. veilor-os defaults to text boot; users wanting
splash can re-enable post-install.
Auto-install round 3 hit:
Configuring storage
Creating disklabel on /dev/vda
Creating luks on /dev/vda3
Creating lvmpv on /dev/mapper/luks-...
Creating btrfs on /dev/mapper/veilor-root
Running in cmdline mode, no interactive debugging allowed.
The exact error message is:
mount failed: wrong fs type, bad option, bad superblock on
/dev/mapper/veilor-root, missing codepage or helper program
LVM+btrfs combination causes mount failure under anaconda --cmdline.
mkfs.btrfs runs but post-create mount can't find a valid superblock.
Fix: drop LVM intermediary. Use native btrfs-on-LUKS — same pattern
Fedora KDE Spin uses by default. Cleaner, snapshot story unchanged
(btrfs subvols give us root/home split + rollback potential without
LVM's overhead).
New layout:
vda1: efi (600M)
vda2: ext4 /boot (1024M)
vda3: LUKS2 → btrfs (label=veilor)
├── subvol root → /
└── subvol home → /home
ksvalidator clean on the new template.
v0.5.5 fixed AnacondaError: 'LANG'. Auto-install harness then
hit next crash:
TypeError: expected str, bytes or os.PathLike object, not NoneType
File "/usr/lib64/python3.14/site-packages/pyanaconda/display.py", line 223
wl_socket_path = os.path.join(os.getenv("XDG_RUNTIME_DIR"), ...)
anaconda's display.setup_display() unconditionally tries to set up
Wayland socket path. tty1 has no XDG_RUNTIME_DIR set. None gets passed
to os.path.join → TypeError.
Two-part fix:
1. export XDG_RUNTIME_DIR=/run/user/0 + mkdir, so even if anaconda
probes it, the env var has a valid string value.
2. Pass --cmdline to anaconda. Fully unattended text-only mode, no
Wayland/X/TUI. Right fit for our gum-driven kickstart flow where
ks is self-contained (disk, pw, locale all pre-answered).
Combined effect: anaconda goes straight from CLI parse → kickstart
execute → reboot. No display subsystem at all.
Surfaced by test/auto-install.sh round 2.
Auto-install harness ran through gum installer cleanly but anaconda
crashed at startup:
File "/usr/lib64/python3.14/site-packages/pyanaconda/keyboard.py",
line 152, in activate_keyboard
sync_run_task(task_proxy)
...
pyanaconda.modules.common.errors.general.AnacondaError: 'LANG'
Anaconda's keyboard.activate_keyboard() reads $LANG and bombs if unset.
TTY1 (where veilor-installer runs) inherits no locale by default;
gum/whiptail don't set it.
Fix: export LANG + LC_ALL = user's chosen locale (defaulting to
en_GB.UTF-8) before invoking anaconda.
Found via test/auto-install.sh end-to-end run.
Workflow now publishes ISO as 1900M chunks. Test harness needs to:
1. gh release download --pattern '*.iso.part-*'
2. cat parts back into single ISO
3. verify sha256 of all parts
If invoked with no ISO arg, auto-fetches from ci-latest release.
Falls back to local ISO path if given as arg (existing behavior).
Reassembled ISO lives at ~/veilor-iso/ci-latest/.
test/auto-install.sh boots ISO, drives gum installer via QEMU
monitor sendkey with hardcoded test answers, waits for anaconda,
reboots into installed system, SSHs in, runs validation checklist.
Co-authored-by: veilor-org <admin@veilor.org>
GH release asset size limit = 2 GiB. Veilor ISO ~2.8 GiB (KDE base +
hardening + grafted /veilor/ tree). zstd -19 only achieves 96.67%
compression (squashfs already xz-compressed). Splitting is the fix.
Workflow now:
- Splits ISO with `split -b 1900M -d --suffix-length=2`
- Drops original ISO before upload (would fail at >2 GiB)
- Includes per-part sha256 for reassembly verification
- Release notes include cat reassembly command
test/auto-install.sh will need follow-up commit to download + cat
the parts before booting.
Two follow-ups to 75a68a1 (releases switchover):
1. action-gh-release got 403 "Resource not accessible by integration"
because default GITHUB_TOKEN has read-only on contents. Added
workflow-level `permissions: contents: write`.
2. Failure-path artifact upload still hit quota wall. Replaced with
inline `tail` of build/out/build.log + anaconda program.log
directly to job log. No artifact upload = no quota.
Artifact storage quota (50GB Pro tier) maxed out with ~18 iterations
of 2.7GB ISOs. Quota recalc 6-12h not in our cadence. Builds succeed
but upload step fails — wasting CI minutes + blocking testing.
Switch to GitHub Releases (no storage quota):
- Every successful build on main updates rolling `ci-latest`
prerelease draft. Replaces files in place.
- Tag-driven releases (v*.*.*) keep their existing publish path.
- Build logs remain as artifacts (small + opt-in failure only,
retention=1d).
User can `gh release download ci-latest --repo veilor-org/veilor-os`
or browse to releases page. No more artifact quota wall.
User boot-tested v0.5.2 + in-VM patch. Requested polish:
- Banner: replace slant-figlet `veilor-os` + "hardened. branded. yours."
tagline with figlet ANSI Regular `VEILOR OS` wordmark (5-line block).
No tagline. Border preserved by gum style call.
- Menu header: "Welcome. What would you like to do?" → "Welcome"
- Menu labels:
"Install veilor-os to disk" → "Install"
"Try live — desktop (KDE Plasma)" → "live - (KDE)"
"Try live — shell" → "live - shell"
"Reboot" / "Power off" unchanged
- Hostname prompt removed — hardcoded to "veilor". User can change
post-install via hostnamectl. Cuts one prompt from install flow.
Confirmation summary drops the Hostname row.
- Locale options trimmed: en_GB.UTF-8, en_US.UTF-8 only (was 4 incl
de_DE, fr_FR). i18n not v0.5 priority.
Verified in-VM rendering of the menu changes via sed-patch on v0.5.2
ISO. ksvalidator + bash -n clean.
QEMU boot test of v0.5.2 found service still status=1/FAILURE despite
file present at /usr/local/bin/veilor-installer. Root cause via
`bash -x`: `exec > >(tee -a "$LOG") 2>&1` ran BEFORE require_tty
check; process substitution replaces fd1 with a pipe, so [[ -t 1 ]]
returns false → require_tty bails out with [ERR] message.
Order was self-inflicted bug from v0.5.0. Fix: move require_tty
function definition + call BEFORE the tee redirect. Drop the
redundant require_tty call in the entry block (would fail post-redirect).
QEMU boot test of v0.5.1 (commit 3cbffaf) revealed both scripts
missing from /usr/local/sbin/ on running system, despite being in
overlay/usr/local/sbin/ in the source tree.
Root cause: Fedora's filesystem package (or post-install scriptlet)
rewrites /usr/local/sbin → /usr/local/bin symlink AFTER kickstart
%post --nochroot's overlay copy runs. The cp -a placed files in
/usr/local/sbin/ as a real directory; the symlink replacement
deleted them.
Confirmed via tty diagnostic: `ls -la /usr/local` shows
`lrwxrwxrwx ... sbin -> bin` with bin mtime predating sbin symlink
ctime by ~5min — overlay copy ran first, scriptlet rewrote sbin
second.
Fix: move both binaries to overlay/usr/local/bin/ where they're
safe from the symlink rewrite. Update all references:
- kickstart/veilor-os.ks chmod path + chown + diagnostic ls
- overlay/etc/systemd/system/getty@tty1.service.d/veilor-installer.conf ExecStart
- overlay/etc/systemd/system/veilor-firstboot.service ExecStart
- scripts/selinux/build-policy.sh fcontext + restorecon paths
- generated install ks template inside veilor-installer
Service drop-in stays at /etc/systemd/system/getty@tty1.service.d/
unchanged. The veilor-installer binary in /usr/local/bin/ is
discoverable via $PATH same as before.
Two user-facing commands shipped in overlay/usr/local/bin/.
Wraps dnf+flatpak update flow and read-only health diagnostic.
Uses gum if available, plain output otherwise. No kickstart wiring
yet beyond chmod — full integration in v0.6.0 release.
Co-authored-by: veilor-org <admin@veilor.org>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* v0.5.1: gum installer + full veilor-os ks generation
Two changes, one commit (matches v0.5.1 milestone):
1. Swap whiptail → gum (charm.sh)
- Source /usr/share/veilor-os/assets/installer/colors.gum at top so all
prompts pick up branded GUM_* env vars.
- Render banner.txt via `gum style --border rounded`.
- Wrap every prompt behind prompt_choose / prompt_input / prompt_password
/ prompt_confirm / prompt_message / prompt_error helpers that dispatch
gum→whiptail based on `command -v gum`. Defensive: minimal images
without /usr/local/bin/gum still get a working TUI.
- Main menu items now use literal labels (case-matched), not 1..5 tags.
2. Generated kickstart now installs full veilor-os
Previously emitted a vanilla F43 KDE + ~12 hardening packages with no
overlay/scripts/branding. Now mirrors live ks (kickstart/veilor-os.ks
63-141) for %packages, plus:
- %post --nochroot copies overlay/, scripts/, assets/ from
/run/install/repo/veilor (single source — boot ISO mount path).
- %post (chroot) runs scripts/10-harden-base.sh, 20-harden-kernel.sh,
selinux/build-policy.sh, kde-theme-apply.sh.
- `chage -d 0 admin` so first login forces password change. (Account
itself is created by anaconda from the `user` directive — admin pw
collected via gum is passed through --plaintext.)
- `systemctl set-default graphical.target` (real install boots SDDM,
not the TTY1 installer like live).
- Drops live-only entries (livesys-scripts, anaconda-live, dracut-live,
isomd5sum, xorriso, livesys.service enables).
Tested: bash -n clean; ksvalidator on a substituted-placeholder copy
exits 0.
gum binary itself (/usr/local/bin/gum) is vendored by a separate
build-side change — not in this PR.
* fix: escape sed special chars + reject & | / in passwords
Reviewer found a password like aA1!@#%^&*()_-+={}[] becomes
aA1!@#%^__ADMIN_PW__*()_-+={}[] because sed expands & to matched
pattern. Two layers of defense:
1. validate_pw rejects & | / newline at input
2. sed_escape() helper escapes any remaining special chars before
substitution
---------
Co-authored-by: veilor-org <admin@veilor.org>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Drops in branded assets the v0.5.1 installer rewrite (whiptail -> gum)
will consume: ASCII banner, sourceable GUM_* env-var palette matching
the veilor-black KDE color scheme, and an INSTALLER.md walkthrough.
The existing v0.5.0 veilor-installer script is intentionally untouched
so the swap can land in a separate, focused PR.
Co-authored-by: s8n-ru <279801990+s8n-ru@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* v0.5.1 build: vendor gum binary + graft /veilor/ onto ISO
- gum 0.17.0 pinned by sha256, downloaded into overlay/usr/local/bin/
so installer can use Charm.sh TUI primitives.
- After livecd-creator produces ISO, extract+re-pack with /veilor/
containing overlay+scripts+assets so installer-generated ks can
copy them into target system at install time.
* fix: extract original ISO boot stanza programmatically (no hardcoded paths)
Reviewer found `-e images/efiboot.img` was wrong — Fedora livecd-creator
places efiboot.img in isolinux/ not images/. Plus missing
--mbr-force-bootable + -partition_* flags would produce hybrid MBR/GPT
mismatch refused by some BIOS firmwares.
Fix: extract original ISO's exact boot stanza via
`xorriso -report_el_torito as_mkisofs` and replay it via eval.
Guarantees exact match, immune to upstream Fedora layout changes.
---------
Co-authored-by: veilor-org <admin@veilor.org>
Pre-existing shellcheck failure blocking all PR merges. Standard
"double-quote array expansions" fix. No behavior change.
Co-authored-by: veilor-org <admin@veilor.org>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Lint flagged false positives on audit reports + CHANGELOG that
self-reference forbidden strings as findings. Restrict scan to
kickstart/, overlay/, scripts/, assets/, build/ — actual ship state.
Co-authored-by: veilor-org <admin@veilor.org>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Bugs found by agent linter on v0.5.0-alpha:
1. logvol missing --size: ksvalidator rejected. Added --size=8192 --grow.
2. bootloader --location=mbr on UEFI: conflicts with /boot/efi part.
Switched to --location=none (anaconda auto-detects EFI vs BIOS).
3. lsblk awk truncated multi-word disk models ("WD PC SN740" → "WD").
Now collapses model spaces to underscores, preserves full string.
Also added mmcblk to disk regex (eMMC support).
4. Heredoc with $VAR expansion + passwords containing $/`/" corrupted
generated ks. Now: single-quoted heredoc + sed placeholder
substitution. Plus input validator rejects "$\` chars in passwords.
ksvalidator clean on sample generated ks.
bash -n clean.
CI build still in flight (3328ffb). This pushes a new commit; CI will
run again with these fixes. Net delay: zero (3328ffb's installer was
broken anyway, so its ISO unusable for install path).
- display-manager.service symlink: livecd-creator skips alias creation
vs Anaconda installer; without it sddm stays inactive at graphical.target
- admin user: replace `passwd -d` with throwaway pw `veilor` + chage -d 0
(SDDM rejects blank pw by default, breaks first-login flow)
Tested in QEMU v0.2.5: confirmed sddm enabled but inactive after boot,
and blank-pw login at SDDM returns "Login Failed".
Live ISO stalled at dracut for 5+min on KVM with init_on_alloc=1
init_on_free=1 — kernel zeroes every page on alloc/free, brutal in
virtualized memory. Keep slab_nomerge + lockdown=integrity +
randomize_kstack + vsyscall=none for live (cheap). Re-add memory
init flags on installed system via veilor-firstboot post-install
GRUB edit (planned v0.3).
Upstream bug in /usr/lib/python3.14/site-packages/imgcreate/live.py:
if self._isDracut:
args["rootlabel"] = "live:LABEL=%(fslabel)s" # WRONG
else:
args["rootlabel"] = "CDLABEL=%(fslabel)s"
For dracut path on EFI grub it writes `root=live:LABEL=...` but
dracut needs `live:CDLABEL=...` to look up ISO9660 by CD volume id.
Result: parse-livenet hook stalls indefinitely.
CI now sed-patches the file in-place before build. Reported upstream
livecd-tools as separate task.
- kde-theme-apply.sh: search /etc/os-release.d/veilor (where overlay
put it) before falling back to $REPO/overlay path. Rewire symlinks
cleanly: /etc/os-release → ../usr/lib/os-release.
- Kickstart: useradd admin in chroot %post since livecd-creator skips
the `user` directive (no installer phase). Blank pw + expired = forced
reset at first login same as before.
Found via debugfs: overlay copy succeeds (veilor-power, tuned profiles,
sshd-hardening, sudoers, systemd units all present in v0.2.1 rootfs) but
`mkdir + cp assets/scripts` aborted with set -eu — leaves /usr/share/
veilor-os missing → all chroot %post scripts fail. Switch to set +e on cp
plus persist trace log to /var/log/veilor-nochroot.log for next debug.