Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add NixOS build variant to oven pipeline

NixOS-based AC Native OS for broad hardware support via modular kernel
+ linux-firmware. Adds "nix" variant to the oven build pipeline:

- fedac/nixos/: NixOS flake with configuration, kiosk module (cage +
ac-native), hardware module (GPU/WiFi/audio firmware), WiFi module,
and ac-native Nix package derivation (confirmed compiling)
- oven: native-builder.mjs gains "nix" variant that runs `nix build`
directly on host (no Docker), uploads ISO with nix- channel prefix
- oven: git poller watches fedac/nixos/ alongside fedac/native/
- oven: deploy.sh installs Nix + enables flakes + GC timer
- ac-os: adds `nixos` subcommand (build/flash/upload/pull)
- upload-release.sh: ISO-only mode now uploads version + sha256 files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+665 -17
+86 -1
fedac/native/ac-os
··· 1370 1370 exec nc "${SWANK_IP}" "${SWANK_PORT}" 1371 1371 fi 1372 1372 ;; 1373 + nixos) 1374 + NIXOS_CMD="${2:-build}" 1375 + NIXOS_DIR="${SCRIPT_DIR}/../nixos" 1376 + if [ ! -f "${NIXOS_DIR}/flake.nix" ]; then 1377 + err "NixOS flake not found at ${NIXOS_DIR}/flake.nix" 1378 + exit 1 1379 + fi 1380 + case "${NIXOS_CMD}" in 1381 + build) 1382 + log "Building NixOS image..." 1383 + cd "${NIXOS_DIR}" 1384 + nix build .#usb-image --print-out-paths 1385 + log "NixOS image built" 1386 + ;; 1387 + flash) 1388 + require_login 1389 + log "Building + flashing NixOS image to USB..." 1390 + cd "${NIXOS_DIR}" 1391 + IMAGE_PATH=$(nix build .#usb-image --print-out-paths --no-link) 1392 + ISO=$(find "${IMAGE_PATH}" -name '*.iso' -type f | head -1) 1393 + if [ -z "${ISO}" ]; then 1394 + err "No ISO found in nix build output" 1395 + exit 1 1396 + fi 1397 + USB_DEV="$(find_usb_dev)" || { err "No USB device found"; exit 1; } 1398 + log "Flashing ${ISO} to ${USB_DEV}..." 1399 + sudo dd if="${ISO}" of="${USB_DEV}" bs=4M status=progress oflag=sync 1400 + sync 1401 + log "NixOS USB ready!" 1402 + ;; 1403 + upload) 1404 + require_login 1405 + require_clean 1406 + log "Building + uploading NixOS image..." 1407 + cd "${NIXOS_DIR}" 1408 + IMAGE_PATH=$(nix build .#usb-image --print-out-paths --no-link) 1409 + ISO=$(find "${IMAGE_PATH}" -name '*.iso' -type f | head -1) 1410 + if [ -z "${ISO}" ]; then 1411 + err "No ISO found in nix build output" 1412 + exit 1 1413 + fi 1414 + load_vault_creds 1415 + OTA_CHANNEL=nix bash "${SCRIPT_DIR}/scripts/upload-release.sh" --iso "${ISO}" 1416 + ;; 1417 + pull) 1418 + require_login 1419 + NIX_VERSION_URL="${CDN_BASE}/nix-native-notepat-latest.version" 1420 + NIX_ISO_URL="${CDN_BASE}/nix-native-notepat-latest.iso" 1421 + NIX_HASH_URL="${CDN_BASE}/nix-native-notepat-latest.sha256" 1422 + 1423 + log "Fetching latest NixOS OTA version..." 1424 + VERSION_INFO=$(curl -sf "${NIX_VERSION_URL}") || { err "No NixOS release found on CDN"; exit 1; } 1425 + BUILD_NAME=$(echo "${VERSION_INFO}" | head -1) 1426 + IMAGE_SIZE=$(echo "${VERSION_INFO}" | tail -1) 1427 + log "Latest NixOS: ${BUILD_NAME} (${IMAGE_SIZE} bytes)" 1428 + 1429 + PULL_DIR="/tmp/ac-os-nix-pull" 1430 + mkdir -p "${PULL_DIR}" 1431 + PULLED_ISO="${PULL_DIR}/ac-os-nixos.iso" 1432 + 1433 + log "Downloading NixOS image (~$(( IMAGE_SIZE / 1048576 ))MB)..." 1434 + curl -f --progress-bar -o "${PULLED_ISO}" "${NIX_ISO_URL}" || { err "Download failed"; exit 1; } 1435 + 1436 + log "Verifying SHA256..." 1437 + EXPECTED_HASH=$(curl -sf "${NIX_HASH_URL}") || { err "Failed to fetch hash"; exit 1; } 1438 + ACTUAL_HASH=$(sha256sum "${PULLED_ISO}" | cut -d' ' -f1) 1439 + if [ "${ACTUAL_HASH}" != "${EXPECTED_HASH}" ]; then 1440 + err "SHA256 mismatch!" 1441 + exit 1 1442 + fi 1443 + log "SHA256 verified" 1444 + 1445 + USB_DEV="$(find_usb_dev)" || { err "No USB device found"; exit 1; } 1446 + log "Flashing NixOS image to ${USB_DEV}..." 1447 + sudo dd if="${PULLED_ISO}" of="${USB_DEV}" bs=4M status=progress oflag=sync 1448 + sync 1449 + log "NixOS USB ready!" 1450 + rm -rf "${PULL_DIR}" 1451 + ;; 1452 + *) 1453 + echo "Usage: ac-os nixos {build|flash|upload|pull}" 1454 + exit 1 1455 + ;; 1456 + esac 1457 + ;; 1373 1458 *) 1374 - echo "Usage: ac-os {build|flash|upload|flash+upload|bfu|pull|scan|repl|simuflash|test|qemu}" 1459 + echo "Usage: ac-os {build|flash|upload|flash+upload|bfu|pull|scan|repl|simuflash|test|qemu|nixos}" 1375 1460 exit 1 1376 1461 ;; 1377 1462 esac
+9 -3
fedac/native/scripts/upload-release.sh
··· 155 155 echo " channel: ${OTA_CHANNEL}" 156 156 fi 157 157 158 - # ISO-only mode: skip vmlinuz uploads entirely, just push the ISO and exit 158 + # ISO-only mode: upload ISO + version + sha256, then exit 159 159 if [ "$ISO_ONLY" = "1" ]; then 160 - echo "Uploading ISO: $(du -sh "$ISO_PATH" | cut -f1)" 161 - do_upload "$ISO_PATH" "os/${CHANNEL_PREFIX}native-notepat-latest.iso" "application/octet-stream" 160 + ISO_SHA256=$(sha256sum "$ISO_PATH" | awk '{print $1}') 161 + ISO_SIZE=$(stat -c%s "$ISO_PATH" 2>/dev/null || stat -f%z "$ISO_PATH") 162 + printf '%s\n%s' "${FULL_VERSION}" "$ISO_SIZE" > "$TMP/version.txt" 163 + printf '%s' "$ISO_SHA256" > "$TMP/sha256.txt" 164 + echo "Uploading ISO: $(du -sh "$ISO_PATH" | cut -f1) sha256=${ISO_SHA256:0:16}..." 165 + do_upload "$TMP/version.txt" "os/${CHANNEL_PREFIX}native-notepat-latest.version" "text/plain" 166 + do_upload "$TMP/sha256.txt" "os/${CHANNEL_PREFIX}native-notepat-latest.sha256" "text/plain" 167 + do_upload "$ISO_PATH" "os/${CHANNEL_PREFIX}native-notepat-latest.iso" "application/octet-stream" 162 168 echo "ISO published: ${BASE_URL}/os/${CHANNEL_PREFIX}native-notepat-latest.iso" 163 169 exit 0 164 170 fi
+101
fedac/nixos/configuration.nix
··· 1 + { config, pkgs, lib, self ? null, gitHash ? "unknown", version ? "dev", ... }: 2 + 3 + let 4 + ac-native = pkgs.callPackage ./packages/ac-native { inherit gitHash version; }; 5 + in 6 + { 7 + imports = [ 8 + ./modules/hardware.nix 9 + ./modules/kiosk.nix 10 + ./modules/wifi.nix 11 + ]; 12 + 13 + # Boot 14 + boot.loader.systemd-boot.enable = true; 15 + boot.loader.efi.canTouchEfiVariables = false; 16 + boot.kernelPackages = pkgs.linuxPackages_latest; 17 + boot.kernelParams = [ 18 + "quiet" 19 + "loglevel=3" 20 + "vt.global_cursor_default=0" 21 + "consoleblank=0" 22 + ]; 23 + boot.consoleLogLevel = 0; 24 + 25 + # Minimal system — no desktop, no SSH, no docs 26 + documentation.enable = false; 27 + services.xserver.enable = false; 28 + services.openssh.enable = false; 29 + security.polkit.enable = true; 30 + 31 + # Networking (WiFi managed by ac-native, not NetworkManager) 32 + networking = { 33 + hostName = "ac-native"; 34 + useDHCP = lib.mkDefault true; 35 + networkmanager.enable = false; 36 + }; 37 + 38 + # Timezone 39 + time.timeZone = "America/Los_Angeles"; 40 + 41 + # Kiosk user 42 + users.users.ac = { 43 + isNormalUser = true; 44 + extraGroups = [ "video" "audio" "input" "seat" "tty" ]; 45 + home = "/tmp/ac-home"; 46 + }; 47 + 48 + # Autologin — no greeter, no display manager 49 + services.getty.autologinUser = "ac"; 50 + 51 + # System packages — only what ac-native needs at runtime 52 + environment.systemPackages = with pkgs; [ 53 + ac-native 54 + cage 55 + wpa_supplicant 56 + iw 57 + dhcpcd 58 + curl 59 + util-linux # sfdisk, blockdev for NVMe install 60 + dosfstools # mkfs.vfat 61 + efibootmgr 62 + parted 63 + ]; 64 + 65 + # zram swap 66 + zramSwap = { 67 + enable = true; 68 + memoryPercent = 50; 69 + }; 70 + 71 + # tmpfs for /tmp 72 + boot.tmp.useTmpfs = true; 73 + 74 + # Mount USB config partition at /mnt 75 + # ac-native reads /mnt/config.json and /mnt/wifi_creds.json 76 + systemd.services.mount-usb-config = { 77 + description = "Mount USB config partition at /mnt"; 78 + wantedBy = [ "multi-user.target" ]; 79 + before = [ "ac-native-kiosk.service" ]; 80 + serviceConfig = { 81 + Type = "oneshot"; 82 + RemainAfterExit = true; 83 + ExecStart = pkgs.writeShellScript "mount-usb-config" '' 84 + # Scan for FAT32 partition with config.json 85 + for dev in /dev/sda1 /dev/sdb1 /dev/sdc1 /dev/sdd1; do 86 + [ -b "$dev" ] || continue 87 + mount -t vfat -o ro "$dev" /mnt 2>/dev/null || continue 88 + if [ -f /mnt/config.json ] || [ -f /mnt/EFI/BOOT/BOOTX64.EFI ]; then 89 + echo "Mounted config from $dev" 90 + exit 0 91 + fi 92 + umount /mnt 93 + done 94 + echo "No USB config partition found" 95 + ''; 96 + }; 97 + }; 98 + 99 + # NixOS release 100 + system.stateVersion = "24.11"; 101 + }
+64
fedac/nixos/flake.lock
··· 1 + { 2 + "nodes": { 3 + "nixlib": { 4 + "locked": { 5 + "lastModified": 1736643958, 6 + "narHash": "sha256-tmpqTSWVRJVhpvfSN9KXBvKEXplrwKnSZNAoNPf/S/s=", 7 + "owner": "nix-community", 8 + "repo": "nixpkgs.lib", 9 + "rev": "1418bc28a52126761c02dd3d89b2d8ca0f521181", 10 + "type": "github" 11 + }, 12 + "original": { 13 + "owner": "nix-community", 14 + "repo": "nixpkgs.lib", 15 + "type": "github" 16 + } 17 + }, 18 + "nixos-generators": { 19 + "inputs": { 20 + "nixlib": "nixlib", 21 + "nixpkgs": [ 22 + "nixpkgs" 23 + ] 24 + }, 25 + "locked": { 26 + "lastModified": 1769813415, 27 + "narHash": "sha256-nnVmNNKBi1YiBNPhKclNYDORoHkuKipoz7EtVnXO50A=", 28 + "owner": "nix-community", 29 + "repo": "nixos-generators", 30 + "rev": "8946737ff703382fda7623b9fab071d037e897d5", 31 + "type": "github" 32 + }, 33 + "original": { 34 + "owner": "nix-community", 35 + "repo": "nixos-generators", 36 + "type": "github" 37 + } 38 + }, 39 + "nixpkgs": { 40 + "locked": { 41 + "lastModified": 1774709303, 42 + "narHash": "sha256-D3Q07BbIA2KnTcSXIqqu9P586uWxN74zNoCH3h2ESHg=", 43 + "owner": "NixOS", 44 + "repo": "nixpkgs", 45 + "rev": "8110df5ad7abf5d4c0f6fb0f8f978390e77f9685", 46 + "type": "github" 47 + }, 48 + "original": { 49 + "owner": "NixOS", 50 + "ref": "nixos-unstable", 51 + "repo": "nixpkgs", 52 + "type": "github" 53 + } 54 + }, 55 + "root": { 56 + "inputs": { 57 + "nixos-generators": "nixos-generators", 58 + "nixpkgs": "nixpkgs" 59 + } 60 + } 61 + }, 62 + "root": "root", 63 + "version": 7 64 + }
+44
fedac/nixos/flake.nix
··· 1 + { 2 + description = "AC Native OS — NixOS kiosk for Aesthetic Computer"; 3 + 4 + inputs = { 5 + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 + nixos-generators = { 7 + url = "github:nix-community/nixos-generators"; 8 + inputs.nixpkgs.follows = "nixpkgs"; 9 + }; 10 + }; 11 + 12 + outputs = { self, nixpkgs, nixos-generators, ... }: 13 + let 14 + system = "x86_64-linux"; 15 + pkgs = import nixpkgs { inherit system; }; 16 + version = builtins.substring 0 8 (self.lastModifiedDate or "unknown"); 17 + gitHash = self.shortRev or "dirty"; 18 + in 19 + { 20 + # The ac-native binary as a standalone package 21 + packages.${system} = { 22 + ac-native = pkgs.callPackage ./packages/ac-native { 23 + inherit gitHash version; 24 + }; 25 + 26 + # Bootable ISO image (no KVM needed to build) 27 + usb-image = nixos-generators.nixosGenerate { 28 + inherit system; 29 + modules = [ ./configuration.nix ]; 30 + format = "iso"; 31 + specialArgs = { inherit self gitHash version; }; 32 + }; 33 + 34 + default = self.packages.${system}.usb-image; 35 + }; 36 + 37 + # Full NixOS system configuration 38 + nixosConfigurations.ac-native-os = nixpkgs.lib.nixosSystem { 39 + inherit system; 40 + modules = [ ./configuration.nix ]; 41 + specialArgs = { inherit self gitHash version; }; 42 + }; 43 + }; 44 + }
+50
fedac/nixos/modules/hardware.nix
··· 1 + { config, pkgs, lib, ... }: 2 + 3 + { 4 + # Broad hardware support — load ALL firmware 5 + hardware.firmware = [ pkgs.linux-firmware ]; 6 + 7 + # GPU: Intel + AMD + Nvidia (nouveau) 8 + hardware.graphics = { 9 + enable = true; 10 + extraPackages = with pkgs; [ 11 + intel-media-driver # iHD for Broadwell+ 12 + intel-vaapi-driver # i965 for older Intel (Sandy Bridge, etc.) 13 + mesa 14 + ]; 15 + }; 16 + 17 + # Audio: ALSA only (ac-native talks to ALSA directly) 18 + security.rtkit.enable = true; 19 + services.pipewire.enable = false; 20 + 21 + # Kernel modules for common hardware 22 + boot.initrd.availableKernelModules = [ 23 + # Storage 24 + "ahci" "nvme" "sd_mod" "usb_storage" "uas" 25 + "xhci_pci" "ehci_pci" "ohci_pci" 26 + # Input 27 + "usbhid" "hid_generic" "hid_multitouch" 28 + # GPU (loaded early for KMS) 29 + "i915" "amdgpu" "nouveau" 30 + # Filesystems 31 + "vfat" "nls_cp437" "nls_iso8859_1" 32 + ]; 33 + 34 + boot.kernelModules = [ 35 + # WiFi 36 + "iwlwifi" "iwlmvm" 37 + # Audio 38 + "snd_hda_intel" "snd_hda_codec_realtek" "snd_hda_codec_hdmi" 39 + "snd_usb_audio" 40 + # Bluetooth 41 + "btusb" "btintel" 42 + ]; 43 + 44 + # Performance 45 + powerManagement.cpuFreqGovernor = "performance"; 46 + 47 + # Console font (small, clean) 48 + console.font = "ter-v16n"; 49 + console.packages = [ pkgs.terminus_font ]; 50 + }
+66
fedac/nixos/modules/kiosk.nix
··· 1 + { config, pkgs, lib, gitHash ? "unknown", version ? "dev", ... }: 2 + 3 + let 4 + ac-native = pkgs.callPackage ../packages/ac-native { inherit gitHash version; }; 5 + in 6 + { 7 + # seatd for unprivileged GPU/input access 8 + services.seatd.enable = true; 9 + 10 + # Kiosk service: cage compositor running ac-native 11 + systemd.services.ac-native-kiosk = { 12 + description = "AC Native OS kiosk"; 13 + after = [ "multi-user.target" "mount-usb-config.service" "seatd.service" ]; 14 + wants = [ "mount-usb-config.service" "seatd.service" ]; 15 + wantedBy = [ "multi-user.target" ]; 16 + 17 + path = with pkgs; [ 18 + coreutils util-linux 19 + wpa_supplicant iw dhcpcd curl 20 + dosfstools efibootmgr parted 21 + ac-native 22 + ]; 23 + 24 + environment = { 25 + XDG_RUNTIME_DIR = "/run/user/1000"; 26 + HOME = "/tmp/ac-home"; 27 + SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; 28 + ALSA_PLUGIN_DIR = "${pkgs.alsa-plugins}/lib/alsa-lib"; 29 + }; 30 + 31 + serviceConfig = { 32 + User = "ac"; 33 + Group = "users"; 34 + SupplementaryGroups = [ "video" "audio" "input" "seat" ]; 35 + Type = "simple"; 36 + Restart = "on-failure"; 37 + RestartSec = 2; 38 + 39 + # cage -s for single-app mode, -- separates cage args from app args 40 + ExecStart = "${pkgs.cage}/bin/cage -s -- ${ac-native}/bin/ac-native ${ac-native}/share/ac-native/piece.mjs"; 41 + 42 + # Exit code handling: 43 + # 0 = shutdown, 2 = reboot (matching current ac-native convention) 44 + SuccessExitStatus = "0 2"; 45 + ExecStopPost = pkgs.writeShellScript "ac-native-stop" '' 46 + STATUS=$EXIT_STATUS 47 + if [ "$STATUS" = "0" ]; then 48 + systemctl poweroff 49 + elif [ "$STATUS" = "2" ]; then 50 + systemctl reboot 51 + fi 52 + ''; 53 + 54 + # Security 55 + ProtectSystem = "strict"; 56 + ReadWritePaths = [ "/tmp" "/mnt" "/run" ]; 57 + PrivateTmp = false; # ac-native uses /tmp for scratch 58 + }; 59 + }; 60 + 61 + # Ensure XDG_RUNTIME_DIR exists for the ac user 62 + systemd.tmpfiles.rules = [ 63 + "d /run/user/1000 0700 ac users -" 64 + "d /tmp/ac-home 0700 ac users -" 65 + ]; 66 + }
+17
fedac/nixos/modules/wifi.nix
··· 1 + { config, pkgs, lib, ... }: 2 + 3 + { 4 + # Do NOT enable networking.wireless (NixOS-managed wpa_supplicant). 5 + # ac-native's wifi.c manages wpa_supplicant directly via system() calls. 6 + # We just need the binaries available in PATH. 7 + networking.wireless.enable = false; 8 + 9 + # Ensure rfkill doesn't block WiFi 10 + boot.kernelModules = [ "rfkill" ]; 11 + 12 + # udev rule to unblock WiFi on boot 13 + services.udev.extraRules = '' 14 + # Unblock WiFi interfaces on hotplug 15 + ACTION=="add", SUBSYSTEM=="rfkill", ATTR{type}=="wlan", RUN+="${pkgs.util-linux}/bin/rfkill unblock wifi" 16 + ''; 17 + }
+95
fedac/nixos/packages/ac-native/default.nix
··· 1 + { lib, stdenv, fetchurl, pkg-config 2 + , libdrm, alsa-lib, flite, openssl, curl 3 + , wayland, wayland-protocols, wayland-scanner 4 + , ffmpeg 5 + , gitHash ? "unknown", version ? "dev" 6 + }: 7 + 8 + let 9 + quickjs-src = fetchurl { 10 + url = "https://bellard.org/quickjs/quickjs-2024-01-13.tar.xz"; 11 + sha256 = "sha256-PEv4+JW/pUvrSGyNEhgRJ3Hs/FrDvhA2hR70FWghLgM="; 12 + }; 13 + in 14 + stdenv.mkDerivation { 15 + pname = "ac-native"; 16 + inherit version; 17 + 18 + src = ../../../native; 19 + 20 + nativeBuildInputs = [ 21 + pkg-config 22 + wayland-scanner 23 + ]; 24 + 25 + buildInputs = [ 26 + libdrm 27 + alsa-lib 28 + flite 29 + openssl 30 + curl 31 + wayland 32 + wayland-protocols 33 + ffmpeg 34 + ]; 35 + 36 + postUnpack = '' 37 + # Unpack QuickJS into build/quickjs/ 38 + mkdir -p $sourceRoot/build/quickjs 39 + tar xf ${quickjs-src} -C $sourceRoot/build/ 40 + cp $sourceRoot/build/quickjs-2024-01-13/*.c \ 41 + $sourceRoot/build/quickjs-2024-01-13/*.h \ 42 + $sourceRoot/build/quickjs/ 43 + ''; 44 + 45 + # Fix hardcoded wayland protocol path 46 + postPatch = '' 47 + substituteInPlace Makefile \ 48 + --replace '/usr/share/wayland-protocols' \ 49 + '${wayland-protocols}/share/wayland-protocols' 50 + ''; 51 + 52 + makeFlags = [ 53 + "USE_WAYLAND=1" 54 + "CC=cc" 55 + ]; 56 + 57 + # Inject build metadata without git 58 + NIX_CFLAGS_COMPILE = toString [ 59 + "-DAC_GIT_HASH=\"${gitHash}\"" 60 + "-DAC_BUILD_NAME=\"nix-${version}\"" 61 + "-DAC_BUILD_TS=\"${version}\"" 62 + ]; 63 + 64 + # Skip the quickjs fetch target (we already unpacked it) 65 + preBuild = '' 66 + touch build/quickjs/quickjs.h 67 + ''; 68 + 69 + enableParallelBuilding = true; 70 + 71 + installPhase = '' 72 + runHook preInstall 73 + 74 + mkdir -p $out/bin $out/share/ac-native/pieces 75 + 76 + # Binary 77 + cp build/ac-native $out/bin/ 78 + 79 + # Default piece (prompt.mjs → piece.mjs) 80 + cp pieces/prompt.mjs $out/share/ac-native/piece.mjs 81 + 82 + # All pieces 83 + cp pieces/*.mjs $out/share/ac-native/pieces/ 2>/dev/null || true 84 + cp pieces/*.lisp $out/share/ac-native/pieces/ 2>/dev/null || true 85 + 86 + runHook postInstall 87 + ''; 88 + 89 + meta = with lib; { 90 + description = "AC Native OS — creative computing kiosk runtime"; 91 + license = licenses.mit; 92 + platforms = [ "x86_64-linux" ]; 93 + mainProgram = "ac-native"; 94 + }; 95 + }
+37
oven/deploy.sh
··· 86 86 "apt-get install -y -q gcc make flex bison libelf-dev libssl-dev bc cpio lz4 musl-tools python3 pahole libdrm-dev libasound2-dev flite1-dev pkg-config dosfstools mtools util-linux 2>&1 | tail -5 || true" 87 87 echo "✅ Kernel build tools ready" 88 88 89 + # Install Nix package manager for NixOS-based native builds (idempotent) 90 + echo "" 91 + echo "❄️ Installing Nix package manager..." 92 + ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no "root@$OVEN_HOST" bash -s <<'NIX_EOF' 93 + if command -v nix >/dev/null 2>&1; then 94 + echo "Nix already installed: $(nix --version)" 95 + else 96 + curl -sSf -L https://install.determinate.systems/nix | sh -s -- install --no-confirm 2>&1 | tail -10 97 + fi 98 + # Enable flakes 99 + mkdir -p /etc/nix 100 + grep -q 'experimental-features' /etc/nix/nix.conf 2>/dev/null || \ 101 + echo 'experimental-features = nix-command flakes' >> /etc/nix/nix.conf 102 + # Garbage collection timer (weekly, keep 7 days) 103 + if ! systemctl is-enabled nix-gc.timer >/dev/null 2>&1; then 104 + cat > /etc/systemd/system/nix-gc.service <<'SVC' 105 + [Unit] 106 + Description=Nix store garbage collection 107 + [Service] 108 + Type=oneshot 109 + ExecStart=/nix/var/nix/profiles/default/bin/nix-collect-garbage --delete-older-than 7d 110 + SVC 111 + cat > /etc/systemd/system/nix-gc.timer <<'TMR' 112 + [Unit] 113 + Description=Weekly Nix garbage collection 114 + [Timer] 115 + OnCalendar=weekly 116 + Persistent=true 117 + [Install] 118 + WantedBy=timers.target 119 + TMR 120 + systemctl daemon-reload 121 + systemctl enable --now nix-gc.timer 122 + fi 123 + NIX_EOF 124 + echo "✅ Nix ready" 125 + 89 126 # Install TeX Live for papers PDF builds (idempotent) 90 127 echo "" 91 128 echo "📄 Installing TeX Live for papers builds..."
+79 -3
oven/native-builder.mjs
··· 158 158 job.percent = Math.max(job.percent, 10); 159 159 } 160 160 } 161 + // Nix build progress hints 162 + if (clean.match(/copying path|building.*\.drv|fetching.*narinfo/i)) { 163 + if (job.stage !== "nix-upload") { 164 + job.stage = "nix-build"; 165 + job.percent = Math.max(job.percent, 65); 166 + } 167 + } 161 168 if (clean.match(/SMOKE TEST|smoke_test|qemu/i)) { 162 169 job.stage = "smoke-test"; 163 170 job.percent = Math.max(job.percent, 80); ··· 399 406 400 407 job.percent = 30; 401 408 402 - // Determine variant: "c" (default), "cl", or "both" 409 + // Determine variant: "c" (default), "cl", "nix", "both", or "all" 403 410 const variant = job.variant || "c"; 404 - const buildC = variant === "c" || variant === "both"; 405 - const buildCL = variant === "cl" || variant === "both"; 411 + const buildC = variant === "c" || variant === "both" || variant === "all"; 412 + const buildCL = variant === "cl" || variant === "both" || variant === "all"; 413 + const buildNix = variant === "nix" || variant === "all"; 406 414 const uploadScript = path.join(NATIVE_DIR, "scripts/upload-release.sh"); 407 415 const uploadEnv = { 408 416 DO_SPACES_KEY: process.env.DO_SPACES_KEY || process.env.ART_SPACES_KEY || "", ··· 497 505 try { await fs.rm(clUploadDir, { recursive: true }); } catch {} 498 506 try { await fs.unlink(clCidFile); } catch {} 499 507 addLogLine(job, "stdout", "CL variant uploaded successfully"); 508 + } 509 + 510 + // ── NixOS variant: build directly on host with nix (no Docker) ── 511 + if (buildNix) { 512 + const nixosDir = path.resolve(NATIVE_DIR, "../nixos"); 513 + const nixUploadDir = `/tmp/oven-nix-upload-${job.id}`; 514 + 515 + // Preflight: garbage collect old nix store entries 516 + addLogLine(job, "stdout", "Phase N: NixOS — cleaning Nix store..."); 517 + try { 518 + await runPhase(job, "nix-gc", "bash", ["-c", 519 + "nix-collect-garbage --delete-older-than 3d 2>&1 | tail -3 || true" 520 + ], nixosDir, { NIX_CONFIG: "experimental-features = nix-command flakes" }); 521 + } catch {} 522 + 523 + addLogLine(job, "stdout", "Phase N: NixOS — building image with Nix..."); 524 + job.stage = "nix-build"; 525 + job.percent = Math.max(job.percent, 60); 526 + if (progressCallback) progressCallback(makeSnapshot(job)); 527 + 528 + // Build the NixOS ISO image 529 + await runPhase(job, "nix-build", "nix", [ 530 + "build", ".#usb-image", 531 + "--no-link", "--print-out-paths", 532 + ], nixosDir, { NIX_CONFIG: "experimental-features = nix-command flakes" }); 533 + 534 + job.percent = Math.max(job.percent, 85); 535 + 536 + // Extract ISO path from nix build output 537 + const nixOutResult = await new Promise((resolve, reject) => { 538 + const { execSync } = require("child_process"); 539 + try { 540 + const out = execSync( 541 + "nix build .#usb-image --no-link --print-out-paths", 542 + { cwd: nixosDir, env: { ...process.env, NIX_CONFIG: "experimental-features = nix-command flakes" }, encoding: "utf-8" } 543 + ).trim(); 544 + resolve(out); 545 + } catch (e) { reject(e); } 546 + }); 547 + 548 + // Find the ISO in the output directory 549 + const { execSync } = require("child_process"); 550 + const isoPath = execSync(`find ${nixOutResult} -name '*.iso' -type f | head -1`, { encoding: "utf-8" }).trim(); 551 + 552 + if (!isoPath) { 553 + throw new Error("NixOS build produced no ISO file"); 554 + } 555 + 556 + addLogLine(job, "stdout", `NixOS image: ${isoPath}`); 557 + 558 + // Copy to upload directory 559 + await fs.mkdir(nixUploadDir, { recursive: true }); 560 + const nixIsoUpload = path.join(nixUploadDir, "ac-os-nixos.iso"); 561 + await fs.copyFile(isoPath, nixIsoUpload); 562 + 563 + job.stage = "nix-upload"; 564 + job.percent = Math.max(job.percent, 90); 565 + 566 + // Upload with nix- channel prefix 567 + await runPhase(job, "nix-upload", "bash", [ 568 + uploadScript, "--iso", nixIsoUpload, 569 + ], NATIVE_DIR, { 570 + ...uploadEnv, 571 + OTA_CHANNEL: "nix", 572 + }); 573 + 574 + try { await fs.rm(nixUploadDir, { recursive: true }); } catch {} 575 + addLogLine(job, "stdout", "NixOS variant uploaded successfully"); 500 576 } 501 577 502 578 job.status = "success";
+16 -9
oven/native-git-poller.mjs
··· 82 82 changedPaths = "fedac/native/src/force-rebuild"; 83 83 } 84 84 85 - // Filter to fedac/native/ paths only 86 - const nativePaths = changedPaths 87 - .split("\n") 88 - .filter((p) => p.startsWith("fedac/native/")); 85 + // Filter to fedac/native/ and fedac/nixos/ paths 86 + const allPaths = changedPaths.split("\n"); 87 + const nativePaths = allPaths.filter((p) => p.startsWith("fedac/native/")); 88 + const nixosPaths = allPaths.filter((p) => p.startsWith("fedac/nixos/")); 89 89 90 - if (nativePaths.length === 0) { 91 - // Changes exist but not in fedac/native/ — update hash, skip build 92 - logFn("info", "⏭️", `New commits (${remoteHead.slice(0, 8)}) but no fedac/native/ changes — skipping build`); 90 + if (nativePaths.length === 0 && nixosPaths.length === 0) { 91 + // Changes exist but not in fedac/native/ or fedac/nixos/ — update hash, skip build 92 + logFn("info", "⏭️", `New commits (${remoteHead.slice(0, 8)}) but no fedac/ build changes — skipping build`); 93 93 await writeLastBuiltHash(remoteHead); 94 94 polling = false; 95 95 return; 96 96 } 97 + 98 + // Determine variant based on which directories changed 99 + let variant = "c"; 100 + if (nixosPaths.length > 0 && nativePaths.length === 0) variant = "nix"; 101 + else if (nixosPaths.length > 0 && nativePaths.length > 0) variant = "all"; 97 102 98 103 // Hard-sync to origin so stale local edits/conflicts cannot leak into OTA builds. 99 104 await git(["checkout", "-f", BRANCH, "--quiet"]); 100 105 await git(["reset", "--hard", `origin/${BRANCH}`, "--quiet"]); 101 106 await git(["clean", "-fdq"]); 102 107 108 + const relevantPaths = [...nativePaths, ...nixosPaths]; 103 109 logFn( 104 110 "info", 105 111 "🔨", 106 - `Native changes detected (${remoteHead.slice(0, 8)}): ${nativePaths.length} file(s) — triggering OTA build` 112 + `Build changes detected (${remoteHead.slice(0, 8)}): ${relevantPaths.length} file(s), variant=${variant} — triggering OTA build` 107 113 ); 108 114 109 115 // Trigger build 110 116 const job = await startBuildFn({ 111 117 ref: remoteHead, 112 - changed_paths: nativePaths.join(","), 118 + changed_paths: relevantPaths.join(","), 119 + variant, 113 120 }); 114 121 115 122 logFn(
+1 -1
oven/server.mjs
··· 3550 3550 const job = await startNativeBuild({ 3551 3551 ref: req.body?.ref || 'unknown', 3552 3552 changed_paths: req.body?.changed_paths || '', 3553 - variant: req.body?.variant || 'c', // "c", "cl", or "both" 3553 + variant: req.body?.variant || 'c', // "c", "cl", "nix", "both", or "all" 3554 3554 }); 3555 3555 addServerLog('info', '🔨', `Native OTA build started: ${job.id} (ref=${job.ref}, flags=${job.flags.join(' ') || 'none'})`); 3556 3556 return res.status(202).json(job);