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 { 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 = 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 = 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 { 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) } }