feat: scaffold Arcline OS build system

Scaffold the Arcline OS build system ("the wires"): a transparent,
auditable pipeline that turns a Debian bookworm base into hardened OS
images for the server, workstation, and cloud editions.

- Makefile orchestrates everything (make iso-<edition>, check, test,
  toolchain, clean); versions.mk is the single source of truth for
  versions and paths.
- scripts/ is the plain-bash pipeline: debootstrap -> install packages
  -> apply overlays -> in-chroot configure -> live ISO, plus a rootfs
  archive along the way.
- ARCLINE_TOOLCHAIN=auto|skip|require controls whether the 11 Go tools
  are bundled into an image (auto by default; minimal builds available
  via make iso-<edition>-minimal).
- GPL-3.0 licensed, sponsored by Arcline IT LLC.
This commit is contained in:
Blake Ridgway
2026-08-21 13:15:43 -05:00
commit 14e5ea9e1e
12 changed files with 765 additions and 0 deletions

18
.gitignore vendored Normal file
View File

@@ -0,0 +1,18 @@
# Build artifacts
build/
*.iso
*.tar.xz
*.deb
*.log
# Toolchain output
toolchain/out/
# Local mirrors / caches
.cache/
mirror/
# Editor cruft
*.swp
*~
.DS_Store

20
LICENSE Normal file
View File

@@ -0,0 +1,20 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2026 Arcline IT LLC
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
The full text of the license is available at:
https://www.gnu.org/licenses/gpl-3.0.txt

104
Makefile Normal file
View File

@@ -0,0 +1,104 @@
# ─────────────────────────────────────────────────────────────────────────────
# Arcline OS — top-level build orchestration
#
# The Makefile is a thin, readable wrapper around the scripts/ pipeline. It
# exists so that "make iso-server" is the one command that takes a host with
# the right build deps to a bootable Arcline OS ISO.
#
# Everything that has real logic lives in scripts/ — this file only wires
# editions to targets and passes through environment.
#
# Quick reference:
# make help → this message
# make deps → install host build dependencies
# make check → validate manifests + shell syntax (fast, safe)
# make rootfs-<edition> → build just the rootfs for an edition
# make iso-<edition> → build a bootable ISO for an edition
# make iso → build all editions
# make toolchain → build the 11 Arcline Go tools into .deb
# make test → run the smoke tests against a built image
# make clean → wipe build/
# ─────────────────────────────────────────────────────────────────────────────
SHELL := /bin/bash
.DEFAULT_GOAL := help
include versions.mk
# Pass edition + env through to the scripts.
export DISTRO_NAME DISTRO_ID VERSION RELEASE_NAME
export DEBIAN_SUITE DEBIAN_MIRROR SECURITY_MIRROR ARCH
export KERNEL_PACKAGE KERNEL_VERSION
export BUILD_DIR ROOTFS_DIR IMAGE_DIR DEB_DIR LOG_DIR ARTIFACT_DIR
export TOOLCHAIN_REPO
ROOT := $(CURDIR)
EDITION := $(filter-out $@,$(MAKECMDGOALS))
.PHONY: help deps check toolchain test clean
# ── help ────────────────────────────────────────────────────────────────────
help:
@echo "Arcline OS build targets"
@echo "───────────────────────"
@echo " make deps Install host build dependencies"
@echo " make check Validate manifests + shell syntax"
@echo " make rootfs-<edition> Build rootfs (server|workstation|cloud)"
@echo " make iso-<edition> Build ISO (server|workstation|cloud)"
@echo " make iso Build ISO for every edition"
@echo " make iso-<edition>-minimal Build ISO WITHOUT the Arcline toolchain"
@echo " make toolchain Build the 11 Go tools into .deb"
@echo " make test Run smoke tests against a built image"
@echo " make clean Remove build/ artifacts"
@echo
@echo "Configuration (see versions.mk):"
@echo " VERSION=$(VERSION) DEBIAN_SUITE=$(DEBIAN_SUITE) ARCH=$(ARCH)"
@echo " ARCLINE_TOOLCHAIN=auto|skip|require (toolchain in image builds)"
# ── host deps ────────────────────────────────────────────────────────────────
deps:
@./scripts/check-host-deps.sh
# ── validation (fast, no network, safe on any machine) ──────────────────────
check:
@./scripts/validate.sh
# ── edition builds ──────────────────────────────────────────────────────────
# ARCLINE_TOOLCHAIN controls whether the 11 Go tools are included:
# auto (default) install if build/debs/ has .deb files, else skip
# skip never install (minimal image)
# require fail if no .deb files are available
# The *-minimal targets are a shortcut for ARCLINE_TOOLCHAIN=skip.
define ISO_RULE
.PHONY: rootfs-$(1) iso-$(1) clean-$(1) rootfs-$(1)-minimal iso-$(1)-minimal
rootfs-$(1):
@./scripts/build-edition.sh $(1) rootfs
iso-$(1):
@./scripts/build-edition.sh $(1) iso
rootfs-$(1)-minimal:
@ARCLINE_TOOLCHAIN=skip ./scripts/build-edition.sh $(1) rootfs
iso-$(1)-minimal:
@ARCLINE_TOOLCHAIN=skip ./scripts/build-edition.sh $(1) iso
clean-$(1):
@rm -rf "$(ROOTFS_DIR)/$(1)" "$(IMAGE_DIR)/$(1)"
endef
$(foreach e,$(EDITIONS),$(eval $(call ISO_RULE,$(e))))
iso: $(foreach e,$(EDITIONS),iso-$(e))
# ── toolchain ───────────────────────────────────────────────────────────────
toolchain:
@./toolchain/build-tools.sh
# ── tests ───────────────────────────────────────────────────────────────────
test:
@./tests/run-tests.sh
# ── clean ───────────────────────────────────────────────────────────────────
clean:
@rm -rf "$(BUILD_DIR)"
@echo "removed $(BUILD_DIR)/"

36
scripts/apply-overlays.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Arcline OS — overlay application
#
# scripts/apply-overlays.sh <rootfs> <edition>
#
# overlays/ is organised as layered filesystem trees:
# overlays/base/<abs path> → every edition
# overlays/<edition>/<abs path> → that edition only
#
# Files are copied preserving structure and permissions. Later layers win.
# overlays/base is applied first, then the edition overlay.
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
TARGET="${1:?usage: apply-overlays.sh <rootfs> <edition>}"
EDITION="${2:?usage: apply-overlays.sh <rootfs> <edition>}"
validate_edition "$EDITION"
[[ -d "$TARGET" ]] || die "rootfs '$TARGET' does not exist"
apply_layer() {
local layer="$1"
local src="$ROOT/overlays/$layer"
[[ -d "$src" ]] || { warn "overlay layer '$layer' not present, skipping"; return 0; }
log "applying overlay layer: $layer"
# Copy the *contents* of the layer dir into the rootfs, keeping dotfiles,
# symlinks, and permissions. Never clobber across layers silently — but
# cp -a with overwrite is the desired "later layer wins" behaviour here.
cp -a "$src/." "$TARGET/"
}
apply_layer base
apply_layer "$EDITION"
log "overlays applied ✓"

27
scripts/build-edition.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Arcline OS — edition build orchestrator
#
# scripts/build-edition.sh <edition> <stage>
# stage: rootfs | iso
#
# Thin dispatcher so the Makefile can stay dumb.
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
EDITION="${1:?usage: build-edition.sh <edition> <rootfs|iso>}"
STAGE="${2:?usage: build-edition.sh <edition> <rootfs|iso>}"
validate_edition "$EDITION"
case "$STAGE" in
rootfs)
"$ROOT/scripts/build-rootfs.sh" "$EDITION"
;;
iso)
"$ROOT/scripts/build-iso.sh" "$EDITION"
;;
*)
die "unknown stage '$STAGE' (expected: rootfs | iso)"
;;
esac

81
scripts/build-iso.sh Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Arcline OS — live ISO builder
#
# scripts/build-iso.sh <edition>
#
# Wraps a rootfs into a hybrid (BIOS+UEFI) live ISO:
# 1. ensure the rootfs exists (build it with live-boot support if needed)
# 2. stage kernel + initramfs + squashfs in isofiles/live
# 3. write the grub boot config (live-boot: boot=live)
# 4. grub-mkrescue → build/artifacts/arcline-<edition>-<version>-<arch>.iso
#
# Requires root for the rootfs stage; the ISO assembly itself runs unprivileged.
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
EDITION="${1:?usage: build-iso.sh <edition>}"
validate_edition "$EDITION"
ROOTFS="$ROOTFS_DIR/$EDITION"
ISOFILES="$IMAGE_DIR/$EDITION/isofiles"
ARTIFACT="$ARTIFACT_DIR/arcline-$EDITION-$VERSION-$ARCH.iso"
# ── 1. rootfs ───────────────────────────────────────────────────────────────
if [[ ! -d "$ROOTFS" ]]; then
log "rootfs missing — building with live-boot support"
ARCLINE_LIVE=1 "$ROOT/scripts/build-rootfs.sh" "$EDITION"
fi
# live-boot must be present in the rootfs for the ISO to boot
if [[ ! -d "$ROOTFS/lib/live" && ! -d "$ROOTFS/usr/lib/live" ]]; then
warn "rootfs has no live-boot support; rebuilding with ARCLINE_LIVE=1"
ARCLINE_LIVE=1 "$ROOT/scripts/build-rootfs.sh" "$EDITION"
fi
# ── 2. stage files ──────────────────────────────────────────────────────────
log "staging ISO files for edition '$EDITION'"
rm -rf "$ISOFILES"
mkdir -p "$ISOFILES/live" "$ISOFILES/boot/grub"
KERNEL="$(find "$ROOTFS/boot" -maxdepth 1 -name 'vmlinuz-*' | sort -V | tail -1)"
INITRD="$(find "$ROOTFS/boot" -maxdepth 1 -name 'initrd.img-*' | sort -V | tail -1)"
[[ -n "$KERNEL" && -n "$INITRD" ]] || die "kernel or initramfs not found in rootfs"
cp -L "$KERNEL" "$ISOFILES/live/vmlinuz"
cp -L "$INITRD" "$ISOFILES/live/initrd.img"
log "compressing rootfs → squashfs (this takes a while)"
# The squashfs is the live root. Keep it complete — offline man pages and
# docs are a product promise (see the landing page), so nothing is excluded.
mksquashfs "$ROOTFS" "$ISOFILES/live/arcline.squashfs" -noappend -comp zstd -Xcompression-level 15 2>/dev/null || \
mksquashfs "$ROOTFS" "$ISOFILES/live/arcline.squashfs" -noappend -comp xz
# ── 3. grub boot config ─────────────────────────────────────────────────────
log "writing grub config"
KCMD="$(tr '\n' ' ' < "$(edition_dir "$EDITION")/kernel.cmdline" | sed 's/ */ /g')"
cat > "$ISOFILES/boot/grub/grub.cfg" <<EOF
set timeout=5
set default=0
menuentry "Arcline $EDITION ($VERSION)" {
linux /live/vmlinuz boot=live config quiet $KCMD
initrd /live/initrd.img
}
menuentry "Arcline $EDITION ($VERSION) — safe mode (no mitigations)" {
linux /live/vmlinuz boot=live config quiet nomodeset
initrd /live/initrd.img
}
EOF
# ── 4. assemble ─────────────────────────────────────────────────────────────
log "assembling ISO with grub-mkrescue"
command -v grub-mkrescue >/dev/null || die "grub-mkrescue not found (run scripts/check-host-deps.sh --install)"
mkdir -p "$ARTIFACT_DIR"
grub-mkrescue -o "$ARTIFACT" "$ISOFILES" -- \
-volume-label "ARCLINE_${EDITION^^}" 2>/dev/null || \
grub-mkrescue -o "$ARTIFACT" "$ISOFILES"
log "ISO artifact: $ARTIFACT"
sha256sum "$ARTIFACT" | tee "$ARTIFACT.sha256"

143
scripts/build-rootfs.sh Executable file
View File

@@ -0,0 +1,143 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Arcline OS — rootfs builder
#
# scripts/build-rootfs.sh <edition>
#
# Pipeline:
# 1. debootstrap a minimal Debian base
# 2. configure apt sources (main + security)
# 3. install the edition package set
# 4. apply overlays (hardening, configs, service units)
# 5. run configure-system.sh inside the chroot
# 6. clean and tar the result → build/artifacts/arcline-<edition>-<version>.tar.xz
#
# Root is required (re-execs under sudo when needed).
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
EDITION="${1:?usage: build-rootfs.sh <edition>}"
validate_edition "$EDITION"
# Re-exec under sudo if we aren't root (debootstrap/chroot/mount need it).
require_root "$0" "$@"
# Fail fast on a bad toolchain policy before debootstrap does any work.
case "$ARCLINE_TOOLCHAIN" in
auto|skip|require) ;;
*) die "ARCLINE_TOOLCHAIN must be auto|skip|require (got '$ARCLINE_TOOLCHAIN')" ;;
esac
EDIR="$(edition_dir "$EDITION")"
ROOTFS="$ROOTFS_DIR/$EDITION"
ARTIFACT="$ARTIFACT_DIR/arcline-$EDITION-$VERSION-$ARCH.tar.xz"
LIVE="${ARCLINE_LIVE:-0}"
mkdir -p "$ROOTFS_DIR" "$LOG_DIR" "$ARTIFACT_DIR"
rm -rf "$ROOTFS" "$ARTIFACT"
log "═══ building Arcline $EDITION rootfs ($DISTRO_NAME $VERSION) ═══"
# ── 1. bootstrap ────────────────────────────────────────────────────────────
log "[1/6] debootstrap $DEBIAN_SUITE ($ARCH)"
debootstrap \
--arch="$ARCH" \
--variant=minbase \
--include=apt-transport-https,ca-certificates,gnupg,curl \
"$DEBIAN_SUITE" "$ROOTFS" "$DEBIAN_MIRROR" \
| tee "$LOG_DIR/bootstrap-$EDITION.log"
# ── 2. apt sources ──────────────────────────────────────────────────────────
log "[2/6] configuring apt sources"
cat > "$ROOTFS/etc/apt/sources.list" <<EOF
deb $DEBIAN_MIRROR $DEBIAN_SUITE main contrib non-free-firmware
deb $DEBIAN_MIRROR $DEBIAN_SUITE-updates main contrib non-free-firmware
deb $SECURITY_MIRROR $DEBIAN_SUITE-security main contrib non-free-firmware
EOF
# ── chroot helpers (bind-mount pseudo-fs, run in chroot, unmount on exit) ──
mount_pseudo() {
mount --bind /dev "$ROOTFS/dev"
mount --bind /proc "$ROOTFS/proc"
mount --bind /sys "$ROOTFS/sys"
mount --bind /dev/pts "$ROOTFS/dev/pts" 2>/dev/null || true
}
unmount_pseudo() {
umount -l "$ROOTFS/dev/pts" 2>/dev/null || true
umount -l "$ROOTFS/sys" 2>/dev/null || true
umount -l "$ROOTFS/proc" 2>/dev/null || true
umount -l "$ROOTFS/dev" 2>/dev/null || true
}
trap 'unmount_pseudo' EXIT
chroot_run() { chroot "$ROOTFS" /bin/bash -c "$*"; }
# ── 3. install edition packages ─────────────────────────────────────────────
log "[3/6] installing edition packages (${EDITION})"
PKGS="$(grep -vE '^\s*(#|$)' "$EDIR/packages.list" | tr '\n' ' ')"
mount_pseudo
chroot_run "export DEBIAN_FRONTEND=noninteractive; apt-get update -qq && apt-get install -y --no-install-recommends $PKGS" \
| tee "$LOG_DIR/packages-$EDITION.log"
unmount_pseudo
# ── 4. overlays ─────────────────────────────────────────────────────────────
log "[4/6] applying overlays"
"$ROOT/scripts/apply-overlays.sh" "$ROOTFS" "$EDITION"
# copy btrfs snapshot/rollback tooling into the image
install -Dm0755 "$ROOT/btrfs/snapshot.sh" "$ROOTFS/usr/local/sbin/arcline-snapshot"
install -Dm0755 "$ROOT/btrfs/rollback.sh" "$ROOTFS/usr/local/sbin/arcline-rollback"
# ── 5. configure system in chroot ───────────────────────────────────────────
log "[5/6] configuring system in chroot"
install -m0755 "$ROOT/scripts/configure-system.sh" "$ROOTFS/root/configure-system.sh"
# Stage the edition manifest INSIDE the chroot (the hook runs in there and
# cannot see host paths).
mkdir -p "$ROOTFS/root/arcline-edition"
cp "$EDIR/kernel.cmdline" "$EDIR/metadata.yaml" "$ROOTFS/root/arcline-edition/"
# Toolchain policy — the Arcline tools are optional in an image build. See
# ARCLINE_TOOLCHAIN in scripts/common.sh (auto | skip | require).
HAVE_DEBS=0
if [[ -d "$DEB_DIR" ]] && ls "$DEB_DIR"/*.deb >/dev/null 2>&1; then
HAVE_DEBS=1
fi
case "$ARCLINE_TOOLCHAIN" in
require)
[[ $HAVE_DEBS -eq 1 ]] || die "ARCLINE_TOOLCHAIN=require but no .deb files in $DEB_DIR (run: make toolchain)"
mkdir -p "$ROOTFS/arcline-debs"
cp "$DEB_DIR"/*.deb "$ROOTFS/arcline-debs/"
log "toolchain: staging $(ls "$DEB_DIR"/*.deb | wc -l) packages (required)"
;;
skip)
log "toolchain: skipped (ARCLINE_TOOLCHAIN=skip) — building without Arcline tools"
;;
auto)
if [[ $HAVE_DEBS -eq 1 ]]; then
mkdir -p "$ROOTFS/arcline-debs"
cp "$DEB_DIR"/*.deb "$ROOTFS/arcline-debs/"
log "toolchain: staging $(ls "$DEB_DIR"/*.deb | wc -l) packages"
else
warn "no toolchain .debs in $DEB_DIR — building WITHOUT Arcline tools (run: make toolchain)"
fi
;;
*)
die "ARCLINE_TOOLCHAIN must be auto|skip|require (got '$ARCLINE_TOOLCHAIN')"
;;
esac
mount_pseudo
chroot_run "ARCLINE_EDITION_DIR='/root/arcline-edition' ARCLINE_LIVE='$LIVE' ARCLINE_LOCK_ROOT='${ARCLINE_LOCK_ROOT:-0}' ARCLINE_EXTRA_REPOS='${ARCLINE_EXTRA_REPOS:-0}' /root/configure-system.sh '$EDITION'"
rm -f "$ROOTFS/root/configure-system.sh"
rm -rf "$ROOTFS/root/arcline-edition" "$ROOTFS/arcline-debs"
unmount_pseudo
# ── 6. clean + archive ──────────────────────────────────────────────────────
log "[6/6] cleaning and archiving"
chroot_run "apt-get clean 2>/dev/null; rm -rf /var/lib/apt/lists/* /var/cache/apt/* /tmp/* /root/.bash_history"
rm -f "$ROOTFS/etc/machine-id"
: > "$ROOTFS/etc/machine-id"
tar -C "$ROOTFS" -cJf "$ARTIFACT" .
log "rootfs artifact: $ARTIFACT"
sha256sum "$ARTIFACT" | tee "$ARTIFACT.sha256"

53
scripts/check-host-deps.sh Executable file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Arcline OS — host build-dependency check
#
# scripts/check-host-deps.sh → report what's missing
# scripts/check-host-deps.sh --install → apt-get install what's missing
#
# Builds run debootstrap (rootfs), copy overlays (standard tools), and produce
# a hybrid ISO via grub-mkrescue + mksquashfs.
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
NEEDED=(
"debootstrap|debootstrap"
"grub-mkrescue|grub2-common"
"mksquashfs|squashfs-tools"
"xorriso|xorriso"
"cpio|cpio"
"bash|bash"
)
MISSING=()
for entry in "${NEEDED[@]}"; do
bin="${entry%%|*}"; pkg="${entry##*|}"
if ! command -v "$bin" >/dev/null 2>&1; then
MISSING+=("$pkg")
warn "missing: $bin (package: $pkg)"
fi
done
if [[ ${#MISSING[@]} -eq 0 ]]; then
log "all host build dependencies present ✓"
exit 0
fi
if [[ "${1:-}" == "--install" ]]; then
log "installing: ${MISSING[*]}"
if [[ $EUID -ne 0 ]] && command -v sudo >/dev/null; then
sudo apt-get update
sudo apt-get install -y "${MISSING[@]}"
else
apt-get update
apt-get install -y "${MISSING[@]}"
fi
log "dependencies installed ✓"
else
echo
echo "Run the following to install them:"
echo " sudo apt-get install -y ${MISSING[*]}"
echo "or: scripts/check-host-deps.sh --install"
exit 1
fi

67
scripts/common.sh Executable file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Arcline OS — shared build environment + helpers
#
# Every script under scripts/ sources this first. Values mirror versions.mk;
# when invoked through the Makefile the exported variables take precedence, so
# versions.mk stays the single source of truth.
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
# ── identity ────────────────────────────────────────────────────────────────
: "${DISTRO_NAME:=Arcline OS}"
: "${DISTRO_ID:=arclineos}"
: "${VERSION:=0.1.0}"
: "${RELEASE_NAME:=arclines}"
: "${DEBIAN_SUITE:=bookworm}"
: "${DEBIAN_MIRROR:=http://deb.debian.org/debian}"
: "${SECURITY_MIRROR:=http://security.debian.org/debian-security}"
: "${ARCH:=amd64}"
: "${KERNEL_PACKAGE:=linux-image-amd64}"
: "${KERNEL_VERSION:=6.1}"
# Toolchain policy for image builds:
# auto (default) install the Arcline tools if build/debs/*.deb exist,
# otherwise build without them (with a warning)
# skip never install the tools, even if debs are present
# require fail the build if no toolchain debs are available
: "${ARCLINE_TOOLCHAIN:=auto}"
# ── paths (relative to the repo root) ───────────────────────────────────────
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
: "${BUILD_DIR:=$ROOT/build}"
: "${ROOTFS_DIR:=$BUILD_DIR/rootfs}"
: "${IMAGE_DIR:=$BUILD_DIR/iso}"
: "${DEB_DIR:=$BUILD_DIR/debs}"
: "${LOG_DIR:=$BUILD_DIR/logs}"
: "${ARTIFACT_DIR:=$BUILD_DIR/artifacts}"
# Valid edition names, mirroring versions.mk.
EDITIONS=(server workstation cloud)
# ── output helpers ──────────────────────────────────────────────────────────
log() { printf '\033[1;34m[arcline]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[arcline]\033[0m warning: %s\n' "$*" >&2; }
die() { printf '\033[1;31m[arcline]\033[0m error: %s\n' "$*" >&2; exit 1; }
# ── path helpers ────────────────────────────────────────────────────────────
edition_dir() { printf '%s/editions/%s' "$ROOT" "$1"; }
validate_edition() {
local e
for e in "${EDITIONS[@]}"; do [[ "$e" == "$1" ]] && return 0; done
die "unknown edition '$1' (expected: ${EDITIONS[*]})"
}
# ── privilege helper ────────────────────────────────────────────────────────
# Builds need root (debootstrap / chroot / mount). If we are not root and sudo
# is present, re-exec under sudo so the rest of the script can assume root.
require_root() {
if [[ $EUID -eq 0 ]]; then
return 0
elif command -v sudo >/dev/null; then
exec sudo -E "$0" "$@"
else
die "build requires root: run with sudo or as root (debootstrap + chroot need it)"
fi
}

112
scripts/configure-system.sh Executable file
View File

@@ -0,0 +1,112 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Arcline OS — in-chroot system configuration
#
# scripts/configure-system.sh <edition>
#
# This script is executed INSIDE the chroot (build-rootfs.sh copies it in and
# runs it via chroot). It turns a raw debootstrap tree into an Arcline system:
# hostname, locale, kernel cmdline, enabled/masked services, optional live-boot,
# optional Arcline toolchain install, optional upstream repos (grafana/loki).
#
# It is idempotent and safe to re-run.
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
EDITION="${1:?usage: configure-system.sh <edition>}"
DISTRO_NAME="${DISTRO_NAME:-Arcline OS}"
VERSION="${VERSION:-0.1.0}"
RELEASE_NAME="${RELEASE_NAME:-arclines}"
LIVE="${ARCLINE_LIVE:-0}"
LOCK_ROOT="${ARCLINE_LOCK_ROOT:-0}"
EXTRA_REPOS="${ARCLINE_EXTRA_REPOS:-0}"
# Absolute path of the edition dir on the host is injected by build-rootfs.sh.
EDIR="${ARCLINE_EDITION_DIR:?ARCLINE_EDITION_DIR must be set}"
log() { printf '\033[1;34m[arcline:chroot]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[arcline:chroot]\033[0m %s\n' "$*" >&2; }
# Read a flat YAML list (key: then indented "- item" lines). Keys may be
# nested under a section (e.g. services: → enabled:).
yaml_list() {
awk -v key="$2" '
$0 ~ "^[[:space:]]*" key ":" { insec=1; next }
insec && /^[[:space:]]*-/ { sub(/^[[:space:]]*-[[:space:]]*/, ""); print; next }
insec && !/^[[:space:]]*-/ && !/^[[:space:]]*$/ { exit }
' "$1"
}
# ── identity ────────────────────────────────────────────────────────────────
echo "$RELEASE_NAME" > /etc/hostname
cat > /etc/arcline-release <<EOF
$DISTRO_NAME $VERSION ($RELEASE_NAME)
Edition: $EDITION
Debian base: $(. /etc/os-release && echo "$PRETTY_NAME")
EOF
ln -sf /etc/arcline-release /etc/os-release-arcline
# ── locale / timezone ───────────────────────────────────────────────────────
sed -i 's/^# *en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen
locale-gen >/dev/null 2>&1 || true
echo "LANG=en_US.UTF-8" > /etc/default/locale
ln -sf /usr/share/zoneinfo/UTC /etc/localtime
# ── kernel cmdline (used when this rootfs is installed to disk) ─────────────
KCMD="$(tr '\n' ' ' < "$EDIR/kernel.cmdline" | sed 's/ */ /g; s/^ *//; s/ *$//')"
cat > /etc/default/grub <<EOF
GRUB_DEFAULT=0
GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR="$DISTRO_NAME"
GRUB_CMDLINE_LINUX_DEFAULT="$KCMD"
GRUB_CMDLINE_LINUX=""
GRUB_DISABLE_OS_PROBER=false
EOF
# ── systemd services ────────────────────────────────────────────────────────
META="$EDIR/metadata.yaml"
while IFS= read -r svc; do
[[ -n "$svc" ]] && systemctl enable "$svc" 2>/dev/null || true
done < <(yaml_list "$META" "enabled" || true)
while IFS= read -r svc; do
[[ -n "$svc" ]] && systemctl mask "$svc" 2>/dev/null || true
done < <(yaml_list "$META" "masked" || true)
# Zero-telemetry: apt's automatic update calls are masked in metadata; also
# ensure no package telemetry survives.
rm -f /var/log/apt/*.log /var/cache/apt/archives/*.deb
# ── live-boot (ISO builds) ──────────────────────────────────────────────────
if [[ "$LIVE" == "1" ]]; then
log "installing live-boot support"
export DEBIAN_FRONTEND=noninteractive
apt-get install -y --no-install-recommends live-boot live-config-systemd live-tools || warn "live-boot install failed"
fi
# ── Arcline toolchain (host-built .debs) ────────────────────────────────────
if [[ -d /arcline-debs ]] && ls /arcline-debs/*.deb >/dev/null 2>&1; then
log "installing Arcline toolchain packages"
export DEBIAN_FRONTEND=noninteractive
apt-get install -y /arcline-debs/*.deb 2>/dev/null \
|| dpkg -i /arcline-debs/*.deb 2>/dev/null \
|| warn "toolchain install incomplete (fix with: apt-get -f install)"
fi
# ── optional upstream repos (grafana, loki) ─────────────────────────────────
if [[ "$EXTRA_REPOS" == "1" ]]; then
log "adding upstream observability repos (grafana, loki)"
install -d /usr/share/keyrings
curl -fsSL https://apt.grafana.com/gpg.key -o /usr/share/keyrings/grafana.asc 2>/dev/null || warn "grafana key fetch failed"
echo "deb [signed-by=/usr/share/keyrings/grafana.asc] https://apt.grafana.com stable main" > /etc/apt/sources.list.d/grafana.list
# Loki ships as a static binary tarball; the packaging lives in toolchain/.
# (No-op here; documented in docs/observability.md.)
fi
# ── root account policy ─────────────────────────────────────────────────────
if [[ "$LOCK_ROOT" == "1" ]]; then
passwd -l root
log "root account locked (sudo/ssh-key access only)"
fi
log "configure-system.sh complete for edition '$EDITION'"

64
scripts/validate.sh Executable file
View File

@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Arcline OS — fast, offline validation of the build tree.
#
# scripts/validate.sh
#
# Runs on any machine (no root, no network). Checks:
# * every shell script parses (bash -n)
# * every edition has a complete manifest (metadata, packages, cmdline, fstab)
# * package lists reference no obvious duplicate lines
# * the Makefile + versions.mk parse
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
fail=0
note_fail() { warn "$1"; fail=1; }
log "validating shell scripts…"
while IFS= read -r -d '' s; do
if ! bash -n "$s"; then
note_fail "syntax error in $s"
fi
done < <(find "$ROOT/scripts" "$ROOT/btrfs" "$ROOT/toolchain" "$ROOT/tests" \
-name '*.sh' -type f -print0 2>/dev/null)
log "validating editions…"
for e in "${EDITIONS[@]}"; do
ed="$(edition_dir "$e")"
for f in metadata.yaml packages.list kernel.cmdline fstab; do
[[ -f "$ed/$f" ]] || note_fail "edition '$e' missing $f"
done
# package list sanity: no blank-or-comment duplicates
if [[ -f "$ed/packages.list" ]]; then
dup="$(grep -vE '^\s*(#|$)' "$ed/packages.list" | sort | uniq -d)"
[[ -z "$dup" ]] || note_fail "edition '$e' has duplicate package entries: $(echo "$dup" | tr '\n' ' ')"
fi
done
log "validating top-level Makefile / versions.mk…"
make -C "$ROOT" -n check >/dev/null 2>&1 || note_fail "Makefile dry-run failed (make -n check)"
log "validating YAML manifests (if python3 + pyyaml available)…"
if python3 -c 'import yaml' 2>/dev/null; then
python3 - "$ROOT" <<'PYEOF' || note_fail "YAML manifest parse failed"
import sys, glob, yaml
root = sys.argv[1]
for f in sorted(glob.glob(root + "/editions/*/metadata.yaml")):
d = yaml.safe_load(open(f))
assert d.get("edition") and d.get("codename"), f"{f}: missing edition/codename"
assert d.get("image", {}).get("type"), f"{f}: missing image.type"
assert "services" in d, f"{f}: missing services"
assert isinstance(d["services"].get("enabled", []), list)
PYEOF
else
warn "python3/pyyaml not available — skipping YAML parse (structural checks above still apply)"
fi
if [[ $fail -eq 0 ]]; then
log "validation passed ✓"
else
die "validation found problems"
fi

40
versions.mk Normal file
View File

@@ -0,0 +1,40 @@
# ─────────────────────────────────────────────────────────────────────────────
# Arcline OS — version pins & build constants
#
# Single source of truth for what a build produces. Every script sources this
# (directly or via scripts/common.sh) so there is exactly one place to bump a
# version or switch the Debian base.
# ─────────────────────────────────────────────────────────────────────────────
# Distro identity
DISTRO_NAME := Arcline OS
DISTRO_ID := arclineos
VERSION := 0.1.0
RELEASE_NAME := arclines
# Debian base we build on. Bookworm = Debian 12 stable.
DEBIAN_SUITE := bookworm
DEBIAN_MIRROR := http://deb.debian.org/debian
SECURITY_MIRROR := http://security.debian.org/debian-security
# Architecture we target first. amd64 is the primary; arm64 builds are a
# stretch goal. Kept in one place so adding arm64 is a one-line change.
ARCH := amd64
# Kernel flavour shipped by the base. Bookworm ships 6.1 LTS.
KERNEL_PACKAGE := linux-image-amd64
KERNEL_VERSION := 6.1
# Edition codenames (the "what do I install" flavours).
EDITIONS := server workstation cloud
# Output layout under build/
BUILD_DIR := build
ROOTFS_DIR := $(BUILD_DIR)/rootfs
IMAGE_DIR := $(BUILD_DIR)/iso
DEB_DIR := $(BUILD_DIR)/debs
LOG_DIR := $(BUILD_DIR)/logs
ARTIFACT_DIR := $(BUILD_DIR)/artifacts
# Toolchain: the 11 Go tools that ship with Arcline OS (see toolchain/).
TOOLCHAIN_REPO := https://git.arcline.it/arcline/tools.git