alarm-tegra-x1 / src/kernel.rs

raw file 44652 bytes
use anyhow::{bail, Context, Result};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;

use crate::config::BuildConfig;

/// Linux kernel source URL
const KERNEL_URL: &str = "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git";
const KERNEL_BRANCH: &str = "v7.2";

/// Download Linux kernel source
pub fn download_source(build_dir: &Path) -> Result<()> {
    let kernel_dir = build_dir.join("kernel/src");
    
    if kernel_dir.exists() && kernel_dir.join(".git").exists() {
        println!("Kernel source already exists, updating...");
        // Use absolute path with -C flag
        let status = Command::new("git")
            .args(&["-C", kernel_dir.to_str().unwrap(), "pull"])
            .status()?;
        if !status.success() {
            anyhow::bail!("Failed to update kernel source");
        }
        return Ok(());
    }

    println!("Cloning Linux kernel...");
    // Clone to absolute path without changing working directory
    let status = Command::new("git")
        .args(&[
            "clone",
            "--depth=1",
            "--branch", KERNEL_BRANCH,
            KERNEL_URL,
            kernel_dir.to_str().unwrap(),
        ])
        .status()
        .context("Failed to run git")?;

    if !status.success() {
        anyhow::bail!("Failed to clone kernel");
    }

    Ok(())
}

/// Build Linux kernel for Tegra X1
pub fn build(
    config: &BuildConfig,
    build_dir: &Path,
    custom_config: Option<&PathBuf>,
) -> Result<()> {
    let kernel_src = build_dir.join("kernel/src");
    let kernel_build = build_dir.join("kernel/build");
    let kernel_out = build_dir.join("kernel/out");

    if !kernel_src.exists() {
        anyhow::bail!("Kernel source not found. Run 'init' first.");
    }

    std::fs::create_dir_all(&kernel_build)?;
    std::fs::create_dir_all(&kernel_out)?;

    let cross_compile = format!("{}-", config.cross_compiler);

    // Copy or generate kernel config
    let config_path = if let Some(custom) = custom_config {
        custom.clone()
    } else {
        generate_tegra_config(&kernel_src, build_dir)?
    };

    // Copy config to kernel source
    std::fs::copy(&config_path, kernel_src.join(".config"))
        .context("Failed to copy kernel config")?;

    // Install the out-of-tree switch-fb driver *before* olddefconfig: its
    // Kconfig stanza has to exist for CONFIG_SWITCH_FB to resolve. Installing
    // it afterwards (as an earlier revision did) meant the symbol was dropped
    // and the driver never got built.
    install_switch_fb(&kernel_src)?;

    install_switch_panel(&kernel_src)?;

    // Joy-Con input over the rails (UARTs). Whole-file install, like the panel.
    install_switch_joycon(&kernel_src)?;

    // Some changes have to edit an upstream file in place rather than add a
    // whole new one -- the DSI hotplug fix, the touchscreen quirks. Those live
    // in patches/ and nothing else re-applies them, so a freshly cloned kernel
    // tree would silently lose them.
    install_switch_patches(&kernel_src)?;

    // The Switch device tree is not upstream either, so it has to be injected
    // into the tree before `make dtbs` will emit it.
    install_switch_dt(&kernel_src)?;

    // Update config with olddefconfig
    println!("Updating kernel config...");
    run_make(
        &[
            &format!("ARCH={}", config.target_arch),
            &format!("CROSS_COMPILE={}", cross_compile),
            "olddefconfig",
        ],
        &kernel_src,
    )?;

    // kconfig discards unknown or unmet symbols without a word. Never trust the
    // file that was written; always read back the one kconfig produced.
    //
    // Skippable for deliberate experiments with a hand-made .config (see
    // ALARM_SKIP_CONFIG_CHECK), where the whole point is to build something
    // minimal that does not carry the full required set.
    if std::env::var_os("ALARM_SKIP_CONFIG_CHECK").is_none() {
        verify_required_options(&kernel_src)?;
    } else {
        println!("Skipping required-option verification (ALARM_SKIP_CONFIG_CHECK set)");
    }

    // Build kernel
    println!("Building kernel...");
    let jobs = num_cpus::get();
    // ALARM_NO_MODULES skips the (slow) module build for quick experiments. The
    // early-boot marker in the initramfs needs no modules at all, so this makes
    // a bring-up iteration several times faster.
    let with_modules = std::env::var_os("ALARM_NO_MODULES").is_none();
    let arch_arg = format!("ARCH={}", config.target_arch);
    let cc_arg = format!("CROSS_COMPILE={}", cross_compile);
    let jobs_arg = format!("-j{}", jobs);
    let mut build_args: Vec<&str> = vec![&arch_arg, &cc_arg, &jobs_arg, "Image"];
    if with_modules {
        build_args.push("modules");
    }
    build_args.push("dtbs");
    run_make(&build_args, &kernel_src)?;

    // The config check above proves the *symbol* survived kconfig. It does not
    // prove the code was built, and those are different things -- see
    // verify_built_in_drivers().
    if std::env::var_os("ALARM_SKIP_CONFIG_CHECK").is_none() {
        verify_built_in_drivers(&kernel_src)?;
    }

    if with_modules {
        println!("Installing modules...");
        run_make(
            &[
                &arch_arg,
                &cc_arg,
                &format!("INSTALL_MOD_PATH={}", kernel_out.display()),
                "modules_install",
            ],
            &kernel_src,
        )?;
    } else {
        println!("Skipping module build and install (ALARM_NO_MODULES set)");
    }

    // Copy kernel image
    std::fs::copy(
        kernel_src.join("arch/arm64/boot/Image"),
        build_dir.join("kernel/Image"),
    ).context("Failed to copy kernel image")?;

    // Copy device trees
    let dtb_src = kernel_src.join("arch/arm64/boot/dts/nvidia");
    if dtb_src.exists() {
        let dtb_dst = build_dir.join("device-tree");
        std::fs::create_dir_all(&dtb_dst)?;
        
        for entry in std::fs::read_dir(&dtb_src)? {
            let entry = entry?;
            if entry.path().extension().map_or(false, |e| e == "dtb") {
                std::fs::copy(entry.path(), dtb_dst.join(entry.file_name()))?;
            }
        }
    }

    println!("Kernel build complete!");
    Ok(())
}

/// Generate kernel config optimized for Tegra X1
fn generate_tegra_config(kernel_src: &Path, build_dir: &Path) -> Result<PathBuf> {
    let config_path = build_dir.join("config/kernel.config");
    
    // Start with defconfig
    let cross_compile = "aarch64-linux-gnu-";
    run_make(
        &[
            "ARCH=arm64",
            &format!("CROSS_COMPILE={}", cross_compile),
            "defconfig",
        ],
        kernel_src,
    )?;

    // Read generated config
    let config_content = std::fs::read_to_string(kernel_src.join(".config"))
        .context("Failed to read generated config")?;

    let mut config = config_content;

    // Tegra X1 specific options.
    //
    // Every symbol below has been checked against the target tree. That matters:
    // an unknown symbol is discarded *silently* by `olddefconfig`. That is how
    // an earlier revision shipped with no PCIe and therefore no WiFi — it asked
    // for `CONFIG_PCIE_TEGRA`, which has never existed (the driver is
    // `CONFIG_PCI_TEGRA`). `verify_required_options()` now fails the build if
    // any critical option does not survive kconfig.
    let tegra_options = [
        // --- CPU feature baseline (Cortex-A57 is ARMv8.0) --------------------
        // The arm64 defconfig enables features that require ARMv8.2/8.3/8.5:
        // MTE and SME/BTI are v8.5, SVE is v8.2, and PTR_AUTH (+_KERNEL) is
        // v8.3 -- the latter puts PAC instructions in every kernel function
        // prologue. The A57 has none of them.
        //
        // These are runtime-gated in principle, but they are still the best
        // explanation for a death with NO output at all: the kernel never even
        // wrote its printk ring buffer, which means it died before start_kernel
        // got anywhere -- before the device tree was parsed. Disabling them is
        // also simply correct here: none of these features can ever be used on
        // this SoC. This is the same class of bug as the VA_BITS_52 default
        // documented above.
        ("CONFIG_ARM64_MTE", "n"),
        ("CONFIG_ARM64_BTI", "n"),
        ("CONFIG_ARM64_BTI_KERNEL", "n"),
        ("CONFIG_ARM64_SVE", "n"),
        ("CONFIG_ARM64_SME", "n"),
        ("CONFIG_ARM64_PTR_AUTH", "n"),
        ("CONFIG_ARM64_PTR_AUTH_KERNEL", "n"),

        // --- Power domains ---------------------------------------------------
        // SOC_TEGRA_PMC registers the Tegra power domains with
        // PM_GENERIC_DOMAINS. On QEMU -- where the identical kernel boots to
        // userspace -- there is no Tegra PMC and no genpd. Here, a domain that
        // gets gated while in use (CPU cluster, memory, display) would kill the
        // kernel with no output and blank the panel, which is exactly the
        // observed behaviour: the colour fills stay visible until the kernel
        // starts, then the screen goes black.
        ("CONFIG_PM_GENERIC_DOMAINS", "n"),
        ("CONFIG_PM_GENERIC_DOMAINS_OF", "n"),

        // NOTE: the Tegra platform drivers (SOC_TEGRA_PMC, TEGRA_MC, TEGRA210_EMC,
        // ARM_SMMU, TEGRA_CLK_DFLL) cannot be bisected out: ARCH_TEGRA forces
        // them on, and disabling ARCH_TEGRA removes the microSD driver that the
        // boot marker depends on. That avenue is closed; the FORBIDDEN_OPTIONS
        // guard is what established it.

        // --- SoC / platform -------------------------------------------------
        ("CONFIG_ARCH_TEGRA", "y"),
        ("CONFIG_ARCH_TEGRA_210_SOC", "y"),
        ("CONFIG_SOC_TEGRA_PMC", "y"),

        // --- CPU ------------------------------------------------------------
        // ARM_CPU_TOPOLOGY was removed upstream (ARCH_TEGRA selects the
        // topology code itself), and CAKE is a *network* qdisc, not a
        // scheduler — both were mistakes.
        ("CONFIG_SCHED_MC", "y"),
        ("CONFIG_CPU_FREQ", "y"),
        ("CONFIG_CPU_FREQ_DEFAULT_GOV_SCHEDUTIL", "y"),
        ("CONFIG_CPU_FREQ_GOV_PERFORMANCE", "y"),
        ("CONFIG_CPU_FREQ_GOV_POWERSAVE", "y"),

        // --- Clocks ---------------------------------------------------------
        ("CONFIG_TEGRA_CLK_DFLL", "y"),

        // --- Memory controller ----------------------------------------------
        // The mainline symbol is CONFIG_TEGRA_MC; CONFIG_ARM_TEGRA_MC never
        // existed.
        ("CONFIG_TEGRA_MC", "y"),
        ("CONFIG_TEGRA210_EMC", "y"),

        // --- Pinmux / GPIO --------------------------------------------------
        // PINCTRL_TEGRA alone does not pull in the T210 pinmux tables.
        ("CONFIG_PINCTRL_TEGRA", "y"),
        ("CONFIG_PINCTRL_TEGRA210", "y"),
        ("CONFIG_GPIO_TEGRA", "y"),

        // --- I2C / PWM ------------------------------------------------------
        ("CONFIG_I2C_TEGRA", "y"),
        ("CONFIG_PWM_TEGRA", "y"),

        // --- PMIC / regulators ----------------------------------------------
        // The Switch powers the SD card, USB and the display through a MAX77620
        // on i2c1. Without it none of those peripherals come up.
        ("CONFIG_MFD_MAX77620", "y"),
        ("CONFIG_REGULATOR_MAX77620", "y"),
        ("CONFIG_REGULATOR_FIXED_VOLTAGE", "y"),

        // --- IOMMU ----------------------------------------------------------
        // CONFIG_IOMMU_TEGRA does not exist; arm64 Tegra uses the SMMU.
        ("CONFIG_ARM_SMMU", "y"),

        // --- Power management ------------------------------------------------
        ("CONFIG_PM_SLEEP", "y"),
        ("CONFIG_SUSPEND", "y"),
        ("CONFIG_HIBERNATION", "n"),

        // --- USB -------------------------------------------------------------
        // XHCI for host mode, XUDC for device mode. The switchroot initramfs
        // exposes the *only* usable console by creating a configfs ACM gadget
        // on tegra-xudc (`700d0000.xudc`) -> /dev/ttyGS0. That stack is not
        // optional: without it a failed boot is completely silent, because
        // there is no wired UART on a retail Switch.
        ("CONFIG_PHY_TEGRA_XUSB", "y"),
        ("CONFIG_USB_XHCI_HCD", "y"),
        ("CONFIG_USB_XHCI_TEGRA", "y"),
        ("CONFIG_USB_GADGET", "y"),
        ("CONFIG_USB_TEGRA_XUDC", "y"),
        ("CONFIG_USB_CONFIGFS", "y"),
        ("CONFIG_USB_CONFIGFS_ACM", "y"),
        // Host side of the console. The gadget above presents a CDC-ACM
        // interface, and this is the driver that claims it and creates
        // /dev/ttyACM0 on the *host*. The stock config has it as =m, and a
        // build with ALARM_NO_MODULES never produces that module -- so the
        // gadget enumerates perfectly and no serial port ever appears.
        ("CONFIG_USB_ACM", "y"),

        // --- PCIe (BCM4356 WiFi) ---------------------------------------------
        // `pci-tegra.c` handles "nvidia,tegra210-pcie"; the T194 driver is not
        // involved. CONFIG_PCIE_TEGRA does not exist.
        ("CONFIG_PCI_TEGRA", "y"),
        ("CONFIG_PCIEPORTBUS", "y"),

        // --- WiFi ------------------------------------------------------------
        // RFKILL must be forced =y: CFG80211 declares
        // `depends on RFKILL || !RFKILL`, which evaluates to *m* when RFKILL=m,
        // and olddefconfig then silently clamps CFG80211/MAC80211/BRCMUTIL/
        // BRCMFMAC to =m so WiFi never lands in the built-in kernel.
        ("CONFIG_RFKILL", "y"),
        ("CONFIG_CFG80211", "y"),
        ("CONFIG_MAC80211", "y"),
        ("CONFIG_BRCMUTIL", "y"),
        ("CONFIG_BRCMFMAC", "y"),
        ("CONFIG_BRCMFMAC_PCIE", "y"),
        ("CONFIG_BRCMFMAC_PROTO_BCDC", "y"),

        // --- Bluetooth --------------------------------------------------------
        ("CONFIG_BT_HCIUART", "y"),
        ("CONFIG_BT_HCIUART_BCM", "y"),

        // --- Storage ----------------------------------------------------------
        ("CONFIG_MMC_SDHCI_TEGRA", "y"),

        // --- Display ----------------------------------------------------------
        // Mainline must describe the Switch's DSI panel itself now (there is no
        // nx-plat.dtimg to borrow from), so the real DRM/KMS driver, the panel
        // helpers and the PWM backlight all have to be in.
        ("CONFIG_DRM", "y"),
        ("CONFIG_DRM_TEGRA", "y"),
        ("CONFIG_DRM_FBDEV_EMULATION", "y"),
        // Simplest possible console: binds to the "simple-framebuffer" child of
        // /chosen (the buffer hekate/U-Boot already set up) and gives fb0 ->
        // fbcon -> console=tty0. Without it a failed boot is a frozen screen
        // with no explanation, because there is no wired UART to fall back on.
        ("CONFIG_DRM_SIMPLEDRM", "y"),
        // The DRM "log" client prints kernel messages straight to the display
        // through DRM/KMS, with no fbcon, VT or console plumbing in the path.
        // fbdev+fbcon is the normal route, but on this board it clears the
        // screen and then never prints, so it is worthless for bring-up.
        // Select at runtime with `drm_client_lib.active=fbdev|log`; this only
        // sets the default.
        ("CONFIG_DRM_CLIENT_LOG", "y"),
        ("CONFIG_DRM_CLIENT_DEFAULT_LOG", "y"),
        ("CONFIG_DRM_CLIENT_DEFAULT_FBDEV", "n"),
        ("CONFIG_DRM_PANEL", "y"),
        ("CONFIG_DRM_PANEL_SIMPLE", "y"),
        // The Switch's own panel: JDI LPM062M326A, 720x1280 DSI. The driver is
        // installed into the tree by install_switch_panel(); it is a port of
        // switchroot's mainline patch 0010 (see reference/switchroot-mainline/).
        ("CONFIG_DRM_PANEL_JDI_LPM062M326A", "y"),
        ("CONFIG_BACKLIGHT_CLASS_DEVICE", "y"),
        ("CONFIG_BACKLIGHT_PWM", "y"),
        ("CONFIG_FB", "y"),
        ("CONFIG_FRAMEBUFFER_CONSOLE", "y"),
        // Fallback: map an already-initialised bootloader framebuffer. Built as
        // a module so it is only loaded when DRM cannot take over.
        ("CONFIG_SWITCH_FB", "m"),

        // --- Input ------------------------------------------------------------
        // The Switch's touchscreen is an STMicroelectronics FTS controller at
        // 0x49 on i2c3, interrupting on PX1. Mainline's stmfts driver needs
        // three Switch quirks (event layout, interrupt unmasking, mt sync),
        // applied from patches/kernel-stmfts-switch.patch.
        ("CONFIG_TOUCHSCREEN_STMFTS", "y"),
        // Joy-Con input over the rails. Those are UARTs, and the controller
        // speaks HID over them -- a path mainline's hid-nintendo does not cover
        // (it handles USB/Bluetooth). The driver is installed into the tree by
        // install_switch_joycon(), from switchroot's patch 0016.
        //
        // CONFIG_INPUT_JOYSTICK is not optional: drivers/input/Makefile gates the
        // whole joystick/ directory on it (`obj-$(CONFIG_INPUT_JOYSTICK) +=
        // joystick/`). Without it JOYSTICK_JOYCON can be set and the driver still
        // never gets built, because kconfig sources joystick/Kconfig
        // unconditionally and so never complains -- the gate is only in the
        // Makefile. That cost a build cycle: the symbol was present in .config
        // and auto.conf while joycon.o was never compiled.
        ("CONFIG_INPUT_JOYSTICK", "y"),
        ("CONFIG_JOYSTICK_JOYCON", "y"),

        // --- Audio / console --------------------------------------------------
        ("CONFIG_SND_SOC_TEGRA", "y"),
        ("CONFIG_SERIAL_TEGRA", "y"),

        // --- Log capture that does not need userspace or surviving DRAM ------
        // The kernel dies before userspace on this board, the display is
        // unreadable (tiled), there is no UART, and hekate refills DRAM every
        // boot so nothing survives a reboot. pstore_blk writes the log to a
        // BLOCK DEVICE during the failing boot itself, which sidesteps all of
        // that. The device is given on the command line
        // (pstore_blk.blkdev=/dev/mmcblk0p3 -- the unused swap partition).
        ("CONFIG_PSTORE", "y"),
        ("CONFIG_PSTORE_BLK", "y"),
        ("CONFIG_PSTORE_BLK_KMSG_SIZE", "256"),
        ("CONFIG_PSTORE_CONSOLE", "y"),
        // The RAM backend, at the 0xb0000000 region the DTS reserves -- the same
        // address the L4T tree uses, which hekate leaves alone. Whether it works
        // depends on DRAM surviving a hekate reboot, which is unproven rather
        // than disproven: the 0xFF seen there is equally consistent with
        // "nothing has ever written it" as with "hekate wiped it". Built-in, not
        // a module -- this kernel ships no modules.
        ("CONFIG_PSTORE_RAM", "y"),
        // Make a hang look like a panic, so the dump actually happens.
        ("CONFIG_SOFTLOCKUP_DETECTOR", "y"),
        ("CONFIG_DETECT_HUNG_TASK", "y"),

        // --- Bring-up helpers -------------------------------------------------
        ("CONFIG_MAGIC_SYSRQ", "y"),
    ];

    // Apply options.
    //
    // Always scan and rewrite, even when the symbol is currently "not set".
    // The earlier form picked its branch on `config.contains("CONFIG_X=")`, so
    // a symbol kconfig had written as `# CONFIG_X is not set` took the append
    // path and left that stale line in place -- two contradictory lines for one
    // symbol. kconfig takes the last, so the value was right and it only warned
    // ("override: reassigning to symbol"), but that made the ordering
    // load-bearing for no reason. This also collapses pre-existing duplicates.
    for (key, value) in &tegra_options {
        let set_line = format!("{}=", key);
        let unset_line = format!("# {} is not set", key);

        let mut replaced = false;
        let mut new_lines = Vec::new();
        for line in config.lines() {
            if line.starts_with(&set_line) || line.starts_with(&unset_line) {
                if !replaced {
                    new_lines.push(format!("{}={}", key, value));
                    replaced = true;
                }
            } else {
                new_lines.push(line.to_string());
            }
        }

        config = new_lines.join("\n");
        if !replaced {
            config.push_str(&format!("\n{}={}", key, value));
        }
    }

    // Pin the address-space-size choice. Linux 7.2's arm64 Kconfig defaults
    // "Virtual address space size" to ARM64_VA_BITS_52, and a bare
    // olddefconfig therefore shipped CONFIG_ARM64_VA_BITS_52 + PA_BITS_52 +
    // ARM64_LPA2 + PGTABLE_LEVELS=5. Cortex-A57 is ARMv8.0: 48-bit VA max,
    // no FEAT_LPA2 — such a kernel dies in head.S on its first page-table
    // walk, i.e. before console, before initcalls, before any framebuffer:
    // the permanently black, SSH-dead boot tests (T1/T2) despite a
    // byte-healthy uImage and boot chain. Same bug class as the RFKILL
    // clamp above: an unpinned choice silently defaults to something fatal.
    // VA choice members are mutually exclusive and PA_BITS_52/LPA2 depend
    // on VA_BITS_52 (PA then falls to its 48 default; LPA2 is def_bool-y on
    // PA52), so pinning VA=48/PA=48 cascades to the whole block; the final
    // olddefconfig in build() recomputes the derived *_BITS ints.
    config = config
        .replace("CONFIG_ARM64_VA_BITS_52=y", "# CONFIG_ARM64_VA_BITS_52 is not set")
        .replace("# CONFIG_ARM64_VA_BITS_48 is not set", "CONFIG_ARM64_VA_BITS_48=y");
    if !config.contains("CONFIG_ARM64_VA_BITS_48=") {
        config.push_str("\nCONFIG_ARM64_VA_BITS_48=y");
    }
    config = config
        .replace("CONFIG_ARM64_PA_BITS_52=y", "# CONFIG_ARM64_PA_BITS_52 is not set")
        .replace("# CONFIG_ARM64_PA_BITS_48 is not set", "CONFIG_ARM64_PA_BITS_48=y");
    if !config.contains("CONFIG_ARM64_PA_BITS_48=") {
        config.push_str("\nCONFIG_ARM64_PA_BITS_48=y");
    }

    // Write config
    std::fs::write(&config_path, &config)
        .context("Failed to write kernel config")?;

    Ok(config_path)
}

/// Options the Switch kernel cannot boot without.
///
/// Checked *after* `olddefconfig`, which is the step that silently discards
/// unknown symbols, unmet dependencies and symbols that lose a `select` fight.
/// Keep this list to things whose absence produces a silent, hard-to-attribute
/// failure (no console, no storage, no PCIe) rather than an obvious one.
const REQUIRED_OPTIONS: &[&str] = &[
    // SoC bring-up
    "CONFIG_ARCH_TEGRA_210_SOC",
    "CONFIG_SOC_TEGRA_PMC",
    "CONFIG_TEGRA_MC",
    "CONFIG_PINCTRL_TEGRA210",
    "CONFIG_GPIO_TEGRA",
    "CONFIG_I2C_TEGRA",
    // Power: SD / USB / display won't come up without the PMIC
    "CONFIG_MFD_MAX77620",
    "CONFIG_REGULATOR_MAX77620",
    // Root filesystem
    "CONFIG_MMC_SDHCI_TEGRA",
    // The only console on a retail Switch
    "CONFIG_PHY_TEGRA_XUSB",
    "CONFIG_USB_TEGRA_XUDC",
    "CONFIG_USB_CONFIGFS",
    "CONFIG_USB_CONFIGFS_ACM",
    "CONFIG_USB_ACM",
    // WiFi
    "CONFIG_PCI_TEGRA",
    "CONFIG_BRCMFMAC",
    // Display
    "CONFIG_DRM_TEGRA",
    "CONFIG_DRM_PANEL_JDI_LPM062M326A",
    // Joy-Con input over the rails. Added here so a silently-dropped symbol
    // fails the build rather than shipping a kernel with no input, which is the
    // failure mode verify_required_options() exists to catch.
    "CONFIG_JOYSTICK_JOYCON",
    // The screen is the only console a retail unit can offer before the USB
    // gadget exists, so losing simpledrm costs us all visibility.
    "CONFIG_DRM_SIMPLEDRM",
    // Prints the boot log to the display without fbcon. This is the thing that
    // makes a silent death readable.
    "CONFIG_DRM_CLIENT_LOG",
    // Captures the kernel log when there is no console at all.
    "CONFIG_PSTORE_BLK",
    "CONFIG_PSTORE_RAM",
    "CONFIG_PSTORE_CONSOLE",
    // Cortex-A57 is ARMv8.0: 52-bit VA kills the kernel in head.S, before the
    // console exists, so it must be pinned every build.
    "CONFIG_ARM64_VA_BITS_48",
];

/// Fail the build when a required option did not survive kconfig.
///
/// Returns the list of missing option names on failure so the message names
/// exactly what vanished (rather than leaving someone to diff `.config`).
fn verify_required_options(kernel_src: &Path) -> Result<()> {
    let config = std::fs::read_to_string(kernel_src.join(".config"))
        .context("failed to read .config after olddefconfig")?;

    // Collect the names of every symbol that ended up =y.
    let enabled: std::collections::HashSet<&str> = config
        .lines()
        .filter_map(|line| {
            let rest = line.strip_prefix("CONFIG_")?;
            let (name, value) = rest.split_once('=')?;
            (value == "y").then_some(name)
        })
        .collect();

    let missing: Vec<&str> = REQUIRED_OPTIONS
        .iter()
        .filter_map(|opt| opt.strip_prefix("CONFIG_"))
        .filter(|name| !enabled.contains(name))
        .collect();

    if !missing.is_empty() {
        bail!(
            "the kernel .config is missing {} required option(s) after \
             olddefconfig: {}.\n\
             kconfig drops unknown or unmet symbols silently, so this usually \
             means a symbol name is wrong or a dependency is not selected. Fix \
             the options in `generate_tegra_config`, or the missing dependency.",
            missing.len(),
            missing.join(", ")
        );
    }

    // Symbols that must NOT be enabled. These all require ARMv8.1 or later, and
    // the Cortex-A57 in the Switch is ARMv8.0. kconfig re-enables several of
    // them via `default y`, which is the same silent-default trap that made
    // VA_BITS_52 fatal, so assert they actually stayed off.
    const FORBIDDEN_OPTIONS: &[&str] = &[
        // ARMv8.1+ CPU features -- the A57 is ARMv8.0.
        "ARM64_MTE",
        "ARM64_BTI",
        "ARM64_BTI_KERNEL",
        "ARM64_SVE",
        "ARM64_SME",
        "ARM64_PTR_AUTH",
        "ARM64_PTR_AUTH_KERNEL",
        // NOTE: the Tegra platform drivers are deliberately NOT listed here.
        // They were tried and cannot be disabled -- ARCH_TEGRA forces them on,
        // which is how that bisect avenue was closed.
    ];
    let wrongly_on: Vec<&str> = FORBIDDEN_OPTIONS
        .iter()
        .copied()
        .filter(|name| enabled.contains(name))
        .collect();
    if !wrongly_on.is_empty() {
        bail!(
            "the kernel .config has {} option(s) enabled that the Cortex-A57 \
             cannot support: {}.\n\
             These need ARMv8.1+ and the Switch is ARMv8.0. kconfig re-enables \
             some of them via `default y`, so they must be explicitly disabled \
             in `generate_tegra_config`.",
            wrongly_on.len(),
            wrongly_on.join(", ")
        );
    }
    Ok(())
}

/// Drivers this build installs into the kernel tree, each paired with a string
/// it defines that can be looked for in the built kernel.
///
/// These are the drivers `alarm-build` *adds*, and they are the ones that can
/// silently go missing. `verify_required_options()` reads `.config`; a symbol
/// can survive kconfig and still never be compiled, because a directory's
/// Makefile is often gated on a separate menu symbol. That is not hypothetical:
/// `CONFIG_JOYSTICK_JOYCON=y` was present in `.config` and `auto.conf` while
/// `joycon.o` was never built, because `drivers/input/Makefile` gates the whole
/// `joystick/` directory on `CONFIG_INPUT_JOYSTICK`. Reading the built kernel is
/// the only check that catches that.
///
/// Only built-in (`=y`) drivers belong here: `switch-fb` is deliberately `=m`
/// and is therefore correctly absent from `vmlinux`.
const INSTALLED_DRIVERS: &[(&str, &str)] = &[
    ("JDI LPM062M326A panel", "lpm062m326a"),
    ("Joy-Con rails", "joycon_wq"),
];

fn verify_built_in_drivers(kernel_src: &Path) -> Result<()> {
    let vmlinux = kernel_src.join("vmlinux");
    if !vmlinux.exists() {
        bail!(
            "{} does not exist, so the installed drivers cannot be verified",
            vmlinux.display()
        );
    }

    let missing: Vec<String> = INSTALLED_DRIVERS
        .iter()
        .filter(|(_, marker)| {
            !Command::new("grep")
                .args(["-qa", "--"])
                .arg(marker)
                .arg(&vmlinux)
                .status()
                .map(|status| status.success())
                .unwrap_or(false)
        })
        .map(|(name, marker)| format!("{name} (looked for \"{marker}\")"))
        .collect();

    if !missing.is_empty() {
        bail!(
            "{} driver(s) are configured but are not in {}:\n  {}\n\
             A symbol can survive kconfig and still never be compiled -- check the \
             Makefile gate for the directory, not only the Kconfig stanza.",
            missing.len(),
            vmlinux.display(),
            missing.join("\n  ")
        );
    }

    println!(
        "Verified all {} installed driver(s) are in the kernel.",
        INSTALLED_DRIVERS.len()
    );
    Ok(())
}

/// Install the Switch device tree into a kernel tree.
///
/// The Switch's board DTS is not upstream (there is no `nintendo` board in
/// `arch/arm64/boot/dts/nvidia/`), so a fresh clone would never build it.
/// Copying `device-tree/tegra210-nintendo-switch.dts` in and registering it in
/// the dts Makefile makes `make dtbs` emit `tegra210-nintendo-switch.dtb`
/// alongside the stock boards.
fn install_switch_dt(kernel_src: &Path) -> Result<()> {
    let project_root = std::env::current_dir()?;
    let dts_src = project_root.join("device-tree/tegra210-nintendo-switch.dts");
    if !dts_src.exists() {
        println!(
            "Warning: {} not found; the Switch device tree will not be built",
            dts_src.display()
        );
        return Ok(());
    }

    let dts_dir = kernel_src.join("arch/arm64/boot/dts/nvidia");
    const DTS_NAME: &str = "tegra210-nintendo-switch.dts";
    std::fs::copy(&dts_src, dts_dir.join(DTS_NAME))
        .context("failed to install the Switch device tree into the kernel tree")?;

    let makefile = dts_dir.join("Makefile");
    let text = std::fs::read_to_string(&makefile).context("failed to read the dts Makefile")?;
    if !text.contains("tegra210-nintendo-switch.dtb") {
        append_line(
            &makefile,
            "dtb-$(CONFIG_ARCH_TEGRA_210_SOC) += tegra210-nintendo-switch.dtb\n",
        )?;
    }

    Ok(())
}

/// Install the out-of-tree `switch-fb` driver into a kernel tree.
///
/// The driver and its Kconfig/Makefile hooks are not upstream, so a fresh
/// clone would silently ignore `CONFIG_SWITCH_FB`. This makes the build
/// idempotent and reproducible.
fn install_switch_fb(kernel_src: &Path) -> Result<()> {
    let project_root = std::env::current_dir()?;
    let driver_src = project_root.join("device-tree/switch-fb.c");
    if !driver_src.exists() {
        println!(
            "Warning: {} not found; switch-fb will not be built",
            driver_src.display()
        );
        return Ok(());
    }

    let fbdev = kernel_src.join("drivers/video/fbdev");
    std::fs::copy(&driver_src, fbdev.join("switch-fb.c"))
        .context("failed to install switch-fb.c into the kernel tree")?;

    const STANZA: &str = "\nconfig SWITCH_FB\n\
        \ttristate \"Nintendo Switch bootloader framebuffer\"\n\
        \tdepends on FB\n\
        \thelp\n\
        \t  Minimal framebuffer driver for the Nintendo Switch.\n\
        \t  Maps an already-initialized bootloader framebuffer as /dev/fb0.\n";

    let kconfig = fbdev.join("Kconfig");
    let text = std::fs::read_to_string(&kconfig).context("failed to read fbdev Kconfig")?;
    if let Some(updated) = replace_kconfig_stanza(&text, "config SWITCH_FB", STANZA) {
        std::fs::write(&kconfig, updated)?;
    } else {
        append_line(&kconfig, STANZA)?;
    }

    let makefile = fbdev.join("Makefile");
    let text = std::fs::read_to_string(&makefile).context("failed to read fbdev Makefile")?;
    if !text.contains("switch-fb.o") {
        append_line(&makefile, "obj-$(CONFIG_SWITCH_FB) += switch-fb.o\n")?;
    }

    Ok(())
}

/// Apply every `patches/kernel-*.patch` to the kernel tree, skipping any that
/// are already applied.
///
/// The device tree, the panel driver and switch-fb are *installed* -- whole
/// files copied in. But some changes have to edit an upstream file in place
/// (the DSI hotplug fix, the touchscreen quirks), and those are kept as patches
/// here. Without this step a re-cloned kernel tree would quietly lose them,
/// which is precisely the class of silent failure this project exists to
/// document.
fn install_switch_patches(kernel_src: &Path) -> Result<()> {
    let project_root = std::env::current_dir()?;
    let patches_dir = project_root.join("patches");
    if !patches_dir.is_dir() {
        return Ok(());
    }

    let mut patches: Vec<PathBuf> = std::fs::read_dir(&patches_dir)
        .with_context(|| format!("failed to read {}", patches_dir.display()))?
        .filter_map(|entry| entry.ok().map(|entry| entry.path()))
        .filter(|path| {
            path.file_name()
                .and_then(|name| name.to_str())
                .is_some_and(|name| name.starts_with("kernel-") && name.ends_with(".patch"))
        })
        .collect();
    patches.sort();

    for patch in patches {
        let name = patch
            .file_name()
            .map(|name| name.to_string_lossy().into_owned())
            .unwrap_or_default();

        // `git apply --check --reverse` succeeds only when the tree already
        // contains the change, which is how this stays idempotent: builds
        // after the first one (and hand-edited trees) skip straight through.
        let already_applied = Command::new("git")
            .args(["apply", "--check", "--reverse"])
            .arg(&patch)
            .current_dir(kernel_src)
            .output()
            .map(|out| out.status.success())
            .unwrap_or(false);

        if already_applied {
            println!("  {name}: already applied");
            continue;
        }

        let output = Command::new("git")
            .args(["apply", "--whitespace=nowarn"])
            .arg(&patch)
            .current_dir(kernel_src)
            .output()
            .with_context(|| format!("failed to run git apply for {name}"))?;

        if !output.status.success() {
            bail!(
                "failed to apply {name} to {}:\n{}",
                kernel_src.display(),
                String::from_utf8_lossy(&output.stderr)
            );
        }

        println!("  {name}: applied");
    }

    Ok(())
}

/// Install the Switch's panel driver into the kernel tree.
///
/// This is the JDI LPM062M326A driver ported from switchroot's mainline patch
/// 0010 (see `reference/switchroot-mainline/`). The vendor kernel keeps the
/// panel's timings and init sequence in C, not in the device tree, so this had
/// to come from their source rather than be derived.
fn install_switch_panel(kernel_src: &Path) -> Result<()> {
    let project_root = std::env::current_dir()?;
    let driver_src = project_root.join("device-tree/panel-jdi-lpm062m326a.c");
    if !driver_src.exists() {
        println!(
            "Warning: {} not found; the Switch panel driver will not be built",
            driver_src.display()
        );
        return Ok(());
    }

    let panel_dir = kernel_src.join("drivers/gpu/drm/panel");
    std::fs::copy(&driver_src, panel_dir.join("panel-jdi-lpm062m326a.c"))
        .context("failed to install the panel driver into the kernel tree")?;

    const STANZA: &str = "\nconfig DRM_PANEL_JDI_LPM062M326A\n\
        \ttristate \"JDI LPM062M326A 720x1280 DSI panel (Nintendo Switch)\"\n\
        \tdepends on DRM && DRM_MIPI_DSI\n\
        \thelp\n\
        \t  The 720x1280 panel used on the Nintendo Switch. Ported from\n\
        \t  switchroot's mainline patch 0010; timings and init sequence are\n\
        \t  theirs.\n";

    let kconfig = panel_dir.join("Kconfig");
    let text = std::fs::read_to_string(&kconfig).context("failed to read panel Kconfig")?;
    if let Some(updated) =
        replace_kconfig_stanza(&text, "config DRM_PANEL_JDI_LPM062M326A", STANZA)
    {
        std::fs::write(&kconfig, updated)?;
    } else {
        append_line(&kconfig, STANZA)?;
    }

    let makefile = panel_dir.join("Makefile");
    let text = std::fs::read_to_string(&makefile).context("failed to read panel Makefile")?;
    if !text.contains("panel-jdi-lpm062m326a.o") {
        append_line(
            &makefile,
            "obj-$(CONFIG_DRM_PANEL_JDI_LPM062M326A) += panel-jdi-lpm062m326a.o\n",
        )?;
    }

    Ok(())
}

/// Install the Joy-Con rails driver into the kernel tree.
///
/// The Switch's Joy-Con rails are UARTs and the controller speaks HID over them.
/// Mainline's `hid-nintendo` does not cover that -- it handles the USB and
/// Bluetooth paths. This is switchroot's mainline patch 0016, installed as a
/// whole file the way the panel driver is, so it is reproducible: an earlier
/// revision applied it by hand and nothing recreated it, which is the same drift
/// that silently lost the boot markers.
///
/// Deliberately **not** included: switchroot's patch 0019 (`hid-joycon.c`). That
/// driver binds `HID_USB_DEVICE` / `HID_BLUETOOTH_DEVICE` Nintendo devices --
/// the USB and Bluetooth paths -- which mainline's `hid-nintendo.c` already
/// covers, and it does nothing for the rails. Shipping both would be two
/// drivers for one job.
fn install_switch_joycon(kernel_src: &Path) -> Result<()> {
    let project_root = std::env::current_dir()?;
    let driver_src = project_root.join("drivers/joycon.c");
    if !driver_src.exists() {
        println!(
            "Warning: {} not found; Joy-Con input will not be built",
            driver_src.display()
        );
        return Ok(());
    }

    let dir = kernel_src.join("drivers/input/joystick");
    std::fs::copy(&driver_src, dir.join("joycon.c"))
        .context("failed to install the Joy-Con driver into the kernel tree")?;

    const STANZA: &str = "\nconfig JOYSTICK_JOYCON\n\
        \ttristate \"Nintendo Switch Joy-Con rails\"\n\
        \tselect SERIAL_DEV_BUS\n\
        \tselect POWER_SUPPLY\n\
        \tselect CRC8\n\
        \thelp\n\
        \t  Say Y here if you have one or more Joy-Con connected over the\n\
        \t  Switch's rail UARTs. Ported from switchroot's mainline patch 0016.\n\
        \n\
        \t  To compile this driver as a module, choose M here: the module\n\
        \t  will be called joycon.\n";

    let kconfig = dir.join("Kconfig");
    let text = std::fs::read_to_string(&kconfig).context("failed to read joystick Kconfig")?;
    if let Some(updated) = replace_kconfig_stanza(&text, "config JOYSTICK_JOYCON", STANZA) {
        std::fs::write(&kconfig, updated)?;
    } else {
        append_line(&kconfig, STANZA)?;
    }

    let makefile = dir.join("Makefile");
    let text = std::fs::read_to_string(&makefile).context("failed to read joystick Makefile")?;
    if !text.contains("joycon.o") {
        append_line(&makefile, "obj-$(CONFIG_JOYSTICK_JOYCON)\t\t+= joycon.o\n")?;
    }

    Ok(())
}

/// Replace an existing Kconfig stanza (up to the next top-level keyword) or
/// return `None` when the symbol is not present.
fn replace_kconfig_stanza(text: &str, marker: &str, stanza: &str) -> Option<String> {
    let start = text.find(marker)?;
    let line_start = text[..start].rfind('\n').map(|i| i + 1).unwrap_or(start);
    let rest = &text[start..];
    // A new stanza begins at any line that starts with one of these keywords.
    let end = rest
        .match_indices('\n')
        .skip(1)
        .map(|(i, _)| i + 1)
        .find(|&i| {
            let line = rest[i..].lines().next().unwrap_or("");
            line.starts_with("config ")
                || line.starts_with("source ")
                || line.starts_with("menu")
                || line.starts_with("endmenu")
        })
        .unwrap_or(rest.len());
    let mut out = String::with_capacity(text.len() + stanza.len());
    out.push_str(&text[..line_start]);
    out.push_str(stanza);
    out.push_str(&rest[end..]);
    Some(out)
}

fn append_line(path: &Path, line: &str) -> Result<()> {
    let mut file = std::fs::OpenOptions::new()
        .append(true)
        .open(path)
        .with_context(|| format!("failed to open {} for append", path.display()))?;
    file.write_all(line.as_bytes())?;
    Ok(())
}

/// Build device tree for Tegra X1
pub fn build_device_tree(build_dir: &Path) -> Result<()> {
    let dt_src = build_dir.join("device-tree");
    let dt_dst = build_dir.join("device-tree/compiled");
    
    std::fs::create_dir_all(&dt_dst)?;

    // Look for custom device tree in project source directory first
    let project_root = std::env::current_dir()?;
    let custom_dt_src = project_root.join("device-tree/tegra210-odnx02-a2.dts");
    let custom_dt_build = dt_src.join("tegra210-odnx02-a2.dts");
    
    // Copy custom DTS to build directory if it exists in project root
    if custom_dt_src.exists() && !custom_dt_build.exists() {
        std::fs::copy(&custom_dt_src, &custom_dt_build)?;
        println!("Copied custom device tree source to build directory");
    }
    
    // Compile custom device tree
    let custom_dt = if custom_dt_build.exists() {
        custom_dt_build
    } else if custom_dt_src.exists() {
        custom_dt_src
    } else {
        PathBuf::new()
    };
    
    if !custom_dt.as_os_str().is_empty() {
        println!("Compiling custom device tree...");
        let output_dtb = dt_dst.join("tegra210-odnx02-a2.dtb");
        
        let status = Command::new("dtc")
            .args(&[
                "-I", "dts",
                "-O", "dtb",
                "-o", output_dtb.to_str().unwrap(),
                custom_dt.to_str().unwrap(),
            ])
            .status()
            .context("Failed to run dtc")?;
        
        if !status.success() {
            anyhow::bail!("dtc failed");
        }
        
        println!("Device tree compiled: {}", output_dtb.display());
    }

    // Copy pre-compiled device trees from kernel build
    let kernel_dtb = build_dir.join("kernel/src/arch/arm64/boot/dts/nvidia");
    if kernel_dtb.exists() {
        for entry in std::fs::read_dir(&kernel_dtb)? {
            let entry = entry?;
            if entry.path().extension().map_or(false, |e| e == "dtb") {
                std::fs::copy(entry.path(), dt_dst.join(entry.file_name()))?;
            }
        }
    }

    println!("Device tree build complete!");
    Ok(())
}

/// Run the kernel's `make` with a raised stack limit.
///
/// GNU make expands `$(foreach)` / `$(call)` by recursing on the C stack. The
/// kernel's `scripts/Makefile.modfinal` iterates over every `=m` symbol, and
/// the arm64 defconfig queues **over 1400 modules**, so that recursion
/// overflows the default 8 MiB stack. make then dies with SIGSEGV and the only
/// clue it leaves is:
///
/// ```text
/// make[1]: *** [Makefile:2118: modules] Segmentation fault (core dumped)
/// make: INTERNAL: Exiting with 22 jobserver tokens available; should be 24!
/// ```
///
/// which reads like a kernel or toolchain bug but is purely make's own stack
/// (a core dump of `/usr/bin/make -f ./scripts/Makefile.modfinal` shows garbage
/// frames full of `$$(...)` expansion text). Raising RLIMIT_STACK fixes it.
///
/// Try `unlimited` first, then a large finite value, in case the hard limit is
/// capped somewhere.
fn run_make(args: &[&str], dir: &Path) -> Result<()> {
    const RAISE_STACK: &str =
        "ulimit -s unlimited 2>/dev/null || ulimit -s 65536 2>/dev/null || true; exec \"$@\"";

    let mut cmd = Command::new("sh");
    cmd.current_dir(dir)
        .arg("-c")
        .arg(RAISE_STACK)
        .arg("sh") // becomes $0
        .arg("make") // becomes $1, so "$@" is make + args
        .args(args);

    let status = cmd.status().context("failed to execute make")?;
    if !status.success() {
        bail!(
            "make failed with status {}\n\
             If this is a SIGSEGV from make itself, check that the stack limit \
             is actually raisable: `ulimit -Hs`.",
            status
        );
    }
    Ok(())
}

/// Get number of CPUs
mod num_cpus {
    pub fn get() -> usize {
        std::thread::available_parallelism()
            .map(|n| n.get())
            .unwrap_or(4)
    }
}