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