Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/iso-check/inspect.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Looks inside a built ISO's system image (without booting it) and prints
# what this branch changed: sizes, what is and is not installed, which
# services are enabled. Usage: inspect.sh ISO
set -uo pipefail
iso=$1
sudo apt-get install -y -qq squashfs-tools >/dev/null
sudo mkdir -p /mnt/iso
sudo mount -o loop,ro "$iso" /mnt/iso
echo "== ISO: $(stat -c %s "$iso") bytes"
ls -l /mnt/iso/LiveOS /mnt/iso/boot | grep -E 'img|vmlinuz'
list=$(mktemp)
sudo unsquashfs -ll /mnt/iso/LiveOS/rootfs.img > "$list" 2>/dev/null
echo "== listing: $(wc -l < "$list") entries"
has() { grep -qE "squashfs-root/$1( ->.*)?\$" "$list" && echo "present $1" || echo "ABSENT $1"; }
echo "== packages in the image: $(grep -cE 'squashfs-root/var/lib/pacman/local/[^/]+/desc$' "$list")"
echo "== should be there"
for f in usr/bin/seg etc/init.d/seg-zram etc/init.d/seg-tune etc/runlevels/default/seg-zram etc/runlevels/default/seg-tune \
etc/seg/tune.conf etc/sysctl.d/99-seg.conf etc/cron.d/seg-fstrim usr/lib/seg/fstrim-weekly \
etc/makepkg.conf.d/10-seglinux.conf usr/lib/seglinux/zram-setup usr/lib/udev/rules.d/60-seglinux-ioschedulers.rules \
etc/runit/sv/seg-zram/run etc/dinit.d/seg-zram; do has "$f"; done
if [[ $EDITION == full ]]; then
for f in usr/bin/rg usr/bin/fd usr/bin/eza usr/bin/zoxide etc/bash/bashrc.d/seglinux-rust-tools.bashrc usr/bin/xwayland usr/bin/Xwayland; do has "$f"; done
fi
echo "== should be gone"
for f in usr/bin/Xorg usr/bin/spectacle usr/bin/inxi; do has "$f"; done
echo "kernel header folders: $(grep -cE 'squashfs-root/usr/src/[^/]+$' "$list") (0 expected)"
echo "== enabled in the image"
grep -E 'squashfs-root/etc/runlevels/[a-z]+/[^/]+ ->' "$list" | sed -E 's#.*squashfs-root/etc/runlevels/##; s# ->.*##' | sort | tr '\n' ' '
echo
echo "== packages that need the most room (installed size, MiB)"
sudo unsquashfs -f -d /tmp/rt /mnt/iso/LiveOS/rootfs.img var/lib/pacman/local >/dev/null 2>&1
for d in /tmp/rt/var/lib/pacman/local/*/; do
n=$(basename "$d"); size=$(awk '/^%SIZE%/{getline; print $1}' "$d/desc"); echo "$((size / 1048576)) $n"
done | sort -rn | head -12
echo "total installed MiB: $(for d in /tmp/rt/var/lib/pacman/local/*/; do awk '/^%SIZE%/{getline; s+=$1} END{print s+0}' "$d/desc"; done | awk '{t+=$1} END{printf "%d", t / 1048576}')"
130 changes: 130 additions & 0 deletions .github/iso-check/qemu_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""Boot an ISO headless in QEMU, log in on a text console and run seg.

Best effort and for the log only: it drives QEMU's monitor (screendump and
sendkey), reads the screen with tesseract and prints what it saw. It always
exits 0; the image checks before it are what fail the job.

usage: qemu_check.py ISO [boot_seconds]
"""
import os
import socket
import subprocess
import sys
import time

from PIL import Image

ISO = sys.argv[1]
BOOT_WAIT = int(sys.argv[2]) if len(sys.argv) > 2 else 240
MON = "/tmp/qemu-mon.sock"
WORK = "/tmp/qemu-check"
os.makedirs(WORK, exist_ok=True)

KEYS = {" ": "spc", "-": "minus", ".": "dot", ";": "semicolon", "/": "slash", "\n": "ret", "_": "shift-minus", "=": "equal"}


def log(msg):
print(msg, flush=True)


class Monitor:
def __init__(self, path):
deadline = time.time() + 30
while True:
try:
self.sock = socket.socket(socket.AF_UNIX)
self.sock.connect(path)
break
except OSError:
if time.time() > deadline:
raise
time.sleep(0.5)
self.sock.settimeout(5)
self.read()

def read(self):
data = b""
try:
while True:
chunk = self.sock.recv(4096)
if not chunk:
break
data += chunk
if data.rstrip().endswith(b"(qemu)"):
break
except socket.timeout:
pass
return data.decode(errors="replace")

def cmd(self, text):
self.sock.sendall(text.encode() + b"\n")
return self.read()

def key(self, name):
self.cmd(f"sendkey {name}")
time.sleep(0.12)

def type(self, text):
for ch in text:
self.key(KEYS.get(ch, ch))

def screen_text(self, tag):
ppm = f"{WORK}/{tag}.ppm"
self.cmd(f"screendump {ppm}")
time.sleep(1)
png = f"{WORK}/{tag}.png"
try:
img = Image.open(ppm)
img = img.resize((img.width * 2, img.height * 2))
img.save(png)
out = subprocess.run(["tesseract", png, "-", "--psm", "6"], capture_output=True, text=True, timeout=60).stdout
except Exception as e: # the screen may not be written yet
return f"(no screen: {e})"
return "\n".join(line for line in out.splitlines() if line.strip())


def main():
args = ["qemu-system-x86_64", "-m", "3072", "-smp", "2", "-cdrom", ISO, "-boot", "d",
"-display", "none", "-vga", "std", "-monitor", f"unix:{MON},server,nowait",
"-netdev", "user,id=n0", "-device", "virtio-net-pci,netdev=n0"]
if os.access("/dev/kvm", os.R_OK | os.W_OK):
args += ["-enable-kvm", "-cpu", "host"]
log("qemu: using KVM")
else:
args += ["-cpu", "max"]
log("qemu: no KVM, emulating (slow)")
boot_wait = BOOT_WAIT if "-enable-kvm" in args else max(BOOT_WAIT, 420)
qemu = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
try:
mon = Monitor(MON)
start = time.time()
n = 0
while time.time() - start < boot_wait:
time.sleep(30)
n += 1
text = mon.screen_text(f"boot{n}")
log(f"--- screen after {int(time.time() - start)} s:\n" + "\n".join(text.splitlines()[:12]))
# Switch to the second console, where agetty waits for a login.
mon.cmd("sendkey ctrl-alt-f2")
time.sleep(5)
log("--- console 2:\n" + mon.screen_text("tty2"))
mon.type("seglinux\n")
time.sleep(4)
mon.type("seglinux\n")
time.sleep(8)
for i, command in enumerate(["seg info", "seg zram status", "seg tune", "seg svc list --enabled", "seg doctor"]):
mon.type("clear\n")
time.sleep(1)
mon.type(command + "\n")
time.sleep(12)
log(f"--- $ {command}:\n" + mon.screen_text(f"cmd{i}"))
except Exception as e:
log(f"qemu check stopped: {type(e).__name__}: {e}")
finally:
qemu.kill()
return 0


if __name__ == "__main__":
sys.exit(main())
62 changes: 60 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,37 @@ jobs:
- name: Validate the default segrun configuration
run: cargo run --locked -q -p segrun -- --config data/seglinux/segrun.toml config

# seg (tools/seg) is a workspace of its own with its own release profile.
seg:
name: seg (fmt, clippy, tests, size)
runs-on: ubuntu-latest
defaults:
run:
working-directory: tools/seg
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: tools/seg
- run: cargo fmt --check
- run: cargo clippy --locked --all-targets -- -D warnings
# Without the test cfg, where unwrap and expect are compile errors.
- run: cargo clippy --locked -- -D warnings
- run: cargo test --locked
- name: Dependencies stay minimal (seg and lexopt only)
run: |
cargo tree --locked -e normal --prefix none | sort -u
test "$(cargo tree --locked -e normal --prefix none | sort -u | wc -l)" -le 2
- name: Release binary size
run: |
cargo build --locked --release
ls -l target/release/seg
target/release/seg --version
echo "seg is $(stat -c %s target/release/seg) bytes" >> "$GITHUB_STEP_SUMMARY"

scripts:
name: Shell scripts and profile files
runs-on: ubuntu-latest
Expand All @@ -68,12 +99,15 @@ jobs:
run: |
shellcheck -s bash iso/build.sh iso/scripts/*.sh packaging/seglinux/seglinux.install data/system/seglinux-upgrade data/system/seglinux-update \
packaging/make-signing-key.sh crates/seglinux-shell/tests/desktop-services.sh \
data/system/fastfetch.bashrc
data/system/fastfetch.bashrc data/system/rust-tools.bashrc
shellcheck -s sh data/system/apply-branding data/system/pipewire-session \
iso/iso-profiles/seglinux/live-overlay/usr/lib/seglinux/live-autostart \
data/desktop/seglinux-session data/desktop/seglinux-gaming-session \
data/desktop/labwc/autostart data/system/seglinux-terminal data/system/restore-trimmed-files \
data/system/profile-gaming.sh data/greeter/seglinux-greeter-session
data/system/profile-gaming.sh data/greeter/seglinux-greeter-session data/system/zram-setup \
tools/seg/dist/fstrim-weekly tools/seg/dist/runit/*/run tools/seg/dist/runit/seg-zram/finish \
tools/seg/dist/openrc/seg-tune tools/seg/dist/openrc/seg-zram
shellcheck -s bash pkg/segctl/segctl.install
shellcheck -s bash iso/iso-profiles/seglinux-lite/live-overlay/etc/skel/.bash_profile
- name: YAML, JSON and XML syntax
run: |
Expand All @@ -85,6 +119,30 @@ jobs:
sed '/^ *\/\//d' data/fastfetch/config.jsonc | python3 -m json.tool > /dev/null
python3 -c 'import sys, xml.dom.minidom as m; [m.parse(f) for f in sys.argv[1:]]' data/desktop/labwc/rc.xml \
data/greeter/labwc/rc.xml data/greeter/labwc/menu.xml
- name: Package lists have no repeats
run: |
python3 - <<'EOF'
import collections, yaml
failed = False
for profile, common in (("seglinux", "iso/iso-profiles/common/common.yaml"),
("seglinux-lite", "iso/iso-profiles/seglinux-lite/common.yaml")):
c = yaml.safe_load(open(common))
p = yaml.safe_load(open(f"iso/iso-profiles/{profile}/profile.yaml"))
xorg = "packages-xlibre" if p["live-session"].get("use-xlibre") else "packages-xorg"
for init in ("openrc", "runit", "dinit"):
names = []
for key in ("packages-base", "packages-apps", xorg, "packages-misc"):
names += c.get(key) or []
names += (c.get("packages-init") or {}).get(init) or []
names += p["rootfs"]["packages"] + ((p["rootfs"].get("packages-init") or {}).get(init) or [])
repeats = [n for n, k in collections.Counter(names).items() if k > 1]
if repeats:
failed = True
print(f"{profile} ({init}): listed more than once: {', '.join(repeats)}")
raise SystemExit(1 if failed else 0)
EOF
- name: udev rules
run: udevadm verify --no-style data/system/*.rules
- name: Overlay links resolve
run: |
broken=$(find iso/iso-profiles -xtype l)
Expand Down
51 changes: 51 additions & 0 deletions .github/workflows/iso-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: ISO check

# Builds an image on a branch WITHOUT publishing anything, looks inside it
# and boots the Lite one in QEMU. Starts on a push to the work branch whose
# commit message contains "[iso check]". Temporary: removed once the
# seg/rust-lean-overhaul branch is checked.
on:
push:
branches: [seg/rust-lean-overhaul]

permissions:
contents: read

jobs:
iso:
name: Build and inspect (${{ matrix.edition }}, openrc, linux-zen)
if: contains(github.event.head_commit.message, '[iso check]')
runs-on: ubuntu-latest
timeout-minutes: 150
strategy:
fail-fast: false
matrix:
edition: [lite, full]
env:
EDITION: ${{ matrix.edition }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
sudo docker system prune -af > /dev/null
df -h /
- name: Load the loop module
run: sudo modprobe loop
- name: Build
run: |
edition=()
[[ $EDITION == lite ]] && edition=(--lite)
SEGLINUX_PKGVER="0.1.0.$(git rev-list --count origin/main)" \
./iso/build.sh "${edition[@]}" -e docker -i openrc -k linux-zen
- name: Inside the image
run: .github/iso-check/inspect.sh "$(ls iso/out/seglinux-*.iso)"
- name: Boot it in QEMU (Lite)
if: matrix.edition == 'lite'
continue-on-error: true
run: |
sudo apt-get install -y -qq qemu-system-x86 tesseract-ocr python3-pil >/dev/null
[[ -e /dev/kvm ]] && sudo chmod 666 /dev/kvm || true
python3 .github/iso-check/qemu_check.py "$(ls iso/out/seglinux-*.iso)" 150
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
/packaging/seglinux/src/
/packaging/seglinux/pkg/
/packaging/seglinux/*.pkg.tar.*
/tools/seg/target/
/pkg/segctl/*.tar.gz
/pkg/segctl/src/
/pkg/segctl/pkg/
/pkg/segctl/*.pkg.tar.*
*.iso
*.iso.sha256
_site/
Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ members = [
"crates/seglinux-installer",
"crates/seglinux-greeter",
]
# seg has its own workspace and release profile (tools/seg/Cargo.toml).
exclude = ["tools/seg"]

[workspace.package]
version = "0.1.0"
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ published.
starts it, under GameMode when it is a game.
* **Fast, systemd-free base.** OpenRC by default (parallel boot), or runit
or dinit; rolling release.
* **`seg`, one small tool for the system.** `seg info`, `seg svc` (the same
commands on OpenRC, runit, dinit and s6), `seg clean`, `seg tune`
(light, balanced or performance), `seg zram` and `seg doctor`, which finds
problems and prints their fixes. A single Rust binary of about 550 KiB; see
[tools/seg](tools/seg/README.md). What the images weigh and what was taken
off them: [docs/lean-audit.md](docs/lean-audit.md).

## Website

Expand Down
1 change: 1 addition & 0 deletions crates/seglinux-installer/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ pub fn demo_facts() -> Facts {
network: true,
memory: 8 * GIB,
gpus: vec![],
kernel: Some("linux-zen".into()),
timezones: Some(set(&[
"UTC",
"Africa/Tunis",
Expand Down
6 changes: 5 additions & 1 deletion crates/seglinux-setup/src/answer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ pub enum CommandPass {
}

/// Services enabled on every installation (missing ones are skipped).
/// `cupsd` is installed but not started: few gaming PCs print, and OpenRC has
/// no socket activation to start it on demand (`seg svc enable cupsd`).
pub const DEFAULT_SERVICES: &[&str] = &[
"dbus",
"elogind",
Expand All @@ -332,12 +334,14 @@ pub const DEFAULT_SERVICES: &[&str] = &[
"NetworkManager",
"greetd",
"bluetoothd",
"cupsd",
"cronie",
"syslog-ng",
"ntpd",
"acpid",
"power-profiles-daemon",
// From the segctl package: zram swap and the saved `seg tune` profile.
"seg-zram",
"seg-tune",
];

/// Groups every user joins (missing ones are skipped).
Expand Down
3 changes: 2 additions & 1 deletion crates/seglinux-setup/src/disk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,8 @@ impl Layout {
));
}
if self.zram.is_some() {
// Created and formatted by /etc/udev/rules.d/99-seglinux-zram.rules.
// Created and formatted at boot: the udev rule 60-seglinux-zram.rules runs
// seg zram on (the seg-zram service does too).
out.push_str("/dev/zram0 none swap defaults,discard,pri=100 0 0\n");
}
out
Expand Down
Loading
Loading