diff --git a/rootfs/scripts/build-ubuntu-rootfs.sh b/rootfs/scripts/build-rootfs.sh similarity index 59% rename from rootfs/scripts/build-ubuntu-rootfs.sh rename to rootfs/scripts/build-rootfs.sh index 6acda3d..dfc2191 100755 --- a/rootfs/scripts/build-ubuntu-rootfs.sh +++ b/rootfs/scripts/build-rootfs.sh @@ -4,43 +4,46 @@ # SPDX-License-Identifier: BSD-3-Clause-Clear # # ============================================================================== -# Script: build-ubuntu-rootfs.sh +# Script: build-rootfs.sh # ------------------------------------------------------------------------------ # DESCRIPTION: # This script creates a bootable Linux root filesystem image for ARM64 # platforms (e.g., Qualcomm IoT/Compute/Server reference boards). # # - Supports Qualcomm product configuration file (.conf) for build parameters. +# - Builds a deterministic baseline rootfs using debootstrap from: +# * qcom-product.conf for DISTRO/CODENAME/ARCH (and optional apt settings) +# * a seed file containing one package per line (# comments allowed) # - Supports JSON package manifest for additional package installation # (via apt or local .deb) inside the rootfs. # - Supports injecting custom apt sources from the package manifest. -# - Backward compatible with legacy 2-argument mode (kernel.deb, firmware.deb). -# - Parses qcom-product.conf (if provided) or uses defaults to determine the base image. -# - Runs target platform-specific image preprocessing to populate rootfs/. # - Injects custom kernel and firmware .deb packages. -# - Installs user-specified packages from manifest (if provided). -# - Dynamically deduces and generates base and custom package manifests +# - Installs user-specified packages from seed and/or overlay manifest. +# - Dynamically deduces and generates base and custom package manifests. # - Configures GRUB bootloader, hostname, DNS, and other system settings. -# - Deploy package manifest output files -# - Produces a flashable ext4 image (ubuntu.img). +# - Produces a flashable ext4 image (rootfs.img). # -# USAGE: -# FULL: ./build-ubuntu-rootfs.sh -# CONFIG: ./build-ubuntu-rootfs.sh -# LEGACY: ./build-ubuntu-rootfs.sh +# USAGE (named inputs): +# ./build-rootfs.sh \ +# --product-conf qcom-product.conf \ +# --seed seed_file \ +# --kernel-package kernel.deb \ +# --firmware firmware.deb \ +# [--overlay package-manifest.json] # # ARGUMENTS: -# Optional. Product configuration file for build parameters. -# Optional. JSON manifest specifying extra packages to install. -# Required. Custom kernel package. -# Required. Custom firmware package. +# --product-conf Required. Product configuration file. +# --seed Required. Seed file: one package per line (# comments allowed). +# --kernel-package Required. Custom kernel package. +# --firmware Required. Custom firmware package. +# --overlay Optional. JSON manifest specifying extra packages/apt sources. # # OUTPUT: -# ubuntu.img Flashable ext4 rootfs image. +# rootfs.img Flashable ext4 rootfs image. # # REQUIREMENTS: # - Run as root (auto-elevates with sudo if needed). -# - Host tools: wget, 7z, jq, losetup, mount, cp, chroot, mkfs.ext4, truncate, etc. +# - Host tools: debootstrap, wget, jq, losetup, mount, cp, chroot, mkfs.ext4, truncate, etc. # # AUTHOR: Bjordis Collaku # ============================================================================== @@ -56,65 +59,80 @@ if [[ "$EUID" -ne 0 ]]; then fi # ============================================================================== -# Globals & Argument Parsing (backward compatible) +# Globals & Argument Parsing (named inputs) # ============================================================================== CONF="" -MANIFEST="" +SEED="" +MANIFEST="" # internal name retained (overlay JSON) KERNEL_DEB="" FIRMWARE_DEB="" USE_CONF=0 USE_MANIFEST=0 TARGET="" -if [[ $# -eq 4 ]]; then - CONF="$1" - MANIFEST="$2" - KERNEL_DEB="$3" - FIRMWARE_DEB="$4" - USE_CONF=1 - USE_MANIFEST=1 -elif [[ $# -eq 3 ]]; then - if [[ "$1" == *.conf ]]; then - CONF="$1" - MANIFEST="" - KERNEL_DEB="$2" - FIRMWARE_DEB="$3" - USE_CONF=1 - USE_MANIFEST=0 - else - CONF="" - MANIFEST="" - KERNEL_DEB="$1" - FIRMWARE_DEB="$2" - USE_CONF=0 - USE_MANIFEST=0 - TARGET="$3" - fi -elif [[ $# -eq 2 ]]; then - CONF="" - MANIFEST="" - KERNEL_DEB="$1" - FIRMWARE_DEB="$2" - USE_CONF=0 - USE_MANIFEST=0 -else +print_usage() { echo "Usage:" - echo " $0 " - echo " $0 " - echo " $0 " + echo " $0 --product-conf --seed --kernel-package --firmware [--overlay ]" + echo + echo "Arguments:" + echo " --product-conf Required. qcom-product.conf" + echo " --seed Required. Seed file (one package per line; supports # comments)" + echo " --kernel-package Required. Kernel .deb" + echo " --firmware Required. Firmware .deb" + echo " --overlay Optional. package-manifest.json (same schema as current manifest)" +} + +# Parse named options +# NOTE: We intentionally keep parsing simple (no getopt dependency) for portability. +while [[ $# -gt 0 ]]; do + case "$1" in + --product-conf) + CONF="${2-}"; shift 2 ;; + --seed) + SEED="${2-}"; shift 2 ;; + --kernel-package) + KERNEL_DEB="${2-}"; shift 2 ;; + --firmware) + FIRMWARE_DEB="${2-}"; shift 2 ;; + --overlay) + MANIFEST="${2-}"; shift 2 ;; + -h|--help) + print_usage + exit 0 + ;; + *) + echo "[ERROR] Unknown option: $1" + print_usage + exit 1 + ;; + esac +done + +# Validate required args +if [[ -z "${CONF}" || -z "${SEED}" || -z "${KERNEL_DEB}" || -z "${FIRMWARE_DEB}" ]]; then + echo "[ERROR] Missing required argument(s)." + print_usage exit 1 fi +USE_CONF=1 +USE_MANIFEST=0 +if [[ -n "${MANIFEST}" ]]; then + USE_MANIFEST=1 +fi + +[[ -f "$CONF" ]] || { echo "[ERROR] Config file not found: $CONF"; exit 1; } +[[ -f "$SEED" ]] || { echo "[ERROR] Seed file not found: $SEED"; exit 1; } [[ -f "$KERNEL_DEB" ]] || { echo "[ERROR] Kernel package not found: $KERNEL_DEB"; exit 1; } [[ -f "$FIRMWARE_DEB" ]] || { echo "[ERROR] Firmware package not found: $FIRMWARE_DEB"; exit 1; } if [[ "$USE_MANIFEST" -eq 1 && -n "$MANIFEST" ]]; then - [[ -f "$MANIFEST" ]] || { echo "[ERROR] Manifest file not found: $MANIFEST"; exit 1; } + [[ -f "$MANIFEST" ]] || { echo "[ERROR] Manifest/overlay file not found: $MANIFEST"; exit 1; } fi WORKDIR=$(pwd) MNT_DIR="$WORKDIR/mnt" ROOTFS_DIR="$WORKDIR/rootfs" -ROOTFS_IMG="ubuntu.img" +ROOTFS_IMG="rootfs.img" mkdir -p "$MNT_DIR" "$ROOTFS_DIR" declare -A CFG @@ -144,144 +162,129 @@ parse_configuration() { } # ============================================================================== -# Function: image_preproccessing_iot -# Target: iot -# Downloads/extracts the base image, mounts, and fills $ROOTFS_DIR/. -# Distro-specific handling is done via an inner case. +# Function: _seed_to_debootstrap_include +# Parses seed file into a comma-separated package list. +# - Supports blank lines and # comments. +# - Ensures required baseline packages exist for later stages (without changing later stages). # ============================================================================== -image_preproccessing_iot() { - case "$(echo "$DISTRO" | tr '[:upper:]' '[:lower:]')" in - ubuntu|ubuntu-server) - echo "[INFO][iot][ubuntu] Preparing environment..." +_seed_to_debootstrap_include() { + local seed_file="$1" + local include_pkgs=() + declare -A seen=() - # --- Silent ensure of 7z (p7zip-full) --- - if ! command -v 7z >/dev/null 2>&1; then - echo "[INFO][iot][ubuntu] '7z' not found. Installing p7zip-full silently..." - export DEBIAN_FRONTEND=noninteractive - # Use -qq for quiet and redirect all output to /dev/null - apt-get -qq update >/dev/null 2>&1 || true - apt-get -qq install -y p7zip-full >/dev/null 2>&1 || { - echo "[ERROR] Failed to install 'p7zip-full' required for 7z." - exit 1 - } - fi + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line%%#*}" + line="$(echo "$line" | xargs)" + [[ -z "$line" ]] && continue + + # Seed must be one package token per line + if [[ "$line" =~ [[:space:]] ]]; then + echo "[ERROR] Invalid seed entry (whitespace found). Use one package per line:" + echo " '$line'" + exit 1 + fi + + if [[ -z "${seen[$line]+x}" ]]; then + include_pkgs+=("$line") + seen["$line"]=1 + fi + done < "$seed_file" + + # These are REQUIRED for your existing later stages to run unchanged + # (Step 8 uses lsb_release; apt/dpkg/user tools/systemctl expected present). + local required_pkgs=( + lsb-release + ca-certificates + sudo + adduser + passwd + systemd-sysv + apt + ) + + for p in "${required_pkgs[@]}"; do + if [[ -z "${seen[$p]+x}" ]]; then + include_pkgs+=("$p") + seen["$p"]=1 + fi + done + + # Output comma-separated list + local out="" + for p in "${include_pkgs[@]}"; do + if [[ -z "$out" ]]; then + out="$p" + else + out="${out},${p}" + fi + done + echo "$out" +} - # --- Silent ensure of unsquashfs (squashfs-tools) --- - if ! command -v unsquashfs >/dev/null 2>&1; then - echo "[INFO][iot][ubuntu] 'unsquashfs' not found. Installing squashfs-tools silently..." +# ============================================================================== +# Function: image_preprocessing +# Generic preprocessing: creates baseline rootfs via debootstrap + seed +# (distro + platform agnostic; controlled via qcom-product.conf inputs). +# +# Notes: +# - Mirror/components can be controlled via qcom-product.conf: +# APT_MIRROR=... +# APT_COMPONENTS=main,universe +# - For production, prefer setting APT_MIRROR in product-conf per distro. +# ============================================================================== +image_preprocessing() { + echo "[INFO][preprocess] Preparing environment for debootstrap baseline rootfs..." + + # --- Ensure debootstrap is installed (silently) --- + if ! command -v debootstrap >/dev/null 2>&1; then + echo "[INFO][preprocess] 'debootstrap' not found. Installing debootstrap silently..." export DEBIAN_FRONTEND=noninteractive apt-get -qq update >/dev/null 2>&1 || true - apt-get -qq install -y squashfs-tools >/dev/null 2>&1 || { - echo "[ERROR] Failed to install 'squashfs-tools' required for unsquashfs." - exit 1 + apt-get -qq install -y debootstrap >/dev/null 2>&1 || { + echo "[ERROR] Failed to install 'debootstrap'." + exit 1 } - fi - - echo "[INFO][iot][ubuntu] Downloading ISO..." + fi - # SAFE under set -u: - ISO_NAME="${ISO_NAME-}" - if [[ -z "${ISO_NAME}" ]]; then - if [[ -z "${IMG_URL-}" ]]; then - echo "[ERROR] IMG_URL is empty/unset; cannot derive ISO_NAME." - exit 1 - fi - ISO_NAME="$(basename "${IMG_URL%%\?*}")" - [[ -z "$ISO_NAME" || "$ISO_NAME" == "/" ]] && ISO_NAME="image.iso" - fi - - # Working dirs - ISO_EXTRACT_DIR="${ISO_EXTRACT_DIR:-isoImage}" - SQUASHFS_WORK_DIR="${SQUASHFS_WORK_DIR:-squashfs-root}" - MNT_DIR="${MNT_DIR:-/mnt/iot-iso-tmp}" # kept for compatibility with other logic - - # Fetch ISO - if ! wget -q -c "$IMG_URL" -O "$ISO_NAME"; then - echo "[ERROR] Failed to download ISO from: $IMG_URL" + # NOTE: ARM64-only assumption: host is arm64 and target ARCH is arm64. + if [[ "${ARCH}" != "arm64" ]]; then + echo "[ERROR][preprocess] This commit assumes ARCH=arm64 only. Current ARCH=$ARCH" exit 1 - fi + fi - echo "[INFO][iot][ubuntu] Extracting ISO with 7z..." - rm -rf "$ISO_EXTRACT_DIR" "$SQUASHFS_WORK_DIR" - mkdir -p "$ISO_EXTRACT_DIR" "$ROOTFS_DIR" + echo "[INFO][preprocess] Creating baseline rootfs via debootstrap using seed: $SEED" - if ! 7z x "$ISO_NAME" -o"$ISO_EXTRACT_DIR" -y >/dev/null; then - echo "[ERROR] Failed to extract ISO: $ISO_NAME" - exit 1 - fi - - # --- Robust squashfs selection --- - local SQUASHFS_PATH="" - for candidate in \ - "$ISO_EXTRACT_DIR/casper/ubuntu-server-minimal.squashfs" \ - "$ISO_EXTRACT_DIR/casper/minimal.squashfs" \ - "$ISO_EXTRACT_DIR/casper/filesystem.squashfs" \ - "$ISO_EXTRACT_DIR/ubuntu-server-minimal.squashfs" \ - "$ISO_EXTRACT_DIR/minimal.squashfs" \ - "$ISO_EXTRACT_DIR/filesystem.squashfs" - do - if [[ -f "$candidate" ]]; then - SQUASHFS_PATH="$candidate" - break - fi - done - if [[ -z "$SQUASHFS_PATH" ]]; then - mapfile -t found_squashfs < <(find "$ISO_EXTRACT_DIR" -maxdepth 3 -type f -name '*.squashfs' 2>/dev/null | sort) - if (( ${#found_squashfs[@]} == 1 )); then - SQUASHFS_PATH="${found_squashfs[0]}" - elif (( ${#found_squashfs[@]} > 1 )); then - for f in "${found_squashfs[@]}"; do - if [[ "$f" =~ minimal\.squashfs$ ]]; then - SQUASHFS_PATH="$f" - break - fi - done - [[ -z "$SQUASHFS_PATH" ]] && SQUASHFS_PATH="${found_squashfs[0]}" - echo "[WARN][iot][ubuntu] Multiple squashfs files found:" - printf ' - %s\n' "${found_squashfs[@]}" - echo " Selected: $SQUASHFS_PATH" - fi - fi - if [[ -z "$SQUASHFS_PATH" ]]; then - echo "[ERROR] No squashfs image found in ISO after scanning." - echo " Looked under: $ISO_EXTRACT_DIR (depth 3)" - exit 1 - fi - echo "[INFO][iot][ubuntu] Using squashfs: $SQUASHFS_PATH" + # Clean rootfs dir and recreate + rm -rf "$ROOTFS_DIR" + mkdir -p "$ROOTFS_DIR" - echo "[INFO][iot][ubuntu] Unsquashing rootfs from: $SQUASHFS_PATH" - if ! unsquashfs -d "$SQUASHFS_WORK_DIR" "$SQUASHFS_PATH" >/dev/null; then - echo "[ERROR] Failed to unsquash: $SQUASHFS_PATH" - exit 1 - fi + # Generic defaults; recommend overriding via qcom-product.conf per distro. + local MIRROR="${CFG[APT_MIRROR]:-http://ports.ubuntu.com/ubuntu-ports}" + local COMPONENTS="${CFG[APT_COMPONENTS]:-main,universe}" - echo "[INFO][iot][ubuntu] Copying rootfs into: $ROOTFS_DIR" - mkdir -p "$ROOTFS_DIR" - if ! cp -rap "${SQUASHFS_WORK_DIR}/"* "$ROOTFS_DIR/"; then - echo "[ERROR] Failed to copy rootfs into: $ROOTFS_DIR" + local INCLUDE_LIST + INCLUDE_LIST="$(_seed_to_debootstrap_include "$SEED")" + + echo "[INFO][preprocess] debootstrap parameters:" + echo " TARGET_PLATFORM=$TARGET_PLATFORM" + echo " DISTRO=$DISTRO" + echo " CODENAME=$CODENAME" + echo " ARCH=$ARCH" + echo " MIRROR=$MIRROR" + echo " COMPONENTS=$COMPONENTS" + echo " INCLUDE(from seed + required)=$INCLUDE_LIST" + + if ! debootstrap --arch="$ARCH" --variant=minbase --components="$COMPONENTS" --include="$INCLUDE_LIST" "$CODENAME" "$ROOTFS_DIR" "$MIRROR"; then + echo "[ERROR][preprocess] debootstrap failed." exit 1 - fi - - echo "[INFO][iot][ubuntu] Rootfs prepared at: $ROOTFS_DIR" - # Optional cleanup: - # rm -rf "$ISO_EXTRACT_DIR" "$SQUASHFS_WORK_DIR" - ;; - - debian) - echo "[ERROR][iot][debian] Not implemented yet." - exit 1 - ;; - - *) - echo "[ERROR][iot] Unsupported distro: $DISTRO" - exit 1 - ;; - esac -} + fi + + # Ensure directories exist for later steps (so Step 3.5+ stays unchanged) + mkdir -p "$ROOTFS_DIR/proc" "$ROOTFS_DIR/sys" "$ROOTFS_DIR/dev" "$ROOTFS_DIR/dev/pts" + mkdir -p "$ROOTFS_DIR/etc/apt/sources.list.d" -# Empty stubs for future targets -image_preproccessing_compute() { :; } -image_preproccessing_server() { :; } + echo "[INFO][preprocess] Rootfs prepared at: $ROOTFS_DIR" +} # ============================================================================== # Step 1: Load configuration (from file or defaults) & derive image parameters @@ -297,7 +300,6 @@ else CFG["CODENAME"]="questing" CFG["ARCH"]="arm64" CFG["VARIANT"]="server" - CFG["BASE_IMAGE_URL"]="https://cdimage.ubuntu.com/releases/questing/release/ubuntu-25.10-live-server-arm64.iso" fi TARGET_PLATFORM="${CFG[QCOM_TARGET_PLATFORM]:-iot}" @@ -305,18 +307,6 @@ DISTRO="${CFG[DISTRO]:-ubuntu}" CODENAME="${CFG[CODENAME]:-questing}" ARCH="${CFG[ARCH]:-arm64}" VARIANT="${CFG[VARIANT]:-server}" -BASE_IMAGE_URL="${CFG[BASE_IMAGE_URL]:-"https://cdimage.ubuntu.com/releases/questing/release/ubuntu-25.10-live-server-arm64.iso"}" - -# Derive image parameters for Ubuntu (others can be added later) -case "$(echo "$DISTRO" | tr '[:upper:]' '[:lower:]')" in - ubuntu|ubuntu-server) - IMG_URL=${BASE_IMAGE_URL} - ;; - *) - # Leave unsupported distros to be implemented inside the respective target function later. - IMG_URL="" - ;; -esac echo "[INFO] Build Source:" echo " TARGET_PLATFORM=$TARGET_PLATFORM" @@ -324,26 +314,11 @@ echo " DISTRO=$DISTRO" echo " CODENAME=$CODENAME" echo " ARCH=$ARCH" echo " VARIANT=$VARIANT" -echo " BASE_IMAGE_URL=${BASE_IMAGE_URL}" - -# ============================================================================== -# Step 2–3: Target platform switch – preprocess image to fill rootfs/ -# ============================================================================== -case "$(echo "$TARGET_PLATFORM" | tr '[:upper:]' '[:lower:]')" in - iot) - image_preproccessing_iot - ;; - compute) - image_preproccessing_compute - ;; - server) - image_preproccessing_server - ;; - *) - echo "[ERROR] Unsupported target platform: $TARGET_PLATFORM" - exit 1 - ;; -esac + +# ============================================================================== +# Step 2–3: Preprocess baseline rootfs to fill rootfs/ +# ============================================================================== +image_preprocessing # ============================================================================== # Step 3.5: Add custom apt sources from manifest (if provided) @@ -380,11 +355,11 @@ cp -L /etc/resolv.conf "$ROOTFS_DIR/etc/resolv.conf" # Step 5: Set Hostname and /etc/hosts # ============================================================================== echo "[INFO] Configuring hostname and /etc/hosts..." -echo "ubuntu" > "$ROOTFS_DIR/etc/hostname" +echo "qcom" > "$ROOTFS_DIR/etc/hostname" cat < "$ROOTFS_DIR/etc/hosts" 127.0.0.1 localhost -127.0.1.1 ubuntu +127.0.1.1 qcom EOF chmod 644 "$ROOTFS_DIR/etc/hosts" @@ -452,21 +427,23 @@ mount --bind /dev/pts "$ROOTFS_DIR/dev/pts" # Step 8: Enter chroot to Install Packages and Configure GRUB # ============================================================================== echo "[INFO] Entering chroot to install packages and configure GRUB..." -chroot "$ROOTFS_DIR" /bin/bash -c " +env DISTRO="$DISTRO" CODENAME="$CODENAME" \ + chroot "$ROOTFS_DIR" /bin/bash -c " set -e -echo '[CHROOT] Updating APT and installing base packages...' +echo '[CHROOT] Updating APT and installing networking tools...' export DEBIAN_FRONTEND=noninteractive -apt update -apt install -y ubuntu-desktop-minimal network-manager iw net-tools +apt-get update +apt-get install -y --no-install-recommends \ + network-manager \ + wpasupplicant \ + iw \ + net-tools echo '[CHROOT] Disabling unnecessary services...' ln -sf /dev/null /etc/systemd/system/systemd-networkd-wait-online.service ln -sf /dev/null /etc/systemd/system/dev-disk-by\\\\x2dlabel-UEFI.device -# Get codename -CODENAME=\$(lsb_release -sc) - echo '[CHROOT] Capturing base package list...' dpkg-query -W -f='\${Package} \${Version}\n' > /tmp/\${CODENAME}_base.manifest @@ -506,7 +483,7 @@ echo '[CHROOT] Writing GRUB configuration for single DTB-agnostic entry...' tee /boot/grub.cfg > /dev/null <