Files
retroSoC/scripts/build_hp_linux.py
T
maksyuki 8d735aafbd feat: refactor tests and update configurations for OpenC906 migration
- Adjusted slot assignments in  to reflect new peripheral mappings.
- Updated  to validate new OpenC906 configurations, including generator and profile checks.
- Modified  to reference the new Std-series VexiiRiscv generator.
- Changed cache maintenance instructions in  to align with C906 specifications.
- Revised assertions in  to account for internal unit removals.
- Updated  to reflect changes in peripheral interface counts and policies.
- Added new test file  to ensure preservation of the frozen HP configuration in the Std-series generator.
2026-10-10 15:15:21 +08:00

318 lines
12 KiB
Python

#!/usr/bin/env python3
"""Build pinned RV64 Linux images with a small, static acceptance initramfs."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import shutil
import subprocess
from pathlib import Path
try:
from scripts.hp_tools import require_revision, require_rv64_elf
except ModuleNotFoundError:
from hp_tools import require_revision, require_rv64_elf
LAYOUT = {
"fw_jump.bin": (0x38000000, 512 * 1024),
"retrosoc_hp.dtb": (0x38080000, 64 * 1024),
"Image": (0x38400000, 12 * 1024 * 1024),
"rootfs.cpio": (0x39000000, 8 * 1024 * 1024),
}
REQUIRED_LINUX_CONFIG = (
"CONFIG_ARCH_RV64I",
"CONFIG_64BIT",
"CONFIG_MMU",
"CONFIG_OF_RESERVED_MEM",
"CONFIG_BINFMT_ELF",
"CONFIG_BINFMT_SCRIPT",
"CONFIG_PRINTK",
"CONFIG_TTY",
"CONFIG_HVC_RISCV_SBI",
"CONFIG_SERIAL_EARLYCON_RISCV_SBI",
"CONFIG_PROC_FS",
"CONFIG_SYSFS",
"CONFIG_SHMEM",
"CONFIG_TMPFS",
"CONFIG_DEVTMPFS",
"CONFIG_DEVMEM",
)
def command(arguments: list[str], cwd: Path, extra_env: dict[str, str] | None = None) -> None:
environment = dict(os.environ)
environment.pop("CONFIG", None)
environment.pop("MAKEFLAGS", None)
environment.pop("MAKEOVERRIDES", None)
environment.pop("MFLAGS", None)
environment.update(extra_env or {})
subprocess.run(arguments, cwd=cwd, check=True, env=environment)
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def require_source(path: Path, name: str) -> Path:
resolved = path.resolve()
if not (resolved / ".git").exists():
raise FileNotFoundError(f"missing locked {name} source: {resolved}; run setup-hp-linux")
return resolved
def source_revision(path: Path) -> str:
return subprocess.check_output(
["git", "-C", str(path), "rev-parse", "HEAD"], text=True
).strip()
def find_toolchain(buildroot_output: Path) -> str:
matches = sorted((buildroot_output / "host/bin").glob("riscv64*-linux-*-gcc"))
if len(matches) != 1:
raise RuntimeError(f"expected one RV64 Linux GCC in {buildroot_output / 'host/bin'}")
return str(matches[0])[: -len("gcc")]
def minimal_busybox(buildroot: Path, output: Path, external: Path, jobs: int) -> str:
"""Expand an allowlist using BusyBox's own allnoconfig, not default-y oldconfig."""
make = ["make", f"O={output}", f"BR2_EXTERNAL={external}", f"-j{jobs}"]
command([*make, "busybox-configure"], buildroot)
sources = [path for path in (output / "build").glob("busybox-*")
if (path / "scripts/kconfig/conf").is_file()]
if len(sources) != 1:
raise RuntimeError("expected one configured locked BusyBox source")
source = sources[0]
config_dir = output.parent / "busybox-config"
(config_dir / "include").mkdir(parents=True, exist_ok=True)
command([str(source / "scripts/kconfig/conf"), "-n", str(source / "Config.in")], config_dir,
{"srctree": str(source), "KCONFIG_NOTIMESTAMP": "1",
"KCONFIG_ALLCONFIG": str(external / "busybox/retrosoc_hp.config")})
# This BusyBox conf implementation resets booleans even when ALLCONFIG
# specified y. Overlay our explicit choices after its all-no expansion;
# Buildroot's ordinary oldconfig then resolves the real dependencies.
selected = dict(line.split("=", 1) for line in
(external / "busybox/retrosoc_hp.config").read_text().splitlines()
if line.startswith("CONFIG_") and "=" in line)
config = config_dir / ".config"
lines = []
for line in config.read_text().splitlines():
key = line[2:].split(" ", 1)[0] if line.startswith("# CONFIG_") else line.split("=", 1)[0]
lines.append(f"{key}={selected.pop(key)}" if key in selected else line)
lines.extend(f"{key}={value}" for key, value in selected.items())
config.write_text("\n".join(lines) + "\n", encoding="utf-8")
config_arg = f"BUSYBOX_CONFIG_FILE={config_dir / '.config'}"
command([*make, config_arg, "busybox-reconfigure"], buildroot)
return config_arg
def validate_linux_config(config: Path) -> None:
values: dict[str, str] = {}
for line in config.read_text(encoding="utf-8").splitlines():
if line.startswith("CONFIG_") and "=" in line:
symbol, value = line.split("=", 1)
values[symbol] = value
elif line.startswith("# CONFIG_") and line.endswith(" is not set"):
symbol = line[len("# ") : -len(" is not set")]
values[symbol] = "n"
invalid = [
f"{symbol}={values.get(symbol, 'missing')}"
for symbol in REQUIRED_LINUX_CONFIG
if values.get(symbol) != "y"
]
if invalid:
raise RuntimeError(
"invalid HP Linux effective configuration: " + ", ".join(invalid)
)
def build(args: argparse.Namespace) -> None:
root = args.root.resolve()
output = args.output.resolve()
output.mkdir(parents=True, exist_ok=True)
buildroot = require_source(args.buildroot, "Buildroot")
linux = require_source(args.linux, "Linux")
opensbi = require_source(args.opensbi, "OpenSBI")
for name, path in (("buildroot_hp", buildroot), ("linux_hp", linux), ("opensbi_hp", opensbi)):
if path != require_revision(root, name).resolve():
raise ValueError(f"{name} must use its locked source directory")
external = root / "app/ports/linux"
buildroot_output = output / "buildroot"
command(
["make", f"O={buildroot_output}", f"BR2_EXTERNAL={external}", "retrosoc_hp_defconfig"],
buildroot,
)
busybox_config = minimal_busybox(buildroot, buildroot_output, external, args.jobs)
command(["make", f"O={buildroot_output}", f"BR2_EXTERNAL={external}",
busybox_config, f"-j{args.jobs}"], buildroot)
busybox_sources = sorted((buildroot_output / "build").glob("busybox-*/.config"))
if len(busybox_sources) != 1:
raise RuntimeError("missing effective BusyBox configuration")
busybox_effective = busybox_sources[0].read_text().splitlines()
for symbol in ("STATIC", "BUSYBOX", "ASH", "MOUNT", "TEST", "ECHO", "CAT", "TRUE"):
if f"CONFIG_{symbol}=y" not in busybox_effective:
raise RuntimeError(f"HP BusyBox requires CONFIG_{symbol}=y")
cross_compile = find_toolchain(buildroot_output)
linux_output = output / "linux"
command(
["make", f"O={linux_output}", "ARCH=riscv", f"CROSS_COMPILE={cross_compile}", "tinyconfig"],
linux,
)
command(
[
str(linux / "scripts/kconfig/merge_config.sh"),
"-m",
"-O",
str(linux_output),
str(linux_output / ".config"),
str(external / "linux/retrosoc_hp.config"),
],
linux,
)
command(
["make", f"O={linux_output}", "ARCH=riscv", f"CROSS_COMPILE={cross_compile}", "olddefconfig"],
linux,
)
validate_linux_config(linux_output / ".config")
command(
["make", f"O={linux_output}", "ARCH=riscv", f"CROSS_COMPILE={cross_compile}",
f"-j{args.jobs}", "Image"],
linux,
)
image_dir = output / "images"
image_dir.mkdir(parents=True, exist_ok=True)
require_rv64_elf(linux_output / "vmlinux")
busybox = buildroot_output / "target/bin/busybox"
require_rv64_elf(busybox)
if "INTERP" in subprocess.check_output([cross_compile + "readelf", "-l", str(busybox)], text=True):
raise ValueError("HP acceptance BusyBox must be statically linked")
helper = output / "hp-ready"
command([cross_compile + "gcc", "-Os", "-static", "-Wall", "-Wextra", "-Werror",
"-o", str(helper), str(external / "hp_ready.c")], root)
require_rv64_elf(helper)
archive_spec = output / "initramfs.list"
entries = [f"dir /{name} 755 0 0" for name in ("bin", "dev", "proc", "sys", "tmp")]
entries += [f"file /bin/busybox {busybox} 755 0 0",
f"file /bin/hp-ready {helper} 755 0 0", f"file /init {external / 'init'} 755 0 0",
"nod /dev/console 600 0 0 c 5 1", "nod /dev/mem 600 0 0 c 1 1"]
entries += [f"slink /bin/{name} busybox 777 0 0" for name in
("sh", "mount", "mkdir", "echo", "test", "cat", "sleep")]
archive_spec.write_text("\n".join(entries) + "\n", encoding="utf-8")
rootfs_source = image_dir / "rootfs.cpio"
with rootfs_source.open("wb") as archive:
subprocess.run([str(linux_output / "usr/gen_init_cpio"), "-t", "0", str(archive_spec)],
stdout=archive, check=True)
initrd_end = LAYOUT["rootfs.cpio"][0] + rootfs_source.stat().st_size
command(
[
"dtc",
"-I",
"dts",
"-O",
"dtb",
"-o",
str(image_dir / "retrosoc_hp.dtb"),
str(external / "linux/retrosoc_hp.dts"),
],
root,
)
command(
[
"fdtput",
"-t",
"x",
str(image_dir / "retrosoc_hp.dtb"),
"/chosen",
"linux,initrd-end",
f"0x{initrd_end:08x}",
],
root,
)
opensbi_output = output / "opensbi"
command(
[
"make",
f"O={opensbi_output}",
f"CROSS_COMPILE={cross_compile}",
"PLATFORM=retrosoc_hp",
f"PLATFORM_DIR={external / 'opensbi'}",
"PLATFORM_RISCV_XLEN=64",
"PLATFORM_RISCV_ABI=lp64d",
"PLATFORM_RISCV_ISA=rv64imafdc_zicsr_zifencei",
"FW_TEXT_START=0x38000000",
"FW_JUMP=y",
"FW_JUMP_ADDR=0x38400000",
"FW_JUMP_FDT_ADDR=0x38080000",
f"-j{args.jobs}",
],
opensbi,
)
copies = {
opensbi_output / "platform/retrosoc_hp/firmware/fw_jump.bin": image_dir / "fw_jump.bin",
linux_output / "arch/riscv/boot/Image": image_dir / "Image",
}
for source, destination in copies.items():
if not source.is_file():
raise FileNotFoundError(f"HP Linux build output is missing: {source}")
shutil.copy2(source, destination)
require_rv64_elf(opensbi_output / "platform/retrosoc_hp/firmware/fw_jump.elf")
manifest: dict[str, object] = {
"schema_version": 1,
"xlen": 64,
"abi": "lp64d",
"initramfs": "minimal-static-uncompressed",
"boot_flow": "opensbi-fw_jump",
"hart_id": 1,
"sources": {
"buildroot": source_revision(buildroot),
"linux": source_revision(linux),
"opensbi": source_revision(opensbi),
},
"artifacts": {},
}
artifacts = manifest["artifacts"]
assert isinstance(artifacts, dict)
for name, (address, maximum) in LAYOUT.items():
path = image_dir / name
size = path.stat().st_size
if size > maximum:
raise ValueError(f"{name} is {size} bytes; maximum is {maximum}")
artifacts[name] = {
"address": f"0x{address:08X}",
"maximum_bytes": maximum,
"size_bytes": size,
"sha256": sha256(path),
}
(image_dir / "manifest.json").write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--root", type=Path, required=True)
parser.add_argument("--buildroot", type=Path, required=True)
parser.add_argument("--linux", type=Path, required=True)
parser.add_argument("--opensbi", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--jobs", type=int, default=1)
build(parser.parse_args())
if __name__ == "__main__":
main()