# Copyright 2025-2026 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

EAPI=8

inherit kernel-build

COMMIT="16f1da3c4e94437449d6aa151589ca0ad4b388bb"

DESCRIPTION="Raspberry Pi kernel built from Foundation sources"
HOMEPAGE="https://github.com/raspberrypi/linux"
SRC_URI="
	https://github.com/raspberrypi/linux/archive/${COMMIT}.tar.gz
		-> ${P}.tar.gz
"
S="${WORKDIR}/linux-${COMMIT}"

LICENSE="GPL-2"
SLOT="${PV}"
KEYWORDS="-* ~arm64"
IUSE="initramfs debug +strip"

RDEPEND="
	>=sys-boot/raspberrypi-firmware-1.20260513
	sys-firmware/raspberrypi-wifi-firmware
"

# Name the Pi 5 firmware bootloader loads. Keep in one place: it appears in the
# staged payload, the versioned boot dir and the top-level fallback.
RPI_IMAGE_NAME="kernel_2712.img"

# Populate RPI_STASH (package-owned payload) and RPI_BOOT (the dir the firmware
# reads when os_prefix points at it) for the current phase.
rpi_dirs() {
	RPI_STASH="${EROOT}/usr/lib/${PN}/${KV_FULL}"
	RPI_BOOT="${EROOT}/boot/rpi-${KV_FULL}"
}

# True when /etc/fstab declares /boot as a separate filesystem but it is not
# usable: absent from /proc/mounts, or mounted read-only. mount-boot.eclass
# disables itself whenever EROOT is non-empty (always during a merge), and this
# cluster's fstab marks /boot noauto, so an unusable /boot otherwise sends the
# kernel and DTBs to the root filesystem, where the Raspberry Pi firmware can
# never see them.
boot_partition_not_usable() {
	local fstab=${RPI_FSTAB:-/etc/fstab}
	local mounts=${RPI_MOUNTS:-/proc/mounts}

	# Only meaningful for a live merge into the current root; skip for ROOT
	# images and binpkg-only builds (mount-boot-utils.eclass does the same).
	if [[ -n ${ROOT} && ${ROOT} != / ]]; then
		return 1
	fi
	if [[ ${MERGE_TYPE} == buildonly ]]; then
		return 1
	fi
	# Same escape hatch mount-boot-utils.eclass documents.
	[[ -n ${DONT_MOUNT_BOOT} ]] && return 1
	[[ -r ${fstab} ]] || return 1

	# Skip comments and the /dev/BOOT placeholder from the default fstab;
	# tolerate a trailing slash on the mount point.
	awk '!/^[[:blank:]]*#/ && $1 != "/dev/BOOT" &&
		($2 == "/boot" || $2 == "/boot/") { found = 1 }
		END { exit !found }' "${fstab}" || return 1

	# Mounted read-write? (mount-boot_check_status semantics.)
	awk '($2 == "/boot" || $2 == "/boot/") {
			n = split($4, opts, ",")
			for (i = 1; i <= n; i++) if (opts[i] == "rw") rw = 1
			found = 1
		}
		END { exit !(found && rw) }' "${mounts}" && return 1

	return 0
}

# Fail before building if the merge target cannot receive the payload.
rpi_check_boot_mounted() {
	if boot_partition_not_usable; then
		eerror "/etc/fstab declares a separate /boot, but it is not mounted"
		eerror "read-write. The kernel and DTBs would land on the root filesystem,"
		eerror "where the firmware cannot find them (boot fails with a missing DTB)."
		die "/boot is not mounted read-write; run 'mount -o remount,rw /boot' and retry"
	fi
}

# Materialise RPI_BOOT from the package-owned payload, atomically: build the new
# tree beside the old one, verify it, then swap. A copy failure is fatal because
# the stash under /usr/lib survives for a retry (emerge --config or a re-merge),
# and a truncated kernel is worse than a failed phase.
rpi_materialise_boot() {
	local staging="${RPI_BOOT}.new"
	local old="${RPI_BOOT}.old"

	[[ -s "${RPI_STASH}/${RPI_IMAGE_NAME}" ]] \
		|| die "Missing ${RPI_STASH}/${RPI_IMAGE_NAME}; the package image is incomplete"

	rm -rf "${staging}" "${old}"
	mkdir -p "${staging}/overlays" || die "Cannot create ${staging}"
	cp -f "${RPI_STASH}/${RPI_IMAGE_NAME}" "${staging}/" \
		|| die "Failed to copy the kernel image into ${staging}"
	cp -f "${RPI_STASH}"/*.dtb "${staging}/" \
		|| die "Failed to copy DTBs into ${staging}"
	cp -f "${RPI_STASH}/overlays/"* "${staging}/overlays/" \
		|| die "Failed to copy overlays into ${staging}"

	# Never install an empty or partial payload.
	[[ -s "${staging}/${RPI_IMAGE_NAME}" ]] || die "Empty kernel image in ${staging}"
	compgen -G "${staging}/*.dtb" >/dev/null || die "No DTBs staged in ${staging}"
	compgen -G "${staging}/overlays/*" >/dev/null || die "No overlays staged in ${staging}"

	if [[ -d "${RPI_BOOT}" ]]; then
		mv "${RPI_BOOT}" "${old}" || die "Cannot move ${RPI_BOOT} aside"
	fi
	mv "${staging}" "${RPI_BOOT}" || die "Cannot install ${RPI_BOOT}"
	rm -rf "${old}"
}

# Populate the top-level fallbacks used by nodes without os_prefix. These paths
# are shared by every installed kernel slot, so they are never package-owned.
# Failures are reported but not fatal: the versioned layout is authoritative.
rpi_install_top_level_fallbacks() {
	local failed=0
	local f

	cp -f "${RPI_BOOT}/${RPI_IMAGE_NAME}" "${EROOT}/boot/${RPI_IMAGE_NAME}" \
		|| { eerror "Failed to update /boot/${RPI_IMAGE_NAME}"; failed=1; }

	for f in "${RPI_BOOT}"/*.dtb; do
		[[ -e ${f} ]] || continue
		cp -f "${f}" "${EROOT}/boot/" \
			|| { eerror "Failed to update /boot/$(basename "${f}")"; failed=1; }
	done

	local ovl_new="${EROOT}/boot/.overlays.new"
	rm -rf "${ovl_new}"
	if cp -rf "${RPI_BOOT}/overlays" "${ovl_new}"; then
		rm -rf "${EROOT}/boot/overlays"
		mv "${ovl_new}" "${EROOT}/boot/overlays" \
			|| { eerror "Failed to install /boot/overlays"; failed=1; }
	else
		rm -rf "${ovl_new}"
		eerror "Failed to stage /boot/overlays"
		failed=1
	fi

	if (( failed )); then
		eerror "Top-level boot fallbacks under /boot are incomplete; a node"
		eerror "without os_prefix may fail to boot. Verify /boot is mounted"
		eerror "read-write and re-merge this package."
	fi
}

# True when another installed package owns the path (e.g. the 6.12 SLOT=0
# ebuild or sys-kernel/raspberrypi-image, which own top-level /boot files).
rpi_owned_by_other_pkg() {
	local path=$1
	[[ -e ${path} ]] || return 1
	[[ -n $(portageq owners "${EROOT}" "${path}" 2>/dev/null) ]]
}

src_prepare() {
	default

	cp arch/arm64/configs/bcm2712_defconfig .config || die

	local myversion="-p$(ver_cut 5)-raspberrypi"
	echo "CONFIG_LOCALVERSION=\"${myversion}\"" > "${T}"/version.config || die

	kernel-build_merge_configs "${T}"/version.config
}

src_install() {
	kernel-build_src_install

	local image_src="${WORKDIR}/build/arch/arm64/boot/Image"
	local dtb_src="${WORKDIR}/build/arch/arm64/boot/dts/broadcom"
	local overlay_src="${WORKDIR}/build/arch/arm64/boot/dts/overlays"

	# Never ship a kernel without DTBs. The old code skipped the copy when the
	# build directory was absent, which produced a package the firmware could
	# not boot and only showed up at the next reboot.
	shopt -s nullglob
	local dtbs=( "${dtb_src}"/*.dtb )
	# *.dtb* (not *.dtbo): the overlays build dir also emits plain-DTB maps
	# (overlay_map.dtb, hat_map.dtb) that the firmware needs for overlay
	# resolution.
	local overlays=( "${overlay_src}"/*.dtb* )
	shopt -u nullglob
	[[ -f "${image_src}" ]] || die "Missing kernel image: ${image_src}"
	[[ ${#dtbs[@]} -gt 0 ]] || die "No DTBs built under ${dtb_src}"
	[[ ${#overlays[@]} -gt 0 ]] || die "No overlays built under ${overlay_src}"

	# --- Package-owned payload ---
	# Keep the firmware payload out of the package's /boot paths and under a
	# path only this package owns. pkg_postinst materialises /boot from here
	# on every merge, which is what makes revision upgrades safe: Portage
	# merges the new files, then runs the *replaced* package's pkg_postrm
	# (which may rm -rf the shared /boot/rpi-${KV_FULL} directory), and only
	# then the new pkg_postinst. Without this staging copy that order leaves
	# the new kernel and DTBs deleted before the firmware ever sees them.
	insinto /usr/lib/${PN}/${KV_FULL}
	newins "${image_src}" "${RPI_IMAGE_NAME}"
	doins "${dtbs[@]}"

	insinto /usr/lib/${PN}/${KV_FULL}/overlays
	doins "${overlays[@]}"
	if [[ -f "${overlay_src}/README" ]]; then
		doins "${overlay_src}/README"
	fi
}

pkg_pretend() {
	rpi_check_boot_mounted

	if use initramfs; then
		ewarn "RPi5 boots without an initramfs. The firmware bootloader loads"
		ewarn "the kernel image directly from /boot/${RPI_IMAGE_NAME}."
		ewarn "USE=initramfs will pull in dracut/ugrd unnecessarily."
	fi
	kernel-install_pkg_pretend
}

pkg_preinst() {
	# pkg_pretend is not run by direct 'ebuild ... merge' invocations, so keep
	# the mount gate here as well.
	rpi_check_boot_mounted

	# Chaining is mandatory: defining pkg_preinst shadows the eclass-exported
	# kernel-install_pkg_preinst, which restores the module signing key, checks
	# the installed kernel dir/version, and rewrites /lib/modules/${KV_FULL}
	# symlinks on merged-usr systems.
	kernel-install_pkg_preinst
}

pkg_postinst() {
	rpi_dirs

	# Materialise the boot payload before installkernel/dracut runs: if that
	# step dies, the replaced package's postrm may already have removed every
	# bootable file, and the node must still have a kernel in place.
	rpi_materialise_boot

	kernel-build_pkg_postinst

	if boot_partition_not_usable; then
		ewarn "A separate /boot is declared in /etc/fstab but is not mounted"
		ewarn "read-write. Files were written to the root filesystem; the firmware"
		ewarn "will not see them. Mount /boot and re-merge this package."
	fi

	rpi_install_top_level_fallbacks

	ewarn ""
	ewarn "Boot layout uses version-isolated directories (no symlinks in /boot):"
	ewarn "  All boot artifacts are in /boot/rpi-${KV_FULL}/"
	ewarn "  Top-level compatibility fallbacks installed to /boot/"
	ewarn ""
	ewarn "Recommended /boot/config.txt setting for native prefix mode:"
	ewarn "  os_prefix=rpi-${KV_FULL}/"
	ewarn "  overlay_prefix=overlays/"
	ewarn ""

	if [[ -f "${EROOT}/boot/config.txt" ]] && \
		! grep -q 'pcie-32bit-dma' "${EROOT}/boot/config.txt"; then
		ewarn "If using a JMicron JMB582/585 SATA controller (e.g. Radxa SATA HAT),"
		ewarn "add to /boot/config.txt:"
		ewarn "  dtoverlay=pcie-32bit-dma-pi5"
		ewarn "See: https://github.com/raspberrypi/linux/issues/7375"
	fi
}

pkg_config() {
	rpi_dirs

	# Recovery path advertised by dist-kernel: restores /boot from the staged
	# copy, then re-runs installkernel/dracut.
	rpi_materialise_boot
	rpi_install_top_level_fallbacks
	kernel-install_pkg_config
}

pkg_postrm() {
	rpi_dirs

	# During a revision replacement the new pkg_postinst runs after this phase
	# and materialises /boot from the new package's staged copy. Doing nothing
	# here keeps the previous kernel bootable for the whole window.
	if [[ -n ${REPLACED_BY_VERSION} ]]; then
		return 0
	fi

	# Removes initramfs/uki leftovers and any dead /lib/modules symlinks.
	kernel-install_pkg_postrm

	rm -rf "${RPI_BOOT}" 2>/dev/null

	# --- Find latest remaining version directory (sort -V) ---
	local best_dir=""
	local candidates
	shopt -s nullglob
	candidates=("${EROOT}/boot"/rpi-*/)
	shopt -u nullglob
	if [[ ${#candidates[@]} -gt 0 ]]; then
		best_dir=$(printf '%s\n' "${candidates[@]}" | sort -V | tail -n1)
		best_dir="${best_dir%/}"
	fi

	# --- Update or clean up top-level compatibility fallbacks ---
	if [[ -n "${best_dir}" ]]; then
		local repointed=1
		local f
		cp -f "${best_dir}/${RPI_IMAGE_NAME}" "${EROOT}/boot/${RPI_IMAGE_NAME}" \
			|| repointed=0
		for f in "${best_dir}"/*.dtb; do
			[[ -e ${f} ]] || continue
			cp -f "${f}" "${EROOT}/boot/" || repointed=0
		done
		if [[ -d "${best_dir}/overlays" ]]; then
			local ovl_new="${EROOT}/boot/.overlays.new"
			rm -rf "${ovl_new}"
			if cp -rf "${best_dir}/overlays" "${ovl_new}"; then
				rm -rf "${EROOT}/boot/overlays"
				mv "${ovl_new}" "${EROOT}/boot/overlays" || repointed=0
			else
				rm -rf "${ovl_new}"
				repointed=0
			fi
		fi
		if (( repointed )); then
			einfo "Updated top-level compatibility fallbacks to $(basename "${best_dir}")"
		else
			ewarn "Failed to update some top-level boot fallbacks from $(basename "${best_dir}")"
		fi
	else
		# Last kernel of this package. Leave the files alone when another
		# installed package owns them (e.g. the 6.12 SLOT=0 ebuild or
		# sys-kernel/raspberrypi-image own the top-level /boot paths).
		if rpi_owned_by_other_pkg "${EROOT}/boot/${RPI_IMAGE_NAME}"; then
			einfo "Leaving top-level boot files alone: owned by another installed package"
		else
			local f
			rm -f "${EROOT}/boot/${RPI_IMAGE_NAME}" 2>/dev/null
			for f in "${EROOT}"/boot/*.dtb; do
				[[ -e ${f} ]] || continue
				rpi_owned_by_other_pkg "${f}" || rm -f "${f}" 2>/dev/null
			done
			rpi_owned_by_other_pkg "${EROOT}/boot/overlays" \
				|| rm -rf "${EROOT}/boot/overlays" 2>/dev/null
		fi
	fi
}