Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

feat: switch nix usb builds to raw images

+735 -372
+2 -1
fedac/native/Dockerfile.flash-helper
··· 7 7 8 8 COPY fedac/native/scripts/media-layout.sh /usr/local/lib/ac-media-layout.sh 9 9 COPY fedac/native/scripts/flash-helper-runner.sh /usr/local/bin/ac-os-flash-helper 10 + COPY fedac/native/scripts/nixos-image-helper.sh /usr/local/bin/ac-os-nixos-image-helper 10 11 COPY fedac/native/boot/systemd-bootx64.efi /usr/local/lib/systemd-bootx64.efi 11 12 COPY fedac/native/bootloader/splash.efi /usr/local/lib/splash.efi 12 13 13 - RUN chmod +x /usr/local/bin/ac-os-flash-helper 14 + RUN chmod +x /usr/local/bin/ac-os-flash-helper /usr/local/bin/ac-os-nixos-image-helper 14 15 15 16 ENTRYPOINT ["/usr/local/bin/ac-os-flash-helper"]
+73 -107
fedac/native/ac-os
··· 16 16 INITRAMFS_ROOT="${BUILD_DIR}/initramfs-root" 17 17 AC_MEDIA_NATIVE_DIR="${SCRIPT_DIR}" 18 18 MEDIA_LAYOUT_LIB="${SCRIPT_DIR}/scripts/media-layout.sh" 19 - MEDIA_HELPER_IMAGE="${AC_MEDIA_HELPER_IMAGE:-ac-os-media-helper:latest}" 19 + MEDIA_HELPER_IMAGE="${AC_MEDIA_HELPER_IMAGE:-ac-os-media-helper:img-v1}" 20 20 MEDIA_HELPER_DOCKERFILE="${SCRIPT_DIR}/Dockerfile.flash-helper" 21 21 22 22 CMD="${1:-build}" ··· 854 854 USB_CONFIG='{"handle":"unknown"}' 855 855 fi 856 856 857 - printf '%s' "${USB_CONFIG}" > "${CONFIG_FILE}" 857 + ac_media_write_legacy_config "${CONFIG_FILE}" "${USB_CONFIG}" 858 858 log "USB config: $(ac_media_summarize_config_file "${CONFIG_FILE}")" 859 859 } 860 860 ··· 864 864 local PART 865 865 866 866 DATA_LABEL="$(ac_media_nixos_data_label)" 867 - for PART in "${USB_DEV}3" "${USB_DEV}p3"; do 867 + for PART in "${USB_DEV}"? "${USB_DEV}"p? "${USB_DEV}"?? "${USB_DEV}"p??; do 868 868 [ -b "${PART}" ] || continue 869 869 if [ "$(sudo blkid -o value -s LABEL "${PART}" 2>/dev/null || true)" = "${DATA_LABEL}" ]; then 870 870 printf '%s\n' "${PART}" ··· 917 917 log "Wrote NixOS config to ${DATA_PART}" 918 918 } 919 919 920 + prepare_nixos_image() { 921 + local IMAGE_PATH="$1" 922 + local CONFIG_FILE="$2" 923 + local WORK_DIR 924 + 925 + ensure_media_helper_image 926 + WORK_DIR="$(dirname "${IMAGE_PATH}")" 927 + sudo docker run --rm --privileged \ 928 + -v "${WORK_DIR}:/work" \ 929 + --entrypoint /bin/bash "${MEDIA_HELPER_IMAGE}" \ 930 + -lc "exec /usr/local/bin/ac-os-nixos-image-helper '/work/$(basename "${IMAGE_PATH}")' '/work/$(basename "${CONFIG_FILE}")'" 931 + } 932 + 920 933 flash_nixos_image_to_usb() { 921 934 local IMAGE_PATH="$1" 922 935 local CONFIG_FILE="$2" 923 936 local USB_DEV 924 937 925 - ac_media_ensure_nixos_data_partition \ 926 - "${IMAGE_PATH}" \ 927 - "${CONFIG_FILE}" \ 928 - "$(ac_media_nixos_data_size_mib)" 929 - ac_media_customize_nixos_efi_boot "${IMAGE_PATH}" 938 + prepare_nixos_image "${IMAGE_PATH}" "${CONFIG_FILE}" 930 939 931 940 USB_DEV="$(find_usb_dev)" || { err "No USB device found"; exit 1; } 932 941 log "Flashing ${IMAGE_PATH} to ${USB_DEV}..." ··· 1042 1051 log "Credentials decrypted and cached for session" 1043 1052 } 1044 1053 1045 - generate_template_iso() { 1046 - # Create a hybrid ISO from the staged chainloader-first boot tree. 1047 - # The config.json uses the existing 32KB identity block so oven can patch it. 1048 - local ISO="/tmp/ac-native.iso" 1049 - local STAGING="/tmp/ac-iso-staging" 1050 - local EFI_IMG="/tmp/ac-efi.img" 1051 - local CONFIG_TMP="/tmp/ac-identity-config.json" 1054 + generate_template_image() { 1055 + local IMAGE="/tmp/ac-native.img" 1056 + local STAGING="/tmp/ac-img-staging" 1057 + local LEGACY_CONFIG="/tmp/ac-legacy-config.json" 1058 + local IDENTITY_FILE="/tmp/$(ac_media_identity_filename)" 1059 + local MANIFEST="/tmp/ac-manifest.json" 1052 1060 1053 - log "Generating template .iso..." >&2 1061 + log "Generating template .img..." >&2 1062 + rm -rf "${STAGING}" "${IMAGE}" "${LEGACY_CONFIG}" "${IDENTITY_FILE}" "${MANIFEST}" 1054 1063 1055 - # Clean staging area 1056 - rm -rf "${STAGING}" "${EFI_IMG}" "${ISO}" "${CONFIG_TMP}" 1064 + ac_media_write_legacy_config "${LEGACY_CONFIG}" 1065 + ac_media_write_identity_config "${IDENTITY_FILE}" 1066 + ac_media_stage_boot_tree "${STAGING}" "${VMLINUZ}" "${LEGACY_CONFIG}" 1067 + cp "${IDENTITY_FILE}" "${STAGING}/$(ac_media_identity_filename)" 1068 + ac_media_create_efi_disk_image "${STAGING}" "${IMAGE}" "AC_NATIVE" 1069 + ac_media_generate_manifest "${IMAGE}" "${AC_BUILD_NAME:-unknown}" "${MANIFEST}" 1057 1070 1058 - # Create identity block (32KB, zero-padded) in ISO root. 1059 - # The block starts with a marker so the edge worker can verify alignment, 1060 - # followed by JSON config, zero-padded to exactly 32768 bytes. 1061 - local IDENTITY_SIZE 1062 - local IDENTITY_MARKER 1063 - IDENTITY_SIZE="$(ac_media_identity_size)" 1064 - IDENTITY_MARKER="$(ac_media_identity_marker)" 1065 - 1066 - ac_media_write_identity_config "${CONFIG_TMP}" 1067 - ac_media_stage_boot_tree "${STAGING}" "${VMLINUZ}" "${CONFIG_TMP}" 1068 - ac_media_create_fat_image "${STAGING}" "${EFI_IMG}" "AC_NATIVE" 1069 - ac_media_build_hybrid_iso "${STAGING}" "${EFI_IMG}" "${ISO}" "AC_NATIVE" 1070 - 1071 - rm -rf "${STAGING}" "${EFI_IMG}" "${CONFIG_TMP}" 1072 - 1073 - local ISO_BYTES=$(stat -c%s "${ISO}") 1074 - local ISO_MB=$(( ISO_BYTES / 1048576 )) 1075 - log "Template ISO: ${ISO} (${ISO_MB}MB)" >&2 1076 - 1077 - # Find the identity block offset by searching for the marker in the ISO. 1078 - # Record it in a manifest so the edge worker can patch without scanning. 1079 - local OFFSET=$(python3 -c " 1080 - import sys 1081 - marker = b'${IDENTITY_MARKER}\n' 1082 - with open('${ISO}', 'rb') as f: 1083 - data = f.read() 1084 - idx = data.find(marker) 1085 - print(idx if idx >= 0 else -1) 1086 - " 2>/dev/null) 1087 - 1088 - if [ "${OFFSET}" = "-1" ] || [ -z "${OFFSET}" ]; then 1089 - log "WARNING: Identity block marker not found in ISO!" >&2 1090 - else 1091 - log "Identity block at offset ${OFFSET} (${IDENTITY_SIZE} bytes)" >&2 1092 - fi 1071 + rm -rf "${STAGING}" "${LEGACY_CONFIG}" "${IDENTITY_FILE}" 1093 1072 1094 - # Write manifest alongside the ISO 1095 - local MANIFEST="/tmp/ac-manifest.json" 1096 - cat > "${MANIFEST}" << MANIFEST_EOF 1097 - { 1098 - "name": "${AC_BUILD_NAME:-unknown}", 1099 - "hash": "$(git rev-parse --short HEAD 2>/dev/null || echo unknown)", 1100 - "timestamp": "$(date -u '+%Y-%m-%dT%H:%M:%SZ')", 1101 - "identityBlockOffset": ${OFFSET:--1}, 1102 - "identityBlockSize": ${IDENTITY_SIZE}, 1103 - "identityMarker": "${IDENTITY_MARKER}", 1104 - "isoSize": ${ISO_BYTES} 1105 - } 1106 - MANIFEST_EOF 1073 + local IMAGE_BYTES 1074 + local IMAGE_MB 1075 + IMAGE_BYTES=$(stat -c%s "${IMAGE}") 1076 + IMAGE_MB=$(( IMAGE_BYTES / 1048576 )) 1077 + log "Template image: ${IMAGE} (${IMAGE_MB}MB)" >&2 1107 1078 log "Manifest: ${MANIFEST}" >&2 1108 1079 1109 - echo "${ISO}" 1080 + echo "${IMAGE}" 1110 1081 } 1111 1082 1112 1083 upload_ota() { ··· 1217 1188 fi 1218 1189 fi 1219 1190 1220 - # Step 4: Generate and upload template .iso for personalized downloads 1221 - local TEMPLATE_ISO 1222 - TEMPLATE_ISO=$(generate_template_iso) && { 1223 - log "Uploading template .iso..." 1224 - # Get presigned URL for the .iso 1225 - local ISO_STEP 1226 - ISO_STEP=$(curl -sf -X POST "${UPLOAD_URL}" \ 1191 + # Step 4: Generate and upload template .img for personalized downloads 1192 + local TEMPLATE_IMG 1193 + TEMPLATE_IMG=$(generate_template_image) && { 1194 + log "Uploading template .img..." 1195 + local IMAGE_STEP 1196 + IMAGE_STEP=$(curl -sf -X POST "${UPLOAD_URL}" \ 1227 1197 -H "Authorization: Bearer ${AC_TOKEN}" \ 1228 1198 -H "X-Build-Name: ${AC_BUILD_NAME}" \ 1229 1199 -H "X-Template-Upload: true" \ 1230 1200 --max-time 30) || { log "Template presign failed (non-fatal)"; } 1231 1201 1232 - if [ -n "${ISO_STEP}" ]; then 1233 - local ISO_PUT_URL=$(echo "$ISO_STEP" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).iso_put_url||''))" 2>/dev/null) 1234 - if [ -n "${ISO_PUT_URL}" ] && [ -f "${TEMPLATE_ISO}" ]; then 1235 - log "Uploading template $(stat -c%s "${TEMPLATE_ISO}" | numfmt --to=iec) to S3..." 1236 - curl -sf -X PUT "${ISO_PUT_URL}" \ 1237 - -H "Content-Type: application/x-iso9660-image" \ 1202 + if [ -n "${IMAGE_STEP}" ]; then 1203 + local IMAGE_PUT_URL=$(echo "$IMAGE_STEP" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).image_put_url||''))" 2>/dev/null) 1204 + if [ -n "${IMAGE_PUT_URL}" ] && [ -f "${TEMPLATE_IMG}" ]; then 1205 + log "Uploading template $(stat -c%s "${TEMPLATE_IMG}" | numfmt --to=iec) to S3..." 1206 + curl -sf -X PUT "${IMAGE_PUT_URL}" \ 1207 + -H "Content-Type: application/octet-stream" \ 1238 1208 -H "x-amz-acl: public-read" \ 1239 - -T "${TEMPLATE_ISO}" \ 1209 + -T "${TEMPLATE_IMG}" \ 1240 1210 --max-time 300 && { 1241 - log "Template .iso uploaded" 1242 - # Upload manifest for edge worker ISO patching 1211 + log "Template .img uploaded" 1212 + # Upload manifest for edge worker image patching 1243 1213 local MANIFEST="/tmp/ac-manifest.json" 1244 1214 if [ -f "${MANIFEST}" ]; then 1245 1215 local MANIFEST_STEP ··· 1264 1234 curl -sf -X POST "https://oven.aesthetic.computer/os-cache-flush" \ 1265 1235 --max-time 10 && log "Oven template cache flushed" \ 1266 1236 || log "Cache flush failed (non-fatal)" 1267 - } || log "Template upload failed (non-fatal)" 1237 + } || log "Template upload failed (non-fatal)" 1268 1238 fi 1269 1239 fi 1270 - rm -f "${TEMPLATE_ISO}" 1240 + rm -f "${TEMPLATE_IMG}" 1271 1241 } || log "Template generation failed (non-fatal)" 1272 1242 1273 1243 # Record in MongoDB ··· 1564 1534 log "Building + flashing NixOS image to USB..." 1565 1535 cd "${NIXOS_DIR}" 1566 1536 IMAGE_PATH=$(AC_NIX_NATIVE_SRC="${SCRIPT_DIR}" nix build .#usb-image --impure --print-out-paths --no-link) 1567 - ISO=$(find "${IMAGE_PATH}" -name '*.iso' -type f | head -1) 1568 - if [ -z "${ISO}" ]; then 1569 - err "No ISO found in nix build output" 1537 + IMG=$(find "${IMAGE_PATH}" -name '*.img' -type f | head -1) 1538 + if [ -z "${IMG}" ]; then 1539 + err "No image found in nix build output" 1570 1540 exit 1 1571 1541 fi 1572 1542 NIX_FLASH_DIR="$(mktemp -d /tmp/ac-nixos-flash.XXXXXX)" 1573 - NIX_FLASH_IMAGE="${NIX_FLASH_DIR}/ac-os-nixos.iso" 1543 + NIX_FLASH_IMAGE="${NIX_FLASH_DIR}/ac-os-nixos.img" 1574 1544 NIX_FLASH_CONFIG="${NIX_FLASH_DIR}/config.json" 1575 - cp "${ISO}" "${NIX_FLASH_IMAGE}" 1545 + cp "${IMG}" "${NIX_FLASH_IMAGE}" 1576 1546 write_usb_config_file "${NIX_FLASH_CONFIG}" 1577 1547 flash_nixos_image_to_usb "${NIX_FLASH_IMAGE}" "${NIX_FLASH_CONFIG}" 1578 1548 rm -rf "${NIX_FLASH_DIR}" ··· 1583 1553 log "Building + uploading NixOS image..." 1584 1554 cd "${NIXOS_DIR}" 1585 1555 IMAGE_PATH=$(AC_NIX_NATIVE_SRC="${SCRIPT_DIR}" nix build .#usb-image --impure --print-out-paths --no-link) 1586 - ISO=$(find "${IMAGE_PATH}" -name '*.iso' -type f | head -1) 1587 - if [ -z "${ISO}" ]; then 1588 - err "No ISO found in nix build output" 1556 + IMG=$(find "${IMAGE_PATH}" -name '*.img' -type f | head -1) 1557 + if [ -z "${IMG}" ]; then 1558 + err "No image found in nix build output" 1589 1559 exit 1 1590 1560 fi 1591 1561 load_vault_creds 1592 1562 NIX_UPLOAD_DIR="$(mktemp -d /tmp/ac-nixos-upload.XXXXXX)" 1593 - NIX_UPLOAD_IMAGE="${NIX_UPLOAD_DIR}/ac-os-nixos.iso" 1563 + NIX_UPLOAD_IMAGE="${NIX_UPLOAD_DIR}/ac-os-nixos.img" 1594 1564 NIX_UPLOAD_CONFIG="${NIX_UPLOAD_DIR}/config.json" 1595 - cp "${ISO}" "${NIX_UPLOAD_IMAGE}" 1596 - ac_media_default_identity_json > "${NIX_UPLOAD_CONFIG}" 1597 - ac_media_ensure_nixos_data_partition \ 1598 - "${NIX_UPLOAD_IMAGE}" \ 1599 - "${NIX_UPLOAD_CONFIG}" \ 1600 - "$(ac_media_nixos_data_size_mib)" 1601 - ac_media_customize_nixos_efi_boot "${NIX_UPLOAD_IMAGE}" 1602 - OTA_CHANNEL=nix bash "${SCRIPT_DIR}/scripts/upload-release.sh" --iso "${NIX_UPLOAD_IMAGE}" 1565 + cp "${IMG}" "${NIX_UPLOAD_IMAGE}" 1566 + ac_media_write_legacy_config "${NIX_UPLOAD_CONFIG}" "$(ac_media_default_identity_json)" 1567 + prepare_nixos_image "${NIX_UPLOAD_IMAGE}" "${NIX_UPLOAD_CONFIG}" 1568 + OTA_CHANNEL=nix bash "${SCRIPT_DIR}/scripts/upload-release.sh" --image "${NIX_UPLOAD_IMAGE}" 1603 1569 rm -rf "${NIX_UPLOAD_DIR}" 1604 1570 ;; 1605 1571 pull) 1606 1572 require_login 1607 1573 NIX_VERSION_URL="${CDN_BASE}/nix-native-notepat-latest.version" 1608 - NIX_ISO_URL="${CDN_BASE}/nix-native-notepat-latest.iso" 1574 + NIX_IMG_URL="${CDN_BASE}/nix-native-notepat-latest.img" 1609 1575 NIX_HASH_URL="${CDN_BASE}/nix-native-notepat-latest.sha256" 1610 1576 1611 1577 log "Fetching latest NixOS OTA version..." ··· 1616 1582 1617 1583 PULL_DIR="/tmp/ac-os-nix-pull" 1618 1584 mkdir -p "${PULL_DIR}" 1619 - PULLED_ISO="${PULL_DIR}/ac-os-nixos.iso" 1585 + PULLED_IMG="${PULL_DIR}/ac-os-nixos.img" 1620 1586 1621 1587 log "Downloading NixOS image (~$(( IMAGE_SIZE / 1048576 ))MB)..." 1622 - curl -f --progress-bar -o "${PULLED_ISO}" "${NIX_ISO_URL}" || { err "Download failed"; exit 1; } 1588 + curl -f --progress-bar -o "${PULLED_IMG}" "${NIX_IMG_URL}" || { err "Download failed"; exit 1; } 1623 1589 1624 1590 log "Verifying SHA256..." 1625 1591 EXPECTED_HASH=$(curl -sf "${NIX_HASH_URL}") || { err "Failed to fetch hash"; exit 1; } 1626 - ACTUAL_HASH=$(sha256sum "${PULLED_ISO}" | cut -d' ' -f1) 1592 + ACTUAL_HASH=$(sha256sum "${PULLED_IMG}" | cut -d' ' -f1) 1627 1593 if [ "${ACTUAL_HASH}" != "${EXPECTED_HASH}" ]; then 1628 1594 err "SHA256 mismatch!" 1629 1595 exit 1 ··· 1632 1598 1633 1599 PULL_CONFIG="${PULL_DIR}/config.json" 1634 1600 write_usb_config_file "${PULL_CONFIG}" 1635 - flash_nixos_image_to_usb "${PULLED_ISO}" "${PULL_CONFIG}" 1601 + flash_nixos_image_to_usb "${PULLED_IMG}" "${PULL_CONFIG}" 1636 1602 rm -rf "${PULL_DIR}" 1637 1603 ;; 1638 1604 *)
+119
fedac/native/scripts/media-layout.sh
··· 21 21 printf '%s\n' "${AC_NIXOS_DATA_PARTITION_MIB:-512}" 22 22 } 23 23 24 + ac_media_legacy_config_size() { 25 + printf '%s\n' "${AC_LEGACY_CONFIG_BYTES:-4096}" 26 + } 27 + 24 28 ac_media_default_identity_json() { 25 29 printf '%s' '{"handle":"","piece":"notepat","sub":"","email":""}' 30 + } 31 + 32 + ac_media_identity_filename() { 33 + printf '%s\n' "${AC_IDENTITY_FILENAME:-identity.bin}" 26 34 } 27 35 28 36 ac_media_bootloader_path() { ··· 101 109 "${json_payload}" 102 110 } 103 111 112 + ac_media_write_legacy_config() { 113 + local out_path="$1" 114 + local json_payload="${2:-$(ac_media_default_identity_json)}" 115 + local total_bytes 116 + local current_size 117 + local pad_size 118 + 119 + total_bytes="$(ac_media_legacy_config_size)" 120 + mkdir -p "$(dirname "${out_path}")" 121 + printf '%s' "${json_payload}" > "${out_path}" 122 + 123 + current_size=$(stat -c%s "${out_path}") 124 + if [ "${current_size}" -gt "${total_bytes}" ]; then 125 + echo "Legacy config payload exceeds ${total_bytes} bytes" >&2 126 + return 1 127 + fi 128 + 129 + pad_size=$(( total_bytes - current_size )) 130 + if [ "${pad_size}" -gt 0 ]; then 131 + head -c "${pad_size}" < /dev/zero | tr '\000' ' ' >> "${out_path}" 132 + fi 133 + } 134 + 104 135 ac_media_stage_boot_tree() { 105 136 local stage_root="$1" 106 137 local kernel_path="$2" ··· 175 206 fi 176 207 } 177 208 209 + ac_media_create_efi_disk_image() { 210 + local stage_root="$1" 211 + local image_path="$2" 212 + local label="${3:-AC_ESP}" 213 + local size_mb="${4:-}" 214 + local esp_start=2048 215 + local esp_offset 216 + 217 + if [ -z "${size_mb}" ]; then 218 + size_mb=$(( $(ac_media_stage_tree_size_mib "${stage_root}") + 96 )) 219 + fi 220 + 221 + dd if=/dev/zero of="${image_path}" bs=1M count="${size_mb}" status=none 222 + printf 'label: gpt\nstart=%s, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, name="%s"\n' \ 223 + "${esp_start}" "${label}" | 224 + sfdisk --force --no-reread "${image_path}" >/dev/null 225 + mkfs.vfat -F 32 --offset="${esp_start}" -n "${label}" "${image_path}" >/dev/null 226 + 227 + esp_offset=$(( esp_start * 512 )) 228 + export MTOOLS_SKIP_CHECK=1 229 + mmd -i "${image_path}@@${esp_offset}" ::EFI ::EFI/BOOT 2>/dev/null || true 230 + if [ -f "${stage_root}/config.json" ]; then 231 + mcopy -o -i "${image_path}@@${esp_offset}" "${stage_root}/config.json" ::config.json 232 + fi 233 + if [ -f "${stage_root}/$(ac_media_identity_filename)" ]; then 234 + mcopy -o -i "${image_path}@@${esp_offset}" "${stage_root}/$(ac_media_identity_filename)" "::$(ac_media_identity_filename)" 235 + fi 236 + mcopy -o -i "${image_path}@@${esp_offset}" "${stage_root}/EFI/BOOT/BOOTX64.EFI" ::EFI/BOOT/BOOTX64.EFI 237 + if [ -f "${stage_root}/EFI/BOOT/BOOTIA32.EFI" ]; then 238 + mcopy -o -i "${image_path}@@${esp_offset}" "${stage_root}/EFI/BOOT/BOOTIA32.EFI" ::EFI/BOOT/BOOTIA32.EFI 239 + fi 240 + } 241 + 178 242 ac_create_fat_boot_image() { 179 243 ac_media_create_fat_image "$@" 180 244 } ··· 211 275 212 276 ac_media_nixos_data_partition_start_sector() { 213 277 ac_media_partition_start_sector "$1" 3 278 + } 279 + 280 + ac_media_generate_manifest() { 281 + local image_path="$1" 282 + local build_name="$2" 283 + local out_path="$3" 284 + local identity_size 285 + local config_size 286 + local identity_marker 287 + 288 + identity_size="$(ac_media_identity_size)" 289 + config_size="$(ac_media_legacy_config_size)" 290 + identity_marker="$(ac_media_identity_marker)" 291 + 292 + python3 - "$image_path" "$build_name" "$out_path" "$identity_size" "$config_size" "$identity_marker" <<'PYEOF' 293 + import json 294 + import os 295 + import sys 296 + from datetime import datetime, timezone 297 + 298 + image_path, build_name, out_path, identity_size, config_size, identity_marker = sys.argv[1:] 299 + identity_size = int(identity_size) 300 + config_size = int(config_size) 301 + needle = b'{"handle":"","piece":"notepat","sub":"","email":""}' 302 + identity_header = (identity_marker + "\n").encode() 303 + 304 + with open(image_path, "rb") as fh: 305 + data = fh.read() 306 + 307 + identity_offset = data.find(identity_header) 308 + config_offsets = [] 309 + start = 0 310 + while True: 311 + idx = data.find(needle, start) 312 + if idx < 0: 313 + break 314 + if idx < len(identity_header) or data[idx - len(identity_header):idx] != identity_header: 315 + config_offsets.append(idx) 316 + start = idx + len(needle) 317 + 318 + manifest = { 319 + "name": build_name, 320 + "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), 321 + "artifactType": "img", 322 + "identityBlockOffset": identity_offset, 323 + "identityBlockSize": identity_size, 324 + "identityMarker": identity_marker, 325 + "configOffsets": config_offsets, 326 + "configPatchSize": config_size, 327 + "imageSize": os.path.getsize(image_path), 328 + } 329 + 330 + with open(out_path, "w", encoding="utf-8") as fh: 331 + json.dump(manifest, fh, indent=2) 332 + PYEOF 214 333 } 215 334 216 335 ac_media_customize_nixos_efi_boot() {
+209
fedac/native/scripts/nixos-image-helper.sh
··· 1 + #!/bin/bash 2 + set -euo pipefail 3 + 4 + source /usr/local/lib/ac-media-layout.sh 5 + 6 + IMAGE_PATH="${1:?usage: nixos-image-helper.sh <image-path> <config-json>}" 7 + CONFIG_JSON_PATH="${2:?usage: nixos-image-helper.sh <image-path> <config-json>}" 8 + 9 + log() { echo "[nixos-image-helper] $*"; } 10 + err() { echo "[nixos-image-helper] $*" >&2; } 11 + 12 + part_path() { 13 + local dev="$1" 14 + local idx="$2" 15 + if [[ "${dev}" =~ [0-9]$ ]]; then 16 + printf '%sp%s\n' "${dev}" "${idx}" 17 + else 18 + printf '%s%s\n' "${dev}" "${idx}" 19 + fi 20 + } 21 + 22 + partition_number_for_type() { 23 + local image_path="$1" 24 + local type_guid="$2" 25 + python3 - "$image_path" "$type_guid" <<'PYEOF' 26 + import re 27 + import subprocess 28 + import sys 29 + 30 + image_path, type_guid = sys.argv[1:] 31 + dump = subprocess.check_output(["sfdisk", "-d", image_path], text=True, stderr=subprocess.DEVNULL) 32 + pattern = re.compile(rf"{re.escape(image_path)}(\d+)\s*:.*type=([0-9A-Fa-f\-]+)") 33 + for line in dump.splitlines(): 34 + match = pattern.match(line.strip()) 35 + if match and match.group(2).lower() == type_guid.lower(): 36 + print(match.group(1)) 37 + raise SystemExit(0) 38 + raise SystemExit(1) 39 + PYEOF 40 + } 41 + 42 + max_partition_number() { 43 + local image_path="$1" 44 + python3 - "$image_path" <<'PYEOF' 45 + import re 46 + import subprocess 47 + import sys 48 + 49 + image_path = sys.argv[1] 50 + dump = subprocess.check_output(["sfdisk", "-d", image_path], text=True, stderr=subprocess.DEVNULL) 51 + pattern = re.compile(rf"{re.escape(image_path)}(\d+)\s*:") 52 + max_idx = 0 53 + for line in dump.splitlines(): 54 + match = pattern.match(line.strip()) 55 + if match: 56 + max_idx = max(max_idx, int(match.group(1))) 57 + print(max_idx) 58 + PYEOF 59 + } 60 + 61 + append_partition() { 62 + local image_path="$1" 63 + local part_number="$2" 64 + local size_mib="$3" 65 + local type_guid="$4" 66 + local part_name="$5" 67 + local sector_size=512 68 + local image_bytes 69 + local image_sectors 70 + local start_sector 71 + local size_sectors 72 + 73 + image_bytes=$(stat -c%s "${image_path}") 74 + image_sectors=$(( (image_bytes + sector_size - 1) / sector_size )) 75 + start_sector=$(( ((image_sectors + 2047) / 2048) * 2048 )) 76 + size_sectors=$(( size_mib * 1024 * 1024 / sector_size )) 77 + 78 + truncate -s $(((start_sector + size_sectors) * sector_size)) "${image_path}" 79 + printf 'start=%s, size=%s, type=%s, name="%s"\n' \ 80 + "${start_sector}" "${size_sectors}" "${type_guid}" "${part_name}" | 81 + sfdisk --no-reread -N "${part_number}" "${image_path}" >/dev/null 82 + } 83 + 84 + wait_for_partition() { 85 + local part="$1" 86 + for _ in $(seq 1 40); do 87 + [ -b "${part}" ] && return 0 88 + sleep 0.25 89 + done 90 + err "Partition did not appear: ${part}" 91 + return 1 92 + } 93 + 94 + LOOP_DEV="" 95 + TMP_DIR="" 96 + EFI_MOUNT="" 97 + MAC_MOUNT="" 98 + DATA_MOUNT="" 99 + 100 + cleanup() { 101 + umount "${EFI_MOUNT:-}" 2>/dev/null || true 102 + umount "${MAC_MOUNT:-}" 2>/dev/null || true 103 + umount "${DATA_MOUNT:-}" 2>/dev/null || true 104 + if [ -n "${LOOP_DEV}" ]; then 105 + losetup -d "${LOOP_DEV}" 2>/dev/null || true 106 + fi 107 + rm -rf "${TMP_DIR:-}" 108 + } 109 + trap cleanup EXIT 110 + 111 + if [ ! -f "${IMAGE_PATH}" ]; then 112 + err "Missing image: ${IMAGE_PATH}" 113 + exit 1 114 + fi 115 + if [ ! -f "${CONFIG_JSON_PATH}" ]; then 116 + err "Missing config JSON: ${CONFIG_JSON_PATH}" 117 + exit 1 118 + fi 119 + 120 + TMP_DIR="$(mktemp -d /tmp/ac-nixos-image.XXXXXX)" 121 + LEGACY_CONFIG="${TMP_DIR}/config.json" 122 + IDENTITY_FILE="${TMP_DIR}/$(ac_media_identity_filename)" 123 + 124 + CONFIG_JSON="$(cat "${CONFIG_JSON_PATH}")" 125 + ac_media_write_legacy_config "${LEGACY_CONFIG}" "${CONFIG_JSON}" 126 + ac_media_write_identity_config "${IDENTITY_FILE}" "${CONFIG_JSON}" 127 + 128 + EFI_PART_NUM="$(partition_number_for_type "${IMAGE_PATH}" "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" || true)" 129 + if [ -z "${EFI_PART_NUM}" ]; then 130 + err "No EFI partition found in ${IMAGE_PATH}" 131 + exit 1 132 + fi 133 + 134 + MAC_PART_NUM="$(partition_number_for_type "${IMAGE_PATH}" "48465300-0000-11aa-aa11-00306543ecac" || true)" 135 + DATA_PART_NUM="$(partition_number_for_type "${IMAGE_PATH}" "ebd0a0a2-b9e5-4433-87c0-68b6b72699c7" || true)" 136 + NEXT_PART_NUM=$(( $(max_partition_number "${IMAGE_PATH}") + 1 )) 137 + 138 + if [ -z "${MAC_PART_NUM}" ]; then 139 + MAC_PART_NUM="${NEXT_PART_NUM}" 140 + append_partition "${IMAGE_PATH}" "${MAC_PART_NUM}" "${AC_NIXOS_MAC_PARTITION_MIB:-256}" \ 141 + "48465300-0000-11AA-AA11-00306543ECAC" "AC-MAC" 142 + NEXT_PART_NUM=$(( NEXT_PART_NUM + 1 )) 143 + fi 144 + 145 + if [ -z "${DATA_PART_NUM}" ]; then 146 + DATA_PART_NUM="${NEXT_PART_NUM}" 147 + append_partition "${IMAGE_PATH}" "${DATA_PART_NUM}" "$(ac_media_nixos_data_size_mib)" \ 148 + "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7" "$(ac_media_nixos_data_label)" 149 + fi 150 + 151 + LOOP_DEV="$(losetup --find --show --partscan "${IMAGE_PATH}")" 152 + EFI_PART="$(part_path "${LOOP_DEV}" "${EFI_PART_NUM}")" 153 + MAC_PART="$(part_path "${LOOP_DEV}" "${MAC_PART_NUM}")" 154 + DATA_PART="$(part_path "${LOOP_DEV}" "${DATA_PART_NUM}")" 155 + 156 + wait_for_partition "${EFI_PART}" 157 + wait_for_partition "${MAC_PART}" 158 + wait_for_partition "${DATA_PART}" 159 + 160 + if ! blkid -o value -s LABEL "${MAC_PART}" >/dev/null 2>&1; then 161 + mkfs.hfsplus -v AC-MAC "${MAC_PART}" >/dev/null 162 + fi 163 + if [ "$(blkid -o value -s LABEL "${DATA_PART}" 2>/dev/null || true)" != "$(ac_media_nixos_data_label)" ]; then 164 + mkfs.vfat -F 32 -n "$(ac_media_nixos_data_label)" "${DATA_PART}" >/dev/null 165 + fi 166 + 167 + EFI_MOUNT="${TMP_DIR}/efi" 168 + MAC_MOUNT="${TMP_DIR}/mac" 169 + DATA_MOUNT="${TMP_DIR}/data" 170 + mkdir -p "${EFI_MOUNT}" "${MAC_MOUNT}" "${DATA_MOUNT}" 171 + 172 + mount -t vfat "${EFI_PART}" "${EFI_MOUNT}" 173 + mount -t vfat "${DATA_PART}" "${DATA_MOUNT}" 174 + mkdir -p "${DATA_MOUNT}/logs" 175 + cp "${LEGACY_CONFIG}" "${DATA_MOUNT}/config.json" 176 + cp "${IDENTITY_FILE}" "${DATA_MOUNT}/$(ac_media_identity_filename)" 177 + sync 178 + umount "${DATA_MOUNT}" 179 + 180 + if mount -t hfsplus "${MAC_PART}" "${MAC_MOUNT}" 2>/dev/null; then 181 + mkdir -p "${MAC_MOUNT}/System/Library/CoreServices" "${MAC_MOUNT}/EFI/BOOT" 182 + cp "${EFI_MOUNT}/EFI/BOOT/BOOTX64.EFI" "${MAC_MOUNT}/System/Library/CoreServices/boot.efi" 183 + cp "${EFI_MOUNT}/EFI/BOOT/BOOTX64.EFI" "${MAC_MOUNT}/EFI/BOOT/BOOTX64.EFI" 184 + cat > "${MAC_MOUNT}/System/Library/CoreServices/SystemVersion.plist" <<'PLIST_EOF' 185 + <?xml version="1.0" encoding="UTF-8"?> 186 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 187 + <plist version="1.0"> 188 + <dict> 189 + <key>ProductBuildVersion</key> 190 + <string></string> 191 + <key>ProductName</key> 192 + <string>Linux</string> 193 + <key>ProductVersion</key> 194 + <string>AC Native OS</string> 195 + </dict> 196 + </plist> 197 + PLIST_EOF 198 + echo "Mach Kernel" > "${MAC_MOUNT}/mach_kernel" 199 + hfs-bless "${MAC_MOUNT}/System/Library/CoreServices/boot.efi" || true 200 + sync 201 + umount "${MAC_MOUNT}" 202 + else 203 + log "Skipping AC-MAC population; hfsplus mount unavailable" 204 + fi 205 + 206 + umount "${EFI_MOUNT}" 207 + sync 208 + fsck.hfsplus -yrdfp "${MAC_PART}" 2>/dev/null || true 209 + log "Prepared ${IMAGE_PATH} with AC-MAC and $(ac_media_nixos_data_label)"
+23 -23
fedac/native/scripts/upload-release.sh
··· 3 3 # Publishes: native-notepat-latest.vmlinuz, .sha256, .version, and updates releases.json 4 4 # 5 5 # Usage: ./upload-release.sh [vmlinuz_path] 6 - # ./upload-release.sh --iso [iso_path] # Upload ISO only 6 + # ./upload-release.sh --image [image_path] # Upload template disk image only 7 7 # Credentials auto-loaded from aesthetic-computer-vault/fedac/native/upload.env 8 8 9 9 set -e 10 10 11 11 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 12 - ISO_ONLY=0 13 - if [ "${1:-}" = "--iso" ]; then 14 - ISO_ONLY=1 15 - ISO_PATH="${2:-${SCRIPT_DIR}/../build/ac-os.iso}" 16 - if [ ! -f "$ISO_PATH" ]; then 17 - echo "Error: ISO not found at $ISO_PATH" >&2 12 + IMAGE_ONLY=0 13 + if [ "${1:-}" = "--image" ] || [ "${1:-}" = "--iso" ]; then 14 + IMAGE_ONLY=1 15 + IMAGE_PATH="${2:-${SCRIPT_DIR}/../build/ac-os.img}" 16 + if [ ! -f "$IMAGE_PATH" ]; then 17 + echo "Error: image not found at $IMAGE_PATH" >&2 18 18 exit 1 19 19 fi 20 20 VMLINUZ="/dev/null" # not used but needed for cred loading below ··· 162 162 echo " channel: ${OTA_CHANNEL}" 163 163 fi 164 164 165 - # ISO-only mode: upload ISO + version + sha256, then exit 166 - if [ "$ISO_ONLY" = "1" ]; then 167 - ISO_SHA256=$(sha256sum "$ISO_PATH" | awk '{print $1}') 168 - ISO_SIZE=$(stat -c%s "$ISO_PATH" 2>/dev/null || stat -f%z "$ISO_PATH") 169 - printf '%s\n%s' "${FULL_VERSION}" "$ISO_SIZE" > "$TMP/version.txt" 170 - printf '%s' "$ISO_SHA256" > "$TMP/sha256.txt" 171 - echo "Uploading ISO: $(du -sh "$ISO_PATH" | cut -f1) sha256=${ISO_SHA256:0:16}..." 165 + # Image-only mode: upload disk image + version + sha256, then exit 166 + if [ "$IMAGE_ONLY" = "1" ]; then 167 + IMAGE_SHA256=$(sha256sum "$IMAGE_PATH" | awk '{print $1}') 168 + IMAGE_SIZE=$(stat -c%s "$IMAGE_PATH" 2>/dev/null || stat -f%z "$IMAGE_PATH") 169 + printf '%s\n%s' "${FULL_VERSION}" "$IMAGE_SIZE" > "$TMP/version.txt" 170 + printf '%s' "$IMAGE_SHA256" > "$TMP/sha256.txt" 171 + echo "Uploading image: $(du -sh "$IMAGE_PATH" | cut -f1) sha256=${IMAGE_SHA256:0:16}..." 172 172 do_upload "$TMP/version.txt" "os/${CHANNEL_PREFIX}native-notepat-latest.version" "text/plain" 173 173 do_upload "$TMP/sha256.txt" "os/${CHANNEL_PREFIX}native-notepat-latest.sha256" "text/plain" 174 - do_upload "$ISO_PATH" "os/${CHANNEL_PREFIX}native-notepat-latest.iso" "application/octet-stream" 175 - echo "ISO published: ${BASE_URL}/os/${CHANNEL_PREFIX}native-notepat-latest.iso" 174 + do_upload "$IMAGE_PATH" "os/${CHANNEL_PREFIX}native-notepat-latest.img" "application/octet-stream" 175 + echo "Image published: ${BASE_URL}/os/${CHANNEL_PREFIX}native-notepat-latest.img" 176 176 exit 0 177 177 fi 178 178 ··· 235 235 do_upload "$INITRAMFS_SIBLING" "os/${CHANNEL_PREFIX}native-notepat-latest.initramfs.cpio.gz" "application/octet-stream" 236 236 fi 237 237 238 - # Also upload ISO if it exists (non-fatal — ISO is optional) 239 - ISO_SIBLING="$(dirname "$VMLINUZ")/ac-os.iso" 240 - if [ -f "$ISO_SIBLING" ]; then 241 - echo " Uploading ISO ($(du -sh "$ISO_SIBLING" | cut -f1))..." 242 - do_upload "$ISO_SIBLING" "os/${CHANNEL_PREFIX}native-notepat-latest.iso" "application/octet-stream" || echo " ISO upload failed (non-fatal)" 238 + # Also upload a template disk image if it exists (non-fatal) 239 + IMAGE_SIBLING="$(dirname "$VMLINUZ")/ac-os.img" 240 + if [ -f "$IMAGE_SIBLING" ]; then 241 + echo " Uploading image ($(du -sh "$IMAGE_SIBLING" | cut -f1))..." 242 + do_upload "$IMAGE_SIBLING" "os/${CHANNEL_PREFIX}native-notepat-latest.img" "application/octet-stream" || echo " Image upload failed (non-fatal)" 243 243 fi 244 244 245 245 echo "" ··· 250 250 echo " ${BASE_URL}/os/${CHANNEL_PREFIX}native-notepat-latest.initramfs.cpio.gz" 251 251 fi 252 252 echo " ${BASE_URL}/os/${CHANNEL_PREFIX}releases.json" 253 - if [ -f "$ISO_SIBLING" ]; then 254 - echo " ${BASE_URL}/os/${CHANNEL_PREFIX}native-notepat-latest.iso" 253 + if [ -f "$IMAGE_SIBLING" ]; then 254 + echo " ${BASE_URL}/os/${CHANNEL_PREFIX}native-notepat-latest.img" 255 255 fi
+14
fedac/nixos/configuration.nix
··· 94 94 ExecStart = pkgs.writeShellScript "mount-usb-config" '' 95 95 set -u 96 96 97 + write_breadcrumb() { 98 + local tag="$1" 99 + local stamp 100 + stamp="$(${pkgs.coreutils}/bin/date -u +%Y%m%dT%H%M%SZ)" 101 + { 102 + echo "tag=${tag}" 103 + echo "stamp=${stamp}" 104 + echo "host=ac-native" 105 + echo "version=${gitHash}-${version}" 106 + } > "/mnt/logs/${tag}-${stamp}.txt" 107 + } 108 + 97 109 mkdir -p /mnt 98 110 ${pkgs.systemd}/bin/udevadm settle --timeout=10 || true 99 111 ··· 104 116 -o rw,uid=1000,gid=100,umask=0077,shortname=mixed,utf8=1 \ 105 117 "$dev" /mnt 2>/dev/null; then 106 118 mkdir -p /mnt/logs 119 + write_breadcrumb "boot-mounted" 107 120 echo "Mounted AC Native data from $dev" 108 121 exit 0 109 122 fi ··· 113 126 chown ac:users /mnt 2>/dev/null || true 114 127 chmod 0700 /mnt 2>/dev/null || true 115 128 mkdir -p /mnt/logs 129 + write_breadcrumb "boot-mounted-temporary" 116 130 echo "No ACDATA partition found; /mnt is temporary" 117 131 ${pkgs.util-linux}/bin/lsblk -o NAME,SIZE,TYPE,FSTYPE,LABEL,MOUNTPOINTS || true 118 132 ${pkgs.util-linux}/bin/blkid || true
+34 -16
fedac/nixos/flake.nix
··· 3 3 4 4 inputs = { 5 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 6 }; 11 7 12 - outputs = { self, nixpkgs, nixos-generators, ... }: 8 + outputs = { self, nixpkgs, ... }: 13 9 let 14 10 system = "x86_64-linux"; 15 11 pkgs = import nixpkgs { inherit system; }; 12 + lib = nixpkgs.lib; 16 13 version = builtins.substring 0 8 (self.lastModifiedDate or "unknown"); 17 14 gitHash = self.shortRev or "dirty"; 18 15 nativeSrcPath = builtins.getEnv "AC_NIX_NATIVE_SRC"; ··· 24 21 } 25 22 else 26 23 throw "AC_NIX_NATIVE_SRC is required for fedac/nixos builds; run nix with --impure and point it at fedac/native."; 24 + specialArgs = { inherit self gitHash version nativeSrc; }; 25 + runtimeModules = [ ./configuration.nix ]; 26 + imageModules = runtimeModules ++ [ ./modules/image.nix ]; 27 + evalConfig = import "${nixpkgs}/nixos/lib/eval-config.nix"; 28 + makeDiskImage = import "${nixpkgs}/nixos/lib/make-disk-image.nix"; 29 + runtimeSystem = lib.nixosSystem { 30 + inherit system; 31 + modules = runtimeModules; 32 + inherit specialArgs; 33 + }; 34 + imageSystem = evalConfig { 35 + inherit system; 36 + modules = imageModules; 37 + inherit specialArgs; 38 + }; 27 39 in 28 40 { 29 41 # The ac-native binary as a standalone package ··· 32 44 inherit gitHash version nativeSrc; 33 45 }; 34 46 35 - # Bootable ISO image (no KVM needed to build) 36 - usb-image = nixos-generators.nixosGenerate { 37 - inherit system; 38 - modules = [ ./configuration.nix ]; 39 - format = "iso"; 40 - specialArgs = { inherit self gitHash version nativeSrc; }; 47 + # Bootable raw disk image with BIOS + UEFI bootloader install. 48 + # In nixpkgs make-disk-image, "hybrid" is GPT with an ESP plus 49 + # a bios_grub partition, not a hybrid MBR. 50 + usb-image = makeDiskImage { 51 + inherit pkgs lib; 52 + config = imageSystem.config; 53 + format = "raw"; 54 + onlyNixStore = false; 55 + partitionTableType = "hybrid"; 56 + installBootLoader = true; 57 + touchEFIVars = false; 58 + copyChannel = false; 59 + diskSize = "auto"; 60 + additionalSpace = "2G"; 61 + memSize = 4096; 41 62 }; 42 63 43 64 default = self.packages.${system}.usb-image; 44 65 }; 45 66 46 67 # Full NixOS system configuration 47 - nixosConfigurations.ac-native-os = nixpkgs.lib.nixosSystem { 48 - inherit system; 49 - modules = [ ./configuration.nix ]; 50 - specialArgs = { inherit self gitHash version nativeSrc; }; 51 - }; 68 + nixosConfigurations.ac-native-os = runtimeSystem; 69 + nixosConfigurations.ac-native-image = imageSystem; 52 70 }; 53 71 }
+27
fedac/nixos/modules/image.nix
··· 1 + { lib, ... }: 2 + 3 + { 4 + # Raw disk image builds need a full installed bootloader, not the live ISO path. 5 + boot.loader.systemd-boot.enable = lib.mkForce false; 6 + boot.loader.efi.canTouchEfiVariables = lib.mkForce false; 7 + boot.loader.grub = { 8 + enable = true; 9 + device = "/dev/vda"; 10 + efiSupport = true; 11 + efiInstallAsRemovable = true; 12 + configurationLimit = 1; 13 + }; 14 + boot.loader.timeout = lib.mkForce 0; 15 + boot.loader.grub.timeoutStyle = lib.mkForce "hidden"; 16 + 17 + # Make early boot text visible on the real display for debug + fallback boot paths. 18 + boot.kernelParams = lib.mkAfter [ "console=tty0" ]; 19 + 20 + # Let make-disk-image perform a full NixOS install onto a raw disk image. 21 + fileSystems."/" = lib.mkForce { 22 + device = "/dev/vda"; 23 + fsType = "ext4"; 24 + autoFormat = true; 25 + }; 26 + virtualisation.useBootLoader = lib.mkForce true; 27 + }
+22
fedac/nixos/modules/kiosk.nix
··· 2 2 3 3 let 4 4 ac-native = pkgs.callPackage ../packages/ac-native { inherit gitHash version nativeSrc; }; 5 + write-breadcrumb = pkgs.writeShellScript "ac-native-write-breadcrumb" '' 6 + set -u 7 + 8 + [ $# -ge 1 ] || exit 0 9 + [ -d /mnt/logs ] || exit 0 10 + 11 + tag="$1" 12 + shift || true 13 + stamp="$(${pkgs.coreutils}/bin/date -u +%Y%m%dT%H%M%SZ)" 14 + out="/mnt/logs/${tag}-${stamp}.txt" 15 + { 16 + echo "tag=${tag}" 17 + echo "stamp=${stamp}" 18 + echo "version=${gitHash}-${version}" 19 + for entry in "$@"; do 20 + echo "$entry" 21 + done 22 + } > "$out" 2>/dev/null || true 23 + sync || true 24 + ''; 5 25 ac-native-client = pkgs.writeShellScript "ac-native-cage-client" '' 6 26 set -u 7 27 8 28 rm -f /tmp/ac-native-cage.log 29 + ${write-breadcrumb} ac-native-starting "binary=${ac-native}/bin/ac-native" "piece=${ac-native}/share/ac-native/piece.mjs" 9 30 printf '[ac-native-cage-client] starting %s %s\n' \ 10 31 "${ac-native}/bin/ac-native" \ 11 32 "${ac-native}/share/ac-native/piece.mjs" >&2 ··· 29 50 set -u 30 51 31 52 rm -f /tmp/cage-stderr.log 53 + ${write-breadcrumb} kiosk-launching "tty=/dev/tty1" "display=cage" 32 54 printf '[ac-native-kiosk] launching cage on tty1\n' >&2 33 55 34 56 status=0
+8 -2
lith/server.mjs
··· 735 735 736 736 if (req.headers["x-template-upload"] === "true") { 737 737 try { 738 - return res.json({ step: "template-upload", iso_put_url: presignUrl("os/native-notepat-latest.iso", "application/x-iso9660-image"), user: userSub }); 738 + return res.json({ step: "template-upload", image_put_url: presignUrl("os/native-notepat-latest.img", "application/octet-stream"), user: userSub }); 739 739 } catch (err) { 740 740 return res.status(500).json({ error: `Template presign failed: ${err.message}` }); 741 741 } ··· 755 755 if (!authHeader) return res.status(401).json({ error: "Authorization required. Log in at aesthetic.computer first." }); 756 756 757 757 try { 758 - const ovenRes = await fetch("https://oven.aesthetic.computer/os-image", { 758 + const search = new URLSearchParams(req.query || {}).toString(); 759 + const ovenUrl = "https://oven.aesthetic.computer/os-image" + (search ? `?${search}` : ""); 760 + const ovenRes = await fetch(ovenUrl, { 759 761 headers: { Authorization: authHeader }, 760 762 }); 761 763 res.status(ovenRes.status); 762 764 res.set("Content-Type", ovenRes.headers.get("content-type") || "application/octet-stream"); 763 765 if (ovenRes.headers.get("content-disposition")) res.set("Content-Disposition", ovenRes.headers.get("content-disposition")); 764 766 if (ovenRes.headers.get("content-length")) res.set("Content-Length", ovenRes.headers.get("content-length")); 767 + if (ovenRes.headers.get("x-ac-os-requested-layout")) res.set("X-AC-OS-Requested-Layout", ovenRes.headers.get("x-ac-os-requested-layout")); 768 + if (ovenRes.headers.get("x-ac-os-layout")) res.set("X-AC-OS-Layout", ovenRes.headers.get("x-ac-os-layout")); 769 + if (ovenRes.headers.get("x-ac-os-fallback")) res.set("X-AC-OS-Fallback", ovenRes.headers.get("x-ac-os-fallback")); 770 + if (ovenRes.headers.get("x-ac-os-fallback-reason")) res.set("X-AC-OS-Fallback-Reason", ovenRes.headers.get("x-ac-os-fallback-reason")); 765 771 res.set("Access-Control-Allow-Origin", "*"); 766 772 const { Readable } = await import("stream"); 767 773 Readable.fromWeb(ovenRes.body).pipe(res);
+86 -51
oven-edge/worker.mjs
··· 1 1 // oven-edge — Cloudflare Worker that serves AC OS images from the edge. 2 - // Template ISOs are cached in R2 (or DO Spaces fallback). Personalized 3 - // images are patched on-the-fly by overwriting the 32KB identity block. 2 + // Template disk images are cached in R2 (or DO Spaces fallback). Personalized 3 + // images are patched on-the-fly by overwriting the identity block and 4 + // legacy config placeholder. 4 5 // 5 6 // Routes: 6 - // /os/latest.iso → latest template ISO (cached 24h at edge) 7 - // /os/<name>.iso → specific build ISO (cached 24h) 7 + // /os/latest.img → latest template image (cached 24h at edge) 8 + // /os/<name>.img → specific build image (cached 24h) 8 9 // /os/<name>.vmlinuz → specific build kernel (cached 24h) 9 10 // /os-releases → build list (cached 1 min) 10 11 // /os-image → personalized image (streaming patch at edge) ··· 15 16 16 17 const IDENTITY_MARKER = "AC_IDENTITY_BLOCK_V1\n"; 17 18 const IDENTITY_BLOCK_SIZE = 32768; 19 + const CONFIG_MARKER = '{"handle":"","piece":"notepat","sub":"","email":""}'; 20 + const DEFAULT_CONFIG_PATCH_SIZE = 4096; 18 21 19 22 function edgeHeaders(request, extra = {}) { 20 23 return { ··· 44 47 return block; 45 48 } 46 49 47 - // Stream a template ISO from source, patching the identity block on-the-fly 48 - function streamPatchedISO(templateBody, identityBlock, manifest) { 49 - const offset = manifest.identityBlockOffset; 50 - const size = IDENTITY_BLOCK_SIZE; 50 + function makeLegacyConfigBlock(config, size = DEFAULT_CONFIG_PATCH_SIZE) { 51 + const json = JSON.stringify(config); 52 + const encoder = new TextEncoder(); 53 + const jsonBytes = encoder.encode(json); 54 + const block = new Uint8Array(size); 55 + block.fill(0x20); 56 + block.set(jsonBytes.subarray(0, size)); 57 + return block; 58 + } 59 + 60 + // Stream a template image from source, patching configured ranges on-the-fly. 61 + function streamPatchedImage(templateBody, patches) { 51 62 let bytesSeen = 0; 52 63 53 64 const { readable, writable } = new TransformStream({ ··· 56 67 const chunkEnd = bytesSeen + chunk.byteLength; 57 68 bytesSeen = chunkEnd; 58 69 59 - // Fast path: chunk doesn't overlap identity block 60 - if (chunkEnd <= offset || chunkStart >= offset + size) { 70 + let buf = null; 71 + for (const patch of patches) { 72 + const offset = patch.offset; 73 + const size = patch.bytes.byteLength; 74 + if (offset < 0 || chunkEnd <= offset || chunkStart >= offset + size) { 75 + continue; 76 + } 77 + if (!buf) buf = new Uint8Array(chunk); 78 + const patchStart = Math.max(0, offset - chunkStart); 79 + const patchOffset = Math.max(0, chunkStart - offset); 80 + const patchLen = Math.min(size - patchOffset, buf.length - patchStart); 81 + buf.set( 82 + patch.bytes.subarray(patchOffset, patchOffset + patchLen), 83 + patchStart, 84 + ); 85 + } 86 + 87 + if (!buf) { 61 88 controller.enqueue(chunk); 62 89 return; 63 90 } 64 - 65 - // Slow path: chunk overlaps identity block — patch it 66 - const buf = new Uint8Array(chunk); 67 - const patchStart = Math.max(0, offset - chunkStart); 68 - const patchOffset = Math.max(0, chunkStart - offset); 69 - const patchLen = Math.min(size - patchOffset, buf.length - patchStart); 70 - buf.set( 71 - identityBlock.subarray(patchOffset, patchOffset + patchLen), 72 - patchStart, 73 - ); 74 91 controller.enqueue(buf); 75 92 }, 76 93 }); ··· 92 109 return null; 93 110 } 94 111 95 - // Get template ISO body as a ReadableStream 112 + // Get template image body as a ReadableStream 96 113 async function getTemplateStream(env, manifest) { 97 114 const buildName = manifest?.name; 98 115 if (!buildName) return null; 99 116 100 117 // Try R2 first 101 118 if (env?.OS_IMAGES) { 102 - const obj = await env.OS_IMAGES.get(`builds/${buildName}/template.iso`); 119 + const obj = await env.OS_IMAGES.get(`builds/${buildName}/template.img`); 103 120 if (obj) { 104 121 const size = Number(obj.size || 0); 105 - if (!manifest?.isoSize || !size || size === manifest.isoSize) { 106 - return { body: obj.body, size: size || manifest?.isoSize || 0 }; 122 + if (!manifest?.imageSize || !size || size === manifest.imageSize) { 123 + return { body: obj.body, size: size || manifest?.imageSize || 0 }; 107 124 } 108 125 } 109 126 } 110 127 111 128 // Fallback: fetch from DO Spaces (with edge caching) 112 - const res = await fetch(SPACES + "/os/native-notepat-latest.iso", { 129 + const res = await fetch(SPACES + "/os/native-notepat-latest.img", { 113 130 cf: { cacheTtl: 86400, cacheEverything: true }, 114 131 }); 115 132 if (res.ok) { 116 133 const size = Number(res.headers.get("content-length") || "0"); 117 - if (!manifest?.isoSize || !size || size === manifest.isoSize) { 118 - return { body: res.body, size: size || manifest?.isoSize || 0 }; 134 + if (!manifest?.imageSize || !size || size === manifest.imageSize) { 135 + return { body: res.body, size: size || manifest?.imageSize || 0 }; 119 136 } 120 137 } 121 138 ··· 132 149 return new Response(null, { headers: edgeHeaders(request) }); 133 150 } 134 151 135 - // --- /os-image → personalized ISO (streaming edge patch) --- 152 + // --- /os-image → personalized image (streaming edge patch) --- 136 153 if (path === "/os-image") { 137 154 const auth = request.headers.get("Authorization") || ""; 138 155 if (!auth) { ··· 155 172 } 156 173 const config = await configRes.json(); 157 174 158 - // 2. Get manifest (has identity block offset) 175 + // 2. Get manifest (has patch offsets) 159 176 const manifest = await getManifest(env); 160 - if (!manifest || manifest.identityBlockOffset < 0) { 161 - // No manifest or no offset — fall through to oven origin for legacy patching 177 + const hasIdentity = Number.isFinite(manifest?.identityBlockOffset) && manifest.identityBlockOffset >= 0; 178 + const configOffsets = Array.isArray(manifest?.configOffsets) ? manifest.configOffsets : []; 179 + if (!manifest || (!hasIdentity && configOffsets.length === 0)) { 180 + // No manifest or no offsets — fall through to oven origin for legacy patching 162 181 const ovenRes = await fetch(ORIGIN + "/os-image" + url.search, { 163 182 headers: { Authorization: auth }, 164 183 }); ··· 175 194 return applyHeaders(ovenRes, request, { "X-Patch": "origin-fallback" }); 176 195 } 177 196 178 - // 4. Build identity block and stream with patch 179 - const identityBlock = makeIdentityBlock(config); 180 - const patched = streamPatchedISO(template.body, identityBlock, manifest); 197 + // 4. Build patch payloads and stream with patch 198 + const patches = []; 199 + if (hasIdentity) { 200 + patches.push({ 201 + offset: manifest.identityBlockOffset, 202 + bytes: makeIdentityBlock(config), 203 + }); 204 + } 205 + const configBlock = makeLegacyConfigBlock( 206 + config, 207 + Number(manifest.configPatchSize || DEFAULT_CONFIG_PATCH_SIZE), 208 + ); 209 + for (const offset of configOffsets) { 210 + if (Number.isFinite(offset) && offset >= 0) { 211 + patches.push({ offset, bytes: configBlock }); 212 + } 213 + } 214 + patches.sort((a, b) => a.offset - b.offset); 215 + const patched = streamPatchedImage(template.body, patches); 181 216 182 217 const handle = config.handle || "unknown"; 183 - const filename = `@${handle}-os-${config.piece || "notepat"}-AC-${manifest.name}.iso`; 184 - const requestedLayout = (url.searchParams.get("layout") || "iso").toLowerCase(); 218 + const filename = `@${handle}-os-${config.piece || "notepat"}-AC-${manifest.name}.img`; 219 + const requestedLayout = (url.searchParams.get("layout") || "img").toLowerCase(); 185 220 186 221 return new Response(patched, { 187 222 headers: { 188 223 ...edgeHeaders(request), 189 - "Content-Type": "application/x-iso9660-image", 224 + "Content-Type": "application/octet-stream", 190 225 "Content-Disposition": `attachment; filename="${filename}"`, 191 - "Content-Length": String(template.size || manifest.isoSize), 226 + "Content-Length": String(template.size || manifest.imageSize), 192 227 "X-AC-OS-Requested-Layout": requestedLayout, 193 - "X-AC-OS-Layout": "iso", 228 + "X-AC-OS-Layout": "img", 194 229 "X-Build": manifest.name, 195 230 "X-Patch": "edge", 196 231 }, 197 232 }); 198 233 } 199 234 200 - // --- /os/latest.iso → latest template ISO from DO Spaces --- 201 - if (path === "/os/latest.iso" || path === "/os-template-iso") { 202 - const isoUrl = SPACES + "/os/native-notepat-latest.iso"; 203 - const res = await fetch(isoUrl, { 235 + // --- /os/latest.img → latest template image from DO Spaces --- 236 + if (path === "/os/latest.img" || path === "/os-template-img") { 237 + const imgUrl = SPACES + "/os/native-notepat-latest.img"; 238 + const res = await fetch(imgUrl, { 204 239 cf: { cacheTtl: 86400, cacheEverything: true }, 205 240 }); 206 241 const out = new Response(res.body, res); 207 242 out.headers.set( 208 243 "Content-Disposition", 209 - "attachment; filename=ac-os-latest.iso", 244 + "attachment; filename=ac-os-latest.img", 210 245 ); 211 246 for (const [k, v] of Object.entries(edgeHeaders(request))) 212 247 out.headers.set(k, v); ··· 233 268 return out; 234 269 } 235 270 236 - // --- /os/<name>.iso → named build ISO from DO Spaces --- 237 - const isoMatch = path.match(/^\/os\/([a-z]+-[a-z]+)\.iso$/); 238 - if (isoMatch) { 239 - const name = isoMatch[1]; 240 - const isoUrl = SPACES + "/os/builds/" + name + ".iso"; 241 - const res = await fetch(isoUrl, { 271 + // --- /os/<name>.img → named build image from DO Spaces --- 272 + const imgMatch = path.match(/^\/os\/([a-z]+-[a-z]+)\.img$/); 273 + if (imgMatch) { 274 + const name = imgMatch[1]; 275 + const imgUrl = SPACES + "/os/builds/" + name + ".img"; 276 + const res = await fetch(imgUrl, { 242 277 cf: { cacheTtl: 86400, cacheEverything: true }, 243 278 }); 244 279 if (!res.ok) ··· 246 281 const out = new Response(res.body, res); 247 282 out.headers.set( 248 283 "Content-Disposition", 249 - "attachment; filename=" + name + ".iso", 284 + "attachment; filename=" + name + ".img", 250 285 ); 251 286 for (const [k, v] of Object.entries(edgeHeaders(request))) 252 287 out.headers.set(k, v);
+27 -23
oven/native-builder.mjs
··· 34 34 process.env.NATIVE_DIR || "/opt/oven/native-git/fedac/native"; 35 35 const NATIVE_BRANCH = process.env.NATIVE_GIT_BRANCH || "main"; 36 36 const NIX_DATA_PARTITION_MIB = process.env.NIX_DATA_PARTITION_MIB || "512"; 37 + const MEDIA_HELPER_IMAGE = 38 + process.env.AC_MEDIA_HELPER_IMAGE || "ac-os-media-helper:img-v1"; 37 39 const NIX_BIN_CANDIDATES = [ 38 40 process.env.NIX_BIN || "", 39 41 "/usr/local/bin/nix", ··· 604 606 if (progressCallback) progressCallback(makeSnapshot(job)); 605 607 606 608 // fedac/nixos reads AC_NIX_NATIVE_SRC from the host env to import fedac/native. 607 - // Build the NixOS ISO image 609 + // Build the raw NixOS disk image. 608 610 await runPhase(job, "nix-build", nixBin, [ 609 611 "build", ".#usb-image", 610 612 "--impure", ··· 618 620 .reverse() 619 621 .find((entry) => 620 622 entry.stream === "stdout" && 621 - /^\/nix\/store\/.+\.iso$/.test(entry.line || "") 623 + /^\/nix\/store\/.+$/.test(entry.line || "") 622 624 )?.line || ""; 623 625 if (!nixOutResult) { 624 626 throw new Error("NixOS build finished without returning an output path"); 625 627 } 626 628 627 - // Find the ISO in the output directory. 628 - const isoPath = await runSync( 629 + // Find the raw disk image in the output directory. 630 + const imgPath = await runSync( 629 631 "bash", 630 - ["-lc", "find \"$1\" -name '*.iso' -type f | head -1", "_", nixOutResult], 632 + ["-lc", "find \"$1\" -name '*.img' -type f | head -1", "_", nixOutResult], 631 633 nixosDir, 632 634 ); 633 635 634 - if (!isoPath) { 635 - throw new Error("NixOS build produced no ISO file"); 636 + if (!imgPath) { 637 + throw new Error("NixOS build produced no image file"); 636 638 } 637 639 638 - addLogLine(job, "stdout", `NixOS image: ${isoPath}`); 640 + addLogLine(job, "stdout", `NixOS image: ${imgPath}`); 639 641 640 642 // Copy to upload directory 641 643 await fs.mkdir(nixUploadDir, { recursive: true }); 642 - const nixIsoUpload = path.join(nixUploadDir, "ac-os-nixos.iso"); 644 + const nixImgUpload = path.join(nixUploadDir, "ac-os-nixos.img"); 643 645 const nixConfigUpload = path.join(nixUploadDir, "config.json"); 644 - await fs.copyFile(isoPath, nixIsoUpload); 646 + await fs.copyFile(imgPath, nixImgUpload); 645 647 await fs.writeFile( 646 648 nixConfigUpload, 647 649 `${JSON.stringify({ handle: "", piece: "notepat", sub: "", email: "" })}\n`, 648 650 ); 649 651 650 - addLogLine(job, "stdout", "Phase N: appending writable ACDATA partition..."); 651 - await runPhase(job, "nix-package", "bash", [ 652 + addLogLine(job, "stdout", "Phase N: building media helper image..."); 653 + await runPhase(job, "nix-helper-build", "docker", [ 654 + "build", "-t", MEDIA_HELPER_IMAGE, 655 + "-f", path.join(repoDir, "fedac/native/Dockerfile.flash-helper"), 656 + repoDir, 657 + ], repoDir); 658 + 659 + addLogLine(job, "stdout", "Phase N: appending AC-MAC + ACDATA partitions..."); 660 + await runPhase(job, "nix-package", "docker", [ 661 + "run", "--rm", "--privileged", 662 + "-v", `${nixUploadDir}:/work`, 663 + "--entrypoint", "/bin/bash", 664 + MEDIA_HELPER_IMAGE, 652 665 "-lc", 653 - [ 654 - "set -euo pipefail", 655 - `. "${path.join(NATIVE_DIR, "scripts/media-layout.sh")}"`, 656 - `ac_media_ensure_nixos_data_partition "$1" "$2" "${NIX_DATA_PARTITION_MIB}"`, 657 - 'ac_media_customize_nixos_efi_boot "$1"', 658 - 'sfdisk -d "$1"', 659 - ].join("\n"), 660 - "_", 661 - nixIsoUpload, 662 - nixConfigUpload, 666 + "exec /usr/local/bin/ac-os-nixos-image-helper /work/ac-os-nixos.img /work/config.json", 663 667 ], nixUploadDir, nixEnv); 664 668 665 669 job.stage = "nix-upload"; ··· 667 671 668 672 // Upload with nix- channel prefix 669 673 await runPhase(job, "nix-upload", "bash", [ 670 - uploadScript, "--iso", nixIsoUpload, 674 + uploadScript, "--image", nixImgUpload, 671 675 ], NATIVE_DIR, { 672 676 ...uploadEnv, 673 677 OTA_CHANNEL: "nix",
+53 -107
oven/server.mjs
··· 2997 2997 res.json({ flushed: true }); 2998 2998 }); 2999 2999 3000 - // Personalized FedAC OS .iso download for authenticated AC users. 3001 - // Downloads the template .iso from DO Spaces, patches config.json in-place, 3000 + // Personalized FedAC OS .img download for authenticated AC users. 3001 + // Downloads the template .img from DO Spaces, patches config.json in-place, 3002 3002 // and streams back. Compatible with Fedora Media Writer, Balena Etcher, dd. 3003 3003 const RELEASES_BASE = 'https://releases-aesthetic-computer.sfo3.digitaloceanspaces.com/os'; 3004 - const TEMPLATE_ISO_URL = `${RELEASES_BASE}/native-notepat-latest.iso`; 3004 + const TEMPLATE_IMG_URL = `${RELEASES_BASE}/native-notepat-latest.img`; 3005 3005 const TEMPLATE_GZ_URL = `${RELEASES_BASE}/native-notepat-latest.img.gz`; // legacy fallback 3006 3006 const TEMPLATE_VMLINUZ_URL = `${RELEASES_BASE}/native-notepat-latest.vmlinuz`; 3007 3007 const TEMPLATE_CL_VMLINUZ_URL = `${RELEASES_BASE}/cl-native-notepat-latest.vmlinuz`; ··· 3019 3019 if (templateCache && Date.now() - templateCacheTime < TEMPLATE_CACHE_TTL) { 3020 3020 return templateCache; 3021 3021 } 3022 - // Try .iso first, fall back to legacy .img.gz 3022 + // Try the raw .img first, fall back to the older compressed image if needed. 3023 3023 let raw; 3024 - const isoRes = await fetch(TEMPLATE_ISO_URL); 3025 - if (isoRes.ok) { 3026 - console.log('[os-image] Downloading template .iso...'); 3027 - raw = Buffer.from(await isoRes.arrayBuffer()); 3024 + const imgRes = await fetch(TEMPLATE_IMG_URL); 3025 + if (imgRes.ok) { 3026 + console.log('[os-image] Downloading template .img...'); 3027 + raw = Buffer.from(await imgRes.arrayBuffer()); 3028 3028 } else { 3029 - console.log('[os-image] No .iso found, trying legacy .img.gz fallback...'); 3029 + console.log('[os-image] No .img found, trying legacy .img.gz fallback...'); 3030 3030 const gzRes = await fetch(TEMPLATE_GZ_URL); 3031 3031 if (gzRes.ok) { 3032 3032 const compressed = Buffer.from(await gzRes.arrayBuffer()); 3033 3033 console.log(`[os-image] Decompressing ${(compressed.length / 1048576).toFixed(1)}MB...`); 3034 3034 raw = gunzipSync(compressed); 3035 3035 } else { 3036 - throw new Error(`Template download failed (no .iso or .img.gz available)`); 3036 + throw new Error(`Template download failed (no .img or .img.gz available)`); 3037 3037 } 3038 3038 } 3039 3039 templateCache = raw; ··· 3089 3089 } 3090 3090 } 3091 3091 3092 - // User config endpoint for edge worker ISO patching 3092 + // User config endpoint for edge worker image patching 3093 3093 app.get('/api/user-config', async (req, res) => { 3094 3094 const authHeader = req.headers.authorization || ''; 3095 3095 const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : ''; ··· 3152 3152 }); 3153 3153 3154 3154 app.get('/os-image', async (req, res) => { 3155 - // Auth: verify AC token 3156 3155 const authHeader = req.headers.authorization || ''; 3157 3156 const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : ''; 3158 3157 if (!token) { ··· 3170 3169 return res.status(401).json({ error: `Authentication failed: ${err.message}` }); 3171 3170 } 3172 3171 3173 - // Look up handle by sub (avoids stale /user cache for new handles) 3174 3172 let handle = ''; 3175 3173 const sub = userInfo.sub || ''; 3176 3174 try { ··· 3187 3185 return res.status(403).json({ error: 'You need a handle first. Visit aesthetic.computer/handle to claim one.' }); 3188 3186 } 3189 3187 3190 - // Boot-to piece preference (default: notepat) 3191 3188 const ALLOWED_PIECES = ['notepat', 'prompt', 'chat', 'laer-klokken']; 3192 3189 const reqPiece = req.query.piece || 'notepat'; 3193 3190 const bootPiece = ALLOWED_PIECES.includes(reqPiece) ? reqPiece : 'notepat'; 3194 - 3195 - // WiFi/internet toggle (default: enabled) 3196 3191 const wifiParam = req.query.wifi; 3197 3192 const wifiEnabled = wifiParam !== '0' && wifiParam !== 'false'; 3193 + const requestedLayout = String(req.query.layout || 'img').toLowerCase(); 3194 + const variant = String(req.query.variant || '').toLowerCase() === 'cl' ? 'cl' : 'c'; 3198 3195 3199 - // Fetch device tokens (Claude + GitHub) from DB 3200 3196 let claudeToken = '', githubPat = ''; 3201 3197 try { 3202 3198 const mongoUri = process.env.MONGODB_CONNECTION_STRING; ··· 3216 3212 console.warn(`[os-image] Token lookup failed: ${err.message}`); 3217 3213 } 3218 3214 3219 - console.log(`[os-image] Building personalized image for @${handle} (boot: ${bootPiece}, wifi: ${wifiEnabled}, claude: ${!!claudeToken}, git: ${!!githubPat})`); 3220 - 3221 - const variant = String(req.query.variant || '').toLowerCase() === 'cl' ? 'cl' : 'c'; 3222 - const layout = String(req.query.layout || '').toLowerCase(); 3223 - const preferEfiLayout = layout === 'efi' || layout === 'img' || layout === 'raw'; 3224 - const strictEfi = 3225 - preferEfiLayout && 3226 - String(req.query.strict || '1').toLowerCase() !== '0' && 3227 - String(req.query.strict || '1').toLowerCase() !== 'false'; 3215 + console.log(`[os-image] Building personalized image for @${handle} (boot: ${bootPiece}, wifi: ${wifiEnabled}, variant: ${variant}, claude: ${!!claudeToken}, git: ${!!githubPat})`); 3228 3216 3229 - // Build personalized config JSON 3230 3217 const configObj = { 3231 3218 handle, 3232 3219 piece: bootPiece, ··· 3239 3226 if (!wifiEnabled) configObj.wifi = false; 3240 3227 const configJson = JSON.stringify(configObj); 3241 3228 3242 - // Build a direct EFI image (single ESP partition) when requested. 3243 - // This layout matches local ac-os flash and is more firmware-compatible 3244 - // than some hybrid ISO scanners on older BIOS/UEFI implementations. 3245 - let imgData = null; 3246 - let contentType = 'application/x-iso9660-image'; 3247 - let extension = 'iso'; 3248 - let servedLayout = 'iso'; 3249 - let efiError = null; 3250 - if (preferEfiLayout) { 3251 - try { 3252 - const kernelUrl = kernelUrlForVariant(variant); 3253 - imgData = await buildPersonalizedEfiImage({ kernelUrl, configJson }); 3254 - contentType = 'application/octet-stream'; 3255 - extension = 'img'; 3256 - servedLayout = 'efi'; 3257 - console.log( 3258 - `[os-image] Built EFI image for @${handle} (${(imgData.length / 1048576).toFixed(1)}MB, variant=${variant})`, 3259 - ); 3260 - } catch (err) { 3261 - efiError = err; 3262 - console.warn(`[os-image] EFI layout build failed, falling back to ISO patch path: ${err.message}`); 3263 - if (strictEfi) { 3264 - return res.status(503).json({ 3265 - error: `EFI layout build failed: ${err.message}`, 3266 - requestedLayout: 'efi', 3267 - }); 3268 - } 3269 - } 3229 + let imgData; 3230 + try { 3231 + const template = await getTemplate(); 3232 + imgData = Buffer.from(template); 3233 + } catch (err) { 3234 + return res.status(503).json({ error: `Template not available: ${err.message}` }); 3270 3235 } 3271 3236 3272 - // Fallback: patch the template ISO in-place 3273 - if (!imgData) { 3274 - try { 3275 - const template = await getTemplate(); 3276 - imgData = Buffer.from(template); // copy so we don't mutate cache 3277 - } catch (err) { 3278 - return res.status(503).json({ error: `Template not available: ${err.message}` }); 3279 - } 3237 + const identityMarkerBuf = Buffer.from(IDENTITY_MARKER + '\n'); 3238 + let idx = imgData.indexOf(identityMarkerBuf); 3239 + let identityPatchCount = 0; 3240 + while (idx !== -1) { 3241 + const block = Buffer.alloc(IDENTITY_BLOCK_SIZE, 0); 3242 + const header = Buffer.from(IDENTITY_MARKER + '\n' + configJson); 3243 + header.copy(block); 3244 + block.copy(imgData, idx); 3245 + identityPatchCount++; 3246 + idx = imgData.indexOf(identityMarkerBuf, idx + IDENTITY_BLOCK_SIZE); 3247 + } 3280 3248 3281 - // Try new identity block format first (32KB, marker-prefixed) 3282 - const identityMarkerBuf = Buffer.from(IDENTITY_MARKER + '\n'); 3283 - let idx = imgData.indexOf(identityMarkerBuf); 3284 - let patchCount = 0; 3249 + const padded = configJson.length >= CONFIG_PAD_SIZE_LEGACY 3250 + ? configJson.slice(0, CONFIG_PAD_SIZE_LEGACY) 3251 + : configJson + ' '.repeat(CONFIG_PAD_SIZE_LEGACY - configJson.length); 3252 + const configBytes = Buffer.from(padded); 3253 + const legacyMarkerBuf = Buffer.from(CONFIG_MARKER_LEGACY); 3254 + let configPatchCount = 0; 3255 + idx = imgData.indexOf(legacyMarkerBuf); 3256 + while (idx !== -1) { 3257 + configBytes.copy(imgData, idx); 3258 + configPatchCount++; 3259 + idx = imgData.indexOf(legacyMarkerBuf, idx + CONFIG_PAD_SIZE_LEGACY); 3260 + } 3285 3261 3286 - if (idx !== -1) { 3287 - // New format: marker + newline + JSON + zero-padding to 32KB 3288 - while (idx !== -1) { 3289 - const block = Buffer.alloc(IDENTITY_BLOCK_SIZE, 0); 3290 - const header = Buffer.from(IDENTITY_MARKER + '\n' + configJson); 3291 - header.copy(block); 3292 - block.copy(imgData, idx); 3293 - patchCount++; 3294 - idx = imgData.indexOf(identityMarkerBuf, idx + IDENTITY_BLOCK_SIZE); 3295 - } 3296 - console.log(`[os-image] Patched ${patchCount} identity block(s) for @${handle} (v1, 32KB)`); 3297 - } else { 3298 - // Legacy format: plain JSON padded to 4KB with spaces 3299 - const legacyMarkerBuf = Buffer.from(CONFIG_MARKER_LEGACY); 3300 - idx = imgData.indexOf(legacyMarkerBuf); 3301 - if (idx === -1) { 3302 - return res.status(500).json({ error: 'Template image missing config placeholder' }); 3303 - } 3304 - const padded = configJson + ' '.repeat(Math.max(0, CONFIG_PAD_SIZE_LEGACY - configJson.length)); 3305 - const configBytes = Buffer.from(padded); 3306 - while (idx !== -1) { 3307 - configBytes.copy(imgData, idx); 3308 - patchCount++; 3309 - idx = imgData.indexOf(legacyMarkerBuf, idx + CONFIG_PAD_SIZE_LEGACY); 3310 - } 3311 - console.log(`[os-image] Patched ${patchCount} config location(s) for @${handle} (legacy, 4KB)`); 3312 - } 3262 + if (identityPatchCount === 0 && configPatchCount === 0) { 3263 + return res.status(500).json({ error: 'Template image missing config placeholder' }); 3313 3264 } 3265 + console.log( 3266 + `[os-image] Patched ${identityPatchCount} identity block(s) and ${configPatchCount} config location(s) for @${handle}`, 3267 + ); 3314 3268 3315 - // Stream the personalized image (ISO patch path or EFI-first image path) 3316 3269 addServerLog('success', '💿', `OS image for @${handle} (${(imgData.length / 1048576).toFixed(1)}MB)`); 3317 - if (preferEfiLayout && servedLayout !== 'efi') { 3318 - addServerLog('warn', '⚠️', `OS image fallback for @${handle}: requested EFI but served ISO`); 3319 - } 3320 - res.setHeader('Content-Type', contentType); 3270 + res.setHeader('Content-Type', 'application/octet-stream'); 3321 3271 res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); 3322 3272 res.setHeader('Pragma', 'no-cache'); 3323 3273 res.setHeader('Expires', '0'); 3324 - res.setHeader('X-AC-OS-Requested-Layout', preferEfiLayout ? 'efi' : 'iso'); 3325 - res.setHeader('X-AC-OS-Layout', servedLayout); 3326 - if (efiError) { 3327 - res.setHeader('X-AC-OS-Fallback', '1'); 3328 - res.setHeader('X-AC-OS-Fallback-Reason', String(efiError.message || 'unknown').slice(0, 180)); 3329 - } 3330 - // Get latest build name for filename 3274 + res.setHeader('X-AC-OS-Requested-Layout', requestedLayout || 'img'); 3275 + res.setHeader('X-AC-OS-Layout', 'img'); 3276 + 3331 3277 let releaseName = 'native'; 3332 3278 try { 3333 3279 const relRes = await fetch(`${RELEASES_BASE}/releases.json`); ··· 3340 3286 const d = new Date(); 3341 3287 const p = (n) => String(n).padStart(2, '0'); 3342 3288 const ts = `${d.getFullYear()}.${p(d.getMonth()+1)}.${p(d.getDate())}.${p(d.getHours())}.${p(d.getMinutes())}.${p(d.getSeconds())}`; 3343 - res.setHeader('Content-Disposition', `attachment; filename="@${handle}-os-${bootPiece}-${coreName}-${ts}.${extension}"`); 3289 + res.setHeader('Content-Disposition', `attachment; filename="@${handle}-os-${bootPiece}-${coreName}-${ts}.img"`); 3344 3290 res.setHeader('Content-Length', imgData.length); 3345 3291 res.end(imgData); 3346 3292 });
+10 -3
system/netlify/edge-functions/os-image.js
··· 1 1 // os-image — Proxy to oven for personalized FedAC OS image downloads. 2 - // Oven handles the heavy lifting (42MB template download, config patching, streaming). 2 + // Oven handles the heavy lifting (template download, config patching, streaming). 3 3 // This edge function just forwards the auth header and streams the response. 4 4 5 - const OVEN_URL = "https://oven.aesthetic.computer/os-image"; 5 + const OVEN_BASE = "https://oven.aesthetic.computer/os-image"; 6 6 7 7 export default async (req) => { 8 8 if (req.method === "OPTIONS") { ··· 29 29 30 30 // Proxy to oven 31 31 try { 32 - const ovenRes = await fetch(OVEN_URL, { 32 + const ovenUrl = OVEN_BASE + new URL(req.url).search; 33 + const ovenRes = await fetch(ovenUrl, { 33 34 headers: { Authorization: authHeader }, 34 35 }); 35 36 ··· 40 41 "Content-Type": ovenRes.headers.get("content-type") || "application/octet-stream", 41 42 "Content-Disposition": ovenRes.headers.get("content-disposition") || "", 42 43 "Content-Length": ovenRes.headers.get("content-length") || "", 44 + "X-AC-OS-Requested-Layout": 45 + ovenRes.headers.get("x-ac-os-requested-layout") || "", 46 + "X-AC-OS-Layout": ovenRes.headers.get("x-ac-os-layout") || "", 47 + "X-AC-OS-Fallback": ovenRes.headers.get("x-ac-os-fallback") || "", 48 + "X-AC-OS-Fallback-Reason": 49 + ovenRes.headers.get("x-ac-os-fallback-reason") || "", 43 50 "Access-Control-Allow-Origin": "*", 44 51 }, 45 52 });
+5 -5
system/netlify/edge-functions/os-release-upload.js
··· 301 301 } 302 302 } 303 303 304 - // Template .iso presigned URL (separate from vmlinuz) 304 + // Template .img presigned URL (separate from vmlinuz) 305 305 const isTemplate = request.headers.get("x-template-upload") === "true"; 306 306 if (isTemplate) { 307 307 try { 308 - const isoUrl = await presignUrl( 309 - "os/native-notepat-latest.iso", 310 - "application/x-iso9660-image", 308 + const imageUrl = await presignUrl( 309 + "os/native-notepat-latest.img", 310 + "application/octet-stream", 311 311 ); 312 312 return Response.json({ 313 313 step: "template-upload", 314 - iso_put_url: isoUrl, 314 + image_put_url: imageUrl, 315 315 user: userSub, 316 316 }); 317 317 } catch (err) {
+23 -34
system/public/aesthetic.computer/disks/os.mjs
··· 4 4 const OVEN = "https://oven-edge.aesthetic-computer.workers.dev"; 5 5 const OVEN_WS = "wss://oven.aesthetic.computer/ws"; 6 6 const OVEN_ORIGIN = "https://oven.aesthetic.computer"; 7 - const ISO_BASE = OVEN + "/os/latest.iso"; 7 + const IMAGE_BASE = OVEN + "/os/latest.img"; 8 8 function OVEN_BASE() { return OVEN; } 9 9 function RELEASES_URL() { return OVEN + "/os-releases"; } 10 10 function OVEN_WS_URL() { return OVEN_WS; } 11 - function templateIsoUrl() { 12 - const base = ISO_BASE; 13 - if (variantIdx === 0) return base; 14 - // CL variant: replace 'native-notepat' with 'cl-native-notepat' 15 - return base.replace("native-notepat", "cl-native-notepat"); 16 - } 11 + function templateImageUrl() { 12 + const base = IMAGE_BASE; 13 + if (variantIdx === 0) return base; 14 + // CL variant: replace 'native-notepat' with 'cl-native-notepat' 15 + return base.replace("native-notepat", "cl-native-notepat"); 16 + } 17 17 const CONFIG_MARKER = '{"handle":"","piece":"notepat","sub":"","email":""}'; 18 18 const CONFIG_PAD = 4096; 19 19 const BOOT_PIECES = ["notepat", "prompt", "chat", "laer-klokken"]; ··· 288 288 buildPollTimer = setInterval(() => fetchBuildStatus(needsPaint), 30000); 289 289 290 290 // Probe CL variant availability (HEAD request) 291 - fetch(ISO_BASE.replace("native-notepat", "cl-native-notepat"), { method: "HEAD" }) 291 + fetch(IMAGE_BASE.replace("native-notepat", "cl-native-notepat"), { method: "HEAD" }) 292 292 .then(r => { clAvailable = r.ok; console.log("[os] CL variant:", clAvailable ? "available" : "not yet"); needsPaint(); }) 293 293 .catch(() => { clAvailable = false; }); 294 294 ··· 674 674 sectionHeader("Install", dark ? [14, 20, 32] : [210, 215, 230], C.secInstBg, 120); 675 675 676 676 const instLines = isMobile ? [ 677 - [C.instText, "1 flash .iso (Fedora Media Writer)"], 677 + [C.instText, "1 flash .img (Fedora Media Writer)"], 678 678 [C.instText, "2 plug USB into x86 PC"], 679 679 [C.instText, "3 BIOS boot menu:"], 680 680 [C.instKey, " F12 Dell/Lenovo F9 HP"], 681 681 [C.instKey, " F2 ASUS/Acer ESC others"], 682 682 [C.instText, "4 select USB drive"], 683 683 ] : [ 684 - [C.instText, "1 flash .iso with Fedora Media Writer"], 684 + [C.instText, "1 flash .img with Fedora Media Writer"], 685 685 [C.instText, "2 plug USB into any x86 PC"], 686 686 [C.instText, "3 enter BIOS boot menu:"], 687 687 [C.instKey, " F12 Dell/Lenovo F9 HP"], ··· 991 991 "&wifi=" + (wifiEnabled ? "1" : "0") + 992 992 "&cb=" + Date.now() + 993 993 (variantIdx === 1 ? "&variant=cl" : ""); 994 - const efiQuery = query + "&layout=efi&strict=1"; 994 + const imageQuery = query + "&layout=img&strict=1"; 995 995 const downloadCandidates = [ 996 996 { 997 - url: OVEN_BASE() + "/os-image" + query, 998 - allowedLayouts: ["iso"], 997 + url: OVEN_BASE() + "/os-image" + imageQuery, 998 + allowedLayouts: ["img"], 999 999 rejectOriginFallback: true, 1000 1000 }, 1001 1001 { 1002 - url: OVEN_ORIGIN + "/os-image" + efiQuery, 1003 - allowedLayouts: ["efi"], 1002 + url: OVEN_ORIGIN + "/os-image" + imageQuery, 1003 + allowedLayouts: ["img"], 1004 1004 rejectOriginFallback: false, 1005 1005 }, 1006 1006 ]; 1007 - const MIN_EXPECTED_EFI_BYTES = 300 * 1024 * 1024; 1008 - const MIN_EXPECTED_ISO_BYTES = 100 * 1024 * 1024; 1007 + const MIN_EXPECTED_IMAGE_BYTES = 300 * 1024 * 1024; 1009 1008 1010 1009 let res = null; 1011 1010 let usedUrl = ""; ··· 1051 1050 } 1052 1051 1053 1052 const len = parseInt(attempt.headers.get("content-length") || "0"); 1054 - const minExpectedBytes = 1055 - servedLayout === "efi" 1056 - ? MIN_EXPECTED_EFI_BYTES 1057 - : servedLayout === "iso" 1058 - ? MIN_EXPECTED_ISO_BYTES 1059 - : MIN_EXPECTED_EFI_BYTES; 1053 + const minExpectedBytes = MIN_EXPECTED_IMAGE_BYTES; 1060 1054 if (len > 0 && len < minExpectedBytes) { 1061 1055 console.warn("[os] Rejecting suspiciously small image response:", len, "bytes from", candidate.url, "layout:", servedLayout || "?"); 1062 1056 try { attempt.body?.cancel(); } catch (_) {} ··· 1097 1091 needsPaint(); 1098 1092 1099 1093 const total = chunks.reduce((s, c) => s + c.length, 0); 1100 - const servedLayout = (res.headers.get("x-ac-os-layout") || "").toLowerCase(); 1101 - const minExpectedBytes = 1102 - servedLayout === "efi" 1103 - ? MIN_EXPECTED_EFI_BYTES 1104 - : servedLayout === "iso" 1105 - ? MIN_EXPECTED_ISO_BYTES 1106 - : MIN_EXPECTED_EFI_BYTES; 1094 + const servedLayout = (res.headers.get("x-ac-os-layout") || "").toLowerCase(); 1095 + const minExpectedBytes = MIN_EXPECTED_IMAGE_BYTES; 1107 1096 if (total < minExpectedBytes) { 1108 1097 throw new Error("Image too small (" + (total / 1048576).toFixed(1) + "MB), refusing to save"); 1109 1098 } ··· 1129 1118 const d = new Date(); 1130 1119 const p = (n) => String(n).padStart(2, "0"); 1131 1120 const ts = `${d.getFullYear()}.${p(d.getMonth()+1)}.${p(d.getDate())}.${p(d.getHours())}.${p(d.getMinutes())}.${p(d.getSeconds())}`; 1132 - const extension = servedLayout === "iso" ? "iso" : "img"; 1133 - const mimeType = servedLayout === "iso" ? "application/x-iso9660-image" : "application/octet-stream"; 1121 + const extension = "img"; 1122 + const mimeType = "application/octet-stream"; 1134 1123 const filename = `@${handle || "user"}-os-${piece}-${coreName}-${ts}.${extension}`; 1135 1124 1136 1125 console.log("[os] Download complete:", filename, (total / 1048576).toFixed(1) + "MB"); ··· 1157 1146 needsPaint(); 1158 1147 1159 1148 try { 1160 - const res = await fetch(templateIsoUrl()); 1149 + const res = await fetch(templateImageUrl()); 1161 1150 if (!res.ok) throw new Error("Download failed: " + res.status); 1162 1151 1163 1152 const contentLength = parseInt(res.headers.get("content-length") || "0"); ··· 1225 1214 const d = new Date(); 1226 1215 const p = (n) => String(n).padStart(2, "0"); 1227 1216 const ts = `${d.getFullYear()}.${p(d.getMonth()+1)}.${p(d.getDate())}.${p(d.getHours())}.${p(d.getMinutes())}.${p(d.getSeconds())}`; 1228 - const filename = `ac-os-${piece}-${coreName}-${ts}.iso`; 1217 + const filename = `ac-os-${piece}-${coreName}-${ts}.img`; 1229 1218 1230 1219 console.log("[os] Template download complete:", filename, (total / 1048576).toFixed(1) + "MB"); 1231 1220 dlFn(filename, combined, { type: "application/octet-stream" });