From 1d756c6023c7d389af409913d3f00c96246f159d Mon Sep 17 00:00:00 2001 From: Sungjoon Moon Date: Thu, 16 Jul 2026 04:02:27 +0900 Subject: [PATCH 1/6] defaults: restore package lists Co-Authored-By: Claude Fable 5 --- README.md | 17 +++ boards/blackhole/target-packages.txt | 2 - boards/k1/target-packages.txt | 9 -- boards/k230/target-packages.txt | 4 - boards/k3/target-packages.txt | 16 +- boards/ky-x1/target-packages.txt | 6 - boards/odroid-m1/target-packages.txt | 2 - boards/odroid-m1s/target-packages.txt | 2 - boards/odroid-m2/target-packages.txt | 2 - boards/pentium-mmx/target-packages.txt | 9 +- crossdev-stages/src/cli/image.rs | 12 +- crossdev-stages/src/cli/sandbox.rs | 4 +- crossdev-stages/src/cli/target.rs | 8 +- crossdev-stages/src/cli/util.rs | 7 +- crossdev-stages/src/error.rs | 9 ++ crossdev-stages/src/image.rs | 94 ++++-------- crossdev-stages/src/main.rs | 16 +- crossdev-stages/src/package_list.rs | 199 +++++++++++++++++++++++++ crossdev-stages/src/portage.rs | 48 +++--- crossdev-stages/src/sandbox.rs | 47 +++++- defaults/sandbox-packages.txt | 32 ++++ defaults/target-packages.txt | 39 +++++ docs/design.md | 33 +++- 23 files changed, 458 insertions(+), 159 deletions(-) delete mode 100644 boards/blackhole/target-packages.txt delete mode 100644 boards/k1/target-packages.txt delete mode 100644 boards/k230/target-packages.txt delete mode 100644 boards/ky-x1/target-packages.txt delete mode 100644 boards/odroid-m1/target-packages.txt delete mode 100644 boards/odroid-m1s/target-packages.txt delete mode 100644 boards/odroid-m2/target-packages.txt create mode 100644 crossdev-stages/src/package_list.rs create mode 100644 defaults/sandbox-packages.txt create mode 100644 defaults/target-packages.txt diff --git a/README.md b/README.md index 377fb8a1..dad3cce5 100644 --- a/README.md +++ b/README.md @@ -120,12 +120,29 @@ eselect repository create crossdev Each board lives in `boards//` with: - `board.conf` -- variables read by Rust and bash scripts - `genimage.cfg` -- disk image layout +- `sandbox-packages.txt` -- extra host packages for the sandbox (optional) +- `sandbox-packages.use` -- USE flags for those packages (optional) +- `target-packages.txt` -- extra packages cross-emerged into the image (optional) - `pre-{step}.sh` -- runs before Rust default (optional) - `post-{step}.sh` -- runs after Rust default (optional) - `override-{step}.sh` -- replaces Rust default entirely (optional) Steps: `deps`, `checkout`, `bootloader`, `kernel`, `assemble`, `pack` +Package lists in `defaults/` apply to every sandbox +(`defaults/sandbox-packages.txt`) and every image +(`defaults/target-packages.txt`). The effective target set is +`defaults/target-packages.txt` UNION `boards//target-packages.txt` +MINUS the board's `-atom` lines (e.g. `-app-misc/fastfetch` drops a +default) -- every part is a plain file. Subtracting an atom not in the +set warns and does nothing; `-atom` lines in the defaults file itself +are an error. Heavy extras (mold, go, cmake, rust, iw, wpa_supplicant) +stay out of the defaults -- boards opt in via their own list. +List lines are `atom [keywords]` -- a keyword override (e.g. +`sys-boot/syslinux **`) lands in `etc/portage/package.accept_keywords/`. +Portage config files under `defaults/portage/` are overlaid onto the +sandbox's `etc/portage/` during prepare. + ### Build step execution ``` diff --git a/boards/blackhole/target-packages.txt b/boards/blackhole/target-packages.txt deleted file mode 100644 index b9f5fc48..00000000 --- a/boards/blackhole/target-packages.txt +++ /dev/null @@ -1,2 +0,0 @@ -# Packages for Blackhole target -net-misc/openssh diff --git a/boards/k1/target-packages.txt b/boards/k1/target-packages.txt deleted file mode 100644 index 2a6a932b..00000000 --- a/boards/k1/target-packages.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Packages to cross-install into the target sysroot for the K1 image - -# Services enabled by default -net-misc/openssh -app-admin/metalog -net-misc/ntp - -# Initramfs / boot -sys-apps/busybox diff --git a/boards/k230/target-packages.txt b/boards/k230/target-packages.txt deleted file mode 100644 index 6219278a..00000000 --- a/boards/k230/target-packages.txt +++ /dev/null @@ -1,4 +0,0 @@ -# Packages to cross-install into the target sysroot for K230 - -net-misc/openssh -sys-apps/busybox diff --git a/boards/k3/target-packages.txt b/boards/k3/target-packages.txt index b970abdf..2c7d07db 100644 --- a/boards/k3/target-packages.txt +++ b/boards/k3/target-packages.txt @@ -1,26 +1,14 @@ -# Packages to cross-install into the target sysroot for the K3 image +# K3 extras on top of defaults/target-packages.txt -# Services -net-misc/openssh -app-admin/metalog -net-misc/ntp - -# Initramfs / boot -sys-apps/busybox - -# Dev tools -app-editors/vim -dev-vcs/git +# Dev tools (git is in defaults) dev-debug/strace dev-debug/gdb net-misc/curl net-misc/wget # System tools -sys-process/htop sys-process/lsof sys-apps/file -app-misc/fastfetch app-misc/screen # Build systems diff --git a/boards/ky-x1/target-packages.txt b/boards/ky-x1/target-packages.txt deleted file mode 100644 index 5f90432d..00000000 --- a/boards/ky-x1/target-packages.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Packages to cross-install into the target sysroot for KY-X1 - -net-misc/openssh -app-admin/metalog -net-misc/ntp -sys-apps/busybox diff --git a/boards/odroid-m1/target-packages.txt b/boards/odroid-m1/target-packages.txt deleted file mode 100644 index d55b4501..00000000 --- a/boards/odroid-m1/target-packages.txt +++ /dev/null @@ -1,2 +0,0 @@ -net-misc/openssh -sys-apps/busybox diff --git a/boards/odroid-m1s/target-packages.txt b/boards/odroid-m1s/target-packages.txt deleted file mode 100644 index d55b4501..00000000 --- a/boards/odroid-m1s/target-packages.txt +++ /dev/null @@ -1,2 +0,0 @@ -net-misc/openssh -sys-apps/busybox diff --git a/boards/odroid-m2/target-packages.txt b/boards/odroid-m2/target-packages.txt deleted file mode 100644 index d55b4501..00000000 --- a/boards/odroid-m2/target-packages.txt +++ /dev/null @@ -1,2 +0,0 @@ -net-misc/openssh -sys-apps/busybox diff --git a/boards/pentium-mmx/target-packages.txt b/boards/pentium-mmx/target-packages.txt index c98692a4..1d28cbb9 100644 --- a/boards/pentium-mmx/target-packages.txt +++ b/boards/pentium-mmx/target-packages.txt @@ -1,12 +1,7 @@ -# Packages cross-installed into the i586 target sysroot +# pentium-mmx extras on top of defaults/target-packages.txt -# Network & remote access (services enabled in BOOT_SERVICES) -net-misc/openssh +# Network (service enabled in BOOT_SERVICES) net-misc/dhcpcd # Disk utilities required by the grow-rootfs first-boot service -sys-block/parted sys-fs/e2fsprogs - -# Minimal shell utilities -sys-apps/busybox diff --git a/crossdev-stages/src/cli/image.rs b/crossdev-stages/src/cli/image.rs index c2c28850..33ca502f 100644 --- a/crossdev-stages/src/cli/image.rs +++ b/crossdev-stages/src/cli/image.rs @@ -8,6 +8,7 @@ pub async fn run( ws: &Workspace, cmd: ImageCmd, boards_root: &Utf8Path, + defaults_root: &Utf8Path, mirror: Option<&str>, dry_run: bool, ) -> Result<()> { @@ -72,6 +73,7 @@ pub async fn run( sandbox.as_deref(), &board_cfg.arch, &board_cfg, + defaults_root, mirror, None, ) @@ -93,7 +95,15 @@ pub async fn run( } else { Some(steps.as_slice()) }; - image::build(ws, &sb, &tgt, &board_cfg, boards_root, steps_opt)?; + image::build( + ws, + &sb, + &tgt, + &board_cfg, + boards_root, + defaults_root, + steps_opt, + )?; } ImageCmd::Prune => { let builds = ws.list_builds()?; diff --git a/crossdev-stages/src/cli/sandbox.rs b/crossdev-stages/src/cli/sandbox.rs index 86ac35fa..c7fedc46 100644 --- a/crossdev-stages/src/cli/sandbox.rs +++ b/crossdev-stages/src/cli/sandbox.rs @@ -7,6 +7,7 @@ pub async fn run( ws: &Workspace, cmd: SandboxCmd, boards_root: &camino::Utf8Path, + defaults_root: &camino::Utf8Path, mirror: Option<&str>, ) -> Result<()> { match cmd { @@ -33,7 +34,7 @@ pub async fn run( SandboxCmd::Prepare { name, bare } => { let dir = ws.resolve_sandbox(name.as_deref())?; let sb = sandbox::Sandbox::open(dir)?; - sb.prepare(mirror, bare)?; + sb.prepare(mirror, defaults_root, bare)?; } SandboxCmd::Crossdev { arch, @@ -51,6 +52,7 @@ pub async fn run( name.as_deref(), &arch, &board_cfg, + defaults_root, mirror, gcc_version.as_deref(), ) diff --git a/crossdev-stages/src/cli/target.rs b/crossdev-stages/src/cli/target.rs index fd6f54cc..5bc5bde3 100644 --- a/crossdev-stages/src/cli/target.rs +++ b/crossdev-stages/src/cli/target.rs @@ -2,7 +2,7 @@ use crate::cli::util::{default_board_config, ensure_crossdev, ensure_target}; use crate::cli::TargetCmd; use crate::error::Result; use crate::{container, stage, target, workspace::Workspace}; -use camino::Utf8PathBuf; +use camino::{Utf8Path, Utf8PathBuf}; pub async fn run( ws: &Workspace, @@ -10,6 +10,7 @@ pub async fn run( sandbox: Option, target_name: Option, cmd: TargetCmd, + defaults_root: &Utf8Path, mirror: Option<&str>, ) -> Result<()> { match cmd { @@ -51,6 +52,7 @@ pub async fn run( sandbox.as_deref(), &resolved_arch, &default_board_config(&resolved_arch), + defaults_root, mirror, None, ) @@ -62,6 +64,7 @@ pub async fn run( target_name.as_deref(), arch.as_deref(), sandbox.as_deref(), + defaults_root, mirror, ) .await?; @@ -73,6 +76,7 @@ pub async fn run( target_name.as_deref(), arch.as_deref(), sandbox.as_deref(), + defaults_root, mirror, ) .await?; @@ -84,6 +88,7 @@ pub async fn run( target_name.as_deref(), arch.as_deref(), sandbox.as_deref(), + defaults_root, mirror, ) .await?; @@ -96,6 +101,7 @@ pub async fn run( target_name.as_deref(), arch.as_deref(), sandbox.as_deref(), + defaults_root, mirror, ) .await?; diff --git a/crossdev-stages/src/cli/util.rs b/crossdev-stages/src/cli/util.rs index 169f15ed..85a94240 100644 --- a/crossdev-stages/src/cli/util.rs +++ b/crossdev-stages/src/cli/util.rs @@ -1,3 +1,5 @@ +use camino::Utf8Path; + use crate::error::Result; use crate::workspace::Workspace; use crate::{board, error, sandbox, stage, target}; @@ -9,6 +11,7 @@ pub async fn ensure_crossdev( sandbox_name: Option<&str>, arch: &str, board_cfg: &board::BoardConfig, + defaults_root: &Utf8Path, mirror: Option<&str>, gcc_version: Option<&str>, ) -> Result { @@ -27,7 +30,7 @@ pub async fn ensure_crossdev( } }; let sb = sandbox::Sandbox::open(sd)?; - sb.prepare(mirror, false)?; + sb.prepare(mirror, defaults_root, false)?; sb.setup_crossdev(arch, board_cfg, gcc_version)?; Ok(sb) } @@ -39,6 +42,7 @@ pub async fn ensure_target( target_name: Option<&str>, arch_override: Option<&str>, sandbox_name: Option<&str>, + defaults_root: &Utf8Path, mirror: Option<&str>, ) -> Result<(target::Target, sandbox::Sandbox)> { let (tgt, resolved_arch) = match ws.resolve_target(target_name) { @@ -67,6 +71,7 @@ pub async fn ensure_target( sandbox_name, &resolved_arch, &default_board_config(&resolved_arch), + defaults_root, mirror, None, ) diff --git a/crossdev-stages/src/error.rs b/crossdev-stages/src/error.rs index 56de69f2..0e7115b5 100644 --- a/crossdev-stages/src/error.rs +++ b/crossdev-stages/src/error.rs @@ -26,6 +26,15 @@ pub enum Error { #[error("command failed (exit {code}): {reason}")] CommandFailed { code: i32, reason: String }, + #[error("required package list not found: {0}")] + PackageListNotFound(camino::Utf8PathBuf), + + #[error("subtraction line '-{atom}' not allowed in defaults list {file} (only board lists may subtract)")] + SubtractionInDefaults { + file: camino::Utf8PathBuf, + atom: String, + }, + #[error("io error: {0}")] Io(#[from] std::io::Error), } diff --git a/crossdev-stages/src/image.rs b/crossdev-stages/src/image.rs index 0f817d78..dd44156e 100644 --- a/crossdev-stages/src/image.rs +++ b/crossdev-stages/src/image.rs @@ -133,74 +133,39 @@ fn default_deps( target: &Target, board: &BoardConfig, boards_root: &Utf8Path, + defaults_root: &Utf8Path, ) -> Result<()> { - let sandbox_pkgs = boards_root.join(&board.name).join("sandbox-packages.txt"); - if sandbox_pkgs.exists() { + // Sandbox extras: defaults are already installed during prepare; only the + // board's own extras (e.g. grub for pentium-mmx) need emerging here. + // merge() with an empty base means a `-atom` line here can only cancel + // the board's own extras, never uninstall a prepare-time default. + let board_dir = boards_root.join(&board.name); + let board_sandbox = crate::package_list::merge( + Vec::new(), + crate::package_list::read_optional(&board_dir.join("sandbox-packages.txt"))?, + ); + if !board_sandbox.is_empty() { + let portage_dir = sandbox.dir.join("etc/portage"); + crate::package_list::write_accept_keywords(&board_sandbox, &portage_dir)?; + // USE overrides from sandbox-packages.use, e.g. "sys-boot/grub grub_platforms_pc" + crate::package_list::write_package_use( + &board_dir.join("sandbox-packages.use"), + &portage_dir, + )?; let host_runner = board_runner(sandbox, board); let portage = Portage::new(&host_runner); - let content = std::fs::read_to_string(&sandbox_pkgs)?; - let lines: Vec<&str> = content - .lines() - .map(str::trim) - .filter(|l| !l.is_empty() && !l.starts_with('#')) - .collect(); - - // Write package.accept_keywords entries for packages with keyword overrides. - // Format: "atom [keywords]" — e.g. "sys-boot/syslinux **" or "dev-libs/foo ~amd64" - let accept_keywords_dir = sandbox.dir.join("etc/portage/package.accept_keywords"); - std::fs::create_dir_all(&accept_keywords_dir)?; - let mut pkgs: Vec<&str> = Vec::new(); - for line in &lines { - let parts: Vec<&str> = line.splitn(2, char::is_whitespace).collect(); - let atom = parts[0]; - pkgs.push(atom); - if parts.len() > 1 { - let keywords = parts[1].trim(); - let safe_name = atom.replace('/', "_"); - std::fs::write( - accept_keywords_dir.join(&safe_name), - format!("{atom} {keywords}\n"), - )?; - } - } - - // Write package.use entries from sandbox-packages.use. - // Format: "atom use_flags..." — e.g. "sys-boot/syslinux bios -uefi" - let sandbox_use = boards_root.join(&board.name).join("sandbox-packages.use"); - if sandbox_use.exists() { - let use_dir = sandbox.dir.join("etc/portage/package.use"); - std::fs::create_dir_all(&use_dir)?; - let use_content = std::fs::read_to_string(&sandbox_use)?; - for line in use_content.lines() { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - continue; - } - let parts: Vec<&str> = line.splitn(2, char::is_whitespace).collect(); - let atom = parts[0]; - let safe_name = atom.replace('/', "_"); - std::fs::write(use_dir.join(&safe_name), format!("{line}\n"))?; - } - } - - if !pkgs.is_empty() { - portage.emerge(&pkgs)?; - } + portage.emerge(&crate::package_list::atoms(&board_sandbox))?; } - let target_pkgs = boards_root.join(&board.name).join("target-packages.txt"); - if target_pkgs.exists() { - let content = std::fs::read_to_string(&target_pkgs)?; - let pkgs: Vec<&str> = content - .lines() - .map(str::trim) - .filter(|l| !l.is_empty() && !l.starts_with('#')) - .collect(); - if !pkgs.is_empty() { - let target_runner = board_runner(sandbox, board).with_target(&target.dir); - let portage = Portage::new(&target_runner); - portage.cross_emerge(&board.chost(), &pkgs)?; - } + // Target packages: defaults UNION board extras MINUS board `-atom` lines. + let target_pkgs = crate::package_list::merge( + crate::package_list::read_required(&defaults_root.join("target-packages.txt"))?, + crate::package_list::read_optional(&board_dir.join("target-packages.txt"))?, + ); + if !target_pkgs.is_empty() { + let target_runner = board_runner(sandbox, board).with_target(&target.dir); + let portage = Portage::new(&target_runner); + portage.cross_emerge(&board.chost(), &crate::package_list::atoms(&target_pkgs))?; } Ok(()) @@ -407,6 +372,7 @@ pub fn build( target: &Target, board: &BoardConfig, boards_root: &Utf8Path, + defaults_root: &Utf8Path, steps: Option<&[String]>, ) -> Result<()> { let bld = Build::create(ws, &board.name)?; @@ -442,7 +408,7 @@ pub fn build( let result = match *step { "deps" => run_step("deps", "deps", &bld, &runner, boards_root, board, |_r| { - default_deps(_r, sandbox, target, board, boards_root) + default_deps(_r, sandbox, target, board, boards_root, defaults_root) }), "checkout" => run_step( "checkout", diff --git a/crossdev-stages/src/main.rs b/crossdev-stages/src/main.rs index 78ac5247..6b0f8234 100644 --- a/crossdev-stages/src/main.rs +++ b/crossdev-stages/src/main.rs @@ -4,6 +4,7 @@ mod cli; mod container; mod error; mod image; +mod package_list; mod portage; mod sandbox; mod source_cache; @@ -27,11 +28,12 @@ async fn main() -> anyhow::Result<()> { let ws = crate::workspace::Workspace::open()?; ws.ensure_dirs()?; - let boards_root = { - let p = std::fs::canonicalize(cli.project_dir.join("boards")) - .unwrap_or_else(|_| cli.project_dir.join("boards")); - Utf8PathBuf::try_from(p).expect("boards path is not UTF-8") + let project_dir = { + let p = std::fs::canonicalize(&cli.project_dir).unwrap_or_else(|_| cli.project_dir.clone()); + Utf8PathBuf::try_from(p).expect("project path is not UTF-8") }; + let boards_root = project_dir.join("boards"); + let defaults_root = project_dir.join("defaults"); let mirror = cli.mirror.as_deref(); let dry_run = cli.dry_run; @@ -40,7 +42,7 @@ async fn main() -> anyhow::Result<()> { cli::stages::run(&ws, cmd, mirror).await?; } Commands::Sandbox(cmd) => { - cli::sandbox::run(&ws, cmd, &boards_root, mirror).await?; + cli::sandbox::run(&ws, cmd, &boards_root, &defaults_root, mirror).await?; } Commands::Target { arch, @@ -48,13 +50,13 @@ async fn main() -> anyhow::Result<()> { target, command, } => { - cli::target::run(&ws, arch, sandbox, target, command, mirror).await?; + cli::target::run(&ws, arch, sandbox, target, command, &defaults_root, mirror).await?; } Commands::Board(cmd) => { cli::board::run(&boards_root, cmd)?; } Commands::Image(cmd) => { - cli::image::run(&ws, cmd, &boards_root, mirror, dry_run).await?; + cli::image::run(&ws, cmd, &boards_root, &defaults_root, mirror, dry_run).await?; } Commands::Maint(cmd) => { cli::maint::run(&ws, cmd, &boards_root, dry_run)?; diff --git a/crossdev-stages/src/package_list.rs b/crossdev-stages/src/package_list.rs new file mode 100644 index 00000000..1938cc7e --- /dev/null +++ b/crossdev-stages/src/package_list.rs @@ -0,0 +1,199 @@ +//! Read portage atom lists from text files (`#` comments, blank lines skipped). +//! +//! Line format: `atom [keywords]` — e.g. `sys-boot/syslinux **` or +//! `dev-libs/foo ~amd64`. Keyword overrides are written to +//! `package.accept_keywords/` via [`write_accept_keywords`]. +//! +//! Board lists may subtract from the defaults with a `-atom` line +//! (e.g. `-app-editors/vim`); see [`merge`]. Defaults files define the +//! base set, so `-atom` lines in them are rejected by [`read_required`]. + +use std::io::ErrorKind; + +use camino::Utf8Path; + +use crate::error::{Error, Result}; + +#[derive(Debug)] +pub struct Entry { + pub atom: String, + /// Optional ACCEPT_KEYWORDS override from the `atom [keywords]` syntax. + pub keywords: Option, + /// `-atom` line: remove `atom` from the merged set (board lists only). + pub remove: bool, +} + +/// Read a defaults package list. Errors if the file is missing or contains +/// `-atom` subtraction lines (defaults define the base set; only board +/// lists may subtract). +pub fn read_required(path: &Utf8Path) -> Result> { + match std::fs::read_to_string(path) { + Ok(content) => parse_base(&content, path), + Err(e) if e.kind() == ErrorKind::NotFound => Err(Error::PackageListNotFound(path.into())), + Err(e) => Err(e.into()), + } +} + +fn parse_base(content: &str, path: &Utf8Path) -> Result> { + let entries = parse(content); + if let Some(e) = entries.iter().find(|e| e.remove) { + return Err(Error::SubtractionInDefaults { + file: path.into(), + atom: e.atom.clone(), + }); + } + Ok(entries) +} + +/// Merge board `overlay` entries into the `base` defaults, in order: +/// additions append, `-atom` entries remove the atom from the set built so +/// far. Subtracting an atom that is not in the set is a no-op with a +/// warning (not an error), so a board list survives a default being +/// dropped from `defaults/`. +pub fn merge(mut base: Vec, overlay: Vec) -> Vec { + for e in overlay { + if e.remove { + let before = base.len(); + base.retain(|b| b.atom != e.atom); + if base.len() == before { + tracing::warn!("package list: -{} removes nothing (not in set)", e.atom); + } + } else { + base.push(e); + } + } + base +} + +/// Read a package list if the file exists, otherwise return an empty list. +pub fn read_optional(path: &Utf8Path) -> Result> { + match std::fs::read_to_string(path) { + Ok(content) => Ok(parse(&content)), + Err(e) if e.kind() == ErrorKind::NotFound => Ok(Vec::new()), + Err(e) => Err(e.into()), + } +} + +/// Borrow just the atoms, e.g. for an emerge command line. +pub fn atoms(entries: &[Entry]) -> Vec<&str> { + entries.iter().map(|e| e.atom.as_str()).collect() +} + +/// Write `package.accept_keywords/` entries under `portage_dir` (an +/// `etc/portage` directory) for atoms with keyword overrides. +pub fn write_accept_keywords(entries: &[Entry], portage_dir: &Utf8Path) -> Result<()> { + let dir = portage_dir.join("package.accept_keywords"); + for e in entries { + if let Some(keywords) = &e.keywords { + std::fs::create_dir_all(&dir)?; + let safe_name = e.atom.replace('/', "_"); + std::fs::write(dir.join(safe_name), format!("{} {keywords}\n", e.atom))?; + } + } + Ok(()) +} + +/// Copy `atom use_flags...` lines from `path` (if present) into +/// `portage_dir/package.use/`, one file per atom. +/// Format: e.g. `sys-boot/grub grub_platforms_pc -grub_platforms_efi-64`. +pub fn write_package_use(path: &Utf8Path, portage_dir: &Utf8Path) -> Result<()> { + let content = match std::fs::read_to_string(path) { + Ok(c) => c, + Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()), + Err(e) => return Err(e.into()), + }; + let dir = portage_dir.join("package.use"); + for line in content.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + std::fs::create_dir_all(&dir)?; + let atom = line.split_whitespace().next().unwrap_or(line); + let safe_name = atom.replace('/', "_"); + std::fs::write(dir.join(safe_name), format!("{line}\n"))?; + } + Ok(()) +} + +fn parse(content: &str) -> Vec { + content + .lines() + .map(str::trim) + .filter(|l| !l.is_empty() && !l.starts_with('#')) + .map(|line| { + let mut parts = line.splitn(2, char::is_whitespace); + let atom = parts.next().unwrap_or(line); + let (atom, remove) = match atom.strip_prefix('-') { + Some(stripped) => (stripped.to_string(), true), + None => (atom.to_string(), false), + }; + let keywords = parts + .next() + .map(|k| k.trim().to_string()) + .filter(|k| !k.is_empty()); + Entry { + atom, + keywords, + remove, + } + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn atoms_owned(entries: &[Entry]) -> Vec { + entries.iter().map(|e| e.atom.clone()).collect() + } + + #[test] + fn parse_subtraction_line() { + let entries = parse("-app-editors/vim\nsys-process/htop\n"); + assert_eq!(entries[0].atom, "app-editors/vim"); + assert!(entries[0].remove); + assert_eq!(entries[1].atom, "sys-process/htop"); + assert!(!entries[1].remove); + } + + #[test] + fn merge_subtracts_default() { + let base = parse("app-editors/vim\nsys-process/htop\n"); + let overlay = parse("-app-editors/vim\nnet-analyzer/nmap\n"); + let merged = merge(base, overlay); + assert_eq!( + atoms_owned(&merged), + vec!["sys-process/htop", "net-analyzer/nmap"] + ); + } + + #[test] + fn merge_subtracting_unlisted_atom_is_noop() { + // Documented behavior: warning, not an error — the merged set is + // unchanged if the subtracted atom was never in it. + let base = parse("app-editors/vim\n"); + let overlay = parse("-net-misc/curl\n"); + let merged = merge(base, overlay); + assert_eq!(atoms_owned(&merged), vec!["app-editors/vim"]); + } + + #[test] + fn defaults_reject_subtraction() { + let path = Utf8Path::new("defaults/target-packages.txt"); + let err = parse_base("net-misc/openssh\n-app-editors/vim\n", path).unwrap_err(); + assert!(matches!( + err, + Error::SubtractionInDefaults { ref atom, .. } if atom == "app-editors/vim" + )); + } + + #[test] + fn keywords_still_parse() { + let entries = parse("sys-boot/syslinux **\n"); + assert_eq!(entries[0].atom, "sys-boot/syslinux"); + assert_eq!(entries[0].keywords.as_deref(), Some("**")); + assert!(!entries[0].remove); + } +} diff --git a/crossdev-stages/src/portage.rs b/crossdev-stages/src/portage.rs index 0f185455..263be584 100644 --- a/crossdev-stages/src/portage.rs +++ b/crossdev-stages/src/portage.rs @@ -146,12 +146,6 @@ impl<'a> Portage<'a> { Ok(()) } - /// Emerge packages from binary packages only (`-G`). - pub fn emerge_binary(&self, packages: &[&str]) -> Result<()> { - let pkgs = packages.join(" "); - self.runner.run(&format!("emerge -G {pkgs}")) - } - /// Emerge packages, using binary if available (`-b -k`). pub fn emerge(&self, packages: &[&str]) -> Result<()> { let pkgs = packages.join(" "); @@ -202,32 +196,28 @@ pub fn sync_portage_tree(runner: &SandboxRunner) -> Result<()> { } /// Install all host-side dependencies required for cross-compilation. -pub fn install_host_deps(runner: &SandboxRunner) -> Result<()> { +/// +/// Reads the package list from `/sandbox-packages.txt`. Per-board +/// extras (boards//sandbox-packages.txt) are emerged separately during +/// image build via `image::default_deps`. Keyword overrides (`atom [keywords]` +/// lines) are written to `/package.accept_keywords/`. +pub fn install_host_deps( + runner: &SandboxRunner, + defaults_root: &Utf8Path, + portage_dir: &Utf8Path, +) -> Result<()> { sync_portage_tree(runner)?; let portage = Portage::new(runner); - let bin_packages = ["app-arch/zstd", "app-arch/bzip2", "app-arch/xz-utils"]; - tracing::info!("Installing binary packages…"); - portage.emerge_binary(&bin_packages)?; - - let packages = [ - "sys-devel/crossdev", - "sys-devel/bc", - "sys-apps/merge-usr", - "dev-vcs/git", - "dev-embedded/u-boot-tools", - "sys-apps/dtc", - "sys-kernel/dracut", - "sys-apps/busybox", - "sys-fs/genimage", - "sys-fs/dosfstools", - "sys-fs/mtools", - "app-eselect/eselect-repository", - "dev-lang/rust", - "dev-python/pyelftools", - ]; - tracing::info!("Installing build dependencies…"); - portage.emerge(&packages)?; + let path = defaults_root.join("sandbox-packages.txt"); + let packages = crate::package_list::read_required(&path)?; + crate::package_list::write_accept_keywords(&packages, portage_dir)?; + + tracing::info!( + "Installing host build dependencies ({} packages)…", + packages.len() + ); + portage.emerge(&crate::package_list::atoms(&packages))?; tracing::info!("Installing Rust ldconfig…"); runner.run("cargo install --root /usr/local ldconfig")?; diff --git a/crossdev-stages/src/sandbox.rs b/crossdev-stages/src/sandbox.rs index e9310134..7f252a23 100644 --- a/crossdev-stages/src/sandbox.rs +++ b/crossdev-stages/src/sandbox.rs @@ -48,7 +48,7 @@ impl Sandbox { /// Idempotent: skips if `.prepared` marker exists (or `.prepared-bare` when `bare`). /// /// With `bare`, writes `make.conf` and syncs the portage tree but does not emerge packages. - pub fn prepare(&self, mirror: Option<&str>, bare: bool) -> Result<()> { + pub fn prepare(&self, mirror: Option<&str>, defaults_root: &Utf8Path, bare: bool) -> Result<()> { if self.dir.join(".prepared").exists() { tracing::info!("Sandbox already prepared, skipping."); return Ok(()); @@ -67,13 +67,22 @@ impl Sandbox { } .write(&self.dir.join("etc/portage"))?; + // Overlay any user-provided portage config from defaults/portage/ + // onto the sandbox's /etc/portage/. Lets boards declare USE flags + // (e.g. sys-fs/dosfstools compat) and other portage tweaks as files + // rather than hardcoded strings. + let portage_src = defaults_root.join("portage"); + if portage_src.is_dir() { + copy_tree(&portage_src, &self.dir.join("etc/portage"))?; + } + if bare { sync_portage_tree(&self.runner())?; std::fs::write(self.dir.join(".prepared-bare"), "")?; tracing::info!("Sandbox bare-prepared."); } else { tracing::info!("Installing host dependencies…"); - install_host_deps(&self.runner())?; + install_host_deps(&self.runner(), defaults_root, &self.dir.join("etc/portage"))?; std::fs::write(self.dir.join(".prepared"), "")?; let _ = std::fs::remove_file(self.dir.join(".prepared-bare")); tracing::info!("Sandbox prepared."); @@ -355,9 +364,7 @@ impl Sandbox { let Some(ref platforms) = board.grub_platforms else { return Ok(()); }; - let grub_mods = self - .dir - .join(format!("usr/{chost}/usr/lib/grub/i386-pc")); + let grub_mods = self.dir.join(format!("usr/{chost}/usr/lib/grub/i386-pc")); if grub_mods.exists() { return Ok(()); } @@ -368,7 +375,9 @@ impl Sandbox { .split_whitespace() .map(|p| format!("grub_platforms_{p}")) .collect(); - let use_dir = self.dir.join(format!("usr/{chost}/etc/portage/package.use")); + let use_dir = self + .dir + .join(format!("usr/{chost}/etc/portage/package.use")); std::fs::create_dir_all(&use_dir)?; std::fs::write( use_dir.join("grub"), @@ -513,3 +522,29 @@ pub struct SandboxInfo { pub prepared: bool, pub bare_prepared: bool, } + +/// Recursively copy directory `src` into `dst`, overwriting files. +/// Symlinks and other special entries are rejected. +fn copy_tree(src: &Utf8Path, dst: &Utf8Path) -> Result<()> { + std::fs::create_dir_all(dst)?; + for entry in std::fs::read_dir(src)? { + let entry = entry?; + let kind = entry.file_type()?; + let from = Utf8PathBuf::try_from(entry.path()).map_err(|e| Error::CommandFailed { + code: 1, + reason: e.to_string(), + })?; + let to = dst.join(from.file_name().unwrap_or_default()); + if kind.is_dir() { + copy_tree(&from, &to)?; + } else if kind.is_file() { + std::fs::copy(&from, &to)?; + } else { + return Err(Error::CommandFailed { + code: 1, + reason: format!("unsupported entry in portage overlay (symlink?): {from}"), + }); + } + } + Ok(()) +} diff --git a/defaults/sandbox-packages.txt b/defaults/sandbox-packages.txt new file mode 100644 index 00000000..a39ce2b2 --- /dev/null +++ b/defaults/sandbox-packages.txt @@ -0,0 +1,32 @@ +# Host build deps installed into every sandbox during prepare. +# Per-board extras can be added in boards//sandbox-packages.txt +# (additive — both lists are emerged). + +# Bootstrap: compression tools for portage tree snapshot +app-arch/zstd +app-arch/bzip2 +app-arch/xz-utils + +# Cross-toolchain + portage utilities +sys-devel/crossdev +sys-devel/bc +sys-apps/merge-usr +app-eselect/eselect-repository + +# Source / VCS +dev-vcs/git + +# Boot / firmware +dev-embedded/u-boot-tools +sys-apps/dtc +sys-kernel/dracut +sys-apps/busybox + +# Image generation +sys-fs/genimage +sys-fs/dosfstools +sys-fs/mtools + +# Languages used during build +dev-lang/rust +dev-python/pyelftools diff --git a/defaults/target-packages.txt b/defaults/target-packages.txt new file mode 100644 index 00000000..06655c99 --- /dev/null +++ b/defaults/target-packages.txt @@ -0,0 +1,39 @@ +# Default packages cross-emerged into every image's sysroot. +# Lifted from cross-stage.sh ADDITIONAL_PACKAGES, trimmed to light +# essentials. Per-board extras go in boards//target-packages.txt; +# a board can drop a default with a `-atom` line (e.g. `-app-misc/fastfetch`). +# +# Deliberately NOT here (boards opt in via their own list): +# - heavy toolchains: sys-devel/mold, dev-lang/go, dev-build/cmake, +# dev-lang/rust +# - wireless: net-wireless/iw, net-wireless/wpa_supplicant +# - app-misc/screen: cross-emerge fails at fowners root:utmp (utmp user +# missing from the cross-prefix passwd) — do not re-add here +# - dev-build/meson: PEP517 wheel build is fragile under cross-compilation +# (host/target python paths confused) — do not re-add here + +# Networking / remote access +net-misc/openssh +net-misc/ntp + +# Logging +app-admin/metalog + +# Initramfs / minimal userland +sys-apps/busybox + +# Basic interactive tools +app-editors/vim +sys-process/htop +app-misc/fastfetch + +# Disk utilities +sys-block/parted + +# Dev / diagnostics +dev-vcs/git +net-analyzer/nmap + +# Portage helpers (equery, genlop) +app-portage/gentoolkit +app-portage/genlop diff --git a/docs/design.md b/docs/design.md index 7d5c986b..282970c5 100644 --- a/docs/design.md +++ b/docs/design.md @@ -120,7 +120,7 @@ and per-package CFLAGS workarounds. Steps run in order (configurable via `BUILD_STEPS` in `board.conf`): ``` -deps Install extra sandbox packages + cross-emerge target-packages.txt. +deps Cross-emerge defaults + per-board package lists into the target. checkout Clone/update kernel, opensbi, u-boot source via git source cache. bootloader Build opensbi and/or u-boot inside the sandbox container. kernel Build Linux kernel + modules; install modules into target stage. @@ -143,6 +143,37 @@ sibling helpers. --- +## Package lists + +`defaults/` holds package lists applied everywhere: + +- `defaults/sandbox-packages.txt` — host build deps emerged during + `sandbox prepare` (required). +- `defaults/target-packages.txt` — packages cross-emerged into every + image's sysroot during `deps` (required). + +Per-board lists (`boards//sandbox-packages.txt`, `target-packages.txt`) +overlay the defaults. The effective target set is +`defaults/target-packages.txt` UNION `boards//target-packages.txt` +MINUS the board's `-atom` lines (e.g. `-app-misc/fastfetch`) — every part +is a plain file. Subtracting an atom not in the merged set warns and does +nothing; `-atom` lines in a defaults file are rejected with an error, since +defaults define the base and only boards subtract. Heavy per-board opt-ins +(mold, go, cmake, rust, iw, wpa_supplicant) are deliberately not defaults. +Sandbox defaults are installed at prepare time, so a `-atom` in a board's +`sandbox-packages.txt` can only cancel the board's own extras. + +List lines are `atom [keywords]`: an optional keyword override (e.g. +`sys-boot/syslinux **`) is written to `etc/portage/package.accept_keywords/` +before emerging. A board's `sandbox-packages.use` (`atom use_flags...` +lines) is written to `etc/portage/package.use/` the same way. + +Files under `defaults/portage/` are overlaid onto the sandbox's +`etc/portage/` during prepare. Prepare is idempotent (`.prepared` +marker), so later changes do not reach already-prepared sandboxes. + +--- + ## Sandboxing approach hakoniwa creates a user-namespace container (unshares Mount, User, Pid, Ipc, From e9f70b3933de2c4b92484aeaed0736f949cd62a4 Mon Sep 17 00:00:00 2001 From: Sungjoon Moon Date: Thu, 16 Jul 2026 04:02:27 +0900 Subject: [PATCH 2/6] restore defaults package lists Co-Authored-By: Claude Fable 5 From ea17c037c73c0387bde9920c17f056ab40bd5a62 Mon Sep 17 00:00:00 2001 From: Sungjoon Moon Date: Sat, 11 Jul 2026 16:21:13 +0900 Subject: [PATCH 3/6] gitignore: ignore vendor firmware blobs and per-board build artifacts Co-Authored-By: Claude Fable 5 --- .gitignore | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gitignore b/.gitignore index 5e6198d2..a63e8722 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,11 @@ target/ k230-build*.log *.log Cargo.lock + +# Vendor closed firmware blobs (NOT redistributable, see firmware/README.md) +boards/*/firmware/vendor/ +boards/zhihe-a210/firmware/*.bin + +# Build artifacts staged into bundle +boards/*/uboot_env.img +boards/*/gpt.img From 0c9edebc2825695c02c1408aad27672a2bb4ef67 Mon Sep 17 00:00:00 2001 From: Sungjoon Moon Date: Sat, 11 Jul 2026 16:21:53 +0900 Subject: [PATCH 4/6] defaults: grow-rootfs handles in-use partition + GPT backup Co-Authored-By: Claude Fable 5 --- defaults/scripts/grow-rootfs.initd | 10 +++++++++- defaults/target-packages.txt | 4 +++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/defaults/scripts/grow-rootfs.initd b/defaults/scripts/grow-rootfs.initd index f8216be3..276d346b 100644 --- a/defaults/scripts/grow-rootfs.initd +++ b/defaults/scripts/grow-rootfs.initd @@ -40,13 +40,21 @@ start() { return 0 fi + if command -v sgdisk >/dev/null 2>&1; then + ebegin "Relocating GPT backup to disk end" + sgdisk -e "$disk" >/dev/null 2>&1 + eend 0 + fi + ebegin "Growing partition $part on $disk to disk end" - parted -s "$disk" resizepart "$part" 100% || { + echo Yes | parted ---pretend-input-tty "$disk" resizepart "$part" 100% >/dev/null 2>&1 || { eend $? "parted resizepart failed" return 0 } eend 0 + partx -u "$disk" >/dev/null 2>&1 || partprobe "$disk" >/dev/null 2>&1 || true + ebegin "Resizing ext4 on $src" resize2fs "$src" || { eend $? "resize2fs failed" diff --git a/defaults/target-packages.txt b/defaults/target-packages.txt index 06655c99..c5e53c89 100644 --- a/defaults/target-packages.txt +++ b/defaults/target-packages.txt @@ -27,8 +27,10 @@ app-editors/vim sys-process/htop app-misc/fastfetch -# Disk utilities +# Disk utilities (grow-rootfs needs parted to resize and sgdisk to +# relocate the GPT backup after dd-ing an image to a larger disk) sys-block/parted +sys-apps/gptfdisk # Dev / diagnostics dev-vcs/git From 099c303e64e23710673a17612400fa88c05e1ede Mon Sep 17 00:00:00 2001 From: Sungjoon Moon Date: Sat, 11 Jul 2026 16:23:32 +0900 Subject: [PATCH 5/6] image: agetty -L on the serial getty Co-Authored-By: Claude Fable 5 --- crossdev-stages/src/image.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crossdev-stages/src/image.rs b/crossdev-stages/src/image.rs index dd44156e..9f3dca3c 100644 --- a/crossdev-stages/src/image.rs +++ b/crossdev-stages/src/image.rs @@ -269,7 +269,7 @@ fn default_assemble(runner: &SandboxRunner, board: &BoardConfig) -> Result<()> { if let (Some(tty), Some(baud)) = (&board.serial_tty, &board.serial_baud) { runner.run(&format!( - "echo 'x1:12345:respawn:/sbin/agetty {baud} {tty} linux' \ + "echo 'x1:12345:respawn:/sbin/agetty -L {baud} {tty} linux' \ >> /build/gen/root/etc/inittab" ))?; } From b4c0fbee712c2a90c6aba19c52085978d612c828 Mon Sep 17 00:00:00 2001 From: Sungjoon Moon Date: Sat, 11 Jul 2026 16:23:56 +0900 Subject: [PATCH 6/6] boards/zhihe-a210: add Zhihe A210 (8-core heterogeneous RISC-V) Co-Authored-By: Claude Fable 5 --- boards/zhihe-a210/README.md | 298 +++++++++++++++++++++++++ boards/zhihe-a210/board.conf | 97 ++++++++ boards/zhihe-a210/firmware/README.md | 63 ++++++ boards/zhihe-a210/flash.sh | 103 +++++++++ boards/zhihe-a210/genimage.cfg | 56 +++++ boards/zhihe-a210/override-kernel.sh | 20 ++ boards/zhihe-a210/post-assemble.sh | 169 ++++++++++++++ boards/zhihe-a210/sandbox-packages.txt | 4 + boards/zhihe-a210/target-packages.txt | 27 +++ 9 files changed, 837 insertions(+) create mode 100644 boards/zhihe-a210/README.md create mode 100644 boards/zhihe-a210/board.conf create mode 100644 boards/zhihe-a210/firmware/README.md create mode 100755 boards/zhihe-a210/flash.sh create mode 100644 boards/zhihe-a210/genimage.cfg create mode 100755 boards/zhihe-a210/override-kernel.sh create mode 100755 boards/zhihe-a210/post-assemble.sh create mode 100644 boards/zhihe-a210/sandbox-packages.txt create mode 100644 boards/zhihe-a210/target-packages.txt diff --git a/boards/zhihe-a210/README.md b/boards/zhihe-a210/README.md new file mode 100644 index 00000000..4a89a215 --- /dev/null +++ b/boards/zhihe-a210/README.md @@ -0,0 +1,298 @@ +# Zhihe A210 (a210-dev) + +Shanghai Zhihe A210 SoC — 8-core heterogeneous RISC-V: 4× T-Head C920 +(OoO) + 4× T-Head C908 (in-order), both at 1.9 GHz, identical ISA. +NPU 12 TOPS, GPU (Vulkan 1.2), LPDDR4X up to 16 GB, eMMC 5.1, +2× GbE, PCIe Gen3, USB-C 3.1 w/ DisplayPort, HDMI 2.0. + +## Boot chain + +``` +BootROM (BOOT_SEL strap) + -> u-boot SPL (CONFIG_TEXT_BASE=0x70000800, RVBL header) + -> OpenSBI fw_dynamic @ 0x80000000 (dynamic handoff, NOT FW_PAYLOAD) + -> u-boot 2024.10 @ 0x90000000 + -> riscv-boot.itb FIT (Image + DTB + fw_dynamic + u-boot) + -> Linux Image @ 0x80200000, DTB @ 0x8c000000 +``` + +Primary boot media is **eMMC**. Recovery is **USB fastboot**: hold the +Flash button on power-on (forces BOOT_SEL=000) and host runs `flash.sh`. + +## ISA / GCC + +`-march=rv64gcv_zvl128b_zba_zbb_zbc_zbs -mabi=lp64d`. + +Per `arch/riscv/boot/dts/zhihe/a210-soc-core.dtsi`: + +``` +riscv,isa-extensions = "i","m","a","f","d","c","v", + "zicntr","zicsr","zifencei","zihpm", + "zba","zbb","zbc","zbs","svpbmt","sscofpmf"; +``` + +RVV 1.0 with **VLEN=128** (vendor spec sheet). **Not RVA23** — missing +Zacas, Zfa, Zvfh, Zicond. Do not pass `-march=rva23u64` here; codegen +will SIGILL on the unsupported instructions. + +GCC has no `-mcpu=zhihe-a210` (or `-mcpu=thead-c920` covering this +exact extension set); pass the `-march` string directly. + +Same RVV 1.0 vector miscompile pattern as K230 — `board.conf` sets +per-package fallbacks (scalar for libgcrypt, -O2 for openssh's ed25519 +sigverify, gentoo bug #975669): + +```sh +WORKAROUND_PKGS=("dev-libs/libgcrypt" "net-misc/openssh") +WORKAROUND_CFLAGS=("-O3 -march=rv64gc -pipe" "-O2 -march=rv64gcv_zvl128b_zba_zbb_zbc_zbs -pipe") +``` + +## Vendor sources + +All from OSU Open Source Lab GitLab mirrors (Zhihe-blessed forks of +upstream — pinned to `v2.9.0` tags): + +| repo | URL | tag | +|-----------|------------------------------------------------------|---------------------| +| linux | `git.osuosl.org/osuosl/zhihe-a210-kernel.git` | `osl/a210-mainline` | +| u-boot | `git.osuosl.org/osuosl/zhihe-a210-u-boot.git` | `v2.9.0` | +| opensbi | `git.osuosl.org/osuosl/zhihe-a210-opensbi.git` | `v2.9.0` | +| buildroot | `git.osuosl.org/osuosl/zhihe-a210-buildroot.git` | reference only | + +LTS alternative: `KERNEL_TAG="osl/a210-6.6.x-lts"` (6.6.141+ base) — more +conservative if `osl/a210-mainline` regresses, and the branch that the +T7 first-board validation actually ran on (`6.6.141-osl+`, 2026-06-15). +Vendor `develop` branches move; pin to the tags above. + +## Closed firmware blobs + +Three vendor blobs are **not** in the OSL mirrors and **not** in this +repo. See `firmware/README.md` for the download path. + +- `a210-aon.bin` (~52K) — E902 AON firmware (PMIC, RTC, reboot, + regulators). `override-kernel.sh` bakes it into the kernel via + `CONFIG_EXTRA_FIRMWARE` when present. +- `bootzero-rvbl.bin` — fastboot stage-1 (DDR init). Needed only for + first-time bring-up; once vendor u-boot is in eMMC, subsequent flashes + skip it. +- `bootzero2.bin` — input for `emmc_boot-loader.img` (eMMC boot0 + loader). Optional: without it flash.sh skips the `mmc0boot0` stage. + +## Flash + +### Bundle layout + +`crossdev-stages image export --board zhihe-a210 --all` produces a flat +tree — post-assemble stages everything flash.sh needs at the build-dir +top level, since `export --all` copies top-level files only: + +``` +/ +├── flash.sh +├── gentoo-linux-zhihe-a210_dev-emmc.img # GPT image (uncompressed) +├── bootfs.ext4 # boot partition (256 MiB) +├── rootfs.ext4 # rootfs partition +├── spl-with-fit-rvbl.bin # OUR build: SPL + opensbi + u-boot +├── uboot_env.img # baked u-boot env (bootcmd + PARTLABEL root) +├── bootzero-rvbl.bin # vendor blob (DDR init) — only if dropped +│ # into firmware/ before the build +└── emmc_boot-loader.img # eMMC boot0 loader — only if bootzero2.bin + # was present at build time +``` + +flash.sh also probes `u-boot/` and `firmware/[vendor/]` for each +artifact, so it works unchanged straight from the per-board build +directory, and skips the optional stages with a warning when the +corresponding file is absent. + +If `image export` gave you `*.img.xz`, decompress and rename so flash.sh +finds it: + +```sh +unxz gentoo-linux-zhihe-a210_dev-emmc-*.img.xz +mv gentoo-linux-zhihe-a210_dev-emmc-*.img gentoo-linux-zhihe-a210_dev-emmc.img +``` + +### Host prerequisites + +- `fastboot` CLI (`emerge dev-util/android-tools`, `pacman -S android-tools`, + or `apt install fastboot`) +- USB-A → USB-C cable to the board +- 12 V DC power supply + +### Enter fastboot mode + +Vendor BOOT_SEL=000 USB recovery path: + +1. Board powered off. +2. **Hold the Flash button**, **press Reset**, plug in USB-A to host. +3. Apply 12 V DC. +4. Verify host sees the board: `fastboot devices` — should list one + device (any string, vendor reports `product: a2*`). + +### Flash + +```sh +cd +sudo ./flash.sh +``` + +`flash.sh` walks the vendor recovery chain — same shape as +`board/zhihe/common/script/fastboot_images.sh` but trimmed to our +4-partition flatten layout: + +1. `fastboot flash ram bootzero-rvbl.bin` → DDR up +2. `fastboot reboot` +3. `fastboot flash ram spl-with-fit-rvbl.bin` → SPL → opensbi → u-boot +4. `fastboot reboot` (now in u-boot fastboot) +5. `dd if=*-emmc.img of=gpt.img bs=512 count=34` (extracted on first run) +6. `fastboot flash gpt gpt.img` (17408 B — GPT header only) +7. `fastboot flash mmc0boot0 emmc_boot-loader.img` (skipped w/ warning if absent) +8. `fastboot flash uboot_env uboot_env.img` (skipped w/ warning if absent) +9. `fastboot flash boot bootfs.ext4` +10. `fastboot flash rootfs rootfs.ext4` +11. `fastboot reboot` + +Sending the **full** disk image to `fastboot flash gpt` triggers +`(remote: '10205000')` — vendor u-boot's gpt handler expects only the +17408-byte GPT structure (LBA 0–33: protective MBR + GPT header + +entries). flash.sh extracts that on the fly. + +If the board is already in u-boot fastboot mode from an earlier run +(`getvar product` reports `a2*`), stages 1–4 are skipped. + +### First-boot env setup (only if the uboot_env stage was skipped) + +`uboot_env.img` already bakes the production `bootcmd`/`bootargs` +(rooted at `PARTLABEL=rootfs` — genimage regenerates GPT PARTUUIDs +every build, so never root by PARTUUID). If flash.sh skipped the +`uboot_env` stage, set the env once over serial (`ttyS4 @ 115200 8N1`, +interrupt u-boot autoboot): + +```text +=> setenv kernel_addr 0x80200000 +=> setenv dtb_addr 0x8c000000 +=> setenv bootargs 'console=ttyS4,115200 root=PARTLABEL=rootfs rw rootwait earlycon clk_ignore_unused loglevel=4' +=> setenv bootcmd 'ext4load mmc 0:3 ${kernel_addr} Image; ext4load mmc 0:3 ${dtb_addr} a210-dev.dtb; booti ${kernel_addr} - ${dtb_addr}' +=> saveenv +=> boot +``` + +MAC setup is separate — see `MAC handling` below. + +### Dumping eMMC before flashing (optional backup) + +Stock A210 u-boot has only `fastboot flash` enabled, **not** +`fastboot fetch` / `oem dump` / UMS, so you **cannot read eMMC back via +fastboot alone**. Two options: + +1. **Add UMS to u-boot** — append to `a210_evb_defconfig`: + ``` + CONFIG_CMD_UMS=y + CONFIG_USB_FUNCTION_MASS_STORAGE=y + ``` + Rebuild `spl-with-fit-rvbl.bin`, `fastboot flash ram` + reboot into + u-boot, then at the prompt: + ```text + => ums 0 mmc 0 # whole user area as /dev/sdX + => ums 0 mmc 0.1 # boot0 hw partition + => ums 0 mmc 0.2 # boot1 hw partition + ``` + On the host: `dd if=/dev/sdX of=a210-emmc.img bs=4M conv=fsync status=progress`. + Dump twice, compare `sha256sum`. + +2. **Boot a rescue initramfs via `fastboot flash ram`** (same path as + VSRVES01's phram-rootfs), then `dd /dev/mmcblk0 | nc host 9000` or + write to a USB stick. More work; useful if you also want live GPT + inspection. + +## Partition layout + +OSL "flatten" layout (per `a210-linux/docs/PARTITIONS.md`) — four +partitions instead of the vendor A/B-redundant nine: + +| # | name | size | purpose | +|---|-----------|----------|---------------------------------------------------| +| 1 | factory | 32 KiB | vendor OTP / MAC / `fnv` factory NVRAM (reserved) | +| 2 | uboot_env | 32 KiB | u-boot `saveenv` target | +| 3 | boot | 256 MiB | ext4 `A210-BOOT` (Image + DTB) | +| 4 | rootfs | 4 GiB | ext4 `A210-ROOT` (userland, grown on first boot) | + +Partitions 1–2 are deliberately reserved at the vendor offset/size so +the board's `fnv` and `saveenv` resolve by GPT name. genimage assigns +fresh GPT PARTUUIDs on every build — that is why the kernel roots by +`root=PARTLABEL=rootfs` (partition names are stable), never by +PARTUUID. + +Full A/B + overlay (vendor's `bootcount`/`bootlimit` rollback) is +documented in `a210-linux/docs/AB_LAYOUT.md` as a future migration, +deferred until flatten is bench-validated end-to-end. + +## Provisioning flow + +OSL fleet install is **netboot installer → eMMC flatten image**, per +board, manual over serial (per `PROVISION_RUNBOOK.md`): + +1. Install server stages `Image`, `a210-dev.dtb`, `installer.cpio.gz` + over TFTP and a rootfs tarball over NFS. +2. On each board: interrupt U-Boot, `dhcp` + `tftpboot` the three + payloads, `booti` with `install_server= board_id=` on the + cmdline. The Debian initramfs installer (busybox, e2fsprogs, gdisk, + nfs-common, udhcpc) DHCPs `eth0`, NFS-mounts the export, runs + `partition-emmc.sh`, writes Image + dtb + rootfs, stamps + `/etc/osl/board-id`, and reboots. +3. Operator persists the production `bootcmd`/`bootargs` (see + `board.conf` header) and the MAC pair, then `boot`. + +That flow is what OSL deploys to a full fleet. Single-board users +should just `dd` the produced eMMC image instead — vendor `booti` +accepts our unsigned `Image` directly (vendor confirmed 2026-05-28). + +## MAC handling + +Factory MAC is **not** present in eFuse (vendor-confirmed); the +`factory` partition stays empty out of the box. Two options per +`MAC_SCHEME.md`: + +- **Durable** (recommended): U-Boot factory-NVRAM — `fnv erase`, + `env set ethaddr 48:da:??:??:??:??`, `env set eth1addr 48:da:…`, + `fnv save`. Prefix `48:da` is locally-administered. **eth1 MUST + differ from eth0** — vendor's docs example mistakenly reused one MAC. +- **Runtime fallback**: `osl-setmac.service` in the OSL Debian overlay + derives both MACs by SHA1 over `/etc/osl/board-id` (or + `machine-id`). Whether we replicate that service in our image is + out of scope here. + +## Console + +`ttyS4 @ 115200 8N1`. UART4 in DTS, matches vendor u-boot and bootargs. + +## Known gaps + +- **NPU** (12 TOPS) — vendor blob only, no open driver. +- **GPU** — vendor Vulkan blob; no Mesa support yet. +- **HDMI/DisplayPort output** — vendor only; use serial console. +- **A/B redundancy** — flatten layout intentionally drops vendor A/B; + see `AB_LAYOUT.md` for the future migration plan. +- **MAC fallback service** — `osl-setmac.service` (OSL Debian overlay) + not yet ported to our Gentoo image; durable `fnv ethaddr/eth1addr` + is the recommended path. +- **u-boot SPL RVBL build target** — vendor `CONFIG_BUILD_TARGET="zhihe-rvbl.bin"` + may produce a pre-wrapped SPL; we re-wrap from raw `u-boot-spl.bin` + in post-assemble for determinism. + +## References + +- DTS / ISA: `zhihe-a210-kernel/arch/riscv/boot/dts/zhihe/a210-soc-core.dtsi` +- Kernel defconfig: `zhihe-a210-kernel/arch/riscv/configs/a210_evb_defconfig` +- U-Boot defconfig: `zhihe-a210-u-boot/configs/a210_evb_defconfig` +- OpenSBI override: `zhihe-a210-opensbi/platform/generic/zhihe/a210.c` +- GPT layout: `zhihe-a210-buildroot/board/zhihe/common/images/gpt/gpt_emmc.txt` +- Flash script: `zhihe-a210-u-boot/board/zhihe/common/script/fastboot_images.sh` +- RVBL header: `zhihe-a210-u-boot/board/zhihe/common/script/generate_firmware.sh` +- FIT template: `zhihe-a210-u-boot/board/zhihe/a210-evb/riscv-boot.its` +- Partition layout: `a210-linux/docs/PARTITIONS.md`, `docs/AB_LAYOUT.md` +- Provisioning: `a210-linux/docs/PROVISION_RUNBOOK.md` +- MAC scheme: `a210-linux/docs/MAC_SCHEME.md` +- T7 validation: `a210-linux/docs/T7-VALIDATION.md` (PASSED 2026-06-15) +- Vendor SDK (closed blobs): https://developer.zhcomputing.com/downloads/release/zhihesdk/v2.8.1/ diff --git a/boards/zhihe-a210/board.conf b/boards/zhihe-a210/board.conf new file mode 100644 index 00000000..9efa1e37 --- /dev/null +++ b/boards/zhihe-a210/board.conf @@ -0,0 +1,97 @@ +# Zhihe A210 board. Boot env baked into uboot_env.img by post-assemble.sh: +# bootargs 'console=ttyS4,115200 root=PARTLABEL=rootfs \ +# rw rootwait earlycon clk_ignore_unused loglevel=4' +# bootcmd 'ext4load mmc 0:3 ${kernel_addr} Image && \ +# ext4load mmc 0:3 ${dtb_addr} a210-dev.dtb && \ +# booti ${kernel_addr} - ${dtb_addr}' +# PARTLABEL-rooted, no initrd (kernel statically links eMMC/eth/pinctrl/PMIC). +# The same bootargs is materialised into extlinux.conf by post-assemble.sh. + +BOARD_NAME="zhihe-a210" +BOARD_ARCH="riscv64" +# Zhihe A210 per arch/riscv/boot/dts/zhihe/a210-soc-core.dtsi `riscv,isa-extensions`: +# "i","m","a","f","d","c","v","zicntr","zicsr","zifencei","zihpm", +# "zba","zbb","zbc","zbs","svpbmt","sscofpmf" +# RVV 1.0 with VLEN=128 (vendor spec). NOT RVA23, and NOT RVA22U64 +# either: the profile mandates zicboz/zicbom/zic64b/zfhmin which are +# absent from the DTS list above — gcc emits cbo.zero for memset when +# zicboz+zic64b are in -march, and the kernel only enables CBZE for +# userland when the DT advertises zicboz, so profile-built binaries +# SIGILL. Spell the extensions out explicitly (README `ISA / GCC`). +BOARD_CFLAGS="-O3 -march=rv64gcv_zvl128b_zba_zbb_zbc_zbs -pipe" +SYSROOT="rv64gcv_zvl128b_zba_zbb_zbc_zbs" +CROSS_COMPILE="riscv64-unknown-linux-gnu-" +KERNEL_ARCH="riscv" + +# Toolchain pins — exact versions, not slot-only. +# "16" alone selects gcc:16 slot which picks newest = 16.2.9999 live ebuild +# (clones full gcc.git, days to build). "16.1.0" pins to released 16.1.0 +# via `emerge =sys-devel/gcc-16.1.0*`, same pattern as boards/k3. +BOARD_GCC_VERSION="16.1.0" + +# Vendor source mirrors — OSU Open Source Lab GitLab (Zhihe-blessed). +# Pin to v2.9.0 tags so vendor `develop` branch movement does not break us. + +# OpenSBI: vendor fork, fw_dynamic handoff (NOT FW_PAYLOAD like k230). +# u-boot SPL loads fw_dynamic.bin to 0x80000000 via CONFIG_SPL_OPENSBI. +# OPENSBI v2.9.0 = ebc44c77997c42485931e6e857a4f5a43aca1b14 (opensbi 2.8.0 base) +OPENSBI_REPO="https://git.osuosl.org/osuosl/zhihe-a210-opensbi.git" +OPENSBI_TAG="v2.9.0" +OPENSBI_PLATFORM="generic" +OPENSBI_FW_TYPE="dynamic" +OPENSBI_MAKE_FLAGS="PLATFORM_RISCV_ISA=rv64imafdc_zicsr_zifencei PLATFORM_RISCV_ABI=lp64d" + +# U-Boot 2024.10 base; SPL output is `zhihe-rvbl.bin` (CONFIG_BUILD_TARGET). +# SPL TEXT_BASE 0x70000800, u-boot proper at 0x90000000. +# v2.9.0 = bb2f09226973ad8d5c11f3cf0745721ba31a8e2a +U_BOOT_REPO="https://git.osuosl.org/osuosl/zhihe-a210-u-boot.git" +U_BOOT_TAG="v2.9.0" +U_BOOT_DEFCONFIG="a210_evb_defconfig" + +# Kernel: OSL's a210-mainline (v7.1 base + A210 enablement) is the +# recommended default per a210-linux/docs/STATUS-2026-06-17.md. T7 +# first-board validation (PASSED 2026-06-15) actually ran on +# osl/a210-6.6.x-lts (6.6.141-osl+) — switch to that branch as a +# conservative fallback if mainline regresses. +# osl/a210-mainline head = c27d0dfab32d2b5b46b0c118a4c393b63d479646 +KERNEL_REPO="https://git.osuosl.org/osuosl/zhihe-a210-kernel.git" +KERNEL_TAG="osl/a210-mainline" +KERNEL_DEFCONFIG="a210_evb_defconfig" +# OSL hardware uses the DEV variant DTS (PROVISION_RUNBOOK.md). +# Vendor also ships a210-evb.dtb for the bare EVB board. +BOARD_DTB_GLOB="arch/riscv/boot/dts/zhihe/a210-dev.dtb" + +# Boot environment. +# A210 console is ttyS4 (UART4 in DTS), 115200 8N1. +# OSL flatten layout: 4 partitions (factory, uboot_env, boot, rootfs). +# Root by PARTLABEL=rootfs, not /dev/mmcblk0pN (device numbering) and +# not PARTUUID (genimage regenerates GPT UUIDs every build); the GPT +# partition names in genimage.cfg are stable. +# Vendor `booti` accepts unsigned kernel (KERNEL.md, 2026-05-28 vendor reply) +# — no FIT-signed kernel path is needed in normal boot. +BOOT_ROOT_DEV="PARTLABEL=rootfs" +BOOT_CONSOLE="ttyS4,115200n8" +BOOT_KERNEL_NAME="Image" +BOOT_DTB_NAME="a210-dev.dtb" +BOOT_HOSTNAME="gentoo" +BOOT_SERIAL_TTY="ttyS4" +BOOT_SERIAL_BAUD="115200" + +# Build order: u-boot SPL embeds opensbi via CONFIG_SPL_OPENSBI, +# so opensbi must be built before u-boot. Standard `bootloader` step +# builds opensbi then u-boot in sequence — same as k3. +BUILD_STEPS=(deps checkout bootloader kernel assemble pack) + +# RVV 1.0 vector miscompile workaround — same pattern as k230. +# libgcrypt's vector CRYPT routines miscompile with -march=rv64gcv on +# this toolchain; fall back to scalar build for that package. +# Per gentoo bug #975669: openssh -O3 + autovec miscompiles ed25519 sigverify. +# -O2 (any march) passes; we keep V for everything else, just drop to -O2 here. +WORKAROUND_PKGS=("dev-libs/libgcrypt" "net-misc/openssh") +WORKAROUND_CFLAGS=("-O3 -march=rv64gc -pipe" "-O2 -march=rv64gcv_zvl128b_zba_zbb_zbc_zbs -pipe") + +BOOT_SERVICES=("dhcpcd:boot" "sshd:default" "metalog:default" "ntp-client:default" "ntpd:default") + +# A210 boots from eMMC (not sdcard), genimage.cfg names the image accordingly. +# Override the default `_dev-sdcard.img` name so pack's mv finds it. +IMAGE_NAME="gentoo-linux-zhihe-a210_dev-emmc.img" diff --git a/boards/zhihe-a210/firmware/README.md b/boards/zhihe-a210/firmware/README.md new file mode 100644 index 00000000..9ea72c5c --- /dev/null +++ b/boards/zhihe-a210/firmware/README.md @@ -0,0 +1,63 @@ +# Zhihe A210 closed firmware blobs + +These three blobs are required for full A210 functionality. They are +**not** redistributed by the OSU Open Source Lab vendor-source mirrors +and **not** committed to this repo — per the vendor buildroot README +they are redistributable *inside* a vendor-built image but not standalone. + +Drop them into this directory before building: + +``` +boards/zhihe-a210/firmware/ +├── a210-aon.bin # ~52K — E902 AON core firmware (PMIC, RTC, reboot, regulators) +├── bootzero-rvbl.bin # early DDR init, fastboot stage 1 +└── bootzero2.bin # fastboot stage 1.5 (BTZ-with-SPL chain) +``` + +## Where to get them + +Download the Zhihe SDK release archive: + +``` +https://developer.zhcomputing.com/downloads/release/zhihesdk/v2.8.1/ +``` + +Unpack and copy: + +- `a210-aon.bin` from `vendor/firmware/` (or extract from any released + vendor image's `/lib/firmware/zhihe/`). +- `bootzero-rvbl.bin`, `bootzero2.bin` from `bootzero/` or `prebuilts/`. + +## What happens without them + +- **a210-aon.bin**: kernel boots, but `reboot`/`poweroff` hang and some + regulators (touch, audio, charger) stay off. `override-kernel.sh` + falls back to "look for it at runtime" mode and prints a warning. +- **bootzero-rvbl.bin**: only matters for first-time bring-up on a + bare board. Once vendor u-boot is in eMMC mmcblk0boot0, subsequent + flashes go through u-boot's own fastboot and skip stage 1. +- **bootzero2.bin**: post-assemble uses it to build + `emmc_boot-loader.img` (bootzero2 + SPL + FIT for eMMC boot0). + Without it that image is not generated and flash.sh skips the + `mmc0boot0` stage with a warning — fine if the eMMC boot0 loader is + already in place from a vendor image, required for a bare board. + +## sha256-pin policy + +Per the project-wide mirror convention (`docs/MIRRORS.md`) and matching +OSL's own posture, we pin the **sha256 of each blob** without +redistributing the blob itself. When a verified copy is dropped into +this directory, the build records its hash; subsequent rebuilds refuse +mismatches. Future work: capture and commit the canonical hashes from +SDK v2.8.1 here so users can spot tampering on download. + +## Vendor source reference + +The exact RVBL header format + wrap scripts live in vendor source at: + +``` +zhihe-a210-u-boot/board/zhihe/common/script/generate_firmware.sh +``` + +Our `post-assemble.sh` re-implements `generate_rvbl()` in inline Python +so we don't need to vendor the shell script. diff --git a/boards/zhihe-a210/flash.sh b/boards/zhihe-a210/flash.sh new file mode 100755 index 00000000..9d7f0f0a --- /dev/null +++ b/boards/zhihe-a210/flash.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# Zhihe A210 fastboot flash — run from a flash-bundle directory produced +# by `crossdev-stages image export --board zhihe-a210 --all`, or straight +# from the per-board build directory (artifacts in u-boot/). +set -e + +cd "$(dirname "$(readlink -f "$0")")" + +device=() +[ -n "$1" ] && device=(-s "$1") + +FAIL="###### Image flashing failed ######" + +# Probe the known artifact locations: bundle root (export --all), build +# dir u-boot/ subdir, and manual drops in firmware/[vendor/]. +find_art() { + local d + for d in . u-boot firmware firmware/vendor; do + [ -f "$d/$1" ] && { echo "$d/$1"; return 0; } + done + return 1 +} + +if [ ! -f bootfs.ext4 ] && [ -f bootfs.ext4.xz ]; then + echo "[*] decompressing bootfs.ext4..." + unxz bootfs.ext4.xz +fi +if [ ! -f rootfs.ext4 ] && [ -f rootfs.ext4.xz ]; then + echo "[*] decompressing rootfs.ext4..." + unxz rootfs.ext4.xz +fi + +cat <<'NOTE' + + HOLD Flash button THROUGHOUT stages 1+2 until u-boot fastboot is up. + +NOTE + +echo "[*] checking fastboot device..." +fastboot "${device[@]}" devices + +if fastboot "${device[@]}" getvar product 2>&1 | grep -q "product: a2"; then + echo "[*] board already in u-boot fastboot mode — skipping bring-up" +else + BOOTZERO=$(find_art bootzero-rvbl.bin) || { + echo "ERROR: bootzero-rvbl.bin not found (vendor blob, see firmware/README.md)" + echo "$FAIL"; exit 1 + } + SPL_FIT=$(find_art spl-with-fit-rvbl.bin) || { + echo "ERROR: spl-with-fit-rvbl.bin not found (built by post-assemble.sh)" + echo "$FAIL"; exit 1 + } + + echo "[*] stage 1: $BOOTZERO" + fastboot "${device[@]}" flash ram "$BOOTZERO" || { echo "$FAIL"; exit 1; } + fastboot "${device[@]}" reboot + + echo "[*] stage 2: $SPL_FIT" + fastboot "${device[@]}" flash ram "$SPL_FIT" || { echo "$FAIL"; exit 1; } + fastboot "${device[@]}" reboot + + echo "[*] waiting for u-boot fastboot..." + sleep 5 +fi + +echo "[*] flashing GPT" +if GPT_IMG=$(find_art emmc-gpt_primary.img); then + fastboot "${device[@]}" flash gpt "$GPT_IMG" || { echo "$FAIL"; exit 1; } +elif [ -f gpt.img ]; then + fastboot "${device[@]}" flash gpt gpt.img || { echo "$FAIL"; exit 1; } +else + echo "[*] extracting gpt.img" + dd if=gentoo-linux-zhihe-a210_dev-emmc.img of=gpt.img bs=512 count=34 status=none + fastboot "${device[@]}" flash gpt gpt.img || { echo "$FAIL"; exit 1; } +fi + +if EMMC_LOADER=$(find_art emmc_boot-loader.img); then + echo "[*] flashing mmc0boot0 ($EMMC_LOADER)" + fastboot "${device[@]}" flash mmc0boot0 "$EMMC_LOADER" || { echo "$FAIL"; exit 1; } +else + echo "[*] WARN: emmc_boot-loader.img not found — skipping mmc0boot0" + echo " (built only when bootzero2.bin is present at build time; eMMC" + echo " boot needs a loader in mmc0boot0 — see firmware/README.md)" +fi + +if UBOOT_ENV=$(find_art uboot_env.img); then + echo "[*] flashing uboot_env ($UBOOT_ENV)" + fastboot "${device[@]}" flash uboot_env "$UBOOT_ENV" || { echo "$FAIL"; exit 1; } +else + echo "[*] WARN: uboot_env.img not found — skipping uboot_env" + echo " (set bootcmd/bootargs manually over serial — see README.md)" +fi + +echo "[*] flashing boot" +fastboot "${device[@]}" -S 64M flash boot bootfs.ext4 || { echo "$FAIL"; exit 1; } + +echo "[*] flashing rootfs" +fastboot "${device[@]}" -S 64M flash rootfs rootfs.ext4 || { echo "$FAIL"; exit 1; } + +echo "[*] rebooting..." +fastboot "${device[@]}" reboot + +echo "###### Flash completed ######" diff --git a/boards/zhihe-a210/genimage.cfg b/boards/zhihe-a210/genimage.cfg new file mode 100644 index 00000000..4218f731 --- /dev/null +++ b/boards/zhihe-a210/genimage.cfg @@ -0,0 +1,56 @@ +config { + outputpath = . + inputpath = . + rootpath = gen + tmppath = tmp +} + +image bootfs.ext4 { + ext4 { + use-mke2fs = "true" + label = "A210-BOOT" + } + size = 256M + mountpoint = "/boot" +} + +image rootfs.ext4 { + ext4 { + use-mke2fs = "true" + label = "A210-ROOT" + } + size = 4G + mountpoint = "/root" +} + +image gentoo-linux-zhihe-a210_dev-emmc.img { + hdimage { + partition-table-type = gpt + align = 512K + } + + partition factory { + offset = "512K" + size = "32K" + in-partition-table = "true" + } + + partition uboot_env { + offset = "1M" + size = "32K" + in-partition-table = "true" + } + + partition boot { + image = "bootfs.ext4" + offset = "2M" + size = "256M" + in-partition-table = "true" + } + + partition rootfs { + image = "rootfs.ext4" + size = "4G" + in-partition-table = "true" + } +} diff --git a/boards/zhihe-a210/override-kernel.sh b/boards/zhihe-a210/override-kernel.sh new file mode 100755 index 00000000..1b6dd796 --- /dev/null +++ b/boards/zhihe-a210/override-kernel.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +cd /build/linux + +make ARCH="${KERNEL_ARCH}" CROSS_COMPILE="${CROSS_COMPILE}" "${KERNEL_DEFCONFIG}" + +BLOB_SRC=/scripts/boards/zhihe-a210/firmware/a210-aon.bin +if [ -f "${BLOB_SRC}" ]; then + echo "[*] baking a210-aon.bin" + install -D "${BLOB_SRC}" /build/linux/firmware/a210-aon.bin + scripts/config \ + --set-str EXTRA_FIRMWARE "a210-aon.bin" \ + --set-str EXTRA_FIRMWARE_DIR "firmware" +else + echo "[!] a210-aon.bin not found at ${BLOB_SRC}" +fi + +make ARCH="${KERNEL_ARCH}" CROSS_COMPILE="${CROSS_COMPILE}" olddefconfig +make ARCH="${KERNEL_ARCH}" CROSS_COMPILE="${CROSS_COMPILE}" -j"$(nproc)" diff --git a/boards/zhihe-a210/post-assemble.sh b/boards/zhihe-a210/post-assemble.sh new file mode 100755 index 00000000..a14f2a91 --- /dev/null +++ b/boards/zhihe-a210/post-assemble.sh @@ -0,0 +1,169 @@ +#!/bin/bash +set -e + +kver=$(ls /build/gen/root/lib/modules/ 2>/dev/null | head -1) +[ -z "$kver" ] && { echo 'Error: no kernel modules found'; exit 1; } + +mkdir -p "/build/gen/boot/zhihe/${kver}" +cp /build/gen/boot/*.dtb "/build/gen/boot/zhihe/${kver}/" 2>/dev/null || true + +mkdir -p /build/gen/boot/extlinux +cat > /build/gen/boot/extlinux/extlinux.conf < "${ITB_STAGE}/riscv-boot.its" <; + + images { + fdt-dev { + description = "A210 DEV DT"; + data = /incbin/("${BOOT_DTB_NAME:-a210-dev.dtb}"); + type = "flat_dt"; + arch = "riscv"; + compression = "none"; + load = <0x8c000000>; + hash { algo = "sha256"; }; + }; + opensbi-1 { + description = "OpenSBI fw_dynamic"; + data = /incbin/("fw_dynamic.bin.gz"); + type = "firmware"; + os = "opensbi"; + arch = "riscv"; + compression = "gzip"; + load = <0x80000000>; + entry = <0x80000000>; + hash { algo = "sha256"; }; + }; + u-boot-1 { + description = "U-Boot"; + data = /incbin/("u-boot.bin.gz"); + type = "firmware"; + os = "u-boot"; + arch = "riscv"; + compression = "gzip"; + load = <0x90000000>; + entry = <0x90000000>; + hash { algo = "sha256"; }; + }; + }; + + configurations { + default = "a210-dev"; + a210-dev { + description = "A210 DEV"; + fdt = "fdt-dev"; + firmware = "opensbi-1"; + loadables = "u-boot-1"; + }; + }; +}; +ITSEOF + +mkdir -p /build/u-boot +( cd "${ITB_STAGE}" && mkimage -f riscv-boot.its /build/u-boot/riscv-boot.itb ) + +rvbl_wrap() { + local bin="$1" payload="$2" out="$3" + python3 - "$bin" "$payload" "$out" <<'PYEOF' +import os, struct, sys +bin_path, payload_path, out_path = sys.argv[1:4] +with open(bin_path, "rb") as f: bin_data = f.read() +pad = (-len(bin_data)) % 16 +pad_bin_size = len(bin_data) + pad +header = b"\x6f\x00\x10\x00" +header += b"RVBL" +header += struct.pack("&2; exit 1; } + +if [ -f /build/u-boot/zhihe-rvbl.bin ]; then + magic=$(head -c 4 /build/u-boot/zhihe-rvbl.bin) + [ "$magic" = "RVBL" ] || echo "WARN: zhihe-rvbl.bin missing RVBL magic (got: $magic)" >&2 +fi + +rvbl_wrap "$SPL_BIN" /build/u-boot/riscv-boot.itb /build/u-boot/spl-with-fit-rvbl.bin +rvbl_wrap "$SPL_BIN" none /build/u-boot/u-boot-spl-rvbl.bin + +BLOB_DIR=/scripts/boards/zhihe-a210/firmware +if [ -f "${BLOB_DIR}/bootzero-rvbl.bin" ]; then + cp "${BLOB_DIR}/bootzero-rvbl.bin" /build/u-boot/bootzero-rvbl.bin +fi +if [ -f "${BLOB_DIR}/bootzero2.bin" ]; then + cp "${BLOB_DIR}/bootzero2.bin" /build/u-boot/bootzero2.bin + OUT=/build/u-boot/emmc_boot-loader.img + cat /build/u-boot/bootzero2.bin /build/u-boot/u-boot-spl-rvbl.bin > "$OUT" + truncate -s 851968 "$OUT" + cat /build/u-boot/riscv-boot.itb >> "$OUT" + echo "[*] generated $OUT ($(stat -c %s $OUT) bytes)" +fi + +python3 - <<'PYEOF' +import struct, binascii +entries = [ + "arch=riscv", + "baudrate=115200", + "board=a210-evb", + "bootdelay=3", + "kernel_addr=0x80200000", + "dtb_addr=0x8c000000", + "bootargs=console=ttyS4,115200 root=PARTLABEL=rootfs rw rootwait earlycon clk_ignore_unused loglevel=4", + "bootcmd=ext4load mmc 0:3 ${kernel_addr} Image && ext4load mmc 0:3 ${dtb_addr} a210-dev.dtb && booti ${kernel_addr} - ${dtb_addr}", +] +data = b'' +for e in entries: + data += e.encode('ascii') + b'\x00' +data += b'\x00' +ENV_SIZE = 16384 +PAYLOAD = ENV_SIZE - 5 +padded = data + b'\xff' * (PAYLOAD - len(data)) +crc = binascii.crc32(padded) & 0xffffffff +copy = struct.pack('