# @description Enables Rust language support in the Linux kernel (`CONFIG_RUST`). Installs a rustup-managed toolchain (`RUST_VERSION`, `bindgen-cli`, `rust-src`) into `${SRC}/cache/tools/rustup/`, keyed by a content hash, and passes `RUSTC`/`RUSTFMT`/`BINDGEN` make params. Ships prebuilt Rust crate artifacts in `linux-headers` so out-of-tree DKMS Rust modules can build.
# @doc-page /build-framework/extensions/kernel-rust/

# Enable Rust support for Linux kernel compilation.
#
# Installs Rust toolchain via rustup into ${SRC}/cache/tools/rustup/ and
# configures the build environment so that CONFIG_RUST appears in kernel
# menuconfig and gets enabled automatically.
#
# The toolchain is cached by a hash of (RUST_VERSION, BINDGEN_VERSION, arch,
# RUST_EXTRA_COMPONENTS, RUST_EXTRA_CARGO_CRATES). Changing any of these
# triggers a full reinstall on the next build.
#
# Other extensions can request additional rustup components or cargo crates:
#   RUST_EXTRA_COMPONENTS+=("clippy" "llvm-tools")
#   RUST_EXTRA_CARGO_CRATES+=("mdbook" "cargo-deb@2.11.0")
#
# The linux-headers package gets the prebuilt Rust crate artifacts
# (rust/*.rmeta, proc-macro rust/*.so), so out-of-tree (DKMS) Rust modules
# build against the installed headers. Proc-macro .so files are build-host
# binaries: for Rust DKMS on the target, build the kernel natively on the
# same architecture. A matching rustc version on the target is required
# (see CONFIG_RUSTC_VERSION_TEXT in the shipped .config).
#
# Usage:  ./compile.sh kernel-config BOARD=... BRANCH=... ENABLE_EXTENSIONS="kernel-rust"
#
# References:
#   https://docs.kernel.org/rust/quick-start.html
#   https://docs.kernel.org/rust/general-information.html
#   https://rust-for-linux.com/rust-version-policy
#   https://rust-lang.github.io/rustup/installation/index.html

# Rust toolchain version installed via rustup.
# Kernel >= 6.12 requires rustc >= 1.78. See rust-version-policy above.
RUST_VERSION="${RUST_VERSION:-1.85.0}"

# bindgen-cli version installed via cargo.
# APT bindgen 0.66.1 panics on kernel >= 6.19 headers (FromBytesWithNulError
# in codegen/mod.rs). Fixed in >= 0.69.
BINDGEN_VERSION="${BINDGEN_VERSION:-0.71.1}"

# Enable Rust sample kernel modules for toolchain smoke testing.
# Set to "yes" to build rust_minimal, rust_print, rust_driver_faux as modules.
# Can also be set via command line: RUST_KERNEL_SAMPLES=yes
RUST_KERNEL_SAMPLES="${RUST_KERNEL_SAMPLES:-no}"

# Extra rustup components to install (e.g. clippy, llvm-tools).
# Other extensions can append: RUST_EXTRA_COMPONENTS+=("clippy")
declare -g -a RUST_EXTRA_COMPONENTS=()

# Extra cargo crates to install. Supports "name" or "name@version" syntax.
# Other extensions can append: RUST_EXTRA_CARGO_CRATES+=("mdbook" "cargo-deb@2.11.0")
declare -g -a RUST_EXTRA_CARGO_CRATES=()

# Resolved tool paths, set by host_dependencies_ready, used by custom_kernel_make_params.
declare -g RUST_TOOL_RUSTC=""
declare -g RUST_TOOL_RUSTFMT=""
declare -g RUST_TOOL_BINDGEN=""
declare -g RUST_TOOL_SYSROOT=""

function add_host_dependencies__add_rust_compiler() {
	display_alert "Adding Rust kernel build dependencies" "${EXTENSION}" "info"
	# bindgen needs libclang for dlopen; available on all target distros.
	# NB: do NOT pre-pull llvm-dev into an earlier layer to "split" this — measured, llvm-dev is a
	# superset far larger than what libclang-dev needs (~+560MB total, bigger max layer). Keep
	# libclang-dev as a single ~633MB 'clang' layer instead.
	EXTRA_BUILD_DEPS+=("clang::libclang-dev")
}

# Download rustup-init binary for the current architecture.
# Follows the project pattern: curl → .tmp → mv → chmod.
_download_rustup_init() {
	local target_dir="$1"
	local target_triple
	case "${BASH_VERSINFO[5]}" in
		*aarch64*) target_triple="aarch64-unknown-linux-gnu" ;;
		*x86_64*) target_triple="x86_64-unknown-linux-gnu" ;;
		*riscv64*) target_triple="riscv64gc-unknown-linux-gnu" ;;
		*) exit_with_error "Unsupported architecture for rustup" "${BASH_VERSINFO[5]}" ;;
	esac

	local url="https://static.rust-lang.org/rustup/dist/${target_triple}/rustup-init"
	local dest="${target_dir}/rustup-init"

	display_alert "Downloading rustup-init" "${target_triple}" "info"
	curl --proto '=https' --tlsv1.2 -sSf -o "${dest}.tmp" "${url}"
	mv "${dest}.tmp" "${dest}"
	chmod +x "${dest}"
}

# Install or reuse cached Rust toolchain in ${SRC}/cache/tools/rustup/.
_prepare_rust_toolchain() {
	local rust_cache_dir="${SRC}/cache/tools/rustup"
	mkdir -p "${rust_cache_dir}"

	local rustup_home="${rust_cache_dir}/rustup-home"
	local cargo_home="${rust_cache_dir}/cargo-home"

	# Content-addressable cache: hash of version config + architecture + extras
	local cache_key="${RUST_VERSION}|${BINDGEN_VERSION}|${BASH_VERSINFO[5]}"
	cache_key+="|${RUST_EXTRA_COMPONENTS[*]}|${RUST_EXTRA_CARGO_CRATES[*]}"
	local cache_hash
	cache_hash="$(echo -n "${cache_key}" | sha256sum | cut -c1-16)"
	local marker="${rust_cache_dir}/.marker-${cache_hash}"

	if [[ -f "${marker}" ]]; then
		display_alert "Rust toolchain cache hit" "${cache_hash}" "cachehit"
		return 0
	fi

	# Remove stale markers from previous versions
	rm -f "${rust_cache_dir}"/.marker-*

	display_alert "Installing Rust toolchain" "rustc ${RUST_VERSION}, bindgen ${BINDGEN_VERSION}" "info"

	# Download rustup-init
	do_with_retries 3 _download_rustup_init "${rust_cache_dir}"

	# Install minimal toolchain; SKIP_PATH_CHECK suppresses warnings about
	# system rustc in /usr/bin (e.g. from mtkflash in Docker images).
	RUSTUP_HOME="${rustup_home}" CARGO_HOME="${cargo_home}" \
		RUSTUP_INIT_SKIP_PATH_CHECK=yes \
		"${rust_cache_dir}/rustup-init" -y \
		--profile minimal \
		--default-toolchain "${RUST_VERSION}" \
		--no-modify-path

	# Components: rustfmt (not in minimal profile) + rust-src (kernel needs it) + extras
	local -a components=(rustfmt rust-src "${RUST_EXTRA_COMPONENTS[@]}")
	display_alert "Installing rustup components" "${components[*]}" "info"
	RUSTUP_HOME="${rustup_home}" CARGO_HOME="${cargo_home}" \
		"${cargo_home}/bin/rustup" component add "${components[@]}"

	# Cargo crates: bindgen-cli (kernel needs it) + extras
	# Supports "name" or "name@version" syntax.
	local -a crates=("bindgen-cli@${BINDGEN_VERSION}" "${RUST_EXTRA_CARGO_CRATES[@]}")
	local crate
	for crate in "${crates[@]}"; do
		display_alert "Installing cargo crate" "${crate}" "info"
		RUSTUP_HOME="${rustup_home}" CARGO_HOME="${cargo_home}" \
			"${cargo_home}/bin/cargo" install --locked "${crate}"
	done

	# Mark cache as valid only after everything succeeds
	touch "${marker}"
	display_alert "Rust toolchain installed" "${cache_hash}" "info"
}

# Resolve absolute paths to Rust tool binaries.
# Uses direct paths into the toolchain (not rustup proxies), so that
# env -i in run_kernel_make_internal() does not need RUSTUP_HOME set.
_resolve_rust_tool_paths() {
	local rustup_home="${SRC}/cache/tools/rustup/rustup-home"
	local cargo_home="${SRC}/cache/tools/rustup/cargo-home"

	RUST_TOOL_SYSROOT="$(RUSTUP_HOME="${rustup_home}" CARGO_HOME="${cargo_home}" \
		"${cargo_home}/bin/rustc" --print sysroot)"

	# Direct binaries inside the toolchain, bypassing rustup proxy
	RUST_TOOL_RUSTC="${RUST_TOOL_SYSROOT}/bin/rustc"
	RUST_TOOL_RUSTFMT="${RUST_TOOL_SYSROOT}/bin/rustfmt"
	RUST_TOOL_BINDGEN="${cargo_home}/bin/bindgen"
}

function host_dependencies_ready__add_rust_compiler() {
	_prepare_rust_toolchain
	_resolve_rust_tool_paths

	# Verify all tools are executable
	local tool_name tool_path
	for tool_name in RUST_TOOL_RUSTC RUST_TOOL_RUSTFMT RUST_TOOL_BINDGEN; do
		tool_path="${!tool_name}"
		[[ -x "${tool_path}" ]] || exit_with_error "Required Rust tool '${tool_name}' not found at ${tool_path}" "${EXTENSION}"
	done

	display_alert "Rust toolchain ready" \
		"rustc $(${RUST_TOOL_RUSTC} --version | awk '{print $2}'), bindgen $(${RUST_TOOL_BINDGEN} --version 2>&1 | awk '{print $2}')" "info"
}

function artifact_kernel_version_parts__add_rust_version() {
	# Include Rust toolchain version in artifact hash so that changing
	# RUST_VERSION or BINDGEN_VERSION triggers a kernel rebuild.
	local cache_key="${RUST_VERSION}|${BINDGEN_VERSION}"
	local short
	short="$(echo -n "${cache_key}" | sha256sum | cut -c1-4)"

	artifact_version_parts["_R"]="rust${short}"

	# Add to order array if not already present
	local found=0 entry
	for entry in "${artifact_version_part_order[@]}"; do
		[[ "${entry}" == *"-_R" ]] && found=1 && break
	done
	if [[ "${found}" -eq 0 ]]; then
		artifact_version_part_order+=("0086-_R")
	fi
}

function custom_kernel_config__add_rust_compiler() {
	# https://docs.kernel.org/rust/quick-start.html
	opts_y+=("RUST")

	# Build sample Rust modules for toolchain smoke testing
	if [[ "${RUST_KERNEL_SAMPLES}" == "yes" ]]; then
		display_alert "Enabling Rust sample modules" "${EXTENSION}" "info"
		opts_y+=("SAMPLES") # Parent menu for all kernel samples
		opts_y+=("SAMPLES_RUST")
		opts_m+=("SAMPLE_RUST_MINIMAL")
		opts_m+=("SAMPLE_RUST_PRINT")
		opts_m+=("SAMPLE_RUST_DRIVER_FAUX")
	fi
}

function pre_package_kernel_headers__add_rust_artifacts() {
	# Ship prebuilt Rust crate artifacts in the linux-headers package so that
	# out-of-tree (DKMS) Rust modules can build against the installed headers.
	# Without them kbuild sees CONFIG_RUST=y in auto.conf but rustc fails with
	# E0463 "can't find crate for core".
	#
	# The file set was determined empirically (strace of an out-of-tree Rust
	# module build against a fully built tree): rustc reads rust/*.rmeta (crate
	# metadata, target-arch neutral) and rust/*.so (proc-macro dylibs). With
	# CONFIG_RUST_INLINE_HELPERS=y (6.19+, clang builds) kbuild additionally
	# links module objects with rust/helpers/helpers_module.bc. The set varies
	# per kernel version and config, so copy by glob, not by name.
	# shellcheck disable=SC2154 # kernel_work_dir is defined in the calling packaging function
	if ! grep -q "^CONFIG_RUST=y" "${kernel_work_dir}/include/config/auto.conf" 2> /dev/null; then
		display_alert "Kernel built without CONFIG_RUST, not adding Rust artifacts to headers" "${EXTENSION}" "debug"
		return 0
	fi

	declare -a rust_artifacts=()
	local f
	for f in "${kernel_work_dir}/rust/"*.rmeta "${kernel_work_dir}/rust/"*.so "${kernel_work_dir}/rust/helpers/"*.bc; do
		if [[ -f "${f}" ]]; then
			rust_artifacts+=("${f#"${kernel_work_dir}/"}")
		fi
	done

	# A headers package that advertises CONFIG_RUST=y without the artifacts is
	# broken-by-construction (worse than no Rust support), so fail the build.
	if [[ ${#rust_artifacts[@]} -eq 0 ]]; then
		exit_with_error "CONFIG_RUST=y but no rust/*.rmeta artifacts found in kernel tree" "${EXTENSION}"
	fi

	# proc-macro .so files run on the *build host*; after cross-compilation they
	# will not load on the target, so Rust DKMS there needs same-arch headers.
	local host_arch target_arch_pattern
	host_arch="$(uname -m)"
	case "${ARCH}" in
		arm64) target_arch_pattern="aarch64" ;;
		amd64) target_arch_pattern="x86_64" ;;
		armhf) target_arch_pattern="armv[6-8]l" ;;
		riscv64) target_arch_pattern="riscv64" ;;
		*) target_arch_pattern="${ARCH}" ;;
	esac
	# shellcheck disable=SC2254 # pattern expansion is the point here
	case "${host_arch}" in
		${target_arch_pattern}) : ;; # native build, .so files match the target
		*)
			display_alert "Rust proc-macro .so in headers are ${host_arch} binaries" \
				"${EXTENSION}: Rust DKMS on ${ARCH} target requires natively-built headers" "wrn"
			;;
	esac

	display_alert "Adding Rust artifacts to linux-headers" "${EXTENSION}: ${#rust_artifacts[@]} files" "info"
	# shellcheck disable=SC2154 # headers_target_dir is defined in the calling packaging function
	tar -c -f - -C "${kernel_work_dir}" "${rust_artifacts[@]}" | tar -xf - -C "${headers_target_dir}"
	if [[ "${PIPESTATUS[0]}" -ne 0 || "${PIPESTATUS[1]}" -ne 0 ]]; then
		exit_with_error "Failed to copy Rust artifacts into headers package" "${EXTENSION}"
	fi
}

function custom_kernel_make_params__add_rust_compiler() {
	# run_kernel_make_internal uses "env -i" which clears all environment
	# variables, so we pass Rust paths explicitly via make parameters.
	# Using direct toolchain binaries (not rustup proxies) avoids needing
	# RUSTUP_HOME in the env -i context.

	common_make_params_quoted+=("RUSTC=${RUST_TOOL_RUSTC}")
	common_make_params_quoted+=("RUSTFMT=${RUST_TOOL_RUSTFMT}")
	common_make_params_quoted+=("BINDGEN=${RUST_TOOL_BINDGEN}")

	# Rust standard library source path for kernel build
	local rust_lib_src="${RUST_TOOL_SYSROOT}/lib/rustlib/src/rust/library"
	if [[ -d "${rust_lib_src}" ]]; then
		display_alert "Rust library source" "${rust_lib_src}" "info"
		common_make_envs+=("RUST_LIB_SRC='${rust_lib_src}'")
	else
		display_alert "Rust library source not found" "CONFIG_RUST will not appear in menuconfig" "wrn"
	fi
}