#!/usr/bin/env bash set -euo pipefail root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${root}" forge_host="${EVERY_CHANNEL_FORGE_HOST:-https://git.every.channel}" forge_repo="${EVERY_CHANNEL_FORGE_REPO:-every-channel/every.channel}" release_tag="${EVERY_CHANNEL_NETBOOT_RELEASE_TAG:-}" local_tarball="${EVERY_CHANNEL_NETBOOT_TARBALL:-}" out_root="${EVERY_CHANNEL_NETBOOT_ROOT:-tmp/netboot}" ipxe_efi_url="${EVERY_CHANNEL_IPXE_EFI_URL:-https://boot.ipxe.org/snponly.efi}" ipxe_efi_path="${EVERY_CHANNEL_IPXE_EFI_PATH:-}" ipxe_efi_filename="${EVERY_CHANNEL_IPXE_EFI_FILENAME:-ipxe.efi}" netboot_hostname="${EVERY_CHANNEL_NETBOOT_HOSTNAME:-boot.every.channel}" http_port="${EVERY_CHANNEL_NETBOOT_HTTP_PORT:-8080}" verify_release_checksums="${EVERY_CHANNEL_NETBOOT_VERIFY_RELEASE_CHECKSUMS:-true}" allow_remote_ipxe="${EVERY_CHANNEL_NETBOOT_ALLOW_REMOTE_IPXE:-false}" ipxe_efi_sha256="${EVERY_CHANNEL_IPXE_EFI_SHA256:-}" chain_token="${EVERY_CHANNEL_NETBOOT_CHAIN_TOKEN:-}" token="${EVERY_CHANNEL_FORGE_TOKEN:-${FORGE_TOKEN:-${CODEBERG_TOKEN:-}}}" need_cmd() { local name="$1" if ! command -v "${name}" >/dev/null 2>&1; then echo "error: required command not found: ${name}" >&2 exit 2 fi } need_cmd curl need_cmd tar need_cmd python3 bool_norm() { local raw raw="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" case "${raw}" in ''|true|1|yes|y|on) echo "true" ;; false|0|no|n|off) echo "false" ;; *) echo "error: invalid boolean value '${1}'" >&2 exit 2 ;; esac } check_sha256() { local file_path="$1" local expected="$2" local actual if command -v sha256sum >/dev/null 2>&1; then actual="$(sha256sum "${file_path}" | awk '{print $1}')" elif command -v shasum >/dev/null 2>&1; then actual="$(shasum -a 256 "${file_path}" | awk '{print $1}')" else echo "error: checksum verification requested but no sha256 tool is available" >&2 exit 2 fi if [[ "${actual}" != "${expected}" ]]; then echo "error: checksum mismatch for ${file_path}" >&2 exit 2 fi echo "$(basename "${file_path}"): OK" } validate_chain_token() { local value="$1" if [[ -z "${value}" ]]; then return 0 fi if [[ ! "${value}" =~ ^[A-Za-z0-9._~-]{16,128}$ ]]; then echo "error: EVERY_CHANNEL_NETBOOT_CHAIN_TOKEN must match [A-Za-z0-9._~-]{16,128}" >&2 exit 2 fi } allow_remote_ipxe="$(bool_norm "${allow_remote_ipxe}")" validate_chain_token "${chain_token}" tmp_dir="$(mktemp -d)" cleanup() { rm -rf "${tmp_dir}" } trap cleanup EXIT archive_path="${tmp_dir}/netboot.tar.gz" release_asset_url="" release_asset_name="" checksum_asset_url="" if [[ -n "${local_tarball}" ]]; then if [[ ! -f "${local_tarball}" ]]; then echo "error: netboot tarball not found: ${local_tarball}" >&2 exit 2 fi cp -f "${local_tarball}" "${archive_path}" else api_base="${forge_host%/}/api/v1/repos/${forge_repo}" release_endpoint="${api_base}/releases/latest" if [[ -n "${release_tag}" ]]; then release_endpoint="${api_base}/releases/tags/${release_tag}" fi auth_args=() if [[ -n "${token}" ]]; then auth_args=(-H "Authorization: token ${token}") fi release_json="${tmp_dir}/release.json" curl -fsSL "${auth_args[@]}" "${release_endpoint}" -o "${release_json}" parsed_assets="$( python3 - "${release_json}" <<'PY' import json import sys path = sys.argv[1] with open(path, "r", encoding="utf-8") as f: data = json.load(f) assets = data.get("assets", []) candidates = [] for asset in assets: name = asset.get("name", "") if name.startswith("ec-runner-x86_64-netboot-") and name.endswith(".tar.gz"): candidates.append(asset) if not candidates: sys.exit(1) # Pick newest by release ordering if API already sorted; otherwise prefer largest id. chosen = sorted(candidates, key=lambda x: x.get("id", 0))[-1] checksum_asset = None for asset in assets: if asset.get("name") == "SHA256SUMS.txt": checksum_asset = asset break print(chosen.get("name", "")) print(chosen.get("browser_download_url", "")) print("" if checksum_asset is None else checksum_asset.get("browser_download_url", "")) PY )" release_asset_name="$(printf '%s\n' "${parsed_assets}" | sed -n '1p')" release_asset_url="$(printf '%s\n' "${parsed_assets}" | sed -n '2p')" checksum_asset_url="$(printf '%s\n' "${parsed_assets}" | sed -n '3p')" if [[ -z "${release_asset_url}" ]]; then echo "error: unable to find x86_64 netboot asset in release" >&2 exit 2 fi curl -fsSL "${auth_args[@]}" -o "${archive_path}" "${release_asset_url}" verify_release_checksums_lc="$(printf '%s' "${verify_release_checksums}" | tr '[:upper:]' '[:lower:]')" if [[ "${verify_release_checksums_lc}" != "false" && -n "${checksum_asset_url}" ]]; then checksum_file="${tmp_dir}/SHA256SUMS.txt" curl -fsSL "${auth_args[@]}" -o "${checksum_file}" "${checksum_asset_url}" if command -v sha256sum >/dev/null 2>&1; then ( cd "${tmp_dir}" grep -F " ${release_asset_name}" "${checksum_file}" | sha256sum -c - ) elif command -v shasum >/dev/null 2>&1; then expected="$(grep -F " ${release_asset_name}" "${checksum_file}" | awk '{print $1}')" actual="$(shasum -a 256 "${archive_path}" | awk '{print $1}')" if [[ -z "${expected}" || "${expected}" != "${actual}" ]]; then echo "error: checksum mismatch for ${release_asset_name}" >&2 exit 2 fi echo "${release_asset_name}: OK" else echo "warning: no sha256 tool available; skipping checksum verification" fi fi fi http_dir="${out_root}/http" tftp_dir="${out_root}/tftp" rm -rf "${http_dir}" mkdir -p "${http_dir}" "${tftp_dir}" tar -xzf "${archive_path}" -C "${http_dir}" for required in kernel initrd netboot.ipxe; do if [[ ! -f "${http_dir}/${required}" ]]; then echo "error: extracted netboot bundle is missing ${required}" >&2 exit 2 fi done if [[ -n "${ipxe_efi_path}" ]]; then if [[ ! -f "${ipxe_efi_path}" ]]; then echo "error: iPXE file path not found: ${ipxe_efi_path}" >&2 exit 2 fi ipxe_dest="${tftp_dir}/${ipxe_efi_filename}" src_resolved="$(realpath -m "${ipxe_efi_path}")" dst_resolved="$(realpath -m "${ipxe_dest}")" # When embedded iPXE already wrote to the destination path, skip self-copy. if [[ "${src_resolved}" != "${dst_resolved}" ]]; then cp -f "${ipxe_efi_path}" "${ipxe_dest}" fi else if [[ "${allow_remote_ipxe}" != "true" ]]; then echo "error: remote iPXE download is disabled by default" >&2 echo "hint: build local iPXE with ./scripts/netboot-build-ipxe.sh and set EVERY_CHANNEL_IPXE_EFI_PATH" >&2 echo "hint: if you must download from URL, set EVERY_CHANNEL_NETBOOT_ALLOW_REMOTE_IPXE=true" >&2 exit 2 fi curl -fsSL -o "${tftp_dir}/${ipxe_efi_filename}" "${ipxe_efi_url}" fi if [[ -n "${ipxe_efi_sha256}" ]]; then check_sha256 "${tftp_dir}/${ipxe_efi_filename}" "${ipxe_efi_sha256}" fi cp -f "${http_dir}/netboot.ipxe" "${tftp_dir}/netboot.ipxe" chain_url="http://${netboot_hostname}:${http_port}/netboot.ipxe" if [[ -n "${chain_token}" ]]; then chain_url="${chain_url}?token=${chain_token}" fi cat > "${tftp_dir}/bootstrap.ipxe" <