From 14e5ea9e1eba7f3f2c595dc73eae73ba30f3a566 Mon Sep 17 00:00:00 2001 From: Blake Ridgway Date: Fri, 21 Aug 2026 13:15:43 -0500 Subject: [PATCH] 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-, 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--minimal). - GPL-3.0 licensed, sponsored by Arcline IT LLC. --- .gitignore | 18 +++++ LICENSE | 20 +++++ Makefile | 104 ++++++++++++++++++++++++++ scripts/apply-overlays.sh | 36 +++++++++ scripts/build-edition.sh | 27 +++++++ scripts/build-iso.sh | 81 ++++++++++++++++++++ scripts/build-rootfs.sh | 143 ++++++++++++++++++++++++++++++++++++ scripts/check-host-deps.sh | 53 +++++++++++++ scripts/common.sh | 67 +++++++++++++++++ scripts/configure-system.sh | 112 ++++++++++++++++++++++++++++ scripts/validate.sh | 64 ++++++++++++++++ versions.mk | 40 ++++++++++ 12 files changed, 765 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100755 scripts/apply-overlays.sh create mode 100755 scripts/build-edition.sh create mode 100755 scripts/build-iso.sh create mode 100755 scripts/build-rootfs.sh create mode 100755 scripts/check-host-deps.sh create mode 100755 scripts/common.sh create mode 100755 scripts/configure-system.sh create mode 100755 scripts/validate.sh create mode 100644 versions.mk diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a5f674 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1ef2773 --- /dev/null +++ b/LICENSE @@ -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 . + +The full text of the license is available at: + https://www.gnu.org/licenses/gpl-3.0.txt diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ac3beb0 --- /dev/null +++ b/Makefile @@ -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- → build just the rootfs for an edition +# make iso- → 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- Build rootfs (server|workstation|cloud)" + @echo " make iso- Build ISO (server|workstation|cloud)" + @echo " make iso Build ISO for every edition" + @echo " make iso--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)/" diff --git a/scripts/apply-overlays.sh b/scripts/apply-overlays.sh new file mode 100755 index 0000000..6dd6595 --- /dev/null +++ b/scripts/apply-overlays.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# Arcline OS — overlay application +# +# scripts/apply-overlays.sh +# +# overlays/ is organised as layered filesystem trees: +# overlays/base/ → every edition +# overlays// → 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 }" +EDITION="${2:?usage: apply-overlays.sh }" +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 ✓" diff --git a/scripts/build-edition.sh b/scripts/build-edition.sh new file mode 100755 index 0000000..6b1f6f2 --- /dev/null +++ b/scripts/build-edition.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# Arcline OS — edition build orchestrator +# +# scripts/build-edition.sh +# 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 }" +STAGE="${2:?usage: build-edition.sh }" +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 diff --git a/scripts/build-iso.sh b/scripts/build-iso.sh new file mode 100755 index 0000000..5dee636 --- /dev/null +++ b/scripts/build-iso.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# Arcline OS — live ISO builder +# +# scripts/build-iso.sh +# +# 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---.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 }" +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" </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" diff --git a/scripts/build-rootfs.sh b/scripts/build-rootfs.sh new file mode 100755 index 0000000..d8b5a23 --- /dev/null +++ b/scripts/build-rootfs.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# Arcline OS — rootfs builder +# +# scripts/build-rootfs.sh +# +# 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--.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 }" +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" </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" diff --git a/scripts/check-host-deps.sh b/scripts/check-host-deps.sh new file mode 100755 index 0000000..d8b6801 --- /dev/null +++ b/scripts/check-host-deps.sh @@ -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 diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100755 index 0000000..6d73c19 --- /dev/null +++ b/scripts/common.sh @@ -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 +} diff --git a/scripts/configure-system.sh b/scripts/configure-system.sh new file mode 100755 index 0000000..32347ba --- /dev/null +++ b/scripts/configure-system.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# Arcline OS — in-chroot system configuration +# +# scripts/configure-system.sh +# +# 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 }" +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 </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 </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'" diff --git a/scripts/validate.sh b/scripts/validate.sh new file mode 100755 index 0000000..a70f1eb --- /dev/null +++ b/scripts/validate.sh @@ -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 diff --git a/versions.mk b/versions.mk new file mode 100644 index 0000000..4f2c95b --- /dev/null +++ b/versions.mk @@ -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