#!/bin/sh
# metier - SliTaz live ISO builder (modern replacement for tazlito).
#
# Pipeline: flavor -> packages + DEPENDS -> rootfs -> rootfs.gz -> rootcd
#          -> isolinux (i486) or GRUB BIOS+UEFI (x86_64) -> bootable ISO.
#
# POSIX sh, KISS first.
#
# Copyright (C) 2026 SliTaz GNU/Linux <pankso@slitaz.org>
# License: GPLv2+

set -eu

METIER_VERSION=1.0
PROG=metier

# Locate sourced lib regardless of how metier is invoked. Resolve symlinks
# one hop (typical /usr/local/bin/metier -> repos/metier/metier install).
_self=$0
case $_self in /*) : ;; *) _self=$(CDPATH='' cd -- "$(dirname -- "$_self")" && pwd)/${_self##*/} ;; esac
[ -L "$_self" ] && _self=$(readlink -f "$_self" 2>/dev/null || echo "$_self")
# Locate lib/. Order: env override, script-relative (dev/symlink), then
# system install path (/usr/lib/metier when packaged).
if [ -n "${METIER_LIB:-}" ]; then
	LIBDIR=$METIER_LIB
elif [ -d "$(dirname -- "$_self")/lib" ]; then
	LIBDIR=$(CDPATH='' cd -- "$(dirname -- "$_self")/lib" && pwd)
elif [ -d /usr/lib/metier ]; then
	LIBDIR=/usr/lib/metier
else
	# shellcheck disable=SC2016
	printf '%s: cannot find lib dir (tried $METIER_LIB, %s/lib, /usr/lib/metier)\n' \
		"$PROG" "$(dirname -- "$_self")" >&2
	exit 70
fi

# shellcheck disable=SC1091
. "$LIBDIR/common.sh"
# shellcheck disable=SC1091
. "$LIBDIR/flavor.sh"
# shellcheck disable=SC1091
. "$LIBDIR/mirror.sh"
# shellcheck disable=SC1091
. "$LIBDIR/pkg.sh"
# shellcheck disable=SC1091
. "$LIBDIR/iso.sh"
# shellcheck disable=SC1091
. "$LIBDIR/grub.sh"
# shellcheck disable=SC1091
. "$LIBDIR/emu.sh"
. "$LIBDIR/flash.sh"

# Only the mutating commands need root. build edits a root-owned rootfs (it
# must preserve real uid/gid for system files — root-owned /etc, setuid
# /usr/bin/sudo — and clear stale root-owned trees on rebuild; fakeroot fakes
# ownership but cannot delete files it does not really own); flash
# write raw block devices. Everything else (help, version, flavors, check,
# emu) and ANY '--help' only reads or prints, so it must never trigger a sudo
# password prompt. Re-exec the mutating path under (passwordless) sudo when a
# regular user runs it, keeping $SLITAZ and $METIER_MIRROR. resolve_base()
# reads $SUDO_USER so metier.conf and ~/.slitaz are the invoking user's. Keep DISPLAY/XAUTHORITY
# so a post-build 'build --emu' can pop a graphical QEMU as the real user.
needs_root() {
	case ${1:-} in build|flash) ;; *) return 1 ;; esac
	[ $# -eq 1 ] && return 1   # bare command → it just prints its usage
	shift
	for _a in "$@"; do
		case $_a in -h|--help|--list) return 1 ;; esac
	done
	return 0
}
if [ "$(id -u)" -ne 0 ] && needs_root "$@"; then
	command -v sudo >/dev/null 2>&1 || \
		die "metier $1 must run as root; install sudo or run it as root"
	exec sudo --preserve-env=DISPLAY,XAUTHORITY,SLITAZ,METIER_MIRROR \
		METIER_LIB="$LIBDIR" "$_self" "$@"
fi

# Defaults — all overridable from CLI.
ARCH=
CHANNEL=cooking
FIRMWARE=auto
BASE=
COMPRESSION_OVERRIDE=
OUTPUT_DIR=
MIRROR_URL=
SERIAL_BOOT=0
EMU_AFTER=0

usage() {
	cat <<EOF
$PROG $METIER_VERSION — SliTaz live ISO builder

Usage: $PROG <command> [args] [options]

Commands:
  build <flavor>          build a bootable ISO  (--arch, or flavor's ARCH=)
  emu <iso|flavor>        boot an ISO (or a flavor's built ISO) in QEMU
  flash <iso> <device>    dd an ISO to a USB key (--list, --plan)
  flavors                 list available flavors
  check [flavor]          preflight: deps, base dirs, perms; with a
                          flavor, its install set: packages added from
                          DEPENDS, excluded, missing
  version                 print version
  help                    this message

Run '$PROG <command> --help' for command-specific options.

Quickstart:
  $PROG build base-x86_64
  $PROG emu base-x86_64 --serial
EOF
}

build_usage() {
	cat <<EOF
$PROG build — build a bootable live ISO from a flavor

Usage:
  $PROG build <flavor> [--arch i486|x86_64] [options]

Installs the flavor's packages and their DEPENDS into a rootfs, packs
rootfs.gz, and assembles a bootable ISO (isolinux for i486, GRUB2 BIOS
and UEFI for x86_64). Flavors live in \$BASE/repos/flavors, see
'$PROG flavors' for the list.

Arch:
  --arch i486|x86_64               target architecture. Optional when the
                                   flavor's receipt declares ARCH= with a
                                   single value; required to disambiguate a
                                   multi-arch flavor (ARCH="x86_64 i486"). An
                                   --arch outside the flavor's ARCH= is rejected.

Options:
  --channel cooking|stable         package channel (default: cooking)
  --firmware bios|uefi|both        firmware target
                                   (default: i486→bios, x86_64→both)
  --compression gzip|lzma|xz|none  override flavor COMPRESSION
                                   (default when the receipt has none: lzma)
  --output DIR                     ISO output dir (default: \$BASE/iso)
  --base DIR                       override \$BASE resolution
  --mirror URL                     override mirror root URL
                                   (or set \$METIER_MIRROR)
  --serial                         mirror boot to the serial console
                                   (for headless QEMU capture)
  --emu                            boot the finished ISO in QEMU
                                   ('$PROG emu'); pair with --serial for a
                                   headless console run

Examples:
  $PROG build base-x86_64                       # arch from receipt ARCH=
  $PROG build base-x86_64 --arch x86_64
  $PROG build base --arch i486 --serial
  $PROG build base-x86_64 --emu --serial
EOF
}

parse_build_opts() {
	while [ $# -gt 0 ]; do
		case $1 in
			--arch)         ARCH=$2; shift 2 ;;
			--arch=*)       ARCH=${1#*=}; shift ;;
			--channel)      CHANNEL=$2; shift 2 ;;
			--channel=*)    CHANNEL=${1#*=}; shift ;;
			--firmware)     FIRMWARE=$2; shift 2 ;;
			--firmware=*)   FIRMWARE=${1#*=}; shift ;;
			--compression)  COMPRESSION_OVERRIDE=$2; shift 2 ;;
			--compression=*) COMPRESSION_OVERRIDE=${1#*=}; shift ;;
			--output)       OUTPUT_DIR=$2; shift 2 ;;
			--output=*)     OUTPUT_DIR=${1#*=}; shift ;;
			--base)         BASE=$2; shift 2 ;;
			--base=*)       BASE=${1#*=}; shift ;;
			--mirror)       MIRROR_URL=$2; shift 2 ;;
			--mirror=*)     MIRROR_URL=${1#*=}; shift ;;
			--serial)       SERIAL_BOOT=1; shift ;;
			--emu)          EMU_AFTER=1; shift ;;
			-h|--help)      build_usage; exit 0 ;;
			--)             shift; break ;;
			-*)             die "unknown option: $1 (try '$PROG build --help')" ;;
			*)              die "unexpected positional arg: $1" ;;
		esac
	done
}

# Settle $ARCH for flavor $1 (load_flavor done): the CLI --arch wins and
# must be one the receipt ARCH= lists; without it, a single-arch receipt
# decides. Also validates $CHANNEL.
resolve_arch() {
	_flavor=$1
	if [ -z "$ARCH" ]; then
		case $(echo $FLAVOR_ARCH | wc -w) in
			0) die "--arch is required (flavor '$_flavor' declares no ARCH=)" ;;
			1) ARCH=$FLAVOR_ARCH ;;
			*) die "flavor '$_flavor' is multi-arch ($FLAVOR_ARCH); pick one with --arch" ;;
		esac
	elif [ -n "$FLAVOR_ARCH" ]; then
		case " $FLAVOR_ARCH " in
			*" $ARCH "*) ;;
			*) die "flavor '$_flavor' does not build arch '$ARCH' (supports: $FLAVOR_ARCH)" ;;
		esac
	fi
	case $ARCH in
		i486|x86_64) ;;
		*) die "unsupported arch: $ARCH" ;;
	esac
	case $CHANNEL in
		cooking|stable) ;;
		*) die "unsupported channel: $CHANNEL (cooking|stable)" ;;
	esac
}

cmd_build() {
	[ $# -ge 1 ] || { build_usage; exit 64; }
	case $1 in -h|--help) build_usage; exit 0 ;; esac
	_flavor=$1
	shift
	parse_build_opts "$@"
	resolve_base

	# Load the flavor first so its receipt can declare ARCH= (one value, or a
	# space-separated list for a multi-arch flavor). The CLI --arch wins; it
	# falls back to the receipt when omitted, and must be one the flavor lists.
	load_flavor "$_flavor"
	[ -n "$COMPRESSION_OVERRIDE" ] && COMPRESSION=$COMPRESSION_OVERRIDE

	resolve_arch "$_flavor"
	# Default firmware per arch: i486 -> BIOS (isolinux), x86_64 -> both
	# (GRUB: UEFI loader + i386-pc BIOS image, one grub.cfg). The x86_64
	# ISO must boot as is in qemu, VirtualBox (BIOS by default), on old
	# 64-bit BIOS PCs and from a dd'd USB key.
	if [ -z "$FIRMWARE" ] || [ "$FIRMWARE" = auto ]; then
		case $ARCH in
			i486)   FIRMWARE=bios ;;
			x86_64) FIRMWARE=both ;;
		esac
	fi
	case $FIRMWARE in
		bios|uefi|both) ;;
		*)     die "unknown firmware: $FIRMWARE" ;;
	esac
	if [ "$ARCH" = i486 ] && [ "$FIRMWARE" != bios ]; then
		die "i486 is BIOS only (no UEFI on legacy hardware — by design)"
	fi

	# Host tools, checked now rather than after the whole rootfs is built:
	# the GRUB path needs xorriso and mtools. curl is checked lazily by
	# mirror_resolve: a build from local packages only does not need it.
	check_deps cpio gzip xz awk sed find tar
	case $FIRMWARE in
		bios) command -v genisoimage >/dev/null 2>&1 || command -v xorriso >/dev/null 2>&1 || \
			die "missing host tool: genisoimage (preferred) or xorriso" ;;
		*)    check_deps xorriso mformat mmd mcopy ;;
	esac

	# Resolve layout (host multi-arch vs chroot mono-arch).
	resolve_paths
	WORK=$DISTRO_ROOT/$FLAVOR
	ROOTFS=$WORK/rootfs
	ROOTCD=$WORK/rootcd
	: "${OUTPUT_DIR:=$ISO_ROOT}"
	# Avoid redundant arch in filename when the flavor already carries
	# it (e.g. FLAVOR=base-x86_64, ARCH=x86_64 -> slitaz-base-x86_64-cooking.iso).
	_iso_flavor=${FLAVOR%-"$ARCH"}
	ISO=$OUTPUT_DIR/slitaz-$_iso_flavor-$ARCH-$CHANNEL.iso

	# Export so sourced lib funcs see them.
	export METIER_VERSION PROG BASE ARCH CHANNEL FLAVOR FLAVOR_DIR \
		WORK ROOTFS ROOTCD ISO COMPRESSION MIRROR_URL SERIAL_BOOT \
		PKG_DIRS DISTRO_ROOT CACHE_ROOT ISO_ROOT

	info "flavor    = $FLAVOR ($SHORT_DESC)"
	info "version   = $VERSION"
	info "arch      = $ARCH"
	info "channel   = $CHANNEL"
	info "firmware  = $FIRMWARE"
	info "compress  = $COMPRESSION"
	info "base      = $BASE"
	info "work      = $WORK"
	info "iso       = $ISO"
	info "packages  = $(printf '%s\n' "$PACKAGES" | awk 'END{print NR}')"

	info "preparing work directory"
	rm -rf "$WORK"
	[ -d "$OUTPUT_DIR" ] || _new_output=1
	mkdir -p "$ROOTFS" "$ROOTCD" "$OUTPUT_DIR"

	info "installing packages from $PKG_DIRS"
	install_packages

	info "overlaying flavor rootfs"
	apply_overlay "$FLAVOR_DIR/rootfs" "$ROOTFS"

	info "relocating /boot (and /EFI) onto rootcd"
	relocate_boot

	info "overlaying flavor rootcd"
	apply_overlay "$FLAVOR_DIR/rootcd" "$ROOTCD"

	make_arch_links
	make_busybox_links
	run_post_install

	# Late overlay: addfiles/ wins over package files AND post_install output.
	# This is where a flavor drops customised boot scripts (/etc/rcS.conf,
	# /etc/init.d/rcS, …) that must be the final word before the rootfs is
	# packed. Distinct from the early rootfs/ overlay above, which is
	# applied before post_install and so can be clobbered by it.
	info "overlaying flavor addfiles (final, pre-pack)"
	apply_overlay "$FLAVOR_DIR/addfiles" "$ROOTFS"

	# distro.sh runs last, on a fully-assembled rootfs (arch/loader links,
	# busybox applets, post_install and addfiles all applied) — the way
	# tazlito's gen-distro hook does. base-x86_64's hook chroots to run the
	# target localedef, which needs the /lib64 loader symlink that
	# make_arch_links lays down above, so it MUST come after it.
	run_distro_hook
	write_flavor_id

	make_kernel_aliases
	make_rootfs_gz

	case $FIRMWARE in
		bios)
			patch_isolinux_cfg ;;
		uefi)
			grub_write_cfg
			grub_build_esp ;;
		both)
			grub_write_cfg
			grub_build_esp
			grub_build_bios ;;
	esac
	build_iso

	# We build as root (sudo re-exec), so the ISO lands root-owned. Hand it
	# back to the invoking user for convenience.
	if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != root ] && [ -f "$ISO" ]; then
		chown "$SUDO_USER:" "$ISO" "${ISO%.iso}.md5" 2>/dev/null || true
		# An --output dir created here too, so the user can remove it.
		if [ -n "${_new_output:-}" ]; then
			chown "$SUDO_USER:" "$OUTPUT_DIR" 2>/dev/null || true
		fi
	fi

	info "done"

	# --emu: boot the freshly built ISO. The build ran as root (sudo re-exec),
	# but QEMU wants the invoking user's X session and KVM group, and the ISO
	# was just chowned back to them — so drop privileges for the emu step.
	# cmd_emu auto-detects the arch from the ISO filename; forward --serial.
	if [ "$EMU_AFTER" = 1 ] && [ -f "$ISO" ]; then
		set -- emu "$ISO"
		[ "$SERIAL_BOOT" = 1 ] && set -- "$@" --serial
		if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != root ]; then
			exec sudo -u "$SUDO_USER" --preserve-env=DISPLAY,XAUTHORITY \
				METIER_LIB="$LIBDIR" "$_self" "$@"
		fi
		exec "$_self" "$@"
	fi
}

cmd_flavors() {
	parse_build_opts "$@"
	resolve_base
	list_flavors
}

# check <flavor>: resolve the install set the way build does, without
# building or root. Lists the DEPENDS it would add or exclude and dies on
# a package found nowhere. A forgotten dep shows here, before the ISO.
check_flavor() {
	_flavor=$1
	shift
	parse_build_opts "$@"
	resolve_base
	load_flavor "$_flavor"
	resolve_arch "$_flavor"
	resolve_paths
	_rd=$(mktemp -d) || die "mktemp failed"
	info "$_flavor ($ARCH): $(printf '%s\n' "$PACKAGES" | awk 'END{print NR+0}') listed package(s)"
	resolve_packages "$_rd"
	if report_resolution "$_rd"; then
		info "$(wc -l < "$_rd/files" | tr -d ' ') package(s) to install"
		rm -rf "$_rd"
	else
		rm -rf "$_rd"
		die "missing packages — cook them or check flavor packages.list"
	fi
}

cmd_check() {
	case ${1:-} in ""|-*) ;; *) check_flavor "$@"; return ;; esac
	parse_build_opts "$@"
	resolve_base
	_st_f=MISSING; [ -d "$BASE/repos/flavors" ] && _st_f=OK
	mkdir -p "$BASE/iso" 2>/dev/null || true
	_st_i=MISSING; [ -d "$BASE/iso" ] && _st_i=OK
	info "base         = $BASE"
	info "flavors dir  = $BASE/repos/flavors  [$_st_f]"
	info "iso dir      = $BASE/iso  [$_st_i]"
	_opt() {
		if command -v "$1" >/dev/null 2>&1; then
			info "$(printf '%-12s OK' "$1")"
		else
			warn "$(printf '%-12s missing  (%s)' "$1" "$2")"
		fi
	}
	[ "$(id -u)" -eq 0 ] || _opt sudo "needed to build as a regular user"
	_opt curl        "packages missing locally cannot come from the mirror"
	_opt genisoimage "i486 ISO (BIOS/isolinux); xorriso is the fallback"
	_opt isohybrid   "i486 ISO: the one of syslinux-extra is used"
	_opt xorriso     "required for the x86_64 ISO (GRUB BIOS+UEFI)"
	_opt mformat     "required for the x86_64 ISO (ESP image, mtools)"
	check_deps cpio gzip xz awk sed find tar
	command -v genisoimage >/dev/null 2>&1 || command -v xorriso >/dev/null 2>&1 || \
		die "no ISO tool: install genisoimage or xorriso"
	info "all required tools present"
}

cmd_version() {
	printf '%s %s\n' "$PROG" "$METIER_VERSION"
}

# Dispatch.
[ $# -ge 1 ] || { usage; exit 64; }
_cmd=$1; shift
case $_cmd in
	build)    cmd_build    "$@" ;;
	emu)      cmd_emu      "$@" ;;
	flash)    cmd_flash    "$@" ;;
	flavors)  cmd_flavors  "$@" ;;
	check)    cmd_check    "$@" ;;
	version)  cmd_version       ;;
	help|-h|--help) usage ;;
	*)        die "unknown command: $_cmd (try '$PROG help')" ;;
esac
