From 11fa753f11bc1ea573dc68f253984beca517f125 Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 12 May 2026 23:05:49 +0800 Subject: [PATCH 01/53] kconfig --- .github/workflows/ci.yml | 2 +- .gitignore | 6 +- Cargo.toml | 56 ----- Jenkinsfile | 40 ++- Makefile | 68 +++-- build.rs | 153 ++++++++--- docs/commit-40d2868-kconfig-migration.md | 237 ++++++++++++++++++ docs/kconfig.md | 90 +++++++ docs/rust-cfgs.md | 102 ++++++++ kconfig/Kconfig | 177 +++++++++++++ kconfig/cfg_map.toml | 36 +++ kernel_build/bin/python | 1 + kernel_build/bin/python3 | 1 + kernel_build/bin/python3.12 | 1 + kernel_build/lib64 | 1 + kernel_build/pyvenv.cfg | 5 + platform/aarch64/imx8mp/kconfig/defconfig | 3 + platform/aarch64/imx8mp/platform.mk | 2 +- platform/aarch64/ok6254-c/kconfig/defconfig | 3 + platform/aarch64/phytium-pi/kconfig/defconfig | 3 + platform/aarch64/phytium-pi/platform.mk | 2 +- platform/aarch64/qemu-gicv2/kconfig/defconfig | 7 + platform/aarch64/qemu-gicv3/kconfig/defconfig | 7 + platform/aarch64/rk3568/kconfig/defconfig | 6 + platform/aarch64/rk3588/kconfig/defconfig | 3 + .../aarch64/sysoul_x3300/kconfig/defconfig | 3 + platform/aarch64/zcu102/kconfig/defconfig | 3 + .../loongarch64/ls3a5000/kconfig/defconfig | 7 + .../loongarch64/ls3a6000/kconfig/defconfig | 7 + .../hifive-premier-p550/kconfig/defconfig | 5 + platform/riscv64/megrez/kconfig/defconfig | 3 + platform/riscv64/qemu-aia/kconfig/defconfig | 8 + platform/riscv64/qemu-plic/kconfig/defconfig | 6 + platform/riscv64/ur-dp1000/kconfig/defconfig | 3 + platform/x86_64/ecx-2300f-peg/board.rs | 2 +- .../x86_64/ecx-2300f-peg/kconfig/defconfig | 4 + platform/x86_64/nuc14mnk/board.rs | 2 +- platform/x86_64/nuc14mnk/kconfig/defconfig | 7 + platform/x86_64/qemu/board.rs | 2 +- platform/x86_64/qemu/kconfig/defconfig | 7 + src/arch/riscv64/cpu.rs | 6 +- src/arch/riscv64/ipi.rs | 8 +- src/arch/riscv64/trap.rs | 6 +- src/arch/x86_64/boot.rs | 4 +- src/arch/x86_64/cpu.rs | 2 +- src/arch/x86_64/entry.rs | 2 +- src/arch/x86_64/graphics.rs | 12 +- src/arch/x86_64/pio.rs | 2 +- src/arch/x86_64/s2pt.rs | 2 +- src/device/iommu/arm_smmu/smmu_hw.rs | 4 +- src/device/iommu/iommu_impl.rs | 8 +- src/device/iommu/mod.rs | 18 +- src/device/iommu/riscv_iommu/iommu_hw.rs | 16 +- src/device/irqchip/ls7a2000/chip.rs | 2 +- src/device/irqchip/mod.rs | 50 ++-- src/device/irqchip/pic/mod.rs | 2 +- src/device/irqchip/plic/plic.rs | 2 +- src/device/mod.rs | 4 +- src/device/uart/mod.rs | 26 +- src/device/uart/uart16550a.rs | 2 +- src/event.rs | 2 +- src/logging.rs | 8 +- src/main.rs | 6 +- src/pci/config_accessors/mod.rs | 14 +- src/pci/mod.rs | 11 + src/pci/pci_config.rs | 74 ++---- src/pci/pci_handler.rs | 12 +- src/pci/pci_struct.rs | 12 +- src/pci/pci_test.rs | 4 +- src/platform/mod.rs | 2 +- src/zone.rs | 22 +- tools/gen_cargo_config.sh | 60 ----- tools/gen_vscode_settings.sh | 62 ----- tools/kconfig/bootstrap_defconfigs.py | 86 +++++++ tools/kconfig/bootstrap_venv.sh | 22 ++ tools/kconfig/host_config.sh | 116 +++++++++ tools/kconfig/kconfig_cli.py | 164 ++++++++++++ tools/kconfig/requirements.txt | 3 + tools/kconfig/save_defconfig.sh | 17 ++ .../vendor/PySocks-1.7.1-py3-none-any.whl | Bin 0 -> 16725 bytes .../kconfiglib-14.1.0-py2.py3-none-any.whl | Bin 0 -> 145875 bytes tools/read_features.sh | 12 - 82 files changed, 1521 insertions(+), 447 deletions(-) create mode 100644 docs/commit-40d2868-kconfig-migration.md create mode 100644 docs/kconfig.md create mode 100644 docs/rust-cfgs.md create mode 100644 kconfig/Kconfig create mode 100644 kconfig/cfg_map.toml create mode 120000 kernel_build/bin/python create mode 120000 kernel_build/bin/python3 create mode 120000 kernel_build/bin/python3.12 create mode 120000 kernel_build/lib64 create mode 100644 kernel_build/pyvenv.cfg create mode 100644 platform/aarch64/imx8mp/kconfig/defconfig create mode 100644 platform/aarch64/ok6254-c/kconfig/defconfig create mode 100644 platform/aarch64/phytium-pi/kconfig/defconfig create mode 100644 platform/aarch64/qemu-gicv2/kconfig/defconfig create mode 100644 platform/aarch64/qemu-gicv3/kconfig/defconfig create mode 100644 platform/aarch64/rk3568/kconfig/defconfig create mode 100644 platform/aarch64/rk3588/kconfig/defconfig create mode 100644 platform/aarch64/sysoul_x3300/kconfig/defconfig create mode 100644 platform/aarch64/zcu102/kconfig/defconfig create mode 100644 platform/loongarch64/ls3a5000/kconfig/defconfig create mode 100644 platform/loongarch64/ls3a6000/kconfig/defconfig create mode 100644 platform/riscv64/hifive-premier-p550/kconfig/defconfig create mode 100644 platform/riscv64/megrez/kconfig/defconfig create mode 100644 platform/riscv64/qemu-aia/kconfig/defconfig create mode 100644 platform/riscv64/qemu-plic/kconfig/defconfig create mode 100644 platform/riscv64/ur-dp1000/kconfig/defconfig create mode 100644 platform/x86_64/ecx-2300f-peg/kconfig/defconfig create mode 100644 platform/x86_64/nuc14mnk/kconfig/defconfig create mode 100644 platform/x86_64/qemu/kconfig/defconfig delete mode 100755 tools/gen_cargo_config.sh delete mode 100755 tools/gen_vscode_settings.sh create mode 100644 tools/kconfig/bootstrap_defconfigs.py create mode 100755 tools/kconfig/bootstrap_venv.sh create mode 100755 tools/kconfig/host_config.sh create mode 100644 tools/kconfig/kconfig_cli.py create mode 100644 tools/kconfig/requirements.txt create mode 100755 tools/kconfig/save_defconfig.sh create mode 100644 tools/kconfig/vendor/PySocks-1.7.1-py3-none-any.whl create mode 100644 tools/kconfig/vendor/kconfiglib-14.1.0-py2.py3-none-any.whl delete mode 100755 tools/read_features.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0510837db..9e64cdb30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -216,7 +216,7 @@ jobs: board: "qemu" runs-on: ["self-hosted", "X64"] qemu_version: "9.2.3" - # Because other features need to be customized scripts + # Board scripts use kconfig/defconfig (see docs/kconfig.md) steps: - name: Checkout Repository uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index d2f815798..ae99c1c40 100644 --- a/.gitignore +++ b/.gitignore @@ -32,10 +32,12 @@ flash.img* .vscode/settings.json compile.sh src/platform/__board.rs -.config +.config* .cursorrules .vscode/extensions.json # systemtest rootfs1.zip* jenkins/__pycache__ -jenkins-cli.jar \ No newline at end of file +jenkins-cli.jar +tools/kconfig/.venv/ +tools/kconfig/__pycache__/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 89b4cd1fc..e26316e0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,62 +47,6 @@ acpi = "=5.2.0" uguid = "=2.2.0" uefi-raw = "=0.9.0" -[features] -############# general ############## -iommu = [] # supported by: aarch64, riscv64 -pci = [] # supported by: aarch64, riscv64,loongarch64 -share_s2pt = ["iommu"] -print_timestamp = [] # print timestamp when logging - -############# IOMMU ############## -riscv_iommu = ["iommu"] -arm_smmu = ["iommu"] -intel_vtd = ["iommu"] - -############# PCIe access mechanism ############## -ecam_pcie = [] # Standard ECAM mechanism (default for most platforms) -dwc_pcie = [] # DesignWare PCIe Core mechanism (CFG0/CFG1, used by RK3568) -loongarch64_pcie = [] # LoongArch PCIe mechanism (used by LoongArch platforms) -no_pcie_bar_realloc = [] - -############# aarch64 ############## -# irqchip driver -gicv2 = [] -gicv3 = [] - -# uart driver -pl011 = [] -xuartps = [] -imx_uart = [] -uart_16550 = [] -sifive_ccache = [] -eic7700_sysreg = [] -uart16550a = [] - -############## riscv64 ############# -# irqchip driver -plic = [] -dp1000_plic = [] -aia = [] -aclint = [] -# extensions -sstc = [] -# platform specific -eic770x_soc = [] - -########### loongarch64 ############ -# irqchip driver -loongson_7a2000 = [] -# uart driver -loongson_uart = [] -# cpu (not used for now) -loongson_3a5000 = [] -loongson_3a6000 = [] - -############# x86_64 ############### -graphics = [] -split_screen = [] - [profile.dev] # panic = "abort" # avoid cargo test failure, this is a bug of cargo debug = 2 diff --git a/Jenkinsfile b/Jenkinsfile index 7ca0d711e..3b88a50bb 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -51,6 +51,18 @@ def normalizeToolArch(String arch) { return mapping.get(raw, raw) } +/** Kconfig: venv + tools/kconfig/kconfig_cli.py (via make defconfig). Keep in sync with Makefile. */ +def kconfigSetupShell(String arch, String board) { + return """ + export PATH=${env.CARGO_HOME}/bin:${env.TOOLCHAIN_PATHS}:\$PATH + chmod +x tools/kconfig/bootstrap_venv.sh tools/kconfig/host_config.sh tools/kconfig/save_defconfig.sh 2>/dev/null || true + if [ ! -x tools/kconfig/.venv/bin/python ]; then + ./tools/kconfig/bootstrap_venv.sh + fi + make defconfig ARCH=${arch} BOARD=${board} + """ +} + pipeline { agent any @@ -143,7 +155,8 @@ pipeline { } def arch = parts[0] def board = parts[1] - echo "Compile hvisor [BID=${env.BID}, ARCH=${arch}, BOARD=${board}]" + echo "Compile hvisor [BID=${env.BID}, ARCH=${arch}, BOARD=${board}] (Kconfig: kconfig_cli.py defconfig -> root .config; no Cargo --features)" + sh kconfigSetupShell(arch, board) if (arch != 'x86_64') { sh """ export PATH=${env.CARGO_HOME}/bin:${env.TOOLCHAIN_PATHS}:\$PATH @@ -219,6 +232,12 @@ pipeline { if (!arch || !board || !kdir || !mode) { error("jenkins/ci.yaml BID=${env.BID}: tests.mode and build_args ARCH/BOARD/KDIR are required") } + def bidPartsPrepare = (env.BID ?: '').split('/', 2) + if (bidPartsPrepare.size() == 2) { + if (arch != bidPartsPrepare[0] || board != bidPartsPrepare[1]) { + error("jenkins/ci.yaml BID=${env.BID}: build_args ARCH/BOARD (${arch}/${board}) must match matrix BID") + } + } if (mode == 'qemu') { def prepareScript = "jenkins/prepare.sh" @@ -256,13 +275,26 @@ pipeline { steps { dir(matrixCellDir()) { script { - def ci = loadCiYaml() - def bidCfg = getBidConfig(ci, env.BID) - echo "Run tests via ci_runner [BID=${env.BID}]" + def bidParts = (env.BID ?: '').split('/', 2) + if (bidParts.size() != 2) { + error("invalid BID for tests: ${env.BID}") + } + def tArch = bidParts[0] + def tBoard = bidParts[1] + def ciRun = loadCiYaml() + def bidCfgRun = getBidConfig(ciRun, env.BID) + def buildArgsRun = parseCiBuildArgs(bidCfgRun) + def yamlArchRun = (buildArgsRun.ARCH ?: '').toString() + def yamlBoardRun = (buildArgsRun.BOARD ?: '').toString() + if (yamlArchRun != tArch || yamlBoardRun != tBoard) { + error("jenkins/ci.yaml BID=${env.BID}: build_args ARCH/BOARD (${yamlArchRun}/${yamlBoardRun}) must match matrix BID (${tArch}/${tBoard})") + } + echo "Run tests via ci_runner [BID=${env.BID}] (Kconfig: same as Compile — kconfig_cli via make defconfig)" sh """ export TERM=\${TERM:-xterm} export PATH=${env.CARGO_HOME}/bin:${env.TOOLCHAIN_PATHS}:\$PATH export PATH=${env.QEMU_PATH}:\$PATH + ${kconfigSetupShell(tArch, tBoard)} python3 jenkins/ci_runner.py \ --bid "${env.BID}" """ diff --git a/Makefile b/Makefile index f0034a09e..8169330dc 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,6 @@ STATS ?= off PORT ?= 2333 MODE ?= release BOARD ?= qemu-gicv3 -FEATURES= BID ?= ptest ?= @@ -23,15 +22,6 @@ else BOARD := $(shell echo $(BID) | cut -d'/' -f2) endif -# if user add FEATURES in environment, we use it -# else we use the default FEATURES in platform/$(ARCH)/$(BOARD)/cargo/features -ifeq ($(FEATURES),) - FEATURES := $(shell ./tools/read_features.sh $(ARCH) $(BOARD) || echo ERROR) - ifeq ($(FEATURES),ERROR) - $(error ERROR: Read FEATURES failed. Please check if ARCH="$(ARCH)" and BOARD="$(BOARD)" are correct.) - endif -endif - ifeq ($(ARCH),aarch64) RUSTC_TARGET := aarch64-unknown-none GDB_ARCH := aarch64 @@ -56,7 +46,6 @@ export ARCH export BOARD export BID export RUSTC_TARGET -export FEATURES # Build paths build_path := target/$(RUSTC_TARGET)/$(MODE) @@ -69,7 +58,6 @@ systemtest_tdownload := $(test_dir)/systemtest/tdownload_all.sh # Build arguments build_args := -build_args += --features "$(FEATURES)" build_args += --target $(RUSTC_TARGET) build_args += -Z build-std=core,alloc build_args += -Z build-std-features=compiler-builtins-mem @@ -86,16 +74,44 @@ COLOR_BLUE := $(shell tput setaf 4) COLOR_BOLD := $(shell tput bold) COLOR_RESET := $(shell tput sgr0) +# Defconfig / menuconfig: see tools/kconfig/kconfig_cli.py +kconfig_python := tools/kconfig/.venv/bin/python + # Targets -.PHONY: all elf disa run gdb monitor clean tools rootfs vscode ci-run -all: clean_check gen_cargo_config vscode $(hvisor_bin) +.PHONY: all elf disa run gdb monitor clean tools rootfs vscode ci-run defconfig menuconfig savedefconfig ensure_config +ensure_config: + @if [ ! -f .config ]; then \ + echo "$(COLOR_YELLOW)No .config; running defconfig for $(ARCH)/$(BOARD)$(COLOR_RESET)"; \ + $(MAKE) --no-print-directory defconfig; \ + fi + +defconfig: + @if [ ! -x $(kconfig_python) ]; then \ + echo "$(COLOR_RED)Missing tools/kconfig/.venv (kconfiglib).$(COLOR_RESET)"; \ + echo " ./tools/kconfig/bootstrap_venv.sh"; \ + exit 1; \ + fi + @$(kconfig_python) tools/kconfig/kconfig_cli.py defconfig + +menuconfig: + @if [ -x $(kconfig_python) ]; then \ + $(kconfig_python) tools/kconfig/kconfig_cli.py menuconfig; \ + else \ + echo "Install kconfiglib: ./tools/kconfig/bootstrap_venv.sh"; \ + exit 1; \ + fi + +savedefconfig: + @./tools/kconfig/save_defconfig.sh "$(ARCH)" "$(BOARD)" + +all: clean_check ensure_config gen_cargo_config vscode $(hvisor_bin) @printf "\n" @printf "$(COLOR_GREEN)$(COLOR_BOLD)hvisor build summary:$(COLOR_RESET)\n" @printf "%-10s %s\n" "ARCH =" "$(COLOR_BOLD)$(ARCH)$(COLOR_RESET)" @printf "%-10s %s\n" "BOARD =" "$(COLOR_BOLD)$(BOARD)$(COLOR_RESET)" @printf "%-10s %s\n" "BID =" "$(COLOR_BOLD)$(BID)$(COLOR_RESET)" @printf "%-10s %s\n" "LOG =" "$(COLOR_BOLD)$(LOG)$(COLOR_RESET)" - @printf "%-10s %s\n" "FEATURES =" "$(COLOR_BOLD)$(FEATURES)$(COLOR_RESET)" + @printf "%-10s %s\n" "DEFCONFIG =" "$(COLOR_BOLD)platform/$(ARCH)/$(BOARD)/kconfig/defconfig$(COLOR_RESET)" @printf "%-10s %s\n" "RUSTC_TARGET =" "$(COLOR_BOLD)$(RUSTC_TARGET)$(COLOR_RESET)" @printf "%-10s %s\n" "BUILD_PATH =" "$(COLOR_BOLD)$(build_path)$(COLOR_RESET)" @printf "%-10s %s\n" "HVISON_BIN_SIZE =" "$(COLOR_BOLD)$(shell du -h $(hvisor_bin) | cut -f1)$(COLOR_RESET)" @@ -113,8 +129,8 @@ clean_check: # if .config not exist, then everything is fine # else we read .config and parse ARCH and BOARD, if they are different, we clean the build @if [ -f ".config" ]; then \ - CONFIG_ARCH=$$(cat .config | grep "ARCH" | cut -d'=' -f2); \ - CONFIG_BOARD=$$(cat .config | grep "BOARD" | cut -d'=' -f2); \ + CONFIG_ARCH=$$(grep '^ARCH=' .config | head -1 | cut -d'=' -f2); \ + CONFIG_BOARD=$$(grep '^BOARD=' .config | head -1 | cut -d'=' -f2); \ if [ "$$CONFIG_ARCH" != "$(ARCH)" ] || [ "$$CONFIG_BOARD" != "$(BOARD)" ]; then \ echo "$(COLOR_YELLOW)$(COLOR_BOLD)ARCH or BOARD changed(OLD: $$CONFIG_ARCH/$$CONFIG_BOARD, NEW: $(ARCH)/$(BOARD)), cleaning...$(COLOR_RESET)"; \ ./tools/clean.sh; \ @@ -123,15 +139,17 @@ clean_check: gen_cargo_config: @printf "$(COLOR_GREEN)$(COLOR_BOLD)generating .cargo/config.toml...$(COLOR_RESET)\n" - ./tools/gen_cargo_config.sh + @chmod +x tools/kconfig/host_config.sh 2>/dev/null || true + ./tools/kconfig/host_config.sh cargo @printf "$(COLOR_GREEN)$(COLOR_BOLD)generating .cargo/config.toml success!$(COLOR_RESET)\n" vscode: @printf "$(COLOR_GREEN)$(COLOR_BOLD)generating .vscode/settings.json...$(COLOR_RESET)\n" - ./tools/gen_vscode_settings.sh + @chmod +x tools/kconfig/host_config.sh 2>/dev/null || true + ./tools/kconfig/host_config.sh vscode @printf "$(COLOR_GREEN)$(COLOR_BOLD)generating .vscode/settings.json success!$(COLOR_RESET)\n" -elf: +elf: ensure_config gen_cargo_config cargo build $(build_args) disa: @@ -175,7 +193,7 @@ fmt: all cargo fmt --all @echo "your code has been formatted" -clippy: +clippy: ensure_config gen_cargo_config cargo clippy $(build_args) flash-img: @@ -191,10 +209,10 @@ download-test-img: else echo "\nflash.img found\n"; \ fi -test: clean test-pre gen_cargo_config +test: clean test-pre ensure_config gen_cargo_config cargo test $(build_args) -vv -stest: clean test-pre gen_cargo_config +stest: clean test-pre ensure_config gen_cargo_config ./platform/$(ARCH)/$(BOARD)/test/systemtest/tcompiledtb.sh ./platform/$(ARCH)/$(BOARD)/test/systemtest/tdownload_all.sh ./platform/$(ARCH)/$(BOARD)/test/systemtest/trootfs_deploy.sh @@ -203,7 +221,7 @@ stest: clean test-pre gen_cargo_config # Performance benchmark: data collection only, does not affect pass/fail. # Calls systemtest scripts for DTS, then perftest/tdownload_all.sh when present # (fallback to systemtest/tdownload_all.sh), and perftest/trootfs_deploy.sh. -perf: clean test-pre gen_cargo_config +perf: clean test-pre ensure_config gen_cargo_config ./platform/$(ARCH)/$(BOARD)/test/systemtest/tcompiledtb.sh @if [ -x "$(perftest_tdownload)" ]; then \ $(perftest_tdownload); \ @@ -215,7 +233,7 @@ perf: clean test-pre gen_cargo_config # Prepare perf image only: reuse existing rootfs img when present, download # missing assets, deploy benchmark scripts/tools into image, and print path. -perf-prepare-img: clean test-pre gen_cargo_config +perf-prepare-img: clean test-pre ensure_config gen_cargo_config ./platform/$(ARCH)/$(BOARD)/test/systemtest/tcompiledtb.sh @if [ -x "$(perftest_tdownload)" ]; then \ $(perftest_tdownload); \ diff --git a/build.rs b/build.rs index 9fdedbbf7..5288ee847 100644 --- a/build.rs +++ b/build.rs @@ -14,6 +14,7 @@ // Authors: // Yulong Han // +use std::collections::{HashMap, HashSet}; use std::io::Write; use std::{env, fs, path::Path}; @@ -29,22 +30,18 @@ fn log(output: &str) { writeln!(file, "{}", output).expect("Failed to write to log file"); } -// .config -// ARCH=... -// BOARD=... struct BuildEnv { arch: String, board: String, bid: String, - features: String, } +// parse ARCH, BOAR and BID from .config fn parse_build_env(file_path: &str) -> BuildEnv { let file = fs::read_to_string(file_path).expect("Failed to read .config file"); let mut arch = String::new(); let mut board = String::new(); let mut bid = String::new(); - let mut features = String::new(); for line in file.lines() { let parts: Vec<&str> = line.split('=').collect(); if parts.len() != 2 { @@ -54,33 +51,81 @@ fn parse_build_env(file_path: &str) -> BuildEnv { "ARCH" => arch = parts[1].to_string(), "BOARD" => board = parts[1].to_string(), "BID" => bid = parts[1].to_string(), - "FEATURES" => features = parts[1].to_string(), _ => {} } } - BuildEnv { - arch, - board, - bid, - features, + BuildEnv { arch, board, bid } +} + +// load cfg_map.toml, which is generated by tools/kconfig/cfg_map.py +fn load_cfg_map(root: &Path) -> HashMap { + let path = root.join("kconfig/cfg_map.toml"); + let raw = fs::read_to_string(&path).unwrap_or_else(|_| panic!("missing {}", path.display())); + let mut m = HashMap::new(); + let mut in_symbols = false; + for line in raw.lines() { + let t = line.trim(); + if t.is_empty() || t.starts_with('#') { + continue; + } + if t == "[symbols]" { + in_symbols = true; + continue; + } + if t.starts_with('[') { + in_symbols = false; + continue; + } + if !in_symbols { + continue; + } + let Some((k, v)) = t.split_once('=') else { + continue; + }; + let k = k.trim().trim_matches('"'); + let v = v.trim().trim_matches('"'); + m.insert(k.to_string(), v.to_string()); + } + if m.is_empty() { + panic!("cfg_map.toml: no entries under [symbols]"); } + m +} + +// parse enabled config keys from .config +fn parse_enabled_config_keys(config_text: &str) -> HashSet { + let mut out = HashSet::new(); + for line in config_text.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let Some((key, val)) = line.split_once('=') else { + continue; + }; + let val = val.trim(); + if val != "y" && val != "m" { + continue; + } + if key.starts_with("CONFIG_") { + out.insert(key.to_string()); + } + } + out } fn main() { - // cleanup the log file let log_path = Path::new(BUILD_LOG_FILE); if log_path.exists() { fs::remove_file(log_path).expect("Failed to remove log file"); } let project_toml_root = env::var("CARGO_MANIFEST_DIR").unwrap(); - // read the .config file at the project root - let config_path = format!("{}/.config", project_toml_root); - let mut build_env = parse_build_env(&config_path); + let root = Path::new(&project_toml_root); + let config_path = root.join(".config"); + let mut build_env = parse_build_env(config_path.to_str().unwrap()); if !build_env.bid.is_empty() { - // BID=$ARCH/$BOARD, parse it - // update the build_env with the parsed values let parts: Vec<&str> = build_env.bid.split('/').collect(); if parts.len() != 2 { log(&format!("Invalid BID format: {}", build_env.bid)); @@ -91,35 +136,45 @@ fn main() { } build_env.arch = parts[0].to_string(); build_env.board = parts[1].to_string(); - } else { - log(&format!( - "BID environment variable not found, using ARCH and BOARD" - )); - if build_env.arch.is_empty() || build_env.board.is_empty() { - log(&format!("ARCH or BOARD environment variable not found")); - panic!( - "ARCH or BOARD environment variable not found, please check the log file({}) for more details", - BUILD_LOG_FILE - ); - } + } else if build_env.arch.is_empty() || build_env.board.is_empty() { + log("ARCH or BOARD missing in .config"); + panic!( + "ARCH or BOARD missing in .config (run: make defconfig), see {}", + BUILD_LOG_FILE + ); } let arch = build_env.arch; let board = build_env.board; let bid = build_env.bid; - let features = build_env.features; let pwd = env::current_dir().unwrap(); log(&format!("Current directory: {}", pwd.display())); + let cfg_map = load_cfg_map(root); + let config_text = fs::read_to_string(&config_path).expect("read .config"); + let enabled = parse_enabled_config_keys(&config_text); + + for rust_cfg in cfg_map.values().collect::>() { + println!("cargo:rustc-check-cfg=cfg({rust_cfg})"); + } + + for k in &enabled { + if let Some(rust_cfg) = cfg_map.get(k) { + println!("cargo:rustc-cfg={rust_cfg}"); + } else if k.starts_with("CONFIG_") { + log(&format!("warning: unknown config key in .config: {k}")); + } + } + let target_path_str = format!("{}/src/platform/__board.rs", pwd.display()); let target_path = Path::new(&target_path_str); let source_path_str = format!("{}/platform/{}/{}/board.rs", pwd.display(), arch, board); let source_path = Path::new(&source_path_str); log(&format!( - "Building for ARCH={} BOARD={}, BID={}, FEATURES={}", - arch, board, bid, features + "Building for ARCH={arch} BOARD={board}, BID={bid}, enabled_cfgs={}", + enabled.len() )); log(&format!( @@ -129,24 +184,40 @@ fn main() { )); if !source_path.exists() { - log(&format!("Invalid board.rs path: {}, make sure the ARCH and BOARD environment variables are set correctly", source_path_str)); - panic!( - "Invalid board.rs, please check the log file({}) for more details", - BUILD_LOG_FILE - ); + log(&format!("Invalid board.rs path: {source_path_str}")); + panic!("Invalid board.rs, please check the log file({BUILD_LOG_FILE}) for more details"); } - // soft link the board.rs to __board.rs if target_path.exists() { fs::remove_file(target_path).expect("Failed to remove existing __board.rs"); } std::os::unix::fs::symlink(source_path, target_path).expect("Failed to create symlink"); - log(&format!("Linking successful")); + log("Linking successful"); println!("cargo:rerun-if-env-changed=ARCH"); println!("cargo:rerun-if-env-changed=BOARD"); println!("cargo:rerun-if-env-changed=BID"); - println!("cargo:rerun-if-env-changed=FEATURES"); - println!("cargo:rerun-if-changed={}", source_path_str); - println!("cargo:rerun-if-changed={}", target_path_str); + println!("cargo:rerun-if-changed={}", config_path.display()); + println!( + "cargo:rerun-if-changed={}", + root.join("kconfig/cfg_map.toml").display() + ); + println!( + "cargo:rerun-if-changed={}", + root.join("kconfig/Kconfig").display() + ); + println!( + "cargo:rerun-if-changed={}", + root.join("tools/kconfig/kconfig_cli.py").display() + ); + println!( + "cargo:rerun-if-changed={}", + root.join("platform") + .join(&arch) + .join(&board) + .join("kconfig/defconfig") + .display() + ); + println!("cargo:rerun-if-changed={source_path_str}"); + println!("cargo:rerun-if-changed={target_path_str}"); } diff --git a/docs/commit-40d2868-kconfig-migration.md b/docs/commit-40d2868-kconfig-migration.md new file mode 100644 index 000000000..1650b1eaa --- /dev/null +++ b/docs/commit-40d2868-kconfig-migration.md @@ -0,0 +1,237 @@ +# Commit `40d2868`:从 Cargo `feature` 到 Kconfig 的完整说明 + +本文档基于 commit `40d2868cf97a96ca9c98d61430f90ec52375e22d`(message: `kconfig`),说明该提交如何将平台能力开关从 **Cargo `[features]` + 每板 `cargo/features` 文件** 迁移到 **Linux 风格的 Kconfig(`kconfig/Kconfig` + 每板 `kconfig/defconfig`)+ 根目录 `.config`**,以及各新增/修改文件的含义与必要性。 + +与日常用法更短的操作说明见同目录下的 [`kconfig.md`](./kconfig.md)。 + +--- + +## 一、动机与总体架构变化 + +### 1.1 旧模型(迁移前) + +- **`Cargo.toml` `[features]`**:声明大量空 feature(如 `pci`、`gicv3`),仅用于在编译期打开 `#[cfg(feature = "...")]`。 +- **`platform///cargo/features`**:每行一个 feature 名;`Makefile` 通过 `tools/read_features.sh` 读入,拼成 `cargo build --features "..."`。 +- **局限**:无统一菜单、无 `depends on` / `select` 语义、互斥关系只能靠文档或运行时;与内核/固件生态的 Kconfig 习惯不一致;`rust-analyzer` 与真实 `--features` 易脱节。 + +### 1.2 新模型(迁移后) + +| 层级 | 作用 | +|------|------| +| `kconfig/Kconfig` | 声明 `config` 符号、菜单、`default` / `depends on` / `select`(由 kconfiglib 解析)。 | +| `platform/.../kconfig/defconfig` | 每板默认打开的 `CONFIG_*=y` 列表(可版本管理)。 | +| 仓库根 `.config` | 生成物:前几行 **元数据**(`ARCH`/`BOARD`/`BID`/`HVISOR_SRC`/`LD_SCRIPT`/`TEMPLATE`)+ Kconfig 展开后的 `CONFIG_*` 行;**不入库**(`.gitignore`)。 | +| `kconfig/cfg_map.toml` | **`CONFIG_*` → Rust `cfg` 名** 的唯一映射表(与源码中 `#[cfg(...)]` 一致)。 | +| `build.rs` | 读 `.config` 与 `cfg_map.toml`,对启用的项打印 `cargo:rustc-cfg=...`,并输出 `cargo:rustc-check-cfg`;继续负责 `src/platform/__board.rs` 符号链接。 | +| `Makefile` | 在 `all`/`elf` 等目标前 **`ensure_config`**:若无 `.config` 则先 `defconfig`;**不再**向 cargo 传递 `--features`。 | + +**必要性**:把「板级选了哪些驱动/子系统」从 Cargo 的 package 级 feature 机制中解耦出来,换成与内核类似的配置表面,便于依赖约束、交互式修改、CI 矩阵统一用 `make defconfig` 生成 `.config`。 + +--- + +## 二、Cargo 与构建入口改动 + +### 2.1 删除 `Cargo.toml` 中的 `[features]` 整块 + +原先约 56 行 `[features]`(`iommu`、`pci`、`gicv2`/`gicv3`、各类 PCIe 访问后端、UART、RISC-V/LoongArch/x86 相关开关等)整段移除。 + +**必要性**:避免两套真相(既在 `Cargo.toml` 又在 Kconfig);编译期开关改由 `build.rs` 根据 `.config` 注入 `rustc --cfg`,Cargo 不再参与 feature 组合。 + +### 2.2 `Makefile` + +新增/调整要点: + +- **`kconfig_python`**:`tools/kconfig/.venv/bin/python`(本地 venv 中的 kconfiglib)。 +- **`defconfig` / `menuconfig` / `savedefconfig`**:调用 Python 脚本或 shell 维护 `.config` 与板级 `defconfig`。 +- **`ensure_config`**:根目录无 `.config` 时自动对该 `ARCH`/`BOARD` 执行 `defconfig`(需已 `bootstrap_venv.sh`)。 +- **`all` 依赖**:从 `clean_check gen_cargo_config ...` 变为 `clean_check ensure_config gen_cargo_config ...`;摘要行用 **`DEFCONFIG`** 路径代替 **`FEATURES`**。 +- **`elf` / `clippy` / `test` 等**:在合适处增加 `ensure_config`,保证 `cargo` 调用前已有 `.config`。 +- **移除**:`FEATURES` 变量、`read_features.sh` 调用、`build_args` 中的 `--features "$(FEATURES)"`。 + +**必要性**:构建入口与 CI 一致地先物化 `.config`,再交给 `build.rs` 转成 `rustc` cfg。 + +### 2.3 `tools/read_features.sh` 删除 + +该脚本仅用于拼接 `cargo --features` 的输入。 + +**必要性**:功能被 **`kconfig_cli.py defconfig`** + 根 `.config` 取代,删除避免误用旧路径。 + +### 2.4 `tools/kconfig/host_config.sh` + +合并原 **`tools/gen_cargo_config.sh`** 与 **`tools/gen_vscode_settings.sh`**:子命令 **`cargo`** 从根 **`.config`** 读取 `ARCH`/`BOARD`/`HVISOR_SRC`/`LD_SCRIPT`/`TEMPLATE` 并生成 **`.cargo/config.toml`**;子命令 **`vscode`** 先按当前 `ARCH`/`BOARD`(或 `BID`)调用 **`kconfig_cli.py defconfig`** 刷新 `.config`,再调用 **`kconfig_cli.py vscode-cfgs`** 写 **`.vscode/settings.json`**。脚本始终 `cd` 到仓库根,可从任意 cwd 调用。 + +**必要性**:与 Kconfig 相关的宿主机侧配置集中在一个目录;减少 `tools/` 顶层脚本数量。 + +**注意**:`cargo` 子命令须先存在 `.config`(例如 `make defconfig`)。 + +### 2.5 `tools/kconfig/merge_config.py` 删除 + +与 **`kconfig_cli.py defconfig` + 环境变量 `ARCH`/`BOARD`** 重复;需要命令行板型时可直接 **`ARCH=… BOARD=…`** 调用 **`kconfig_cli.py defconfig`**。 + +--- + +## 三、`build.rs` 行为(核心) + +相对旧版,主要新增/强化逻辑: + +1. **`load_cfg_map`**:解析 `kconfig/cfg_map.toml` 的 `[symbols]` 表。 +2. **`parse_enabled_config_keys`**:扫描 `.config` 中值为 `y` 或 `m` 的 `CONFIG_*` 键。 +3. 对每个映射到的 **Rust cfg 名** 打印 `cargo:rustc-check-cfg=cfg(...)`(配合 Rust 1.80+ 对自定义 cfg 的检查,减少拼写错误)。 +4. 对每个启用的 `CONFIG_*`,若存在于映射表则 `cargo:rustc-cfg=`;未知 `CONFIG_*` 写日志警告。 +5. **`rerun-if-changed`**:除原 `.config` 外,增加 `cfg_map.toml`、`Kconfig`、`tools/kconfig/kconfig_cli.py`、对应板 `kconfig/defconfig`、`board.rs` 等,保证配置变更会触发重新构建脚本。 + +仍保留:从 `.config` 解析 `ARCH`/`BOARD`/`BID`、`__board.rs` 符号链接、`target/build_rs.log` 日志。 + +**必要性**:在 **不修改 `Cargo.toml` features** 的前提下,把 Kconfig 输出接到 Rust 的条件编译;映射表集中管理,避免在 `build.rs` 里硬编码长列表。 + +--- + +## 四、Kconfig 与映射文件 + +### 4.1 `kconfig/Kconfig` + +定义与旧 feature 集对应的 `config` 符号(如 `IRQ_GICV2`/`IRQ_GICV3`、`PCI`、`PCIE_*`、`IOMMU` 及各硬件后端、`PL011`、`AIA`、`GRAPHICS` 等),并使用 `depends on` / `select` 表达依赖(例如 `SHARE_S2PT` `select IOMMU`,PCIe 子选项 `depends on PCI`)。 + +**必要性**:提供可校验的配置空间;`kconfig_cli.py defconfig` / `menuconfig` 加载 defconfig 时会应用这些规则。 + +### 4.2 `kconfig/cfg_map.toml` + +键为 `.config` 中的 **`CONFIG_*` 字面量**,值为源码中使用的 **自定义 cfg 标识符**(如 `CONFIG_IRQ_GICV3` → `irq_gicv3`)。 + +**必要性**:Kconfig 符号名习惯全大写加前缀,而 Rust `cfg` 惯用小写蛇形;单层映射表避免二者混用导致难以检索。 + +**命名调整说明(相对旧 Cargo feature)**:ARM GIC 由原先的 feature 名 `gicv2` / `gicv3` 改为 **`irq_gicv2` / `irq_gicv3`**(与 `CONFIG_IRQ_GICV2` 等对齐),源码中所有 `#[cfg(feature = "gicv3")]` 类用法改为 `#[cfg(irq_gicv3)]` 等。其余多数 cfg 名与旧 feature 名一致或仅增加 `CONFIG_` 前缀映射。 + +### 4.3 各板 `platform/.../kconfig/defconfig` + +由迁移脚本或手工从原 `cargo/features` 生成;内容为 `CONFIG_*=y` 行(排序便于 diff)。 + +**必要性**:板级默认配置仍与旧行为对齐,但格式与内核 defconfig 一致,便于 review。 + +部分 `platform.mk` 等若曾引用旧路径,会改为指向 `kconfig/defconfig` 或相关说明(以具体板文件为准)。 + +--- + +## 五、Python / Shell 工具链(新增代码含义) + +### 5.1 `tools/kconfig/requirements.txt` + `vendor/*.whl` + +依赖 **kconfiglib**(及 **PySocks**,供代理场景)。`bootstrap_venv.sh` 使用 **`--no-index --find-links=vendor`** 安装,避免在仅 `socks5://` 代理环境下首次 `pip install` 因缺少 PySocks 失败(Jenkins 常见)。 + +**必要性**:可复现、可离线的工具链引导;与 [`kconfig.md`](./kconfig.md) 中说明一致。 + +### 5.2 `tools/kconfig/bootstrap_venv.sh` + +创建 `tools/kconfig/.venv` 并从 `vendor/` 安装 wheel。 + +**必要性**:统一团队与 CI 的 Python 环境;`.gitignore` 忽略 `.venv/`,避免把虚拟环境提交进 Git。 + +### 5.3 `tools/kconfig/kconfig_cli.py` + +统一入口(`argparse` 子命令): + +- **`defconfig`**:读环境变量 **`ARCH`/`BOARD`**,加载 **`kconfig/Kconfig`** 与 **`platform/.../kconfig/defconfig`**,校验 **Kconfig 中四选一架构** 与路径一致后,写根目录 **`.config`**(Kconfig 主体 + 元数据 `ARCH`/`BOARD`/`BID`/`HVISOR_SRC`/`LD_SCRIPT`/`TEMPLATE`)。 +- **`menuconfig`**:终端 UI;保存后同样写回带元数据的 **`.config`**。仓库内**不要**再增加名为 **`menuconfig.py`** 的脚本,以免遮蔽 kconfiglib 的同名模块。 +- **`vscode-cfgs <.config> `**:输出 **rust-analyzer** 用的 **`rustc` cfg** JSON 数组(`tomllib` + 映射表)。 + +**必要性**:减少分散的 Python 小文件,便于维护与 `build.rs` 的 `rerun-if-changed` 指向单一脚本。 + +### 5.4 `tools/kconfig/save_defconfig.sh` + +从根 `.config` 中 `grep '^CONFIG_'` 排序写回 `platform/$ARCH/$BOARD/kconfig/defconfig`。 + +**必要性**:与内核 `savedefconfig` 工作流类似,便于把交互或本地调试结果固化进仓库。 + +### 5.5 `tools/kconfig/bootstrap_defconfigs.py` + +遍历仍存在的 `platform/*/.../cargo/features`,通过内置 **`FEATURE_LINES`** 映射表把旧 feature **token** 转为 `CONFIG_*=y`,写入对应 `kconfig/defconfig`;并为目录名对应的架构追加 **`CONFIG_ARCH_*=y`**。PCIe 后端默认值由 **`kconfig/Kconfig`** 中 `choice` 的 **`default PCIE_ECAM`** 在后续 **`kconfig_cli.py defconfig`** 时处理。 + +**必要性**:一次性从旧目录结构批量生成新 defconfig,降低手工迁移错误率。 + +--- + +## 六、Rust 源码层面的改动模式 + +### 6.1 条件编译属性 + +- **由** `#[cfg(feature = "xxx")]` **改为** `#[cfg(xxx)]`(`xxx` 为 `cfg_map.toml` 右侧字符串)。 +- **`cfg(feature = ...)` 与 `cfg(all(...))`**:凡与平台驱动相关的,一律改为自定义 cfg 名(与 `build.rs` 输出一致)。 + +### 6.2 `src/pci/mod.rs` 等处的 `compile_error!` + +在原先互斥依赖人工约束的基础上,用 **`#[cfg(all(ecam_pcie, dwc_pcie))]`** 等组合在编译期直接报错(与 Kconfig 的互斥可双重保险;Kconfig 侧若未完全禁止组合,Rust 侧仍可兜底)。 + +**必要性**:PCIe 访问后端多选一,错误组合应尽早失败。 + +### 6.3 其它模块(`src/device/*`、`src/arch/*`、`src/zone.rs`、`src/logging.rs` 等) + +同一模式:按板能力用 `#[cfg(pci)]`、`#[cfg(iommu)]`、`#[cfg(irq_gicv3)]` 等包裹模块、impl 或代码路径。 + +**必要性**:语义与旧 feature 一致,仅配置来源改变。 + +### 6.4 `src/main.rs` / `src/platform/mod.rs` + +确保启动路径读取的板级信息与新的配置流一致(具体 diff 以该提交为准,通常为 cfg 名或条件分支对齐)。 + +--- + +## 七、IDE、CI 与 x86 板级小改 + +### 7.1 `tools/kconfig/host_config.sh`(`vscode`) + +在生成 `settings.json` 前调用 **`kconfig_cli.py defconfig`** 刷新 `.config`,再用 **`kconfig_cli.py vscode-cfgs`** 生成 `rust-analyzer.cargo.cfgs`(`make vscode`)。 + +**必要性**:保证编辑器与当前 `ARCH`/`BOARD` 及 Kconfig 展开结果一致。 + +### 7.2 GitHub Actions / `Jenkinsfile` + +- **GitHub Actions**:该提交中 **仅修改一条注释**,说明板级脚本与 **`kconfig/defconfig`** 的关系(并指向 `docs/kconfig.md`);矩阵与安装步骤未变。本地/CI 要成功执行 **`make defconfig` / `ensure_config`**,仍需可用的 **`tools/kconfig/.venv`**(见 `bootstrap_venv.sh`);是否在 GHA 各 job 里显式引导 venv 以当前仓库 workflow 为准。 +- **Jenkinsfile**:矩阵构建单元内增加 **`bootstrap_venv.sh`** + **`make defconfig ARCH=... BOARD=...`**,日志中明确不再使用 **`cargo --features`**。 + +**必要性**:Jenkins 在干净工作区中先创建 venv 再生成 `.config`,与「根 `.config` 不入库」模型一致。 + +### 7.3 `platform/x86_64/*/board.rs` + +个别板文件小改(通常与条件编译或默认配置引用路径对齐,见该提交 diff)。 + +--- + +## 八、文档与忽略规则 + +- **`docs/kconfig.md`**:新增,描述命令、目录布局、`menuconfig` 常见问题、与 `rust-analyzer` 的关系、Jenkins 行为等(用户日常查阅)。 +- **`.gitignore`**:增加 `tools/kconfig/.venv/`、`tools/kconfig/__pycache__/` 等,避免将本地 Python 环境提交入库。 + +--- + +## 九、提交中带有的 `kernel_build/` 目录说明 + +该 commit 的统计中包含 `kernel_build/` 下的 `pyvenv.cfg`、指向 Python 的 symlink 等。通常 **虚拟环境不应入库**;若仓库后续已删除或 `.gitignore` 已屏蔽,以当前主干为准。若仍保留,建议团队确认是否为误提交或是否有独立用途(一般推荐仅保留 `tools/kconfig/.venv` 由脚本本地生成)。 + +--- + +## 十、文件改动分类速查(按职责) + +| 类别 | 路径示例 | +|------|-----------| +| Kconfig 定义 | `kconfig/Kconfig` | +| 映射表 | `kconfig/cfg_map.toml` | +| 板级默认 | `platform/**/kconfig/defconfig` | +| 构建脚本 | `Makefile`, `build.rs`, `tools/kconfig/host_config.sh` | +| Kconfig 工具 | `tools/kconfig/kconfig_cli.py`、`bootstrap_venv.sh`、`save_defconfig.sh`、`host_config.sh`、`requirements.txt`、`vendor/*.whl` | +| 迁移辅助 | `tools/kconfig/bootstrap_defconfigs.py` | +| 源码 cfg 切换 | `src/**/*.rs`(属性由 `feature` 改为自定义 cfg) | +| 包定义 | `Cargo.toml`(移除 `[features]`) | +| 文档 | `docs/kconfig.md`、本文档 | + +--- + +## 十一、小结 + +| 问题 | 答案 | +|------|------| +| 配置真相在哪里? | 板级 `kconfig/defconfig` + `kconfig/Kconfig` 规则;生成根 `.config`;`cfg_map.toml` 定义到 Rust cfg 的映射。 | +| Cargo 还参与开关吗? | 不参与;`build.rs` 根据 `.config` 注入 `rustc-cfg`。 | +| 为何引入 Python? | 复用 **kconfiglib** 做 `defconfig`/`menuconfig` 与依赖展开,与手写 parser 相比更可靠。 | +| 旧 `cargo/features` 怎么办? | 可用 `bootstrap_defconfigs.py` 批量转换;长期以 `kconfig/defconfig` 为准。 | + +若仅需操作步骤与故障排除,请优先阅读 [`kconfig.md`](./kconfig.md)。 diff --git a/docs/kconfig.md b/docs/kconfig.md new file mode 100644 index 000000000..4a73d3a7e --- /dev/null +++ b/docs/kconfig.md @@ -0,0 +1,90 @@ +# 平台配置(Kconfig / defconfig) + +hvisor 的平台能力开关由 **Kconfig 符号**(写入根目录 `.config` 的 `CONFIG_*=y` 行)描述,构建时由 [`build.rs`](../build.rs) 读入并转换为 `rustc --cfg`(例如 `irq_gicv3`、`pci`)。源码中使用 `#[cfg(irq_gicv3)]` 等形式,**不再**使用 Cargo 的 `feature`。 + +## 根目录 `.config` 是怎么来的(流程与为何「多段拼接」) + +1. **入口**:`make defconfig` 或 `make menuconfig` 结束时,在仓库根需要一份 **`.config`**,供 `cargo`/`build.rs` 与其它脚本使用。 +2. **板级输入**:只读 **`platform///kconfig/defconfig`**。这里的 **`ARCH`/`BOARD` 来自 Makefile 导出的环境变量**(或手动设置后调用 **`kconfig_cli.py defconfig`**),用来**定位文件路径**;不要求把 `ARCH=` 写进 defconfig 文件本身。 +3. **为何不把 `ARCH`/`BOARD` 写进 defconfig?** + - defconfig 交给 **kconfiglib** 解析,内容应是 **`CONFIG_*=y`**(以及 `#` 注释),与 Linux 习惯一致;`ARCH=` 不是 Kconfig 符号,混进去要么要在加载前剥离,要么污染 `menuconfig`/`savedefconfig` 的语义。 + - 目录 **`platform///`** 已经唯一确定了板型,再在 defconfig 里重复一遍容易与路径不同步。 +4. **拼接内容**:[`tools/kconfig/kconfig_cli.py`](../tools/kconfig/kconfig_cli.py) 在写出 Kconfig 主体后,在文件**开头**追加几行 **Makefile/脚本 约定的元数据**(`ARCH`、`BOARD`、`BID`、`HVISOR_SRC`、`LD_SCRIPT`、`TEMPLATE` 的绝对路径)。这样一份文件同时满足: + - **kconfiglib**:主体仍是标准 `CONFIG_*`; + - **`build.rs`**:读 `ARCH`/`BOARD` 做 `__board.rs` 链接; + - **[`tools/kconfig/host_config.sh`](../tools/kconfig/host_config.sh)**(子命令 `cargo`):从同一份 `.config` 读取上述路径生成 `.cargo/config.toml`,**不再**根据环境变量重复拼 `platform/$ARCH/$BOARD/...`(避免两处维护)。 + +**一句话**:defconfig 只管 Kconfig;「哪块板」由**路径 + 调用参数**决定;根 `.config` 是 **元数据 + Kconfig 展开结果** 的合并文件,减少脚本重复逻辑。 + +## 常用命令 + +- **生成当前 ARCH/BOARD 的 `.config`**(合并元数据 + `CONFIG_*`): + + ```bash + make defconfig + # 或(需已创建 venv 并安装 kconfiglib) + ARCH= BOARD= tools/kconfig/.venv/bin/python tools/kconfig/kconfig_cli.py defconfig + ``` + +- **首次构建 / 切换板型**:根目录若无 `.config`,`make all` 会先 **`make defconfig`**(需 `tools/kconfig/.venv` + kconfiglib)。 + +- **交互菜单**(可选,需 Python venv + kconfiglib): + + ```bash + ./tools/kconfig/bootstrap_venv.sh + make menuconfig + ``` + + (Jenkins 等仅提供 `socks5://` 代理时,在线 `pip install` 会因缺少 PySocks 报错;`bootstrap_venv.sh` 从 `tools/kconfig/vendor/*.whl` 离线安装,可绕过该问题。若升级 `requirements.txt` 中的版本,请同步更新 vendor 目录:`python3 -m pip download -d tools/kconfig/vendor -r tools/kconfig/requirements.txt`。) + +- **`make menuconfig` 失败而 `make defconfig` 正常** + 常见原因之一是曾将入口脚本命名为 `menuconfig.py`,与 kconfiglib 安装在 `site-packages` 里的 **`menuconfig` 模块同名**,`sys.path` 会优先加载仓库脚本,导致 `import menuconfig` 失败;入口已统一为 **`kconfig_cli.py menuconfig`**(不要新增名为 `menuconfig.py` 的仓库脚本)。若仍报错且含 `_curses` / `curses`,说明解释器缺少 curses 支持,请按发行版安装对应包(例如 Debian/Ubuntu 使用 **`apt install python3-curses`**,包名随 Python 主版本可能为 `python3.12-curses` 等;Fedora 常为 **`dnf install python3-curses`**)。 + +- **把当前 `.config` 中的 `CONFIG_*` 行写回板级 defconfig**: + + ```bash + make savedefconfig + ``` + +## 文件布局 + +| 路径 | 说明 | +|------|------| +| [`kconfig/Kconfig`](../kconfig/Kconfig) | 含 **Target architecture**(4 选 1)及驱动等菜单 | +| [`tools/kconfig/kconfig_cli.py`](../tools/kconfig/kconfig_cli.py) | **`defconfig`**:生成根 `.config`;**`menuconfig`**:交互配置;**`vscode-cfgs`**:输出 rust-analyzer 用的 cfg JSON(由 `host_config.sh` 调用) | +| [`tools/kconfig/host_config.sh`](../tools/kconfig/host_config.sh) | `cargo`:写 `.cargo/config.toml`;`vscode`:刷新 `.config` 并写 `.vscode/settings.json`(`make gen_cargo_config` / `make vscode`) | +| [`tools/kconfig/bootstrap_venv.sh`](../tools/kconfig/bootstrap_venv.sh) | 创建 `.venv` 并离线安装 kconfiglib | +| [`tools/kconfig/save_defconfig.sh`](../tools/kconfig/save_defconfig.sh) | `make savedefconfig`:把 `CONFIG_*` 写回板级 defconfig | +| [`kconfig/cfg_map.toml`](../kconfig/cfg_map.toml) | `CONFIG_*` → Rust `cfg` 名映射表(**唯一真相**) | +| `platform///kconfig/defconfig` | 该板的默认 `CONFIG_*=y` 集合 | +| 根目录 `.config` | 本地生成,含 `ARCH`/`BOARD`/… 元数据 + Kconfig 输出(已被 `.gitignore`) | + +## 仅运行 `cargo build` 时 + +须先在仓库根生成 `.config`(例如安装 kconfiglib venv 后 **`make defconfig`**),否则 `build.rs` 会因缺少 `ARCH`/`BOARD` 报错。 + +## 修改或新增开关 + +1. 在 `kconfig/Kconfig` 中增加 `config` 项(若需 `menuconfig` 校验/依赖)。**目标架构**在 **`Target architecture`** 菜单中为四选一;板级 **`kconfig/defconfig`** 里请**直接写上**与目录 **`platform//`** 对应的一行 **`CONFIG_ARCH_*=y`**(与 Kconfig 一致即可,无需额外同步脚本)。 +2. 在 `kconfig/cfg_map.toml` 的 `[symbols]` 中增加 `CONFIG_FOO = "rust_cfg_name"`。 +3. 在 `build.rs` 中已通过映射表统一输出 `cargo:rustc-check-cfg`,一般**无需**再手写列表。 +4. 更新相关板的 `kconfig/defconfig`(或运行 `make menuconfig` 后 `make savedefconfig`)。 +5. 在 Rust 源码中使用 `#[cfg(rust_cfg_name)]`(完整列表见 [`rust-cfgs.md`](./rust-cfgs.md))。 + +## rust-analyzer + +运行 `make vscode` 会根据当前 `.config` 与 `cfg_map.toml` 生成 `.vscode/settings.json` 中的 `rust-analyzer.cargo.cfgs`,与真实编译开关对齐。 + +## 从旧版 `platform/.../cargo/features` 批量生成 defconfig + +若调整了 `cargo/features` 行并希望同步到 `kconfig/defconfig`,可在仓库根执行: + +```bash +python3 tools/kconfig/bootstrap_defconfigs.py +``` + +随后检查各板 `kconfig/defconfig` 并提交。 + +## Jenkins(`Jenkinsfile`) + +矩阵编译阶段会先执行 **`make defconfig ARCH=… BOARD=…`**,再 **`make dtb`**(非 x86)、最后 **`make all MODE=release`**。对 **`jenkins/ci.yaml`** 中配置了测试的 BID,**Run test cases** 阶段在调用 **`jenkins/ci_runner.py`** 前会再次保证 **`tools/kconfig/.venv`** 存在(缺失则运行 **`bootstrap_venv.sh`**)并执行 **`make defconfig`**,以便后续 **`make ci-run`**(依赖 `ensure_config` / `host_config.sh`)与 Kconfig 一致。`.config` 不入库,每个 matrix 工作副本在编译前都会从对应板的 `kconfig/defconfig` 生成;与旧版通过 `FEATURES` / `cargo --features` 传参的方式已脱钩。 diff --git a/docs/rust-cfgs.md b/docs/rust-cfgs.md new file mode 100644 index 000000000..a3d733169 --- /dev/null +++ b/docs/rust-cfgs.md @@ -0,0 +1,102 @@ +# hvisor 中使用的 Rust `cfg` 一览 + +本文说明 **由 Kconfig / `.config` 注入的自定义 `cfg` 名**(经 [`kconfig/cfg_map.toml`](../kconfig/cfg_map.toml) 与 [`build.rs`](../build.rs) 转为 `cargo:rustc-cfg=...`),以及源码里实际出现的 **`#[cfg(...)]` / `cfg!(...)`** 用法。自定义名须与 **`cfg_map.toml` 右侧字符串** 完全一致(否则 `build.rs` 会告警或 rust-analyzer 不一致)。 + +--- + +## 1. 自定义 `cfg`(Kconfig → `rustc --cfg`) + +下表 **`cfg` 列**为在 Rust 里写的标识符;**`CONFIG_*` 列**为 `.config` / defconfig 中的键。 + +| `cfg` 名 | `CONFIG_*` | 含义(简要) | 在 `src/` 中的使用 | +|----------|--------------|----------------|-------------------| +| `irq_gicv2` | `CONFIG_IRQ_GICV2` | AArch64 GICv2 | [`device/irqchip/mod.rs`](../src/device/irqchip/mod.rs)、[`pci/mod.rs`](../src/pci/mod.rs)(`compile_error` 互斥) | +| `irq_gicv3` | `CONFIG_IRQ_GICV3` | AArch64 GICv3 | 同上 | +| `plic` | `CONFIG_PLIC` | RISC-V PLIC | [`device/irqchip`](../src/device/irqchip/mod.rs)、[`arch/riscv64/trap.rs`](../src/arch/riscv64/trap.rs)、[`ipi.rs`](../src/arch/riscv64/ipi.rs) | +| `dp1000_plic` | `CONFIG_DP1000_PLIC` | PLIC 变体 | [`plic/plic.rs`](../src/device/irqchip/plic/plic.rs) `cfg!` | +| `aia` | `CONFIG_AIA` | RISC-V AIA | irqchip、trap | +| `aclint` | `CONFIG_ACLINT` | RISC-V ACLINT | irqchip、ipi | +| `sstc` | `CONFIG_SSTC` | RISC-V Sstc | [`arch/riscv64/cpu.rs`](../src/arch/riscv64/cpu.rs) `cfg!` | +| `eic770x_soc` | `CONFIG_EIC770X_SOC` | EIC770x SoC | irqchip `mmio_init` | +| `eic7700_sysreg` | `CONFIG_EIC7700_SYSREG` | EIC7700 系统寄存器 | [`device/mod.rs`](../src/device/mod.rs)、irqchip | +| `sifive_ccache` | `CONFIG_SIFIVE_CCACHE` | SiFive ccache | 同上 | +| `loongson_7a2000` | `CONFIG_LOONGSON_7A2000` | LoongArch 7A2000 中断 | **当前 `src/` 中无 `#[cfg(loongson_7a2000)]`**;LoongArch 中断路径由 **`target_arch = "loongarch64"`** 统一包含 [`ls7a2000`](../src/device/irqchip/ls7a2000)。该符号仍可由 Kconfig/defconfig 打开并传给 `rustc`,供后续或外设条件使用。 | +| `loongson_3a5000` | `CONFIG_LOONGSON_3A5000` | 3A5000 CPU 画像 | **当前 `src/` 中无引用**(仅映射与 defconfig) | +| `loongson_3a6000` | `CONFIG_LOONGSON_3A6000` | 3A6000 CPU 画像 | **当前 `src/` 中无引用** | +| `pl011` | `CONFIG_PL011` | ARM PL011 UART | [`device/uart/mod.rs`](../src/device/uart/mod.rs) | +| `imx_uart` | `CONFIG_IMX_UART` | i.MX UART | 同上 | +| `xuartps` | `CONFIG_XUARTPS` | Xilinx XUARTPS | 同上 | +| `uart_16550` | `CONFIG_UART_16550` | 16550 通用 | 同上 | +| `uart16550a` | `CONFIG_UART16550A` | 16550A(x86) | 同上、[`uart16550a.rs`](../src/device/uart/uart16550a.rs) | +| `loongson_uart` | `CONFIG_LOONGSON_UART` | Loongson UART | uart/mod | +| `pci` | `CONFIG_PCI` | PCI/PCIe 总开关 | [`main.rs`](../src/main.rs)、[`platform/mod.rs`](../src/platform/mod.rs)、[`zone.rs`](../src/zone.rs) 等 | +| `ecam_pcie` | `CONFIG_PCIE_ECAM` | ECAM 配置空间 | [`pci/*`](../src/pci/)、[`pci/config_accessors`](../src/pci/config_accessors/mod.rs)、`compile_error` 互斥 | +| `dwc_pcie` | `CONFIG_PCIE_DWC` | DesignWare PCIe | 同上、[`pci_handler.rs`](../src/pci/pci_handler.rs)、[`zone.rs`](../src/zone.rs) | +| `loongarch64_pcie` | `CONFIG_PCIE_LOONGARCH64` | LoongArch PCIe | pci 各模块、`config_accessors` | +| `no_pcie_bar_realloc` | `CONFIG_NO_PCIE_BAR_REALLOC` | 不重分配 BAR | [`pci_struct.rs`](../src/pci/pci_struct.rs)、[`pci_config.rs`](../src/pci/pci_config.rs) | +| `iommu` | `CONFIG_IOMMU` | IOMMU 总开关 | [`main.rs`](../src/main.rs)、[`zone.rs`](../src/zone.rs)、[`pci_config.rs`](../src/pci/pci_config.rs)、iommu 子模块等 | +| `share_s2pt` | `CONFIG_SHARE_S2PT` | 与 IOMMU 共享 Stage-2 页表 | [`pci_config.rs`](../src/pci/pci_config.rs) | +| `arm_smmu` | `CONFIG_ARM_SMMU` | ARM SMMU | [`device/iommu`](../src/device/iommu/) | +| `intel_vtd` | `CONFIG_INTEL_VTD` | Intel VT-d | iommu、[`arch/x86_64`](../src/arch/x86_64/)、[`pci_handler.rs`](../src/pci/pci_handler.rs)、[`irqchip/pic`](../src/device/irqchip/pic/mod.rs) | +| `riscv_iommu` | `CONFIG_RISCV_IOMMU` | RISC-V IOMMU | iommu | +| `graphics` | `CONFIG_GRAPHICS` | 帧缓冲 / 图形 | [`logging.rs`](../src/logging.rs)、[`arch/x86_64/entry.rs`](../src/arch/x86_64/entry.rs)、[`pio.rs`](../src/arch/x86_64/pio.rs)、[`uart16550a.rs`](../src/device/uart/uart16550a.rs) | +| `split_screen` | `CONFIG_SPLIT_SCREEN` | 分屏布局 | [`arch/x86_64/graphics.rs`](../src/arch/x86_64/graphics.rs)、[`boot.rs`](../src/arch/x86_64/boot.rs) | +| `print_timestamp` | `CONFIG_PRINT_TIMESTAMP` | 日志打时间戳 | [`logging.rs`](../src/logging.rs) | +| `extioi_debug` | `CONFIG_EXTIOI_DEBUG` | LoongArch EXTIOI 调试 | [`device/irqchip/ls7a2000/chip.rs`](../src/device/irqchip/ls7a2000/chip.rs) | + +**说明**:[`build.rs`](../build.rs) 会为 `cfg_map.toml` 中**每一个**映射值打印 `cargo:rustc-check-cfg=cfg(...)`,因此上表中的名即使暂未出现在 `#[cfg]` 里,也属于**合法自定义 cfg**(例如便于 `rust-analyzer` 与将来代码一致)。 + +此外,**`platform/*/*/board.rs`** 中也可能出现 **`#[cfg(all(graphics))]`** 等(随板级代码演进)。 + +--- + +## 2. 编译器内置 / 标准 `cfg`(非 Kconfig) + +源码中大量使用的内置条件(**不由** `cfg_map.toml` 提供): + +| 条件 | 典型用途 | +|------|-----------| +| `target_arch = "aarch64"` / `"riscv64"` / `"loongarch64"` / `"x86_64"` | 架构分支,常与上述自定义 `cfg` 组合为 `all(...)` | +| `test` | 单元测试、[`main.rs`](../src/main.rs)、[`panic.rs`](../src/panic.rs)、[`pci/mod.rs`](../src/pci/mod.rs)、[`tests.rs`](../src/tests.rs) | +| `target_pointer_width = "32"` / `"64"` | [`arch/x86_64/vmcs.rs`](../src/arch/x86_64/vmcs.rs) | +| `not(...)` | 与上述条件搭配,表示反向分支 | + +--- + +## 3. `cfg!` 宏(编译期布尔) + +| 表达式 | 文件 | +|--------|------| +| `cfg!(sstc)` | [`arch/riscv64/cpu.rs`](../src/arch/riscv64/cpu.rs) | +| `cfg!(iommu)` | [`zone.rs`](../src/zone.rs) | +| `cfg!(dp1000_plic)` | [`device/irqchip/plic/plic.rs`](../src/device/irqchip/plic/plic.rs) | + +--- + +## 4. `compile_error!` 中使用的互斥组合(`src/pci/mod.rs`) + +下列 **非法组合** 在编译期直接报错(与 Kconfig 中 PCIe `choice` 等互补): + +- `all(target_arch = "aarch64", irq_gicv2, irq_gicv3)` — GICv2 与 GICv3 不能同时启用 +- `all(ecam_pcie, dwc_pcie)`、`all(ecam_pcie, loongarch64_pcie)`、`all(dwc_pcie, loongarch64_pcie)` — PCIe 配置空间访问后端互斥 + +--- + +## 5. 维护约定 + +1. **新增 Kconfig 开关并在 Rust 里使用**:在 [`kconfig/Kconfig`](../kconfig/Kconfig) 增加 `config`,在 [`kconfig/cfg_map.toml`](../kconfig/cfg_map.toml) 增加 `CONFIG_*` → **`cfg` 名**,源码写 **`#[cfg(该名)]`**;一般**无需**改 `build.rs`(已按映射表统一 `rustc-cfg` / `rustc-check-cfg`)。 +2. **重命名 `cfg`**:同时改 **`cfg_map.toml`**、全仓库 **`#[cfg` / `cfg!`** 与文档。 +3. **IDE**:`make vscode` 通过 [`tools/kconfig/kconfig_cli.py`](../tools/kconfig/kconfig_cli.py) `vscode-cfgs` 子命令,按 **`.config` + `cfg_map.toml`** 生成 `rust-analyzer.cargo.cfgs`,应与上表一致。 + +--- + +## 6. 生成方式(便于以后重新扫描) + +在仓库根执行(仅供参考,输出需人工对照 Kconfig): + +```bash +rg '#\[cfg' src --no-heading -o | sed 's/#\[cfg(//;s/)\]//' | sort -u +rg 'cfg!\(' src --no-heading +``` + +以 **`cfg_map.toml`** 与 **`build.rs`** 为「配置侧真相」,以 **`rg '#\[cfg'`** 结果为「源码侧引用」;二者差异见上文 **§1 表内说明列**。 diff --git a/kconfig/Kconfig b/kconfig/Kconfig new file mode 100644 index 000000000..497585d90 --- /dev/null +++ b/kconfig/Kconfig @@ -0,0 +1,177 @@ +mainmenu "hvisor platform options" + +menu "Target architecture" +choice + prompt "CPU architecture" + default ARCH_AARCH64 + +config ARCH_AARCH64 + bool "aarch64 (ARM64)" + +config ARCH_RISCV64 + bool "riscv64 (RV64)" + +config ARCH_LOONGARCH64 + bool "loongarch64" + +config ARCH_X86_64 + bool "x86_64" + +endchoice +endmenu + +menu "General" +config IOMMU + bool "IOMMU support" + default n +config SHARE_S2PT + bool "Share stage-2 page tables with IOMMU" + select IOMMU + default n +config PRINT_TIMESTAMP + bool "Print timestamp in logs" + default n +endmenu + +menu "PCIe" +config PCI + bool "PCI/PCIe support" + default n + +choice + prompt "PCIe Architecture" + depends on PCI + default PCIE_ECAM + +config PCIE_ECAM + bool "ECAM PCIe" + +config PCIE_DWC + bool "DesignWare Core PCIe" + +config PCIE_LOONGARCH64 + bool "LoongArch PCIe" + depends on ARCH_LOONGARCH64 + +endchoice + +config NO_PCIE_BAR_REALLOC + bool "Do not reallocate PCIe BARs (board-specific)" + depends on PCI + default n +endmenu + +menu "IOMMU hardware" +config ARM_SMMU + bool "ARM SMMU" + depends on ARCH_AARCH64 + select IOMMU + default n +config INTEL_VTD + bool "Intel VT-d" + depends on ARCH_X86_64 + select IOMMU + default n +config RISCV_IOMMU + bool "RISC-V IOMMU" + depends on ARCH_RISCV64 + select IOMMU + default n +endmenu + +menu "ARM (AArch64) interrupt controller (GIC)" + depends on ARCH_AARCH64 + +config IRQ_GICV2 + bool "GICv2" + default n +config IRQ_GICV3 + bool "GICv3" + default n +endmenu + +menu "RISC-V interrupt controller / timer" + depends on ARCH_RISCV64 + +config PLIC + bool "PLIC" + default n +config DP1000_PLIC + bool "DP1000 PLIC variant" + depends on PLIC + default n +config AIA + bool "AIA (APLIC/IMSIC)" + default n +config ACLINT + bool "ACLINT (MTI/MSI)" + default n +config SSTC + bool "Sstc extension" + default n +config EIC770X_SOC + bool "EIC770x SoC integration" + default n +config EIC7700_SYSREG + bool "EIC7700 system registers" + depends on EIC770X_SOC + default n +config SIFIVE_CCACHE + bool "SiFive ccache (EIC770x)" + depends on EIC770X_SOC + default n +endmenu + +menu "LoongArch" + depends on ARCH_LOONGARCH64 + +config LOONGSON_7A2000 + bool "Loongson 7A2000 interrupt controller" + default n +config LOONGSON_3A5000 + bool "Loongson 3A5000 CPU profile" + default n +config LOONGSON_3A6000 + bool "Loongson 3A6000 CPU profile" + default n +endmenu + +menu "UART" +config PL011 + bool "ARM PL011 UART" + default n +config IMX_UART + bool "NXP i.MX UART" + default n +config XUARTPS + bool "Xilinx XUARTPS" + default n +config UART_16550 + bool "16550-compatible (generic)" + default n +config UART16550A + bool "16550A (x86)" + default n +config LOONGSON_UART + bool "Loongson UART" + default n +endmenu + +menu "x86 display" + depends on ARCH_X86_64 + +config GRAPHICS + bool "Framebuffer / graphics" + default n +config SPLIT_SCREEN + bool "Split screen layout" + depends on GRAPHICS + default n +endmenu + +menu "Debug" +config EXTIOI_DEBUG + bool "EXTIOI debug (LoongArch LS7A2000)" + depends on ARCH_LOONGARCH64 + default n +endmenu diff --git a/kconfig/cfg_map.toml b/kconfig/cfg_map.toml new file mode 100644 index 000000000..bfb6ca8c4 --- /dev/null +++ b/kconfig/cfg_map.toml @@ -0,0 +1,36 @@ +# Kconfig / .config symbol -> rustc cfg name (must match #[cfg(...)] in source). +# Keys are exactly as in .config (CONFIG_*). +[symbols] +CONFIG_IRQ_GICV2 = "irq_gicv2" +CONFIG_IRQ_GICV3 = "irq_gicv3" +CONFIG_PL011 = "pl011" +CONFIG_IMX_UART = "imx_uart" +CONFIG_XUARTPS = "xuartps" +CONFIG_UART_16550 = "uart_16550" +CONFIG_UART16550A = "uart16550a" +CONFIG_LOONGSON_UART = "loongson_uart" +CONFIG_PCI = "pci" +CONFIG_PCIE_ECAM = "ecam_pcie" +CONFIG_PCIE_DWC = "dwc_pcie" +CONFIG_PCIE_LOONGARCH64 = "loongarch64_pcie" +CONFIG_NO_PCIE_BAR_REALLOC = "no_pcie_bar_realloc" +CONFIG_IOMMU = "iommu" +CONFIG_SHARE_S2PT = "share_s2pt" +CONFIG_ARM_SMMU = "arm_smmu" +CONFIG_INTEL_VTD = "intel_vtd" +CONFIG_RISCV_IOMMU = "riscv_iommu" +CONFIG_PLIC = "plic" +CONFIG_DP1000_PLIC = "dp1000_plic" +CONFIG_AIA = "aia" +CONFIG_ACLINT = "aclint" +CONFIG_SSTC = "sstc" +CONFIG_EIC770X_SOC = "eic770x_soc" +CONFIG_EIC7700_SYSREG = "eic7700_sysreg" +CONFIG_SIFIVE_CCACHE = "sifive_ccache" +CONFIG_LOONGSON_7A2000 = "loongson_7a2000" +CONFIG_LOONGSON_3A5000 = "loongson_3a5000" +CONFIG_LOONGSON_3A6000 = "loongson_3a6000" +CONFIG_GRAPHICS = "graphics" +CONFIG_SPLIT_SCREEN = "split_screen" +CONFIG_PRINT_TIMESTAMP = "print_timestamp" +CONFIG_EXTIOI_DEBUG = "extioi_debug" diff --git a/kernel_build/bin/python b/kernel_build/bin/python new file mode 120000 index 000000000..b8a0adbbb --- /dev/null +++ b/kernel_build/bin/python @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/kernel_build/bin/python3 b/kernel_build/bin/python3 new file mode 120000 index 000000000..ae65fdaa1 --- /dev/null +++ b/kernel_build/bin/python3 @@ -0,0 +1 @@ +/usr/bin/python3 \ No newline at end of file diff --git a/kernel_build/bin/python3.12 b/kernel_build/bin/python3.12 new file mode 120000 index 000000000..b8a0adbbb --- /dev/null +++ b/kernel_build/bin/python3.12 @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/kernel_build/lib64 b/kernel_build/lib64 new file mode 120000 index 000000000..7951405f8 --- /dev/null +++ b/kernel_build/lib64 @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/kernel_build/pyvenv.cfg b/kernel_build/pyvenv.cfg new file mode 100644 index 000000000..84f912f9b --- /dev/null +++ b/kernel_build/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.12.3 +executable = /usr/bin/python3.12 +command = /usr/bin/python3 -m venv /home/dallas/DEMO/syswonder/hvisor/kernel_build diff --git a/platform/aarch64/imx8mp/kconfig/defconfig b/platform/aarch64/imx8mp/kconfig/defconfig new file mode 100644 index 000000000..d9c75b093 --- /dev/null +++ b/platform/aarch64/imx8mp/kconfig/defconfig @@ -0,0 +1,3 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_IMX_UART=y +CONFIG_IRQ_GICV3=y diff --git a/platform/aarch64/imx8mp/platform.mk b/platform/aarch64/imx8mp/platform.mk index 5e5809f0f..d4014f9a9 100644 --- a/platform/aarch64/imx8mp/platform.mk +++ b/platform/aarch64/imx8mp/platform.mk @@ -1,6 +1,6 @@ QEMU := qemu-system-aarch64 -ifeq ($(findstring gicv3, $(FEATURES)),gicv3) +ifneq ($(shell grep -Fx CONFIG_IRQ_GICV3=y platform/$(ARCH)/$(BOARD)/kconfig/defconfig 2>/dev/null),) UBOOT := $(image_dir)/bootloader/u-boot-atf.bin zone0_dtb := $(image_dir)/devicetree/linux1.dtb QEMU_ARGS := -machine virt,secure=on,gic-version=3,virtualization=on,iommu=smmuv3 diff --git a/platform/aarch64/ok6254-c/kconfig/defconfig b/platform/aarch64/ok6254-c/kconfig/defconfig new file mode 100644 index 000000000..242faa328 --- /dev/null +++ b/platform/aarch64/ok6254-c/kconfig/defconfig @@ -0,0 +1,3 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_IRQ_GICV3=y +CONFIG_UART_16550=y diff --git a/platform/aarch64/phytium-pi/kconfig/defconfig b/platform/aarch64/phytium-pi/kconfig/defconfig new file mode 100644 index 000000000..f24edf758 --- /dev/null +++ b/platform/aarch64/phytium-pi/kconfig/defconfig @@ -0,0 +1,3 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_IRQ_GICV3=y +CONFIG_PL011=y diff --git a/platform/aarch64/phytium-pi/platform.mk b/platform/aarch64/phytium-pi/platform.mk index 061516bcc..46c773778 100644 --- a/platform/aarch64/phytium-pi/platform.mk +++ b/platform/aarch64/phytium-pi/platform.mk @@ -1,6 +1,6 @@ QEMU := qemu-system-aarch64 -ifeq ($(findstring gicv3, $(FEATURES)),gicv3) +ifneq ($(shell grep -Fx CONFIG_IRQ_GICV3=y platform/$(ARCH)/$(BOARD)/kconfig/defconfig 2>/dev/null),) UBOOT := $(image_dir)/bootloader/u-boot-atf.bin zone0_dtb := $(image_dir)/devicetree/linux1.dtb QEMU_ARGS := -machine virt,secure=on,gic-version=3,virtualization=on,iommu=smmuv3 diff --git a/platform/aarch64/qemu-gicv2/kconfig/defconfig b/platform/aarch64/qemu-gicv2/kconfig/defconfig new file mode 100644 index 000000000..fb8f4ff36 --- /dev/null +++ b/platform/aarch64/qemu-gicv2/kconfig/defconfig @@ -0,0 +1,7 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_ARM_SMMU=y +CONFIG_IOMMU=y +CONFIG_IRQ_GICV2=y +CONFIG_PCI=y +CONFIG_PCIE_ECAM=y +CONFIG_PL011=y diff --git a/platform/aarch64/qemu-gicv3/kconfig/defconfig b/platform/aarch64/qemu-gicv3/kconfig/defconfig new file mode 100644 index 000000000..6ca96900d --- /dev/null +++ b/platform/aarch64/qemu-gicv3/kconfig/defconfig @@ -0,0 +1,7 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_ARM_SMMU=y +CONFIG_IOMMU=y +CONFIG_IRQ_GICV3=y +CONFIG_PCI=y +CONFIG_PCIE_ECAM=y +CONFIG_PL011=y diff --git a/platform/aarch64/rk3568/kconfig/defconfig b/platform/aarch64/rk3568/kconfig/defconfig new file mode 100644 index 000000000..212ee3ba3 --- /dev/null +++ b/platform/aarch64/rk3568/kconfig/defconfig @@ -0,0 +1,6 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_IRQ_GICV3=y +CONFIG_NO_PCIE_BAR_REALLOC=y +CONFIG_PCI=y +CONFIG_PCIE_DWC=y +CONFIG_UART_16550=y diff --git a/platform/aarch64/rk3588/kconfig/defconfig b/platform/aarch64/rk3588/kconfig/defconfig new file mode 100644 index 000000000..242faa328 --- /dev/null +++ b/platform/aarch64/rk3588/kconfig/defconfig @@ -0,0 +1,3 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_IRQ_GICV3=y +CONFIG_UART_16550=y diff --git a/platform/aarch64/sysoul_x3300/kconfig/defconfig b/platform/aarch64/sysoul_x3300/kconfig/defconfig new file mode 100644 index 000000000..242faa328 --- /dev/null +++ b/platform/aarch64/sysoul_x3300/kconfig/defconfig @@ -0,0 +1,3 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_IRQ_GICV3=y +CONFIG_UART_16550=y diff --git a/platform/aarch64/zcu102/kconfig/defconfig b/platform/aarch64/zcu102/kconfig/defconfig new file mode 100644 index 000000000..b3563178f --- /dev/null +++ b/platform/aarch64/zcu102/kconfig/defconfig @@ -0,0 +1,3 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_IRQ_GICV2=y +CONFIG_XUARTPS=y diff --git a/platform/loongarch64/ls3a5000/kconfig/defconfig b/platform/loongarch64/ls3a5000/kconfig/defconfig new file mode 100644 index 000000000..c061a0fd3 --- /dev/null +++ b/platform/loongarch64/ls3a5000/kconfig/defconfig @@ -0,0 +1,7 @@ +CONFIG_ARCH_LOONGARCH64=y +CONFIG_LOONGSON_3A5000=y +CONFIG_LOONGSON_7A2000=y +CONFIG_LOONGSON_UART=y +CONFIG_NO_PCIE_BAR_REALLOC=y +CONFIG_PCI=y +CONFIG_PCIE_LOONGARCH64=y diff --git a/platform/loongarch64/ls3a6000/kconfig/defconfig b/platform/loongarch64/ls3a6000/kconfig/defconfig new file mode 100644 index 000000000..ef0a73e99 --- /dev/null +++ b/platform/loongarch64/ls3a6000/kconfig/defconfig @@ -0,0 +1,7 @@ +CONFIG_ARCH_LOONGARCH64=y +CONFIG_LOONGSON_3A6000=y +CONFIG_LOONGSON_7A2000=y +CONFIG_LOONGSON_UART=y +CONFIG_NO_PCIE_BAR_REALLOC=y +CONFIG_PCI=y +CONFIG_PCIE_LOONGARCH64=y diff --git a/platform/riscv64/hifive-premier-p550/kconfig/defconfig b/platform/riscv64/hifive-premier-p550/kconfig/defconfig new file mode 100644 index 000000000..454d53ce7 --- /dev/null +++ b/platform/riscv64/hifive-premier-p550/kconfig/defconfig @@ -0,0 +1,5 @@ +CONFIG_ARCH_RISCV64=y +CONFIG_EIC7700_SYSREG=y +CONFIG_EIC770X_SOC=y +CONFIG_PLIC=y +CONFIG_SIFIVE_CCACHE=y diff --git a/platform/riscv64/megrez/kconfig/defconfig b/platform/riscv64/megrez/kconfig/defconfig new file mode 100644 index 000000000..5893693bd --- /dev/null +++ b/platform/riscv64/megrez/kconfig/defconfig @@ -0,0 +1,3 @@ +CONFIG_ARCH_RISCV64=y +CONFIG_EIC770X_SOC=y +CONFIG_PLIC=y diff --git a/platform/riscv64/qemu-aia/kconfig/defconfig b/platform/riscv64/qemu-aia/kconfig/defconfig new file mode 100644 index 000000000..d95039822 --- /dev/null +++ b/platform/riscv64/qemu-aia/kconfig/defconfig @@ -0,0 +1,8 @@ +CONFIG_AIA=y +CONFIG_ARCH_RISCV64=y +CONFIG_IOMMU=y +CONFIG_PCI=y +CONFIG_PCIE_ECAM=y +CONFIG_RISCV_IOMMU=y +CONFIG_SHARE_S2PT=y +CONFIG_SSTC=y diff --git a/platform/riscv64/qemu-plic/kconfig/defconfig b/platform/riscv64/qemu-plic/kconfig/defconfig new file mode 100644 index 000000000..e3556c4f2 --- /dev/null +++ b/platform/riscv64/qemu-plic/kconfig/defconfig @@ -0,0 +1,6 @@ +CONFIG_ACLINT=y +CONFIG_ARCH_RISCV64=y +CONFIG_PCI=y +CONFIG_PCIE_ECAM=y +CONFIG_PLIC=y +CONFIG_SSTC=y diff --git a/platform/riscv64/ur-dp1000/kconfig/defconfig b/platform/riscv64/ur-dp1000/kconfig/defconfig new file mode 100644 index 000000000..58f138b2b --- /dev/null +++ b/platform/riscv64/ur-dp1000/kconfig/defconfig @@ -0,0 +1,3 @@ +CONFIG_ARCH_RISCV64=y +CONFIG_DP1000_PLIC=y +CONFIG_PLIC=y diff --git a/platform/x86_64/ecx-2300f-peg/board.rs b/platform/x86_64/ecx-2300f-peg/board.rs index 1b295342d..e628dd84c 100644 --- a/platform/x86_64/ecx-2300f-peg/board.rs +++ b/platform/x86_64/ecx-2300f-peg/board.rs @@ -180,6 +180,6 @@ pub const ROOT_PCI_DEVS: [HvPciDevConfig; 19] = [ pci_dev!(0x0, 0x3, 0x0, 0x0, VpciDevType::Physical), // ethernet controller ]; -#[cfg(all(feature = "graphics"))] +#[cfg(all(graphics))] pub const GRAPHICS_FONT: &[u8] = include_bytes!("../../platform/x86_64/qemu/image/font/spleen-6x12.psf"); diff --git a/platform/x86_64/ecx-2300f-peg/kconfig/defconfig b/platform/x86_64/ecx-2300f-peg/kconfig/defconfig new file mode 100644 index 000000000..770397dbb --- /dev/null +++ b/platform/x86_64/ecx-2300f-peg/kconfig/defconfig @@ -0,0 +1,4 @@ +CONFIG_ARCH_X86_64=y +CONFIG_PCI=y +CONFIG_PCIE_ECAM=y +CONFIG_UART16550A=y diff --git a/platform/x86_64/nuc14mnk/board.rs b/platform/x86_64/nuc14mnk/board.rs index bceb2bfe8..b59520f4c 100644 --- a/platform/x86_64/nuc14mnk/board.rs +++ b/platform/x86_64/nuc14mnk/board.rs @@ -200,6 +200,6 @@ pub const ROOT_PCI_DEVS: [HvPciDevConfig; 18] = [ pci_dev!(0x0, 0x2, 0x0, 0x0, VpciDevType::Physical), // memory controller ]; -#[cfg(all(feature = "graphics"))] +#[cfg(all(graphics))] pub const GRAPHICS_FONT: &[u8] = include_bytes!("../../platform/x86_64/qemu/image/font/spleen-6x12.psf"); diff --git a/platform/x86_64/nuc14mnk/kconfig/defconfig b/platform/x86_64/nuc14mnk/kconfig/defconfig new file mode 100644 index 000000000..fb35c8fcb --- /dev/null +++ b/platform/x86_64/nuc14mnk/kconfig/defconfig @@ -0,0 +1,7 @@ +CONFIG_ARCH_X86_64=y +CONFIG_GRAPHICS=y +CONFIG_NO_PCIE_BAR_REALLOC=y +CONFIG_PCI=y +CONFIG_PCIE_ECAM=y +CONFIG_SPLIT_SCREEN=y +CONFIG_UART16550A=y diff --git a/platform/x86_64/qemu/board.rs b/platform/x86_64/qemu/board.rs index a0a420b58..c3dc8669c 100644 --- a/platform/x86_64/qemu/board.rs +++ b/platform/x86_64/qemu/board.rs @@ -160,6 +160,6 @@ pub const ROOT_PCI_DEVS: [HvPciDevConfig; 7] = [ pci_dev!(0x0, 0x1, 0x0, 0x0, VpciDevType::Physical), // SCSI controller ]; -#[cfg(all(feature = "graphics"))] +#[cfg(all(graphics))] pub const GRAPHICS_FONT: &[u8] = include_bytes!("../../platform/x86_64/qemu/image/font/spleen-6x12.psf"); diff --git a/platform/x86_64/qemu/kconfig/defconfig b/platform/x86_64/qemu/kconfig/defconfig new file mode 100644 index 000000000..007ce3484 --- /dev/null +++ b/platform/x86_64/qemu/kconfig/defconfig @@ -0,0 +1,7 @@ +CONFIG_ARCH_X86_64=y +CONFIG_INTEL_VTD=y +CONFIG_IOMMU=y +CONFIG_NO_PCIE_BAR_REALLOC=y +CONFIG_PCI=y +CONFIG_PCIE_ECAM=y +CONFIG_UART16550A=y diff --git a/src/arch/riscv64/cpu.rs b/src/arch/riscv64/cpu.rs index 0dea57d17..c5bdd69ce 100644 --- a/src/arch/riscv64/cpu.rs +++ b/src/arch/riscv64/cpu.rs @@ -56,7 +56,7 @@ impl ArchCpu { // first_cpu: 0, power_on: false, init: false, - sstc: cfg!(feature = "sstc"), + sstc: cfg!(sstc), }; ret } @@ -72,7 +72,7 @@ impl ArchCpu { write_csr!(CSR_SSCRATCH, self as *const _ as usize); //arch cpu pointer self.sepc = entry; self.hstatus = 1 << 7 | 2 << 32; // HSTATUS_SPV | HSTATUS_VSXL_64 - #[cfg(feature = "aia")] + #[cfg(aia)] { self.hstatus |= 1 << 12; // HSTATUS_VGEIN } @@ -91,7 +91,7 @@ impl ArchCpu { set_csr!(CSR_VSTIMECMP, usize::MAX); } else { // In megrez board, this instruction is not supported. (illegal instruction) - #[cfg(not(feature = "eic770x_soc"))] + #[cfg(not(eic770x_soc))] set_csr!(CSR_HENVCFG, 0); } set_csr!(CSR_HCOUNTEREN, 1 << 1); // HCOUNTEREN_TM diff --git a/src/arch/riscv64/ipi.rs b/src/arch/riscv64/ipi.rs index d4ad55d27..242023cad 100644 --- a/src/arch/riscv64/ipi.rs +++ b/src/arch/riscv64/ipi.rs @@ -15,7 +15,7 @@ // Jingyu Liu // use crate::consts::IPI_EVENT_SEND_IPI; -#[cfg(feature = "plic")] +#[cfg(plic)] use crate::consts::IPI_EVENT_UPDATE_HART_LINE; use crate::platform::BOARD_HARTID_MAP; @@ -23,9 +23,9 @@ use crate::platform::BOARD_HARTID_MAP; pub fn arch_send_event(cpu_id: u64, _sgi_num: u64) { let hart_id = BOARD_HARTID_MAP[cpu_id as usize]; debug!("arch_send_event: cpu_id: {}", hart_id); - #[cfg(feature = "aclint")] + #[cfg(aclint)] crate::device::irqchip::aclint::aclint_send_ipi(hart_id as usize); - #[cfg(not(feature = "aclint"))] + #[cfg(not(aclint))] { let sbi_ret: sbi_rt::SbiRet = sbi_rt::send_ipi(sbi_rt::HartMask::from_mask_base(1 << hart_id, 0)); @@ -44,7 +44,7 @@ pub fn arch_ipi_handler() { pub fn arch_check_events(event: Option) { match event { - #[cfg(feature = "plic")] + #[cfg(plic)] Some(IPI_EVENT_UPDATE_HART_LINE) => { use crate::device::irqchip::plic::update_hart_line; update_hart_line(); diff --git a/src/arch/riscv64/trap.rs b/src/arch/riscv64/trap.rs index 99277f72e..4c8de184f 100644 --- a/src/arch/riscv64/trap.rs +++ b/src/arch/riscv64/trap.rs @@ -15,7 +15,7 @@ // use super::cpu::ArchCpu; use crate::arch::sbi::sbi_vs_handler; -#[cfg(feature = "plic")] +#[cfg(plic)] use crate::device::irqchip::plic::{inject_irq, plic_get_hwirq}; use crate::event::check_events; use crate::memory::GuestPhysAddr; @@ -417,7 +417,7 @@ pub fn handle_software_interrupt(_current_cpu: &mut ArchCpu) { /// Handle supervisor external interrupt. pub fn handle_external_interrupt(_current_cpu: &mut ArchCpu) { - #[cfg(feature = "plic")] + #[cfg(plic)] { // Note: in hvisor, all external interrupts are assigned to VS. // 1. claim hw irq. @@ -431,7 +431,7 @@ pub fn handle_external_interrupt(_current_cpu: &mut ArchCpu) { // 2. inject hw irq to zone. inject_irq(irq_id as usize, true); } - #[cfg(feature = "aia")] + #[cfg(aia)] { // Note: no irq belongs to hvisor, so hvisor won't handle external interrupt. panic!("HS extensional interrupt") diff --git a/src/arch/x86_64/boot.rs b/src/arch/x86_64/boot.rs index b3191ef19..c3ec46367 100644 --- a/src/arch/x86_64/boot.rs +++ b/src/arch/x86_64/boot.rs @@ -394,9 +394,9 @@ impl BootParams { let bytes_per_pixel = (fb_info.bpp as usize) / 8; let width = fb_info.width as usize; - #[cfg(not(feature = "split_screen"))] + #[cfg(not(split_screen))] let height = fb_info.height as usize; - #[cfg(all(feature = "split_screen"))] + #[cfg(all(split_screen))] let height = (fb_info.height / 2) as usize; self.screen_info.lfb_base = config.arch_config.screen_base as _; diff --git a/src/arch/x86_64/cpu.rs b/src/arch/x86_64/cpu.rs index 808515b0e..c2cae4e9c 100644 --- a/src/arch/x86_64/cpu.rs +++ b/src/arch/x86_64/cpu.rs @@ -298,7 +298,7 @@ impl ArchCpu { if per_cpu.boot_cpu { // must be called after activate_gpm() - #[cfg(feature = "intel_vtd")] + #[cfg(intel_vtd)] iommu::activate(); self.guest_regs = self.vm_launch_guest_regs.clone(); } diff --git a/src/arch/x86_64/entry.rs b/src/arch/x86_64/entry.rs index 2a6db8bd6..9b7a883b9 100644 --- a/src/arch/x86_64/entry.rs +++ b/src/arch/x86_64/entry.rs @@ -80,7 +80,7 @@ extern "C" fn rust_entry(magic: u32, info_addr: usize) { crate::clear_bss(); unsafe { PHYS_VIRT_OFFSET = X86_PHYS_VIRT_OFFSET }; boot::multiboot_init(info_addr); - #[cfg(all(feature = "graphics"))] + #[cfg(all(graphics))] font_init(__board::GRAPHICS_FONT); boot::print_memory_map(); rust_main(this_apic_id(), info_addr); diff --git a/src/arch/x86_64/graphics.rs b/src/arch/x86_64/graphics.rs index 2097e401f..34367850c 100644 --- a/src/arch/x86_64/graphics.rs +++ b/src/arch/x86_64/graphics.rs @@ -94,18 +94,18 @@ pub fn font_init(psf: &'static [u8]) { cursor_x: 0, cursor_y: 0, max_char_nr_x: (framebuffer.width / font_width) as _, - #[cfg(not(feature = "split_screen"))] + #[cfg(not(split_screen))] max_char_nr_y: (framebuffer.height / psf_header.height) as _, - #[cfg(all(feature = "split_screen"))] + #[cfg(all(split_screen))] max_char_nr_y: (framebuffer.height / 2 / psf_header.height) as _, - #[cfg(not(feature = "split_screen"))] + #[cfg(not(split_screen))] addr: framebuffer.addr as _, - #[cfg(all(feature = "split_screen"))] + #[cfg(all(split_screen))] addr: mid_addr as _, width: framebuffer.width as _, - #[cfg(not(feature = "split_screen"))] + #[cfg(not(split_screen))] height: framebuffer.height as _, - #[cfg(all(feature = "split_screen"))] + #[cfg(all(split_screen))] height: (framebuffer.height / 2) as _, }) }); diff --git a/src/arch/x86_64/pio.rs b/src/arch/x86_64/pio.rs index f89a55643..255e76ddb 100644 --- a/src/arch/x86_64/pio.rs +++ b/src/arch/x86_64/pio.rs @@ -89,7 +89,7 @@ impl PortIoBitmap { bitmap.set_range_intercept(PCI_CONFIG_DATA_PORT, true); // if zone_id == 0 { - #[cfg(feature = "graphics")] + #[cfg(graphics)] bitmap.set_range_intercept(UART_COM1_PORT, true); // } diff --git a/src/arch/x86_64/s2pt.rs b/src/arch/x86_64/s2pt.rs index a4fe82ce8..440796f84 100644 --- a/src/arch/x86_64/s2pt.rs +++ b/src/arch/x86_64/s2pt.rs @@ -263,7 +263,7 @@ impl PagingInstr for S2PTInstr { // if this cpu is boot cpu and it is running if this_cpu_data().arch_cpu.power_on && this_cpu_data().boot_cpu { - #[cfg(feature = "intel_vtd")] + #[cfg(intel_vtd)] iommu::fill_dma_translation_tables(this_zone_id(), root_paddr); } } diff --git a/src/device/iommu/arm_smmu/smmu_hw.rs b/src/device/iommu/arm_smmu/smmu_hw.rs index fe7b3244a..ad34d08a6 100644 --- a/src/device/iommu/arm_smmu/smmu_hw.rs +++ b/src/device/iommu/arm_smmu/smmu_hw.rs @@ -529,13 +529,13 @@ impl Smmuv3 { static SMMUV3: spin::Once> = spin::Once::new(); /// iommu feature is disabled. -#[cfg(not(feature = "iommu"))] +#[cfg(not(iommu))] pub fn iommu_init() { info!("aarch64: iommu_init: do nothing now"); } /// smmuv3 init (enabled) -#[cfg(feature = "iommu")] +#[cfg(iommu)] pub fn iommu_init() { info!("Smmuv3 init..."); SMMUV3.call_once(|| Mutex::new(Smmuv3::new())); diff --git a/src/device/iommu/iommu_impl.rs b/src/device/iommu/iommu_impl.rs index eac9ea16e..49e0d3698 100644 --- a/src/device/iommu/iommu_impl.rs +++ b/src/device/iommu/iommu_impl.rs @@ -24,17 +24,17 @@ static IOMMU_IMPL: Once> = Once::new(); // Dispatch to the appropriate IOMMU implementation based on hardware support fn iommu_impl_init() -> Box { - #[cfg(feature = "arm_smmu")] + #[cfg(arm_smmu)] return Box::new(super::arm_smmu::ArmSmmu); - #[cfg(feature = "intel_vtd")] + #[cfg(intel_vtd)] return Box::new(super::intel_vtd::IntelVtd); - #[cfg(feature = "riscv_iommu")] + #[cfg(riscv_iommu)] return Box::new(super::riscv_iommu::RiscvIommu); // Default return DummyIommu if no IOMMU support - #[cfg(not(any(feature = "arm_smmu", feature = "intel_vtd", feature = "riscv_iommu",)))] + #[cfg(not(any(arm_smmu, intel_vtd, riscv_iommu)))] return Box::new(super::dummy_iommu::DummyIommu); } diff --git a/src/device/iommu/mod.rs b/src/device/iommu/mod.rs index 548bc3922..e0adf5691 100644 --- a/src/device/iommu/mod.rs +++ b/src/device/iommu/mod.rs @@ -25,13 +25,13 @@ use crate::consts::MAX_ZONE_NUM; use iommu_impl::iommu_impl; use iommu_trait::Iommu; -#[cfg(feature = "arm_smmu")] +#[cfg(arm_smmu)] mod arm_smmu; -#[cfg(not(any(feature = "arm_smmu", feature = "intel_vtd", feature = "riscv_iommu")))] +#[cfg(not(any(arm_smmu, intel_vtd, riscv_iommu)))] mod dummy_iommu; -#[cfg(feature = "intel_vtd")] +#[cfg(intel_vtd)] mod intel_vtd; -#[cfg(feature = "riscv_iommu")] +#[cfg(riscv_iommu)] mod riscv_iommu; fn check_zone_id(zone_id: usize) -> Result<(), &'static str> { @@ -64,28 +64,28 @@ pub fn iommu_add_device_with_root_pt_addr(zone_id: usize, did: usize, root_pt_ad // Below pub apis are used for compatibility for old code(for x86_64) // // These apis will be replaced by IOMMU trait later. // //////////////////////////////////////////////////////////////////////// -#[cfg(feature = "intel_vtd")] +#[cfg(intel_vtd)] pub fn clear_dma_translation_tables(zone_id: usize) { intel_vtd::clear_dma_translation_tables(zone_id); } -#[cfg(feature = "intel_vtd")] +#[cfg(intel_vtd)] pub fn fill_dma_translation_tables(zone_id: usize, zone_s2pt_hpa: crate::memory::HostPhysAddr) { intel_vtd::fill_dma_translation_tables(zone_id, zone_s2pt_hpa); } /// should be called after gpm is activated -#[cfg(feature = "intel_vtd")] +#[cfg(intel_vtd)] pub fn activate() { intel_vtd::activate(); } -#[cfg(feature = "intel_vtd")] +#[cfg(intel_vtd)] pub fn flush(zone_id: usize, bus: u8, dev_func: u8) { intel_vtd::flush(zone_id, bus, dev_func); } -#[cfg(feature = "riscv_iommu")] +#[cfg(riscv_iommu)] pub fn iommu_msi_pt_tlb_invalid(zone_id: u16, msi_gpa: usize) { riscv_iommu::iommu_msi_pt_tlb_invalid(zone_id, msi_gpa); } diff --git a/src/device/iommu/riscv_iommu/iommu_hw.rs b/src/device/iommu/riscv_iommu/iommu_hw.rs index 0b1386fd2..a3105c99d 100644 --- a/src/device/iommu/riscv_iommu/iommu_hw.rs +++ b/src/device/iommu/riscv_iommu/iommu_hw.rs @@ -113,15 +113,15 @@ fn get_iommu<'a>() -> &'a Mutex { /// Initialize IOMMU with default mode pub fn iommu_init() { - #[cfg(feature = "iommu")] + #[cfg(iommu)] riscv_iommu_init(); - #[cfg(not(feature = "iommu"))] + #[cfg(not(iommu))] info!("RISC-V IOMMU: do nothing now"); } /// Add a device to IOMMU pub fn iommu_add_device(vm_id: usize, device_id: usize, root_pt: usize) { - #[cfg(feature = "iommu")] + #[cfg(iommu)] { info!( "RV IOMMU: Add device, root_pt {:#x}, vm_id {}, device_id {}", @@ -130,13 +130,13 @@ pub fn iommu_add_device(vm_id: usize, device_id: usize, root_pt: usize) { let iommu = get_iommu(); iommu.lock().rv_iommu_add_device(device_id, vm_id, root_pt); } - #[cfg(not(feature = "iommu"))] + #[cfg(not(iommu))] info!("RISC-V: iommu_add_device do nothing now"); } /// Remove a device from IOMMU (reserved for future hot-unplug paths). pub fn iommu_remove_device(vm_id: usize, device_id: usize) { - #[cfg(feature = "iommu")] + #[cfg(iommu)] { info!( "RV IOMMU: Remove device, vm_id {}, device_id {}", @@ -145,12 +145,12 @@ pub fn iommu_remove_device(vm_id: usize, device_id: usize) { let iommu = get_iommu(); iommu.lock().rv_iommu_remove_device(device_id); } - #[cfg(not(feature = "iommu"))] + #[cfg(not(iommu))] info!("RISC-V: iommu_remove_device do nothing now"); } pub fn iommu_msi_pt_tlb_invalid(gscid: u16, msi_gpa: usize) { - #[cfg(feature = "iommu")] + #[cfg(iommu)] { // If software changes a MSI page-table entry identified by interrupt file number I that corresponds to an // untranslated MSI address A then the following invalidations must be performed: @@ -170,7 +170,7 @@ pub fn iommu_msi_pt_tlb_invalid(gscid: u16, msi_gpa: usize) { let iommu = get_iommu(); iommu.lock().rv_iommu_msi_pt_tlb_invalid(gscid, msi_gpa); } - #[cfg(not(feature = "iommu"))] + #[cfg(not(iommu))] info!("RISC-V: iommu_msi_pt_tlb_invalid do nothing now"); } diff --git a/src/device/irqchip/ls7a2000/chip.rs b/src/device/irqchip/ls7a2000/chip.rs index 0b09350b7..d7c567516 100644 --- a/src/device/irqchip/ls7a2000/chip.rs +++ b/src/device/irqchip/ls7a2000/chip.rs @@ -663,7 +663,7 @@ pub fn extioi_int_enable_all() { CHIP_EXTIOI_ENABLE.extioi_en1.set(0xffff_ffff_ffff_ffff); CHIP_EXTIOI_ENABLE.extioi_en2.set(0xffff_ffff_ffff_ffff); CHIP_EXTIOI_ENABLE.extioi_en3.set(0xffff_ffff_ffff_ffff); - #[cfg(feature = "extioi_debug")] + #[cfg(extioi_debug)] { // dump ht int vector and enable /* diff --git a/src/device/irqchip/mod.rs b/src/device/irqchip/mod.rs index 345c4d20b..627babbdb 100644 --- a/src/device/irqchip/mod.rs +++ b/src/device/irqchip/mod.rs @@ -17,32 +17,32 @@ use crate::arch::zone::HvArchZoneConfig; use crate::config::HvZoneConfig; use crate::zone::Zone; -#[cfg(all(feature = "gicv2", target_arch = "aarch64"))] +#[cfg(all(irq_gicv2, target_arch = "aarch64"))] pub mod gicv2; -#[cfg(all(feature = "gicv2", target_arch = "aarch64"))] +#[cfg(all(irq_gicv2, target_arch = "aarch64"))] pub use gicv2::{ gic::inject_irq, gicd::set_ispender, percpu_init, primary_init_early, primary_init_late, vgic::set_sgi_irq, }; -#[cfg(all(feature = "gicv3", target_arch = "aarch64"))] +#[cfg(all(irq_gicv3, target_arch = "aarch64"))] pub mod gicv3; -#[cfg(all(feature = "gicv3", target_arch = "aarch64"))] +#[cfg(all(irq_gicv3, target_arch = "aarch64"))] pub use gicv3::{ gicd::set_ispender, inject_irq, percpu_init, primary_init_early, primary_init_late, }; #[cfg(target_arch = "aarch64")] pub fn gic_handle_irq() { - #[cfg(feature = "gicv2")] + #[cfg(irq_gicv2)] gicv2::gic::gicv2_handle_irq(); - #[cfg(feature = "gicv3")] + #[cfg(irq_gicv3)] gicv3::gicv3_handle_irq_el1(); } #[cfg(target_arch = "aarch64")] pub fn gic_send_event(cpu_id: u64, sgi_num: u64) { - #[cfg(feature = "gicv3")] + #[cfg(irq_gicv3)] { /*Actually, the value passed to ICC_SGI1R_EL1 should be derived from the MPIDR of the target CPU. However, since we cannot access this @@ -63,7 +63,7 @@ pub fn gic_send_event(cpu_id: u64, sgi_num: u64) { write_sysreg!(icc_sgi1r_el1, val); debug!("write sgi sys value = {:#x}", val); } - #[cfg(feature = "gicv2")] + #[cfg(irq_gicv2)] { let sgi_id: u64 = sgi_num; let target_list: u64 = 1 << cpu_id; @@ -73,11 +73,11 @@ pub fn gic_send_event(cpu_id: u64, sgi_num: u64) { impl Zone { pub fn virqc_init(&mut self, _config: &HvZoneConfig) { - #[cfg(all(feature = "plic", target_arch = "riscv64"))] + #[cfg(all(plic, target_arch = "riscv64"))] { self.vplic_init(_config); } - #[cfg(all(feature = "aia", target_arch = "riscv64"))] + #[cfg(all(aia, target_arch = "riscv64"))] { self.vaplic_init(_config); self.vimsic_init(_config); @@ -85,28 +85,28 @@ impl Zone { } pub fn mmio_init(&mut self, hv_config: &HvArchZoneConfig) { - #[cfg(all(feature = "gicv2", target_arch = "aarch64"))] + #[cfg(all(irq_gicv2, target_arch = "aarch64"))] { self.vgicv2_mmio_init(hv_config); self.vgicv2_remap_init(hv_config); } - #[cfg(all(feature = "gicv3", target_arch = "aarch64"))] + #[cfg(all(irq_gicv3, target_arch = "aarch64"))] { self.vgicv3_mmio_init(hv_config); } - #[cfg(all(feature = "plic", target_arch = "riscv64"))] + #[cfg(all(plic, target_arch = "riscv64"))] { self.vplic_mmio_init(hv_config); } - #[cfg(all(feature = "aia", target_arch = "riscv64"))] + #[cfg(all(aia, target_arch = "riscv64"))] { self.vaplic_mmio_init(hv_config); } - #[cfg(all(feature = "eic770x_soc", target_arch = "riscv64"))] + #[cfg(all(eic770x_soc, target_arch = "riscv64"))] { - #[cfg(feature = "sifive_ccache")] + #[cfg(sifive_ccache)] self.virtual_sifive_ccache_mmio_init(); - #[cfg(feature = "eic7700_sysreg")] + #[cfg(eic7700_sysreg)] self.virtual_syscon_mmio_init(); } #[cfg(target_arch = "x86_64")] @@ -117,30 +117,30 @@ impl Zone { } } -#[cfg(all(feature = "aclint", target_arch = "riscv64"))] +#[cfg(all(aclint, target_arch = "riscv64"))] pub mod aclint; -#[cfg(all(feature = "plic", target_arch = "riscv64"))] +#[cfg(all(plic, target_arch = "riscv64"))] pub mod plic; -#[cfg(all(feature = "plic", target_arch = "riscv64"))] +#[cfg(all(plic, target_arch = "riscv64"))] pub use plic::{inject_irq, percpu_init, primary_init_late}; -#[cfg(all(feature = "aia", target_arch = "riscv64"))] +#[cfg(all(aia, target_arch = "riscv64"))] pub mod aia; -#[cfg(all(feature = "aia", target_arch = "riscv64"))] +#[cfg(all(aia, target_arch = "riscv64"))] pub use aia::{host_aplic, inject_irq, percpu_init, primary_init_late}; #[cfg(target_arch = "riscv64")] pub fn primary_init_early() { // aclint is local interrupt controller // plic & aia is global interrupt controller - #[cfg(feature = "plic")] + #[cfg(plic)] plic::primary_init_early(); - #[cfg(feature = "aia")] + #[cfg(aia)] aia::primary_init_early(); - #[cfg(feature = "aclint")] + #[cfg(aclint)] aclint::aclint_init(crate::platform::ACLINT_SSWI_BASE); } diff --git a/src/device/irqchip/pic/mod.rs b/src/device/irqchip/pic/mod.rs index 5ad49ef5c..177785765 100644 --- a/src/device/irqchip/pic/mod.rs +++ b/src/device/irqchip/pic/mod.rs @@ -145,7 +145,7 @@ pub fn primary_init_late() {} impl Zone { pub fn arch_irqchip_reset(&self) { - #[cfg(feature = "intel_vtd")] + #[cfg(intel_vtd)] iommu::clear_dma_translation_tables(self.id()); } } diff --git a/src/device/irqchip/plic/plic.rs b/src/device/irqchip/plic/plic.rs index a7f069bcd..af009b381 100644 --- a/src/device/irqchip/plic/plic.rs +++ b/src/device/irqchip/plic/plic.rs @@ -235,7 +235,7 @@ impl Plic { */ let mut hwirq = 0; - if cfg!(feature = "dp1000_plic") { + if cfg!(dp1000_plic) { let mut ie: [u32; 32] = [0; 32]; // max 1024 irqs hwirq = if self.plic_check_enable_first_pending(context, &mut ie) { self.claim(context) diff --git a/src/device/mod.rs b/src/device/mod.rs index fa5aab22e..7fab9db48 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -19,8 +19,8 @@ pub mod irqchip; pub mod uart; pub mod virtio_trampoline; -#[cfg(feature = "eic7700_sysreg")] +#[cfg(eic7700_sysreg)] pub mod eic7700_syscrg; -#[cfg(feature = "sifive_ccache")] +#[cfg(sifive_ccache)] pub mod sifive_ccache; diff --git a/src/device/uart/mod.rs b/src/device/uart/mod.rs index f30f036db..16026b7f2 100644 --- a/src/device/uart/mod.rs +++ b/src/device/uart/mod.rs @@ -13,23 +13,23 @@ // // Authors: // -// we specified the driver for each platform in the FEATURES environment variable. +// UART driver is selected per board via kconfig/defconfig (see kconfig/cfg_map.toml). #![allow(unused)] -#[cfg(all(feature = "pl011", target_arch = "aarch64"))] +#[cfg(all(pl011, target_arch = "aarch64"))] mod pl011; -#[cfg(all(feature = "pl011", target_arch = "aarch64"))] +#[cfg(all(pl011, target_arch = "aarch64"))] pub use pl011::{console_getchar, console_putchar}; -#[cfg(all(feature = "imx_uart", target_arch = "aarch64"))] +#[cfg(all(imx_uart, target_arch = "aarch64"))] mod imx_uart; -#[cfg(all(feature = "imx_uart", target_arch = "aarch64"))] +#[cfg(all(imx_uart, target_arch = "aarch64"))] pub use imx_uart::{console_getchar, console_putchar}; -#[cfg(all(feature = "xuartps", target_arch = "aarch64"))] +#[cfg(all(xuartps, target_arch = "aarch64"))] mod xuartps; -#[cfg(all(feature = "xuartps", target_arch = "aarch64"))] +#[cfg(all(xuartps, target_arch = "aarch64"))] pub use xuartps::{console_getchar, console_putchar}; #[cfg(target_arch = "riscv64")] @@ -37,19 +37,19 @@ pub use crate::arch::riscv64::sbi::{ sbi_console_getchar as console_getchar, sbi_console_putchar as console_putchar, }; -#[cfg(all(feature = "loongson_uart", target_arch = "loongarch64"))] +#[cfg(all(loongson_uart, target_arch = "loongarch64"))] pub mod loongson_uart; -#[cfg(all(feature = "loongson_uart", target_arch = "loongarch64"))] +#[cfg(all(loongson_uart, target_arch = "loongarch64"))] pub use loongson_uart::{console_getchar, console_putchar}; -#[cfg(all(feature = "uart_16550", target_arch = "aarch64"))] +#[cfg(all(uart_16550, target_arch = "aarch64"))] mod uart_16550; -#[cfg(all(feature = "uart_16550", target_arch = "aarch64"))] +#[cfg(all(uart_16550, target_arch = "aarch64"))] pub use uart_16550::{console_getchar, console_putchar}; -#[cfg(all(feature = "uart16550a", target_arch = "x86_64"))] +#[cfg(all(uart16550a, target_arch = "x86_64"))] mod uart16550a; -#[cfg(all(feature = "uart16550a", target_arch = "x86_64"))] +#[cfg(all(uart16550a, target_arch = "x86_64"))] pub use uart16550a::{ console_getchar, console_putchar, virt_console_io_read, virt_console_io_write, UartReg, }; diff --git a/src/device/uart/uart16550a.rs b/src/device/uart/uart16550a.rs index 144b788b9..e180c0d69 100644 --- a/src/device/uart/uart16550a.rs +++ b/src/device/uart/uart16550a.rs @@ -348,7 +348,7 @@ impl VirtUart16550a { pub fn console_putchar(c: u8) { COM1.lock().putchar(c); - #[cfg(all(feature = "graphics"))] + #[cfg(all(graphics))] fb_putchar(c, 0xffffffff, 0); } diff --git a/src/event.rs b/src/event.rs index f31b11594..52398e4ff 100644 --- a/src/event.rs +++ b/src/event.rs @@ -115,7 +115,7 @@ pub fn check_events() -> bool { // irqchip::ls7a2000::clear_hwi_injected_irq(); // true // } - // #[cfg(all(target_arch = "riscv64", feature = "plic"))] + // #[cfg(all(target_arch = "riscv64", plic))] // Some(IPI_EVENT_UPDATE_HART_LINE) => { // use crate::device::irqchip; // info!("cpu {} update hart line", cpu_data.id); diff --git a/src/logging.rs b/src/logging.rs index 6f8320c05..21316a98c 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -133,7 +133,7 @@ pub fn init() { struct SimpleLogger; impl SimpleLogger { - #[cfg(feature = "graphics")] + #[cfg(graphics)] fn print( &self, level: Level, @@ -154,7 +154,7 @@ impl SimpleLogger { ); } - #[cfg(not(feature = "graphics"))] + #[cfg(not(graphics))] fn print( &self, level: Level, @@ -165,7 +165,7 @@ impl SimpleLogger { args_color: ColorCode, record: &Record, ) { - #[cfg(feature = "print_timestamp")] + #[cfg(print_timestamp)] { let time_us: u64 = crate::arch::time::get_time_us(); let sec = time_us / 1_000_000; @@ -180,7 +180,7 @@ impl SimpleLogger { with_color!(args_color, "{}", record.args()), )); } - #[cfg(not(feature = "print_timestamp"))] + #[cfg(not(print_timestamp))] print(with_color!( ColorCode::White, "[{} {}] {} {}\n", diff --git a/src/main.rs b/src/main.rs index 736d209b7..4379ad487 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,7 +73,7 @@ use arch::{cpu::cpu_start, entry::arch_entry}; use config::root_zone_config; use core::sync::atomic::{AtomicI32, AtomicU32, Ordering}; use cpu_data::PerCpu; -#[cfg(feature = "pci")] +#[cfg(pci)] use pci::pci_config::hvisor_pci_init; static INITED_CPUS: AtomicU32 = AtomicU32::new(0); @@ -132,12 +132,12 @@ fn primary_init_early() { device::irqchip::primary_init_early(); - #[cfg(feature = "iommu")] + #[cfg(iommu)] iommu_init(); let root_config = root_zone_config(); - #[cfg(feature = "pci")] + #[cfg(pci)] if root_config.num_pci_bus > 0 { let num_pci_bus = root_config.num_pci_bus as usize; let _ = hvisor_pci_init(&root_config.pci_config[..num_pci_bus]); diff --git a/src/pci/config_accessors/mod.rs b/src/pci/config_accessors/mod.rs index 544505a6a..ec9405c2f 100644 --- a/src/pci/config_accessors/mod.rs +++ b/src/pci/config_accessors/mod.rs @@ -65,11 +65,7 @@ impl PciRegionMmio { } } -#[cfg(all( - not(feature = "ecam_pcie"), - not(feature = "dwc_pcie"), - not(feature = "loongarch64_pcie") -))] +#[cfg(all(not(ecam_pcie), not(dwc_pcie), not(loongarch64_pcie)))] impl PciConfigMmio { pub fn access(&self, offset: PciConfigAddress) -> *mut T { (self.base + offset) as *mut T @@ -147,13 +143,13 @@ pub enum PciAccessorType { } // Export accessor implementations -#[cfg(feature = "ecam_pcie")] +#[cfg(ecam_pcie)] pub mod ecam; -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] pub mod dwc; -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] pub mod dwc_atu; -#[cfg(feature = "loongarch64_pcie")] +#[cfg(loongarch64_pcie)] pub mod loongarch64; diff --git a/src/pci/mod.rs b/src/pci/mod.rs index 7217cce55..3ce07a625 100644 --- a/src/pci/mod.rs +++ b/src/pci/mod.rs @@ -15,6 +15,17 @@ // #![allow(dead_code)] + +#[cfg(all(target_arch = "aarch64", irq_gicv2, irq_gicv3))] +compile_error!("invalid: irq_gicv2 and irq_gicv3 are mutually exclusive"); + +#[cfg(all(ecam_pcie, dwc_pcie))] +compile_error!("invalid: ecam_pcie and dwc_pcie are mutually exclusive"); +#[cfg(all(ecam_pcie, loongarch64_pcie))] +compile_error!("invalid: ecam_pcie and loongarch64_pcie are mutually exclusive"); +#[cfg(all(dwc_pcie, loongarch64_pcie))] +compile_error!("invalid: dwc_pcie and loongarch64_pcie are mutually exclusive"); + pub mod config_accessors; pub mod mem_alloc; pub mod pci_access; diff --git a/src/pci/pci_config.rs b/src/pci/pci_config.rs index 56f6b7f93..79e8b12b7 100644 --- a/src/pci/pci_config.rs +++ b/src/pci/pci_config.rs @@ -24,33 +24,25 @@ use crate::{ zone::Zone, }; -#[cfg(feature = "loongarch64_pcie")] +#[cfg(loongarch64_pcie)] use alloc::vec::Vec; -#[cfg(any( - all(feature = "iommu", target_arch = "aarch64"), - all(feature = "iommu", target_arch = "riscv64"), - target_arch = "x86_64" -))] +#[cfg(iommu)] use crate::device::iommu::iommu_add_device_with_root_pt_addr; -#[cfg(feature = "ecam_pcie")] +#[cfg(ecam_pcie)] use crate::pci::{ pci_struct::VirtualPciConfigSpace, vpci_dev::{get_handler, VpciDevType}, }; -#[cfg(any( - feature = "ecam_pcie", - feature = "dwc_pcie", - feature = "loongarch64_pcie" -))] +#[cfg(any(ecam_pcie, dwc_pcie, loongarch64_pcie))] use crate::pci::{mem_alloc::BaseAllocator, pci_struct::RootComplex}; -#[cfg(feature = "ecam_pcie")] +#[cfg(ecam_pcie)] use crate::pci::{config_accessors::ecam::EcamConfigAccessor, pci_handler::mmio_vpci_handler}; -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] use crate::{ memory::mmio_generic_handler, pci::{ @@ -63,7 +55,7 @@ use crate::{ platform, }; -#[cfg(feature = "loongarch64_pcie")] +#[cfg(loongarch64_pcie)] use crate::pci::{ config_accessors::loongarch64::LoongArchConfigAccessor, pci_handler::mmio_vpci_direct_handler, }; @@ -77,11 +69,7 @@ pub static GLOBAL_PCIE_LIST: Lazy HvResult { warn!("begin {:#x?}", pci_config); - #[cfg(any( - feature = "ecam_pcie", - feature = "dwc_pcie", - feature = "loongarch64_pcie" - ))] + #[cfg(any(ecam_pcie, dwc_pcie, loongarch64_pcie))] for (_index, rootcomplex_config) in pci_config.iter().enumerate() { /* empty config */ if rootcomplex_config.ecam_base == 0 { @@ -99,12 +87,12 @@ pub fn hvisor_pci_init(pci_config: &[HvPciConfig]) -> HvResult { // TODO: refactor // in x86, we do not take the initiative to reallocate BAR space - #[cfg(feature = "no_pcie_bar_realloc")] + #[cfg(no_pcie_bar_realloc)] let allocator_opt: Option = None; - #[cfg(not(feature = "no_pcie_bar_realloc"))] + #[cfg(not(no_pcie_bar_realloc))] let allocator_opt: Option = Some(allocator); - // #[cfg(feature = "loongarch64_pcie")] + // #[cfg(loongarch64_pcie)] // let allocator_opt: Option = { // let mut allocator = LoongArchAllocator::default(); // allocator.set_mem( @@ -119,7 +107,7 @@ pub fn hvisor_pci_init(pci_config: &[HvPciConfig]) -> HvResult { // }; let mut rootcomplex = { - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] { // warn!("dwc pcie"); let ecam_base = rootcomplex_config.ecam_base; @@ -140,7 +128,7 @@ pub fn hvisor_pci_init(pci_config: &[HvPciConfig]) -> HvResult { RootComplex::new_dwc(rootcomplex_config.ecam_base, atu_config, root_bus) } - #[cfg(feature = "loongarch64_pcie")] + #[cfg(loongarch64_pcie)] { let root_bus = rootcomplex_config.bus_range_begin as u8; RootComplex::new_loongarch( @@ -150,7 +138,7 @@ pub fn hvisor_pci_init(pci_config: &[HvPciConfig]) -> HvResult { ) } - #[cfg(feature = "ecam_pcie")] + #[cfg(ecam_pcie)] { RootComplex::new_ecam(rootcomplex_config.ecam_base) } @@ -195,7 +183,7 @@ impl Zone { let bus_range_begin = target_pci_config.bus_range_begin as u8; // Create accessor for VirtualRootComplex, similar to RootComplex - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] { use alloc::sync::Arc; let atu_config = platform::ROOT_DWC_ATU_CONFIG @@ -215,7 +203,7 @@ impl Zone { } } - #[cfg(feature = "loongarch64_pcie")] + #[cfg(loongarch64_pcie)] { use alloc::sync::Arc; let root_bus = bus_range_begin; @@ -227,7 +215,7 @@ impl Zone { inner.vpci_bus_mut().set_accessor(accessor); } - #[cfg(feature = "ecam_pcie")] + #[cfg(ecam_pcie)] { use alloc::sync::Arc; let accessor = Arc::new(EcamConfigAccessor::new(ecam_base)); @@ -321,11 +309,7 @@ impl Zone { info!("set bdf {:#?} to vbdf {:#?}", bdf, vbdf); - #[cfg(any( - all(feature = "iommu", target_arch = "aarch64"), - all(feature = "iommu", target_arch = "riscv64"), - target_arch = "x86_64" - ))] + #[cfg(iommu)] { let iommu_pt_addr = if inner.iommu_pt().is_some() { inner.iommu_pt().unwrap().root_paddr() @@ -335,13 +319,13 @@ impl Zone { let device_id = (dev_config.bus as usize) << 8 | (dev_config.device as usize) << 3 | dev_config.function as usize; - #[cfg(feature = "share_s2pt")] + #[cfg(share_s2pt)] iommu_add_device_with_root_pt_addr( _zone_id, device_id as _, inner.gpm().root_paddr(), ); - #[cfg(not(feature = "share_s2pt"))] + #[cfg(not(share_s2pt))] iommu_add_device_with_root_pt_addr(_zone_id, device_id as _, iommu_pt_addr); } @@ -372,7 +356,7 @@ impl Zone { } } else { warn!("can not find dev {:#?} in GLOBAL_PCIE_LIST (not detected during enumeration)", bdf); - #[cfg(feature = "ecam_pcie")] + #[cfg(ecam_pcie)] { let dev_type = dev_config.dev_type; match dev_type { @@ -405,7 +389,7 @@ impl Zone { pci_rootcomplex_config: &[HvPciConfig; CONFIG_PCI_BUS_MAXNUM], _num_pci_config: usize, ) { - #[cfg(feature = "loongarch64_pcie")] + #[cfg(loongarch64_pcie)] let mut emergency_map_regions: Vec<(usize, usize)> = Vec::new(); let mut inner = self.write(); @@ -414,7 +398,7 @@ impl Zone { if rootcomplex_config.ecam_base == 0 { continue; } - #[cfg(feature = "ecam_pcie")] + #[cfg(ecam_pcie)] { // use crate::pci::pci_handler::mmio_vpci_direct_handler; inner.mmio_region_register( @@ -425,7 +409,7 @@ impl Zone { rootcomplex_config.ecam_base as usize, ); } - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] { inner.mmio_region_register( rootcomplex_config.ecam_base as usize, @@ -501,7 +485,7 @@ impl Zone { ); } } - #[cfg(feature = "loongarch64_pcie")] + #[cfg(loongarch64_pcie)] { inner.mmio_region_register( rootcomplex_config.ecam_base as usize, @@ -514,11 +498,7 @@ impl Zone { rootcomplex_config.ecam_size as usize, )); } - #[cfg(not(any( - feature = "ecam_pcie", - feature = "dwc_pcie", - feature = "loongarch64_pcie" - )))] + #[cfg(not(any(ecam_pcie, dwc_pcie, loongarch64_pcie)))] { warn!( "No extend config found for base 0x{:x}", @@ -527,7 +507,7 @@ impl Zone { } } - #[cfg(feature = "loongarch64_pcie")] + #[cfg(loongarch64_pcie)] { drop(inner); for (base, size) in emergency_map_regions { diff --git a/src/pci/pci_handler.rs b/src/pci/pci_handler.rs index af3c42b1f..ac7a72b31 100644 --- a/src/pci/pci_handler.rs +++ b/src/pci/pci_handler.rs @@ -31,7 +31,7 @@ use super::PciConfigAddress; #[cfg(target_arch = "x86_64")] use crate::zone::this_zone_id; -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] use crate::{ memory::mmio_perform_access, pci::config_accessors::{ @@ -338,7 +338,7 @@ fn handle_endpoint_access( /* after update gpm, need to flush iommu table * in x86_64 */ - #[cfg(all(target_arch = "x86_64", feature = "intel_vtd"))] + #[cfg(all(target_arch = "x86_64", intel_vtd))] { let vbdf = dev.get_vbdf(); crate::device::iommu::flush( @@ -468,7 +468,7 @@ fn handle_endpoint_access( /* after update gpm, need to flush iommu table * in x86_64 */ - #[cfg(all(target_arch = "x86_64", feature = "intel_vtd"))] + #[cfg(all(target_arch = "x86_64", intel_vtd))] { let vbdf = dev.get_vbdf(); crate::device::iommu::flush( @@ -679,7 +679,7 @@ pub fn mmio_vpci_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { Ok(()) } -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] pub fn mmio_dwc_io_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { { let zone = this_zone(); @@ -720,7 +720,7 @@ pub fn mmio_dwc_io_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { Ok(()) } -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] pub fn mmio_dwc_cfg_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { // info!("mmio_dwc_cfg_handler {:#x}", mmio.address + _base); let zone = this_zone(); @@ -805,7 +805,7 @@ pub fn mmio_dwc_cfg_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { Ok(()) } -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] pub fn mmio_vpci_handler_dbi(mmio: &mut MMIOAccess, _base: usize) -> HvResult { // info!("mmio_vpci_handler_dbi {:#x}", mmio.address); diff --git a/src/pci/pci_struct.rs b/src/pci/pci_struct.rs index 87fdf70b6..837969c12 100644 --- a/src/pci/pci_struct.rs +++ b/src/pci/pci_struct.rs @@ -1323,7 +1323,7 @@ impl Iterator for PciIterator { // (UEFI/BIOS) may skip bus numbers for subordinate bus reservation, // causing calculated bus numbers to diverge from actual hardware // bus assignments — making devices behind bridges invisible. - #[cfg(feature = "no_pcie_bar_realloc")] + #[cfg(no_pcie_bar_realloc)] let next_bus = { let bridge_base = node.get_base(); let bus_reg = unsafe { @@ -1358,7 +1358,7 @@ impl Iterator for PciIterator { parent.subordinate_bus + 1 } }; - #[cfg(not(feature = "no_pcie_bar_realloc"))] + #[cfg(not(no_pcie_bar_realloc))] let next_bus = parent.subordinate_bus + 1; let bdf = Bdf::new(domain, next_bus, 0, 0); @@ -1465,10 +1465,10 @@ impl Bridge { } // When no_pcie_bar_realloc is enabled, firmware already assigned correct bus // numbers — don't overwrite them. - #[cfg(feature = "no_pcie_bar_realloc")] + #[cfg(no_pcie_bar_realloc)] return; - #[cfg(not(feature = "no_pcie_bar_realloc"))] + #[cfg(not(no_pcie_bar_realloc))] { // we need to update the bridge bus number if we want linux not to update bus number unsafe { @@ -1987,9 +1987,9 @@ impl VirtualPciConfigSpace { // } // } // false - // #[cfg(feature = "dwc_pcie")] + // #[cfg(dwc_pcie)] // return true; - // #[cfg(not(feature = "dwc_pcie"))] + // #[cfg(not(dwc_pcie))] return false; } _ => false, diff --git a/src/pci/pci_test.rs b/src/pci/pci_test.rs index 182a614de..c6d7b22d4 100644 --- a/src/pci/pci_test.rs +++ b/src/pci/pci_test.rs @@ -25,7 +25,7 @@ use super::{ pci_struct::{Bdf, VirtualPciConfigSpace, CONFIG_LENTH}, }; -#[cfg(feature = "ecam_pcie")] +#[cfg(ecam_pcie)] use super::{mem_alloc::BaseAllocator, pci_struct::RootComplex}; use crate::{ @@ -40,7 +40,7 @@ pub static GLOBAL_PCIE_LIST_TEST: Lazy HvZoneConfig { let mut _num_pci_devs: u64 = 0; let mut _num_pci_bus: u64 = 0; - #[cfg(feature = "pci")] + #[cfg(pci)] { check!(ROOT_PCI_DEVS.len(), CONFIG_MAX_PCI_DEV, "ROOT_PCI_DEVS"); pci_devs[..ROOT_PCI_DEVS.len()].copy_from_slice(&ROOT_PCI_DEVS); diff --git a/src/zone.rs b/src/zone.rs index 158d8c677..ee10b4237 100644 --- a/src/zone.rs +++ b/src/zone.rs @@ -20,9 +20,9 @@ use crate::consts::{INVALID_ADDRESS, MAX_CPU_NUM}; use crate::pci::pci_struct::VirtualRootComplex; use spin::{RwLock, RwLockReadGuard, RwLockWriteGuard}; -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] use crate::pci::{config_accessors::dwc_atu::AtuConfig, PciConfigAddress}; -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] use alloc::collections::btree_map::BTreeMap; use crate::arch::mm::new_s2_memory_set; @@ -36,7 +36,7 @@ use crate::memory::{MMIOConfig, MMIOHandler, MMIORegion, MemorySet}; use core::panic; use core::sync::atomic::{AtomicBool, Ordering}; -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] #[derive(Debug)] pub struct VirtualAtuConfigs { ecam_to_atu: BTreeMap, @@ -44,7 +44,7 @@ pub struct VirtualAtuConfigs { cfg_base_to_ecam: BTreeMap, } -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] impl VirtualAtuConfigs { pub fn new() -> Self { Self { @@ -123,7 +123,7 @@ pub struct ZoneInner { gpm: MemorySet, iommu_pt: Option>, vpci_bus: VirtualRootComplex, - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] atu_configs: VirtualAtuConfigs, } @@ -175,13 +175,13 @@ impl ZoneInner { cpu_num: 0, cpu_set: CpuSet::new(MAX_CPU_NUM as usize, 0), irq_bitmap: [0; 1024 / 32], - iommu_pt: if cfg!(feature = "iommu") { + iommu_pt: if cfg!(iommu) { Some(new_s2_memory_set()) } else { None }, vpci_bus: VirtualRootComplex::new(), - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] atu_configs: VirtualAtuConfigs::new(), } } @@ -308,12 +308,12 @@ impl ZoneInner { &mut self.vpci_bus } - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] pub fn atu_configs(&self) -> &VirtualAtuConfigs { &self.atu_configs } - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] pub fn atu_configs_mut(&mut self) -> &mut VirtualAtuConfigs { &mut self.atu_configs } @@ -388,7 +388,7 @@ pub fn zone_create(config: &HvZoneConfig) -> HvResult> { zone.pt_init(config.memory_regions())?; zone.mmio_init(&config.arch_config); - #[cfg(feature = "pci")] + #[cfg(pci)] { let _ = zone.virtual_pci_mmio_init(&config.pci_config, config.num_pci_bus as usize); let _ = zone.guest_pci_init( @@ -434,7 +434,7 @@ pub fn zone_create(config: &HvZoneConfig) -> HvResult> { // #[cfg(target_arch = "aarch64")] // zone.ivc_init(config.ivc_config()); - #[cfg(all(feature = "iommu", target_arch = "aarch64"))] + #[cfg(all(iommu, target_arch = "aarch64"))] zone.iommu_pt_init(config.memory_regions(), &config.arch_config)?; /* loongarch page table emergency */ diff --git a/tools/gen_cargo_config.sh b/tools/gen_cargo_config.sh deleted file mode 100755 index 3bb6832ab..000000000 --- a/tools/gen_cargo_config.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -set -e - -CONFIG_TOML=".cargo/config.toml" -echo "" > $CONFIG_TOML - -function find_hvisor_src { - # find in ./CHANGELOG.md ../CHANGELOG.md - ret=$(find . -name CHANGELOG.md -exec grep -l "hvisor" {} \;) - if [ -z "$ret" ]; then - ret=$(find .. -name CHANGELOG.md -exec grep -l "hvisor" {} \;) - fi - if [ -z "$ret" ]; then - echo "." - fi - echo $(dirname $ret) -} - -CONFIG_TOML_TEMPLATE="platform/$ARCH/$BOARD/cargo/config.template.toml" -CONFIG_TOML_TEMPLATE=$(realpath $CONFIG_TOML_TEMPLATE) -TEMPLATE=$(cat $CONFIG_TOML_TEMPLATE) -LD_SCRIPT="platform/$ARCH/$BOARD/linker.ld" -LD_SCRIPT=$(realpath $LD_SCRIPT) -HVISOR_SRC=$(realpath $(find_hvisor_src)) -HOST_ARCH=$(uname -m) -OS=$(uname -s) -OS_VERSION=$(uname -r) -DISTRO=unknown -if [ -f /etc/os-release ]; then - DISTRO=$(grep '^ID=' /etc/os-release | cut -d= -f2) -fi - -echo "# Generated by gen_cargo_config.sh at $(date) ($(whoami)@$(hostname))" >> $CONFIG_TOML -echo "# BASH_VERSION = $BASH_VERSION" >> $CONFIG_TOML -echo "# HOST_ARCH = $HOST_ARCH" >> $CONFIG_TOML -echo "# OS_INFO = $OS $OS_VERSION $DISTRO" >> $CONFIG_TOML -echo "# ARCH = $ARCH" >> $CONFIG_TOML -echo "# BOARD = $BOARD" >> $CONFIG_TOML -echo "# FEATURES = $FEATURES" >> $CONFIG_TOML -echo "# HVISOR_SRC = $HVISOR_SRC" >> $CONFIG_TOML -echo "# LD_SCRIPT = $LD_SCRIPT" >> $CONFIG_TOML -echo "# TEMPLATE = $CONFIG_TOML_TEMPLATE" >> $CONFIG_TOML -echo "" >> $CONFIG_TOML - -# place holders: __XXX__ -> XXX -TEMPLATE=$(echo "$TEMPLATE" | sed -e 's/__ARCH__/'"$ARCH"'/g') -TEMPLATE=$(echo "$TEMPLATE" | sed -e 's/__BOARD__/'"$BOARD"'/g') - -echo "$TEMPLATE" >> $CONFIG_TOML - -# also generate a .config file in the HVISOR_SRC directory -CONFIG_FILE="$HVISOR_SRC/.config" -echo "ARCH=$ARCH" > $CONFIG_FILE -echo "BOARD=$BOARD" >> $CONFIG_FILE -echo "BID=$ARCH/$BOARD" >> $CONFIG_FILE -echo "FEATURES=$FEATURES" >> $CONFIG_FILE -echo "HVISOR_SRC=$HVISOR_SRC" >> $CONFIG_FILE -echo "LD_SCRIPT=$LD_SCRIPT" >> $CONFIG_FILE -echo "TEMPLATE=$CONFIG_TOML_TEMPLATE" >> $CONFIG_FILE \ No newline at end of file diff --git a/tools/gen_vscode_settings.sh b/tools/gen_vscode_settings.sh deleted file mode 100755 index 2d911faa0..000000000 --- a/tools/gen_vscode_settings.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/bash -set -e - -function find_hvisor_src { - ret=$(find . -name CHANGELOG.md -exec grep -l "hvisor" {} \;) - if [ -z "$ret" ]; then - ret=$(find .. -name CHANGELOG.md -exec grep -l "hvisor" {} \;) - fi - if [ -z "$ret" ]; then - echo "ERROR: Could not locate hvisor source root (CHANGELOG.md with 'hvisor')" >&2 - exit 1 - fi - echo "$(dirname "$ret")" -} - -HVISOR_SRC=$(realpath $(find_hvisor_src)) -cd "$HVISOR_SRC" - -if [ -n "$BID" ]; then - ARCH=$(echo "$BID" | cut -d'/' -f1) - BOARD=$(echo "$BID" | cut -d'/' -f2) -fi - -if [ -z "$ARCH" ] || [ -z "$BOARD" ]; then - echo "ERROR: ARCH/BOARD not set, please use BID=arch/board or ARCH=... BOARD=..." - exit 1 -fi - -if [ -z "$FEATURES" ]; then - FEATURES=$(./tools/read_features.sh "$ARCH" "$BOARD") -fi - -FEATURES=$(echo "$FEATURES" | tr '\n' ' ' | tr -s ' ' | sed 's/^ *//;s/ *$//') - -case "$ARCH" in - aarch64) TARGET="aarch64-unknown-none" ;; - riscv64) TARGET="riscv64gc-unknown-none-elf" ;; - loongarch64) TARGET="loongarch64-unknown-none" ;; - x86_64) TARGET="x86_64-unknown-none" ;; - *) - echo "ERROR: Unsupported ARCH value: $ARCH" - exit 1 - ;; -esac - -mkdir -p .vscode - -cat > .vscode/settings.json <//kconfig/defconfig from legacy cargo/features lines.""" +from __future__ import annotations + +import pathlib + +ROOT = pathlib.Path(__file__).resolve().parents[2] + +# platform//... -> Kconfig arch choice line +ARCH_LINE: dict[str, str] = { + "aarch64": "CONFIG_ARCH_AARCH64=y", + "riscv64": "CONFIG_ARCH_RISCV64=y", + "loongarch64": "CONFIG_ARCH_LOONGARCH64=y", + "x86_64": "CONFIG_ARCH_X86_64=y", +} + "gicv2": ["CONFIG_IRQ_GICV2=y"], + "gicv3": ["CONFIG_IRQ_GICV3=y"], + "pl011": ["CONFIG_PL011=y"], + "imx_uart": ["CONFIG_IMX_UART=y"], + "xuartps": ["CONFIG_XUARTPS=y"], + "uart_16550": ["CONFIG_UART_16550=y"], + "uart16550a": ["CONFIG_UART16550A=y"], + "loongson_uart": ["CONFIG_LOONGSON_UART=y"], + "pci": ["CONFIG_PCI=y"], + "ecam_pcie": ["CONFIG_PCIE_ECAM=y"], + "dwc_pcie": ["CONFIG_PCIE_DWC=y"], + "loongarch64_pcie": ["CONFIG_PCIE_LOONGARCH64=y"], + "no_pcie_bar_realloc": ["CONFIG_NO_PCIE_BAR_REALLOC=y"], + "arm_smmu": ["CONFIG_ARM_SMMU=y"], + "intel_vtd": ["CONFIG_INTEL_VTD=y"], + "riscv_iommu": ["CONFIG_RISCV_IOMMU=y"], + "share_s2pt": ["CONFIG_SHARE_S2PT=y"], + "aclint": ["CONFIG_ACLINT=y"], + "plic": ["CONFIG_PLIC=y"], + "dp1000_plic": ["CONFIG_DP1000_PLIC=y"], + "aia": ["CONFIG_AIA=y"], + "sstc": ["CONFIG_SSTC=y"], + "eic770x_soc": ["CONFIG_EIC770X_SOC=y"], + "eic7700_sysreg": ["CONFIG_EIC7700_SYSREG=y"], + "sifive_ccache": ["CONFIG_SIFIVE_CCACHE=y"], + "loongson_7a2000": ["CONFIG_LOONGSON_7A2000=y"], + "loongson_3a5000": ["CONFIG_LOONGSON_3A5000=y"], + "loongson_3a6000": ["CONFIG_LOONGSON_3A6000=y"], + "graphics": ["CONFIG_GRAPHICS=y"], + "split_screen": ["CONFIG_SPLIT_SCREEN=y"], +} + + +def features_to_configs(lines: list[str]) -> list[str]: + out: list[str] = [] + seen: set[str] = set() + for raw in lines: + w = raw.strip() + if not w: + continue + if w not in FEATURE_LINES: + raise SystemExit(f"unknown feature token: {w!r}") + for ln in FEATURE_LINES[w]: + if ln not in seen: + seen.add(ln) + out.append(ln) + return sorted(out) + + +def main() -> None: + for feat_path in sorted(ROOT.glob("platform/*/")): + arch = feat_path.name + for board_path in sorted(feat_path.glob("*/")): + board = board_path.name + legacy = board_path / "cargo" / "features" + if not legacy.is_file(): + continue + lines = legacy.read_text().splitlines() + configs = features_to_configs(lines) + arch_ln = ARCH_LINE.get(arch) + if arch_ln is None: + raise SystemExit(f"unknown arch directory {arch!r}") + configs = sorted(set([*configs, arch_ln])) + out_dir = board_path / "kconfig" + out_dir.mkdir(parents=True, exist_ok=True) + (out_dir / "defconfig").write_text("\n".join(configs) + "\n") + print(f"wrote {out_dir / 'defconfig'} ({len(configs)} symbols)") + + +if __name__ == "__main__": + main() diff --git a/tools/kconfig/bootstrap_venv.sh b/tools/kconfig/bootstrap_venv.sh new file mode 100755 index 000000000..2b999cbf6 --- /dev/null +++ b/tools/kconfig/bootstrap_venv.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Create tools/kconfig/.venv and install kconfiglib (+ PySocks) from vendored wheels +# so pip never needs SOCKS support during the first install (fixes Jenkins when +# ALL_PROXY/HTTPS_PROXY is socks5:// — urllib3 errors with "Missing dependencies for SOCKS support"). +set -euo pipefail +ROOT="$(cd "$(dirname "$0")" && pwd)" +VENV="${ROOT}/.venv" +REQ="${ROOT}/requirements.txt" +VEN="${ROOT}/vendor" + +if [[ ! -f "${REQ}" ]]; then + echo "ERROR: missing ${REQ}" >&2 + exit 1 +fi +if [[ ! -d "${VEN}" ]] || ! compgen -G "${VEN}/*.whl" > /dev/null; then + echo "ERROR: missing wheels under ${VEN}/ (run: python3 -m pip download -d ${VEN} -r ${REQ})" >&2 + exit 1 +fi + +python3 -m venv "${VENV}" +# Do not upgrade pip here: that would hit PyPI and can fail behind socks5:// before PySocks exists. +"${VENV}/bin/pip" install -q --no-index --find-links="${VEN}" -r "${REQ}" diff --git a/tools/kconfig/host_config.sh b/tools/kconfig/host_config.sh new file mode 100755 index 000000000..976334bf5 --- /dev/null +++ b/tools/kconfig/host_config.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# Host-side helpers that consume repo-root .config (Kconfig + metadata from kconfig_cli.py). +# Usage (from repo root, or any cwd — script cds to root): +# ./tools/kconfig/host_config.sh cargo # write .cargo/config.toml +# ./tools/kconfig/host_config.sh vscode # refresh .config + write .vscode/settings.json +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$ROOT" + +KPY="$ROOT/tools/kconfig/.venv/bin/python" +CLI="$ROOT/tools/kconfig/kconfig_cli.py" + +cmd_gen_cargo() { + local CONFIG_TOML=".cargo/config.toml" + echo "" >"$CONFIG_TOML" + + if [ ! -f .config ]; then + echo "error: missing .config (run: make defconfig)" >&2 + exit 1 + fi + local ARCH BOARD HVISOR_SRC LD_SCRIPT CONFIG_TOML_TEMPLATE + ARCH=$(grep '^ARCH=' .config | head -1 | cut -d= -f2-) + BOARD=$(grep '^BOARD=' .config | head -1 | cut -d= -f2-) + HVISOR_SRC=$(grep '^HVISOR_SRC=' .config | head -1 | cut -d= -f2-) + LD_SCRIPT=$(grep '^LD_SCRIPT=' .config | head -1 | cut -d= -f2-) + CONFIG_TOML_TEMPLATE=$(grep '^TEMPLATE=' .config | head -1 | cut -d= -f2-) + if [ -z "$ARCH" ] || [ -z "$BOARD" ] || [ -z "$HVISOR_SRC" ] || [ -z "$LD_SCRIPT" ] || [ -z "$CONFIG_TOML_TEMPLATE" ]; then + echo "error: .config missing ARCH/BOARD/HVISOR_SRC/LD_SCRIPT/TEMPLATE lines (run: make defconfig)" >&2 + exit 1 + fi + local TEMPLATE + TEMPLATE=$(cat "$CONFIG_TOML_TEMPLATE") + local HOST_ARCH OS OS_VERSION DISTRO + HOST_ARCH=$(uname -m) + OS=$(uname -s) + OS_VERSION=$(uname -r) + DISTRO=unknown + if [ -f /etc/os-release ]; then + DISTRO=$(grep '^ID=' /etc/os-release | cut -d= -f2) + fi + + { + echo "# Generated by tools/kconfig/host_config.sh cargo at $(date) ($(whoami)@$(hostname))" + echo "# BASH_VERSION = $BASH_VERSION" + echo "# HOST_ARCH = $HOST_ARCH" + echo "# OS_INFO = $OS $OS_VERSION $DISTRO" + echo "# ARCH = $ARCH" + echo "# BOARD = $BOARD" + echo "# (platform toggles come from .config CONFIG_* via build.rs)" + echo "# HVISOR_SRC = $HVISOR_SRC" + echo "# LD_SCRIPT = $LD_SCRIPT" + echo "# TEMPLATE = $CONFIG_TOML_TEMPLATE" + echo "" + } >>"$CONFIG_TOML" + + TEMPLATE=$(echo "$TEMPLATE" | sed -e 's/__ARCH__/'"$ARCH"'/g') + TEMPLATE=$(echo "$TEMPLATE" | sed -e 's/__BOARD__/'"$BOARD"'/g') + echo "$TEMPLATE" >>"$CONFIG_TOML" +} + +cmd_vscode() { + local ARCH="${ARCH:-}" BOARD="${BOARD:-}" + if [ -n "${BID:-}" ]; then + ARCH=$(echo "$BID" | cut -d'/' -f1) + BOARD=$(echo "$BID" | cut -d'/' -f2) + fi + if [ -z "$ARCH" ] || [ -z "$BOARD" ]; then + echo "ERROR: ARCH/BOARD not set, use BID=arch/board or ARCH=... BOARD=..." >&2 + exit 1 + fi + if [ ! -x "$KPY" ]; then + echo "ERROR: tools/kconfig/.venv missing. Run: ./tools/kconfig/bootstrap_venv.sh" >&2 + exit 1 + fi + ARCH="$ARCH" BOARD="$BOARD" "$KPY" "$CLI" defconfig + + local CFG_JSON + CFG_JSON=$("$KPY" "$CLI" vscode-cfgs .config kconfig/cfg_map.toml) + + local TARGET + case "$ARCH" in + aarch64) TARGET="aarch64-unknown-none" ;; + riscv64) TARGET="riscv64gc-unknown-none-elf" ;; + loongarch64) TARGET="loongarch64-unknown-none" ;; + x86_64) TARGET="x86_64-unknown-none" ;; + *) + echo "ERROR: Unsupported ARCH value: $ARCH" >&2 + exit 1 + ;; + esac + + mkdir -p .vscode + cat >.vscode/settings.json <&2 + exit 2 + ;; +esac diff --git a/tools/kconfig/kconfig_cli.py b/tools/kconfig/kconfig_cli.py new file mode 100644 index 000000000..ca135fa4e --- /dev/null +++ b/tools/kconfig/kconfig_cli.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""Kconfig entrypoints: defconfig, menuconfig, vscode-cfgs (one module to avoid scattered scripts).""" +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +import tomllib +from pathlib import Path + +_KCONF_ARCH_TO_DIR: dict[str, str] = { + "ARCH_AARCH64": "aarch64", + "ARCH_RISCV64": "riscv64", + "ARCH_LOONGARCH64": "loongarch64", + "ARCH_X86_64": "x86_64", +} + + +def arch_from_kconf(kconf) -> str | None: + for sym_name, arch in _KCONF_ARCH_TO_DIR.items(): + sym = kconf.syms.get(sym_name) + if sym is not None and sym.str_value == "y": + return arch + return None + + +def write_root_dot_config(root: Path, arch: str, board: str, kconf) -> None: + """Write repo-root `.config`: metadata lines then kconfiglib body.""" + cfg = root / ".config" + derived = arch_from_kconf(kconf) + if derived is None: + raise SystemExit( + "Kconfig: no architecture selected; enable exactly one of " + "ARCH_AARCH64 / ARCH_RISCV64 / ARCH_LOONGARCH64 / ARCH_X86_64 in defconfig." + ) + if derived != arch: + raise SystemExit( + f"Kconfig architecture ({derived}) does not match " + f"platform path arch ({arch}) for board {board!r}. " + "Fix kconfig/defconfig or pick the correct ARCH/BOARD." + ) + arch_out = derived + ld = (root / "platform" / arch_out / board / "linker.ld").resolve() + tmpl = (root / "platform" / arch_out / board / "cargo" / "config.template.toml").resolve() + hvisor_src = str(root.resolve()) + bid = f"{arch_out}/{board}" + kconf.write_config(str(cfg)) + body = cfg.read_text() + meta = ( + f"ARCH={arch_out}\n" + f"BOARD={board}\n" + f"BID={bid}\n" + f"HVISOR_SRC={hvisor_src}\n" + f"LD_SCRIPT={ld}\n" + f"TEMPLATE={tmpl}\n" + "\n" + ) + cfg.write_text(meta + body) + + +def _repo_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def cmd_defconfig() -> None: + arch = os.environ.get("ARCH", "") + board = os.environ.get("BOARD", "") + if not arch or not board: + print("error: set ARCH and BOARD", file=sys.stderr) + sys.exit(2) + root = _repo_root() + os.chdir(root) + try: + from kconfiglib import Kconfig + except ImportError: + print("error: pip install kconfiglib (see tools/kconfig/requirements.txt)", file=sys.stderr) + sys.exit(1) + kconf = Kconfig(str(root / "kconfig" / "Kconfig")) + defcfg = root / "platform" / arch / board / "kconfig" / "defconfig" + if not defcfg.is_file(): + print(f"error: missing {defcfg}", file=sys.stderr) + sys.exit(1) + kconf.load_config(str(defcfg)) + write_root_dot_config(root, arch, board, kconf) + print(f"wrote .config for {arch}/{board}") + + +def cmd_menuconfig() -> None: + arch = os.environ.get("ARCH", "") + board = os.environ.get("BOARD", "") + if not arch or not board: + print("error: set ARCH and BOARD", file=sys.stderr) + sys.exit(2) + root = _repo_root() + os.chdir(root) + try: + from kconfiglib import Kconfig + except ImportError as e: + print(f"error: kconfiglib is not installed: {e}", file=sys.stderr) + print("hint: tools/kconfig/.venv/bin/pip install -r tools/kconfig/requirements.txt", file=sys.stderr) + sys.exit(1) + try: + import menuconfig as kc_menu + except ImportError as e: + print(f"error: kconfiglib TUI (menuconfig) could not load: {e}", file=sys.stderr) + print( + "hint: the UI needs Python stdlib curses. " + "Debian/Ubuntu: apt install python3-curses; Fedora: dnf install python3-curses; " + "Windows: pip install windows-curses", + file=sys.stderr, + ) + sys.exit(1) + kconf = Kconfig(str(root / "kconfig" / "Kconfig")) + cfg = root / ".config" + defcfg = root / "platform" / arch / board / "kconfig" / "defconfig" + if cfg.is_file(): + kconf.load_config(str(cfg)) + elif defcfg.is_file(): + kconf.load_config(str(defcfg)) + kc_menu.menuconfig(kconf) + write_root_dot_config(root, arch, board, kconf) + + +def cmd_vscode_cfgs(cfg_path: Path, map_path: Path) -> None: + sym = tomllib.loads(map_path.read_text(encoding="utf-8"))["symbols"] + enabled: set[str] = set() + for line in cfg_path.read_text().splitlines(): + line = line.strip() + m = re.match(r"^(CONFIG_[A-Z0-9_]+)=(y|m)$", line) + if not m: + continue + k = m.group(1) + if k in sym: + enabled.add(sym[k]) + print(json.dumps(sorted(enabled))) + + +def main() -> int: + p = argparse.ArgumentParser(description="hvisor Kconfig helpers") + sub = p.add_subparsers(dest="cmd", required=True) + + sub.add_parser("defconfig", help="merge board defconfig + Kconfig into repo-root .config (env ARCH, BOARD)") + sub.add_parser( + "menuconfig", + help="interactive Kconfig UI (env ARCH, BOARD); do not name this file menuconfig.py", + ) + p_vc = sub.add_parser("vscode-cfgs", help="emit JSON array of rustc cfg names for rust-analyzer") + p_vc.add_argument("config", type=Path, help="path to repo-root .config") + p_vc.add_argument("map", type=Path, help="path to kconfig/cfg_map.toml") + + args = p.parse_args() + if args.cmd == "defconfig": + cmd_defconfig() + elif args.cmd == "menuconfig": + cmd_menuconfig() + elif args.cmd == "vscode-cfgs": + cmd_vscode_cfgs(args.config, args.map) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/kconfig/requirements.txt b/tools/kconfig/requirements.txt new file mode 100644 index 000000000..49154ab14 --- /dev/null +++ b/tools/kconfig/requirements.txt @@ -0,0 +1,3 @@ +# Lets pip/urllib3 use socks5:// proxies (needed before first online pip install in some CI agents). +PySocks>=1.7.1 +kconfiglib>=14.1.0 diff --git a/tools/kconfig/save_defconfig.sh b/tools/kconfig/save_defconfig.sh new file mode 100755 index 000000000..f474d5d3a --- /dev/null +++ b/tools/kconfig/save_defconfig.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Write CONFIG_* lines from repo-root .config to the board defconfig (sorted). +set -euo pipefail +ARCH="${1:?arch}" +BOARD="${2:?board}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$ROOT" + +OUT="platform/${ARCH}/${BOARD}/kconfig/defconfig" +mkdir -p "$(dirname "$OUT")" +if [[ ! -f .config ]]; then + echo "error: no .config in repo root" >&2 + exit 1 +fi +grep '^CONFIG_' .config | LC_ALL=C sort >"$OUT" +echo "wrote $OUT" diff --git a/tools/kconfig/vendor/PySocks-1.7.1-py3-none-any.whl b/tools/kconfig/vendor/PySocks-1.7.1-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..ac66f59021d16eb13c326494e7a5565507a92f02 GIT binary patch literal 16725 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 Y8Su~H1_%i4^TqSo`2?_j4kAGR3nHOvdjJ3c literal 0 HcmV?d00001 diff --git a/tools/read_features.sh b/tools/read_features.sh deleted file mode 100755 index 81b0b4841..000000000 --- a/tools/read_features.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -set -e - -ARCH=$1 -BOARD=$2 - -FEATURES="platform/$ARCH/$BOARD/cargo/features" -FEATURES=$(cat $FEATURES) -FEATURES=$(echo $FEATURES | tr '\n' ' ') -FEATURES=$(echo $FEATURES | tr -d '\r') -echo $FEATURES From 1c2900a4a2af5eef167e6830eef85f6c40a6a70a Mon Sep 17 00:00:00 2001 From: dallas Date: Mon, 25 May 2026 17:26:28 +0800 Subject: [PATCH 02/53] update feature for config --- .github/workflows/ci.yml | 256 +----------------- .gitignore | 3 +- Makefile | 6 +- build.rs | 30 +- docs/commit-40d2868-kconfig-migration.md | 237 ---------------- docs/kconfig.md | 90 ------ docs/rust-cfgs.md | 102 ------- kconfig/Kconfig | 204 ++++++++------ kconfig/cfg_map.toml | 2 +- kernel_build/bin/python | 1 - kernel_build/bin/python3 | 1 - kernel_build/bin/python3.12 | 1 - kernel_build/lib64 | 1 - kernel_build/pyvenv.cfg | 5 - platform/aarch64/imx8mp/cargo/features | 2 - platform/aarch64/ok6254-c/cargo/features | 2 - platform/aarch64/phytium-pi/cargo/features | 2 - platform/aarch64/qemu-gicv2/cargo/features | 4 - platform/aarch64/qemu-gicv3/cargo/features | 5 - platform/aarch64/rk3568/cargo/features | 5 - platform/aarch64/rk3588/cargo/features | 2 - platform/aarch64/sysoul_x3300/cargo/features | 2 - platform/aarch64/zcu102/cargo/features | 2 - platform/loongarch64/ls3a5000/cargo/features | 7 - platform/loongarch64/ls3a6000/cargo/features | 7 - .../hifive-premier-p550/cargo/features | 4 - .../hifive-premier-p550/kconfig/defconfig | 2 +- platform/riscv64/megrez/cargo/features | 2 - platform/riscv64/megrez/kconfig/defconfig | 2 +- platform/riscv64/qemu-aia/cargo/features | 6 - platform/riscv64/qemu-plic/README.md | 2 +- platform/riscv64/qemu-plic/cargo/features | 5 - platform/riscv64/ur-dp1000/cargo/features | 2 - platform/x86_64/ecx-2300f-peg/cargo/features | 2 - platform/x86_64/nuc14mnk/cargo/features | 6 - platform/x86_64/qemu/cargo/features | 5 - src/arch/riscv64/cpu.rs | 2 +- src/device/irqchip/mod.rs | 2 +- src/pci/mod.rs | 11 - tools/kconfig/bootstrap_defconfigs.py | 86 ------ tools/kconfig/host_config.sh | 15 +- tools/kconfig/kconfig_cli.py | 14 +- 42 files changed, 186 insertions(+), 961 deletions(-) delete mode 100644 docs/commit-40d2868-kconfig-migration.md delete mode 100644 docs/kconfig.md delete mode 100644 docs/rust-cfgs.md delete mode 120000 kernel_build/bin/python delete mode 120000 kernel_build/bin/python3 delete mode 120000 kernel_build/bin/python3.12 delete mode 120000 kernel_build/lib64 delete mode 100644 kernel_build/pyvenv.cfg delete mode 100644 platform/aarch64/imx8mp/cargo/features delete mode 100644 platform/aarch64/ok6254-c/cargo/features delete mode 100644 platform/aarch64/phytium-pi/cargo/features delete mode 100644 platform/aarch64/qemu-gicv2/cargo/features delete mode 100644 platform/aarch64/qemu-gicv3/cargo/features delete mode 100644 platform/aarch64/rk3568/cargo/features delete mode 100644 platform/aarch64/rk3588/cargo/features delete mode 100644 platform/aarch64/sysoul_x3300/cargo/features delete mode 100644 platform/aarch64/zcu102/cargo/features delete mode 100644 platform/loongarch64/ls3a5000/cargo/features delete mode 100644 platform/loongarch64/ls3a6000/cargo/features delete mode 100644 platform/riscv64/hifive-premier-p550/cargo/features delete mode 100644 platform/riscv64/megrez/cargo/features delete mode 100644 platform/riscv64/qemu-aia/cargo/features delete mode 100644 platform/riscv64/qemu-plic/cargo/features delete mode 100644 platform/riscv64/ur-dp1000/cargo/features delete mode 100644 platform/x86_64/ecx-2300f-peg/cargo/features delete mode 100644 platform/x86_64/nuc14mnk/cargo/features delete mode 100644 platform/x86_64/qemu/cargo/features delete mode 100644 tools/kconfig/bootstrap_defconfigs.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e64cdb30..ce91f8d2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,18 @@ # hvisor Github Actions CI # wheatfox +# +# GitHub: format + license checks only. name: CI on: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev workflow_dispatch: env: MODE: debug @@ -26,248 +36,6 @@ jobs: - name: Format Check run: make fmt-test - # this job uses `cargo test` semantics to run unit test exclusively in QEMU - unittest: - name: unittest - runs-on: ubuntu-latest - strategy: - fail-fast: false # continue to run all jobs even if some fail - matrix: - include: - # aarch64 - - arch: aarch64 - board: "qemu-gicv3" - rustc_target: aarch64-unknown-none - - arch: aarch64 - board: "qemu-gicv2" - rustc_target: aarch64-unknown-none - - arch: riscv64 - board: "qemu-plic" - rustc_target: riscv64gc-unknown-none-elf - - arch: riscv64 - board: "qemu-aia" - rustc_target: riscv64gc-unknown-none-elf - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - name: Install Rust Toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.rustc_target }} - components: rust-src - - name: Setup QEMU from Source - if: matrix.arch == 'riscv64' - uses: ./.github/actions/setup-qemu - with: - qemu-version: "10.1.0" - install-path: "/opt/qemu" - - name: Install Dependencies - run: | - sudo apt-get update - if [ "${{ matrix.arch }}" = "riscv64" ]; then - sudo apt-get install -y gdb-multiarch llvm-dev libclang-dev wget - else - sudo apt-get install -y qemu-system-aarch64 gdb-multiarch llvm-dev libclang-dev wget - fi - cargo install --version 0.3.0 --locked cargo-binutils - - name: Set up environment variables - run: | - echo "ARCH=${{ matrix.arch }}" >> $GITHUB_ENV - echo "BOARD=${{ matrix.board }}" >> $GITHUB_ENV - - name: Unit Test - run: make test - - # this job is for compilation test, it will make sure all aarch64, riscv64, loongarch64 can be compiled successfully - # there is no actual running in this job - build: - name: build - runs-on: ${{ matrix.runs-on }} - strategy: - fail-fast: false - matrix: - include: - # aarch64 - - arch: aarch64 - board: "qemu-gicv3" - rustc_target: aarch64-unknown-none - runs-on: ubuntu-latest - - arch: aarch64 - board: "qemu-gicv2" - rustc_target: aarch64-unknown-none - runs-on: ubuntu-latest - - arch: aarch64 - board: "zcu102" - rustc_target: aarch64-unknown-none - runs-on: ubuntu-latest - - arch: aarch64 - board: "imx8mp" - rustc_target: aarch64-unknown-none - runs-on: ubuntu-latest - - arch: aarch64 - board: "rk3568" - rustc_target: aarch64-unknown-none - runs-on: ubuntu-latest - - arch: aarch64 - board: "rk3588" - rustc_target: aarch64-unknown-none - runs-on: ubuntu-latest - - arch: aarch64 - board: "ok6254-c" - rustc_target: aarch64-unknown-none - runs-on: ubuntu-latest - - arch: aarch64 - board: "phytium-pi" - rustc_target: aarch64-unknown-none - runs-on: ubuntu-latest - - arch: aarch64 - board: "sysoul_x3300" - rustc_target: aarch64-unknown-none - runs-on: ubuntu-latest - # riscv64 - - arch: riscv64 - board: "qemu-aia" - rustc_target: riscv64gc-unknown-none-elf - runs-on: ubuntu-latest - - arch: riscv64 - board: "qemu-plic" - rustc_target: riscv64gc-unknown-none-elf - runs-on: ubuntu-latest - - arch: riscv64 - board: "hifive-premier-p550" - rustc_target: riscv64gc-unknown-none-elf - runs-on: ubuntu-latest - - arch: riscv64 - board: "megrez" - rustc_target: riscv64gc-unknown-none-elf - runs-on: ubuntu-latest - - arch: riscv64 - board: "ur-dp1000" - rustc_target: riscv64gc-unknown-none-elf - runs-on: ubuntu-latest - # loongarch64 - - arch: loongarch64 - board: "ls3a5000" - rustc_target: loongarch64-unknown-none - runs-on: ubuntu-latest - - arch: loongarch64 - board: "ls3a6000" - rustc_target: loongarch64-unknown-none - runs-on: ubuntu-latest - # x86_64 - - arch: x86_64 - board: "qemu" - rustc_target: x86_64-unknown-none - runs-on: ubuntu-latest - - arch: x86_64 - board: "nuc14mnk" - rustc_target: x86_64-unknown-none - runs-on: ubuntu-latest - - arch: x86_64 - board: "ecx-2300f-peg" - rustc_target: x86_64-unknown-none - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - name: Install Rust Toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.rustc_target }} - components: rust-src - - name: Install Dependencies - run: | - sudo apt-get update - sudo apt-get install -y wget - cargo install --version 0.3.0 --locked cargo-binutils - wget -q https://github.com/sunhaiyong1978/CLFS-for-LoongArch/releases/download/8.0/loongarch64-clfs-8.0-cross-tools-gcc-full.tar.xz - tar -xf loongarch64-clfs-8.0-cross-tools-gcc-full.tar.xz - echo "$GITHUB_WORKSPACE/cross-tools/bin" >> $GITHUB_PATH - - name: Set up environment variables - run: | - echo "ARCH=${{ matrix.arch }}" >> $GITHUB_ENV - echo "BOARD=${{ matrix.board }}" >> $GITHUB_ENV - - name: Build - run: make - - # this job is for booting root and nonroot inside qemu for system test - systemtest: - # if: false # we need to update corresponding scripts for the new board abstraction - name: systemtest - runs-on: ${{ matrix.runs-on }} - strategy: - fail-fast: false - matrix: - include: - # aarch64 - - arch: aarch64 - rustc_target: aarch64-unknown-none - board: "qemu-gicv3" - runs-on: ubuntu-22.04 - qemu_version: "9.0.2" - # riscv64 - - arch: riscv64 - rustc_target: riscv64gc-unknown-none-elf - board: "qemu-plic" - runs-on: ubuntu-22.04 - qemu_version: "9.0.2" - # x86_64 - - arch: x86_64 - rustc_target: x86_64-unknown-none - board: "qemu" - runs-on: ["self-hosted", "X64"] - qemu_version: "9.2.3" - # Board scripts use kconfig/defconfig (see docs/kconfig.md) - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - name: Install Rust Toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.rustc_target }} - components: rust-src - - - name: Setup QEMU from Source - if: matrix.arch != 'x86_64' - uses: ./.github/actions/setup-qemu - with: - qemu-version: ${{ matrix.qemu_version }} - install-path: '/opt/qemu' - - - name: Setup ARM aarch64-none-linux-gnu - if: matrix.arch == 'aarch64' - uses: ./.github/actions/setup-aarch64-none-linux-gnu - with: - version: '10.3-2021.07' - install-path: '/opt/aarch64-none-linux-gnu' - - - name: Setup RISCV riscv64-unknown-linux-gnu - if: matrix.arch == 'riscv64' - uses: ./.github/actions/setup-riscv64-unknown-linux-gnu - with: - version: '13.2.0' - install-path: '/opt/riscv64-unknown-linux-gnu' - - - name: Install Dependencies - run: | - sudo apt-get update - sudo apt-get install -y qemu-system-aarch64 qemu-system-riscv64 gdb-multiarch llvm-dev libclang-dev wget expect device-tree-compiler p7zip-full gcc-aarch64-linux-gnu gcc-riscv64-linux-gnu - cargo install --version 0.3.0 --locked cargo-binutils - - - name: x86_64 Dependencies - if: matrix.arch == 'x86_64' - run: | - sudo apt-get install -y qemu-system-x86-64 grub-common xorriso grub-efi-amd64 mtools ovmf - sudo chmod 777 /dev/kvm - sed -i 's/ecam_base:.*,/ecam_base: 0xb0000000,/' ./platform/x86_64/qemu/board.rs - - - name: Set up environment variables - run: | - echo "ARCH=${{ matrix.arch }}" >> $GITHUB_ENV - echo "BOARD=${{ matrix.board }}" >> $GITHUB_ENV - - name: Run System Test - run: | - make stest - license-checker: name: license-checker runs-on: ubuntu-latest @@ -276,5 +44,5 @@ jobs: uses: actions/checkout@v4 - name: License Check run: | - set -x - ./tools/license_checker.sh \ No newline at end of file + set -x + ./tools/license_checker.sh diff --git a/.gitignore b/.gitignore index ae99c1c40..087d952d0 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,5 @@ rootfs1.zip* jenkins/__pycache__ jenkins-cli.jar tools/kconfig/.venv/ -tools/kconfig/__pycache__/ \ No newline at end of file +tools/kconfig/__pycache__/ +kernel_build/ \ No newline at end of file diff --git a/Makefile b/Makefile index 8169330dc..5577ac66d 100644 --- a/Makefile +++ b/Makefile @@ -129,8 +129,10 @@ clean_check: # if .config not exist, then everything is fine # else we read .config and parse ARCH and BOARD, if they are different, we clean the build @if [ -f ".config" ]; then \ - CONFIG_ARCH=$$(grep '^ARCH=' .config | head -1 | cut -d'=' -f2); \ - CONFIG_BOARD=$$(grep '^BOARD=' .config | head -1 | cut -d'=' -f2); \ + CONFIG_ARCH=$$(grep '^# ARCH=' .config 2>/dev/null | head -1 | sed 's/^# ARCH=//'); \ + [ -n "$$CONFIG_ARCH" ] || CONFIG_ARCH=$$(grep '^ARCH=' .config | head -1 | cut -d'=' -f2); \ + CONFIG_BOARD=$$(grep '^# BOARD=' .config 2>/dev/null | head -1 | sed 's/^# BOARD=//'); \ + [ -n "$$CONFIG_BOARD" ] || CONFIG_BOARD=$$(grep '^BOARD=' .config | head -1 | cut -d'=' -f2); \ if [ "$$CONFIG_ARCH" != "$(ARCH)" ] || [ "$$CONFIG_BOARD" != "$(BOARD)" ]; then \ echo "$(COLOR_YELLOW)$(COLOR_BOLD)ARCH or BOARD changed(OLD: $$CONFIG_ARCH/$$CONFIG_BOARD, NEW: $(ARCH)/$(BOARD)), cleaning...$(COLOR_RESET)"; \ ./tools/clean.sh; \ diff --git a/build.rs b/build.rs index 5288ee847..0b01b8d7b 100644 --- a/build.rs +++ b/build.rs @@ -36,21 +36,37 @@ struct BuildEnv { bid: String, } -// parse ARCH, BOAR and BID from .config +// parse ARCH, BOARD and BID from .config (comment-prefixed metadata from kconfig_cli.py, +// or legacy unprefixed lines). fn parse_build_env(file_path: &str) -> BuildEnv { let file = fs::read_to_string(file_path).expect("Failed to read .config file"); let mut arch = String::new(); let mut board = String::new(); let mut bid = String::new(); for line in file.lines() { - let parts: Vec<&str> = line.split('=').collect(); - if parts.len() != 2 { + let t = line.trim(); + if let Some(v) = t.strip_prefix("# ARCH=") { + arch = v.to_string(); + continue; + } + if let Some(v) = t.strip_prefix("# BOARD=") { + board = v.to_string(); + continue; + } + if let Some(v) = t.strip_prefix("# BID=") { + bid = v.to_string(); continue; } - match parts[0] { - "ARCH" => arch = parts[1].to_string(), - "BOARD" => board = parts[1].to_string(), - "BID" => bid = parts[1].to_string(), + if t.starts_with('#') { + continue; + } + let Some((k, v)) = t.split_once('=') else { + continue; + }; + match k { + "ARCH" if arch.is_empty() => arch = v.to_string(), + "BOARD" if board.is_empty() => board = v.to_string(), + "BID" if bid.is_empty() => bid = v.to_string(), _ => {} } } diff --git a/docs/commit-40d2868-kconfig-migration.md b/docs/commit-40d2868-kconfig-migration.md deleted file mode 100644 index 1650b1eaa..000000000 --- a/docs/commit-40d2868-kconfig-migration.md +++ /dev/null @@ -1,237 +0,0 @@ -# Commit `40d2868`:从 Cargo `feature` 到 Kconfig 的完整说明 - -本文档基于 commit `40d2868cf97a96ca9c98d61430f90ec52375e22d`(message: `kconfig`),说明该提交如何将平台能力开关从 **Cargo `[features]` + 每板 `cargo/features` 文件** 迁移到 **Linux 风格的 Kconfig(`kconfig/Kconfig` + 每板 `kconfig/defconfig`)+ 根目录 `.config`**,以及各新增/修改文件的含义与必要性。 - -与日常用法更短的操作说明见同目录下的 [`kconfig.md`](./kconfig.md)。 - ---- - -## 一、动机与总体架构变化 - -### 1.1 旧模型(迁移前) - -- **`Cargo.toml` `[features]`**:声明大量空 feature(如 `pci`、`gicv3`),仅用于在编译期打开 `#[cfg(feature = "...")]`。 -- **`platform///cargo/features`**:每行一个 feature 名;`Makefile` 通过 `tools/read_features.sh` 读入,拼成 `cargo build --features "..."`。 -- **局限**:无统一菜单、无 `depends on` / `select` 语义、互斥关系只能靠文档或运行时;与内核/固件生态的 Kconfig 习惯不一致;`rust-analyzer` 与真实 `--features` 易脱节。 - -### 1.2 新模型(迁移后) - -| 层级 | 作用 | -|------|------| -| `kconfig/Kconfig` | 声明 `config` 符号、菜单、`default` / `depends on` / `select`(由 kconfiglib 解析)。 | -| `platform/.../kconfig/defconfig` | 每板默认打开的 `CONFIG_*=y` 列表(可版本管理)。 | -| 仓库根 `.config` | 生成物:前几行 **元数据**(`ARCH`/`BOARD`/`BID`/`HVISOR_SRC`/`LD_SCRIPT`/`TEMPLATE`)+ Kconfig 展开后的 `CONFIG_*` 行;**不入库**(`.gitignore`)。 | -| `kconfig/cfg_map.toml` | **`CONFIG_*` → Rust `cfg` 名** 的唯一映射表(与源码中 `#[cfg(...)]` 一致)。 | -| `build.rs` | 读 `.config` 与 `cfg_map.toml`,对启用的项打印 `cargo:rustc-cfg=...`,并输出 `cargo:rustc-check-cfg`;继续负责 `src/platform/__board.rs` 符号链接。 | -| `Makefile` | 在 `all`/`elf` 等目标前 **`ensure_config`**:若无 `.config` 则先 `defconfig`;**不再**向 cargo 传递 `--features`。 | - -**必要性**:把「板级选了哪些驱动/子系统」从 Cargo 的 package 级 feature 机制中解耦出来,换成与内核类似的配置表面,便于依赖约束、交互式修改、CI 矩阵统一用 `make defconfig` 生成 `.config`。 - ---- - -## 二、Cargo 与构建入口改动 - -### 2.1 删除 `Cargo.toml` 中的 `[features]` 整块 - -原先约 56 行 `[features]`(`iommu`、`pci`、`gicv2`/`gicv3`、各类 PCIe 访问后端、UART、RISC-V/LoongArch/x86 相关开关等)整段移除。 - -**必要性**:避免两套真相(既在 `Cargo.toml` 又在 Kconfig);编译期开关改由 `build.rs` 根据 `.config` 注入 `rustc --cfg`,Cargo 不再参与 feature 组合。 - -### 2.2 `Makefile` - -新增/调整要点: - -- **`kconfig_python`**:`tools/kconfig/.venv/bin/python`(本地 venv 中的 kconfiglib)。 -- **`defconfig` / `menuconfig` / `savedefconfig`**:调用 Python 脚本或 shell 维护 `.config` 与板级 `defconfig`。 -- **`ensure_config`**:根目录无 `.config` 时自动对该 `ARCH`/`BOARD` 执行 `defconfig`(需已 `bootstrap_venv.sh`)。 -- **`all` 依赖**:从 `clean_check gen_cargo_config ...` 变为 `clean_check ensure_config gen_cargo_config ...`;摘要行用 **`DEFCONFIG`** 路径代替 **`FEATURES`**。 -- **`elf` / `clippy` / `test` 等**:在合适处增加 `ensure_config`,保证 `cargo` 调用前已有 `.config`。 -- **移除**:`FEATURES` 变量、`read_features.sh` 调用、`build_args` 中的 `--features "$(FEATURES)"`。 - -**必要性**:构建入口与 CI 一致地先物化 `.config`,再交给 `build.rs` 转成 `rustc` cfg。 - -### 2.3 `tools/read_features.sh` 删除 - -该脚本仅用于拼接 `cargo --features` 的输入。 - -**必要性**:功能被 **`kconfig_cli.py defconfig`** + 根 `.config` 取代,删除避免误用旧路径。 - -### 2.4 `tools/kconfig/host_config.sh` - -合并原 **`tools/gen_cargo_config.sh`** 与 **`tools/gen_vscode_settings.sh`**:子命令 **`cargo`** 从根 **`.config`** 读取 `ARCH`/`BOARD`/`HVISOR_SRC`/`LD_SCRIPT`/`TEMPLATE` 并生成 **`.cargo/config.toml`**;子命令 **`vscode`** 先按当前 `ARCH`/`BOARD`(或 `BID`)调用 **`kconfig_cli.py defconfig`** 刷新 `.config`,再调用 **`kconfig_cli.py vscode-cfgs`** 写 **`.vscode/settings.json`**。脚本始终 `cd` 到仓库根,可从任意 cwd 调用。 - -**必要性**:与 Kconfig 相关的宿主机侧配置集中在一个目录;减少 `tools/` 顶层脚本数量。 - -**注意**:`cargo` 子命令须先存在 `.config`(例如 `make defconfig`)。 - -### 2.5 `tools/kconfig/merge_config.py` 删除 - -与 **`kconfig_cli.py defconfig` + 环境变量 `ARCH`/`BOARD`** 重复;需要命令行板型时可直接 **`ARCH=… BOARD=…`** 调用 **`kconfig_cli.py defconfig`**。 - ---- - -## 三、`build.rs` 行为(核心) - -相对旧版,主要新增/强化逻辑: - -1. **`load_cfg_map`**:解析 `kconfig/cfg_map.toml` 的 `[symbols]` 表。 -2. **`parse_enabled_config_keys`**:扫描 `.config` 中值为 `y` 或 `m` 的 `CONFIG_*` 键。 -3. 对每个映射到的 **Rust cfg 名** 打印 `cargo:rustc-check-cfg=cfg(...)`(配合 Rust 1.80+ 对自定义 cfg 的检查,减少拼写错误)。 -4. 对每个启用的 `CONFIG_*`,若存在于映射表则 `cargo:rustc-cfg=`;未知 `CONFIG_*` 写日志警告。 -5. **`rerun-if-changed`**:除原 `.config` 外,增加 `cfg_map.toml`、`Kconfig`、`tools/kconfig/kconfig_cli.py`、对应板 `kconfig/defconfig`、`board.rs` 等,保证配置变更会触发重新构建脚本。 - -仍保留:从 `.config` 解析 `ARCH`/`BOARD`/`BID`、`__board.rs` 符号链接、`target/build_rs.log` 日志。 - -**必要性**:在 **不修改 `Cargo.toml` features** 的前提下,把 Kconfig 输出接到 Rust 的条件编译;映射表集中管理,避免在 `build.rs` 里硬编码长列表。 - ---- - -## 四、Kconfig 与映射文件 - -### 4.1 `kconfig/Kconfig` - -定义与旧 feature 集对应的 `config` 符号(如 `IRQ_GICV2`/`IRQ_GICV3`、`PCI`、`PCIE_*`、`IOMMU` 及各硬件后端、`PL011`、`AIA`、`GRAPHICS` 等),并使用 `depends on` / `select` 表达依赖(例如 `SHARE_S2PT` `select IOMMU`,PCIe 子选项 `depends on PCI`)。 - -**必要性**:提供可校验的配置空间;`kconfig_cli.py defconfig` / `menuconfig` 加载 defconfig 时会应用这些规则。 - -### 4.2 `kconfig/cfg_map.toml` - -键为 `.config` 中的 **`CONFIG_*` 字面量**,值为源码中使用的 **自定义 cfg 标识符**(如 `CONFIG_IRQ_GICV3` → `irq_gicv3`)。 - -**必要性**:Kconfig 符号名习惯全大写加前缀,而 Rust `cfg` 惯用小写蛇形;单层映射表避免二者混用导致难以检索。 - -**命名调整说明(相对旧 Cargo feature)**:ARM GIC 由原先的 feature 名 `gicv2` / `gicv3` 改为 **`irq_gicv2` / `irq_gicv3`**(与 `CONFIG_IRQ_GICV2` 等对齐),源码中所有 `#[cfg(feature = "gicv3")]` 类用法改为 `#[cfg(irq_gicv3)]` 等。其余多数 cfg 名与旧 feature 名一致或仅增加 `CONFIG_` 前缀映射。 - -### 4.3 各板 `platform/.../kconfig/defconfig` - -由迁移脚本或手工从原 `cargo/features` 生成;内容为 `CONFIG_*=y` 行(排序便于 diff)。 - -**必要性**:板级默认配置仍与旧行为对齐,但格式与内核 defconfig 一致,便于 review。 - -部分 `platform.mk` 等若曾引用旧路径,会改为指向 `kconfig/defconfig` 或相关说明(以具体板文件为准)。 - ---- - -## 五、Python / Shell 工具链(新增代码含义) - -### 5.1 `tools/kconfig/requirements.txt` + `vendor/*.whl` - -依赖 **kconfiglib**(及 **PySocks**,供代理场景)。`bootstrap_venv.sh` 使用 **`--no-index --find-links=vendor`** 安装,避免在仅 `socks5://` 代理环境下首次 `pip install` 因缺少 PySocks 失败(Jenkins 常见)。 - -**必要性**:可复现、可离线的工具链引导;与 [`kconfig.md`](./kconfig.md) 中说明一致。 - -### 5.2 `tools/kconfig/bootstrap_venv.sh` - -创建 `tools/kconfig/.venv` 并从 `vendor/` 安装 wheel。 - -**必要性**:统一团队与 CI 的 Python 环境;`.gitignore` 忽略 `.venv/`,避免把虚拟环境提交进 Git。 - -### 5.3 `tools/kconfig/kconfig_cli.py` - -统一入口(`argparse` 子命令): - -- **`defconfig`**:读环境变量 **`ARCH`/`BOARD`**,加载 **`kconfig/Kconfig`** 与 **`platform/.../kconfig/defconfig`**,校验 **Kconfig 中四选一架构** 与路径一致后,写根目录 **`.config`**(Kconfig 主体 + 元数据 `ARCH`/`BOARD`/`BID`/`HVISOR_SRC`/`LD_SCRIPT`/`TEMPLATE`)。 -- **`menuconfig`**:终端 UI;保存后同样写回带元数据的 **`.config`**。仓库内**不要**再增加名为 **`menuconfig.py`** 的脚本,以免遮蔽 kconfiglib 的同名模块。 -- **`vscode-cfgs <.config> `**:输出 **rust-analyzer** 用的 **`rustc` cfg** JSON 数组(`tomllib` + 映射表)。 - -**必要性**:减少分散的 Python 小文件,便于维护与 `build.rs` 的 `rerun-if-changed` 指向单一脚本。 - -### 5.4 `tools/kconfig/save_defconfig.sh` - -从根 `.config` 中 `grep '^CONFIG_'` 排序写回 `platform/$ARCH/$BOARD/kconfig/defconfig`。 - -**必要性**:与内核 `savedefconfig` 工作流类似,便于把交互或本地调试结果固化进仓库。 - -### 5.5 `tools/kconfig/bootstrap_defconfigs.py` - -遍历仍存在的 `platform/*/.../cargo/features`,通过内置 **`FEATURE_LINES`** 映射表把旧 feature **token** 转为 `CONFIG_*=y`,写入对应 `kconfig/defconfig`;并为目录名对应的架构追加 **`CONFIG_ARCH_*=y`**。PCIe 后端默认值由 **`kconfig/Kconfig`** 中 `choice` 的 **`default PCIE_ECAM`** 在后续 **`kconfig_cli.py defconfig`** 时处理。 - -**必要性**:一次性从旧目录结构批量生成新 defconfig,降低手工迁移错误率。 - ---- - -## 六、Rust 源码层面的改动模式 - -### 6.1 条件编译属性 - -- **由** `#[cfg(feature = "xxx")]` **改为** `#[cfg(xxx)]`(`xxx` 为 `cfg_map.toml` 右侧字符串)。 -- **`cfg(feature = ...)` 与 `cfg(all(...))`**:凡与平台驱动相关的,一律改为自定义 cfg 名(与 `build.rs` 输出一致)。 - -### 6.2 `src/pci/mod.rs` 等处的 `compile_error!` - -在原先互斥依赖人工约束的基础上,用 **`#[cfg(all(ecam_pcie, dwc_pcie))]`** 等组合在编译期直接报错(与 Kconfig 的互斥可双重保险;Kconfig 侧若未完全禁止组合,Rust 侧仍可兜底)。 - -**必要性**:PCIe 访问后端多选一,错误组合应尽早失败。 - -### 6.3 其它模块(`src/device/*`、`src/arch/*`、`src/zone.rs`、`src/logging.rs` 等) - -同一模式:按板能力用 `#[cfg(pci)]`、`#[cfg(iommu)]`、`#[cfg(irq_gicv3)]` 等包裹模块、impl 或代码路径。 - -**必要性**:语义与旧 feature 一致,仅配置来源改变。 - -### 6.4 `src/main.rs` / `src/platform/mod.rs` - -确保启动路径读取的板级信息与新的配置流一致(具体 diff 以该提交为准,通常为 cfg 名或条件分支对齐)。 - ---- - -## 七、IDE、CI 与 x86 板级小改 - -### 7.1 `tools/kconfig/host_config.sh`(`vscode`) - -在生成 `settings.json` 前调用 **`kconfig_cli.py defconfig`** 刷新 `.config`,再用 **`kconfig_cli.py vscode-cfgs`** 生成 `rust-analyzer.cargo.cfgs`(`make vscode`)。 - -**必要性**:保证编辑器与当前 `ARCH`/`BOARD` 及 Kconfig 展开结果一致。 - -### 7.2 GitHub Actions / `Jenkinsfile` - -- **GitHub Actions**:该提交中 **仅修改一条注释**,说明板级脚本与 **`kconfig/defconfig`** 的关系(并指向 `docs/kconfig.md`);矩阵与安装步骤未变。本地/CI 要成功执行 **`make defconfig` / `ensure_config`**,仍需可用的 **`tools/kconfig/.venv`**(见 `bootstrap_venv.sh`);是否在 GHA 各 job 里显式引导 venv 以当前仓库 workflow 为准。 -- **Jenkinsfile**:矩阵构建单元内增加 **`bootstrap_venv.sh`** + **`make defconfig ARCH=... BOARD=...`**,日志中明确不再使用 **`cargo --features`**。 - -**必要性**:Jenkins 在干净工作区中先创建 venv 再生成 `.config`,与「根 `.config` 不入库」模型一致。 - -### 7.3 `platform/x86_64/*/board.rs` - -个别板文件小改(通常与条件编译或默认配置引用路径对齐,见该提交 diff)。 - ---- - -## 八、文档与忽略规则 - -- **`docs/kconfig.md`**:新增,描述命令、目录布局、`menuconfig` 常见问题、与 `rust-analyzer` 的关系、Jenkins 行为等(用户日常查阅)。 -- **`.gitignore`**:增加 `tools/kconfig/.venv/`、`tools/kconfig/__pycache__/` 等,避免将本地 Python 环境提交入库。 - ---- - -## 九、提交中带有的 `kernel_build/` 目录说明 - -该 commit 的统计中包含 `kernel_build/` 下的 `pyvenv.cfg`、指向 Python 的 symlink 等。通常 **虚拟环境不应入库**;若仓库后续已删除或 `.gitignore` 已屏蔽,以当前主干为准。若仍保留,建议团队确认是否为误提交或是否有独立用途(一般推荐仅保留 `tools/kconfig/.venv` 由脚本本地生成)。 - ---- - -## 十、文件改动分类速查(按职责) - -| 类别 | 路径示例 | -|------|-----------| -| Kconfig 定义 | `kconfig/Kconfig` | -| 映射表 | `kconfig/cfg_map.toml` | -| 板级默认 | `platform/**/kconfig/defconfig` | -| 构建脚本 | `Makefile`, `build.rs`, `tools/kconfig/host_config.sh` | -| Kconfig 工具 | `tools/kconfig/kconfig_cli.py`、`bootstrap_venv.sh`、`save_defconfig.sh`、`host_config.sh`、`requirements.txt`、`vendor/*.whl` | -| 迁移辅助 | `tools/kconfig/bootstrap_defconfigs.py` | -| 源码 cfg 切换 | `src/**/*.rs`(属性由 `feature` 改为自定义 cfg) | -| 包定义 | `Cargo.toml`(移除 `[features]`) | -| 文档 | `docs/kconfig.md`、本文档 | - ---- - -## 十一、小结 - -| 问题 | 答案 | -|------|------| -| 配置真相在哪里? | 板级 `kconfig/defconfig` + `kconfig/Kconfig` 规则;生成根 `.config`;`cfg_map.toml` 定义到 Rust cfg 的映射。 | -| Cargo 还参与开关吗? | 不参与;`build.rs` 根据 `.config` 注入 `rustc-cfg`。 | -| 为何引入 Python? | 复用 **kconfiglib** 做 `defconfig`/`menuconfig` 与依赖展开,与手写 parser 相比更可靠。 | -| 旧 `cargo/features` 怎么办? | 可用 `bootstrap_defconfigs.py` 批量转换;长期以 `kconfig/defconfig` 为准。 | - -若仅需操作步骤与故障排除,请优先阅读 [`kconfig.md`](./kconfig.md)。 diff --git a/docs/kconfig.md b/docs/kconfig.md deleted file mode 100644 index 4a73d3a7e..000000000 --- a/docs/kconfig.md +++ /dev/null @@ -1,90 +0,0 @@ -# 平台配置(Kconfig / defconfig) - -hvisor 的平台能力开关由 **Kconfig 符号**(写入根目录 `.config` 的 `CONFIG_*=y` 行)描述,构建时由 [`build.rs`](../build.rs) 读入并转换为 `rustc --cfg`(例如 `irq_gicv3`、`pci`)。源码中使用 `#[cfg(irq_gicv3)]` 等形式,**不再**使用 Cargo 的 `feature`。 - -## 根目录 `.config` 是怎么来的(流程与为何「多段拼接」) - -1. **入口**:`make defconfig` 或 `make menuconfig` 结束时,在仓库根需要一份 **`.config`**,供 `cargo`/`build.rs` 与其它脚本使用。 -2. **板级输入**:只读 **`platform///kconfig/defconfig`**。这里的 **`ARCH`/`BOARD` 来自 Makefile 导出的环境变量**(或手动设置后调用 **`kconfig_cli.py defconfig`**),用来**定位文件路径**;不要求把 `ARCH=` 写进 defconfig 文件本身。 -3. **为何不把 `ARCH`/`BOARD` 写进 defconfig?** - - defconfig 交给 **kconfiglib** 解析,内容应是 **`CONFIG_*=y`**(以及 `#` 注释),与 Linux 习惯一致;`ARCH=` 不是 Kconfig 符号,混进去要么要在加载前剥离,要么污染 `menuconfig`/`savedefconfig` 的语义。 - - 目录 **`platform///`** 已经唯一确定了板型,再在 defconfig 里重复一遍容易与路径不同步。 -4. **拼接内容**:[`tools/kconfig/kconfig_cli.py`](../tools/kconfig/kconfig_cli.py) 在写出 Kconfig 主体后,在文件**开头**追加几行 **Makefile/脚本 约定的元数据**(`ARCH`、`BOARD`、`BID`、`HVISOR_SRC`、`LD_SCRIPT`、`TEMPLATE` 的绝对路径)。这样一份文件同时满足: - - **kconfiglib**:主体仍是标准 `CONFIG_*`; - - **`build.rs`**:读 `ARCH`/`BOARD` 做 `__board.rs` 链接; - - **[`tools/kconfig/host_config.sh`](../tools/kconfig/host_config.sh)**(子命令 `cargo`):从同一份 `.config` 读取上述路径生成 `.cargo/config.toml`,**不再**根据环境变量重复拼 `platform/$ARCH/$BOARD/...`(避免两处维护)。 - -**一句话**:defconfig 只管 Kconfig;「哪块板」由**路径 + 调用参数**决定;根 `.config` 是 **元数据 + Kconfig 展开结果** 的合并文件,减少脚本重复逻辑。 - -## 常用命令 - -- **生成当前 ARCH/BOARD 的 `.config`**(合并元数据 + `CONFIG_*`): - - ```bash - make defconfig - # 或(需已创建 venv 并安装 kconfiglib) - ARCH= BOARD= tools/kconfig/.venv/bin/python tools/kconfig/kconfig_cli.py defconfig - ``` - -- **首次构建 / 切换板型**:根目录若无 `.config`,`make all` 会先 **`make defconfig`**(需 `tools/kconfig/.venv` + kconfiglib)。 - -- **交互菜单**(可选,需 Python venv + kconfiglib): - - ```bash - ./tools/kconfig/bootstrap_venv.sh - make menuconfig - ``` - - (Jenkins 等仅提供 `socks5://` 代理时,在线 `pip install` 会因缺少 PySocks 报错;`bootstrap_venv.sh` 从 `tools/kconfig/vendor/*.whl` 离线安装,可绕过该问题。若升级 `requirements.txt` 中的版本,请同步更新 vendor 目录:`python3 -m pip download -d tools/kconfig/vendor -r tools/kconfig/requirements.txt`。) - -- **`make menuconfig` 失败而 `make defconfig` 正常** - 常见原因之一是曾将入口脚本命名为 `menuconfig.py`,与 kconfiglib 安装在 `site-packages` 里的 **`menuconfig` 模块同名**,`sys.path` 会优先加载仓库脚本,导致 `import menuconfig` 失败;入口已统一为 **`kconfig_cli.py menuconfig`**(不要新增名为 `menuconfig.py` 的仓库脚本)。若仍报错且含 `_curses` / `curses`,说明解释器缺少 curses 支持,请按发行版安装对应包(例如 Debian/Ubuntu 使用 **`apt install python3-curses`**,包名随 Python 主版本可能为 `python3.12-curses` 等;Fedora 常为 **`dnf install python3-curses`**)。 - -- **把当前 `.config` 中的 `CONFIG_*` 行写回板级 defconfig**: - - ```bash - make savedefconfig - ``` - -## 文件布局 - -| 路径 | 说明 | -|------|------| -| [`kconfig/Kconfig`](../kconfig/Kconfig) | 含 **Target architecture**(4 选 1)及驱动等菜单 | -| [`tools/kconfig/kconfig_cli.py`](../tools/kconfig/kconfig_cli.py) | **`defconfig`**:生成根 `.config`;**`menuconfig`**:交互配置;**`vscode-cfgs`**:输出 rust-analyzer 用的 cfg JSON(由 `host_config.sh` 调用) | -| [`tools/kconfig/host_config.sh`](../tools/kconfig/host_config.sh) | `cargo`:写 `.cargo/config.toml`;`vscode`:刷新 `.config` 并写 `.vscode/settings.json`(`make gen_cargo_config` / `make vscode`) | -| [`tools/kconfig/bootstrap_venv.sh`](../tools/kconfig/bootstrap_venv.sh) | 创建 `.venv` 并离线安装 kconfiglib | -| [`tools/kconfig/save_defconfig.sh`](../tools/kconfig/save_defconfig.sh) | `make savedefconfig`:把 `CONFIG_*` 写回板级 defconfig | -| [`kconfig/cfg_map.toml`](../kconfig/cfg_map.toml) | `CONFIG_*` → Rust `cfg` 名映射表(**唯一真相**) | -| `platform///kconfig/defconfig` | 该板的默认 `CONFIG_*=y` 集合 | -| 根目录 `.config` | 本地生成,含 `ARCH`/`BOARD`/… 元数据 + Kconfig 输出(已被 `.gitignore`) | - -## 仅运行 `cargo build` 时 - -须先在仓库根生成 `.config`(例如安装 kconfiglib venv 后 **`make defconfig`**),否则 `build.rs` 会因缺少 `ARCH`/`BOARD` 报错。 - -## 修改或新增开关 - -1. 在 `kconfig/Kconfig` 中增加 `config` 项(若需 `menuconfig` 校验/依赖)。**目标架构**在 **`Target architecture`** 菜单中为四选一;板级 **`kconfig/defconfig`** 里请**直接写上**与目录 **`platform//`** 对应的一行 **`CONFIG_ARCH_*=y`**(与 Kconfig 一致即可,无需额外同步脚本)。 -2. 在 `kconfig/cfg_map.toml` 的 `[symbols]` 中增加 `CONFIG_FOO = "rust_cfg_name"`。 -3. 在 `build.rs` 中已通过映射表统一输出 `cargo:rustc-check-cfg`,一般**无需**再手写列表。 -4. 更新相关板的 `kconfig/defconfig`(或运行 `make menuconfig` 后 `make savedefconfig`)。 -5. 在 Rust 源码中使用 `#[cfg(rust_cfg_name)]`(完整列表见 [`rust-cfgs.md`](./rust-cfgs.md))。 - -## rust-analyzer - -运行 `make vscode` 会根据当前 `.config` 与 `cfg_map.toml` 生成 `.vscode/settings.json` 中的 `rust-analyzer.cargo.cfgs`,与真实编译开关对齐。 - -## 从旧版 `platform/.../cargo/features` 批量生成 defconfig - -若调整了 `cargo/features` 行并希望同步到 `kconfig/defconfig`,可在仓库根执行: - -```bash -python3 tools/kconfig/bootstrap_defconfigs.py -``` - -随后检查各板 `kconfig/defconfig` 并提交。 - -## Jenkins(`Jenkinsfile`) - -矩阵编译阶段会先执行 **`make defconfig ARCH=… BOARD=…`**,再 **`make dtb`**(非 x86)、最后 **`make all MODE=release`**。对 **`jenkins/ci.yaml`** 中配置了测试的 BID,**Run test cases** 阶段在调用 **`jenkins/ci_runner.py`** 前会再次保证 **`tools/kconfig/.venv`** 存在(缺失则运行 **`bootstrap_venv.sh`**)并执行 **`make defconfig`**,以便后续 **`make ci-run`**(依赖 `ensure_config` / `host_config.sh`)与 Kconfig 一致。`.config` 不入库,每个 matrix 工作副本在编译前都会从对应板的 `kconfig/defconfig` 生成;与旧版通过 `FEATURES` / `cargo --features` 传参的方式已脱钩。 diff --git a/docs/rust-cfgs.md b/docs/rust-cfgs.md deleted file mode 100644 index a3d733169..000000000 --- a/docs/rust-cfgs.md +++ /dev/null @@ -1,102 +0,0 @@ -# hvisor 中使用的 Rust `cfg` 一览 - -本文说明 **由 Kconfig / `.config` 注入的自定义 `cfg` 名**(经 [`kconfig/cfg_map.toml`](../kconfig/cfg_map.toml) 与 [`build.rs`](../build.rs) 转为 `cargo:rustc-cfg=...`),以及源码里实际出现的 **`#[cfg(...)]` / `cfg!(...)`** 用法。自定义名须与 **`cfg_map.toml` 右侧字符串** 完全一致(否则 `build.rs` 会告警或 rust-analyzer 不一致)。 - ---- - -## 1. 自定义 `cfg`(Kconfig → `rustc --cfg`) - -下表 **`cfg` 列**为在 Rust 里写的标识符;**`CONFIG_*` 列**为 `.config` / defconfig 中的键。 - -| `cfg` 名 | `CONFIG_*` | 含义(简要) | 在 `src/` 中的使用 | -|----------|--------------|----------------|-------------------| -| `irq_gicv2` | `CONFIG_IRQ_GICV2` | AArch64 GICv2 | [`device/irqchip/mod.rs`](../src/device/irqchip/mod.rs)、[`pci/mod.rs`](../src/pci/mod.rs)(`compile_error` 互斥) | -| `irq_gicv3` | `CONFIG_IRQ_GICV3` | AArch64 GICv3 | 同上 | -| `plic` | `CONFIG_PLIC` | RISC-V PLIC | [`device/irqchip`](../src/device/irqchip/mod.rs)、[`arch/riscv64/trap.rs`](../src/arch/riscv64/trap.rs)、[`ipi.rs`](../src/arch/riscv64/ipi.rs) | -| `dp1000_plic` | `CONFIG_DP1000_PLIC` | PLIC 变体 | [`plic/plic.rs`](../src/device/irqchip/plic/plic.rs) `cfg!` | -| `aia` | `CONFIG_AIA` | RISC-V AIA | irqchip、trap | -| `aclint` | `CONFIG_ACLINT` | RISC-V ACLINT | irqchip、ipi | -| `sstc` | `CONFIG_SSTC` | RISC-V Sstc | [`arch/riscv64/cpu.rs`](../src/arch/riscv64/cpu.rs) `cfg!` | -| `eic770x_soc` | `CONFIG_EIC770X_SOC` | EIC770x SoC | irqchip `mmio_init` | -| `eic7700_sysreg` | `CONFIG_EIC7700_SYSREG` | EIC7700 系统寄存器 | [`device/mod.rs`](../src/device/mod.rs)、irqchip | -| `sifive_ccache` | `CONFIG_SIFIVE_CCACHE` | SiFive ccache | 同上 | -| `loongson_7a2000` | `CONFIG_LOONGSON_7A2000` | LoongArch 7A2000 中断 | **当前 `src/` 中无 `#[cfg(loongson_7a2000)]`**;LoongArch 中断路径由 **`target_arch = "loongarch64"`** 统一包含 [`ls7a2000`](../src/device/irqchip/ls7a2000)。该符号仍可由 Kconfig/defconfig 打开并传给 `rustc`,供后续或外设条件使用。 | -| `loongson_3a5000` | `CONFIG_LOONGSON_3A5000` | 3A5000 CPU 画像 | **当前 `src/` 中无引用**(仅映射与 defconfig) | -| `loongson_3a6000` | `CONFIG_LOONGSON_3A6000` | 3A6000 CPU 画像 | **当前 `src/` 中无引用** | -| `pl011` | `CONFIG_PL011` | ARM PL011 UART | [`device/uart/mod.rs`](../src/device/uart/mod.rs) | -| `imx_uart` | `CONFIG_IMX_UART` | i.MX UART | 同上 | -| `xuartps` | `CONFIG_XUARTPS` | Xilinx XUARTPS | 同上 | -| `uart_16550` | `CONFIG_UART_16550` | 16550 通用 | 同上 | -| `uart16550a` | `CONFIG_UART16550A` | 16550A(x86) | 同上、[`uart16550a.rs`](../src/device/uart/uart16550a.rs) | -| `loongson_uart` | `CONFIG_LOONGSON_UART` | Loongson UART | uart/mod | -| `pci` | `CONFIG_PCI` | PCI/PCIe 总开关 | [`main.rs`](../src/main.rs)、[`platform/mod.rs`](../src/platform/mod.rs)、[`zone.rs`](../src/zone.rs) 等 | -| `ecam_pcie` | `CONFIG_PCIE_ECAM` | ECAM 配置空间 | [`pci/*`](../src/pci/)、[`pci/config_accessors`](../src/pci/config_accessors/mod.rs)、`compile_error` 互斥 | -| `dwc_pcie` | `CONFIG_PCIE_DWC` | DesignWare PCIe | 同上、[`pci_handler.rs`](../src/pci/pci_handler.rs)、[`zone.rs`](../src/zone.rs) | -| `loongarch64_pcie` | `CONFIG_PCIE_LOONGARCH64` | LoongArch PCIe | pci 各模块、`config_accessors` | -| `no_pcie_bar_realloc` | `CONFIG_NO_PCIE_BAR_REALLOC` | 不重分配 BAR | [`pci_struct.rs`](../src/pci/pci_struct.rs)、[`pci_config.rs`](../src/pci/pci_config.rs) | -| `iommu` | `CONFIG_IOMMU` | IOMMU 总开关 | [`main.rs`](../src/main.rs)、[`zone.rs`](../src/zone.rs)、[`pci_config.rs`](../src/pci/pci_config.rs)、iommu 子模块等 | -| `share_s2pt` | `CONFIG_SHARE_S2PT` | 与 IOMMU 共享 Stage-2 页表 | [`pci_config.rs`](../src/pci/pci_config.rs) | -| `arm_smmu` | `CONFIG_ARM_SMMU` | ARM SMMU | [`device/iommu`](../src/device/iommu/) | -| `intel_vtd` | `CONFIG_INTEL_VTD` | Intel VT-d | iommu、[`arch/x86_64`](../src/arch/x86_64/)、[`pci_handler.rs`](../src/pci/pci_handler.rs)、[`irqchip/pic`](../src/device/irqchip/pic/mod.rs) | -| `riscv_iommu` | `CONFIG_RISCV_IOMMU` | RISC-V IOMMU | iommu | -| `graphics` | `CONFIG_GRAPHICS` | 帧缓冲 / 图形 | [`logging.rs`](../src/logging.rs)、[`arch/x86_64/entry.rs`](../src/arch/x86_64/entry.rs)、[`pio.rs`](../src/arch/x86_64/pio.rs)、[`uart16550a.rs`](../src/device/uart/uart16550a.rs) | -| `split_screen` | `CONFIG_SPLIT_SCREEN` | 分屏布局 | [`arch/x86_64/graphics.rs`](../src/arch/x86_64/graphics.rs)、[`boot.rs`](../src/arch/x86_64/boot.rs) | -| `print_timestamp` | `CONFIG_PRINT_TIMESTAMP` | 日志打时间戳 | [`logging.rs`](../src/logging.rs) | -| `extioi_debug` | `CONFIG_EXTIOI_DEBUG` | LoongArch EXTIOI 调试 | [`device/irqchip/ls7a2000/chip.rs`](../src/device/irqchip/ls7a2000/chip.rs) | - -**说明**:[`build.rs`](../build.rs) 会为 `cfg_map.toml` 中**每一个**映射值打印 `cargo:rustc-check-cfg=cfg(...)`,因此上表中的名即使暂未出现在 `#[cfg]` 里,也属于**合法自定义 cfg**(例如便于 `rust-analyzer` 与将来代码一致)。 - -此外,**`platform/*/*/board.rs`** 中也可能出现 **`#[cfg(all(graphics))]`** 等(随板级代码演进)。 - ---- - -## 2. 编译器内置 / 标准 `cfg`(非 Kconfig) - -源码中大量使用的内置条件(**不由** `cfg_map.toml` 提供): - -| 条件 | 典型用途 | -|------|-----------| -| `target_arch = "aarch64"` / `"riscv64"` / `"loongarch64"` / `"x86_64"` | 架构分支,常与上述自定义 `cfg` 组合为 `all(...)` | -| `test` | 单元测试、[`main.rs`](../src/main.rs)、[`panic.rs`](../src/panic.rs)、[`pci/mod.rs`](../src/pci/mod.rs)、[`tests.rs`](../src/tests.rs) | -| `target_pointer_width = "32"` / `"64"` | [`arch/x86_64/vmcs.rs`](../src/arch/x86_64/vmcs.rs) | -| `not(...)` | 与上述条件搭配,表示反向分支 | - ---- - -## 3. `cfg!` 宏(编译期布尔) - -| 表达式 | 文件 | -|--------|------| -| `cfg!(sstc)` | [`arch/riscv64/cpu.rs`](../src/arch/riscv64/cpu.rs) | -| `cfg!(iommu)` | [`zone.rs`](../src/zone.rs) | -| `cfg!(dp1000_plic)` | [`device/irqchip/plic/plic.rs`](../src/device/irqchip/plic/plic.rs) | - ---- - -## 4. `compile_error!` 中使用的互斥组合(`src/pci/mod.rs`) - -下列 **非法组合** 在编译期直接报错(与 Kconfig 中 PCIe `choice` 等互补): - -- `all(target_arch = "aarch64", irq_gicv2, irq_gicv3)` — GICv2 与 GICv3 不能同时启用 -- `all(ecam_pcie, dwc_pcie)`、`all(ecam_pcie, loongarch64_pcie)`、`all(dwc_pcie, loongarch64_pcie)` — PCIe 配置空间访问后端互斥 - ---- - -## 5. 维护约定 - -1. **新增 Kconfig 开关并在 Rust 里使用**:在 [`kconfig/Kconfig`](../kconfig/Kconfig) 增加 `config`,在 [`kconfig/cfg_map.toml`](../kconfig/cfg_map.toml) 增加 `CONFIG_*` → **`cfg` 名**,源码写 **`#[cfg(该名)]`**;一般**无需**改 `build.rs`(已按映射表统一 `rustc-cfg` / `rustc-check-cfg`)。 -2. **重命名 `cfg`**:同时改 **`cfg_map.toml`**、全仓库 **`#[cfg` / `cfg!`** 与文档。 -3. **IDE**:`make vscode` 通过 [`tools/kconfig/kconfig_cli.py`](../tools/kconfig/kconfig_cli.py) `vscode-cfgs` 子命令,按 **`.config` + `cfg_map.toml`** 生成 `rust-analyzer.cargo.cfgs`,应与上表一致。 - ---- - -## 6. 生成方式(便于以后重新扫描) - -在仓库根执行(仅供参考,输出需人工对照 Kconfig): - -```bash -rg '#\[cfg' src --no-heading -o | sed 's/#\[cfg(//;s/)\]//' | sort -u -rg 'cfg!\(' src --no-heading -``` - -以 **`cfg_map.toml`** 与 **`build.rs`** 为「配置侧真相」,以 **`rg '#\[cfg'`** 结果为「源码侧引用」;二者差异见上文 **§1 表内说明列**。 diff --git a/kconfig/Kconfig b/kconfig/Kconfig index 497585d90..a092d32e8 100644 --- a/kconfig/Kconfig +++ b/kconfig/Kconfig @@ -6,10 +6,10 @@ choice default ARCH_AARCH64 config ARCH_AARCH64 - bool "aarch64 (ARM64)" + bool "aarch64" config ARCH_RISCV64 - bool "riscv64 (RV64)" + bool "riscv64" config ARCH_LOONGARCH64 bool "loongarch64" @@ -20,156 +20,196 @@ config ARCH_X86_64 endchoice endmenu -menu "General" -config IOMMU - bool "IOMMU support" - default n -config SHARE_S2PT - bool "Share stage-2 page tables with IOMMU" - select IOMMU - default n -config PRINT_TIMESTAMP - bool "Print timestamp in logs" - default n -endmenu - -menu "PCIe" -config PCI - bool "PCI/PCIe support" - default n +menu "Interrupt controller" + depends on ARCH_AARCH64 || ARCH_RISCV64 || ARCH_LOONGARCH64 choice - prompt "PCIe Architecture" - depends on PCI - default PCIE_ECAM - -config PCIE_ECAM - bool "ECAM PCIe" - -config PCIE_DWC - bool "DesignWare Core PCIe" - -config PCIE_LOONGARCH64 - bool "LoongArch PCIe" - depends on ARCH_LOONGARCH64 - -endchoice - -config NO_PCIE_BAR_REALLOC - bool "Do not reallocate PCIe BARs (board-specific)" - depends on PCI - default n -endmenu - -menu "IOMMU hardware" -config ARM_SMMU - bool "ARM SMMU" - depends on ARCH_AARCH64 - select IOMMU - default n -config INTEL_VTD - bool "Intel VT-d" - depends on ARCH_X86_64 - select IOMMU - default n -config RISCV_IOMMU - bool "RISC-V IOMMU" - depends on ARCH_RISCV64 - select IOMMU - default n -endmenu - -menu "ARM (AArch64) interrupt controller (GIC)" + prompt "GIC version" + optional + default IRQ_GICV3 depends on ARCH_AARCH64 config IRQ_GICV2 bool "GICv2" - default n + config IRQ_GICV3 bool "GICv3" - default n -endmenu -menu "RISC-V interrupt controller / timer" - depends on ARCH_RISCV64 +endchoice config PLIC bool "PLIC" + depends on ARCH_RISCV64 default n + config DP1000_PLIC bool "DP1000 PLIC variant" - depends on PLIC + depends on ARCH_RISCV64 && PLIC default n + config AIA bool "AIA (APLIC/IMSIC)" + depends on ARCH_RISCV64 default n + config ACLINT bool "ACLINT (MTI/MSI)" + depends on ARCH_RISCV64 default n + +config LOONGSON_7A2000 + bool "Loongson 7A2000" + depends on ARCH_LOONGARCH64 + default n + +endmenu + +menu "RISC-V options" + depends on ARCH_RISCV64 + config SSTC - bool "Sstc extension" + bool "Sstc" default n -config EIC770X_SOC - bool "EIC770x SoC integration" + +config Hypervisor_v0_6 + bool "Hypervisor v0.6" default n + config EIC7700_SYSREG bool "EIC7700 system registers" - depends on EIC770X_SOC + depends on Hypervisor_v0_6 default n + config SIFIVE_CCACHE - bool "SiFive ccache (EIC770x)" - depends on EIC770X_SOC + bool "SiFive ccache" + depends on Hypervisor_v0_6 default n + endmenu -menu "LoongArch" +menu "LoongArch platform" depends on ARCH_LOONGARCH64 -config LOONGSON_7A2000 - bool "Loongson 7A2000 interrupt controller" - default n config LOONGSON_3A5000 bool "Loongson 3A5000 CPU profile" default n + config LOONGSON_3A6000 bool "Loongson 3A6000 CPU profile" default n endmenu menu "UART" + config PL011 - bool "ARM PL011 UART" + bool "PL011" + depends on ARCH_AARCH64 default n + config IMX_UART - bool "NXP i.MX UART" + bool "iMX UART" + depends on ARCH_AARCH64 default n + config XUARTPS - bool "Xilinx XUARTPS" + bool "XUARTPS" + depends on ARCH_AARCH64 default n + config UART_16550 - bool "16550-compatible (generic)" + bool "UART_16550" + depends on ARCH_AARCH64 default n + config UART16550A - bool "16550A (x86)" + bool "UART16550A" + depends on ARCH_X86_64 default n + config LOONGSON_UART bool "Loongson UART" + depends on ARCH_LOONGARCH64 default n endmenu -menu "x86 display" +menu "PCIe" + +config PCI + bool "PCI/PCIe support" + default n + +choice + prompt "PCIe Architecture" + depends on PCI + default PCIE_ECAM + +config PCIE_ECAM + bool "ECAM PCIe" + +config PCIE_DWC + bool "DesignWare Core PCIe" + +config PCIE_LOONGARCH64 + bool "LoongArch PCIe" + depends on ARCH_LOONGARCH64 + +endchoice + +config NO_PCIE_BAR_REALLOC + bool "Do not reallocate PCIe BARs (board-specific)" + depends on PCI + default n +endmenu + +menu "IOMMU" + +config IOMMU + bool + default n + +config ARM_SMMU + bool "ARM SMMU" + depends on ARCH_AARCH64 + select IOMMU + default n + +config INTEL_VTD + bool "Intel VT-d" + depends on ARCH_X86_64 + select IOMMU + default n + +config RISCV_IOMMU + bool "RISC-V IOMMU" + depends on ARCH_RISCV64 + select IOMMU + default n + +config SHARE_S2PT + bool "Share stage-2 page tables with IOMMU (RISC-V IOMMU only)" + depends on RISCV_IOMMU + default n +endmenu + +menu "x86-specific options" depends on ARCH_X86_64 config GRAPHICS bool "Framebuffer / graphics" default n + config SPLIT_SCREEN bool "Split screen layout" depends on GRAPHICS default n + +config PRINT_TIMESTAMP + bool "Print timestamp in logs" + default n endmenu menu "Debug" + config EXTIOI_DEBUG bool "EXTIOI debug (LoongArch LS7A2000)" depends on ARCH_LOONGARCH64 diff --git a/kconfig/cfg_map.toml b/kconfig/cfg_map.toml index bfb6ca8c4..f0226b67d 100644 --- a/kconfig/cfg_map.toml +++ b/kconfig/cfg_map.toml @@ -24,7 +24,7 @@ CONFIG_DP1000_PLIC = "dp1000_plic" CONFIG_AIA = "aia" CONFIG_ACLINT = "aclint" CONFIG_SSTC = "sstc" -CONFIG_EIC770X_SOC = "eic770x_soc" +CONFIG_Hypervisor_v0_6 = "hypervisor_v0_6" CONFIG_EIC7700_SYSREG = "eic7700_sysreg" CONFIG_SIFIVE_CCACHE = "sifive_ccache" CONFIG_LOONGSON_7A2000 = "loongson_7a2000" diff --git a/kernel_build/bin/python b/kernel_build/bin/python deleted file mode 120000 index b8a0adbbb..000000000 --- a/kernel_build/bin/python +++ /dev/null @@ -1 +0,0 @@ -python3 \ No newline at end of file diff --git a/kernel_build/bin/python3 b/kernel_build/bin/python3 deleted file mode 120000 index ae65fdaa1..000000000 --- a/kernel_build/bin/python3 +++ /dev/null @@ -1 +0,0 @@ -/usr/bin/python3 \ No newline at end of file diff --git a/kernel_build/bin/python3.12 b/kernel_build/bin/python3.12 deleted file mode 120000 index b8a0adbbb..000000000 --- a/kernel_build/bin/python3.12 +++ /dev/null @@ -1 +0,0 @@ -python3 \ No newline at end of file diff --git a/kernel_build/lib64 b/kernel_build/lib64 deleted file mode 120000 index 7951405f8..000000000 --- a/kernel_build/lib64 +++ /dev/null @@ -1 +0,0 @@ -lib \ No newline at end of file diff --git a/kernel_build/pyvenv.cfg b/kernel_build/pyvenv.cfg deleted file mode 100644 index 84f912f9b..000000000 --- a/kernel_build/pyvenv.cfg +++ /dev/null @@ -1,5 +0,0 @@ -home = /usr/bin -include-system-site-packages = false -version = 3.12.3 -executable = /usr/bin/python3.12 -command = /usr/bin/python3 -m venv /home/dallas/DEMO/syswonder/hvisor/kernel_build diff --git a/platform/aarch64/imx8mp/cargo/features b/platform/aarch64/imx8mp/cargo/features deleted file mode 100644 index 7acf44c69..000000000 --- a/platform/aarch64/imx8mp/cargo/features +++ /dev/null @@ -1,2 +0,0 @@ -gicv3 -imx_uart diff --git a/platform/aarch64/ok6254-c/cargo/features b/platform/aarch64/ok6254-c/cargo/features deleted file mode 100644 index afa594897..000000000 --- a/platform/aarch64/ok6254-c/cargo/features +++ /dev/null @@ -1,2 +0,0 @@ -gicv3 -uart_16550 diff --git a/platform/aarch64/phytium-pi/cargo/features b/platform/aarch64/phytium-pi/cargo/features deleted file mode 100644 index 416c6913e..000000000 --- a/platform/aarch64/phytium-pi/cargo/features +++ /dev/null @@ -1,2 +0,0 @@ -gicv3 -pl011 \ No newline at end of file diff --git a/platform/aarch64/qemu-gicv2/cargo/features b/platform/aarch64/qemu-gicv2/cargo/features deleted file mode 100644 index e0ed82fd3..000000000 --- a/platform/aarch64/qemu-gicv2/cargo/features +++ /dev/null @@ -1,4 +0,0 @@ -gicv2 -pl011 -arm_smmu -pci diff --git a/platform/aarch64/qemu-gicv3/cargo/features b/platform/aarch64/qemu-gicv3/cargo/features deleted file mode 100644 index d9b303955..000000000 --- a/platform/aarch64/qemu-gicv3/cargo/features +++ /dev/null @@ -1,5 +0,0 @@ -gicv3 -pl011 -arm_smmu -pci -ecam_pcie \ No newline at end of file diff --git a/platform/aarch64/rk3568/cargo/features b/platform/aarch64/rk3568/cargo/features deleted file mode 100644 index 0b11e0fab..000000000 --- a/platform/aarch64/rk3568/cargo/features +++ /dev/null @@ -1,5 +0,0 @@ -gicv3 -uart_16550 -pci -dwc_pcie -no_pcie_bar_realloc \ No newline at end of file diff --git a/platform/aarch64/rk3588/cargo/features b/platform/aarch64/rk3588/cargo/features deleted file mode 100644 index afa594897..000000000 --- a/platform/aarch64/rk3588/cargo/features +++ /dev/null @@ -1,2 +0,0 @@ -gicv3 -uart_16550 diff --git a/platform/aarch64/sysoul_x3300/cargo/features b/platform/aarch64/sysoul_x3300/cargo/features deleted file mode 100644 index afa594897..000000000 --- a/platform/aarch64/sysoul_x3300/cargo/features +++ /dev/null @@ -1,2 +0,0 @@ -gicv3 -uart_16550 diff --git a/platform/aarch64/zcu102/cargo/features b/platform/aarch64/zcu102/cargo/features deleted file mode 100644 index 4189d1494..000000000 --- a/platform/aarch64/zcu102/cargo/features +++ /dev/null @@ -1,2 +0,0 @@ -gicv2 -xuartps diff --git a/platform/loongarch64/ls3a5000/cargo/features b/platform/loongarch64/ls3a5000/cargo/features deleted file mode 100644 index 442505f46..000000000 --- a/platform/loongarch64/ls3a5000/cargo/features +++ /dev/null @@ -1,7 +0,0 @@ -loongson_3a5000 -loongson_7a2000 -loongson_uart - -pci -loongarch64_pcie -no_pcie_bar_realloc \ No newline at end of file diff --git a/platform/loongarch64/ls3a6000/cargo/features b/platform/loongarch64/ls3a6000/cargo/features deleted file mode 100644 index 86cd7a5c3..000000000 --- a/platform/loongarch64/ls3a6000/cargo/features +++ /dev/null @@ -1,7 +0,0 @@ -loongson_3a6000 -loongson_7a2000 -loongson_uart - -pci -loongarch64_pcie -no_pcie_bar_realloc \ No newline at end of file diff --git a/platform/riscv64/hifive-premier-p550/cargo/features b/platform/riscv64/hifive-premier-p550/cargo/features deleted file mode 100644 index d72ea9ac9..000000000 --- a/platform/riscv64/hifive-premier-p550/cargo/features +++ /dev/null @@ -1,4 +0,0 @@ -eic770x_soc -plic -eic7700_sysreg -sifive_ccache diff --git a/platform/riscv64/hifive-premier-p550/kconfig/defconfig b/platform/riscv64/hifive-premier-p550/kconfig/defconfig index 454d53ce7..32165b1ad 100644 --- a/platform/riscv64/hifive-premier-p550/kconfig/defconfig +++ b/platform/riscv64/hifive-premier-p550/kconfig/defconfig @@ -1,5 +1,5 @@ CONFIG_ARCH_RISCV64=y CONFIG_EIC7700_SYSREG=y -CONFIG_EIC770X_SOC=y +CONFIG_Hypervisor_v0_6=y CONFIG_PLIC=y CONFIG_SIFIVE_CCACHE=y diff --git a/platform/riscv64/megrez/cargo/features b/platform/riscv64/megrez/cargo/features deleted file mode 100644 index d1314d861..000000000 --- a/platform/riscv64/megrez/cargo/features +++ /dev/null @@ -1,2 +0,0 @@ -eic770x_soc -plic diff --git a/platform/riscv64/megrez/kconfig/defconfig b/platform/riscv64/megrez/kconfig/defconfig index 5893693bd..2ba427204 100644 --- a/platform/riscv64/megrez/kconfig/defconfig +++ b/platform/riscv64/megrez/kconfig/defconfig @@ -1,3 +1,3 @@ CONFIG_ARCH_RISCV64=y -CONFIG_EIC770X_SOC=y +CONFIG_Hypervisor_v0_6=y CONFIG_PLIC=y diff --git a/platform/riscv64/qemu-aia/cargo/features b/platform/riscv64/qemu-aia/cargo/features deleted file mode 100644 index 2600021b4..000000000 --- a/platform/riscv64/qemu-aia/cargo/features +++ /dev/null @@ -1,6 +0,0 @@ -aia -sstc -pci -ecam_pcie -riscv_iommu -share_s2pt diff --git a/platform/riscv64/qemu-plic/README.md b/platform/riscv64/qemu-plic/README.md index b6884c55d..898babb38 100644 --- a/platform/riscv64/qemu-plic/README.md +++ b/platform/riscv64/qemu-plic/README.md @@ -73,5 +73,5 @@ Both the root zone and zone1 are assigned one virtio-pci-blk device(using wired- Please confirm qemu-system-riscv64 version >= 10.0.0, add iommu-sys=on for machine param and iommu_platform=on for devices linked behind iommu. (refer to platform.mk) -Add `iommu` and `share_s2pt` features for hvisor. +Enable IOMMU in **`kconfig/defconfig`** (`CONFIG_IOMMU`, `CONFIG_RISCV_IOMMU`, `CONFIG_SHARE_S2PT`, etc.) for hvisor. diff --git a/platform/riscv64/qemu-plic/cargo/features b/platform/riscv64/qemu-plic/cargo/features deleted file mode 100644 index 6be000e7e..000000000 --- a/platform/riscv64/qemu-plic/cargo/features +++ /dev/null @@ -1,5 +0,0 @@ -aclint -plic -sstc -pci -ecam_pcie diff --git a/platform/riscv64/ur-dp1000/cargo/features b/platform/riscv64/ur-dp1000/cargo/features deleted file mode 100644 index cac3e049a..000000000 --- a/platform/riscv64/ur-dp1000/cargo/features +++ /dev/null @@ -1,2 +0,0 @@ -plic -dp1000_plic diff --git a/platform/x86_64/ecx-2300f-peg/cargo/features b/platform/x86_64/ecx-2300f-peg/cargo/features deleted file mode 100644 index e4ad1c134..000000000 --- a/platform/x86_64/ecx-2300f-peg/cargo/features +++ /dev/null @@ -1,2 +0,0 @@ -pci -uart16550a diff --git a/platform/x86_64/nuc14mnk/cargo/features b/platform/x86_64/nuc14mnk/cargo/features deleted file mode 100644 index 69fb95c99..000000000 --- a/platform/x86_64/nuc14mnk/cargo/features +++ /dev/null @@ -1,6 +0,0 @@ -pci -ecam_pcie -no_pcie_bar_realloc -uart16550a -graphics -split_screen \ No newline at end of file diff --git a/platform/x86_64/qemu/cargo/features b/platform/x86_64/qemu/cargo/features deleted file mode 100644 index fc6ef1e68..000000000 --- a/platform/x86_64/qemu/cargo/features +++ /dev/null @@ -1,5 +0,0 @@ -pci -ecam_pcie -no_pcie_bar_realloc -uart16550a -intel_vtd \ No newline at end of file diff --git a/src/arch/riscv64/cpu.rs b/src/arch/riscv64/cpu.rs index c5bdd69ce..7e633a74e 100644 --- a/src/arch/riscv64/cpu.rs +++ b/src/arch/riscv64/cpu.rs @@ -91,7 +91,7 @@ impl ArchCpu { set_csr!(CSR_VSTIMECMP, usize::MAX); } else { // In megrez board, this instruction is not supported. (illegal instruction) - #[cfg(not(eic770x_soc))] + #[cfg(not(hypervisor_v0_6))] set_csr!(CSR_HENVCFG, 0); } set_csr!(CSR_HCOUNTEREN, 1 << 1); // HCOUNTEREN_TM diff --git a/src/device/irqchip/mod.rs b/src/device/irqchip/mod.rs index 627babbdb..c32b0e3cb 100644 --- a/src/device/irqchip/mod.rs +++ b/src/device/irqchip/mod.rs @@ -102,7 +102,7 @@ impl Zone { { self.vaplic_mmio_init(hv_config); } - #[cfg(all(eic770x_soc, target_arch = "riscv64"))] + #[cfg(all(hypervisor_v0_6, target_arch = "riscv64"))] { #[cfg(sifive_ccache)] self.virtual_sifive_ccache_mmio_init(); diff --git a/src/pci/mod.rs b/src/pci/mod.rs index 3ce07a625..7217cce55 100644 --- a/src/pci/mod.rs +++ b/src/pci/mod.rs @@ -15,17 +15,6 @@ // #![allow(dead_code)] - -#[cfg(all(target_arch = "aarch64", irq_gicv2, irq_gicv3))] -compile_error!("invalid: irq_gicv2 and irq_gicv3 are mutually exclusive"); - -#[cfg(all(ecam_pcie, dwc_pcie))] -compile_error!("invalid: ecam_pcie and dwc_pcie are mutually exclusive"); -#[cfg(all(ecam_pcie, loongarch64_pcie))] -compile_error!("invalid: ecam_pcie and loongarch64_pcie are mutually exclusive"); -#[cfg(all(dwc_pcie, loongarch64_pcie))] -compile_error!("invalid: dwc_pcie and loongarch64_pcie are mutually exclusive"); - pub mod config_accessors; pub mod mem_alloc; pub mod pci_access; diff --git a/tools/kconfig/bootstrap_defconfigs.py b/tools/kconfig/bootstrap_defconfigs.py deleted file mode 100644 index 9542b12dd..000000000 --- a/tools/kconfig/bootstrap_defconfigs.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python3 -"""Generate platform///kconfig/defconfig from legacy cargo/features lines.""" -from __future__ import annotations - -import pathlib - -ROOT = pathlib.Path(__file__).resolve().parents[2] - -# platform//... -> Kconfig arch choice line -ARCH_LINE: dict[str, str] = { - "aarch64": "CONFIG_ARCH_AARCH64=y", - "riscv64": "CONFIG_ARCH_RISCV64=y", - "loongarch64": "CONFIG_ARCH_LOONGARCH64=y", - "x86_64": "CONFIG_ARCH_X86_64=y", -} - "gicv2": ["CONFIG_IRQ_GICV2=y"], - "gicv3": ["CONFIG_IRQ_GICV3=y"], - "pl011": ["CONFIG_PL011=y"], - "imx_uart": ["CONFIG_IMX_UART=y"], - "xuartps": ["CONFIG_XUARTPS=y"], - "uart_16550": ["CONFIG_UART_16550=y"], - "uart16550a": ["CONFIG_UART16550A=y"], - "loongson_uart": ["CONFIG_LOONGSON_UART=y"], - "pci": ["CONFIG_PCI=y"], - "ecam_pcie": ["CONFIG_PCIE_ECAM=y"], - "dwc_pcie": ["CONFIG_PCIE_DWC=y"], - "loongarch64_pcie": ["CONFIG_PCIE_LOONGARCH64=y"], - "no_pcie_bar_realloc": ["CONFIG_NO_PCIE_BAR_REALLOC=y"], - "arm_smmu": ["CONFIG_ARM_SMMU=y"], - "intel_vtd": ["CONFIG_INTEL_VTD=y"], - "riscv_iommu": ["CONFIG_RISCV_IOMMU=y"], - "share_s2pt": ["CONFIG_SHARE_S2PT=y"], - "aclint": ["CONFIG_ACLINT=y"], - "plic": ["CONFIG_PLIC=y"], - "dp1000_plic": ["CONFIG_DP1000_PLIC=y"], - "aia": ["CONFIG_AIA=y"], - "sstc": ["CONFIG_SSTC=y"], - "eic770x_soc": ["CONFIG_EIC770X_SOC=y"], - "eic7700_sysreg": ["CONFIG_EIC7700_SYSREG=y"], - "sifive_ccache": ["CONFIG_SIFIVE_CCACHE=y"], - "loongson_7a2000": ["CONFIG_LOONGSON_7A2000=y"], - "loongson_3a5000": ["CONFIG_LOONGSON_3A5000=y"], - "loongson_3a6000": ["CONFIG_LOONGSON_3A6000=y"], - "graphics": ["CONFIG_GRAPHICS=y"], - "split_screen": ["CONFIG_SPLIT_SCREEN=y"], -} - - -def features_to_configs(lines: list[str]) -> list[str]: - out: list[str] = [] - seen: set[str] = set() - for raw in lines: - w = raw.strip() - if not w: - continue - if w not in FEATURE_LINES: - raise SystemExit(f"unknown feature token: {w!r}") - for ln in FEATURE_LINES[w]: - if ln not in seen: - seen.add(ln) - out.append(ln) - return sorted(out) - - -def main() -> None: - for feat_path in sorted(ROOT.glob("platform/*/")): - arch = feat_path.name - for board_path in sorted(feat_path.glob("*/")): - board = board_path.name - legacy = board_path / "cargo" / "features" - if not legacy.is_file(): - continue - lines = legacy.read_text().splitlines() - configs = features_to_configs(lines) - arch_ln = ARCH_LINE.get(arch) - if arch_ln is None: - raise SystemExit(f"unknown arch directory {arch!r}") - configs = sorted(set([*configs, arch_ln])) - out_dir = board_path / "kconfig" - out_dir.mkdir(parents=True, exist_ok=True) - (out_dir / "defconfig").write_text("\n".join(configs) + "\n") - print(f"wrote {out_dir / 'defconfig'} ({len(configs)} symbols)") - - -if __name__ == "__main__": - main() diff --git a/tools/kconfig/host_config.sh b/tools/kconfig/host_config.sh index 976334bf5..fa40c950d 100755 --- a/tools/kconfig/host_config.sh +++ b/tools/kconfig/host_config.sh @@ -21,11 +21,16 @@ cmd_gen_cargo() { exit 1 fi local ARCH BOARD HVISOR_SRC LD_SCRIPT CONFIG_TOML_TEMPLATE - ARCH=$(grep '^ARCH=' .config | head -1 | cut -d= -f2-) - BOARD=$(grep '^BOARD=' .config | head -1 | cut -d= -f2-) - HVISOR_SRC=$(grep '^HVISOR_SRC=' .config | head -1 | cut -d= -f2-) - LD_SCRIPT=$(grep '^LD_SCRIPT=' .config | head -1 | cut -d= -f2-) - CONFIG_TOML_TEMPLATE=$(grep '^TEMPLATE=' .config | head -1 | cut -d= -f2-) + ARCH=$(grep '^# ARCH=' .config 2>/dev/null | head -1 | sed 's/^# ARCH=//') + [ -n "$ARCH" ] || ARCH=$(grep '^ARCH=' .config | head -1 | cut -d= -f2-) + BOARD=$(grep '^# BOARD=' .config 2>/dev/null | head -1 | sed 's/^# BOARD=//') + [ -n "$BOARD" ] || BOARD=$(grep '^BOARD=' .config | head -1 | cut -d= -f2-) + HVISOR_SRC=$(grep '^# HVISOR_SRC=' .config 2>/dev/null | head -1 | sed 's/^# HVISOR_SRC=//') + [ -n "$HVISOR_SRC" ] || HVISOR_SRC=$(grep '^HVISOR_SRC=' .config | head -1 | cut -d= -f2-) + LD_SCRIPT=$(grep '^# LD_SCRIPT=' .config 2>/dev/null | head -1 | sed 's/^# LD_SCRIPT=//') + [ -n "$LD_SCRIPT" ] || LD_SCRIPT=$(grep '^LD_SCRIPT=' .config | head -1 | cut -d= -f2-) + CONFIG_TOML_TEMPLATE=$(grep '^# TEMPLATE=' .config 2>/dev/null | head -1 | sed 's/^# TEMPLATE=//') + [ -n "$CONFIG_TOML_TEMPLATE" ] || CONFIG_TOML_TEMPLATE=$(grep '^TEMPLATE=' .config | head -1 | cut -d= -f2-) if [ -z "$ARCH" ] || [ -z "$BOARD" ] || [ -z "$HVISOR_SRC" ] || [ -z "$LD_SCRIPT" ] || [ -z "$CONFIG_TOML_TEMPLATE" ]; then echo "error: .config missing ARCH/BOARD/HVISOR_SRC/LD_SCRIPT/TEMPLATE lines (run: make defconfig)" >&2 exit 1 diff --git a/tools/kconfig/kconfig_cli.py b/tools/kconfig/kconfig_cli.py index ca135fa4e..6631c5dc4 100644 --- a/tools/kconfig/kconfig_cli.py +++ b/tools/kconfig/kconfig_cli.py @@ -27,7 +27,7 @@ def arch_from_kconf(kconf) -> str | None: def write_root_dot_config(root: Path, arch: str, board: str, kconf) -> None: - """Write repo-root `.config`: metadata lines then kconfiglib body.""" + """Write repo-root `.config`: comment metadata (Kconfig-safe) then kconfiglib body.""" cfg = root / ".config" derived = arch_from_kconf(kconf) if derived is None: @@ -49,12 +49,12 @@ def write_root_dot_config(root: Path, arch: str, board: str, kconf) -> None: kconf.write_config(str(cfg)) body = cfg.read_text() meta = ( - f"ARCH={arch_out}\n" - f"BOARD={board}\n" - f"BID={bid}\n" - f"HVISOR_SRC={hvisor_src}\n" - f"LD_SCRIPT={ld}\n" - f"TEMPLATE={tmpl}\n" + f"# ARCH={arch_out}\n" + f"# BOARD={board}\n" + f"# BID={bid}\n" + f"# HVISOR_SRC={hvisor_src}\n" + f"# LD_SCRIPT={ld}\n" + f"# TEMPLATE={tmpl}\n" "\n" ) cfg.write_text(meta + body) From 287accfbede734698937cdebaa5519744b006a62 Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 26 May 2026 15:05:47 +0800 Subject: [PATCH 03/53] fix linter env error --- .github/workflows/ci.yml | 2 +- Makefile | 24 ++++++++++-------------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce91f8d2a..33187b152 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: - name: Install Dependencies run: | sudo apt-get update - sudo apt-get install -y qemu-system-aarch64 qemu-system-riscv64 gdb-multiarch llvm-dev libclang-dev wget + sudo apt-get install -y qemu-system-aarch64 qemu-system-riscv64 gdb-multiarch llvm-dev libclang-dev wget python3-venv cargo install --version 0.3.0 --locked cargo-binutils - name: Format Check run: make fmt-test diff --git a/Makefile b/Makefile index 5577ac66d..23c45d475 100644 --- a/Makefile +++ b/Makefile @@ -78,28 +78,24 @@ COLOR_RESET := $(shell tput sgr0) kconfig_python := tools/kconfig/.venv/bin/python # Targets -.PHONY: all elf disa run gdb monitor clean tools rootfs vscode ci-run defconfig menuconfig savedefconfig ensure_config +.PHONY: all elf disa run gdb monitor clean tools rootfs vscode ci-run defconfig menuconfig savedefconfig ensure_config kconfig_venv +kconfig_venv: + @if [ ! -x $(kconfig_python) ]; then \ + echo "$(COLOR_YELLOW)Creating tools/kconfig/.venv (kconfiglib)...$(COLOR_RESET)"; \ + ./tools/kconfig/bootstrap_venv.sh; \ + fi + ensure_config: @if [ ! -f .config ]; then \ echo "$(COLOR_YELLOW)No .config; running defconfig for $(ARCH)/$(BOARD)$(COLOR_RESET)"; \ $(MAKE) --no-print-directory defconfig; \ fi -defconfig: - @if [ ! -x $(kconfig_python) ]; then \ - echo "$(COLOR_RED)Missing tools/kconfig/.venv (kconfiglib).$(COLOR_RESET)"; \ - echo " ./tools/kconfig/bootstrap_venv.sh"; \ - exit 1; \ - fi +defconfig: kconfig_venv @$(kconfig_python) tools/kconfig/kconfig_cli.py defconfig -menuconfig: - @if [ -x $(kconfig_python) ]; then \ - $(kconfig_python) tools/kconfig/kconfig_cli.py menuconfig; \ - else \ - echo "Install kconfiglib: ./tools/kconfig/bootstrap_venv.sh"; \ - exit 1; \ - fi +menuconfig: kconfig_venv + @$(kconfig_python) tools/kconfig/kconfig_cli.py menuconfig savedefconfig: @./tools/kconfig/save_defconfig.sh "$(ARCH)" "$(BOARD)" From 8151677a1ba391ed7bc08e7da8a2f9dc658a0e1e Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 26 May 2026 15:27:10 +0800 Subject: [PATCH 04/53] fmt arch parse in ci --- Jenkinsfile | 61 +++++++++++++++++++------------------------- jenkins/ci.yaml | 8 ------ jenkins/ci_config.py | 7 +++++ jenkins/ci_runner.py | 13 +++++----- 4 files changed, 40 insertions(+), 49 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 3b88a50bb..cbffa0bfd 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,3 +1,12 @@ +/** ARCH/BOARD come from matrix BID (arch/board); ci.yaml must not duplicate them. */ +def parseBid(String bid) { + def parts = (bid ?: '').split('/', 2) + if (parts.size() != 2 || !parts[0] || !parts[1]) { + error("invalid BID: ${bid}") + } + return [arch: parts[0], board: parts[1]] +} + def parseCiBuildArgs(cfg) { def buildArgs = [:] if (!cfg?.build_args) { @@ -149,13 +158,10 @@ pipeline { steps { dir(matrixCellDir()) { script { - def parts = (env.BID ?: '').split('/', 2) - if (parts.size() != 2) { - error("invalid BID: ${env.BID}") - } - def arch = parts[0] - def board = parts[1] - echo "Compile hvisor [BID=${env.BID}, ARCH=${arch}, BOARD=${board}] (Kconfig: kconfig_cli.py defconfig -> root .config; no Cargo --features)" + def bid = parseBid(env.BID) + def arch = bid.arch + def board = bid.board + echo "Compile hvisor [BID=${env.BID}, ARCH=${arch}, BOARD=${board}]" sh kconfigSetupShell(arch, board) if (arch != 'x86_64') { sh """ @@ -184,10 +190,11 @@ pipeline { def ci = loadCiYaml() def bidCfg = getBidConfig(ci, env.BID) def buildArgs = parseCiBuildArgs(bidCfg) - def tarch = normalizeToolArch(buildArgs.TARCH ?: buildArgs.ARCH) + def bidTool = parseBid(env.BID) + def tarch = normalizeToolArch(buildArgs.TARCH ?: bidTool.arch) def kdir = buildArgs.KDIR - if (!tarch || !kdir) { - error("jenkins/ci.yaml BID=${env.BID}: build_args must include ARCH/TARCH and KDIR") + if (!kdir) { + error("jenkins/ci.yaml BID=${env.BID}: build_args must include KDIR") } echo "Build hvisor-tool [BID=${env.BID}, TARCH=${tarch}, KDIR=${kdir}]" @@ -224,19 +231,14 @@ pipeline { def ci = loadCiYaml() def bidCfg = getBidConfig(ci, env.BID) def buildArgs = parseCiBuildArgs(bidCfg) - def arch = (buildArgs.ARCH ?: '').toString() - def board = (buildArgs.BOARD ?: '').toString() + def bidPrepare = parseBid(env.BID) + def arch = bidPrepare.arch + def board = bidPrepare.board def kdir = (buildArgs.KDIR ?: '').toString() def testsCfg = bidCfg.tests ?: [:] def mode = (testsCfg.mode ?: '').toString().trim() - if (!arch || !board || !kdir || !mode) { - error("jenkins/ci.yaml BID=${env.BID}: tests.mode and build_args ARCH/BOARD/KDIR are required") - } - def bidPartsPrepare = (env.BID ?: '').split('/', 2) - if (bidPartsPrepare.size() == 2) { - if (arch != bidPartsPrepare[0] || board != bidPartsPrepare[1]) { - error("jenkins/ci.yaml BID=${env.BID}: build_args ARCH/BOARD (${arch}/${board}) must match matrix BID") - } + if (!kdir || !mode) { + error("jenkins/ci.yaml BID=${env.BID}: tests.mode and build_args KDIR are required") } if (mode == 'qemu') { @@ -275,21 +277,10 @@ pipeline { steps { dir(matrixCellDir()) { script { - def bidParts = (env.BID ?: '').split('/', 2) - if (bidParts.size() != 2) { - error("invalid BID for tests: ${env.BID}") - } - def tArch = bidParts[0] - def tBoard = bidParts[1] - def ciRun = loadCiYaml() - def bidCfgRun = getBidConfig(ciRun, env.BID) - def buildArgsRun = parseCiBuildArgs(bidCfgRun) - def yamlArchRun = (buildArgsRun.ARCH ?: '').toString() - def yamlBoardRun = (buildArgsRun.BOARD ?: '').toString() - if (yamlArchRun != tArch || yamlBoardRun != tBoard) { - error("jenkins/ci.yaml BID=${env.BID}: build_args ARCH/BOARD (${yamlArchRun}/${yamlBoardRun}) must match matrix BID (${tArch}/${tBoard})") - } - echo "Run tests via ci_runner [BID=${env.BID}] (Kconfig: same as Compile — kconfig_cli via make defconfig)" + def bidRun = parseBid(env.BID) + def tArch = bidRun.arch + def tBoard = bidRun.board + echo "Run tests via ci_runner [BID=${env.BID}]" sh """ export TERM=\${TERM:-xterm} export PATH=${env.CARGO_HOME}/bin:${env.TOOLCHAIN_PATHS}:\$PATH diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index de3b79ba3..b23c396e0 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -1,8 +1,6 @@ bids: - bid: riscv64/qemu-plic build_args: - - ARCH=riscv64 - - BOARD=qemu-plic - KDIR=/home/light/DEMO/linux/linux-6.10 tests: mode: qemu @@ -12,8 +10,6 @@ bids: - bid: aarch64/qemu-gicv3 build_args: - - ARCH=aarch64 - - BOARD=qemu-gicv3 - KDIR=/home/light/DEMO/linux/linux-5.4 tests: mode: qemu @@ -24,8 +20,6 @@ bids: # - bid: aarch64/rk3568 # build_args: - # - ARCH=aarch64 - # - BOARD=rk3568 # - KDIR=/home/light/DEMO/sdk/rk356x-up4-2c/kernel # tests: # mode: board @@ -39,8 +33,6 @@ bids: - bid: x86_64/qemu build_args: - - ARCH=x86_64 - - BOARD=qemu - KDIR=/home/light/DEMO/linux/linux-5.19 tests: mode: qemu diff --git a/jenkins/ci_config.py b/jenkins/ci_config.py index b74521249..f47c5f3c9 100755 --- a/jenkins/ci_config.py +++ b/jenkins/ci_config.py @@ -15,6 +15,13 @@ CI_YAML = ROOT / "jenkins" / "ci.yaml" +def parse_bid(bid: str) -> tuple[str, str]: + parts = bid.strip().split("/", 1) + if len(parts) != 2 or not parts[0].strip() or not parts[1].strip(): + raise ValueError(f"invalid BID: {bid!r}") + return parts[0].strip(), parts[1].strip() + + def normalize_build_args(raw: list[Any] | None) -> dict[str, str]: result: dict[str, str] = {} for item in raw or []: diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index d7d645849..835ddb869 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -11,7 +11,7 @@ from pathlib import Path from typing import Any, Callable -from ci_config import get_bid_entry, load_ci +from ci_config import get_bid_entry, load_ci, parse_bid from terminal import Terminal, TerminalCommandError, TerminalTimeoutError @@ -205,15 +205,16 @@ def parse_args() -> argparse.Namespace: def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: ci = load_ci() bid_entry = get_bid_entry(ci, args.bid) - build_args = bid_entry["build_args"] tests = bid_entry["tests"] - arch = build_args.get("ARCH", "").strip() - board = build_args.get("BOARD", "").strip() + try: + arch, board = parse_bid(args.bid) + except ValueError as exc: + raise SystemExit(str(exc)) from exc mode = bid_entry.get("mode", "").strip() cases = bid_entry.get("cases", []) - if not arch or not board or not mode: - raise SystemExit(f"incomplete config for bid '{args.bid}': ARCH/BOARD/mode are required") + if not mode: + raise SystemExit(f"incomplete config for bid '{args.bid}': tests.mode is required") if not cases: raise SystemExit(f"no test cases configured for bid '{args.bid}'") From 82ae09ccc1e00df7592ae75523d5ea91909fc289 Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 26 May 2026 16:11:12 +0800 Subject: [PATCH 05/53] beautify ci --- Jenkinsfile | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index cbffa0bfd..5f071a3c4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -60,6 +60,32 @@ def normalizeToolArch(String arch) { return mapping.get(raw, raw) } +/** GitHub Check name for this matrix cell */ +def matrixCheckName() { + return (env.BID ?: '').toString() +} + +def publishMatrixCheckInProgress() { + publishChecks( + name: matrixCheckName(), + title: matrixCheckName(), + summary: 'In progress', + status: 'IN_PROGRESS', + detailsURL: "${env.RUN_DISPLAY_URL ?: env.BUILD_URL}", + ) +} + +def publishMatrixCheckCompleted(String conclusion) { + publishChecks( + name: matrixCheckName(), + title: matrixCheckName(), + summary: conclusion == 'SUCCESS' ? 'Passed' : 'Failed', + status: 'COMPLETED', + conclusion: conclusion, + detailsURL: "${env.RUN_DISPLAY_URL ?: env.BUILD_URL}", + ) +} + /** Kconfig: venv + tools/kconfig/kconfig_cli.py (via make defconfig). Keep in sync with Makefile. */ def kconfigSetupShell(String arch, String board) { return """ @@ -142,6 +168,7 @@ pipeline { stage('Prepare cell workspace') { steps { script { + publishMatrixCheckInProgress() def cellWs = matrixCellDir() sh """ mkdir -p '${cellWs}' @@ -294,6 +321,18 @@ pipeline { } } } + + post { + success { + script { publishMatrixCheckCompleted('SUCCESS') } + } + failure { + script { publishMatrixCheckCompleted('FAILURE') } + } + unstable { + script { publishMatrixCheckCompleted('FAILURE') } + } + } } } } From 69551083b595c379f27128d5bf16a1836f0f9892 Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 26 May 2026 18:57:05 +0800 Subject: [PATCH 06/53] move linter and license-checker to jenkins --- .github/workflows/ci.yml | 18 +++++------ Jenkinsfile | 67 ++++++++++++++++++++++++++++++++++++---- jenkins/ci_runner.py | 2 +- 3 files changed, 71 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33187b152..7972f545f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,15 +5,15 @@ name: CI on: - push: - branches: - - main - - dev - pull_request: - branches: - - main - - dev - workflow_dispatch: + # push: + # branches: + # - main + # - dev + # pull_request: + # branches: + # - main + # - dev + # workflow_dispatch: env: MODE: debug jobs: diff --git a/Jenkinsfile b/Jenkinsfile index 5f071a3c4..0854490a1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -65,20 +65,21 @@ def matrixCheckName() { return (env.BID ?: '').toString() } -def publishMatrixCheckInProgress() { +def publishGithubCheckInProgress(String checkName) { publishChecks( - name: matrixCheckName(), - title: matrixCheckName(), + name: checkName, + title: checkName, summary: 'In progress', status: 'IN_PROGRESS', + conclusion: 'NONE', detailsURL: "${env.RUN_DISPLAY_URL ?: env.BUILD_URL}", ) } -def publishMatrixCheckCompleted(String conclusion) { +def publishGithubCheckCompleted(String checkName, String conclusion) { publishChecks( - name: matrixCheckName(), - title: matrixCheckName(), + name: checkName, + title: checkName, summary: conclusion == 'SUCCESS' ? 'Passed' : 'Failed', status: 'COMPLETED', conclusion: conclusion, @@ -86,6 +87,14 @@ def publishMatrixCheckCompleted(String conclusion) { ) } +def publishMatrixCheckInProgress() { + publishGithubCheckInProgress(matrixCheckName()) +} + +def publishMatrixCheckCompleted(String conclusion) { + publishGithubCheckCompleted(matrixCheckName(), conclusion) +} + /** Kconfig: venv + tools/kconfig/kconfig_cli.py (via make defconfig). Keep in sync with Makefile. */ def kconfigSetupShell(String arch, String board) { return """ @@ -135,6 +144,52 @@ pipeline { } } + stage('Linter') { + steps { + script { + publishGithubCheckInProgress('linter') + sh """ + export PATH=${env.CARGO_HOME}/bin:${env.TOOLCHAIN_PATHS}:\$PATH + make fmt-test + """ + } + } + post { + success { + script { publishGithubCheckCompleted('linter', 'SUCCESS') } + } + failure { + script { publishGithubCheckCompleted('linter', 'FAILURE') } + } + unstable { + script { publishGithubCheckCompleted('linter', 'FAILURE') } + } + } + } + + stage('License checker') { + steps { + script { + publishGithubCheckInProgress('license-checker') + sh """ + chmod +x tools/license_checker.sh + ./tools/license_checker.sh + """ + } + } + post { + success { + script { publishGithubCheckCompleted('license-checker', 'SUCCESS') } + } + failure { + script { publishGithubCheckCompleted('license-checker', 'FAILURE') } + } + unstable { + script { publishGithubCheckCompleted('license-checker', 'FAILURE') } + } + } + } + stage('Multi-Platform Matrix') { matrix { axes { diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 835ddb869..6036ff9c4 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -180,7 +180,7 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: if not pts_numbers: raise TerminalCommandError("failed to find numeric pts from 'ls -1 /dev/pts/[0-9]*'") max_pts = pts_numbers[-1] - _ = run_and_print_send_only(term, f"screen /dev/pts/{max_pts}", read_duration=5.0) + _ = run_and_print_send_only(term, f"screen /dev/pts/{max_pts}", read_duration=20.0) _ = run_and_print_send_only(term, "\n", read_duration=2.0) _, _ = run_and_print_quiet(term, "ls", quiet_seconds=1.0, max_duration=15.0) if boot_rc != 0: From 34185d4f90ae590e061b9cb9aa57026379c51b96 Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 26 May 2026 19:05:19 +0800 Subject: [PATCH 07/53] delete workspace after build --- Jenkinsfile | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 0854490a1..e54620529 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -118,6 +118,7 @@ pipeline { always { echo "=== DEBUG: Branch ${env.BRANCH_NAME} ===" echo "=== DEBUG: Commit ${env.GIT_COMMIT} ===" + deleteDir() } } @@ -136,13 +137,13 @@ pipeline { } stages { - stage('Checkout') { - steps { - // Ensure no stale files from previous builds. - deleteDir() - checkout scm - } - } + // stage('Checkout') { + // steps { + // // Ensure no stale files from previous builds. + // deleteDir() + // checkout scm + // } + // } stage('Linter') { steps { From f8f553e1954ac49c6e5ec302d4018ed091fa5de7 Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 26 May 2026 19:19:52 +0800 Subject: [PATCH 08/53] add abort for ci --- Jenkinsfile | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index e54620529..efc252332 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -77,10 +77,15 @@ def publishGithubCheckInProgress(String checkName) { } def publishGithubCheckCompleted(String checkName, String conclusion) { + def summaries = [ + 'SUCCESS' : 'Passed', + 'FAILURE' : 'Failed', + 'CANCELLED': 'Cancelled', + ] publishChecks( name: checkName, title: checkName, - summary: conclusion == 'SUCCESS' ? 'Passed' : 'Failed', + summary: summaries.get(conclusion, conclusion), status: 'COMPLETED', conclusion: conclusion, detailsURL: "${env.RUN_DISPLAY_URL ?: env.BUILD_URL}", @@ -165,6 +170,9 @@ pipeline { unstable { script { publishGithubCheckCompleted('linter', 'FAILURE') } } + aborted { + script { publishGithubCheckCompleted('linter', 'CANCELLED') } + } } } @@ -188,6 +196,9 @@ pipeline { unstable { script { publishGithubCheckCompleted('license-checker', 'FAILURE') } } + aborted { + script { publishGithubCheckCompleted('license-checker', 'CANCELLED') } + } } } @@ -221,7 +232,7 @@ pipeline { } stages { - stage('Prepare cell workspace') { + stage('Prepare cell') { steps { script { publishMatrixCheckInProgress() @@ -388,6 +399,9 @@ pipeline { unstable { script { publishMatrixCheckCompleted('FAILURE') } } + aborted { + script { publishMatrixCheckCompleted('CANCELLED') } + } } } } From 5c3901fbe238195ddf7c21910c81badfd92a6ca1 Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 26 May 2026 21:21:08 +0800 Subject: [PATCH 09/53] add workspace for linter and license --- Jenkinsfile | 50 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index efc252332..ae674e5d2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -32,6 +32,23 @@ def matrixCellDir() { return "${env.WORKSPACE}/.matrix/${bid.replace('/', '__')}" } +/** Isolated workspace for top-level CI jobs (linter, license-checker, …). */ +def jenkinsJobDir(String name) { + return "${env.WORKSPACE}/.jenkins/${name}" +} + +/** Copy repo into an isolated dir; excludes other Jenkins sandboxes from the source tree. */ +def syncWorkspaceTo(String destDir) { + sh """ + mkdir -p '${destDir}' + rsync -a --delete \\ + --exclude '.jenkins/' \\ + --exclude '.matrix/' \\ + --exclude '.jenkins-matrix/' \\ + '${env.WORKSPACE}/' '${destDir}/' + """ +} + def loadCiYaml() { def data = readYaml file: 'jenkins/ci.yaml' def bids = data?.bids @@ -154,10 +171,14 @@ pipeline { steps { script { publishGithubCheckInProgress('linter') - sh """ - export PATH=${env.CARGO_HOME}/bin:${env.TOOLCHAIN_PATHS}:\$PATH - make fmt-test - """ + def cellWs = jenkinsJobDir('linter') + syncWorkspaceTo(cellWs) + dir(cellWs) { + sh """ + export PATH=${env.CARGO_HOME}/bin:${env.TOOLCHAIN_PATHS}:\$PATH + make fmt-test + """ + } } } post { @@ -180,10 +201,14 @@ pipeline { steps { script { publishGithubCheckInProgress('license-checker') - sh """ - chmod +x tools/license_checker.sh - ./tools/license_checker.sh - """ + def cellWs = jenkinsJobDir('license-checker') + syncWorkspaceTo(cellWs) + dir(cellWs) { + sh """ + chmod +x tools/license_checker.sh + ./tools/license_checker.sh + """ + } } } post { @@ -236,14 +261,7 @@ pipeline { steps { script { publishMatrixCheckInProgress() - def cellWs = matrixCellDir() - sh """ - mkdir -p '${cellWs}' - rsync -a --delete \\ - --exclude '.matrix/' \\ - --exclude '.jenkins-matrix/' \\ - '${env.WORKSPACE}/' '${cellWs}/' - """ + syncWorkspaceTo(matrixCellDir()) } } } From b213936cb7748f91f47a8c6348248d64586f4889 Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 26 May 2026 21:38:28 +0800 Subject: [PATCH 10/53] beautify jenkinsfile --- Jenkinsfile | 108 ++++++++++++++++++++++------------------------------ 1 file changed, 46 insertions(+), 62 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index ae674e5d2..8c7062677 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -117,10 +117,33 @@ def publishMatrixCheckCompleted(String conclusion) { publishGithubCheckCompleted(matrixCheckName(), conclusion) } +def finishGithubCheck(String checkName, String buildResult) { + def conclusion = [ + 'SUCCESS' : 'SUCCESS', + 'FAILURE' : 'FAILURE', + 'UNSTABLE': 'FAILURE', + 'ABORTED' : 'CANCELLED', + 'NOT_BUILT': 'CANCELLED', + ].get(buildResult ?: '', 'FAILURE') + publishGithubCheckCompleted(checkName, conclusion) +} + +def hasCiTests() { + return getBidConfig(loadCiYaml(), env.BID) != null +} + +def toolchainPathShell() { + return "export PATH=${env.CARGO_HOME}/bin:${env.TOOLCHAIN_PATHS}:\$PATH" +} + +def qemuPathShell() { + return "export PATH=${env.QEMU_PATH}:\$PATH" +} + /** Kconfig: venv + tools/kconfig/kconfig_cli.py (via make defconfig). Keep in sync with Makefile. */ def kconfigSetupShell(String arch, String board) { return """ - export PATH=${env.CARGO_HOME}/bin:${env.TOOLCHAIN_PATHS}:\$PATH + ${toolchainPathShell()} chmod +x tools/kconfig/bootstrap_venv.sh tools/kconfig/host_config.sh tools/kconfig/save_defconfig.sh 2>/dev/null || true if [ ! -x tools/kconfig/.venv/bin/python ]; then ./tools/kconfig/bootstrap_venv.sh @@ -159,14 +182,6 @@ pipeline { } stages { - // stage('Checkout') { - // steps { - // // Ensure no stale files from previous builds. - // deleteDir() - // checkout scm - // } - // } - stage('Linter') { steps { script { @@ -175,24 +190,15 @@ pipeline { syncWorkspaceTo(cellWs) dir(cellWs) { sh """ - export PATH=${env.CARGO_HOME}/bin:${env.TOOLCHAIN_PATHS}:\$PATH + ${toolchainPathShell()} make fmt-test """ } } } post { - success { - script { publishGithubCheckCompleted('linter', 'SUCCESS') } - } - failure { - script { publishGithubCheckCompleted('linter', 'FAILURE') } - } - unstable { - script { publishGithubCheckCompleted('linter', 'FAILURE') } - } - aborted { - script { publishGithubCheckCompleted('linter', 'CANCELLED') } + always { + script { finishGithubCheck('linter', currentBuild.currentResult) } } } } @@ -212,17 +218,8 @@ pipeline { } } post { - success { - script { publishGithubCheckCompleted('license-checker', 'SUCCESS') } - } - failure { - script { publishGithubCheckCompleted('license-checker', 'FAILURE') } - } - unstable { - script { publishGithubCheckCompleted('license-checker', 'FAILURE') } - } - aborted { - script { publishGithubCheckCompleted('license-checker', 'CANCELLED') } + always { + script { finishGithubCheck('license-checker', currentBuild.currentResult) } } } } @@ -277,12 +274,12 @@ pipeline { sh kconfigSetupShell(arch, board) if (arch != 'x86_64') { sh """ - export PATH=${env.CARGO_HOME}/bin:${env.TOOLCHAIN_PATHS}:\$PATH + ${toolchainPathShell()} make dtb ARCH=${arch} BOARD=${board} """ } sh """ - export PATH=${env.CARGO_HOME}/bin:${env.TOOLCHAIN_PATHS}:\$PATH + ${toolchainPathShell()} make all ARCH=${arch} BOARD=${board} MODE=release """ } @@ -292,15 +289,12 @@ pipeline { stage('Build hvisor-tool') { when { - expression { - return getBidConfig(loadCiYaml(), env.BID) != null - } + expression { return hasCiTests() } } steps { dir(matrixCellDir()) { script { - def ci = loadCiYaml() - def bidCfg = getBidConfig(ci, env.BID) + def bidCfg = getBidConfig(loadCiYaml(), env.BID) def buildArgs = parseCiBuildArgs(bidCfg) def bidTool = parseBid(env.BID) def tarch = normalizeToolArch(buildArgs.TARCH ?: bidTool.arch) @@ -323,8 +317,7 @@ pipeline { } sh """ export PATH=${env.TOOLCHAIN_PATHS}:\$PATH - cd ${env.HVISOR_TOOL_PATH} - make all ARCH=${tarch} KDIR=${kdir} + make -C ${env.HVISOR_TOOL_PATH} all ARCH=${tarch} KDIR=${kdir} """ } } @@ -333,19 +326,16 @@ pipeline { stage('Prepare test') { when { - expression { - return getBidConfig(loadCiYaml(), env.BID) != null - } + expression { return hasCiTests() } } steps { dir(matrixCellDir()) { script { - def ci = loadCiYaml() - def bidCfg = getBidConfig(ci, env.BID) + def bidCfg = getBidConfig(loadCiYaml(), env.BID) def buildArgs = parseCiBuildArgs(bidCfg) - def bidPrepare = parseBid(env.BID) - def arch = bidPrepare.arch - def board = bidPrepare.board + def bidParsed = parseBid(env.BID) + def arch = bidParsed.arch + def board = bidParsed.board def kdir = (buildArgs.KDIR ?: '').toString() def testsCfg = bidCfg.tests ?: [:] def mode = (testsCfg.mode ?: '').toString().trim() @@ -382,22 +372,16 @@ pipeline { stage('Run test cases') { when { - expression { - return getBidConfig(loadCiYaml(), env.BID) != null - } + expression { return hasCiTests() } } steps { dir(matrixCellDir()) { script { - def bidRun = parseBid(env.BID) - def tArch = bidRun.arch - def tBoard = bidRun.board echo "Run tests via ci_runner [BID=${env.BID}]" sh """ export TERM=\${TERM:-xterm} - export PATH=${env.CARGO_HOME}/bin:${env.TOOLCHAIN_PATHS}:\$PATH - export PATH=${env.QEMU_PATH}:\$PATH - ${kconfigSetupShell(tArch, tBoard)} + ${toolchainPathShell()} + ${qemuPathShell()} python3 jenkins/ci_runner.py \ --bid "${env.BID}" """ @@ -409,16 +393,16 @@ pipeline { post { success { - script { publishMatrixCheckCompleted('SUCCESS') } + script { finishGithubCheck(matrixCheckName(), 'SUCCESS') } } failure { - script { publishMatrixCheckCompleted('FAILURE') } + script { finishGithubCheck(matrixCheckName(), 'FAILURE') } } unstable { - script { publishMatrixCheckCompleted('FAILURE') } + script { finishGithubCheck(matrixCheckName(), 'UNSTABLE') } } aborted { - script { publishMatrixCheckCompleted('CANCELLED') } + script { finishGithubCheck(matrixCheckName(), 'ABORTED') } } } } From 9ad5758549afebaaf058cc6093df8d3fb17099ea Mon Sep 17 00:00:00 2001 From: dallas Date: Wed, 27 May 2026 14:33:25 +0800 Subject: [PATCH 11/53] fix(riscv64): remove invalid power_on/init from ArchCpu::new after merge --- src/arch/riscv64/cpu.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/arch/riscv64/cpu.rs b/src/arch/riscv64/cpu.rs index 8dcd7f69a..bc6997dbd 100644 --- a/src/arch/riscv64/cpu.rs +++ b/src/arch/riscv64/cpu.rs @@ -52,8 +52,6 @@ impl ArchCpu { stack_top: 0, cpuid, // first_cpu: 0, - power_on: false, - init: false, sstc: cfg!(sstc), }; ret From fd057aea9a7ae58fb9b07860b40d47e54061665e Mon Sep 17 00:00:00 2001 From: dallas Date: Wed, 27 May 2026 15:30:40 +0800 Subject: [PATCH 12/53] beauty --- build.rs | 3 +-- src/pci/config_accessors/mod.rs | 2 +- src/pci/pci_config.rs | 6 +++--- tools/kconfig/bootstrap_venv.sh | 2 -- tools/kconfig/kconfig_cli.py | 2 +- tools/kconfig/requirements.txt | 1 - 6 files changed, 6 insertions(+), 10 deletions(-) diff --git a/build.rs b/build.rs index 0b01b8d7b..d256463c5 100644 --- a/build.rs +++ b/build.rs @@ -36,8 +36,7 @@ struct BuildEnv { bid: String, } -// parse ARCH, BOARD and BID from .config (comment-prefixed metadata from kconfig_cli.py, -// or legacy unprefixed lines). +// parse ARCH, BOARD and BID from .config fn parse_build_env(file_path: &str) -> BuildEnv { let file = fs::read_to_string(file_path).expect("Failed to read .config file"); let mut arch = String::new(); diff --git a/src/pci/config_accessors/mod.rs b/src/pci/config_accessors/mod.rs index ec9405c2f..1a974546d 100644 --- a/src/pci/config_accessors/mod.rs +++ b/src/pci/config_accessors/mod.rs @@ -65,7 +65,7 @@ impl PciRegionMmio { } } -#[cfg(all(not(ecam_pcie), not(dwc_pcie), not(loongarch64_pcie)))] +#[cfg(not(pci))] impl PciConfigMmio { pub fn access(&self, offset: PciConfigAddress) -> *mut T { (self.base + offset) as *mut T diff --git a/src/pci/pci_config.rs b/src/pci/pci_config.rs index 944ed6901..99df487e2 100644 --- a/src/pci/pci_config.rs +++ b/src/pci/pci_config.rs @@ -39,7 +39,7 @@ use crate::pci::{ vpci_dev::{get_handler, VpciDevType}, }; -#[cfg(any(ecam_pcie, dwc_pcie, loongarch64_pcie))] +#[cfg(pci)] use crate::pci::{mem_alloc::BaseAllocator, pci_struct::RootComplex}; #[cfg(ecam_pcie)] @@ -72,7 +72,7 @@ pub static GLOBAL_PCIE_LIST: Lazy HvResult { warn!("begin {:#x?}", pci_config); - #[cfg(any(ecam_pcie, dwc_pcie, loongarch64_pcie))] + #[cfg(pci)] for (_index, rootcomplex_config) in pci_config.iter().enumerate() { /* empty config */ if rootcomplex_config.ecam_base == 0 { @@ -511,7 +511,7 @@ impl Zone { rootcomplex_config.ecam_size as usize, )); } - #[cfg(not(any(ecam_pcie, dwc_pcie, loongarch64_pcie)))] + #[cfg(not(pci))] { warn!( "No extend config found for base 0x{:x}", diff --git a/tools/kconfig/bootstrap_venv.sh b/tools/kconfig/bootstrap_venv.sh index 2b999cbf6..5a386d67e 100755 --- a/tools/kconfig/bootstrap_venv.sh +++ b/tools/kconfig/bootstrap_venv.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash # Create tools/kconfig/.venv and install kconfiglib (+ PySocks) from vendored wheels -# so pip never needs SOCKS support during the first install (fixes Jenkins when -# ALL_PROXY/HTTPS_PROXY is socks5:// — urllib3 errors with "Missing dependencies for SOCKS support"). set -euo pipefail ROOT="$(cd "$(dirname "$0")" && pwd)" VENV="${ROOT}/.venv" diff --git a/tools/kconfig/kconfig_cli.py b/tools/kconfig/kconfig_cli.py index 6631c5dc4..ffef3f699 100644 --- a/tools/kconfig/kconfig_cli.py +++ b/tools/kconfig/kconfig_cli.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Kconfig entrypoints: defconfig, menuconfig, vscode-cfgs (one module to avoid scattered scripts).""" +"""Kconfig entrypoints: defconfig, menuconfig, vscode-cfgs.""" from __future__ import annotations import argparse diff --git a/tools/kconfig/requirements.txt b/tools/kconfig/requirements.txt index 49154ab14..3c91d8914 100644 --- a/tools/kconfig/requirements.txt +++ b/tools/kconfig/requirements.txt @@ -1,3 +1,2 @@ -# Lets pip/urllib3 use socks5:// proxies (needed before first online pip install in some CI agents). PySocks>=1.7.1 kconfiglib>=14.1.0 From 9ac5101024fe92b7dad1c3f12066fff98ccf5a9d Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 2 Jun 2026 12:37:09 +0800 Subject: [PATCH 13/53] adjust makefile --- CHANGELOG.md | 2 +- Makefile | 33 +++++++++---------- build.rs | 1 - kconfig/Kconfig | 6 ++-- kconfig/cfg_map.toml | 2 +- .../hifive-premier-p550/kconfig/defconfig | 2 +- platform/riscv64/megrez/kconfig/defconfig | 2 +- 7 files changed, 23 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3b5d3a9b..66abbcf71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,7 +53,7 @@ - [platform] **aarch64**: Improve QEMU GICv2/GICv3 configurations and add zone1-linux support. ([agicy](https://github.com/agicy)) - [platform] **loongarch64**: Add support for Loongson 3A5 / 3A6 platforms and improve clock and trap handling. ([wheatfox](https://github.com/enkerewpo)) - [feature] Add **aarch32** support. ([Guowei Li](https://github.com/KouweiLee)) -- [feature] **riscv64** enhancements: g-stage dynamic detection, eic770x_soc, and syscrg emulation. ([Jingyu Liu](https://github.com/liulog)) +- [feature] **riscv64** enhancements: g-stage dynamic detection, hypervisor_v0_6 (EIC770x SoC), and syscrg emulation. ([Jingyu Liu](https://github.com/liulog)) - [infra] Unify UART / MPIDR mapping, centralize IOMMU configuration, remove redundant arch feature flags, and tidy Cargo/zone/hypercall code. ([Nehckl](https://github.com/Inquisitor-201), [Ren HangQi](https://github.com/ForeverYolo)) - [infra/tool] **aarch64**: Optimized the structure of GIC parameters ([Ren HangQi](https://github.com/ForeverYolo)) - [ci/misc] Update dependencies, add ccache support, and improve build/tooling workflows. ([Jingyu Liu](https://github.com/liulog)) diff --git a/Makefile b/Makefile index de87890ee..23f94ce0b 100644 --- a/Makefile +++ b/Makefile @@ -78,7 +78,7 @@ COLOR_RESET := $(shell tput sgr0) kconfig_python := tools/kconfig/.venv/bin/python # Targets -.PHONY: all elf disa run gdb monitor clean tools rootfs vscode ci-run defconfig menuconfig savedefconfig ensure_config kconfig_venv check-hv-mem-overlap +.PHONY: all elf disa run gdb monitor clean tools rootfs vscode ci-run defconfig menuconfig savedefconfig ensure_config clean_check kconfig_venv check-hv-mem-overlap kconfig_venv: @if [ ! -x $(kconfig_python) ]; then \ echo "$(COLOR_YELLOW)Creating tools/kconfig/.venv (kconfiglib)...$(COLOR_RESET)"; \ @@ -86,11 +86,24 @@ kconfig_venv: fi ensure_config: - @if [ ! -f .config ]; then \ + @CONFIG_ARCH=; CONFIG_BOARD=; \ + if [ -f .config ]; then \ + CONFIG_ARCH=$$(grep '^# ARCH=' .config 2>/dev/null | head -1 | sed 's/^# ARCH=//'); \ + [ -n "$$CONFIG_ARCH" ] || CONFIG_ARCH=$$(grep '^ARCH=' .config | head -1 | cut -d'=' -f2); \ + CONFIG_BOARD=$$(grep '^# BOARD=' .config 2>/dev/null | head -1 | sed 's/^# BOARD=//'); \ + [ -n "$$CONFIG_BOARD" ] || CONFIG_BOARD=$$(grep '^BOARD=' .config | head -1 | cut -d'=' -f2); \ + fi; \ + if [ ! -f .config ]; then \ echo "$(COLOR_YELLOW)No .config; running defconfig for $(ARCH)/$(BOARD)$(COLOR_RESET)"; \ $(MAKE) --no-print-directory defconfig; \ + elif [ "$$CONFIG_ARCH" != "$(ARCH)" ] || [ "$$CONFIG_BOARD" != "$(BOARD)" ]; then \ + echo "$(COLOR_YELLOW)$(COLOR_BOLD)ARCH or BOARD changed (OLD: $$CONFIG_ARCH/$$CONFIG_BOARD, NEW: $(ARCH)/$(BOARD)), cleaning and reloading defconfig...$(COLOR_RESET)"; \ + ./tools/clean.sh; \ + $(MAKE) --no-print-directory defconfig; \ fi +clean_check: ensure_config + defconfig: kconfig_venv @$(kconfig_python) tools/kconfig/kconfig_cli.py defconfig @@ -100,7 +113,7 @@ menuconfig: kconfig_venv savedefconfig: @./tools/kconfig/save_defconfig.sh "$(ARCH)" "$(BOARD)" -all: clean_check ensure_config gen_cargo_config vscode $(hvisor_bin) check-hv-mem-overlap +all: ensure_config gen_cargo_config vscode $(hvisor_bin) check-hv-mem-overlap @printf "\n" @printf "$(COLOR_GREEN)$(COLOR_BOLD)hvisor build summary:$(COLOR_RESET)\n" @printf "%-10s %s\n" "ARCH =" "$(COLOR_BOLD)$(ARCH)$(COLOR_RESET)" @@ -121,20 +134,6 @@ all: clean_check ensure_config gen_cargo_config vscode $(hvisor_bin) check-hv-me @printf "\n" @printf "$(COLOR_GREEN)$(COLOR_BOLD)hvisor build success!$(COLOR_RESET)\n" -clean_check: -# if .config not exist, then everything is fine -# else we read .config and parse ARCH and BOARD, if they are different, we clean the build - @if [ -f ".config" ]; then \ - CONFIG_ARCH=$$(grep '^# ARCH=' .config 2>/dev/null | head -1 | sed 's/^# ARCH=//'); \ - [ -n "$$CONFIG_ARCH" ] || CONFIG_ARCH=$$(grep '^ARCH=' .config | head -1 | cut -d'=' -f2); \ - CONFIG_BOARD=$$(grep '^# BOARD=' .config 2>/dev/null | head -1 | sed 's/^# BOARD=//'); \ - [ -n "$$CONFIG_BOARD" ] || CONFIG_BOARD=$$(grep '^BOARD=' .config | head -1 | cut -d'=' -f2); \ - if [ "$$CONFIG_ARCH" != "$(ARCH)" ] || [ "$$CONFIG_BOARD" != "$(BOARD)" ]; then \ - echo "$(COLOR_YELLOW)$(COLOR_BOLD)ARCH or BOARD changed(OLD: $$CONFIG_ARCH/$$CONFIG_BOARD, NEW: $(ARCH)/$(BOARD)), cleaning...$(COLOR_RESET)"; \ - ./tools/clean.sh; \ - fi; \ - fi - gen_cargo_config: @printf "$(COLOR_GREEN)$(COLOR_BOLD)generating .cargo/config.toml...$(COLOR_RESET)\n" @chmod +x tools/kconfig/host_config.sh 2>/dev/null || true diff --git a/build.rs b/build.rs index d256463c5..9de7058df 100644 --- a/build.rs +++ b/build.rs @@ -72,7 +72,6 @@ fn parse_build_env(file_path: &str) -> BuildEnv { BuildEnv { arch, board, bid } } -// load cfg_map.toml, which is generated by tools/kconfig/cfg_map.py fn load_cfg_map(root: &Path) -> HashMap { let path = root.join("kconfig/cfg_map.toml"); let raw = fs::read_to_string(&path).unwrap_or_else(|_| panic!("missing {}", path.display())); diff --git a/kconfig/Kconfig b/kconfig/Kconfig index 03c2efff1..ffa71cb51 100644 --- a/kconfig/Kconfig +++ b/kconfig/Kconfig @@ -71,18 +71,18 @@ config SSTC bool "Sstc" default n -config Hypervisor_v0_6 +config HYPERVISOR_V0_6 bool "Hypervisor v0.6" default n config EIC7700_SYSREG bool "EIC7700 system registers" - depends on Hypervisor_v0_6 + depends on HYPERVISOR_V0_6 default n config SIFIVE_CCACHE bool "SiFive ccache" - depends on Hypervisor_v0_6 + depends on HYPERVISOR_V0_6 default n endmenu diff --git a/kconfig/cfg_map.toml b/kconfig/cfg_map.toml index c8cda0dc3..d9cb418ee 100644 --- a/kconfig/cfg_map.toml +++ b/kconfig/cfg_map.toml @@ -25,7 +25,7 @@ CONFIG_DP1000_PLIC = "dp1000_plic" CONFIG_AIA = "aia" CONFIG_ACLINT = "aclint" CONFIG_SSTC = "sstc" -CONFIG_Hypervisor_v0_6 = "hypervisor_v0_6" +CONFIG_HYPERVISOR_V0_6 = "hypervisor_v0_6" CONFIG_EIC7700_SYSREG = "eic7700_sysreg" CONFIG_SIFIVE_CCACHE = "sifive_ccache" CONFIG_LOONGSON_7A2000 = "loongson_7a2000" diff --git a/platform/riscv64/hifive-premier-p550/kconfig/defconfig b/platform/riscv64/hifive-premier-p550/kconfig/defconfig index 32165b1ad..769ea39d6 100644 --- a/platform/riscv64/hifive-premier-p550/kconfig/defconfig +++ b/platform/riscv64/hifive-premier-p550/kconfig/defconfig @@ -1,5 +1,5 @@ CONFIG_ARCH_RISCV64=y CONFIG_EIC7700_SYSREG=y -CONFIG_Hypervisor_v0_6=y +CONFIG_HYPERVISOR_V0_6=y CONFIG_PLIC=y CONFIG_SIFIVE_CCACHE=y diff --git a/platform/riscv64/megrez/kconfig/defconfig b/platform/riscv64/megrez/kconfig/defconfig index 2ba427204..54604c280 100644 --- a/platform/riscv64/megrez/kconfig/defconfig +++ b/platform/riscv64/megrez/kconfig/defconfig @@ -1,3 +1,3 @@ CONFIG_ARCH_RISCV64=y -CONFIG_Hypervisor_v0_6=y +CONFIG_HYPERVISOR_V0_6=y CONFIG_PLIC=y From 9d8299bfb5497b0334f9b304b8eee8d381c44f69 Mon Sep 17 00:00:00 2001 From: dallas Date: Wed, 3 Jun 2026 14:53:15 +0800 Subject: [PATCH 14/53] remove Vendor wheels --- Jenkinsfile | 19 +++++++++++------- Makefile | 4 ---- tools/kconfig/bootstrap_venv.sh | 13 ++++++------ .../vendor/PySocks-1.7.1-py3-none-any.whl | Bin 16725 -> 0 bytes .../kconfiglib-14.1.0-py2.py3-none-any.whl | Bin 145875 -> 0 bytes 5 files changed, 18 insertions(+), 18 deletions(-) delete mode 100644 tools/kconfig/vendor/PySocks-1.7.1-py3-none-any.whl delete mode 100644 tools/kconfig/vendor/kconfiglib-14.1.0-py2.py3-none-any.whl diff --git a/Jenkinsfile b/Jenkinsfile index 8c7062677..38af869d7 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -140,13 +140,18 @@ def qemuPathShell() { return "export PATH=${env.QEMU_PATH}:\$PATH" } -/** Kconfig: venv + tools/kconfig/kconfig_cli.py (via make defconfig). Keep in sync with Makefile. */ +/** Kconfig: symlink agent venv + make defconfig. Keep in sync with Makefile kconfig_python path. */ def kconfigSetupShell(String arch, String board) { return """ ${toolchainPathShell()} - chmod +x tools/kconfig/bootstrap_venv.sh tools/kconfig/host_config.sh tools/kconfig/save_defconfig.sh 2>/dev/null || true + chmod +x tools/kconfig/host_config.sh tools/kconfig/save_defconfig.sh 2>/dev/null || true if [ ! -x tools/kconfig/.venv/bin/python ]; then - ./tools/kconfig/bootstrap_venv.sh + if [ ! -x ${env.KCONFIG_VENV}/bin/python ]; then + echo "ERROR: CI kconfig venv missing: ${env.KCONFIG_VENV}/bin/python" >&2 + exit 1 + fi + mkdir -p tools/kconfig + ln -sfn ${env.KCONFIG_VENV} tools/kconfig/.venv fi make defconfig ARCH=${arch} BOARD=${board} """ @@ -174,6 +179,7 @@ pipeline { CARGO_HOME = '/usr/local/cargo' QEMU_PATH = '/home/light/DEMO/qemu-9.2.3/build' TEST_IMG_BASE = '/home/light/DEMO/syswonder/test_img' + KCONFIG_VENV = "/home/light/DEMO/syswonder/test_img/venv" RISCV_TOOLCHAIN_PATH = '/home/light/DEMO/toolchain/riscv64-glibc-ubuntu-24.04-gcc' AARCH64_TOOLCHAIN_PATH = '/home/light/DEMO/toolchain/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu' LOONGARCH64_TOOLCHAIN_PATH = '/home/light/DEMO/toolchain/loongarch_cross_tools' @@ -189,10 +195,9 @@ pipeline { def cellWs = jenkinsJobDir('linter') syncWorkspaceTo(cellWs) dir(cellWs) { - sh """ - ${toolchainPathShell()} - make fmt-test - """ + sh kconfigSetupShell('aarch64', 'qemu-gicv3') + ''' + cargo fmt --all -- --check + ''' } } } diff --git a/Makefile b/Makefile index 23f94ce0b..36d798205 100644 --- a/Makefile +++ b/Makefile @@ -189,10 +189,6 @@ test-pre: download-test-img chmod +x platform/$(ARCH)/$(BOARD)/test/runner.sh @echo "added execute permission to test runner.sh for board $(BOARD)" -fmt-test: all - cargo fmt --all -- --check - @echo "cargo fmt check passed!" - fmt: all cargo fmt --all @echo "your code has been formatted" diff --git a/tools/kconfig/bootstrap_venv.sh b/tools/kconfig/bootstrap_venv.sh index 5a386d67e..a1830b9c3 100755 --- a/tools/kconfig/bootstrap_venv.sh +++ b/tools/kconfig/bootstrap_venv.sh @@ -1,20 +1,19 @@ #!/usr/bin/env bash -# Create tools/kconfig/.venv and install kconfiglib (+ PySocks) from vendored wheels +# Create tools/kconfig/.venv and install kconfiglib (+ PySocks) from PyPI. +# Local/non-CI only; Jenkins uses a pre-baked venv via symlink set -euo pipefail ROOT="$(cd "$(dirname "$0")" && pwd)" VENV="${ROOT}/.venv" REQ="${ROOT}/requirements.txt" -VEN="${ROOT}/vendor" if [[ ! -f "${REQ}" ]]; then echo "ERROR: missing ${REQ}" >&2 exit 1 fi -if [[ ! -d "${VEN}" ]] || ! compgen -G "${VEN}/*.whl" > /dev/null; then - echo "ERROR: missing wheels under ${VEN}/ (run: python3 -m pip download -d ${VEN} -r ${REQ})" >&2 - exit 1 + +if [[ -x "${VENV}/bin/python" ]] && "${VENV}/bin/python" -c "import kconfiglib" 2>/dev/null; then + exit 0 fi python3 -m venv "${VENV}" -# Do not upgrade pip here: that would hit PyPI and can fail behind socks5:// before PySocks exists. -"${VENV}/bin/pip" install -q --no-index --find-links="${VEN}" -r "${REQ}" +"${VENV}/bin/pip" install -q -r "${REQ}" diff --git a/tools/kconfig/vendor/PySocks-1.7.1-py3-none-any.whl b/tools/kconfig/vendor/PySocks-1.7.1-py3-none-any.whl deleted file mode 100644 index ac66f59021d16eb13c326494e7a5565507a92f02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16725 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 Y8Su~H1_%i4^TqSo`2?_j4kAGR3nHOvdjJ3c From 730436e6b409d9df6b8b382ddfb4b4ddbf62dcc7 Mon Sep 17 00:00:00 2001 From: dallas Date: Wed, 17 Jun 2026 13:32:14 +0800 Subject: [PATCH 15/53] fix ci bug --- Jenkinsfile | 33 ++++++++++++++++++++++----------- Makefile | 7 ++++++- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 38af869d7..a1304dbc8 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -82,6 +82,21 @@ def matrixCheckName() { return (env.BID ?: '').toString() } +/** Marker file: only finish checks that published IN_PROGRESS. */ +def githubCheckStartedMarker(String checkName) { + def safe = checkName.replace('/', '__').replaceAll(/[^A-Za-z0-9_.-]/, '_') + return "${env.WORKSPACE}/.jenkins/check-started/${safe}" +} + +def markGithubCheckStarted(String checkName) { + def marker = githubCheckStartedMarker(checkName) + sh "mkdir -p '${env.WORKSPACE}/.jenkins/check-started' && : > '${marker}'" +} + +def isGithubCheckStarted(String checkName) { + return fileExists(githubCheckStartedMarker(checkName)) +} + def publishGithubCheckInProgress(String checkName) { publishChecks( name: checkName, @@ -91,6 +106,7 @@ def publishGithubCheckInProgress(String checkName) { conclusion: 'NONE', detailsURL: "${env.RUN_DISPLAY_URL ?: env.BUILD_URL}", ) + markGithubCheckStarted(checkName) } def publishGithubCheckCompleted(String checkName, String conclusion) { @@ -118,6 +134,10 @@ def publishMatrixCheckCompleted(String conclusion) { } def finishGithubCheck(String checkName, String buildResult) { + if (!isGithubCheckStarted(checkName)) { + echo "Skip GitHub check completion for '${checkName}' (in-progress was never published)" + return + } def conclusion = [ 'SUCCESS' : 'SUCCESS', 'FAILURE' : 'FAILURE', @@ -397,17 +417,8 @@ pipeline { } post { - success { - script { finishGithubCheck(matrixCheckName(), 'SUCCESS') } - } - failure { - script { finishGithubCheck(matrixCheckName(), 'FAILURE') } - } - unstable { - script { finishGithubCheck(matrixCheckName(), 'UNSTABLE') } - } - aborted { - script { finishGithubCheck(matrixCheckName(), 'ABORTED') } + always { + script { finishGithubCheck(matrixCheckName(), currentBuild.currentResult) } } } } diff --git a/Makefile b/Makefile index 36d798205..4fb832a86 100644 --- a/Makefile +++ b/Makefile @@ -78,7 +78,7 @@ COLOR_RESET := $(shell tput sgr0) kconfig_python := tools/kconfig/.venv/bin/python # Targets -.PHONY: all elf disa run gdb monitor clean tools rootfs vscode ci-run defconfig menuconfig savedefconfig ensure_config clean_check kconfig_venv check-hv-mem-overlap +.PHONY: all elf disa run gdb monitor clean tools rootfs vscode ci-run defconfig menuconfig savedefconfig ensure_config clean_check kconfig_venv link_board check-hv-mem-overlap kconfig_venv: @if [ ! -x $(kconfig_python) ]; then \ echo "$(COLOR_YELLOW)Creating tools/kconfig/.venv (kconfiglib)...$(COLOR_RESET)"; \ @@ -104,8 +104,13 @@ ensure_config: clean_check: ensure_config +link_board: + @mkdir -p src/platform + @ln -sfn ../../platform/$(ARCH)/$(BOARD)/board.rs src/platform/__board.rs + defconfig: kconfig_venv @$(kconfig_python) tools/kconfig/kconfig_cli.py defconfig + @$(MAKE) --no-print-directory link_board menuconfig: kconfig_venv @$(kconfig_python) tools/kconfig/kconfig_cli.py menuconfig From 2953f013bffe7f90411a5d4cfdef89c1b0ddd270 Mon Sep 17 00:00:00 2001 From: dallas Date: Wed, 17 Jun 2026 14:43:50 +0800 Subject: [PATCH 16/53] fmt --- src/event.rs | 7 +------ src/zone.rs | 6 +----- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/event.rs b/src/event.rs index fd53c92a9..7b147e161 100644 --- a/src/event.rs +++ b/src/event.rs @@ -114,12 +114,7 @@ pub fn check_events() -> bool { true } Some(IPI_EVENT_DWC_MSI_INJECT) => { - #[cfg(all( - target_arch = "aarch64", - irq_gicv3, - dwc_pcie, - dwc_msi - ))] + #[cfg(all(target_arch = "aarch64", irq_gicv3, dwc_pcie, dwc_msi))] { crate::pci::dwc_msi::handle_dwc_msi_inject_event(); } diff --git a/src/zone.rs b/src/zone.rs index 8fb83a51e..b9e68bc87 100644 --- a/src/zone.rs +++ b/src/zone.rs @@ -715,11 +715,7 @@ impl ZoneInner { rootcomplex_config.ecam_size as usize, )); } - #[cfg(not(any( - ecam_pcie, - dwc_pcie, - loongarch64_pcie - )))] + #[cfg(not(any(ecam_pcie, dwc_pcie, loongarch64_pcie)))] { warn!( "No extend config found for base 0x{:x}", From 5b50eeec048df0ce8b15bc62e59fb1bdfd5d8de1 Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 30 Jun 2026 15:59:53 +0800 Subject: [PATCH 17/53] update ci for hardware test --- .gitignore | 1 + Jenkinsfile | 2 +- jenkins/check_serial.sh | 28 ++++ jenkins/ci.yaml | 3 +- jenkins/ci_runner.py | 236 ++++++++++++++++--------------- jenkins/prepare.sh | 6 + jenkins/terminal.py | 304 ++++++++++++++++++---------------------- 7 files changed, 293 insertions(+), 287 deletions(-) create mode 100755 jenkins/check_serial.sh diff --git a/.gitignore b/.gitignore index 087d952d0..c5d0b3981 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ src/platform/__board.rs # systemtest rootfs1.zip* jenkins/__pycache__ +/logs/ jenkins-cli.jar tools/kconfig/.venv/ tools/kconfig/__pycache__/ diff --git a/Jenkinsfile b/Jenkinsfile index a1304dbc8..1535dfeb2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -188,7 +188,7 @@ pipeline { always { echo "=== DEBUG: Branch ${env.BRANCH_NAME} ===" echo "=== DEBUG: Commit ${env.GIT_COMMIT} ===" - deleteDir() + sh 'rm -rf .matrix' } } diff --git a/jenkins/check_serial.sh b/jenkins/check_serial.sh new file mode 100755 index 000000000..2f2fdb7d8 --- /dev/null +++ b/jenkins/check_serial.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# Non-interactive serial check for inner zone virtio-console (/dev/pts/x). +# Usage: check_serial.sh + +set -eu + +pts_dev=${1:?pts device required} +log_file=${2:?log file required} +shift 2 + +: > "$log_file" + +stty -F "$pts_dev" 115200 cs8 -cstopb -parenb -ixon -ixoff -echo 2>/dev/null \ + || stty -F "$pts_dev" raw -echo 2>/dev/null \ + || true + +{ + printf '%s\r\n' "$*" + sleep 3 + timeout 10 cat "$pts_dev" 2>/dev/null || true +} >> "$log_file" 2>&1 + +if [ ! -s "$log_file" ]; then + echo "check_serial: no output from $pts_dev" >> "$log_file" + exit 1 +fi + +exit 0 diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index b23c396e0..b5724c852 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -14,6 +14,7 @@ bids: tests: mode: qemu uboot_cmd: bootm 0x40400000 - 0x40000000 + uboot_ready_pattern: => cases: - zone0_start - zone1_start @@ -38,6 +39,6 @@ bids: mode: qemu cases: - zone0_start - - zone1_start + - zone1_start diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 6036ff9c4..e6f5d59c9 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -17,6 +17,23 @@ CaseFunc = Callable[[dict[str, Any], Terminal | None], int] +def bid_log_key(bid: str) -> str: + return bid.replace("/", "__") + + +def jenkins_workspace_root(fallback: Path) -> Path: + workspace = os.environ.get("WORKSPACE", "").strip() + if workspace: + return Path(workspace) + return fallback + + +def logs_dir(cfg: dict[str, Any]) -> Path: + root = jenkins_workspace_root(cfg["workspace"]) + path = root / "logs" / bid_log_key(cfg["bid"]) + path.mkdir(parents=True, exist_ok=True) + return path + def wait_qemu_socket(path: str, timeout: float = 30.0) -> None: deadline = time.monotonic() + timeout @@ -51,80 +68,48 @@ def terminate_managed_process(cfg: dict[str, Any]) -> None: pass -def run_and_print(term: Terminal, command: str) -> str: - output = term.send_until_quiet(command, quiet_seconds=1.0, max_duration=40.0) - if output: - print(output, end="", flush=True) - return output - - -def run_and_print_quiet( - term: Terminal, - command: str, - quiet_seconds: float = 1.0, - max_duration: float = 30.0, - check_exit: bool = True, -) -> tuple[str, int]: - # Send a leading Enter to synchronize shell prompt state. - # term.send("\n") - # _ = term.read_for(duration=0.2) - - output, rc = term.run_until_quiet_with_status( - command, - quiet_seconds=quiet_seconds, - max_duration=max_duration, - ) - if output: - print(output, end="", flush=True) - if check_exit and rc != 0: - raise TerminalCommandError(f"command failed with rc={rc}: {command}") - return output, rc - - -def run_and_print_quiet_raw( - term: Terminal, - command: str, - quiet_seconds: float = 1.0, - max_duration: float = 30.0, -) -> str: - # For non-shell environments (e.g. U-Boot), do not append shell-style - # status markers; just send and wait for output to go quiet. - output = term.send_until_quiet( - command, - quiet_seconds=quiet_seconds, - max_duration=max_duration, - ) - if output: - print(output, end="", flush=True) - return output - - -def read_and_print_until_quiet( - term: Terminal, - quiet_seconds: float = 3.0, - max_duration: float = 120.0, -) -> str: - # Read side is decoupled from send side for interactive boot flows. - output = term.read_until_quiet( - quiet_seconds=quiet_seconds, - max_duration=max_duration, - ) - if output: - print(output, end="", flush=True) - return output +def parse_pts(output: str) -> list[int]: + return sorted({int(match) for match in re.findall(r"/dev/pts/(\d+)", output)}) + +def find_zone1_pts(term: Terminal) -> int: + """List virtio-console pts devices; retry once on failure.""" + for attempt in range(2): + _, pts_output = term.run( + f"zone1_pts_{attempt}", + "ls -1 /dev/pts/[0-9]*", + timeout=15.0, + ) + pts_numbers = parse_pts(pts_output) + if pts_numbers: + return pts_numbers[-1] + if attempt == 0: + time.sleep(2.0) + raise TerminalCommandError("failed to find numeric pts from 'ls -1 /dev/pts/[0-9]*'") + + +def ensure_qemu_terminal(cfg: dict[str, Any], log_path: Path) -> Terminal: + term = cfg.get("_qemu_term") + if term is None: + term = build_terminal(cfg, log_path) + term.open() + cfg["_qemu_term"] = term + cfg["_zone0_log_path"] = log_path + return term -def run_and_print_send_only( - term: Terminal, - command: str, - read_duration: float = 0.5, -) -> str: - # For commands that switch interactive context (e.g. screen attach), - # only send and collect a short best-effort echo. - output = term.send_and_drain(command, read_duration=read_duration) - if output: - print(output, end="", flush=True) - return output + +def close_qemu_terminal(cfg: dict[str, Any]) -> None: + term = cfg.get("_qemu_term") + if term is not None: + term.close() + cfg["_qemu_term"] = None + + +def save_inner_serial_log(cfg: dict[str, Any], content: str) -> None: + if not content: + return + path = logs_dir(cfg) / "zone1_inner_serial.log" + path.write_text(content, encoding="utf-8") def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: @@ -135,22 +120,21 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: cfg["_managed_proc"] = proc cfg["_managed_proc_name"] = "qemu ci-run" wait_qemu_socket(cfg["socket_path"], timeout=30.0) - with build_terminal(cfg) as qemu_term: - bid = cfg["bid"] - if bid == "aarch64/qemu-gicv3": - _ = read_and_print_until_quiet( - qemu_term, - quiet_seconds=3.0, - max_duration=10.0, - ) - qemu_term.send("bootm 0x40400000 - 0x40000000") - if bid == "x86_64/qemu": - time.sleep(10.0) - _ = read_and_print_until_quiet( - qemu_term, - quiet_seconds=5, - max_duration=180.0, - ) + + log_path = logs_dir(cfg) / "zone0_console.log" + log_path.write_text("", encoding="utf-8") + + qemu_term = ensure_qemu_terminal(cfg, log_path) + uboot_cmd = cfg.get("uboot_cmd", "") + uboot_ready = cfg.get("uboot_ready_pattern", "") + if uboot_cmd: + if not uboot_ready: + uboot_ready = r"*=>" + if not qemu_term.wait_pattern(uboot_ready, timeout=10.0): + raise TerminalTimeoutError("timed out waiting for U-Boot prompt") + qemu_term.send(uboot_cmd) + if not qemu_term.wait_pattern(r"buildroot|login:|# ", timeout=180.0): + raise TerminalTimeoutError("timed out waiting for zone0 boot") return 0 if cfg["mode"] == "board": # TODO: reboot board @@ -162,31 +146,29 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: print("————————————————\ncase: zone1_start\n————————————————\n", flush=True) if term is None: raise SystemExit("terminal backend is required") - # _ = run_and_print_quiet_raw(term, "bash", quiet_seconds=1.0, max_duration=15.0) - _, _ = run_and_print_quiet(term, "cd /root", quiet_seconds=1.0, max_duration=15.0) - _, _ = run_and_print_quiet(term, "ls", quiet_seconds=1.0, max_duration=15.0) - _, _ = run_and_print_quiet(term, "cat boot_zone1.sh", quiet_seconds=1.0, max_duration=15.0) - _, boot_rc = run_and_print_quiet( - term, - "./boot_zone1.sh", - quiet_seconds=15, - max_duration=30.0, + + inner_log = "/tmp/zone1_inner.log" + _, _ = term.run("zone1_cd", "cd /root", timeout=15.0) + _, _ = term.run("zone1_ls", "ls", timeout=15.0) + _, _ = term.run("zone1_cat_boot", "cat boot_zone1.sh", timeout=15.0) + boot_rc, _ = term.run("zone1_boot", "./boot_zone1.sh", timeout=120.0) + _, _ = term.run("zone1_list", "./hvisor zone list", timeout=15.0) + + max_pts = find_zone1_pts(term) + + check_rc, _ = term.run( + "zone1_serial_check", + f"./check_serial.sh /dev/pts/{max_pts} {inner_log} ls", + timeout=30.0, ) - _, _ = run_and_print_quiet(term, "./hvisor zone list", quiet_seconds=1.0, max_duration=15.0) - if cfg["arch"] != "x86_64": - _ = run_and_print_quiet_raw(term, "script /dev/null", quiet_seconds=1.0, max_duration=15.0) - pts_output, _ = run_and_print_quiet(term, "ls -1 /dev/pts/[0-9]*", quiet_seconds=1.0, max_duration=15.0) - pts_numbers = sorted(int(match) for match in re.findall(r"/dev/pts/(\d+)", pts_output)) - if not pts_numbers: - raise TerminalCommandError("failed to find numeric pts from 'ls -1 /dev/pts/[0-9]*'") - max_pts = pts_numbers[-1] - _ = run_and_print_send_only(term, f"screen /dev/pts/{max_pts}", read_duration=20.0) - _ = run_and_print_send_only(term, "\n", read_duration=2.0) - _, _ = run_and_print_quiet(term, "ls", quiet_seconds=1.0, max_duration=15.0) + _, inner_output = term.run("zone1_inner_log", f"cat {inner_log}", timeout=15.0) + save_inner_serial_log(cfg, inner_output) + if boot_rc != 0: - raise TerminalCommandError(f"command failed with rc={boot_rc}: sh ./boot_zone1.sh") - else: - print("zone1_started successfully", flush=True) + raise TerminalCommandError(f"command failed with rc={boot_rc}: ./boot_zone1.sh") + if check_rc != 0: + raise TerminalCommandError(f"command failed with rc={check_rc}: check_serial.sh") + print("zone1_started successfully", flush=True) return 0 @@ -218,23 +200,26 @@ def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: if not cases: raise SystemExit(f"no test cases configured for bid '{args.bid}'") + cell_root = Path.cwd() return { "bid": args.bid, "arch": arch, "board": board, "mode": mode, "cases": cases, - "workspace": Path(__file__).resolve().parent.parent, - "socket_path": str((Path(__file__).resolve().parent.parent / ".qemu" / "qemu.sock").resolve()), + "workspace": cell_root, + "socket_path": str((cell_root / ".qemu" / "qemu.sock").resolve()), "serial_port": str(tests.get("serial", "/dev/null")), "baudrate": int(tests.get("baudrate", 1500000)), + "uboot_cmd": str(tests.get("uboot_cmd", "")).strip(), + "uboot_ready_pattern": str(tests.get("uboot_ready_pattern", "")).strip(), } -def build_terminal(cfg: dict[str, Any]) -> Terminal: +def build_terminal(cfg: dict[str, Any], log_path: Path) -> Terminal: if cfg["mode"] == "qemu": - return Terminal.from_qemu_socket(path=cfg["socket_path"]) - return Terminal.from_serial(port=cfg["serial_port"], baudrate=cfg["baudrate"]) + return Terminal.from_qemu_socket(path=cfg["socket_path"], log_path=log_path) + return Terminal.from_serial(port=cfg["serial_port"], baudrate=cfg["baudrate"], log_path=log_path) def main() -> int: @@ -253,8 +238,20 @@ def main() -> int: return rc time.sleep(5.0) continue - - with build_terminal(cfg) as term: + + term = cfg.get("_qemu_term") + if term is None: + log_path = logs_dir(cfg) / "zone1_console.log" + log_path.write_text("", encoding="utf-8") + with build_terminal(cfg, log_path) as term: + try: + rc = case_fn(cfg, term) + except (TerminalTimeoutError, TerminalCommandError) as exc: + print(f"[ci_runner] terminal command failed in case '{case_name}': {exc}", flush=True) + return 1 + if rc != 0: + return rc + else: try: rc = case_fn(cfg, term) except (TerminalTimeoutError, TerminalCommandError) as exc: @@ -262,9 +259,10 @@ def main() -> int: return 1 if rc != 0: return rc - time.sleep(5.0) + time.sleep(5.0) return 0 finally: + close_qemu_terminal(cfg) terminate_managed_process(cfg) diff --git a/jenkins/prepare.sh b/jenkins/prepare.sh index ad0f3d1a9..56bd41af9 100644 --- a/jenkins/prepare.sh +++ b/jenkins/prepare.sh @@ -106,6 +106,12 @@ fi cp "${ZONE1_BOOT_SCRIPT}" "${ROOTFS_DIR}/root/" +CHECK_SERIAL_SCRIPT="${WORKSPACE_ROOT}/jenkins/check_serial.sh" +if [ -f "${CHECK_SERIAL_SCRIPT}" ]; then + cp "${CHECK_SERIAL_SCRIPT}" "${ROOTFS_DIR}/root/" + chmod +x "${ROOTFS_DIR}/root/check_serial.sh" +fi + if [ -f "${ROOTFS_DIR}/root/boot_zone1.sh" ]; then chmod +x "${ROOTFS_DIR}/root/boot_zone1.sh" fi diff --git a/jenkins/terminal.py b/jenkins/terminal.py index 33a96c4aa..ba273eb60 100644 --- a/jenkins/terminal.py +++ b/jenkins/terminal.py @@ -3,16 +3,22 @@ from __future__ import annotations +import re import select import socket +import threading import time import uuid -import re from abc import ABC, abstractmethod from dataclasses import dataclass +from pathlib import Path import serial +RUN_RESULT_RE = re.compile( + r"__CI_RUN_RESULT__ case=(?P[^\s]+) run_id=(?P[a-f0-9]+) rc=(?P\d+)" +) + class TerminalTimeoutError(TimeoutError): """Raised when terminal command wait times out.""" @@ -116,46 +122,141 @@ def write(self, data: bytes) -> None: self._serial.flush() +class LogCollector: + """Background reader that writes terminal output to a log file and console.""" + + def __init__( + self, + backend: TerminalBackend, + log_path: Path, + encoding: str = "utf-8", + console: bool = True, + poll_interval: float = 0.05, + ) -> None: + self.backend = backend + self.log_path = log_path + self.encoding = encoding + self.console = console + self.poll_interval = poll_interval + self._lock = threading.Lock() + self._buffer = "" + self._stop = threading.Event() + self._thread: threading.Thread | None = None + + def start(self) -> None: + self.log_path.parent.mkdir(parents=True, exist_ok=True) + self._thread = threading.Thread(target=self._run_loop, name="LogCollector", daemon=True) + self._thread.start() + + def stop(self) -> None: + self._stop.set() + if self._thread is not None: + self._thread.join(timeout=5.0) + self._thread = None + # Drain any bytes still buffered in the backend after the reader exits. + deadline = time.monotonic() + 1.0 + while time.monotonic() < deadline: + chunk = self.backend.read() + if not chunk: + break + self._append(chunk.decode(self.encoding, errors="replace")) + + def offset(self) -> int: + with self._lock: + return len(self._buffer) + + def tail_since(self, offset: int) -> str: + with self._lock: + if offset < 0 or offset > len(self._buffer): + return self._buffer + return self._buffer[offset:] + + def text(self) -> str: + with self._lock: + return self._buffer + + def _append(self, chunk: str) -> None: + if not chunk: + return + with self._lock: + self._buffer += chunk + try: + with self.log_path.open("a", encoding=self.encoding) as fh: + fh.write(chunk) + except OSError: + pass + if self.console: + print(chunk, end="", flush=True) + + def _run_loop(self) -> None: + while not self._stop.is_set(): + chunk = self.backend.read() + if chunk: + self._append(chunk.decode(self.encoding, errors="replace")) + continue + time.sleep(self.poll_interval) + + class Terminal: - """High level terminal wrapper with command helpers.""" + """High level terminal wrapper with log-file-driven command helpers.""" - def __init__(self, backend: TerminalBackend, encoding: str = "utf-8") -> None: + def __init__( + self, + backend: TerminalBackend, + log_path: Path, + encoding: str = "utf-8", + console: bool = True, + ) -> None: self.backend = backend self.encoding = encoding + self._collector = LogCollector(backend, log_path, encoding=encoding, console=console) self._opened = False @classmethod def from_qemu_socket( cls, path: str, + log_path: Path, connect_timeout: float = 10.0, io_timeout: float = 0.2, encoding: str = "utf-8", + console: bool = True, ) -> "Terminal": return cls( QemuSocketBackend(path=path, connect_timeout=connect_timeout, io_timeout=io_timeout), + log_path=log_path, encoding=encoding, + console=console, ) @classmethod def from_serial( cls, port: str, + log_path: Path, baudrate: int = 115200, timeout: float = 0.2, encoding: str = "utf-8", + console: bool = True, ) -> "Terminal": - return cls(SerialBackend(port=port, baudrate=baudrate, timeout=timeout), encoding=encoding) + return cls( + SerialBackend(port=port, baudrate=baudrate, timeout=timeout), + log_path=log_path, + encoding=encoding, + console=console, + ) def open(self) -> None: if self._opened: return self.backend.open() + self._collector.start() self._opened = True def close(self) -> None: if not self._opened: return + self._collector.stop() self.backend.close() self._opened = False @@ -171,185 +272,56 @@ def send(self, command: str) -> None: payload = command.rstrip("\n") + "\n" self.backend.write(payload.encode(self.encoding, errors="replace")) - def read_for( - self, - duration: float = 2.0, - poll_interval: float = 0.05, - ) -> str: - self._ensure_open() - deadline = time.monotonic() + duration - chunks: list[str] = [] - while time.monotonic() < deadline: - chunk = self.backend.read() - if chunk: - chunks.append(chunk.decode(self.encoding, errors="replace")) - continue - time.sleep(poll_interval) - return "".join(chunks) - - def send_until_get( + def run( self, + case: str, command: str, timeout: float = 30.0, poll_interval: float = 0.05, - include_marker_line: bool = False, - ) -> str: + ) -> tuple[int, str]: + """Run a shell command and wait for __CI_RUN_RESULT__ in the log.""" self._ensure_open() - marker = f"__HV_TERMINAL_DONE_{uuid.uuid4().hex}__" - self.send(f"{command}; echo {marker}") + run_id = uuid.uuid4().hex + offset = self._collector.offset() + wrapped = f"{command}; echo __CI_RUN_RESULT__ case={case} run_id={run_id} rc=$?" + self.send(wrapped) deadline = time.monotonic() + timeout - buf = "" - while time.monotonic() < deadline: - chunk = self.backend.read() - if chunk: - buf += chunk.decode(self.encoding, errors="replace") - if marker in buf: - if include_marker_line: - return buf - return self._trim_after_marker(buf, marker) - time.sleep(poll_interval) - raise TerminalTimeoutError(f"timed out waiting for terminal marker: {marker}") - - def send_until_quiet( - self, - command: str, - quiet_seconds: float = 1.0, - max_duration: float = 30.0, - poll_interval: float = 0.05, - ) -> str: - self._ensure_open() - self.send(command) - - start = time.monotonic() - deadline = start + max_duration - last_output_at = start - buf = "" - - while True: - now = time.monotonic() - if now >= deadline: - raise TerminalTimeoutError( - f"timed out waiting for terminal quiet period after command: {command}" - ) - - chunk = self.backend.read() - if chunk: - buf += chunk.decode(self.encoding, errors="replace") - last_output_at = time.monotonic() - continue - - if (now - last_output_at) >= quiet_seconds: - return buf - time.sleep(poll_interval) - - def send_and_drain( - self, - command: str, - read_duration: float = 0.5, - poll_interval: float = 0.05, - ) -> str: - """Send command and collect best-effort output for a fixed duration.""" - self._ensure_open() - self.send(command) - - deadline = time.monotonic() + read_duration - buf = "" + run_id_needle = f"run_id={run_id}" while time.monotonic() < deadline: - chunk = self.backend.read() - if chunk: - buf += chunk.decode(self.encoding, errors="replace") - continue + chunk = self._collector.tail_since(offset) + if run_id_needle in chunk: + matches = list(RUN_RESULT_RE.finditer(chunk)) + for match in reversed(matches): + if match.group("run_id") == run_id: + rc = int(match.group("rc")) + output = chunk[: match.start()] + return rc, output time.sleep(poll_interval) - return buf - def read_until_quiet( - self, - quiet_seconds: float = 3.0, - max_duration: float = 120.0, - poll_interval: float = 0.05, - ) -> str: - """Continuously read until quiet for x seconds or total timeout.""" - self._ensure_open() - start = time.monotonic() - deadline = start + max_duration - last_output_at = start - buf = "" - - while time.monotonic() < deadline: - chunk = self.backend.read() - if chunk: - buf += chunk.decode(self.encoding, errors="replace") - last_output_at = time.monotonic() - continue - - now = time.monotonic() - if (now - last_output_at) >= quiet_seconds: - return buf - time.sleep(poll_interval) - return buf + raise TerminalTimeoutError( + f"timed out waiting for run result (case={case}, run_id={run_id}): {command}" + ) - def run_until_quiet_with_status( + def wait_pattern( self, - command: str, - quiet_seconds: float = 1.0, - max_duration: float = 30.0, + pattern: str, + timeout: float = 120.0, poll_interval: float = 0.05, - ) -> tuple[str, int]: - marker = f"__HV_TERMINAL_RC_{uuid.uuid4().hex}__" - wrapped = f"{command}; echo {marker}0" + from_offset: int | None = None, + ) -> bool: + """Wait until regex pattern appears in the collected log.""" self._ensure_open() - self.send(wrapped) - - deadline = time.monotonic() + max_duration - buf = "" - marker_pattern = re.compile(re.escape(marker) + r"(\d+)") - rc = -1 - marker_seen_at = 0.0 - + offset = self._collector.offset() if from_offset is None else from_offset + compiled = re.compile(pattern) + deadline = time.monotonic() + timeout while time.monotonic() < deadline: - chunk = self.backend.read() - if chunk: - buf += chunk.decode(self.encoding, errors="replace") - matches = list(marker_pattern.finditer(buf)) - if matches: - last = matches[-1] - rc = int(last.group(1)) - if marker_seen_at <= 0.0: - marker_seen_at = time.monotonic() - continue - - if marker_seen_at > 0.0 and (time.monotonic() - marker_seen_at) >= quiet_seconds: - cleaned = self._strip_status_marker(buf, marker) - return cleaned, rc - + chunk = self._collector.tail_since(offset) + if compiled.search(chunk): + return True time.sleep(poll_interval) - - raise TerminalTimeoutError(f"timed out waiting for command status marker: {marker}") + return False def _ensure_open(self) -> None: if not self._opened: self.open() - - @staticmethod - def _trim_after_marker(output: str, marker: str) -> str: - idx = output.find(marker) - if idx < 0: - return output - return output[:idx] - - @staticmethod - def _extract_status_marker(output: str, marker: str) -> int: - pattern = re.compile(re.escape(marker) + r"(\d+)") - matches = list(pattern.finditer(output)) - if not matches: - raise TerminalTimeoutError(f"status marker without exit code: {marker}") - return int(matches[-1].group(1)) - - @staticmethod - def _strip_status_marker(output: str, marker: str) -> str: - pattern = re.compile(re.escape(marker) + r"\d+") - matches = list(pattern.finditer(output)) - if not matches: - return output - return output[: matches[-1].start()] From b5df18820932fff37b4423a7862dff7fa6332439 Mon Sep 17 00:00:00 2001 From: dallas Date: Wed, 1 Jul 2026 11:00:29 +0800 Subject: [PATCH 18/53] fix boot for rk3568 --- platform/aarch64/rk3568/board.rs | 4 ++-- platform/aarch64/rk3568/kconfig/defconfig | 1 + platform/aarch64/rk3568/scripts/boot_zone1.sh | 3 +-- platform/aarch64/rk3568/scripts/uboot_cmd | 2 +- src/zone.rs | 16 ++++++++++++++++ 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/platform/aarch64/rk3568/board.rs b/platform/aarch64/rk3568/board.rs index 6b3182d6e..b4500a0ae 100644 --- a/platform/aarch64/rk3568/board.rs +++ b/platform/aarch64/rk3568/board.rs @@ -359,6 +359,6 @@ pub const ROOT_DWC_ATU_CONFIG: &[HvDwcAtuConfig] = &[HvDwcAtuConfig { }]; pub const ROOT_PCI_DEVS: [HvPciDevConfig; 2] = [ - pci_dev!(0x0, 0x00, 0x0, 0x0 => 0x00, 0x0, 0x0, VpciDevType::Physical), - pci_dev!(0x0, 0x01, 0x0, 0x0 => 0x01, 0x0, 0x0, VpciDevType::Physical), + pci_dev!(0x1, 0x00, 0x0, 0x0 => 0x00, 0x0, 0x0, VpciDevType::Physical), + pci_dev!(0x1, 0x01, 0x0, 0x0 => 0x01, 0x0, 0x0, VpciDevType::Physical), ]; diff --git a/platform/aarch64/rk3568/kconfig/defconfig b/platform/aarch64/rk3568/kconfig/defconfig index 212ee3ba3..5c49af8a2 100644 --- a/platform/aarch64/rk3568/kconfig/defconfig +++ b/platform/aarch64/rk3568/kconfig/defconfig @@ -4,3 +4,4 @@ CONFIG_NO_PCIE_BAR_REALLOC=y CONFIG_PCI=y CONFIG_PCIE_DWC=y CONFIG_UART_16550=y +CONFIG_PCI_INIT_DELAY=y diff --git a/platform/aarch64/rk3568/scripts/boot_zone1.sh b/platform/aarch64/rk3568/scripts/boot_zone1.sh index 6de678558..b881d4954 100644 --- a/platform/aarch64/rk3568/scripts/boot_zone1.sh +++ b/platform/aarch64/rk3568/scripts/boot_zone1.sh @@ -7,5 +7,4 @@ rm nohup.out mkdir -p /dev/pts mount -t devpts devpts /dev/pts nohup ./hvisor virtio start virtio.json & -./hvisor zone start linux2.json && \ -cat nohup.out | grep "char device" \ No newline at end of file +./hvisor zone start linux2.json \ No newline at end of file diff --git a/platform/aarch64/rk3568/scripts/uboot_cmd b/platform/aarch64/rk3568/scripts/uboot_cmd index 731ecc3a9..cbbb1f06b 100644 --- a/platform/aarch64/rk3568/scripts/uboot_cmd +++ b/platform/aarch64/rk3568/scripts/uboot_cmd @@ -1 +1 @@ -pci enum;setenv serverip 192.168.0.1; setenv ipaddr 192.168.0.2; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:hvisor.bin; tftp ${fdt_addr} ${serverip}:rk3568_limit_zone0.dtb; tftp ${zone0_kernel_addr} ${serverip}:Image_test5; bootm ${loadaddr} - ${fdt_addr}; +pci enum;setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.240; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:hvisor.bin; tftp ${fdt_addr} ${serverip}:rk3568_limit_zone0.dtb; tftp ${zone0_kernel_addr} ${serverip}:Image; bootm ${loadaddr} - ${fdt_addr}; diff --git a/src/zone.rs b/src/zone.rs index b9e68bc87..9aefa419a 100644 --- a/src/zone.rs +++ b/src/zone.rs @@ -569,7 +569,9 @@ impl ZoneInner { pci_rootcomplex_config: &[HvPciConfig; CONFIG_PCI_BUS_MAXNUM], _num_pci_config: usize, ) { + use crate::memory::mmio_generic_handler; use crate::pci::pci_handler::mmio_vpci_handler_dbi; + use crate::platform; for rootcomplex_config in pci_rootcomplex_config { if rootcomplex_config.ecam_base == 0 { @@ -584,6 +586,20 @@ impl ZoneInner { mmio_vpci_handler_dbi, encoded_arg, ); + + let extend_config = platform::ROOT_DWC_ATU_CONFIG + .iter() + .find(|cfg| cfg.ecam_base == rootcomplex_config.ecam_base); + if let Some(extend_config) = extend_config { + if extend_config.apb_base != 0 && extend_config.apb_size != 0 { + self.mmio_region_register( + extend_config.apb_base as usize, + extend_config.apb_size as usize, + mmio_generic_handler, + extend_config.apb_base as usize, + ); + } + } } } From d1b7bca71cc5bc599940bc0e51bb6f035aa3d427 Mon Sep 17 00:00:00 2001 From: dallas Date: Wed, 1 Jul 2026 11:59:06 +0800 Subject: [PATCH 19/53] add rk3568 test --- Jenkinsfile | 12 +++++++-- jenkins/board_power.sh | 32 ++++++++++++++++++++++++ jenkins/ci.yaml | 26 +++++++++++--------- jenkins/ci_runner.py | 56 +++++++++++++++++++++++++++++++++++++++--- 4 files changed, 109 insertions(+), 17 deletions(-) create mode 100755 jenkins/board_power.sh diff --git a/Jenkinsfile b/Jenkinsfile index 1535dfeb2..911d9f920 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -385,8 +385,16 @@ pipeline { "${prepareScript}" """ } else if (mode == 'board') { - // Placeholder for future board artifact distribution by network. - echo "Board prepare placeholder [BID=${env.BID}]" + def tftpDir = (testsCfg.tftp_dir ?: '/home/light/tftp').toString() + echo "Deploy TFTP artifacts [BID=${env.BID}, TFTP_DIR=${tftpDir}]" + sh """ + TFTP_DIR="${tftpDir}" + mkdir -p "\$TFTP_DIR" + ${toolchainPathShell()} + make cp ARCH=${arch} BOARD=${board} MODE=release TFTP_DIR="\$TFTP_DIR" + cp platform/${arch}/${board}/image/dts/rk3568_limit_zone0.dtb "\$TFTP_DIR/" + cp ${kdir}/arch/arm64/boot/Image "\$TFTP_DIR/" + """ } else { error("jenkins/ci.yaml BID=${env.BID}: unsupported tests.mode='${mode}'") } diff --git a/jenkins/board_power.sh b/jenkins/board_power.sh new file mode 100755 index 000000000..0eb7a28bc --- /dev/null +++ b/jenkins/board_power.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# Relay power control for board CI (socat hex frames). +# Usage: board_power.sh off|on|cycle +# cycle: off, wait 3s, on + +set -eu + +action=${1:?action required (off|on|cycle)} +port=${2:?serial port required, e.g. /dev/ttyUSB1} + +send_frame() { + frame=$1 + printf '%b' "$frame" | sudo socat - "$port" +} + +case "$action" in + off) + send_frame '\xA0\x04\x00\xA4' + ;; + on) + send_frame '\xA0\x04\x01\xA5' + ;; + cycle) + send_frame '\xA0\x04\x00\xA4' + sleep 3 + send_frame '\xA0\x04\x01\xA5' + ;; + *) + echo "usage: $0 off|on|cycle " >&2 + exit 1 + ;; +esac diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index b5724c852..1bf125b4f 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -19,18 +19,20 @@ bids: - zone0_start - zone1_start - # - bid: aarch64/rk3568 - # build_args: - # - KDIR=/home/light/DEMO/sdk/rk356x-up4-2c/kernel - # tests: - # mode: board - # serial: /dev/serial/by-id/usb-1a86_USB2.0-Ser_-if00-port0 - # baudrate: 115200 - # uboot_cmd: pci enum;setenv serverip 192.168.0.1; setenv ipaddr 192.168.0.2; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:hvisor.bin; tftp ${fdt_addr} ${serverip}:rk3568_limit_zone0.dtb; tftp ${zone0_kernel_addr} ${serverip}:Image_test5; bootm ${loadaddr} - ${fdt_addr}; - # cases: - # - zone0_start - # - zone1_start - + - bid: aarch64/rk3568 + build_args: + - KDIR=/home/light/DEMO/sdk/rk356x-up4-2c/kernel + tests: + mode: board + serial: /dev/ttyUSB0 + power_serial: /dev/ttyUSB1 + baudrate: 1500000 + uboot_ready_pattern: => + uboot_cmd: pci enum;setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.240; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:hvisor.bin; tftp ${fdt_addr} ${serverip}:rk3568_limit_zone0.dtb; tftp ${zone0_kernel_addr} ${serverip}:Image; bootm ${loadaddr} - ${fdt_addr}; + tftp_dir: /home/light/tftp + cases: + - zone0_start + - zone1_start - bid: x86_64/qemu build_args: diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index e6f5d59c9..1a0257b9e 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -105,6 +105,36 @@ def close_qemu_terminal(cfg: dict[str, Any]) -> None: cfg["_qemu_term"] = None +def close_board_terminal(cfg: dict[str, Any]) -> None: + term = cfg.get("_board_term") + if term is not None: + term.close() + cfg["_board_term"] = None + + +def get_active_terminal(cfg: dict[str, Any]) -> Terminal | None: + return cfg.get("_qemu_term") or cfg.get("_board_term") + + +def close_active_terminal(cfg: dict[str, Any]) -> None: + close_qemu_terminal(cfg) + close_board_terminal(cfg) + + +def board_power_script(cfg: dict[str, Any]) -> Path: + return cfg["workspace"] / "jenkins" / "board_power.sh" + + +def board_power_cycle(cfg: dict[str, Any]) -> None: + power_port = str(cfg.get("power_serial", "")).strip() + if not power_port: + return + script = board_power_script(cfg) + if not script.is_file(): + raise SystemExit(f"board power script not found: {script}") + subprocess.run(["bash", str(script), "cycle", power_port], check=True, cwd=cfg["workspace"]) + + def save_inner_serial_log(cfg: dict[str, Any], content: str) -> None: if not content: return @@ -137,7 +167,25 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: raise TerminalTimeoutError("timed out waiting for zone0 boot") return 0 if cfg["mode"] == "board": - # TODO: reboot board + board_power_cycle(cfg) + + log_path = logs_dir(cfg) / "zone0_console.log" + log_path.write_text("", encoding="utf-8") + + board_term = build_terminal(cfg, log_path) + board_term.open() + cfg["_board_term"] = board_term + + uboot_cmd = cfg.get("uboot_cmd", "") + uboot_ready = cfg.get("uboot_ready_pattern", "") + if uboot_cmd: + if not uboot_ready: + uboot_ready = r"*=>" + if not board_term.wait_pattern(uboot_ready, timeout=60.0): + raise TerminalTimeoutError("timed out waiting for U-Boot prompt") + board_term.send(uboot_cmd) + if not board_term.wait_pattern(r"buildroot|login:|# ", timeout=180.0): + raise TerminalTimeoutError("timed out waiting for zone0 boot") return 0 return 0 @@ -210,9 +258,11 @@ def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: "workspace": cell_root, "socket_path": str((cell_root / ".qemu" / "qemu.sock").resolve()), "serial_port": str(tests.get("serial", "/dev/null")), + "power_serial": str(tests.get("power_serial", "")).strip(), "baudrate": int(tests.get("baudrate", 1500000)), "uboot_cmd": str(tests.get("uboot_cmd", "")).strip(), "uboot_ready_pattern": str(tests.get("uboot_ready_pattern", "")).strip(), + "tftp_dir": str(tests.get("tftp_dir", "/home/light/tftp")).strip(), } @@ -239,7 +289,7 @@ def main() -> int: time.sleep(5.0) continue - term = cfg.get("_qemu_term") + term = get_active_terminal(cfg) if term is None: log_path = logs_dir(cfg) / "zone1_console.log" log_path.write_text("", encoding="utf-8") @@ -262,7 +312,7 @@ def main() -> int: time.sleep(5.0) return 0 finally: - close_qemu_terminal(cfg) + close_active_terminal(cfg) terminate_managed_process(cfg) From 6bec4f4cc70b14ad1ee2ade9a169ef200a3b327a Mon Sep 17 00:00:00 2001 From: XingyuChen <124949998+dallasxy@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:17:05 +0800 Subject: [PATCH 20/53] refactor(build): migrate platform options from Cargo features to Kconfig (#319) --- .github/workflows/ci.yml | 20 +- .gitignore | 7 +- CHANGELOG.md | 2 +- Cargo.toml | 61 --- Jenkinsfile | 493 +++++++++++------- Makefile | 94 ++-- build.rs | 179 +++++-- jenkins/ci.yaml | 8 - jenkins/ci_config.py | 7 + jenkins/ci_runner.py | 15 +- kconfig/Kconfig | 242 +++++++++ kconfig/cfg_map.toml | 41 ++ platform/aarch64/imx8mp/cargo/features | 8 - platform/aarch64/imx8mp/kconfig/defconfig | 9 + platform/aarch64/imx8mp/platform.mk | 2 +- platform/aarch64/ok6254-c/cargo/features | 2 - platform/aarch64/ok6254-c/kconfig/defconfig | 3 + platform/aarch64/phytium-pi/cargo/features | 2 - platform/aarch64/phytium-pi/kconfig/defconfig | 3 + platform/aarch64/phytium-pi/platform.mk | 2 +- platform/aarch64/qemu-gicv2/cargo/features | 4 - platform/aarch64/qemu-gicv2/kconfig/defconfig | 7 + platform/aarch64/qemu-gicv3/cargo/features | 6 - platform/aarch64/qemu-gicv3/kconfig/defconfig | 8 + platform/aarch64/rk3568/cargo/features | 5 - platform/aarch64/rk3568/kconfig/defconfig | 6 + platform/aarch64/rk3588/cargo/features | 2 - platform/aarch64/rk3588/kconfig/defconfig | 3 + platform/aarch64/sysoul_x3300/cargo/features | 2 - .../aarch64/sysoul_x3300/kconfig/defconfig | 3 + platform/aarch64/zcu102/cargo/features | 2 - platform/aarch64/zcu102/kconfig/defconfig | 3 + platform/loongarch64/ls3a5000/cargo/features | 7 - .../loongarch64/ls3a5000/kconfig/defconfig | 7 + platform/loongarch64/ls3a6000/cargo/features | 7 - .../loongarch64/ls3a6000/kconfig/defconfig | 7 + .../hifive-premier-p550/cargo/features | 4 - .../hifive-premier-p550/kconfig/defconfig | 5 + platform/riscv64/megrez/cargo/features | 2 - platform/riscv64/megrez/kconfig/defconfig | 3 + platform/riscv64/qemu-aia/cargo/features | 7 - platform/riscv64/qemu-aia/kconfig/defconfig | 9 + platform/riscv64/qemu-plic/README.md | 2 +- platform/riscv64/qemu-plic/cargo/features | 5 - platform/riscv64/qemu-plic/kconfig/defconfig | 6 + platform/riscv64/ur-dp1000/cargo/features | 2 - platform/riscv64/ur-dp1000/kconfig/defconfig | 3 + platform/x86_64/ecx-2300f-peg/board.rs | 2 +- platform/x86_64/ecx-2300f-peg/cargo/features | 2 - .../x86_64/ecx-2300f-peg/kconfig/defconfig | 4 + platform/x86_64/nuc14mnk/board.rs | 2 +- platform/x86_64/nuc14mnk/cargo/features | 6 - platform/x86_64/nuc14mnk/kconfig/defconfig | 7 + platform/x86_64/qemu/board.rs | 2 +- platform/x86_64/qemu/cargo/features | 5 - platform/x86_64/qemu/kconfig/defconfig | 7 + src/arch/riscv64/cpu.rs | 6 +- src/arch/riscv64/ipi.rs | 8 +- src/arch/riscv64/trap.rs | 6 +- src/arch/x86_64/boot.rs | 4 +- src/arch/x86_64/cpu.rs | 2 +- src/arch/x86_64/entry.rs | 2 +- src/arch/x86_64/graphics.rs | 12 +- src/arch/x86_64/pio.rs | 2 +- src/arch/x86_64/s2pt.rs | 2 +- src/device/iommu/arm_smmu/smmu_hw.rs | 4 +- src/device/iommu/iommu_impl.rs | 8 +- src/device/iommu/mod.rs | 18 +- src/device/iommu/riscv_iommu/iommu_hw.rs | 16 +- src/device/iommu/riscv_iommu/mod.rs | 16 +- src/device/irqchip/aia/vimsic.rs | 4 +- src/device/irqchip/gicv3/mod.rs | 8 +- src/device/irqchip/gicv3/vgic.rs | 2 +- src/device/irqchip/ls7a2000/chip.rs | 2 +- src/device/irqchip/mod.rs | 50 +- src/device/irqchip/pic/mod.rs | 2 +- src/device/irqchip/plic/plic.rs | 2 +- src/device/mod.rs | 4 +- src/device/uart/mod.rs | 26 +- src/device/uart/uart16550a.rs | 2 +- src/event.rs | 17 +- src/hypercall/mod.rs | 2 +- src/logging.rs | 8 +- src/main.rs | 6 +- src/pci/config_accessors/mod.rs | 14 +- src/pci/dwc_msi.rs | 28 +- src/pci/mod.rs | 2 +- src/pci/msix.rs | 2 +- src/pci/pci_config.rs | 88 ++-- src/pci/pci_handler.rs | 70 +-- src/pci/pci_struct.rs | 26 +- src/pci/pci_test.rs | 4 +- src/pci/vpci_dev/mod.rs | 4 +- src/pci/vpci_dev/virtio_cap.rs | 6 +- src/platform/mod.rs | 2 +- src/zone.rs | 76 ++- tools/gen_cargo_config.sh | 60 --- tools/gen_vscode_settings.sh | 62 --- tools/kconfig/bootstrap_venv.sh | 19 + tools/kconfig/host_config.sh | 121 +++++ tools/kconfig/kconfig_cli.py | 164 ++++++ tools/kconfig/requirements.txt | 2 + tools/kconfig/save_defconfig.sh | 17 + tools/read_features.sh | 12 - 104 files changed, 1512 insertions(+), 872 deletions(-) create mode 100644 kconfig/Kconfig create mode 100644 kconfig/cfg_map.toml delete mode 100644 platform/aarch64/imx8mp/cargo/features create mode 100644 platform/aarch64/imx8mp/kconfig/defconfig delete mode 100644 platform/aarch64/ok6254-c/cargo/features create mode 100644 platform/aarch64/ok6254-c/kconfig/defconfig delete mode 100644 platform/aarch64/phytium-pi/cargo/features create mode 100644 platform/aarch64/phytium-pi/kconfig/defconfig delete mode 100644 platform/aarch64/qemu-gicv2/cargo/features create mode 100644 platform/aarch64/qemu-gicv2/kconfig/defconfig delete mode 100644 platform/aarch64/qemu-gicv3/cargo/features create mode 100644 platform/aarch64/qemu-gicv3/kconfig/defconfig delete mode 100644 platform/aarch64/rk3568/cargo/features create mode 100644 platform/aarch64/rk3568/kconfig/defconfig delete mode 100644 platform/aarch64/rk3588/cargo/features create mode 100644 platform/aarch64/rk3588/kconfig/defconfig delete mode 100644 platform/aarch64/sysoul_x3300/cargo/features create mode 100644 platform/aarch64/sysoul_x3300/kconfig/defconfig delete mode 100644 platform/aarch64/zcu102/cargo/features create mode 100644 platform/aarch64/zcu102/kconfig/defconfig delete mode 100644 platform/loongarch64/ls3a5000/cargo/features create mode 100644 platform/loongarch64/ls3a5000/kconfig/defconfig delete mode 100644 platform/loongarch64/ls3a6000/cargo/features create mode 100644 platform/loongarch64/ls3a6000/kconfig/defconfig delete mode 100644 platform/riscv64/hifive-premier-p550/cargo/features create mode 100644 platform/riscv64/hifive-premier-p550/kconfig/defconfig delete mode 100644 platform/riscv64/megrez/cargo/features create mode 100644 platform/riscv64/megrez/kconfig/defconfig delete mode 100644 platform/riscv64/qemu-aia/cargo/features create mode 100644 platform/riscv64/qemu-aia/kconfig/defconfig delete mode 100644 platform/riscv64/qemu-plic/cargo/features create mode 100644 platform/riscv64/qemu-plic/kconfig/defconfig delete mode 100644 platform/riscv64/ur-dp1000/cargo/features create mode 100644 platform/riscv64/ur-dp1000/kconfig/defconfig delete mode 100644 platform/x86_64/ecx-2300f-peg/cargo/features create mode 100644 platform/x86_64/ecx-2300f-peg/kconfig/defconfig delete mode 100644 platform/x86_64/nuc14mnk/cargo/features create mode 100644 platform/x86_64/nuc14mnk/kconfig/defconfig delete mode 100644 platform/x86_64/qemu/cargo/features create mode 100644 platform/x86_64/qemu/kconfig/defconfig delete mode 100755 tools/gen_cargo_config.sh delete mode 100755 tools/gen_vscode_settings.sh create mode 100755 tools/kconfig/bootstrap_venv.sh create mode 100755 tools/kconfig/host_config.sh create mode 100644 tools/kconfig/kconfig_cli.py create mode 100644 tools/kconfig/requirements.txt create mode 100755 tools/kconfig/save_defconfig.sh delete mode 100755 tools/read_features.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce91f8d2a..7972f545f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,15 +5,15 @@ name: CI on: - push: - branches: - - main - - dev - pull_request: - branches: - - main - - dev - workflow_dispatch: + # push: + # branches: + # - main + # - dev + # pull_request: + # branches: + # - main + # - dev + # workflow_dispatch: env: MODE: debug jobs: @@ -31,7 +31,7 @@ jobs: - name: Install Dependencies run: | sudo apt-get update - sudo apt-get install -y qemu-system-aarch64 qemu-system-riscv64 gdb-multiarch llvm-dev libclang-dev wget + sudo apt-get install -y qemu-system-aarch64 qemu-system-riscv64 gdb-multiarch llvm-dev libclang-dev wget python3-venv cargo install --version 0.3.0 --locked cargo-binutils - name: Format Check run: make fmt-test diff --git a/.gitignore b/.gitignore index d2f815798..087d952d0 100644 --- a/.gitignore +++ b/.gitignore @@ -32,10 +32,13 @@ flash.img* .vscode/settings.json compile.sh src/platform/__board.rs -.config +.config* .cursorrules .vscode/extensions.json # systemtest rootfs1.zip* jenkins/__pycache__ -jenkins-cli.jar \ No newline at end of file +jenkins-cli.jar +tools/kconfig/.venv/ +tools/kconfig/__pycache__/ +kernel_build/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c3b5d3a9b..66abbcf71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,7 +53,7 @@ - [platform] **aarch64**: Improve QEMU GICv2/GICv3 configurations and add zone1-linux support. ([agicy](https://github.com/agicy)) - [platform] **loongarch64**: Add support for Loongson 3A5 / 3A6 platforms and improve clock and trap handling. ([wheatfox](https://github.com/enkerewpo)) - [feature] Add **aarch32** support. ([Guowei Li](https://github.com/KouweiLee)) -- [feature] **riscv64** enhancements: g-stage dynamic detection, eic770x_soc, and syscrg emulation. ([Jingyu Liu](https://github.com/liulog)) +- [feature] **riscv64** enhancements: g-stage dynamic detection, hypervisor_v0_6 (EIC770x SoC), and syscrg emulation. ([Jingyu Liu](https://github.com/liulog)) - [infra] Unify UART / MPIDR mapping, centralize IOMMU configuration, remove redundant arch feature flags, and tidy Cargo/zone/hypercall code. ([Nehckl](https://github.com/Inquisitor-201), [Ren HangQi](https://github.com/ForeverYolo)) - [infra/tool] **aarch64**: Optimized the structure of GIC parameters ([Ren HangQi](https://github.com/ForeverYolo)) - [ci/misc] Update dependencies, add ccache support, and improve build/tooling workflows. ([Jingyu Liu](https://github.com/liulog)) diff --git a/Cargo.toml b/Cargo.toml index a19c43266..e26316e0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,67 +47,6 @@ acpi = "=5.2.0" uguid = "=2.2.0" uefi-raw = "=0.9.0" -[features] -############# general ############## -iommu = [] # supported by: aarch64, riscv64 -pci = [] # supported by: aarch64, riscv64,loongarch64 -sriov = ["pci"] # SR-IOV PF/VF enumeration and passthrough -print_timestamp = [] # print timestamp when logging - -############# IOMMU ############## -riscv_iommu = ["iommu"] -arm_smmu = ["iommu"] -intel_vtd = ["iommu"] -share_s2pt = ["iommu"] -viommu = ["iommu"] # supported by: riscv64 - -############# PCIe access mechanism ############## -ecam_pcie = [] # Standard ECAM mechanism (default for most platforms) -dwc_pcie = [] # DesignWare PCIe Core mechanism (CFG0/CFG1, used by RK3568) -loongarch64_pcie = [] # LoongArch PCIe mechanism (used by LoongArch platforms) -no_pcie_bar_realloc = [] -dwc_msi = ["dwc_pcie"] # DesignWare Native MSI (used when arch doesn't provide MSI, e.g., ARM without ITS) -pci_init_delay = ["dwc_pcie"] # Delay hvisor PCI init until guest DBI offset 0 access -virtio_pci = [] # Virtual virtio pci support - -############# aarch64 ############## -# irqchip driver -gicv2 = [] -gicv3 = [] - -# uart driver -pl011 = [] -xuartps = [] -imx_uart = [] -uart_16550 = [] -sifive_ccache = [] -eic7700_sysreg = [] -uart16550a = [] - -############## riscv64 ############# -# irqchip driver -plic = [] -dp1000_plic = [] -aia = [] -aclint = [] -# extensions -sstc = [] -# platform specific -eic770x_soc = [] - -########### loongarch64 ############ -# irqchip driver -loongson_7a2000 = [] -# uart driver -loongson_uart = [] -# cpu (not used for now) -loongson_3a5000 = [] -loongson_3a6000 = [] - -############# x86_64 ############### -graphics = [] -split_screen = [] - [profile.dev] # panic = "abort" # avoid cargo test failure, this is a bug of cargo debug = 2 diff --git a/Jenkinsfile b/Jenkinsfile index 1d95e4d49..a1304dbc8 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,3 +1,12 @@ +/** ARCH/BOARD come from matrix BID (arch/board); ci.yaml must not duplicate them. */ +def parseBid(String bid) { + def parts = (bid ?: '').split('/', 2) + if (parts.size() != 2 || !parts[0] || !parts[1]) { + error("invalid BID: ${bid}") + } + return [arch: parts[0], board: parts[1]] +} + def parseCiBuildArgs(cfg) { def buildArgs = [:] if (!cfg?.build_args) { @@ -23,6 +32,23 @@ def matrixCellDir() { return "${env.WORKSPACE}/.matrix/${bid.replace('/', '__')}" } +/** Isolated workspace for top-level CI jobs (linter, license-checker, …). */ +def jenkinsJobDir(String name) { + return "${env.WORKSPACE}/.jenkins/${name}" +} + +/** Copy repo into an isolated dir; excludes other Jenkins sandboxes from the source tree. */ +def syncWorkspaceTo(String destDir) { + sh """ + mkdir -p '${destDir}' + rsync -a --delete \\ + --exclude '.jenkins/' \\ + --exclude '.matrix/' \\ + --exclude '.jenkins-matrix/' \\ + '${env.WORKSPACE}/' '${destDir}/' + """ +} + def loadCiYaml() { def data = readYaml file: 'jenkins/ci.yaml' def bids = data?.bids @@ -51,6 +77,106 @@ def normalizeToolArch(String arch) { return mapping.get(raw, raw) } +/** GitHub Check name for this matrix cell */ +def matrixCheckName() { + return (env.BID ?: '').toString() +} + +/** Marker file: only finish checks that published IN_PROGRESS. */ +def githubCheckStartedMarker(String checkName) { + def safe = checkName.replace('/', '__').replaceAll(/[^A-Za-z0-9_.-]/, '_') + return "${env.WORKSPACE}/.jenkins/check-started/${safe}" +} + +def markGithubCheckStarted(String checkName) { + def marker = githubCheckStartedMarker(checkName) + sh "mkdir -p '${env.WORKSPACE}/.jenkins/check-started' && : > '${marker}'" +} + +def isGithubCheckStarted(String checkName) { + return fileExists(githubCheckStartedMarker(checkName)) +} + +def publishGithubCheckInProgress(String checkName) { + publishChecks( + name: checkName, + title: checkName, + summary: 'In progress', + status: 'IN_PROGRESS', + conclusion: 'NONE', + detailsURL: "${env.RUN_DISPLAY_URL ?: env.BUILD_URL}", + ) + markGithubCheckStarted(checkName) +} + +def publishGithubCheckCompleted(String checkName, String conclusion) { + def summaries = [ + 'SUCCESS' : 'Passed', + 'FAILURE' : 'Failed', + 'CANCELLED': 'Cancelled', + ] + publishChecks( + name: checkName, + title: checkName, + summary: summaries.get(conclusion, conclusion), + status: 'COMPLETED', + conclusion: conclusion, + detailsURL: "${env.RUN_DISPLAY_URL ?: env.BUILD_URL}", + ) +} + +def publishMatrixCheckInProgress() { + publishGithubCheckInProgress(matrixCheckName()) +} + +def publishMatrixCheckCompleted(String conclusion) { + publishGithubCheckCompleted(matrixCheckName(), conclusion) +} + +def finishGithubCheck(String checkName, String buildResult) { + if (!isGithubCheckStarted(checkName)) { + echo "Skip GitHub check completion for '${checkName}' (in-progress was never published)" + return + } + def conclusion = [ + 'SUCCESS' : 'SUCCESS', + 'FAILURE' : 'FAILURE', + 'UNSTABLE': 'FAILURE', + 'ABORTED' : 'CANCELLED', + 'NOT_BUILT': 'CANCELLED', + ].get(buildResult ?: '', 'FAILURE') + publishGithubCheckCompleted(checkName, conclusion) +} + +def hasCiTests() { + return getBidConfig(loadCiYaml(), env.BID) != null +} + +def toolchainPathShell() { + return "export PATH=${env.CARGO_HOME}/bin:${env.TOOLCHAIN_PATHS}:\$PATH" +} + +def qemuPathShell() { + return "export PATH=${env.QEMU_PATH}:\$PATH" +} + +/** Kconfig: symlink agent venv + make defconfig. Keep in sync with Makefile kconfig_python path. */ +def kconfigSetupShell(String arch, String board) { + return """ + ${toolchainPathShell()} + chmod +x tools/kconfig/host_config.sh tools/kconfig/save_defconfig.sh 2>/dev/null || true + if [ ! -x tools/kconfig/.venv/bin/python ]; then + if [ ! -x ${env.KCONFIG_VENV}/bin/python ]; then + echo "ERROR: CI kconfig venv missing: ${env.KCONFIG_VENV}/bin/python" >&2 + exit 1 + fi + mkdir -p tools/kconfig + ln -sfn ${env.KCONFIG_VENV} tools/kconfig/.venv + fi + make defconfig ARCH=${arch} BOARD=${board} + """ +} + pipeline { agent any @@ -62,6 +188,7 @@ pipeline { always { echo "=== DEBUG: Branch ${env.BRANCH_NAME} ===" echo "=== DEBUG: Commit ${env.GIT_COMMIT} ===" + deleteDir() } } @@ -72,6 +199,7 @@ pipeline { CARGO_HOME = '/usr/local/cargo' QEMU_PATH = '/home/light/DEMO/qemu-9.2.3/build' TEST_IMG_BASE = '/home/light/DEMO/syswonder/test_img' + KCONFIG_VENV = "/home/light/DEMO/syswonder/test_img/venv" RISCV_TOOLCHAIN_PATH = '/home/light/DEMO/toolchain/riscv64-glibc-ubuntu-24.04-gcc' AARCH64_TOOLCHAIN_PATH = '/home/light/DEMO/toolchain/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu' LOONGARCH64_TOOLCHAIN_PATH = '/home/light/DEMO/toolchain/loongarch_cross_tools' @@ -80,214 +208,219 @@ pipeline { } stages { - stage('CI') { - when { - anyOf { - branch 'main' - branch 'dev' - allOf { - changeRequest() - anyOf { - expression { return (env.CHANGE_TARGET ?: '') == 'main' } - expression { return (env.CHANGE_TARGET ?: '') == 'dev' } - } + stage('Linter') { + steps { + script { + publishGithubCheckInProgress('linter') + def cellWs = jenkinsJobDir('linter') + syncWorkspaceTo(cellWs) + dir(cellWs) { + sh kconfigSetupShell('aarch64', 'qemu-gicv3') + ''' + cargo fmt --all -- --check + ''' + } + } + } + post { + always { + script { finishGithubCheck('linter', currentBuild.currentResult) } + } + } + } + + stage('License checker') { + steps { + script { + publishGithubCheckInProgress('license-checker') + def cellWs = jenkinsJobDir('license-checker') + syncWorkspaceTo(cellWs) + dir(cellWs) { + sh """ + chmod +x tools/license_checker.sh + ./tools/license_checker.sh + """ } } } - stages { - stage('Checkout') { - steps { - // Ensure no stale files from previous builds. - deleteDir() - checkout scm + post { + always { + script { finishGithubCheck('license-checker', currentBuild.currentResult) } + } + } + } + + stage('Multi-Platform Matrix') { + matrix { + axes { + axis { + name 'BID' + values( + 'aarch64/imx8mp', + 'aarch64/ok6254-c', + 'aarch64/phytium-pi', + 'aarch64/qemu-gicv2', + 'aarch64/qemu-gicv3', + 'aarch64/rk3568', + 'aarch64/rk3588', + 'aarch64/sysoul_x3300', + 'aarch64/zcu102', + 'loongarch64/ls3a5000', + 'loongarch64/ls3a6000', + 'riscv64/hifive-premier-p550', + 'riscv64/megrez', + 'riscv64/qemu-aia', + 'riscv64/qemu-plic', + 'riscv64/ur-dp1000', + 'x86_64/ecx-2300f-peg', + 'x86_64/nuc14mnk', + 'x86_64/qemu', + ) } } - stage('Multi-Platform Matrix') { - matrix { - axes { - axis { - name 'BID' - values( - 'aarch64/imx8mp', - 'aarch64/ok6254-c', - 'aarch64/phytium-pi', - 'aarch64/qemu-gicv2', - 'aarch64/qemu-gicv3', - 'aarch64/rk3568', - 'aarch64/rk3588', - 'aarch64/sysoul_x3300', - 'aarch64/zcu102', - 'loongarch64/ls3a5000', - 'loongarch64/ls3a6000', - 'riscv64/hifive-premier-p550', - 'riscv64/megrez', - 'riscv64/qemu-aia', - 'riscv64/qemu-plic', - 'riscv64/ur-dp1000', - 'x86_64/ecx-2300f-peg', - 'x86_64/nuc14mnk', - 'x86_64/qemu', - ) + stages { + stage('Prepare cell') { + steps { + script { + publishMatrixCheckInProgress() + syncWorkspaceTo(matrixCellDir()) } } + } - stages { - stage('Prepare cell workspace') { - steps { - script { - def cellWs = matrixCellDir() + stage('Compile') { + steps { + dir(matrixCellDir()) { + script { + def bid = parseBid(env.BID) + def arch = bid.arch + def board = bid.board + echo "Compile hvisor [BID=${env.BID}, ARCH=${arch}, BOARD=${board}]" + sh kconfigSetupShell(arch, board) + if (arch != 'x86_64') { sh """ - mkdir -p '${cellWs}' - rsync -a --delete \\ - --exclude '.matrix/' \\ - --exclude '.jenkins-matrix/' \\ - '${env.WORKSPACE}/' '${cellWs}/' + ${toolchainPathShell()} + make dtb ARCH=${arch} BOARD=${board} """ } + sh """ + ${toolchainPathShell()} + make all ARCH=${arch} BOARD=${board} MODE=release + """ } } - - stage('Compile') { - steps { - dir(matrixCellDir()) { - script { - def parts = (env.BID ?: '').split('/', 2) - if (parts.size() != 2) { - error("invalid BID: ${env.BID}") - } - def arch = parts[0] - def board = parts[1] - echo "Compile hvisor [BID=${env.BID}, ARCH=${arch}, BOARD=${board}]" - if (arch != 'x86_64') { - sh """ - export PATH=${env.CARGO_HOME}/bin:${env.TOOLCHAIN_PATHS}:\$PATH - make dtb ARCH=${arch} BOARD=${board} - """ - } - sh """ - export PATH=${env.CARGO_HOME}/bin:${env.TOOLCHAIN_PATHS}:\$PATH - make all ARCH=${arch} BOARD=${board} MODE=release - """ - } + } + } + + stage('Build hvisor-tool') { + when { + expression { return hasCiTests() } + } + steps { + dir(matrixCellDir()) { + script { + def bidCfg = getBidConfig(loadCiYaml(), env.BID) + def buildArgs = parseCiBuildArgs(bidCfg) + def bidTool = parseBid(env.BID) + def tarch = normalizeToolArch(buildArgs.TARCH ?: bidTool.arch) + def kdir = buildArgs.KDIR + if (!kdir) { + error("jenkins/ci.yaml BID=${env.BID}: build_args must include KDIR") } - } - } - - stage('Build hvisor-tool') { - when { - expression { - return getBidConfig(loadCiYaml(), env.BID) != null + + echo "Build hvisor-tool [BID=${env.BID}, TARCH=${tarch}, KDIR=${kdir}]" + if (!fileExists(env.HVISOR_TOOL_PATH)) { + sh "mkdir -p ${env.HVISOR_TOOL_PATH}" } - } - steps { - dir(matrixCellDir()) { - script { - def ci = loadCiYaml() - def bidCfg = getBidConfig(ci, env.BID) - def buildArgs = parseCiBuildArgs(bidCfg) - def tarch = normalizeToolArch(buildArgs.TARCH ?: buildArgs.ARCH) - def kdir = buildArgs.KDIR - if (!tarch || !kdir) { - error("jenkins/ci.yaml BID=${env.BID}: build_args must include ARCH/TARCH and KDIR") - } - - echo "Build hvisor-tool [BID=${env.BID}, TARCH=${tarch}, KDIR=${kdir}]" - if (!fileExists(env.HVISOR_TOOL_PATH)) { - sh "mkdir -p ${env.HVISOR_TOOL_PATH}" - } - dir(env.HVISOR_TOOL_PATH) { - checkout([ - $class: 'GitSCM', - branches: [[name: '*/main']], - extensions: [[$class: 'CloneOption', depth: 1, noTags: true]], - userRemoteConfigs: [[url: env.HVISOR_TOOL_URL]] - ]) - } - sh """ - export PATH=${env.TOOLCHAIN_PATHS}:\$PATH - cd ${env.HVISOR_TOOL_PATH} - make all ARCH=${tarch} KDIR=${kdir} - """ - } + dir(env.HVISOR_TOOL_PATH) { + checkout([ + $class: 'GitSCM', + branches: [[name: '*/main']], + extensions: [[$class: 'CloneOption', depth: 1, noTags: true]], + userRemoteConfigs: [[url: env.HVISOR_TOOL_URL]] + ]) } + sh """ + export PATH=${env.TOOLCHAIN_PATHS}:\$PATH + make -C ${env.HVISOR_TOOL_PATH} all ARCH=${tarch} KDIR=${kdir} + """ } } - - stage('Prepare test') { - when { - expression { - return getBidConfig(loadCiYaml(), env.BID) != null + } + } + + stage('Prepare test') { + when { + expression { return hasCiTests() } + } + steps { + dir(matrixCellDir()) { + script { + def bidCfg = getBidConfig(loadCiYaml(), env.BID) + def buildArgs = parseCiBuildArgs(bidCfg) + def bidParsed = parseBid(env.BID) + def arch = bidParsed.arch + def board = bidParsed.board + def kdir = (buildArgs.KDIR ?: '').toString() + def testsCfg = bidCfg.tests ?: [:] + def mode = (testsCfg.mode ?: '').toString().trim() + if (!kdir || !mode) { + error("jenkins/ci.yaml BID=${env.BID}: tests.mode and build_args KDIR are required") } - } - steps { - dir(matrixCellDir()) { - script { - def ci = loadCiYaml() - def bidCfg = getBidConfig(ci, env.BID) - def buildArgs = parseCiBuildArgs(bidCfg) - def arch = (buildArgs.ARCH ?: '').toString() - def board = (buildArgs.BOARD ?: '').toString() - def kdir = (buildArgs.KDIR ?: '').toString() - def testsCfg = bidCfg.tests ?: [:] - def mode = (testsCfg.mode ?: '').toString().trim() - if (!arch || !board || !kdir || !mode) { - error("jenkins/ci.yaml BID=${env.BID}: tests.mode and build_args ARCH/BOARD/KDIR are required") - } - - if (mode == 'qemu') { - def prepareScript = "jenkins/prepare.sh" - def externalFile = "${env.TEST_IMG_BASE}/${arch}/${board}" - def configure = "./platform/${arch}/${board}/" - echo "Prepare rootfs [BID=${env.BID}]" - sh """ - cp -r ${externalFile}/* ${configure} - chmod +x "${prepareScript}" - sudo -E env \\ - ARCH="${arch}" \\ - BOARD="${board}" \\ - KDIR="${kdir}" \\ - WORKSPACE_ROOT="\$(pwd)" \\ - HVISOR_TOOL_PATH="${env.HVISOR_TOOL_PATH}" \\ - "${prepareScript}" - """ - } else if (mode == 'board') { - // Placeholder for future board artifact distribution by network. - echo "Board prepare placeholder [BID=${env.BID}]" - } else { - error("jenkins/ci.yaml BID=${env.BID}: unsupported tests.mode='${mode}'") - } - } + + if (mode == 'qemu') { + def prepareScript = "jenkins/prepare.sh" + def externalFile = "${env.TEST_IMG_BASE}/${arch}/${board}" + def configure = "./platform/${arch}/${board}/" + echo "Prepare rootfs [BID=${env.BID}]" + sh """ + cp -r ${externalFile}/* ${configure} + chmod +x "${prepareScript}" + sudo -E env \\ + ARCH="${arch}" \\ + BOARD="${board}" \\ + KDIR="${kdir}" \\ + WORKSPACE_ROOT="\$(pwd)" \\ + HVISOR_TOOL_PATH="${env.HVISOR_TOOL_PATH}" \\ + "${prepareScript}" + """ + } else if (mode == 'board') { + // Placeholder for future board artifact distribution by network. + echo "Board prepare placeholder [BID=${env.BID}]" + } else { + error("jenkins/ci.yaml BID=${env.BID}: unsupported tests.mode='${mode}'") } } } - - stage('Run test cases') { - when { - expression { - return getBidConfig(loadCiYaml(), env.BID) != null - } - } - steps { - dir(matrixCellDir()) { - script { - def ci = loadCiYaml() - def bidCfg = getBidConfig(ci, env.BID) - echo "Run tests via ci_runner [BID=${env.BID}]" - sh """ - export TERM=\${TERM:-xterm} - export PATH=${env.CARGO_HOME}/bin:${env.TOOLCHAIN_PATHS}:\$PATH - export PATH=${env.QEMU_PATH}:\$PATH - python3 jenkins/ci_runner.py \ - --bid "${env.BID}" - """ - } - } + } + } + + stage('Run test cases') { + when { + expression { return hasCiTests() } + } + steps { + dir(matrixCellDir()) { + script { + echo "Run tests via ci_runner [BID=${env.BID}]" + sh """ + export TERM=\${TERM:-xterm} + ${toolchainPathShell()} + ${qemuPathShell()} + python3 jenkins/ci_runner.py \ + --bid "${env.BID}" + """ } } } } } + + post { + always { + script { finishGithubCheck(matrixCheckName(), currentBuild.currentResult) } + } + } } } } diff --git a/Makefile b/Makefile index 55497c849..4fb832a86 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,6 @@ STATS ?= off PORT ?= 2333 MODE ?= release BOARD ?= qemu-gicv3 -FEATURES= BID ?= ptest ?= @@ -23,15 +22,6 @@ else BOARD := $(shell echo $(BID) | cut -d'/' -f2) endif -# if user add FEATURES in environment, we use it -# else we use the default FEATURES in platform/$(ARCH)/$(BOARD)/cargo/features -ifeq ($(FEATURES),) - FEATURES := $(shell ./tools/read_features.sh $(ARCH) $(BOARD) || echo ERROR) - ifeq ($(FEATURES),ERROR) - $(error ERROR: Read FEATURES failed. Please check if ARCH="$(ARCH)" and BOARD="$(BOARD)" are correct.) - endif -endif - ifeq ($(ARCH),aarch64) RUSTC_TARGET := aarch64-unknown-none GDB_ARCH := aarch64 @@ -56,7 +46,6 @@ export ARCH export BOARD export BID export RUSTC_TARGET -export FEATURES # Build paths build_path := target/$(RUSTC_TARGET)/$(MODE) @@ -69,7 +58,6 @@ systemtest_tdownload := $(test_dir)/systemtest/tdownload_all.sh # Build arguments build_args := -build_args += --features "$(FEATURES)" build_args += --target $(RUSTC_TARGET) build_args += -Z build-std=core,alloc build_args += -Z build-std-features=compiler-builtins-mem @@ -86,16 +74,58 @@ COLOR_BLUE := $(shell tput setaf 4) COLOR_BOLD := $(shell tput bold) COLOR_RESET := $(shell tput sgr0) +# Defconfig / menuconfig: see tools/kconfig/kconfig_cli.py +kconfig_python := tools/kconfig/.venv/bin/python + # Targets -.PHONY: all elf disa run gdb monitor clean tools rootfs vscode ci-run check-hv-mem-overlap -all: clean_check gen_cargo_config vscode $(hvisor_bin) check-hv-mem-overlap +.PHONY: all elf disa run gdb monitor clean tools rootfs vscode ci-run defconfig menuconfig savedefconfig ensure_config clean_check kconfig_venv link_board check-hv-mem-overlap +kconfig_venv: + @if [ ! -x $(kconfig_python) ]; then \ + echo "$(COLOR_YELLOW)Creating tools/kconfig/.venv (kconfiglib)...$(COLOR_RESET)"; \ + ./tools/kconfig/bootstrap_venv.sh; \ + fi + +ensure_config: + @CONFIG_ARCH=; CONFIG_BOARD=; \ + if [ -f .config ]; then \ + CONFIG_ARCH=$$(grep '^# ARCH=' .config 2>/dev/null | head -1 | sed 's/^# ARCH=//'); \ + [ -n "$$CONFIG_ARCH" ] || CONFIG_ARCH=$$(grep '^ARCH=' .config | head -1 | cut -d'=' -f2); \ + CONFIG_BOARD=$$(grep '^# BOARD=' .config 2>/dev/null | head -1 | sed 's/^# BOARD=//'); \ + [ -n "$$CONFIG_BOARD" ] || CONFIG_BOARD=$$(grep '^BOARD=' .config | head -1 | cut -d'=' -f2); \ + fi; \ + if [ ! -f .config ]; then \ + echo "$(COLOR_YELLOW)No .config; running defconfig for $(ARCH)/$(BOARD)$(COLOR_RESET)"; \ + $(MAKE) --no-print-directory defconfig; \ + elif [ "$$CONFIG_ARCH" != "$(ARCH)" ] || [ "$$CONFIG_BOARD" != "$(BOARD)" ]; then \ + echo "$(COLOR_YELLOW)$(COLOR_BOLD)ARCH or BOARD changed (OLD: $$CONFIG_ARCH/$$CONFIG_BOARD, NEW: $(ARCH)/$(BOARD)), cleaning and reloading defconfig...$(COLOR_RESET)"; \ + ./tools/clean.sh; \ + $(MAKE) --no-print-directory defconfig; \ + fi + +clean_check: ensure_config + +link_board: + @mkdir -p src/platform + @ln -sfn ../../platform/$(ARCH)/$(BOARD)/board.rs src/platform/__board.rs + +defconfig: kconfig_venv + @$(kconfig_python) tools/kconfig/kconfig_cli.py defconfig + @$(MAKE) --no-print-directory link_board + +menuconfig: kconfig_venv + @$(kconfig_python) tools/kconfig/kconfig_cli.py menuconfig + +savedefconfig: + @./tools/kconfig/save_defconfig.sh "$(ARCH)" "$(BOARD)" + +all: ensure_config gen_cargo_config vscode $(hvisor_bin) check-hv-mem-overlap @printf "\n" @printf "$(COLOR_GREEN)$(COLOR_BOLD)hvisor build summary:$(COLOR_RESET)\n" @printf "%-10s %s\n" "ARCH =" "$(COLOR_BOLD)$(ARCH)$(COLOR_RESET)" @printf "%-10s %s\n" "BOARD =" "$(COLOR_BOLD)$(BOARD)$(COLOR_RESET)" @printf "%-10s %s\n" "BID =" "$(COLOR_BOLD)$(BID)$(COLOR_RESET)" @printf "%-10s %s\n" "LOG =" "$(COLOR_BOLD)$(LOG)$(COLOR_RESET)" - @printf "%-10s %s\n" "FEATURES =" "$(COLOR_BOLD)$(FEATURES)$(COLOR_RESET)" + @printf "%-10s %s\n" "DEFCONFIG =" "$(COLOR_BOLD)platform/$(ARCH)/$(BOARD)/kconfig/defconfig$(COLOR_RESET)" @printf "%-10s %s\n" "RUSTC_TARGET =" "$(COLOR_BOLD)$(RUSTC_TARGET)$(COLOR_RESET)" @printf "%-10s %s\n" "BUILD_PATH =" "$(COLOR_BOLD)$(build_path)$(COLOR_RESET)" @printf "%-10s %s\n" "HVISON_BIN_SIZE =" "$(COLOR_BOLD)$(shell du -h $(hvisor_bin) | cut -f1)$(COLOR_RESET)" @@ -109,21 +139,10 @@ all: clean_check gen_cargo_config vscode $(hvisor_bin) check-hv-mem-overlap @printf "\n" @printf "$(COLOR_GREEN)$(COLOR_BOLD)hvisor build success!$(COLOR_RESET)\n" -clean_check: -# if .config not exist, then everything is fine -# else we read .config and parse ARCH and BOARD, if they are different, we clean the build - @if [ -f ".config" ]; then \ - CONFIG_ARCH=$$(cat .config | grep "ARCH" | cut -d'=' -f2); \ - CONFIG_BOARD=$$(cat .config | grep "BOARD" | cut -d'=' -f2); \ - if [ "$$CONFIG_ARCH" != "$(ARCH)" ] || [ "$$CONFIG_BOARD" != "$(BOARD)" ]; then \ - echo "$(COLOR_YELLOW)$(COLOR_BOLD)ARCH or BOARD changed(OLD: $$CONFIG_ARCH/$$CONFIG_BOARD, NEW: $(ARCH)/$(BOARD)), cleaning...$(COLOR_RESET)"; \ - ./tools/clean.sh; \ - fi; \ - fi - gen_cargo_config: @printf "$(COLOR_GREEN)$(COLOR_BOLD)generating .cargo/config.toml...$(COLOR_RESET)\n" - ./tools/gen_cargo_config.sh + @chmod +x tools/kconfig/host_config.sh 2>/dev/null || true + ./tools/kconfig/host_config.sh cargo @printf "$(COLOR_GREEN)$(COLOR_BOLD)generating .cargo/config.toml success!$(COLOR_RESET)\n" check-hv-mem-overlap: $(hvisor_bin) platform/$(ARCH)/$(BOARD)/board.rs @@ -135,10 +154,11 @@ check-hv-mem-overlap: $(hvisor_bin) platform/$(ARCH)/$(BOARD)/board.rs vscode: @printf "$(COLOR_GREEN)$(COLOR_BOLD)generating .vscode/settings.json...$(COLOR_RESET)\n" - ./tools/gen_vscode_settings.sh + @chmod +x tools/kconfig/host_config.sh 2>/dev/null || true + ./tools/kconfig/host_config.sh vscode @printf "$(COLOR_GREEN)$(COLOR_BOLD)generating .vscode/settings.json success!$(COLOR_RESET)\n" -elf: +elf: ensure_config gen_cargo_config cargo build $(build_args) disa: @@ -174,15 +194,11 @@ test-pre: download-test-img chmod +x platform/$(ARCH)/$(BOARD)/test/runner.sh @echo "added execute permission to test runner.sh for board $(BOARD)" -fmt-test: all - cargo fmt --all -- --check - @echo "cargo fmt check passed!" - fmt: all cargo fmt --all @echo "your code has been formatted" -clippy: +clippy: ensure_config gen_cargo_config cargo clippy $(build_args) flash-img: @@ -198,10 +214,10 @@ download-test-img: else echo "\nflash.img found\n"; \ fi -test: clean test-pre gen_cargo_config +test: clean test-pre ensure_config gen_cargo_config cargo test $(build_args) -vv -stest: clean test-pre gen_cargo_config +stest: clean test-pre ensure_config gen_cargo_config ./platform/$(ARCH)/$(BOARD)/test/systemtest/tcompiledtb.sh ./platform/$(ARCH)/$(BOARD)/test/systemtest/tdownload_all.sh ./platform/$(ARCH)/$(BOARD)/test/systemtest/trootfs_deploy.sh @@ -210,7 +226,7 @@ stest: clean test-pre gen_cargo_config # Performance benchmark: data collection only, does not affect pass/fail. # Calls systemtest scripts for DTS, then perftest/tdownload_all.sh when present # (fallback to systemtest/tdownload_all.sh), and perftest/trootfs_deploy.sh. -perf: clean test-pre gen_cargo_config +perf: clean test-pre ensure_config gen_cargo_config ./platform/$(ARCH)/$(BOARD)/test/systemtest/tcompiledtb.sh @if [ -x "$(perftest_tdownload)" ]; then \ $(perftest_tdownload); \ @@ -222,7 +238,7 @@ perf: clean test-pre gen_cargo_config # Prepare perf image only: reuse existing rootfs img when present, download # missing assets, deploy benchmark scripts/tools into image, and print path. -perf-prepare-img: clean test-pre gen_cargo_config +perf-prepare-img: clean test-pre ensure_config gen_cargo_config ./platform/$(ARCH)/$(BOARD)/test/systemtest/tcompiledtb.sh @if [ -x "$(perftest_tdownload)" ]; then \ $(perftest_tdownload); \ diff --git a/build.rs b/build.rs index 9fdedbbf7..9de7058df 100644 --- a/build.rs +++ b/build.rs @@ -14,6 +14,7 @@ // Authors: // Yulong Han // +use std::collections::{HashMap, HashSet}; use std::io::Write; use std::{env, fs, path::Path}; @@ -29,58 +30,116 @@ fn log(output: &str) { writeln!(file, "{}", output).expect("Failed to write to log file"); } -// .config -// ARCH=... -// BOARD=... struct BuildEnv { arch: String, board: String, bid: String, - features: String, } +// parse ARCH, BOARD and BID from .config fn parse_build_env(file_path: &str) -> BuildEnv { let file = fs::read_to_string(file_path).expect("Failed to read .config file"); let mut arch = String::new(); let mut board = String::new(); let mut bid = String::new(); - let mut features = String::new(); for line in file.lines() { - let parts: Vec<&str> = line.split('=').collect(); - if parts.len() != 2 { + let t = line.trim(); + if let Some(v) = t.strip_prefix("# ARCH=") { + arch = v.to_string(); + continue; + } + if let Some(v) = t.strip_prefix("# BOARD=") { + board = v.to_string(); + continue; + } + if let Some(v) = t.strip_prefix("# BID=") { + bid = v.to_string(); + continue; + } + if t.starts_with('#') { continue; } - match parts[0] { - "ARCH" => arch = parts[1].to_string(), - "BOARD" => board = parts[1].to_string(), - "BID" => bid = parts[1].to_string(), - "FEATURES" => features = parts[1].to_string(), + let Some((k, v)) = t.split_once('=') else { + continue; + }; + match k { + "ARCH" if arch.is_empty() => arch = v.to_string(), + "BOARD" if board.is_empty() => board = v.to_string(), + "BID" if bid.is_empty() => bid = v.to_string(), _ => {} } } - BuildEnv { - arch, - board, - bid, - features, + BuildEnv { arch, board, bid } +} + +fn load_cfg_map(root: &Path) -> HashMap { + let path = root.join("kconfig/cfg_map.toml"); + let raw = fs::read_to_string(&path).unwrap_or_else(|_| panic!("missing {}", path.display())); + let mut m = HashMap::new(); + let mut in_symbols = false; + for line in raw.lines() { + let t = line.trim(); + if t.is_empty() || t.starts_with('#') { + continue; + } + if t == "[symbols]" { + in_symbols = true; + continue; + } + if t.starts_with('[') { + in_symbols = false; + continue; + } + if !in_symbols { + continue; + } + let Some((k, v)) = t.split_once('=') else { + continue; + }; + let k = k.trim().trim_matches('"'); + let v = v.trim().trim_matches('"'); + m.insert(k.to_string(), v.to_string()); + } + if m.is_empty() { + panic!("cfg_map.toml: no entries under [symbols]"); } + m +} + +// parse enabled config keys from .config +fn parse_enabled_config_keys(config_text: &str) -> HashSet { + let mut out = HashSet::new(); + for line in config_text.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let Some((key, val)) = line.split_once('=') else { + continue; + }; + let val = val.trim(); + if val != "y" && val != "m" { + continue; + } + if key.starts_with("CONFIG_") { + out.insert(key.to_string()); + } + } + out } fn main() { - // cleanup the log file let log_path = Path::new(BUILD_LOG_FILE); if log_path.exists() { fs::remove_file(log_path).expect("Failed to remove log file"); } let project_toml_root = env::var("CARGO_MANIFEST_DIR").unwrap(); - // read the .config file at the project root - let config_path = format!("{}/.config", project_toml_root); - let mut build_env = parse_build_env(&config_path); + let root = Path::new(&project_toml_root); + let config_path = root.join(".config"); + let mut build_env = parse_build_env(config_path.to_str().unwrap()); if !build_env.bid.is_empty() { - // BID=$ARCH/$BOARD, parse it - // update the build_env with the parsed values let parts: Vec<&str> = build_env.bid.split('/').collect(); if parts.len() != 2 { log(&format!("Invalid BID format: {}", build_env.bid)); @@ -91,35 +150,45 @@ fn main() { } build_env.arch = parts[0].to_string(); build_env.board = parts[1].to_string(); - } else { - log(&format!( - "BID environment variable not found, using ARCH and BOARD" - )); - if build_env.arch.is_empty() || build_env.board.is_empty() { - log(&format!("ARCH or BOARD environment variable not found")); - panic!( - "ARCH or BOARD environment variable not found, please check the log file({}) for more details", - BUILD_LOG_FILE - ); - } + } else if build_env.arch.is_empty() || build_env.board.is_empty() { + log("ARCH or BOARD missing in .config"); + panic!( + "ARCH or BOARD missing in .config (run: make defconfig), see {}", + BUILD_LOG_FILE + ); } let arch = build_env.arch; let board = build_env.board; let bid = build_env.bid; - let features = build_env.features; let pwd = env::current_dir().unwrap(); log(&format!("Current directory: {}", pwd.display())); + let cfg_map = load_cfg_map(root); + let config_text = fs::read_to_string(&config_path).expect("read .config"); + let enabled = parse_enabled_config_keys(&config_text); + + for rust_cfg in cfg_map.values().collect::>() { + println!("cargo:rustc-check-cfg=cfg({rust_cfg})"); + } + + for k in &enabled { + if let Some(rust_cfg) = cfg_map.get(k) { + println!("cargo:rustc-cfg={rust_cfg}"); + } else if k.starts_with("CONFIG_") { + log(&format!("warning: unknown config key in .config: {k}")); + } + } + let target_path_str = format!("{}/src/platform/__board.rs", pwd.display()); let target_path = Path::new(&target_path_str); let source_path_str = format!("{}/platform/{}/{}/board.rs", pwd.display(), arch, board); let source_path = Path::new(&source_path_str); log(&format!( - "Building for ARCH={} BOARD={}, BID={}, FEATURES={}", - arch, board, bid, features + "Building for ARCH={arch} BOARD={board}, BID={bid}, enabled_cfgs={}", + enabled.len() )); log(&format!( @@ -129,24 +198,40 @@ fn main() { )); if !source_path.exists() { - log(&format!("Invalid board.rs path: {}, make sure the ARCH and BOARD environment variables are set correctly", source_path_str)); - panic!( - "Invalid board.rs, please check the log file({}) for more details", - BUILD_LOG_FILE - ); + log(&format!("Invalid board.rs path: {source_path_str}")); + panic!("Invalid board.rs, please check the log file({BUILD_LOG_FILE}) for more details"); } - // soft link the board.rs to __board.rs if target_path.exists() { fs::remove_file(target_path).expect("Failed to remove existing __board.rs"); } std::os::unix::fs::symlink(source_path, target_path).expect("Failed to create symlink"); - log(&format!("Linking successful")); + log("Linking successful"); println!("cargo:rerun-if-env-changed=ARCH"); println!("cargo:rerun-if-env-changed=BOARD"); println!("cargo:rerun-if-env-changed=BID"); - println!("cargo:rerun-if-env-changed=FEATURES"); - println!("cargo:rerun-if-changed={}", source_path_str); - println!("cargo:rerun-if-changed={}", target_path_str); + println!("cargo:rerun-if-changed={}", config_path.display()); + println!( + "cargo:rerun-if-changed={}", + root.join("kconfig/cfg_map.toml").display() + ); + println!( + "cargo:rerun-if-changed={}", + root.join("kconfig/Kconfig").display() + ); + println!( + "cargo:rerun-if-changed={}", + root.join("tools/kconfig/kconfig_cli.py").display() + ); + println!( + "cargo:rerun-if-changed={}", + root.join("platform") + .join(&arch) + .join(&board) + .join("kconfig/defconfig") + .display() + ); + println!("cargo:rerun-if-changed={source_path_str}"); + println!("cargo:rerun-if-changed={target_path_str}"); } diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index de3b79ba3..b23c396e0 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -1,8 +1,6 @@ bids: - bid: riscv64/qemu-plic build_args: - - ARCH=riscv64 - - BOARD=qemu-plic - KDIR=/home/light/DEMO/linux/linux-6.10 tests: mode: qemu @@ -12,8 +10,6 @@ bids: - bid: aarch64/qemu-gicv3 build_args: - - ARCH=aarch64 - - BOARD=qemu-gicv3 - KDIR=/home/light/DEMO/linux/linux-5.4 tests: mode: qemu @@ -24,8 +20,6 @@ bids: # - bid: aarch64/rk3568 # build_args: - # - ARCH=aarch64 - # - BOARD=rk3568 # - KDIR=/home/light/DEMO/sdk/rk356x-up4-2c/kernel # tests: # mode: board @@ -39,8 +33,6 @@ bids: - bid: x86_64/qemu build_args: - - ARCH=x86_64 - - BOARD=qemu - KDIR=/home/light/DEMO/linux/linux-5.19 tests: mode: qemu diff --git a/jenkins/ci_config.py b/jenkins/ci_config.py index b74521249..f47c5f3c9 100755 --- a/jenkins/ci_config.py +++ b/jenkins/ci_config.py @@ -15,6 +15,13 @@ CI_YAML = ROOT / "jenkins" / "ci.yaml" +def parse_bid(bid: str) -> tuple[str, str]: + parts = bid.strip().split("/", 1) + if len(parts) != 2 or not parts[0].strip() or not parts[1].strip(): + raise ValueError(f"invalid BID: {bid!r}") + return parts[0].strip(), parts[1].strip() + + def normalize_build_args(raw: list[Any] | None) -> dict[str, str]: result: dict[str, str] = {} for item in raw or []: diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index d7d645849..6036ff9c4 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -11,7 +11,7 @@ from pathlib import Path from typing import Any, Callable -from ci_config import get_bid_entry, load_ci +from ci_config import get_bid_entry, load_ci, parse_bid from terminal import Terminal, TerminalCommandError, TerminalTimeoutError @@ -180,7 +180,7 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: if not pts_numbers: raise TerminalCommandError("failed to find numeric pts from 'ls -1 /dev/pts/[0-9]*'") max_pts = pts_numbers[-1] - _ = run_and_print_send_only(term, f"screen /dev/pts/{max_pts}", read_duration=5.0) + _ = run_and_print_send_only(term, f"screen /dev/pts/{max_pts}", read_duration=20.0) _ = run_and_print_send_only(term, "\n", read_duration=2.0) _, _ = run_and_print_quiet(term, "ls", quiet_seconds=1.0, max_duration=15.0) if boot_rc != 0: @@ -205,15 +205,16 @@ def parse_args() -> argparse.Namespace: def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: ci = load_ci() bid_entry = get_bid_entry(ci, args.bid) - build_args = bid_entry["build_args"] tests = bid_entry["tests"] - arch = build_args.get("ARCH", "").strip() - board = build_args.get("BOARD", "").strip() + try: + arch, board = parse_bid(args.bid) + except ValueError as exc: + raise SystemExit(str(exc)) from exc mode = bid_entry.get("mode", "").strip() cases = bid_entry.get("cases", []) - if not arch or not board or not mode: - raise SystemExit(f"incomplete config for bid '{args.bid}': ARCH/BOARD/mode are required") + if not mode: + raise SystemExit(f"incomplete config for bid '{args.bid}': tests.mode is required") if not cases: raise SystemExit(f"no test cases configured for bid '{args.bid}'") diff --git a/kconfig/Kconfig b/kconfig/Kconfig new file mode 100644 index 000000000..ced854fe3 --- /dev/null +++ b/kconfig/Kconfig @@ -0,0 +1,242 @@ +mainmenu "hvisor platform options" + +menu "Target architecture" +choice + prompt "CPU architecture" + default ARCH_AARCH64 + +config ARCH_AARCH64 + bool "aarch64" + +config ARCH_RISCV64 + bool "riscv64" + +config ARCH_LOONGARCH64 + bool "loongarch64" + +config ARCH_X86_64 + bool "x86_64" + +endchoice +endmenu + +menu "Interrupt controller" + depends on ARCH_AARCH64 || ARCH_RISCV64 || ARCH_LOONGARCH64 + +choice + prompt "GIC version" + optional + default IRQ_GICV3 + depends on ARCH_AARCH64 + +config IRQ_GICV2 + bool "GICv2" + +config IRQ_GICV3 + bool "GICv3" + +endchoice + +config PLIC + bool "PLIC" + depends on ARCH_RISCV64 + default n + +config DP1000_PLIC + bool "DP1000 PLIC variant" + depends on ARCH_RISCV64 && PLIC + default n + +config AIA + bool "AIA (APLIC/IMSIC)" + depends on ARCH_RISCV64 + default n + +config ACLINT + bool "ACLINT (MTI/MSI)" + depends on ARCH_RISCV64 + default n + +config LOONGSON_7A2000 + bool "Loongson 7A2000" + depends on ARCH_LOONGARCH64 + default n + +endmenu + +menu "RISC-V options" + depends on ARCH_RISCV64 + +config SSTC + bool "Sstc" + default n + +config HYPERVISOR_V0_6 + bool "Hypervisor v0.6" + default n + +config EIC7700_SYSREG + bool "EIC7700 system registers" + depends on HYPERVISOR_V0_6 + default n + +config SIFIVE_CCACHE + bool "SiFive ccache" + depends on HYPERVISOR_V0_6 + default n + +endmenu + +menu "LoongArch platform" + depends on ARCH_LOONGARCH64 + +config LOONGSON_3A5000 + bool "Loongson 3A5000 CPU profile" + default n + +config LOONGSON_3A6000 + bool "Loongson 3A6000 CPU profile" + default n +endmenu + +menu "UART" + +config PL011 + bool "PL011" + depends on ARCH_AARCH64 + default n + +config IMX_UART + bool "iMX UART" + depends on ARCH_AARCH64 + default n + +config XUARTPS + bool "XUARTPS" + depends on ARCH_AARCH64 + default n + +config UART_16550 + bool "UART_16550" + depends on ARCH_AARCH64 + default n + +config UART16550A + bool "UART16550A" + depends on ARCH_X86_64 + default n + +config LOONGSON_UART + bool "Loongson UART" + depends on ARCH_LOONGARCH64 + default n +endmenu + +menu "PCIe" + +config PCI + bool "PCI/PCIe support" + default n + +choice + prompt "PCIe Architecture" + depends on PCI + default PCIE_ECAM + +config PCIE_ECAM + bool "ECAM PCIe" + +config PCIE_DWC + bool "DesignWare Core PCIe" + +config PCIE_LOONGARCH64 + bool "LoongArch PCIe" + depends on ARCH_LOONGARCH64 + +endchoice + +config NO_PCIE_BAR_REALLOC + bool "Do not reallocate PCIe BARs (board-specific)" + depends on PCI + default n + +config DWC_MSI + bool "DesignWare native MSI" + depends on PCIE_DWC + default n + +config PCI_INIT_DELAY + bool "Delay PCI init until guest DBI offset 0 access" + depends on PCIE_DWC + default n + +config SRIOV + bool "SR-IOV PF/VF enumeration and passthrough" + depends on PCI + default n + +config VIRTIO_PCI + bool "Virtio over PCI" + depends on PCI && IRQ_GICV3 + default n +endmenu + +menu "IOMMU" + +config IOMMU + bool + default n + +config ARM_SMMU + bool "ARM SMMU" + depends on ARCH_AARCH64 + select IOMMU + default n + +config INTEL_VTD + bool "Intel VT-d" + depends on ARCH_X86_64 + select IOMMU + default n + +config RISCV_IOMMU + bool "RISC-V IOMMU" + depends on ARCH_RISCV64 + select IOMMU + default n + +config SHARE_S2PT + bool "Share stage-2 page tables with IOMMU (RISC-V IOMMU only)" + depends on RISCV_IOMMU + default n + +config VIOMMU + bool "Virtual RISC-V IOMMU" + depends on RISCV_IOMMU + default n +endmenu + +menu "x86-specific options" + depends on ARCH_X86_64 + +config GRAPHICS + bool "Framebuffer / graphics" + default n + +config SPLIT_SCREEN + bool "Split screen layout" + depends on GRAPHICS + default n + +config PRINT_TIMESTAMP + bool "Print timestamp in logs" + default n +endmenu + +menu "Debug" + +config EXTIOI_DEBUG + bool "EXTIOI debug (LoongArch LS7A2000)" + depends on ARCH_LOONGARCH64 + default n +endmenu diff --git a/kconfig/cfg_map.toml b/kconfig/cfg_map.toml new file mode 100644 index 000000000..062ba84f7 --- /dev/null +++ b/kconfig/cfg_map.toml @@ -0,0 +1,41 @@ +# Kconfig / .config symbol -> rustc cfg name (must match #[cfg(...)] in source). +# Keys are exactly as in .config (CONFIG_*). +[symbols] +CONFIG_IRQ_GICV2 = "irq_gicv2" +CONFIG_IRQ_GICV3 = "irq_gicv3" +CONFIG_PL011 = "pl011" +CONFIG_IMX_UART = "imx_uart" +CONFIG_XUARTPS = "xuartps" +CONFIG_UART_16550 = "uart_16550" +CONFIG_UART16550A = "uart16550a" +CONFIG_LOONGSON_UART = "loongson_uart" +CONFIG_PCI = "pci" +CONFIG_PCIE_ECAM = "ecam_pcie" +CONFIG_PCIE_DWC = "dwc_pcie" +CONFIG_PCIE_LOONGARCH64 = "loongarch64_pcie" +CONFIG_NO_PCIE_BAR_REALLOC = "no_pcie_bar_realloc" +CONFIG_DWC_MSI = "dwc_msi" +CONFIG_PCI_INIT_DELAY = "pci_init_delay" +CONFIG_SRIOV = "sriov" +CONFIG_VIRTIO_PCI = "virtio_pci" +CONFIG_IOMMU = "iommu" +CONFIG_SHARE_S2PT = "share_s2pt" +CONFIG_ARM_SMMU = "arm_smmu" +CONFIG_INTEL_VTD = "intel_vtd" +CONFIG_RISCV_IOMMU = "riscv_iommu" +CONFIG_VIOMMU = "viommu" +CONFIG_PLIC = "plic" +CONFIG_DP1000_PLIC = "dp1000_plic" +CONFIG_AIA = "aia" +CONFIG_ACLINT = "aclint" +CONFIG_SSTC = "sstc" +CONFIG_HYPERVISOR_V0_6 = "hypervisor_v0_6" +CONFIG_EIC7700_SYSREG = "eic7700_sysreg" +CONFIG_SIFIVE_CCACHE = "sifive_ccache" +CONFIG_LOONGSON_7A2000 = "loongson_7a2000" +CONFIG_LOONGSON_3A5000 = "loongson_3a5000" +CONFIG_LOONGSON_3A6000 = "loongson_3a6000" +CONFIG_GRAPHICS = "graphics" +CONFIG_SPLIT_SCREEN = "split_screen" +CONFIG_PRINT_TIMESTAMP = "print_timestamp" +CONFIG_EXTIOI_DEBUG = "extioi_debug" diff --git a/platform/aarch64/imx8mp/cargo/features b/platform/aarch64/imx8mp/cargo/features deleted file mode 100644 index e2b1a51a6..000000000 --- a/platform/aarch64/imx8mp/cargo/features +++ /dev/null @@ -1,8 +0,0 @@ -gicv3 -imx_uart -pci -dwc_pcie -no_pcie_bar_realloc -dwc_msi -pci_init_delay -sriov \ No newline at end of file diff --git a/platform/aarch64/imx8mp/kconfig/defconfig b/platform/aarch64/imx8mp/kconfig/defconfig new file mode 100644 index 000000000..83416cbda --- /dev/null +++ b/platform/aarch64/imx8mp/kconfig/defconfig @@ -0,0 +1,9 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_IMX_UART=y +CONFIG_IRQ_GICV3=y +CONFIG_PCI=y +CONFIG_PCIE_DWC=y +CONFIG_NO_PCIE_BAR_REALLOC=y +CONFIG_DWC_MSI=y +CONFIG_PCI_INIT_DELAY=y +CONFIG_SRIOV=y diff --git a/platform/aarch64/imx8mp/platform.mk b/platform/aarch64/imx8mp/platform.mk index 5e5809f0f..d4014f9a9 100644 --- a/platform/aarch64/imx8mp/platform.mk +++ b/platform/aarch64/imx8mp/platform.mk @@ -1,6 +1,6 @@ QEMU := qemu-system-aarch64 -ifeq ($(findstring gicv3, $(FEATURES)),gicv3) +ifneq ($(shell grep -Fx CONFIG_IRQ_GICV3=y platform/$(ARCH)/$(BOARD)/kconfig/defconfig 2>/dev/null),) UBOOT := $(image_dir)/bootloader/u-boot-atf.bin zone0_dtb := $(image_dir)/devicetree/linux1.dtb QEMU_ARGS := -machine virt,secure=on,gic-version=3,virtualization=on,iommu=smmuv3 diff --git a/platform/aarch64/ok6254-c/cargo/features b/platform/aarch64/ok6254-c/cargo/features deleted file mode 100644 index afa594897..000000000 --- a/platform/aarch64/ok6254-c/cargo/features +++ /dev/null @@ -1,2 +0,0 @@ -gicv3 -uart_16550 diff --git a/platform/aarch64/ok6254-c/kconfig/defconfig b/platform/aarch64/ok6254-c/kconfig/defconfig new file mode 100644 index 000000000..242faa328 --- /dev/null +++ b/platform/aarch64/ok6254-c/kconfig/defconfig @@ -0,0 +1,3 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_IRQ_GICV3=y +CONFIG_UART_16550=y diff --git a/platform/aarch64/phytium-pi/cargo/features b/platform/aarch64/phytium-pi/cargo/features deleted file mode 100644 index 416c6913e..000000000 --- a/platform/aarch64/phytium-pi/cargo/features +++ /dev/null @@ -1,2 +0,0 @@ -gicv3 -pl011 \ No newline at end of file diff --git a/platform/aarch64/phytium-pi/kconfig/defconfig b/platform/aarch64/phytium-pi/kconfig/defconfig new file mode 100644 index 000000000..f24edf758 --- /dev/null +++ b/platform/aarch64/phytium-pi/kconfig/defconfig @@ -0,0 +1,3 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_IRQ_GICV3=y +CONFIG_PL011=y diff --git a/platform/aarch64/phytium-pi/platform.mk b/platform/aarch64/phytium-pi/platform.mk index 061516bcc..46c773778 100644 --- a/platform/aarch64/phytium-pi/platform.mk +++ b/platform/aarch64/phytium-pi/platform.mk @@ -1,6 +1,6 @@ QEMU := qemu-system-aarch64 -ifeq ($(findstring gicv3, $(FEATURES)),gicv3) +ifneq ($(shell grep -Fx CONFIG_IRQ_GICV3=y platform/$(ARCH)/$(BOARD)/kconfig/defconfig 2>/dev/null),) UBOOT := $(image_dir)/bootloader/u-boot-atf.bin zone0_dtb := $(image_dir)/devicetree/linux1.dtb QEMU_ARGS := -machine virt,secure=on,gic-version=3,virtualization=on,iommu=smmuv3 diff --git a/platform/aarch64/qemu-gicv2/cargo/features b/platform/aarch64/qemu-gicv2/cargo/features deleted file mode 100644 index e0ed82fd3..000000000 --- a/platform/aarch64/qemu-gicv2/cargo/features +++ /dev/null @@ -1,4 +0,0 @@ -gicv2 -pl011 -arm_smmu -pci diff --git a/platform/aarch64/qemu-gicv2/kconfig/defconfig b/platform/aarch64/qemu-gicv2/kconfig/defconfig new file mode 100644 index 000000000..fb8f4ff36 --- /dev/null +++ b/platform/aarch64/qemu-gicv2/kconfig/defconfig @@ -0,0 +1,7 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_ARM_SMMU=y +CONFIG_IOMMU=y +CONFIG_IRQ_GICV2=y +CONFIG_PCI=y +CONFIG_PCIE_ECAM=y +CONFIG_PL011=y diff --git a/platform/aarch64/qemu-gicv3/cargo/features b/platform/aarch64/qemu-gicv3/cargo/features deleted file mode 100644 index cf034a83b..000000000 --- a/platform/aarch64/qemu-gicv3/cargo/features +++ /dev/null @@ -1,6 +0,0 @@ -gicv3 -pl011 -arm_smmu -pci -ecam_pcie -virtio_pci \ No newline at end of file diff --git a/platform/aarch64/qemu-gicv3/kconfig/defconfig b/platform/aarch64/qemu-gicv3/kconfig/defconfig new file mode 100644 index 000000000..674eecc5a --- /dev/null +++ b/platform/aarch64/qemu-gicv3/kconfig/defconfig @@ -0,0 +1,8 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_ARM_SMMU=y +CONFIG_IOMMU=y +CONFIG_IRQ_GICV3=y +CONFIG_PCI=y +CONFIG_PCIE_ECAM=y +CONFIG_PL011=y +CONFIG_VIRTIO_PCI=y diff --git a/platform/aarch64/rk3568/cargo/features b/platform/aarch64/rk3568/cargo/features deleted file mode 100644 index 0b11e0fab..000000000 --- a/platform/aarch64/rk3568/cargo/features +++ /dev/null @@ -1,5 +0,0 @@ -gicv3 -uart_16550 -pci -dwc_pcie -no_pcie_bar_realloc \ No newline at end of file diff --git a/platform/aarch64/rk3568/kconfig/defconfig b/platform/aarch64/rk3568/kconfig/defconfig new file mode 100644 index 000000000..212ee3ba3 --- /dev/null +++ b/platform/aarch64/rk3568/kconfig/defconfig @@ -0,0 +1,6 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_IRQ_GICV3=y +CONFIG_NO_PCIE_BAR_REALLOC=y +CONFIG_PCI=y +CONFIG_PCIE_DWC=y +CONFIG_UART_16550=y diff --git a/platform/aarch64/rk3588/cargo/features b/platform/aarch64/rk3588/cargo/features deleted file mode 100644 index afa594897..000000000 --- a/platform/aarch64/rk3588/cargo/features +++ /dev/null @@ -1,2 +0,0 @@ -gicv3 -uart_16550 diff --git a/platform/aarch64/rk3588/kconfig/defconfig b/platform/aarch64/rk3588/kconfig/defconfig new file mode 100644 index 000000000..242faa328 --- /dev/null +++ b/platform/aarch64/rk3588/kconfig/defconfig @@ -0,0 +1,3 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_IRQ_GICV3=y +CONFIG_UART_16550=y diff --git a/platform/aarch64/sysoul_x3300/cargo/features b/platform/aarch64/sysoul_x3300/cargo/features deleted file mode 100644 index afa594897..000000000 --- a/platform/aarch64/sysoul_x3300/cargo/features +++ /dev/null @@ -1,2 +0,0 @@ -gicv3 -uart_16550 diff --git a/platform/aarch64/sysoul_x3300/kconfig/defconfig b/platform/aarch64/sysoul_x3300/kconfig/defconfig new file mode 100644 index 000000000..242faa328 --- /dev/null +++ b/platform/aarch64/sysoul_x3300/kconfig/defconfig @@ -0,0 +1,3 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_IRQ_GICV3=y +CONFIG_UART_16550=y diff --git a/platform/aarch64/zcu102/cargo/features b/platform/aarch64/zcu102/cargo/features deleted file mode 100644 index 4189d1494..000000000 --- a/platform/aarch64/zcu102/cargo/features +++ /dev/null @@ -1,2 +0,0 @@ -gicv2 -xuartps diff --git a/platform/aarch64/zcu102/kconfig/defconfig b/platform/aarch64/zcu102/kconfig/defconfig new file mode 100644 index 000000000..b3563178f --- /dev/null +++ b/platform/aarch64/zcu102/kconfig/defconfig @@ -0,0 +1,3 @@ +CONFIG_ARCH_AARCH64=y +CONFIG_IRQ_GICV2=y +CONFIG_XUARTPS=y diff --git a/platform/loongarch64/ls3a5000/cargo/features b/platform/loongarch64/ls3a5000/cargo/features deleted file mode 100644 index 442505f46..000000000 --- a/platform/loongarch64/ls3a5000/cargo/features +++ /dev/null @@ -1,7 +0,0 @@ -loongson_3a5000 -loongson_7a2000 -loongson_uart - -pci -loongarch64_pcie -no_pcie_bar_realloc \ No newline at end of file diff --git a/platform/loongarch64/ls3a5000/kconfig/defconfig b/platform/loongarch64/ls3a5000/kconfig/defconfig new file mode 100644 index 000000000..c061a0fd3 --- /dev/null +++ b/platform/loongarch64/ls3a5000/kconfig/defconfig @@ -0,0 +1,7 @@ +CONFIG_ARCH_LOONGARCH64=y +CONFIG_LOONGSON_3A5000=y +CONFIG_LOONGSON_7A2000=y +CONFIG_LOONGSON_UART=y +CONFIG_NO_PCIE_BAR_REALLOC=y +CONFIG_PCI=y +CONFIG_PCIE_LOONGARCH64=y diff --git a/platform/loongarch64/ls3a6000/cargo/features b/platform/loongarch64/ls3a6000/cargo/features deleted file mode 100644 index 86cd7a5c3..000000000 --- a/platform/loongarch64/ls3a6000/cargo/features +++ /dev/null @@ -1,7 +0,0 @@ -loongson_3a6000 -loongson_7a2000 -loongson_uart - -pci -loongarch64_pcie -no_pcie_bar_realloc \ No newline at end of file diff --git a/platform/loongarch64/ls3a6000/kconfig/defconfig b/platform/loongarch64/ls3a6000/kconfig/defconfig new file mode 100644 index 000000000..ef0a73e99 --- /dev/null +++ b/platform/loongarch64/ls3a6000/kconfig/defconfig @@ -0,0 +1,7 @@ +CONFIG_ARCH_LOONGARCH64=y +CONFIG_LOONGSON_3A6000=y +CONFIG_LOONGSON_7A2000=y +CONFIG_LOONGSON_UART=y +CONFIG_NO_PCIE_BAR_REALLOC=y +CONFIG_PCI=y +CONFIG_PCIE_LOONGARCH64=y diff --git a/platform/riscv64/hifive-premier-p550/cargo/features b/platform/riscv64/hifive-premier-p550/cargo/features deleted file mode 100644 index d72ea9ac9..000000000 --- a/platform/riscv64/hifive-premier-p550/cargo/features +++ /dev/null @@ -1,4 +0,0 @@ -eic770x_soc -plic -eic7700_sysreg -sifive_ccache diff --git a/platform/riscv64/hifive-premier-p550/kconfig/defconfig b/platform/riscv64/hifive-premier-p550/kconfig/defconfig new file mode 100644 index 000000000..769ea39d6 --- /dev/null +++ b/platform/riscv64/hifive-premier-p550/kconfig/defconfig @@ -0,0 +1,5 @@ +CONFIG_ARCH_RISCV64=y +CONFIG_EIC7700_SYSREG=y +CONFIG_HYPERVISOR_V0_6=y +CONFIG_PLIC=y +CONFIG_SIFIVE_CCACHE=y diff --git a/platform/riscv64/megrez/cargo/features b/platform/riscv64/megrez/cargo/features deleted file mode 100644 index d1314d861..000000000 --- a/platform/riscv64/megrez/cargo/features +++ /dev/null @@ -1,2 +0,0 @@ -eic770x_soc -plic diff --git a/platform/riscv64/megrez/kconfig/defconfig b/platform/riscv64/megrez/kconfig/defconfig new file mode 100644 index 000000000..54604c280 --- /dev/null +++ b/platform/riscv64/megrez/kconfig/defconfig @@ -0,0 +1,3 @@ +CONFIG_ARCH_RISCV64=y +CONFIG_HYPERVISOR_V0_6=y +CONFIG_PLIC=y diff --git a/platform/riscv64/qemu-aia/cargo/features b/platform/riscv64/qemu-aia/cargo/features deleted file mode 100644 index b0b27b6da..000000000 --- a/platform/riscv64/qemu-aia/cargo/features +++ /dev/null @@ -1,7 +0,0 @@ -aia -sstc -pci -ecam_pcie -riscv_iommu -share_s2pt -viommu diff --git a/platform/riscv64/qemu-aia/kconfig/defconfig b/platform/riscv64/qemu-aia/kconfig/defconfig new file mode 100644 index 000000000..70c7639e2 --- /dev/null +++ b/platform/riscv64/qemu-aia/kconfig/defconfig @@ -0,0 +1,9 @@ +CONFIG_AIA=y +CONFIG_ARCH_RISCV64=y +CONFIG_IOMMU=y +CONFIG_PCI=y +CONFIG_PCIE_ECAM=y +CONFIG_RISCV_IOMMU=y +CONFIG_SHARE_S2PT=y +CONFIG_VIOMMU=y +CONFIG_SSTC=y diff --git a/platform/riscv64/qemu-plic/README.md b/platform/riscv64/qemu-plic/README.md index b6884c55d..898babb38 100644 --- a/platform/riscv64/qemu-plic/README.md +++ b/platform/riscv64/qemu-plic/README.md @@ -73,5 +73,5 @@ Both the root zone and zone1 are assigned one virtio-pci-blk device(using wired- Please confirm qemu-system-riscv64 version >= 10.0.0, add iommu-sys=on for machine param and iommu_platform=on for devices linked behind iommu. (refer to platform.mk) -Add `iommu` and `share_s2pt` features for hvisor. +Enable IOMMU in **`kconfig/defconfig`** (`CONFIG_IOMMU`, `CONFIG_RISCV_IOMMU`, `CONFIG_SHARE_S2PT`, etc.) for hvisor. diff --git a/platform/riscv64/qemu-plic/cargo/features b/platform/riscv64/qemu-plic/cargo/features deleted file mode 100644 index 6be000e7e..000000000 --- a/platform/riscv64/qemu-plic/cargo/features +++ /dev/null @@ -1,5 +0,0 @@ -aclint -plic -sstc -pci -ecam_pcie diff --git a/platform/riscv64/qemu-plic/kconfig/defconfig b/platform/riscv64/qemu-plic/kconfig/defconfig new file mode 100644 index 000000000..e3556c4f2 --- /dev/null +++ b/platform/riscv64/qemu-plic/kconfig/defconfig @@ -0,0 +1,6 @@ +CONFIG_ACLINT=y +CONFIG_ARCH_RISCV64=y +CONFIG_PCI=y +CONFIG_PCIE_ECAM=y +CONFIG_PLIC=y +CONFIG_SSTC=y diff --git a/platform/riscv64/ur-dp1000/cargo/features b/platform/riscv64/ur-dp1000/cargo/features deleted file mode 100644 index cac3e049a..000000000 --- a/platform/riscv64/ur-dp1000/cargo/features +++ /dev/null @@ -1,2 +0,0 @@ -plic -dp1000_plic diff --git a/platform/riscv64/ur-dp1000/kconfig/defconfig b/platform/riscv64/ur-dp1000/kconfig/defconfig new file mode 100644 index 000000000..58f138b2b --- /dev/null +++ b/platform/riscv64/ur-dp1000/kconfig/defconfig @@ -0,0 +1,3 @@ +CONFIG_ARCH_RISCV64=y +CONFIG_DP1000_PLIC=y +CONFIG_PLIC=y diff --git a/platform/x86_64/ecx-2300f-peg/board.rs b/platform/x86_64/ecx-2300f-peg/board.rs index e2f35550a..97e630435 100644 --- a/platform/x86_64/ecx-2300f-peg/board.rs +++ b/platform/x86_64/ecx-2300f-peg/board.rs @@ -180,6 +180,6 @@ pub const ROOT_PCI_DEVS: [HvPciDevConfig; 19] = [ pci_dev!(0x0, 0x3, 0x0, 0x0 => 0x3, 0x0, 0x0, VpciDevType::Physical), // ethernet controller ]; -#[cfg(all(feature = "graphics"))] +#[cfg(all(graphics))] pub const GRAPHICS_FONT: &[u8] = include_bytes!("../../platform/x86_64/qemu/image/font/spleen-6x12.psf"); diff --git a/platform/x86_64/ecx-2300f-peg/cargo/features b/platform/x86_64/ecx-2300f-peg/cargo/features deleted file mode 100644 index e4ad1c134..000000000 --- a/platform/x86_64/ecx-2300f-peg/cargo/features +++ /dev/null @@ -1,2 +0,0 @@ -pci -uart16550a diff --git a/platform/x86_64/ecx-2300f-peg/kconfig/defconfig b/platform/x86_64/ecx-2300f-peg/kconfig/defconfig new file mode 100644 index 000000000..770397dbb --- /dev/null +++ b/platform/x86_64/ecx-2300f-peg/kconfig/defconfig @@ -0,0 +1,4 @@ +CONFIG_ARCH_X86_64=y +CONFIG_PCI=y +CONFIG_PCIE_ECAM=y +CONFIG_UART16550A=y diff --git a/platform/x86_64/nuc14mnk/board.rs b/platform/x86_64/nuc14mnk/board.rs index 93d4149c8..9ee5d4c7c 100644 --- a/platform/x86_64/nuc14mnk/board.rs +++ b/platform/x86_64/nuc14mnk/board.rs @@ -200,6 +200,6 @@ pub const ROOT_PCI_DEVS: [HvPciDevConfig; 18] = [ pci_dev!(0x0, 0x2, 0x0, 0x0 => 0x2, 0x0, 0x0, VpciDevType::Physical), // memory controller ]; -#[cfg(all(feature = "graphics"))] +#[cfg(all(graphics))] pub const GRAPHICS_FONT: &[u8] = include_bytes!("../../platform/x86_64/qemu/image/font/spleen-6x12.psf"); diff --git a/platform/x86_64/nuc14mnk/cargo/features b/platform/x86_64/nuc14mnk/cargo/features deleted file mode 100644 index 69fb95c99..000000000 --- a/platform/x86_64/nuc14mnk/cargo/features +++ /dev/null @@ -1,6 +0,0 @@ -pci -ecam_pcie -no_pcie_bar_realloc -uart16550a -graphics -split_screen \ No newline at end of file diff --git a/platform/x86_64/nuc14mnk/kconfig/defconfig b/platform/x86_64/nuc14mnk/kconfig/defconfig new file mode 100644 index 000000000..fb35c8fcb --- /dev/null +++ b/platform/x86_64/nuc14mnk/kconfig/defconfig @@ -0,0 +1,7 @@ +CONFIG_ARCH_X86_64=y +CONFIG_GRAPHICS=y +CONFIG_NO_PCIE_BAR_REALLOC=y +CONFIG_PCI=y +CONFIG_PCIE_ECAM=y +CONFIG_SPLIT_SCREEN=y +CONFIG_UART16550A=y diff --git a/platform/x86_64/qemu/board.rs b/platform/x86_64/qemu/board.rs index a5cd3ad86..fc25028db 100644 --- a/platform/x86_64/qemu/board.rs +++ b/platform/x86_64/qemu/board.rs @@ -160,6 +160,6 @@ pub const ROOT_PCI_DEVS: [HvPciDevConfig; 7] = [ pci_dev!(0x0, 0x1, 0x0, 0x0 => 0x1, 0x0, 0x0, VpciDevType::Physical), // SCSI controller ]; -#[cfg(all(feature = "graphics"))] +#[cfg(all(graphics))] pub const GRAPHICS_FONT: &[u8] = include_bytes!("../../platform/x86_64/qemu/image/font/spleen-6x12.psf"); diff --git a/platform/x86_64/qemu/cargo/features b/platform/x86_64/qemu/cargo/features deleted file mode 100644 index fc6ef1e68..000000000 --- a/platform/x86_64/qemu/cargo/features +++ /dev/null @@ -1,5 +0,0 @@ -pci -ecam_pcie -no_pcie_bar_realloc -uart16550a -intel_vtd \ No newline at end of file diff --git a/platform/x86_64/qemu/kconfig/defconfig b/platform/x86_64/qemu/kconfig/defconfig new file mode 100644 index 000000000..007ce3484 --- /dev/null +++ b/platform/x86_64/qemu/kconfig/defconfig @@ -0,0 +1,7 @@ +CONFIG_ARCH_X86_64=y +CONFIG_INTEL_VTD=y +CONFIG_IOMMU=y +CONFIG_NO_PCIE_BAR_REALLOC=y +CONFIG_PCI=y +CONFIG_PCIE_ECAM=y +CONFIG_UART16550A=y diff --git a/src/arch/riscv64/cpu.rs b/src/arch/riscv64/cpu.rs index a9c54238b..bc6997dbd 100644 --- a/src/arch/riscv64/cpu.rs +++ b/src/arch/riscv64/cpu.rs @@ -52,7 +52,7 @@ impl ArchCpu { stack_top: 0, cpuid, // first_cpu: 0, - sstc: cfg!(feature = "sstc"), + sstc: cfg!(sstc), }; ret } @@ -68,7 +68,7 @@ impl ArchCpu { write_csr!(CSR_SSCRATCH, self as *const _ as usize); //arch cpu pointer self.sepc = entry; self.hstatus = 1 << 7 | 2 << 32; // HSTATUS_SPV | HSTATUS_VSXL_64 - #[cfg(feature = "aia")] + #[cfg(aia)] { self.hstatus |= 1 << 12; // HSTATUS_VGEIN } @@ -87,7 +87,7 @@ impl ArchCpu { set_csr!(CSR_VSTIMECMP, usize::MAX); } else { // In megrez board, this instruction is not supported. (illegal instruction) - #[cfg(not(feature = "eic770x_soc"))] + #[cfg(not(hypervisor_v0_6))] set_csr!(CSR_HENVCFG, 0); } set_csr!(CSR_HCOUNTEREN, 1 << 1); // HCOUNTEREN_TM diff --git a/src/arch/riscv64/ipi.rs b/src/arch/riscv64/ipi.rs index d4ad55d27..242023cad 100644 --- a/src/arch/riscv64/ipi.rs +++ b/src/arch/riscv64/ipi.rs @@ -15,7 +15,7 @@ // Jingyu Liu // use crate::consts::IPI_EVENT_SEND_IPI; -#[cfg(feature = "plic")] +#[cfg(plic)] use crate::consts::IPI_EVENT_UPDATE_HART_LINE; use crate::platform::BOARD_HARTID_MAP; @@ -23,9 +23,9 @@ use crate::platform::BOARD_HARTID_MAP; pub fn arch_send_event(cpu_id: u64, _sgi_num: u64) { let hart_id = BOARD_HARTID_MAP[cpu_id as usize]; debug!("arch_send_event: cpu_id: {}", hart_id); - #[cfg(feature = "aclint")] + #[cfg(aclint)] crate::device::irqchip::aclint::aclint_send_ipi(hart_id as usize); - #[cfg(not(feature = "aclint"))] + #[cfg(not(aclint))] { let sbi_ret: sbi_rt::SbiRet = sbi_rt::send_ipi(sbi_rt::HartMask::from_mask_base(1 << hart_id, 0)); @@ -44,7 +44,7 @@ pub fn arch_ipi_handler() { pub fn arch_check_events(event: Option) { match event { - #[cfg(feature = "plic")] + #[cfg(plic)] Some(IPI_EVENT_UPDATE_HART_LINE) => { use crate::device::irqchip::plic::update_hart_line; update_hart_line(); diff --git a/src/arch/riscv64/trap.rs b/src/arch/riscv64/trap.rs index 99277f72e..4c8de184f 100644 --- a/src/arch/riscv64/trap.rs +++ b/src/arch/riscv64/trap.rs @@ -15,7 +15,7 @@ // use super::cpu::ArchCpu; use crate::arch::sbi::sbi_vs_handler; -#[cfg(feature = "plic")] +#[cfg(plic)] use crate::device::irqchip::plic::{inject_irq, plic_get_hwirq}; use crate::event::check_events; use crate::memory::GuestPhysAddr; @@ -417,7 +417,7 @@ pub fn handle_software_interrupt(_current_cpu: &mut ArchCpu) { /// Handle supervisor external interrupt. pub fn handle_external_interrupt(_current_cpu: &mut ArchCpu) { - #[cfg(feature = "plic")] + #[cfg(plic)] { // Note: in hvisor, all external interrupts are assigned to VS. // 1. claim hw irq. @@ -431,7 +431,7 @@ pub fn handle_external_interrupt(_current_cpu: &mut ArchCpu) { // 2. inject hw irq to zone. inject_irq(irq_id as usize, true); } - #[cfg(feature = "aia")] + #[cfg(aia)] { // Note: no irq belongs to hvisor, so hvisor won't handle external interrupt. panic!("HS extensional interrupt") diff --git a/src/arch/x86_64/boot.rs b/src/arch/x86_64/boot.rs index b3191ef19..c3ec46367 100644 --- a/src/arch/x86_64/boot.rs +++ b/src/arch/x86_64/boot.rs @@ -394,9 +394,9 @@ impl BootParams { let bytes_per_pixel = (fb_info.bpp as usize) / 8; let width = fb_info.width as usize; - #[cfg(not(feature = "split_screen"))] + #[cfg(not(split_screen))] let height = fb_info.height as usize; - #[cfg(all(feature = "split_screen"))] + #[cfg(all(split_screen))] let height = (fb_info.height / 2) as usize; self.screen_info.lfb_base = config.arch_config.screen_base as _; diff --git a/src/arch/x86_64/cpu.rs b/src/arch/x86_64/cpu.rs index fd56d747c..97d69ccd5 100644 --- a/src/arch/x86_64/cpu.rs +++ b/src/arch/x86_64/cpu.rs @@ -296,7 +296,7 @@ impl ArchCpu { if per_cpu.boot_cpu { // must be called after activate_gpm() - #[cfg(feature = "intel_vtd")] + #[cfg(intel_vtd)] iommu::activate(); self.guest_regs = self.vm_launch_guest_regs.clone(); } diff --git a/src/arch/x86_64/entry.rs b/src/arch/x86_64/entry.rs index 2a6db8bd6..9b7a883b9 100644 --- a/src/arch/x86_64/entry.rs +++ b/src/arch/x86_64/entry.rs @@ -80,7 +80,7 @@ extern "C" fn rust_entry(magic: u32, info_addr: usize) { crate::clear_bss(); unsafe { PHYS_VIRT_OFFSET = X86_PHYS_VIRT_OFFSET }; boot::multiboot_init(info_addr); - #[cfg(all(feature = "graphics"))] + #[cfg(all(graphics))] font_init(__board::GRAPHICS_FONT); boot::print_memory_map(); rust_main(this_apic_id(), info_addr); diff --git a/src/arch/x86_64/graphics.rs b/src/arch/x86_64/graphics.rs index 2097e401f..34367850c 100644 --- a/src/arch/x86_64/graphics.rs +++ b/src/arch/x86_64/graphics.rs @@ -94,18 +94,18 @@ pub fn font_init(psf: &'static [u8]) { cursor_x: 0, cursor_y: 0, max_char_nr_x: (framebuffer.width / font_width) as _, - #[cfg(not(feature = "split_screen"))] + #[cfg(not(split_screen))] max_char_nr_y: (framebuffer.height / psf_header.height) as _, - #[cfg(all(feature = "split_screen"))] + #[cfg(all(split_screen))] max_char_nr_y: (framebuffer.height / 2 / psf_header.height) as _, - #[cfg(not(feature = "split_screen"))] + #[cfg(not(split_screen))] addr: framebuffer.addr as _, - #[cfg(all(feature = "split_screen"))] + #[cfg(all(split_screen))] addr: mid_addr as _, width: framebuffer.width as _, - #[cfg(not(feature = "split_screen"))] + #[cfg(not(split_screen))] height: framebuffer.height as _, - #[cfg(all(feature = "split_screen"))] + #[cfg(all(split_screen))] height: (framebuffer.height / 2) as _, }) }); diff --git a/src/arch/x86_64/pio.rs b/src/arch/x86_64/pio.rs index f89a55643..255e76ddb 100644 --- a/src/arch/x86_64/pio.rs +++ b/src/arch/x86_64/pio.rs @@ -89,7 +89,7 @@ impl PortIoBitmap { bitmap.set_range_intercept(PCI_CONFIG_DATA_PORT, true); // if zone_id == 0 { - #[cfg(feature = "graphics")] + #[cfg(graphics)] bitmap.set_range_intercept(UART_COM1_PORT, true); // } diff --git a/src/arch/x86_64/s2pt.rs b/src/arch/x86_64/s2pt.rs index db9bed50c..7d62106ca 100644 --- a/src/arch/x86_64/s2pt.rs +++ b/src/arch/x86_64/s2pt.rs @@ -263,7 +263,7 @@ impl PagingInstr for S2PTInstr { // if this cpu is boot cpu and it is running if this_cpu_data().vcpu_state.is_running() && this_cpu_data().boot_cpu { - #[cfg(feature = "intel_vtd")] + #[cfg(intel_vtd)] iommu::fill_dma_translation_tables(this_zone_id(), root_paddr); } } diff --git a/src/device/iommu/arm_smmu/smmu_hw.rs b/src/device/iommu/arm_smmu/smmu_hw.rs index d564b01d0..1e4c09ea1 100644 --- a/src/device/iommu/arm_smmu/smmu_hw.rs +++ b/src/device/iommu/arm_smmu/smmu_hw.rs @@ -527,13 +527,13 @@ impl Smmuv3 { static SMMUV3: spin::Once> = spin::Once::new(); /// iommu feature is disabled. -#[cfg(not(feature = "iommu"))] +#[cfg(not(iommu))] pub fn iommu_init() { info!("aarch64: iommu_init: do nothing now"); } /// smmuv3 init (enabled) -#[cfg(feature = "iommu")] +#[cfg(iommu)] pub fn iommu_init() { info!("Smmuv3 init..."); SMMUV3.call_once(|| Mutex::new(Smmuv3::new())); diff --git a/src/device/iommu/iommu_impl.rs b/src/device/iommu/iommu_impl.rs index eac9ea16e..49e0d3698 100644 --- a/src/device/iommu/iommu_impl.rs +++ b/src/device/iommu/iommu_impl.rs @@ -24,17 +24,17 @@ static IOMMU_IMPL: Once> = Once::new(); // Dispatch to the appropriate IOMMU implementation based on hardware support fn iommu_impl_init() -> Box { - #[cfg(feature = "arm_smmu")] + #[cfg(arm_smmu)] return Box::new(super::arm_smmu::ArmSmmu); - #[cfg(feature = "intel_vtd")] + #[cfg(intel_vtd)] return Box::new(super::intel_vtd::IntelVtd); - #[cfg(feature = "riscv_iommu")] + #[cfg(riscv_iommu)] return Box::new(super::riscv_iommu::RiscvIommu); // Default return DummyIommu if no IOMMU support - #[cfg(not(any(feature = "arm_smmu", feature = "intel_vtd", feature = "riscv_iommu",)))] + #[cfg(not(any(arm_smmu, intel_vtd, riscv_iommu)))] return Box::new(super::dummy_iommu::DummyIommu); } diff --git a/src/device/iommu/mod.rs b/src/device/iommu/mod.rs index 37cd69d05..0bb463914 100644 --- a/src/device/iommu/mod.rs +++ b/src/device/iommu/mod.rs @@ -26,13 +26,13 @@ use crate::zone::Zone; use iommu_impl::iommu_impl; use iommu_trait::Iommu; -#[cfg(feature = "arm_smmu")] +#[cfg(arm_smmu)] mod arm_smmu; -#[cfg(not(any(feature = "arm_smmu", feature = "intel_vtd", feature = "riscv_iommu")))] +#[cfg(not(any(arm_smmu, intel_vtd, riscv_iommu)))] mod dummy_iommu; -#[cfg(feature = "intel_vtd")] +#[cfg(intel_vtd)] mod intel_vtd; -#[cfg(feature = "riscv_iommu")] +#[cfg(riscv_iommu)] mod riscv_iommu; fn check_zone_id(zone_id: usize) -> Result<(), &'static str> { @@ -93,28 +93,28 @@ pub(crate) fn viommu_mmio_handler_register(zone: &Zone, viommu_base: usize, viom // Below pub apis are used for compatibility for old code(for x86_64) // // These apis will be replaced by IOMMU trait later. // //////////////////////////////////////////////////////////////////////// -#[cfg(feature = "intel_vtd")] +#[cfg(intel_vtd)] pub fn clear_dma_translation_tables(zone_id: usize) { intel_vtd::clear_dma_translation_tables(zone_id); } -#[cfg(feature = "intel_vtd")] +#[cfg(intel_vtd)] pub fn fill_dma_translation_tables(zone_id: usize, zone_s2pt_hpa: crate::memory::HostPhysAddr) { intel_vtd::fill_dma_translation_tables(zone_id, zone_s2pt_hpa); } /// should be called after gpm is activated -#[cfg(feature = "intel_vtd")] +#[cfg(intel_vtd)] pub fn activate() { intel_vtd::activate(); } -#[cfg(feature = "intel_vtd")] +#[cfg(intel_vtd)] pub fn flush(zone_id: usize, bus: u8, dev_func: u8) { intel_vtd::flush(zone_id, bus, dev_func); } -#[cfg(feature = "riscv_iommu")] +#[cfg(riscv_iommu)] pub fn iommu_msi_pt_tlb_invalid(zone_id: u16, msi_gpa: usize) { riscv_iommu::iommu_msi_pt_tlb_invalid(zone_id, msi_gpa); } diff --git a/src/device/iommu/riscv_iommu/iommu_hw.rs b/src/device/iommu/riscv_iommu/iommu_hw.rs index c79faa8da..97262ac86 100644 --- a/src/device/iommu/riscv_iommu/iommu_hw.rs +++ b/src/device/iommu/riscv_iommu/iommu_hw.rs @@ -113,15 +113,15 @@ fn get_iommu<'a>() -> &'a Mutex { /// Initialize IOMMU with default mode pub fn iommu_init() { - #[cfg(feature = "iommu")] + #[cfg(iommu)] riscv_iommu_init(); - #[cfg(not(feature = "iommu"))] + #[cfg(not(iommu))] info!("RISC-V IOMMU: do nothing now"); } /// Add a device to IOMMU pub fn iommu_add_device(vm_id: usize, device_id: usize, root_pt: usize) { - #[cfg(feature = "iommu")] + #[cfg(iommu)] { info!( "RV IOMMU: Add device, root_pt {:#x}, vm_id {}, device_id {}", @@ -130,13 +130,13 @@ pub fn iommu_add_device(vm_id: usize, device_id: usize, root_pt: usize) { let iommu = get_iommu(); iommu.lock().rv_iommu_add_device(device_id, vm_id, root_pt); } - #[cfg(not(feature = "iommu"))] + #[cfg(not(iommu))] info!("RISC-V: iommu_add_device do nothing now"); } /// Remove a device from IOMMU (reserved for future hot-unplug paths). pub fn iommu_remove_device(vm_id: usize, device_id: usize) { - #[cfg(feature = "iommu")] + #[cfg(iommu)] { info!( "RV IOMMU: Remove device, vm_id {}, device_id {}", @@ -145,12 +145,12 @@ pub fn iommu_remove_device(vm_id: usize, device_id: usize) { let iommu = get_iommu(); iommu.lock().rv_iommu_remove_device(device_id); } - #[cfg(not(feature = "iommu"))] + #[cfg(not(iommu))] info!("RISC-V: iommu_remove_device do nothing now"); } pub fn iommu_msi_pt_tlb_invalid(gscid: u16, msi_gpa: usize) { - #[cfg(feature = "iommu")] + #[cfg(iommu)] { // If software changes a MSI page-table entry identified by interrupt file number I that corresponds to an // untranslated MSI address A then the following invalidations must be performed: @@ -170,7 +170,7 @@ pub fn iommu_msi_pt_tlb_invalid(gscid: u16, msi_gpa: usize) { let iommu = get_iommu(); iommu.lock().rv_iommu_msi_pt_tlb_invalid(gscid, msi_gpa); } - #[cfg(not(feature = "iommu"))] + #[cfg(not(iommu))] info!("RISC-V: iommu_msi_pt_tlb_invalid do nothing now"); } diff --git a/src/device/iommu/riscv_iommu/mod.rs b/src/device/iommu/riscv_iommu/mod.rs index 2284341f6..dd8232042 100644 --- a/src/device/iommu/riscv_iommu/mod.rs +++ b/src/device/iommu/riscv_iommu/mod.rs @@ -30,7 +30,7 @@ mod cmd; mod iommu_hw; mod reg_bits; -#[cfg(feature = "viommu")] +#[cfg(viommu)] mod viommu; use super::Iommu; @@ -38,7 +38,7 @@ use crate::zone::Zone; use cmd::*; use iommu_hw::*; pub use iommu_hw::{iommu_msi_pt_tlb_invalid, iommu_remove_device}; -#[cfg(feature = "viommu")] +#[cfg(viommu)] use viommu::{viommu_init, viommu_mmio_handler_register, viommu_remove}; pub(super) struct RiscvIommu; @@ -85,27 +85,27 @@ impl Iommu for RiscvIommu { ); } fn viommu_init(&self, zone_id: usize) { - #[cfg(feature = "viommu")] + #[cfg(viommu)] viommu_init(zone_id); - #[cfg(not(feature = "viommu"))] + #[cfg(not(viommu))] warn!( "Virtual IOMMU is not enabled, skipping viommu init for zone {}", zone_id ); } fn viommu_remove(&self, zone_id: usize) { - #[cfg(feature = "viommu")] + #[cfg(viommu)] viommu_remove(zone_id); - #[cfg(not(feature = "viommu"))] + #[cfg(not(viommu))] warn!( "Virtual IOMMU is not enabled, skipping viommu remove for zone {}", zone_id ); } fn viommu_mmio_handler_register(&self, zone: &Zone, viommu_base: usize, viommu_size: usize) { - #[cfg(feature = "viommu")] + #[cfg(viommu)] viommu_mmio_handler_register(zone, viommu_base, viommu_size); - #[cfg(not(feature = "viommu"))] + #[cfg(not(viommu))] warn!("Virtual IOMMU is not enabled, skipping viommu mmio handler for zone {}, viommu_base {}, viommu_size {}.", zone.id(), viommu_base, viommu_size); } } diff --git a/src/device/irqchip/aia/vimsic.rs b/src/device/irqchip/aia/vimsic.rs index c49747666..b7a1296ae 100644 --- a/src/device/irqchip/aia/vimsic.rs +++ b/src/device/irqchip/aia/vimsic.rs @@ -17,7 +17,7 @@ use alloc::vec::Vec; use crate::consts::PAGE_SIZE; -#[cfg(feature = "iommu")] +#[cfg(iommu)] use crate::device::iommu::iommu_msi_pt_tlb_invalid; use crate::memory::Frame; use crate::memory::GuestPhysAddr; @@ -78,7 +78,7 @@ fn msi_pt_fill( // Fill MSI PTE to MSI PT. frame.as_slice_mut()[off..off + MSI_PTE_BYTES].copy_from_slice(&pte); // After change the MSI PTE, invalidate the related TLBs in IOMMU. - #[cfg(feature = "iommu")] + #[cfg(iommu)] iommu_msi_pt_tlb_invalid(zone_id as u16, imsic_gpa as usize); } } diff --git a/src/device/irqchip/gicv3/mod.rs b/src/device/irqchip/gicv3/mod.rs index e281d44d3..0633b501e 100644 --- a/src/device/irqchip/gicv3/mod.rs +++ b/src/device/irqchip/gicv3/mod.rs @@ -111,7 +111,7 @@ pub fn gicv3_handle_irq_el1() { warn!("skip sgi {}", irq_id); deactivate_irq(irq_id); } else { - #[cfg(all(feature = "dwc_pcie", feature = "dwc_msi"))] + #[cfg(all(dwc_pcie, dwc_msi))] let mut is_dwc_msi_irq = false; if irq_id == 27 { @@ -133,7 +133,7 @@ pub fn gicv3_handle_irq_el1() { //inject phy irq trace!("*** get spi_irq id = {}", irq_id); - #[cfg(all(feature = "dwc_pcie", feature = "dwc_msi"))] + #[cfg(all(dwc_pcie, dwc_msi))] { if let Some(domain_id) = crate::pci::dwc_msi::get_domain_id_by_irq(irq_id as u32) @@ -146,12 +146,12 @@ pub fn gicv3_handle_irq_el1() { warn!("not konw irq id = {}", irq_id); } - #[cfg(all(feature = "dwc_pcie", feature = "dwc_msi"))] + #[cfg(all(dwc_pcie, dwc_msi))] if irq_id != 25 && !is_dwc_msi_irq { inject_irq(irq_id, true); } - #[cfg(not(all(feature = "dwc_pcie", feature = "dwc_msi")))] + #[cfg(not(all(dwc_pcie, dwc_msi)))] if irq_id != 25 { inject_irq(irq_id, true); } diff --git a/src/device/irqchip/gicv3/vgic.rs b/src/device/irqchip/gicv3/vgic.rs index 4cf4926c1..9bf9a3a9a 100644 --- a/src/device/irqchip/gicv3/vgic.rs +++ b/src/device/irqchip/gicv3/vgic.rs @@ -311,7 +311,7 @@ pub fn vgicv3_dist_handler(mmio: &mut MMIOAccess, _arg: usize) -> HvResult { reg if reg_range(GICD_IROUTER, 1024, 8).contains(®) => { let irq = (reg - GICD_IROUTER) as u32 / 8; - #[cfg(all(feature = "dwc_pcie", feature = "dwc_msi"))] + #[cfg(all(dwc_pcie, dwc_msi))] { // For zone0, the domainmsiinfo is empty, but it will always register the intterrupt to cpu0 // So this remap operation is needed for other zones diff --git a/src/device/irqchip/ls7a2000/chip.rs b/src/device/irqchip/ls7a2000/chip.rs index 0b09350b7..d7c567516 100644 --- a/src/device/irqchip/ls7a2000/chip.rs +++ b/src/device/irqchip/ls7a2000/chip.rs @@ -663,7 +663,7 @@ pub fn extioi_int_enable_all() { CHIP_EXTIOI_ENABLE.extioi_en1.set(0xffff_ffff_ffff_ffff); CHIP_EXTIOI_ENABLE.extioi_en2.set(0xffff_ffff_ffff_ffff); CHIP_EXTIOI_ENABLE.extioi_en3.set(0xffff_ffff_ffff_ffff); - #[cfg(feature = "extioi_debug")] + #[cfg(extioi_debug)] { // dump ht int vector and enable /* diff --git a/src/device/irqchip/mod.rs b/src/device/irqchip/mod.rs index 345c4d20b..c32b0e3cb 100644 --- a/src/device/irqchip/mod.rs +++ b/src/device/irqchip/mod.rs @@ -17,32 +17,32 @@ use crate::arch::zone::HvArchZoneConfig; use crate::config::HvZoneConfig; use crate::zone::Zone; -#[cfg(all(feature = "gicv2", target_arch = "aarch64"))] +#[cfg(all(irq_gicv2, target_arch = "aarch64"))] pub mod gicv2; -#[cfg(all(feature = "gicv2", target_arch = "aarch64"))] +#[cfg(all(irq_gicv2, target_arch = "aarch64"))] pub use gicv2::{ gic::inject_irq, gicd::set_ispender, percpu_init, primary_init_early, primary_init_late, vgic::set_sgi_irq, }; -#[cfg(all(feature = "gicv3", target_arch = "aarch64"))] +#[cfg(all(irq_gicv3, target_arch = "aarch64"))] pub mod gicv3; -#[cfg(all(feature = "gicv3", target_arch = "aarch64"))] +#[cfg(all(irq_gicv3, target_arch = "aarch64"))] pub use gicv3::{ gicd::set_ispender, inject_irq, percpu_init, primary_init_early, primary_init_late, }; #[cfg(target_arch = "aarch64")] pub fn gic_handle_irq() { - #[cfg(feature = "gicv2")] + #[cfg(irq_gicv2)] gicv2::gic::gicv2_handle_irq(); - #[cfg(feature = "gicv3")] + #[cfg(irq_gicv3)] gicv3::gicv3_handle_irq_el1(); } #[cfg(target_arch = "aarch64")] pub fn gic_send_event(cpu_id: u64, sgi_num: u64) { - #[cfg(feature = "gicv3")] + #[cfg(irq_gicv3)] { /*Actually, the value passed to ICC_SGI1R_EL1 should be derived from the MPIDR of the target CPU. However, since we cannot access this @@ -63,7 +63,7 @@ pub fn gic_send_event(cpu_id: u64, sgi_num: u64) { write_sysreg!(icc_sgi1r_el1, val); debug!("write sgi sys value = {:#x}", val); } - #[cfg(feature = "gicv2")] + #[cfg(irq_gicv2)] { let sgi_id: u64 = sgi_num; let target_list: u64 = 1 << cpu_id; @@ -73,11 +73,11 @@ pub fn gic_send_event(cpu_id: u64, sgi_num: u64) { impl Zone { pub fn virqc_init(&mut self, _config: &HvZoneConfig) { - #[cfg(all(feature = "plic", target_arch = "riscv64"))] + #[cfg(all(plic, target_arch = "riscv64"))] { self.vplic_init(_config); } - #[cfg(all(feature = "aia", target_arch = "riscv64"))] + #[cfg(all(aia, target_arch = "riscv64"))] { self.vaplic_init(_config); self.vimsic_init(_config); @@ -85,28 +85,28 @@ impl Zone { } pub fn mmio_init(&mut self, hv_config: &HvArchZoneConfig) { - #[cfg(all(feature = "gicv2", target_arch = "aarch64"))] + #[cfg(all(irq_gicv2, target_arch = "aarch64"))] { self.vgicv2_mmio_init(hv_config); self.vgicv2_remap_init(hv_config); } - #[cfg(all(feature = "gicv3", target_arch = "aarch64"))] + #[cfg(all(irq_gicv3, target_arch = "aarch64"))] { self.vgicv3_mmio_init(hv_config); } - #[cfg(all(feature = "plic", target_arch = "riscv64"))] + #[cfg(all(plic, target_arch = "riscv64"))] { self.vplic_mmio_init(hv_config); } - #[cfg(all(feature = "aia", target_arch = "riscv64"))] + #[cfg(all(aia, target_arch = "riscv64"))] { self.vaplic_mmio_init(hv_config); } - #[cfg(all(feature = "eic770x_soc", target_arch = "riscv64"))] + #[cfg(all(hypervisor_v0_6, target_arch = "riscv64"))] { - #[cfg(feature = "sifive_ccache")] + #[cfg(sifive_ccache)] self.virtual_sifive_ccache_mmio_init(); - #[cfg(feature = "eic7700_sysreg")] + #[cfg(eic7700_sysreg)] self.virtual_syscon_mmio_init(); } #[cfg(target_arch = "x86_64")] @@ -117,30 +117,30 @@ impl Zone { } } -#[cfg(all(feature = "aclint", target_arch = "riscv64"))] +#[cfg(all(aclint, target_arch = "riscv64"))] pub mod aclint; -#[cfg(all(feature = "plic", target_arch = "riscv64"))] +#[cfg(all(plic, target_arch = "riscv64"))] pub mod plic; -#[cfg(all(feature = "plic", target_arch = "riscv64"))] +#[cfg(all(plic, target_arch = "riscv64"))] pub use plic::{inject_irq, percpu_init, primary_init_late}; -#[cfg(all(feature = "aia", target_arch = "riscv64"))] +#[cfg(all(aia, target_arch = "riscv64"))] pub mod aia; -#[cfg(all(feature = "aia", target_arch = "riscv64"))] +#[cfg(all(aia, target_arch = "riscv64"))] pub use aia::{host_aplic, inject_irq, percpu_init, primary_init_late}; #[cfg(target_arch = "riscv64")] pub fn primary_init_early() { // aclint is local interrupt controller // plic & aia is global interrupt controller - #[cfg(feature = "plic")] + #[cfg(plic)] plic::primary_init_early(); - #[cfg(feature = "aia")] + #[cfg(aia)] aia::primary_init_early(); - #[cfg(feature = "aclint")] + #[cfg(aclint)] aclint::aclint_init(crate::platform::ACLINT_SSWI_BASE); } diff --git a/src/device/irqchip/pic/mod.rs b/src/device/irqchip/pic/mod.rs index 5ad49ef5c..177785765 100644 --- a/src/device/irqchip/pic/mod.rs +++ b/src/device/irqchip/pic/mod.rs @@ -145,7 +145,7 @@ pub fn primary_init_late() {} impl Zone { pub fn arch_irqchip_reset(&self) { - #[cfg(feature = "intel_vtd")] + #[cfg(intel_vtd)] iommu::clear_dma_translation_tables(self.id()); } } diff --git a/src/device/irqchip/plic/plic.rs b/src/device/irqchip/plic/plic.rs index a7f069bcd..af009b381 100644 --- a/src/device/irqchip/plic/plic.rs +++ b/src/device/irqchip/plic/plic.rs @@ -235,7 +235,7 @@ impl Plic { */ let mut hwirq = 0; - if cfg!(feature = "dp1000_plic") { + if cfg!(dp1000_plic) { let mut ie: [u32; 32] = [0; 32]; // max 1024 irqs hwirq = if self.plic_check_enable_first_pending(context, &mut ie) { self.claim(context) diff --git a/src/device/mod.rs b/src/device/mod.rs index fa5aab22e..7fab9db48 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -19,8 +19,8 @@ pub mod irqchip; pub mod uart; pub mod virtio_trampoline; -#[cfg(feature = "eic7700_sysreg")] +#[cfg(eic7700_sysreg)] pub mod eic7700_syscrg; -#[cfg(feature = "sifive_ccache")] +#[cfg(sifive_ccache)] pub mod sifive_ccache; diff --git a/src/device/uart/mod.rs b/src/device/uart/mod.rs index f30f036db..16026b7f2 100644 --- a/src/device/uart/mod.rs +++ b/src/device/uart/mod.rs @@ -13,23 +13,23 @@ // // Authors: // -// we specified the driver for each platform in the FEATURES environment variable. +// UART driver is selected per board via kconfig/defconfig (see kconfig/cfg_map.toml). #![allow(unused)] -#[cfg(all(feature = "pl011", target_arch = "aarch64"))] +#[cfg(all(pl011, target_arch = "aarch64"))] mod pl011; -#[cfg(all(feature = "pl011", target_arch = "aarch64"))] +#[cfg(all(pl011, target_arch = "aarch64"))] pub use pl011::{console_getchar, console_putchar}; -#[cfg(all(feature = "imx_uart", target_arch = "aarch64"))] +#[cfg(all(imx_uart, target_arch = "aarch64"))] mod imx_uart; -#[cfg(all(feature = "imx_uart", target_arch = "aarch64"))] +#[cfg(all(imx_uart, target_arch = "aarch64"))] pub use imx_uart::{console_getchar, console_putchar}; -#[cfg(all(feature = "xuartps", target_arch = "aarch64"))] +#[cfg(all(xuartps, target_arch = "aarch64"))] mod xuartps; -#[cfg(all(feature = "xuartps", target_arch = "aarch64"))] +#[cfg(all(xuartps, target_arch = "aarch64"))] pub use xuartps::{console_getchar, console_putchar}; #[cfg(target_arch = "riscv64")] @@ -37,19 +37,19 @@ pub use crate::arch::riscv64::sbi::{ sbi_console_getchar as console_getchar, sbi_console_putchar as console_putchar, }; -#[cfg(all(feature = "loongson_uart", target_arch = "loongarch64"))] +#[cfg(all(loongson_uart, target_arch = "loongarch64"))] pub mod loongson_uart; -#[cfg(all(feature = "loongson_uart", target_arch = "loongarch64"))] +#[cfg(all(loongson_uart, target_arch = "loongarch64"))] pub use loongson_uart::{console_getchar, console_putchar}; -#[cfg(all(feature = "uart_16550", target_arch = "aarch64"))] +#[cfg(all(uart_16550, target_arch = "aarch64"))] mod uart_16550; -#[cfg(all(feature = "uart_16550", target_arch = "aarch64"))] +#[cfg(all(uart_16550, target_arch = "aarch64"))] pub use uart_16550::{console_getchar, console_putchar}; -#[cfg(all(feature = "uart16550a", target_arch = "x86_64"))] +#[cfg(all(uart16550a, target_arch = "x86_64"))] mod uart16550a; -#[cfg(all(feature = "uart16550a", target_arch = "x86_64"))] +#[cfg(all(uart16550a, target_arch = "x86_64"))] pub use uart16550a::{ console_getchar, console_putchar, virt_console_io_read, virt_console_io_write, UartReg, }; diff --git a/src/device/uart/uart16550a.rs b/src/device/uart/uart16550a.rs index 144b788b9..e180c0d69 100644 --- a/src/device/uart/uart16550a.rs +++ b/src/device/uart/uart16550a.rs @@ -348,7 +348,7 @@ impl VirtUart16550a { pub fn console_putchar(c: u8) { COM1.lock().putchar(c); - #[cfg(all(feature = "graphics"))] + #[cfg(all(graphics))] fb_putchar(c, 0xffffffff, 0); } diff --git a/src/event.rs b/src/event.rs index adf6c8152..7b147e161 100644 --- a/src/event.rs +++ b/src/event.rs @@ -25,7 +25,7 @@ use crate::{ device::{irqchip::inject_irq, virtio_trampoline::handle_virtio_irq}, platform::IRQ_WAKEUP_VIRTIO_DEVICE, }; -#[cfg(feature = "virtio_pci")] +#[cfg(virtio_pci)] use crate::{ pci::msix::activate_msix, platform::{IRQ_WAKEUP_VIRTIO_PCI_CONFIG, IRQ_WAKEUP_VIRTIO_PCI_DATA}, @@ -114,28 +114,23 @@ pub fn check_events() -> bool { true } Some(IPI_EVENT_DWC_MSI_INJECT) => { - #[cfg(all( - target_arch = "aarch64", - feature = "gicv3", - feature = "dwc_pcie", - feature = "dwc_msi" - ))] + #[cfg(all(target_arch = "aarch64", irq_gicv3, dwc_pcie, dwc_msi))] { crate::pci::dwc_msi::handle_dwc_msi_inject_event(); } true } - #[cfg(feature = "virtio_pci")] + #[cfg(virtio_pci)] Some(IPI_EVENT_VIRTIO_PCI_CONFIG) => { inject_irq(IRQ_WAKEUP_VIRTIO_PCI_CONFIG, false); true } - #[cfg(feature = "virtio_pci")] + #[cfg(virtio_pci)] Some(IPI_EVENT_VIRTIO_PCI_DATA) => { inject_irq(IRQ_WAKEUP_VIRTIO_PCI_DATA, false); true } - #[cfg(feature = "virtio_pci")] + #[cfg(virtio_pci)] Some(IPI_EVENT_VIRTIO_PCI_DONE) => { // Virtio PCI notice // unsafe { @@ -160,7 +155,7 @@ pub fn check_events() -> bool { // irqchip::ls7a2000::clear_hwi_injected_irq(); // true // } - // #[cfg(all(target_arch = "riscv64", feature = "plic"))] + // #[cfg(all(target_arch = "riscv64", plic))] // Some(IPI_EVENT_UPDATE_HART_LINE) => { // use crate::device::irqchip; // info!("cpu {} update hart line", cpu_data.id); diff --git a/src/hypercall/mod.rs b/src/hypercall/mod.rs index 40834f682..ca4f33890 100644 --- a/src/hypercall/mod.rs +++ b/src/hypercall/mod.rs @@ -282,7 +282,7 @@ impl<'a> HyperCall<'a> { zone.arch_irqchip_reset(); // Remove viommu instance related to this zone. - #[cfg(feature = "viommu")] + #[cfg(viommu)] crate::device::iommu::viommu_remove(zone_id as usize); drop(zone); diff --git a/src/logging.rs b/src/logging.rs index 6f8320c05..21316a98c 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -133,7 +133,7 @@ pub fn init() { struct SimpleLogger; impl SimpleLogger { - #[cfg(feature = "graphics")] + #[cfg(graphics)] fn print( &self, level: Level, @@ -154,7 +154,7 @@ impl SimpleLogger { ); } - #[cfg(not(feature = "graphics"))] + #[cfg(not(graphics))] fn print( &self, level: Level, @@ -165,7 +165,7 @@ impl SimpleLogger { args_color: ColorCode, record: &Record, ) { - #[cfg(feature = "print_timestamp")] + #[cfg(print_timestamp)] { let time_us: u64 = crate::arch::time::get_time_us(); let sec = time_us / 1_000_000; @@ -180,7 +180,7 @@ impl SimpleLogger { with_color!(args_color, "{}", record.args()), )); } - #[cfg(not(feature = "print_timestamp"))] + #[cfg(not(print_timestamp))] print(with_color!( ColorCode::White, "[{} {}] {} {}\n", diff --git a/src/main.rs b/src/main.rs index 639f87832..2f2161116 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,7 +73,7 @@ use arch::{cpu::cpu_start, entry::arch_entry}; use config::root_zone_config; use core::sync::atomic::{AtomicI32, AtomicU32, Ordering}; use cpu_data::PerCpu; -#[cfg(all(feature = "pci", not(feature = "pci_init_delay")))] +#[cfg(all(pci, not(pci_init_delay)))] use pci::pci_config::hvisor_pci_init; static INITED_CPUS: AtomicU32 = AtomicU32::new(0); @@ -132,12 +132,12 @@ fn primary_init_early() { device::irqchip::primary_init_early(); - #[cfg(feature = "iommu")] + #[cfg(iommu)] iommu_init(); let root_config = root_zone_config(); - #[cfg(all(feature = "pci", not(feature = "pci_init_delay")))] + #[cfg(all(pci, not(pci_init_delay)))] if root_config.num_pci_bus > 0 { let num_pci_bus = root_config.num_pci_bus as usize; let _ = hvisor_pci_init(&root_config.pci_config[..num_pci_bus]); diff --git a/src/pci/config_accessors/mod.rs b/src/pci/config_accessors/mod.rs index 544505a6a..1a974546d 100644 --- a/src/pci/config_accessors/mod.rs +++ b/src/pci/config_accessors/mod.rs @@ -65,11 +65,7 @@ impl PciRegionMmio { } } -#[cfg(all( - not(feature = "ecam_pcie"), - not(feature = "dwc_pcie"), - not(feature = "loongarch64_pcie") -))] +#[cfg(not(pci))] impl PciConfigMmio { pub fn access(&self, offset: PciConfigAddress) -> *mut T { (self.base + offset) as *mut T @@ -147,13 +143,13 @@ pub enum PciAccessorType { } // Export accessor implementations -#[cfg(feature = "ecam_pcie")] +#[cfg(ecam_pcie)] pub mod ecam; -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] pub mod dwc; -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] pub mod dwc_atu; -#[cfg(feature = "loongarch64_pcie")] +#[cfg(loongarch64_pcie)] pub mod loongarch64; diff --git a/src/pci/dwc_msi.rs b/src/pci/dwc_msi.rs index fbdc338ca..bd22abf60 100644 --- a/src/pci/dwc_msi.rs +++ b/src/pci/dwc_msi.rs @@ -15,18 +15,18 @@ // use alloc::collections::btree_map::BTreeMap; -#[cfg(all(feature = "dwc_pcie", feature = "dwc_msi"))] +#[cfg(all(dwc_pcie, dwc_msi))] use alloc::collections::VecDeque; use alloc::vec::Vec; use spin::{Lazy, Mutex}; -#[cfg(all(feature = "dwc_pcie", feature = "dwc_msi"))] +#[cfg(all(dwc_pcie, dwc_msi))] use crate::cpu_data::this_cpu_data; use crate::error::HvResult; -#[cfg(all(feature = "dwc_pcie", feature = "dwc_msi"))] +#[cfg(all(dwc_pcie, dwc_msi))] use crate::event::send_event; use crate::memory::Frame; -#[cfg(all(feature = "dwc_pcie", feature = "dwc_msi"))] +#[cfg(all(dwc_pcie, dwc_msi))] use crate::{ consts::{IPI_EVENT_DWC_MSI_INJECT, MAX_CPU_NUM}, device::irqchip::inject_irq, @@ -143,7 +143,7 @@ impl DwMsiDomain { pub static DW_MSI_DOMAINS: Lazy>> = Lazy::new(|| Mutex::new(BTreeMap::new())); -#[cfg(all(feature = "dwc_pcie", feature = "dwc_msi"))] +#[cfg(all(dwc_pcie, dwc_msi))] static DWC_MSI_IPI_PENDING_IRQS: Lazy>>> = Lazy::new(|| { let mut queues = Vec::with_capacity(MAX_CPU_NUM); for _ in 0..MAX_CPU_NUM { @@ -231,7 +231,7 @@ pub fn get_domain_owner_by_hwirq_bit(domain_id: u8, hwbit: u32) -> Option Option { let ecam_base = crate::platform::ROOT_PCI_CONFIG .iter() @@ -244,7 +244,7 @@ fn get_domain_dbi_base(domain_id: u8) -> Option { .map(|cfg| cfg.dbi_base as usize) } -#[cfg(all(feature = "dwc_pcie", feature = "dwc_msi"))] +#[cfg(all(dwc_pcie, dwc_msi))] fn first_set_bit(mask: u32) -> Option { if mask == 0 { None @@ -253,12 +253,12 @@ fn first_set_bit(mask: u32) -> Option { } } -#[cfg(all(feature = "dwc_pcie", feature = "dwc_msi"))] +#[cfg(all(dwc_pcie, dwc_msi))] fn find_target_cpu(domain_id: u8, irq_bit: usize) -> Option { get_domain_owner_by_hwirq_bit(domain_id, irq_bit as u32).map(|owner| owner.target_cpu) } -#[cfg(all(feature = "dwc_pcie", feature = "dwc_msi"))] +#[cfg(all(dwc_pcie, dwc_msi))] fn dwc_msi_pending_irq_bit(domain_id: u8) -> Option { let dbi_base = get_domain_dbi_base(domain_id)?; let status = @@ -266,7 +266,7 @@ fn dwc_msi_pending_irq_bit(domain_id: u8) -> Option { first_set_bit(status) } -#[cfg(all(feature = "dwc_pcie", feature = "dwc_msi"))] +#[cfg(all(dwc_pcie, dwc_msi))] pub fn get_domain_id_by_irq(irq: u32) -> Option { let domains = DW_MSI_DOMAINS.lock(); domains.iter().find_map(|(domain_id, domain)| { @@ -278,7 +278,7 @@ pub fn get_domain_id_by_irq(irq: u32) -> Option { }) } -#[cfg(all(feature = "dwc_pcie", feature = "dwc_msi"))] +#[cfg(all(dwc_pcie, dwc_msi))] fn enqueue_dwc_msi_ipi_irq(target_cpu: usize, irq_id: usize) -> bool { if target_cpu >= MAX_CPU_NUM { error!( @@ -297,7 +297,7 @@ fn enqueue_dwc_msi_ipi_irq(target_cpu: usize, irq_id: usize) -> bool { } } -#[cfg(all(feature = "dwc_pcie", feature = "dwc_msi"))] +#[cfg(all(dwc_pcie, dwc_msi))] fn pop_dwc_msi_ipi_irq(cpu_id: usize) -> Option { if cpu_id >= MAX_CPU_NUM { return None; @@ -307,7 +307,7 @@ fn pop_dwc_msi_ipi_irq(cpu_id: usize) -> Option { queues.get_mut(cpu_id).and_then(|queue| queue.pop_front()) } -#[cfg(all(feature = "dwc_pcie", feature = "dwc_msi"))] +#[cfg(all(dwc_pcie, dwc_msi))] pub fn dwc_msi_transfer_and_inject(domain_id: u8, irq_id: usize) { if let Some(irq_bit) = dwc_msi_pending_irq_bit(domain_id) { if let Some(target_cpu) = find_target_cpu(domain_id, irq_bit as usize) { @@ -331,7 +331,7 @@ pub fn dwc_msi_transfer_and_inject(domain_id: u8, irq_id: usize) { } } -#[cfg(all(feature = "dwc_pcie", feature = "dwc_msi"))] +#[cfg(all(dwc_pcie, dwc_msi))] pub fn handle_dwc_msi_inject_event() { let cpu_id = this_cpu_data().id; if let Some(irq_id) = pop_dwc_msi_ipi_irq(cpu_id) { diff --git a/src/pci/mod.rs b/src/pci/mod.rs index dcf8899ee..49113e9ef 100644 --- a/src/pci/mod.rs +++ b/src/pci/mod.rs @@ -24,7 +24,7 @@ pub mod pci_handler; pub mod pci_struct; pub mod vpci_dev; -#[cfg(all(feature = "dwc_msi", feature = "dwc_pcie"))] +#[cfg(all(dwc_msi, dwc_pcie))] pub mod dwc_msi; #[cfg(test)] diff --git a/src/pci/msix.rs b/src/pci/msix.rs index 365cb4002..03adbd1f4 100644 --- a/src/pci/msix.rs +++ b/src/pci/msix.rs @@ -51,7 +51,7 @@ pub fn activate_msix() { #[allow(unreachable_code)] pub fn get_arch_msix_backend() -> Option>> { - #[cfg(all(target_arch = "aarch64", feature = "gicv3"))] + #[cfg(all(target_arch = "aarch64", irq_gicv3))] { use crate::device::irqchip::gicv3::msix_backend::get_gicv3_backend; return Some(get_gicv3_backend()); diff --git a/src/pci/pci_config.rs b/src/pci/pci_config.rs index b280bbb02..9612ccf00 100644 --- a/src/pci/pci_config.rs +++ b/src/pci/pci_config.rs @@ -27,30 +27,25 @@ use crate::{ zone::Zone, }; -#[cfg(feature = "loongarch64_pcie")] +#[cfg(loongarch64_pcie)] use alloc::vec::Vec; -#[cfg(any( - all(feature = "iommu", target_arch = "aarch64"), - all(feature = "iommu", target_arch = "riscv64"), - target_arch = "x86_64" -))] +#[cfg(iommu)] use crate::device::iommu::iommu_add_device_with_root_pt_addr; -#[cfg(feature = "ecam_pcie")] -use crate::pci::vpci_dev::{get_handler, VpciDevType}; +#[cfg(ecam_pcie)] +use crate::pci::{ + pci_struct::VirtualPciConfigSpace, + vpci_dev::{get_handler, VpciDevType}, +}; -#[cfg(any( - feature = "ecam_pcie", - feature = "dwc_pcie", - feature = "loongarch64_pcie" -))] +#[cfg(pci)] use crate::pci::{mem_alloc::BaseAllocator, pci_struct::RootComplex}; -#[cfg(feature = "ecam_pcie")] +#[cfg(ecam_pcie)] use crate::pci::{config_accessors::ecam::EcamConfigAccessor, pci_handler::mmio_vpci_handler}; -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] use crate::{ memory::mmio_generic_handler, pci::{ @@ -63,7 +58,7 @@ use crate::{ platform, }; -#[cfg(feature = "loongarch64_pcie")] +#[cfg(loongarch64_pcie)] use crate::pci::{ config_accessors::loongarch64::LoongArchConfigAccessor, pci_handler::mmio_vpci_direct_handler, }; @@ -77,16 +72,11 @@ pub static GLOBAL_PCIE_LIST: Lazy HvResult { warn!("begin {:#x?}", pci_config); - // Track domains that have been initialized for DW MSI - #[cfg(all(feature = "dwc_msi", feature = "dwc_pcie"))] + #[cfg(all(dwc_msi, dwc_pcie))] let mut initialized_domains: alloc::vec::Vec = alloc::vec::Vec::new(); - #[cfg(any( - feature = "ecam_pcie", - feature = "dwc_pcie", - feature = "loongarch64_pcie" - ))] + #[cfg(pci)] for (_index, rootcomplex_config) in pci_config.iter().enumerate() { /* empty config */ if rootcomplex_config.ecam_base == 0 { @@ -104,12 +94,12 @@ pub fn hvisor_pci_init(pci_config: &[HvPciConfig]) -> HvResult { // TODO: refactor // in x86, we do not take the initiative to reallocate BAR space - #[cfg(feature = "no_pcie_bar_realloc")] + #[cfg(no_pcie_bar_realloc)] let allocator_opt: Option = None; - #[cfg(not(feature = "no_pcie_bar_realloc"))] + #[cfg(not(no_pcie_bar_realloc))] let allocator_opt: Option = Some(allocator); - // #[cfg(feature = "loongarch64_pcie")] + // #[cfg(loongarch64_pcie)] // let allocator_opt: Option = { // let mut allocator = LoongArchAllocator::default(); // allocator.set_mem( @@ -124,7 +114,7 @@ pub fn hvisor_pci_init(pci_config: &[HvPciConfig]) -> HvResult { // }; let mut rootcomplex = { - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] { // warn!("dwc pcie"); let ecam_base = rootcomplex_config.ecam_base; @@ -145,7 +135,7 @@ pub fn hvisor_pci_init(pci_config: &[HvPciConfig]) -> HvResult { RootComplex::new_dwc(rootcomplex_config.ecam_base, atu_config, root_bus) } - #[cfg(feature = "loongarch64_pcie")] + #[cfg(loongarch64_pcie)] { let root_bus = rootcomplex_config.bus_range_begin as u8; RootComplex::new_loongarch( @@ -155,7 +145,7 @@ pub fn hvisor_pci_init(pci_config: &[HvPciConfig]) -> HvResult { ) } - #[cfg(feature = "ecam_pcie")] + #[cfg(ecam_pcie)] { RootComplex::new_ecam(rootcomplex_config.ecam_base) } @@ -180,7 +170,7 @@ pub fn hvisor_pci_init(pci_config: &[HvPciConfig]) -> HvResult { } // Initialize DW MSI domain for this domain ID (only once per domain) - #[cfg(all(feature = "dwc_msi", feature = "dwc_pcie"))] + #[cfg(all(dwc_msi, dwc_pcie))] { if !initialized_domains.contains(&domain) { let msi_irq = platform::ROOT_DWC_ATU_CONFIG @@ -220,7 +210,7 @@ impl Zone { let bus_range_begin = target_pci_config.bus_range_begin as u8; // Create accessor for VirtualRootComplex, similar to RootComplex - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] { use alloc::sync::Arc; let atu_config = platform::ROOT_DWC_ATU_CONFIG @@ -240,7 +230,7 @@ impl Zone { } } - #[cfg(feature = "loongarch64_pcie")] + #[cfg(loongarch64_pcie)] { use alloc::sync::Arc; let root_bus = bus_range_begin; @@ -252,7 +242,7 @@ impl Zone { inner.vpci_bus_mut().set_accessor(accessor); } - #[cfg(feature = "ecam_pcie")] + #[cfg(ecam_pcie)] { use alloc::sync::Arc; let accessor = Arc::new(EcamConfigAccessor::new(ecam_base)); @@ -313,11 +303,7 @@ impl Zone { info!("set bdf {:#?} to vbdf {:#?}", bdf, vbdf); - #[cfg(any( - all(feature = "iommu", target_arch = "aarch64"), - all(feature = "iommu", target_arch = "riscv64"), - target_arch = "x86_64" - ))] + #[cfg(iommu)] { let iommu_pt_addr = if inner.iommu_pt().is_some() { inner.iommu_pt().unwrap().root_paddr() @@ -327,13 +313,13 @@ impl Zone { let device_id = (dev_config.bus as usize) << 8 | (dev_config.device as usize) << 3 | dev_config.function as usize; - #[cfg(feature = "share_s2pt")] + #[cfg(share_s2pt)] iommu_add_device_with_root_pt_addr( _zone_id, device_id as _, inner.gpm().root_paddr(), ); - #[cfg(not(feature = "share_s2pt"))] + #[cfg(not(share_s2pt))] iommu_add_device_with_root_pt_addr(_zone_id, device_id as _, iommu_pt_addr); } @@ -380,7 +366,7 @@ impl Zone { } } else { warn!("can not find dev {:#?} in GLOBAL_PCIE_LIST (not detected during enumeration)", bdf); - #[cfg(feature = "ecam_pcie")] + #[cfg(ecam_pcie)] { let dev_type = dev_config.dev_type; warn!("dev_type:{:?}", dev_config); @@ -410,7 +396,7 @@ impl Zone { // After processing all devices for this domain, allocate hardware MSI bits if domain_msi_count > 0 { - #[cfg(all(feature = "dwc_msi", feature = "dwc_pcie"))] + #[cfg(all(dwc_msi, dwc_pcie))] { // Get the DW MSI domain allocator and allocate hwbit if let Some(mut domain_lock) = @@ -439,7 +425,7 @@ impl Zone { } } - #[cfg(not(feature = "dwc_msi"))] + #[cfg(not(dwc_msi))] { // Without dwc_msi feature, just register without hardware bit allocation inner.vpci_bus_mut().add_msi_count_for_domain( @@ -459,7 +445,7 @@ impl Zone { pci_rootcomplex_config: &[HvPciConfig; CONFIG_PCI_BUS_MAXNUM], _num_pci_config: usize, ) { - #[cfg(feature = "loongarch64_pcie")] + #[cfg(loongarch64_pcie)] let mut emergency_map_regions: Vec<(usize, usize)> = Vec::new(); let mut inner = self.write(); @@ -469,7 +455,7 @@ impl Zone { if rootcomplex_config.ecam_base == 0 { continue; } - #[cfg(feature = "ecam_pcie")] + #[cfg(ecam_pcie)] { // use crate::pci::pci_handler::mmio_vpci_direct_handler; inner.mmio_region_register( @@ -480,7 +466,7 @@ impl Zone { rootcomplex_config.ecam_base as usize, ); } - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] { // Encode domain_id into the arg parameter: arg = ecam_base + domain_id // Since ecam_base is 4KB aligned, its low 12 bits are 0 @@ -565,7 +551,7 @@ impl Zone { ); } } - #[cfg(feature = "loongarch64_pcie")] + #[cfg(loongarch64_pcie)] { inner.mmio_region_register( rootcomplex_config.ecam_base as usize, @@ -578,11 +564,7 @@ impl Zone { rootcomplex_config.ecam_size as usize, )); } - #[cfg(not(any( - feature = "ecam_pcie", - feature = "dwc_pcie", - feature = "loongarch64_pcie" - )))] + #[cfg(not(pci))] { warn!( "No extend config found for base 0x{:x}", @@ -591,7 +573,7 @@ impl Zone { } } - #[cfg(feature = "loongarch64_pcie")] + #[cfg(loongarch64_pcie)] { drop(inner); for (base, size) in emergency_map_regions { diff --git a/src/pci/pci_handler.rs b/src/pci/pci_handler.rs index ae1d6adcf..71cc787a1 100644 --- a/src/pci/pci_handler.rs +++ b/src/pci/pci_handler.rs @@ -14,13 +14,13 @@ // Authors: // -#[cfg(all(feature = "dwc_pcie", feature = "pci_init_delay"))] +#[cfg(all(dwc_pcie, pci_init_delay))] use alloc::collections::btree_map::BTreeMap; use alloc::string::String; use alloc::vec::Vec; -#[cfg(all(feature = "dwc_pcie", feature = "pci_init_delay"))] +#[cfg(all(dwc_pcie, pci_init_delay))] use spin::Lazy; -#[cfg(all(feature = "dwc_pcie", feature = "pci_init_delay"))] +#[cfg(all(dwc_pcie, pci_init_delay))] use spin::Mutex; use crate::cpu_data::this_zone; @@ -41,7 +41,7 @@ use super::PciConfigAddress; #[cfg(target_arch = "x86_64")] use crate::zone::this_zone_id; -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] use crate::pci::config_accessors::{ dwc::DwcConfigRegionBackend, dwc_atu::{ @@ -53,15 +53,15 @@ use crate::pci::config_accessors::{ PciRegionMmio, }; -#[cfg(all(feature = "dwc_msi", feature = "dwc_pcie"))] +#[cfg(all(dwc_msi, dwc_pcie))] use super::dwc_msi::{ PCIE_MSI_ADDR_HI, PCIE_MSI_ADDR_LO, PCIE_MSI_INTR0_ENABLE, PCIE_MSI_INTR0_MASK, PCIE_MSI_INTR0_STATUS, }; -#[cfg(not(feature = "dwc_msi"))] +#[cfg(not(dwc_msi))] const PCIE_MSI_ADDR_LO: usize = 0x820; -#[cfg(not(feature = "dwc_msi"))] +#[cfg(not(dwc_msi))] const PCIE_MSI_INTR0_STATUS: usize = 0x830; const SRIOV_CTRL_OFFSET: PciConfigAddress = 0x08; @@ -386,7 +386,7 @@ fn handle_cap_access( } } } else if offset >= 0x100 { - #[cfg(feature = "sriov")] + #[cfg(sriov)] if let Some(cap_offset) = dev.with_sriov_info(|sriov_info| sriov_info.cap_offset) { if offset >= cap_offset && offset < cap_offset + SRIOV_CAP_SIZE { if is_write { @@ -402,7 +402,7 @@ fn handle_cap_access( // When `sriov` feature is disabled, hide the SR-IOV extended capability // from guest VMs by patching the ext-cap linked list on the fly. - #[cfg(not(feature = "sriov"))] + #[cfg(not(sriov))] if let Some(hide) = dev.with_hide_sriov(|h| h.clone()) { use bit_field::BitField; // Accesses inside the SR-IOV cap range: return 0 / silently drop writes. @@ -485,7 +485,7 @@ fn handle_cap_access( matches!(relative_offset, 8 | 9) }; - #[cfg(all(feature = "dwc_msi", feature = "dwc_pcie"))] + #[cfg(all(dwc_msi, dwc_pcie))] { if is_write { if _is_addr_low { @@ -637,7 +637,7 @@ fn handle_endpoint_access( let bar_type = dev.with_bar_ref(slot, |bar| bar.get_type()); // Check if this BAR contains MSIX table (only when dwc_msi feature is enabled) - #[cfg(all(feature = "dwc_msi", feature = "dwc_pcie"))] + #[cfg(all(dwc_msi, dwc_pcie))] let is_msix_bar = { let msix_check_slot = if bar_type == PciMemType::Mem64High && slot > 0 { slot - 1 @@ -656,7 +656,7 @@ fn handle_endpoint_access( .unwrap_or(false) }; - #[cfg(not(feature = "dwc_msi"))] + #[cfg(not(dwc_msi))] let is_msix_bar = false; if bar_type != PciMemType::default() { @@ -798,7 +798,7 @@ fn handle_endpoint_access( core::arch::asm!("tlbi vmalls12e1is"); core::arch::asm!("dsb nsh"); } - #[cfg(all(target_arch = "x86_64", feature = "intel_vtd"))] + #[cfg(all(target_arch = "x86_64", intel_vtd))] { let vbdf = dev.get_vbdf(); crate::device::iommu::flush( @@ -944,7 +944,7 @@ fn handle_endpoint_access( /* after update gpm, need to flush iommu table * in x86_64 */ - #[cfg(all(target_arch = "x86_64", feature = "intel_vtd"))] + #[cfg(all(target_arch = "x86_64", intel_vtd))] { let vbdf = dev.get_vbdf(); crate::device::iommu::flush( @@ -1090,7 +1090,7 @@ fn handle_endpoint_access( /* after update gpm, need to flush iommu table * in x86_64 */ - #[cfg(all(target_arch = "x86_64", feature = "intel_vtd"))] + #[cfg(all(target_arch = "x86_64", intel_vtd))] { let vbdf = dev.get_vbdf(); crate::device::iommu::flush( @@ -1153,7 +1153,7 @@ fn handle_pci_bridge_access( let bar_type = dev.with_bar_ref(slot, |bar| bar.get_type()); // Check if this BAR contains MSIX table (only when dwc_msi feature is enabled) - #[cfg(all(feature = "dwc_msi", feature = "dwc_pcie"))] + #[cfg(all(dwc_msi, dwc_pcie))] let is_msix_bar = { let msix_check_slot = if bar_type == PciMemType::Mem64High && slot > 0 { slot - 1 @@ -1172,7 +1172,7 @@ fn handle_pci_bridge_access( .unwrap_or(false) }; - #[cfg(not(feature = "dwc_msi"))] + #[cfg(not(dwc_msi))] let is_msix_bar = false; if bar_type != PciMemType::default() { @@ -1294,7 +1294,7 @@ fn handle_pci_bridge_access( core::arch::asm!("tlbi vmalls12e1is"); core::arch::asm!("dsb nsh"); } - #[cfg(all(target_arch = "x86_64", feature = "intel_vtd"))] + #[cfg(all(target_arch = "x86_64", intel_vtd))] { let vbdf = dev.get_vbdf(); crate::device::iommu::flush( @@ -1420,7 +1420,7 @@ fn handle_pci_bridge_access( /* after update gpm, need to flush iommu table * in x86_64 */ - #[cfg(all(target_arch = "x86_64", feature = "intel_vtd"))] + #[cfg(all(target_arch = "x86_64", intel_vtd))] { let vbdf = dev.get_vbdf(); crate::device::iommu::flush( @@ -1561,7 +1561,7 @@ fn handle_pci_bridge_access( /* after update gpm, need to flush iommu table * in x86_64 */ - #[cfg(all(target_arch = "x86_64", feature = "intel_vtd"))] + #[cfg(all(target_arch = "x86_64", intel_vtd))] { let vbdf = dev.get_vbdf(); crate::device::iommu::flush( @@ -1817,7 +1817,7 @@ pub fn mmio_vpci_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { Ok(()) } -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] pub fn mmio_dwc_io_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { { let zone = this_zone(); @@ -1858,7 +1858,7 @@ pub fn mmio_dwc_io_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { Ok(()) } -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] pub fn mmio_dwc_cfg_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { // info!("mmio_dwc_cfg_handler {:#x}", mmio.address + _base); let zone = this_zone(); @@ -1993,7 +1993,7 @@ pub fn mmio_dwc_cfg_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { Ok(()) } -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] pub fn mmio_vpci_handler_dbi(mmio: &mut MMIOAccess, _base: usize) -> HvResult { // info!("mmio_vpci_handler_dbi {:#x}", mmio.address); @@ -2006,7 +2006,7 @@ pub fn mmio_vpci_handler_dbi(mmio: &mut MMIOAccess, _base: usize) -> HvResult { let domain_id = (_base & 0xF) as u8; let ecam_base = _base - (domain_id as usize); - #[cfg(all(feature = "pci_init_delay", feature = "dwc_pcie"))] + #[cfg(all(pci_init_delay, dwc_pcie))] { // Delay mode semantics: // - Before init-done, accesses to non-zero DBI regs are normally passed through. @@ -2014,7 +2014,7 @@ pub fn mmio_vpci_handler_dbi(mmio: &mut MMIOAccess, _base: usize) -> HvResult { // - Access to DBI reg 0 triggers hvisor PCI init, then normal DBI virtualization continues. if !is_pci_init_done(domain_id) { if mmio.address != 0 { - #[cfg(feature = "dwc_msi")] + #[cfg(dwc_msi)] match mmio.address { PCIE_MSI_ADDR_LO | PCIE_MSI_ADDR_HI => { let zone = this_zone(); @@ -2071,7 +2071,7 @@ pub fn mmio_vpci_handler_dbi(mmio: &mut MMIOAccess, _base: usize) -> HvResult { num_pci_bus, )?; - #[cfg(feature = "dwc_msi")] + #[cfg(dwc_msi)] { // Why this is inside init-delay only: // before init-done, VM may have already written MSI_ADDR_LO/HI and those writes were @@ -2246,7 +2246,7 @@ pub fn mmio_vpci_handler_dbi(mmio: &mut MMIOAccess, _base: usize) -> HvResult { mmio_perform_access(ecam_base, mmio); } else if mmio.address >= PCIE_MSI_ADDR_LO && mmio.address <= PCIE_MSI_INTR0_STATUS { // Handle MSI registers - virtuize only if dwc_msi feature enabled - #[cfg(all(feature = "dwc_msi", feature = "dwc_pcie"))] + #[cfg(all(dwc_msi, dwc_pcie))] { // Non-delay path (or delay after init-done) uses this handler for MSI DBI regs. // LO/HI writes are virtualized and synchronized with hvisor-managed doorbell here. @@ -2451,7 +2451,7 @@ pub fn mmio_vpci_handler_dbi(mmio: &mut MMIOAccess, _base: usize) -> HvResult { } } - #[cfg(not(feature = "dwc_msi"))] + #[cfg(not(dwc_msi))] { // Without dwc_msi feature, directly pass through MSI register access mmio_perform_access(ecam_base, mmio); @@ -2515,11 +2515,11 @@ pub fn mmio_vpci_handler_dbi(mmio: &mut MMIOAccess, _base: usize) -> HvResult { Ok(()) } -#[cfg(all(feature = "dwc_pcie", feature = "pci_init_delay"))] +#[cfg(all(dwc_pcie, pci_init_delay))] static DBI_PCI_INIT_DONE: Lazy>> = Lazy::new(|| Mutex::new(BTreeMap::new())); -#[cfg(all(feature = "dwc_pcie", feature = "pci_init_delay"))] +#[cfg(all(dwc_pcie, pci_init_delay))] pub fn is_pci_init_done(domain_id: u8) -> bool { DBI_PCI_INIT_DONE .lock() @@ -2528,7 +2528,7 @@ pub fn is_pci_init_done(domain_id: u8) -> bool { .unwrap_or(false) } -#[cfg(all(feature = "dwc_pcie", feature = "pci_init_delay"))] +#[cfg(all(dwc_pcie, pci_init_delay))] fn set_pci_init_done(domain_id: u8) { DBI_PCI_INIT_DONE.lock().insert(domain_id, true); } @@ -2640,7 +2640,7 @@ pub fn mmio_msix_table_handler(mmio: &mut MMIOAccess, base: usize) -> HvResult { }); // Replace with hvisor's doorbell before writing to hardware - #[cfg(all(feature = "dwc_msi", feature = "dwc_pcie"))] + #[cfg(all(dwc_msi, dwc_pcie))] { if _domain_id != 0xFF { let hw_paddr = @@ -2658,7 +2658,7 @@ pub fn mmio_msix_table_handler(mmio: &mut MMIOAccess, base: usize) -> HvResult { }); // Replace with hvisor's doorbell before writing to hardware - #[cfg(all(feature = "dwc_msi", feature = "dwc_pcie"))] + #[cfg(all(dwc_msi, dwc_pcie))] { if _domain_id != 0xFF { let hw_paddr = @@ -2670,7 +2670,7 @@ pub fn mmio_msix_table_handler(mmio: &mut MMIOAccess, base: usize) -> HvResult { } 8..=11 => { // Convert VM vector index to hardware vector index. - #[cfg(all(feature = "dwc_msi", feature = "dwc_pcie"))] + #[cfg(all(dwc_msi, dwc_pcie))] { if _domain_id != 0xFF { let zone = this_zone(); @@ -2722,7 +2722,7 @@ pub fn mmio_msix_table_handler(mmio: &mut MMIOAccess, base: usize) -> HvResult { } 8..=11 => { mmio.value = hw_value; - #[cfg(all(feature = "dwc_msi", feature = "dwc_pcie"))] + #[cfg(all(dwc_msi, dwc_pcie))] { if _domain_id != 0xFF { let zone = this_zone(); diff --git a/src/pci/pci_struct.rs b/src/pci/pci_struct.rs index 9ecefdda7..2ec58c853 100644 --- a/src/pci/pci_struct.rs +++ b/src/pci/pci_struct.rs @@ -1730,7 +1730,7 @@ impl PciIterator { let _ = node.capability_enumerate(); node.ext_capability_enumerate(); - #[cfg(feature = "sriov")] + #[cfg(sriov)] let _ = node.build_sriov_info(); // Build MSI/MSIX info once during device discovery node.build_msi_info(); @@ -1758,7 +1758,7 @@ impl PciIterator { let _ = node.capability_enumerate(); node.ext_capability_enumerate(); - #[cfg(feature = "sriov")] + #[cfg(sriov)] let _ = node.build_sriov_info(); // Build MSI/MSIX info once during device discovery node.build_msi_info(); @@ -1989,7 +1989,7 @@ impl Iterator for PciIterator { // (UEFI/BIOS) may skip bus numbers for subordinate bus reservation, // causing calculated bus numbers to diverge from actual hardware // bus assignments — making devices behind bridges invisible. - #[cfg(feature = "no_pcie_bar_realloc")] + #[cfg(no_pcie_bar_realloc)] let next_bus = { let bridge_base = node.get_base(); let bus_reg = unsafe { @@ -2024,7 +2024,7 @@ impl Iterator for PciIterator { parent.subordinate_bus + 1 } }; - #[cfg(not(feature = "no_pcie_bar_realloc"))] + #[cfg(not(no_pcie_bar_realloc))] let next_bus = parent.subordinate_bus + 1; let bdf = Bdf::new(domain, next_bus, 0, 0); @@ -2131,10 +2131,10 @@ impl Bridge { } // When no_pcie_bar_realloc is enabled, firmware already assigned correct bus // numbers — don't overwrite them. - #[cfg(feature = "no_pcie_bar_realloc")] + #[cfg(no_pcie_bar_realloc)] return; - #[cfg(not(feature = "no_pcie_bar_realloc"))] + #[cfg(not(no_pcie_bar_realloc))] { // we need to update the bridge bus number if we want linux not to update bus number unsafe { @@ -2291,9 +2291,9 @@ impl VirtualRootComplex { let _parent_bus = dev.parent_bdf.bus(); let _offset = 0; let base = if let Some(accessor) = &self.accessor { - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] let addr = accessor.get_pci_addr_base(bdf); - #[cfg(not(feature = "dwc_pcie"))] + #[cfg(not(dwc_pcie))] let addr = accessor.get_physical_address(bdf, _offset, _parent_bus); match addr { Ok(addr) => addr, @@ -2315,7 +2315,7 @@ impl VirtualRootComplex { // let host_bdf = dev.get_bdf(); // let vbdf = dev.get_vbdf(); - // #[cfg(feature = "dwc_pcie")] + // #[cfg(dwc_pcie)] // let key = { // let bus = bdf.bus() as PciConfigAddress; // let device = bdf.device() as PciConfigAddress; @@ -2328,10 +2328,10 @@ impl VirtualRootComplex { // } // }; - // #[cfg(not(feature = "dwc_pcie"))] + // #[cfg(not(dwc_pcie))] // let key = base; - // #[cfg(feature = "dwc_pcie")] + // #[cfg(dwc_pcie)] // { // let bus = bdf.bus() as PciConfigAddress; // let device = bdf.device() as PciConfigAddress; @@ -2350,7 +2350,7 @@ impl VirtualRootComplex { // ); // } - // #[cfg(not(feature = "dwc_pcie"))] + // #[cfg(not(dwc_pcie))] // info!( // "vpci insert: base_to_bdf[{:#x}] = key_bdf {:#?}, source base, base {:#x}, dev_host_bdf {:#?}, dev_vbdf {:#?}, remapped {}", // key, @@ -3304,7 +3304,7 @@ impl VirtualPciConfigSpace { // When the `sriov` feature is disabled, record info needed to splice // the SR-IOV cap out of the ext-cap linked list for guest VMs. - #[cfg(not(feature = "sriov"))] + #[cfg(not(sriov))] { use bit_field::BitField; let sriov_offset = ext_caps diff --git a/src/pci/pci_test.rs b/src/pci/pci_test.rs index 182a614de..c6d7b22d4 100644 --- a/src/pci/pci_test.rs +++ b/src/pci/pci_test.rs @@ -25,7 +25,7 @@ use super::{ pci_struct::{Bdf, VirtualPciConfigSpace, CONFIG_LENTH}, }; -#[cfg(feature = "ecam_pcie")] +#[cfg(ecam_pcie)] use super::{mem_alloc::BaseAllocator, pci_struct::RootComplex}; use crate::{ @@ -40,7 +40,7 @@ pub static GLOBAL_PCIE_LIST_TEST: Lazy Option { - #[cfg(feature = "virtio_pci")] + #[cfg(virtio_pci)] { // Create initial VirtualPciConfigSpace with default values @@ -139,7 +139,7 @@ pub(super) fn virt_dev_init( } } } - #[cfg(not(feature = "virtio_pci"))] + #[cfg(not(virtio_pci))] { warn!( "Try to initialize a virtual virtio pci device when feature virtio-pci is not enabled" diff --git a/src/pci/vpci_dev/virtio_cap.rs b/src/pci/vpci_dev/virtio_cap.rs index d819bc684..9ae3c721d 100644 --- a/src/pci/vpci_dev/virtio_cap.rs +++ b/src/pci/vpci_dev/virtio_cap.rs @@ -44,7 +44,7 @@ const VIRTIO_F_VERSION_1: usize = 32; // #[allow(unused_variables)] // pub unsafe fn virtio_pci_intercept_its(deviceid: usize, event_id: usize, intid: usize) { -// #[cfg(feature = "virtio_pci")] +// #[cfg(virtio_pci)] // unsafe { // use crate::pci::msix::MAPTI_INTERCEPTOR; @@ -56,7 +56,7 @@ const VIRTIO_F_VERSION_1: usize = 32; // #[allow(unused_variables)] // pub unsafe fn virtio_pci_add_pending_data_req_id(data_req_id: u64) { -// #[cfg(feature = "virtio_pci")] +// #[cfg(virtio_pci)] // unsafe { // use crate::pci::msix::VIRTIO_MSIX_MANAGER; @@ -68,7 +68,7 @@ const VIRTIO_F_VERSION_1: usize = 32; // #[allow(unused_variables)] // pub unsafe fn virtio_pci_activate_all_pending_irq() { -// #[cfg(feature = "virtio_pci")] +// #[cfg(virtio_pci)] // unsafe { // use crate::pci::msix::VIRTIO_MSIX_MANAGER; diff --git a/src/platform/mod.rs b/src/platform/mod.rs index d9dd18a65..64605c31c 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -72,7 +72,7 @@ pub fn platform_root_zone_config() -> HvZoneConfig { let mut _num_pci_devs: u64 = 0; let mut _num_pci_bus: u64 = 0; - #[cfg(feature = "pci")] + #[cfg(pci)] { check!(ROOT_PCI_DEVS.len(), CONFIG_MAX_PCI_DEV, "ROOT_PCI_DEVS"); pci_devs[..ROOT_PCI_DEVS.len()].copy_from_slice(&ROOT_PCI_DEVS); diff --git a/src/zone.rs b/src/zone.rs index b01553f0e..b9e68bc87 100644 --- a/src/zone.rs +++ b/src/zone.rs @@ -20,9 +20,9 @@ use crate::consts::{INVALID_ADDRESS, MAX_CPU_NUM}; use crate::pci::pci_struct::VirtualRootComplex; use spin::{RwLock, RwLockReadGuard, RwLockWriteGuard}; -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] use crate::pci::{config_accessors::dwc_atu::AtuConfig, PciConfigAddress}; -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] use alloc::collections::btree_map::BTreeMap; use crate::arch::mm::new_s2_memory_set; @@ -36,14 +36,14 @@ use crate::memory::{MMIOConfig, MMIOHandler, MMIORegion, MemorySet}; use core::panic; use core::sync::atomic::{AtomicBool, Ordering}; -#[cfg(all(feature = "pci_init_delay", feature = "dwc_pcie"))] +#[cfg(all(pci_init_delay, dwc_pcie))] use crate::config::{HvPciConfig, HvPciDevConfig, CONFIG_MAX_PCI_DEV, CONFIG_PCI_BUS_MAXNUM}; -#[cfg(all(feature = "pci_init_delay", feature = "dwc_pcie"))] +#[cfg(all(pci_init_delay, dwc_pcie))] use crate::pci::pci_config::GLOBAL_PCIE_LIST; -#[cfg(all(feature = "pci_init_delay", feature = "dwc_pcie"))] +#[cfg(all(pci_init_delay, dwc_pcie))] use crate::pci::pci_struct::Bdf; -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] #[derive(Debug)] pub struct VirtualAtuConfigs { ecam_to_atu: BTreeMap, @@ -51,7 +51,7 @@ pub struct VirtualAtuConfigs { cfg_base_to_ecam: BTreeMap, } -#[cfg(feature = "dwc_pcie")] +#[cfg(dwc_pcie)] impl VirtualAtuConfigs { pub fn new() -> Self { Self { @@ -123,7 +123,7 @@ pub struct ZoneInner { gpm: MemorySet, iommu_pt: Option>, vpci_bus: VirtualRootComplex, - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] atu_configs: VirtualAtuConfigs, } @@ -175,13 +175,13 @@ impl ZoneInner { cpu_num: 0, cpu_set: CpuSet::new(MAX_CPU_NUM as usize, 0), irq_bitmap: [0; 1024 / 32], - iommu_pt: if cfg!(feature = "iommu") { + iommu_pt: if cfg!(iommu) { Some(new_s2_memory_set()) } else { None }, vpci_bus: VirtualRootComplex::new(), - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] atu_configs: VirtualAtuConfigs::new(), } } @@ -308,17 +308,17 @@ impl ZoneInner { &mut self.vpci_bus } - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] pub fn atu_configs(&self) -> &VirtualAtuConfigs { &self.atu_configs } - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] pub fn atu_configs_mut(&mut self) -> &mut VirtualAtuConfigs { &mut self.atu_configs } - #[cfg(all(feature = "pci_init_delay", feature = "dwc_pcie"))] + #[cfg(all(pci_init_delay, dwc_pcie))] pub fn guest_pci_init_delay( &mut self, _zone_id: usize, @@ -340,7 +340,7 @@ impl ZoneInner { let bus_range_begin = target_pci_config.bus_range_begin as u8; // Create accessor for VirtualRootComplex, similar to RootComplex - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] { use crate::pci::config_accessors::dwc::DwcConfigAccessor; use crate::platform; @@ -363,7 +363,7 @@ impl ZoneInner { } } - #[cfg(feature = "loongarch64_pcie")] + #[cfg(loongarch64_pcie)] { use crate::pci::config_accessors::loongarch64::LoongArchConfigAccessor; use alloc::sync::Arc; @@ -377,7 +377,7 @@ impl ZoneInner { self.vpci_bus_mut().set_accessor(accessor); } - #[cfg(feature = "ecam_pcie")] + #[cfg(ecam_pcie)] { use crate::pci::config_accessors::ecam::EcamConfigAccessor; use alloc::sync::Arc; @@ -420,8 +420,8 @@ impl ZoneInner { info!("set bdf {:#?} to vbdf {:#?}", bdf, vbdf); #[cfg(any( - all(feature = "iommu", target_arch = "aarch64"), - all(feature = "iommu", target_arch = "riscv64"), + all(iommu, target_arch = "aarch64"), + all(iommu, target_arch = "riscv64"), target_arch = "x86_64" ))] { @@ -433,13 +433,13 @@ impl ZoneInner { let device_id = (dev_config.bus as usize) << 8 | (dev_config.device as usize) << 3 | dev_config.function as usize; - #[cfg(feature = "share_s2pt")] + #[cfg(share_s2pt)] crate::device::iommu::iommu_add_device_with_root_pt_addr( _zone_id, device_id as _, self.gpm().root_paddr(), ); - #[cfg(not(feature = "share_s2pt"))] + #[cfg(not(share_s2pt))] crate::device::iommu::iommu_add_device_with_root_pt_addr( _zone_id, device_id as _, @@ -490,7 +490,7 @@ impl ZoneInner { } } else { warn!("can not find dev {:#?} in GLOBAL_PCIE_LIST (not detected during enumeration)", bdf); - #[cfg(feature = "ecam_pcie")] + #[cfg(ecam_pcie)] { use crate::pci::pci_struct::VirtualPciConfigSpace; use crate::pci::vpci_dev::{get_handler, VpciDevType}; @@ -519,7 +519,7 @@ impl ZoneInner { // After processing all devices for this domain, allocate hardware MSI bits if domain_msi_count > 0 { - #[cfg(all(feature = "dwc_msi", feature = "dwc_pcie"))] + #[cfg(all(dwc_msi, dwc_pcie))] { // Get the DW MSI domain allocator and allocate hwbit if let Some(mut domain_lock) = @@ -548,7 +548,7 @@ impl ZoneInner { } } - #[cfg(not(feature = "dwc_msi"))] + #[cfg(not(dwc_msi))] { // Without dwc_msi feature, just register without hardware bit allocation self.vpci_bus_mut().add_msi_count_for_domain( @@ -563,7 +563,7 @@ impl ZoneInner { Ok(()) } - #[cfg(all(feature = "pci_init_delay", feature = "dwc_pcie"))] + #[cfg(all(pci_init_delay, dwc_pcie))] pub fn virtual_pci_dbi_pref_init( &mut self, pci_rootcomplex_config: &[HvPciConfig; CONFIG_PCI_BUS_MAXNUM], @@ -587,20 +587,20 @@ impl ZoneInner { } } - #[cfg(all(feature = "pci_init_delay", feature = "dwc_pcie"))] + #[cfg(all(pci_init_delay, dwc_pcie))] pub fn virtual_pci_mmio_init_delay( &mut self, pci_rootcomplex_config: &[HvPciConfig; CONFIG_PCI_BUS_MAXNUM], _num_pci_config: usize, ) { - #[cfg(feature = "loongarch64_pcie")] + #[cfg(loongarch64_pcie)] let mut emergency_map_regions: alloc::vec::Vec<(usize, usize)> = alloc::vec::Vec::new(); for rootcomplex_config in pci_rootcomplex_config { if rootcomplex_config.ecam_base == 0 { continue; } - #[cfg(feature = "ecam_pcie")] + #[cfg(ecam_pcie)] { use crate::pci::pci_handler::mmio_vpci_handler; @@ -611,7 +611,7 @@ impl ZoneInner { rootcomplex_config.ecam_base as usize, ); } - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] { use crate::memory::mmio_generic_handler; use crate::pci::config_accessors::dwc_atu::AtuConfig; @@ -700,7 +700,7 @@ impl ZoneInner { ); } } - #[cfg(feature = "loongarch64_pcie")] + #[cfg(loongarch64_pcie)] { use crate::pci::pci_handler::mmio_vpci_direct_handler; @@ -715,11 +715,7 @@ impl ZoneInner { rootcomplex_config.ecam_size as usize, )); } - #[cfg(not(any( - feature = "ecam_pcie", - feature = "dwc_pcie", - feature = "loongarch64_pcie" - )))] + #[cfg(not(any(ecam_pcie, dwc_pcie, loongarch64_pcie)))] { warn!( "No extend config found for base 0x{:x}", @@ -818,11 +814,11 @@ pub fn zone_create(config: &HvZoneConfig) -> HvResult> { } zone.write().set_cpu_num(cpu_num); - #[cfg(feature = "pci")] + #[cfg(pci)] { - #[cfg(feature = "pci_init_delay")] + #[cfg(pci_init_delay)] { - #[cfg(feature = "dwc_pcie")] + #[cfg(dwc_pcie)] { let num_pci_bus = config.num_pci_bus as usize; if zone_id == 0 { @@ -841,7 +837,7 @@ pub fn zone_create(config: &HvZoneConfig) -> HvResult> { } } - #[cfg(all(feature = "pci", not(feature = "pci_init_delay")))] + #[cfg(not(pci_init_delay))] { let _ = zone.virtual_pci_mmio_init(&config.pci_config, config.num_pci_bus as usize); let _ = zone.guest_pci_init( @@ -854,7 +850,7 @@ pub fn zone_create(config: &HvZoneConfig) -> HvResult> { } } - #[cfg(feature = "viommu")] + #[cfg(viommu)] { use crate::platform::{IOMMU_SYS_BASE, IOMMU_SYS_SIZE}; // Create viommu instance and register mmio handler for target zone. @@ -880,7 +876,7 @@ pub fn zone_create(config: &HvZoneConfig) -> HvResult> { // #[cfg(target_arch = "aarch64")] // zone.ivc_init(config.ivc_config()); - #[cfg(all(feature = "iommu", target_arch = "aarch64"))] + #[cfg(all(iommu, target_arch = "aarch64"))] zone.iommu_pt_init(config.memory_regions(), &config.arch_config)?; /* loongarch page table emergency */ diff --git a/tools/gen_cargo_config.sh b/tools/gen_cargo_config.sh deleted file mode 100755 index 3bb6832ab..000000000 --- a/tools/gen_cargo_config.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -set -e - -CONFIG_TOML=".cargo/config.toml" -echo "" > $CONFIG_TOML - -function find_hvisor_src { - # find in ./CHANGELOG.md ../CHANGELOG.md - ret=$(find . -name CHANGELOG.md -exec grep -l "hvisor" {} \;) - if [ -z "$ret" ]; then - ret=$(find .. -name CHANGELOG.md -exec grep -l "hvisor" {} \;) - fi - if [ -z "$ret" ]; then - echo "." - fi - echo $(dirname $ret) -} - -CONFIG_TOML_TEMPLATE="platform/$ARCH/$BOARD/cargo/config.template.toml" -CONFIG_TOML_TEMPLATE=$(realpath $CONFIG_TOML_TEMPLATE) -TEMPLATE=$(cat $CONFIG_TOML_TEMPLATE) -LD_SCRIPT="platform/$ARCH/$BOARD/linker.ld" -LD_SCRIPT=$(realpath $LD_SCRIPT) -HVISOR_SRC=$(realpath $(find_hvisor_src)) -HOST_ARCH=$(uname -m) -OS=$(uname -s) -OS_VERSION=$(uname -r) -DISTRO=unknown -if [ -f /etc/os-release ]; then - DISTRO=$(grep '^ID=' /etc/os-release | cut -d= -f2) -fi - -echo "# Generated by gen_cargo_config.sh at $(date) ($(whoami)@$(hostname))" >> $CONFIG_TOML -echo "# BASH_VERSION = $BASH_VERSION" >> $CONFIG_TOML -echo "# HOST_ARCH = $HOST_ARCH" >> $CONFIG_TOML -echo "# OS_INFO = $OS $OS_VERSION $DISTRO" >> $CONFIG_TOML -echo "# ARCH = $ARCH" >> $CONFIG_TOML -echo "# BOARD = $BOARD" >> $CONFIG_TOML -echo "# FEATURES = $FEATURES" >> $CONFIG_TOML -echo "# HVISOR_SRC = $HVISOR_SRC" >> $CONFIG_TOML -echo "# LD_SCRIPT = $LD_SCRIPT" >> $CONFIG_TOML -echo "# TEMPLATE = $CONFIG_TOML_TEMPLATE" >> $CONFIG_TOML -echo "" >> $CONFIG_TOML - -# place holders: __XXX__ -> XXX -TEMPLATE=$(echo "$TEMPLATE" | sed -e 's/__ARCH__/'"$ARCH"'/g') -TEMPLATE=$(echo "$TEMPLATE" | sed -e 's/__BOARD__/'"$BOARD"'/g') - -echo "$TEMPLATE" >> $CONFIG_TOML - -# also generate a .config file in the HVISOR_SRC directory -CONFIG_FILE="$HVISOR_SRC/.config" -echo "ARCH=$ARCH" > $CONFIG_FILE -echo "BOARD=$BOARD" >> $CONFIG_FILE -echo "BID=$ARCH/$BOARD" >> $CONFIG_FILE -echo "FEATURES=$FEATURES" >> $CONFIG_FILE -echo "HVISOR_SRC=$HVISOR_SRC" >> $CONFIG_FILE -echo "LD_SCRIPT=$LD_SCRIPT" >> $CONFIG_FILE -echo "TEMPLATE=$CONFIG_TOML_TEMPLATE" >> $CONFIG_FILE \ No newline at end of file diff --git a/tools/gen_vscode_settings.sh b/tools/gen_vscode_settings.sh deleted file mode 100755 index 2d911faa0..000000000 --- a/tools/gen_vscode_settings.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/bash -set -e - -function find_hvisor_src { - ret=$(find . -name CHANGELOG.md -exec grep -l "hvisor" {} \;) - if [ -z "$ret" ]; then - ret=$(find .. -name CHANGELOG.md -exec grep -l "hvisor" {} \;) - fi - if [ -z "$ret" ]; then - echo "ERROR: Could not locate hvisor source root (CHANGELOG.md with 'hvisor')" >&2 - exit 1 - fi - echo "$(dirname "$ret")" -} - -HVISOR_SRC=$(realpath $(find_hvisor_src)) -cd "$HVISOR_SRC" - -if [ -n "$BID" ]; then - ARCH=$(echo "$BID" | cut -d'/' -f1) - BOARD=$(echo "$BID" | cut -d'/' -f2) -fi - -if [ -z "$ARCH" ] || [ -z "$BOARD" ]; then - echo "ERROR: ARCH/BOARD not set, please use BID=arch/board or ARCH=... BOARD=..." - exit 1 -fi - -if [ -z "$FEATURES" ]; then - FEATURES=$(./tools/read_features.sh "$ARCH" "$BOARD") -fi - -FEATURES=$(echo "$FEATURES" | tr '\n' ' ' | tr -s ' ' | sed 's/^ *//;s/ *$//') - -case "$ARCH" in - aarch64) TARGET="aarch64-unknown-none" ;; - riscv64) TARGET="riscv64gc-unknown-none-elf" ;; - loongarch64) TARGET="loongarch64-unknown-none" ;; - x86_64) TARGET="x86_64-unknown-none" ;; - *) - echo "ERROR: Unsupported ARCH value: $ARCH" - exit 1 - ;; -esac - -mkdir -p .vscode - -cat > .vscode/settings.json <&2 + exit 1 +fi + +if [[ -x "${VENV}/bin/python" ]] && "${VENV}/bin/python" -c "import kconfiglib" 2>/dev/null; then + exit 0 +fi + +python3 -m venv "${VENV}" +"${VENV}/bin/pip" install -q -r "${REQ}" diff --git a/tools/kconfig/host_config.sh b/tools/kconfig/host_config.sh new file mode 100755 index 000000000..fa40c950d --- /dev/null +++ b/tools/kconfig/host_config.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Host-side helpers that consume repo-root .config (Kconfig + metadata from kconfig_cli.py). +# Usage (from repo root, or any cwd — script cds to root): +# ./tools/kconfig/host_config.sh cargo # write .cargo/config.toml +# ./tools/kconfig/host_config.sh vscode # refresh .config + write .vscode/settings.json +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$ROOT" + +KPY="$ROOT/tools/kconfig/.venv/bin/python" +CLI="$ROOT/tools/kconfig/kconfig_cli.py" + +cmd_gen_cargo() { + local CONFIG_TOML=".cargo/config.toml" + echo "" >"$CONFIG_TOML" + + if [ ! -f .config ]; then + echo "error: missing .config (run: make defconfig)" >&2 + exit 1 + fi + local ARCH BOARD HVISOR_SRC LD_SCRIPT CONFIG_TOML_TEMPLATE + ARCH=$(grep '^# ARCH=' .config 2>/dev/null | head -1 | sed 's/^# ARCH=//') + [ -n "$ARCH" ] || ARCH=$(grep '^ARCH=' .config | head -1 | cut -d= -f2-) + BOARD=$(grep '^# BOARD=' .config 2>/dev/null | head -1 | sed 's/^# BOARD=//') + [ -n "$BOARD" ] || BOARD=$(grep '^BOARD=' .config | head -1 | cut -d= -f2-) + HVISOR_SRC=$(grep '^# HVISOR_SRC=' .config 2>/dev/null | head -1 | sed 's/^# HVISOR_SRC=//') + [ -n "$HVISOR_SRC" ] || HVISOR_SRC=$(grep '^HVISOR_SRC=' .config | head -1 | cut -d= -f2-) + LD_SCRIPT=$(grep '^# LD_SCRIPT=' .config 2>/dev/null | head -1 | sed 's/^# LD_SCRIPT=//') + [ -n "$LD_SCRIPT" ] || LD_SCRIPT=$(grep '^LD_SCRIPT=' .config | head -1 | cut -d= -f2-) + CONFIG_TOML_TEMPLATE=$(grep '^# TEMPLATE=' .config 2>/dev/null | head -1 | sed 's/^# TEMPLATE=//') + [ -n "$CONFIG_TOML_TEMPLATE" ] || CONFIG_TOML_TEMPLATE=$(grep '^TEMPLATE=' .config | head -1 | cut -d= -f2-) + if [ -z "$ARCH" ] || [ -z "$BOARD" ] || [ -z "$HVISOR_SRC" ] || [ -z "$LD_SCRIPT" ] || [ -z "$CONFIG_TOML_TEMPLATE" ]; then + echo "error: .config missing ARCH/BOARD/HVISOR_SRC/LD_SCRIPT/TEMPLATE lines (run: make defconfig)" >&2 + exit 1 + fi + local TEMPLATE + TEMPLATE=$(cat "$CONFIG_TOML_TEMPLATE") + local HOST_ARCH OS OS_VERSION DISTRO + HOST_ARCH=$(uname -m) + OS=$(uname -s) + OS_VERSION=$(uname -r) + DISTRO=unknown + if [ -f /etc/os-release ]; then + DISTRO=$(grep '^ID=' /etc/os-release | cut -d= -f2) + fi + + { + echo "# Generated by tools/kconfig/host_config.sh cargo at $(date) ($(whoami)@$(hostname))" + echo "# BASH_VERSION = $BASH_VERSION" + echo "# HOST_ARCH = $HOST_ARCH" + echo "# OS_INFO = $OS $OS_VERSION $DISTRO" + echo "# ARCH = $ARCH" + echo "# BOARD = $BOARD" + echo "# (platform toggles come from .config CONFIG_* via build.rs)" + echo "# HVISOR_SRC = $HVISOR_SRC" + echo "# LD_SCRIPT = $LD_SCRIPT" + echo "# TEMPLATE = $CONFIG_TOML_TEMPLATE" + echo "" + } >>"$CONFIG_TOML" + + TEMPLATE=$(echo "$TEMPLATE" | sed -e 's/__ARCH__/'"$ARCH"'/g') + TEMPLATE=$(echo "$TEMPLATE" | sed -e 's/__BOARD__/'"$BOARD"'/g') + echo "$TEMPLATE" >>"$CONFIG_TOML" +} + +cmd_vscode() { + local ARCH="${ARCH:-}" BOARD="${BOARD:-}" + if [ -n "${BID:-}" ]; then + ARCH=$(echo "$BID" | cut -d'/' -f1) + BOARD=$(echo "$BID" | cut -d'/' -f2) + fi + if [ -z "$ARCH" ] || [ -z "$BOARD" ]; then + echo "ERROR: ARCH/BOARD not set, use BID=arch/board or ARCH=... BOARD=..." >&2 + exit 1 + fi + if [ ! -x "$KPY" ]; then + echo "ERROR: tools/kconfig/.venv missing. Run: ./tools/kconfig/bootstrap_venv.sh" >&2 + exit 1 + fi + ARCH="$ARCH" BOARD="$BOARD" "$KPY" "$CLI" defconfig + + local CFG_JSON + CFG_JSON=$("$KPY" "$CLI" vscode-cfgs .config kconfig/cfg_map.toml) + + local TARGET + case "$ARCH" in + aarch64) TARGET="aarch64-unknown-none" ;; + riscv64) TARGET="riscv64gc-unknown-none-elf" ;; + loongarch64) TARGET="loongarch64-unknown-none" ;; + x86_64) TARGET="x86_64-unknown-none" ;; + *) + echo "ERROR: Unsupported ARCH value: $ARCH" >&2 + exit 1 + ;; + esac + + mkdir -p .vscode + cat >.vscode/settings.json <&2 + exit 2 + ;; +esac diff --git a/tools/kconfig/kconfig_cli.py b/tools/kconfig/kconfig_cli.py new file mode 100644 index 000000000..ffef3f699 --- /dev/null +++ b/tools/kconfig/kconfig_cli.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""Kconfig entrypoints: defconfig, menuconfig, vscode-cfgs.""" +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +import tomllib +from pathlib import Path + +_KCONF_ARCH_TO_DIR: dict[str, str] = { + "ARCH_AARCH64": "aarch64", + "ARCH_RISCV64": "riscv64", + "ARCH_LOONGARCH64": "loongarch64", + "ARCH_X86_64": "x86_64", +} + + +def arch_from_kconf(kconf) -> str | None: + for sym_name, arch in _KCONF_ARCH_TO_DIR.items(): + sym = kconf.syms.get(sym_name) + if sym is not None and sym.str_value == "y": + return arch + return None + + +def write_root_dot_config(root: Path, arch: str, board: str, kconf) -> None: + """Write repo-root `.config`: comment metadata (Kconfig-safe) then kconfiglib body.""" + cfg = root / ".config" + derived = arch_from_kconf(kconf) + if derived is None: + raise SystemExit( + "Kconfig: no architecture selected; enable exactly one of " + "ARCH_AARCH64 / ARCH_RISCV64 / ARCH_LOONGARCH64 / ARCH_X86_64 in defconfig." + ) + if derived != arch: + raise SystemExit( + f"Kconfig architecture ({derived}) does not match " + f"platform path arch ({arch}) for board {board!r}. " + "Fix kconfig/defconfig or pick the correct ARCH/BOARD." + ) + arch_out = derived + ld = (root / "platform" / arch_out / board / "linker.ld").resolve() + tmpl = (root / "platform" / arch_out / board / "cargo" / "config.template.toml").resolve() + hvisor_src = str(root.resolve()) + bid = f"{arch_out}/{board}" + kconf.write_config(str(cfg)) + body = cfg.read_text() + meta = ( + f"# ARCH={arch_out}\n" + f"# BOARD={board}\n" + f"# BID={bid}\n" + f"# HVISOR_SRC={hvisor_src}\n" + f"# LD_SCRIPT={ld}\n" + f"# TEMPLATE={tmpl}\n" + "\n" + ) + cfg.write_text(meta + body) + + +def _repo_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def cmd_defconfig() -> None: + arch = os.environ.get("ARCH", "") + board = os.environ.get("BOARD", "") + if not arch or not board: + print("error: set ARCH and BOARD", file=sys.stderr) + sys.exit(2) + root = _repo_root() + os.chdir(root) + try: + from kconfiglib import Kconfig + except ImportError: + print("error: pip install kconfiglib (see tools/kconfig/requirements.txt)", file=sys.stderr) + sys.exit(1) + kconf = Kconfig(str(root / "kconfig" / "Kconfig")) + defcfg = root / "platform" / arch / board / "kconfig" / "defconfig" + if not defcfg.is_file(): + print(f"error: missing {defcfg}", file=sys.stderr) + sys.exit(1) + kconf.load_config(str(defcfg)) + write_root_dot_config(root, arch, board, kconf) + print(f"wrote .config for {arch}/{board}") + + +def cmd_menuconfig() -> None: + arch = os.environ.get("ARCH", "") + board = os.environ.get("BOARD", "") + if not arch or not board: + print("error: set ARCH and BOARD", file=sys.stderr) + sys.exit(2) + root = _repo_root() + os.chdir(root) + try: + from kconfiglib import Kconfig + except ImportError as e: + print(f"error: kconfiglib is not installed: {e}", file=sys.stderr) + print("hint: tools/kconfig/.venv/bin/pip install -r tools/kconfig/requirements.txt", file=sys.stderr) + sys.exit(1) + try: + import menuconfig as kc_menu + except ImportError as e: + print(f"error: kconfiglib TUI (menuconfig) could not load: {e}", file=sys.stderr) + print( + "hint: the UI needs Python stdlib curses. " + "Debian/Ubuntu: apt install python3-curses; Fedora: dnf install python3-curses; " + "Windows: pip install windows-curses", + file=sys.stderr, + ) + sys.exit(1) + kconf = Kconfig(str(root / "kconfig" / "Kconfig")) + cfg = root / ".config" + defcfg = root / "platform" / arch / board / "kconfig" / "defconfig" + if cfg.is_file(): + kconf.load_config(str(cfg)) + elif defcfg.is_file(): + kconf.load_config(str(defcfg)) + kc_menu.menuconfig(kconf) + write_root_dot_config(root, arch, board, kconf) + + +def cmd_vscode_cfgs(cfg_path: Path, map_path: Path) -> None: + sym = tomllib.loads(map_path.read_text(encoding="utf-8"))["symbols"] + enabled: set[str] = set() + for line in cfg_path.read_text().splitlines(): + line = line.strip() + m = re.match(r"^(CONFIG_[A-Z0-9_]+)=(y|m)$", line) + if not m: + continue + k = m.group(1) + if k in sym: + enabled.add(sym[k]) + print(json.dumps(sorted(enabled))) + + +def main() -> int: + p = argparse.ArgumentParser(description="hvisor Kconfig helpers") + sub = p.add_subparsers(dest="cmd", required=True) + + sub.add_parser("defconfig", help="merge board defconfig + Kconfig into repo-root .config (env ARCH, BOARD)") + sub.add_parser( + "menuconfig", + help="interactive Kconfig UI (env ARCH, BOARD); do not name this file menuconfig.py", + ) + p_vc = sub.add_parser("vscode-cfgs", help="emit JSON array of rustc cfg names for rust-analyzer") + p_vc.add_argument("config", type=Path, help="path to repo-root .config") + p_vc.add_argument("map", type=Path, help="path to kconfig/cfg_map.toml") + + args = p.parse_args() + if args.cmd == "defconfig": + cmd_defconfig() + elif args.cmd == "menuconfig": + cmd_menuconfig() + elif args.cmd == "vscode-cfgs": + cmd_vscode_cfgs(args.config, args.map) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/kconfig/requirements.txt b/tools/kconfig/requirements.txt new file mode 100644 index 000000000..3c91d8914 --- /dev/null +++ b/tools/kconfig/requirements.txt @@ -0,0 +1,2 @@ +PySocks>=1.7.1 +kconfiglib>=14.1.0 diff --git a/tools/kconfig/save_defconfig.sh b/tools/kconfig/save_defconfig.sh new file mode 100755 index 000000000..f474d5d3a --- /dev/null +++ b/tools/kconfig/save_defconfig.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Write CONFIG_* lines from repo-root .config to the board defconfig (sorted). +set -euo pipefail +ARCH="${1:?arch}" +BOARD="${2:?board}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$ROOT" + +OUT="platform/${ARCH}/${BOARD}/kconfig/defconfig" +mkdir -p "$(dirname "$OUT")" +if [[ ! -f .config ]]; then + echo "error: no .config in repo root" >&2 + exit 1 +fi +grep '^CONFIG_' .config | LC_ALL=C sort >"$OUT" +echo "wrote $OUT" diff --git a/tools/read_features.sh b/tools/read_features.sh deleted file mode 100755 index 81b0b4841..000000000 --- a/tools/read_features.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -set -e - -ARCH=$1 -BOARD=$2 - -FEATURES="platform/$ARCH/$BOARD/cargo/features" -FEATURES=$(cat $FEATURES) -FEATURES=$(echo $FEATURES | tr '\n' ' ') -FEATURES=$(echo $FEATURES | tr -d '\r') -echo $FEATURES From daa81bf3e782d8ff3222444a1818afd2bfdba493 Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 7 Jul 2026 13:53:17 +0800 Subject: [PATCH 21/53] add pcie for rk3568 --- platform/aarch64/rk3568/board.rs | 195 ++++++++++++------ platform/aarch64/rk3568/configs/linux2.json | 3 +- .../rk3568/image/dts/rk3568_limit_zone0.dts | 184 ++++++++--------- src/pci/pci_struct.rs | 4 +- 4 files changed, 227 insertions(+), 159 deletions(-) diff --git a/platform/aarch64/rk3568/board.rs b/platform/aarch64/rk3568/board.rs index b4500a0ae..b51035673 100644 --- a/platform/aarch64/rk3568/board.rs +++ b/platform/aarch64/rk3568/board.rs @@ -57,42 +57,81 @@ pub const ROOT_ZONE_CPUS: u64 = (1 << 0) | (1 << 1); pub const ROOT_ZONE_NAME: &str = "root-linux"; pub const ROOT_ZONE_MEMORY_REGIONS: &[HvConfigMemoryRegion] = &[ - // HvConfigMemoryRegion { - // mem_type: MEM_TYPE_IO, - // physical_start: 0x3c0400000, - // virtual_start: 0x3c0400000, - // size: 0x400000, - // }, //pcie - // HvConfigMemoryRegion { - // mem_type: MEM_TYPE_IO, - // physical_start: 0xfe270000, - // virtual_start: 0xfe270000, - // size: 0x10000, - // }, //pcie + // pcie@fe260000 (domain 0) + HvConfigMemoryRegion { + mem_type: MEM_TYPE_IO, + physical_start: 0xf4000000, + virtual_start: 0xf4000000, + size: 0x100000, + }, + HvConfigMemoryRegion { + mem_type: MEM_TYPE_IO, + physical_start: 0xf4100000, + virtual_start: 0xf4100000, + size: 0x100000, + }, + HvConfigMemoryRegion { + mem_type: MEM_TYPE_IO, + physical_start: 0xf4200000, + virtual_start: 0xf4200000, + size: 0x1e00000, + }, + HvConfigMemoryRegion { + mem_type: MEM_TYPE_IO, + physical_start: 0x300000000, + virtual_start: 0x300000000, + size: 0x40000000, + }, + // pcie@fe270000 (domain 1) HvConfigMemoryRegion { mem_type: MEM_TYPE_IO, physical_start: 0xf2000000, virtual_start: 0xf2000000, size: 0x100000, - }, //pcie + }, HvConfigMemoryRegion { mem_type: MEM_TYPE_IO, physical_start: 0xf2100000, virtual_start: 0xf2100000, size: 0x100000, - }, //pcie + }, HvConfigMemoryRegion { mem_type: MEM_TYPE_IO, physical_start: 0xf2200000, virtual_start: 0xf2200000, size: 0x1e00000, - }, //pcie + }, HvConfigMemoryRegion { mem_type: MEM_TYPE_IO, physical_start: 0x340000000, virtual_start: 0x340000000, size: 0x40000000, - }, //pcie + }, + // pcie@fe280000 (domain 2) + HvConfigMemoryRegion { + mem_type: MEM_TYPE_IO, + physical_start: 0xf0000000, + virtual_start: 0xf0000000, + size: 0x100000, + }, + HvConfigMemoryRegion { + mem_type: MEM_TYPE_IO, + physical_start: 0xf0100000, + virtual_start: 0xf0100000, + size: 0x100000, + }, + HvConfigMemoryRegion { + mem_type: MEM_TYPE_IO, + physical_start: 0xf0200000, + virtual_start: 0xf0200000, + size: 0x1e00000, + }, + HvConfigMemoryRegion { + mem_type: MEM_TYPE_IO, + physical_start: 0x380000000, + virtual_start: 0x380000000, + size: 0x40000000, + }, HvConfigMemoryRegion { mem_type: MEM_TYPE_IO, physical_start: 0xfdcb8000, @@ -278,7 +317,7 @@ pub const ROOT_ZONE_MEMORY_REGIONS: &[HvConfigMemoryRegion] = &[ pub const IRQ_WAKEUP_VIRTIO_DEVICE: usize = 32 + 0x20; pub const ROOT_ZONE_IRQS_BITMAP: &[BitmapWord] = &get_irqs_bitmap(&[ 0x84, 0x98, 0x40, 0x104, 0x105, 0x106, 0x107, 0x2d, 0x2e, 0x2b, 0x2a, 0x29, 0x33, 0x96, 0x11c, - 0x44, 0x43, 0x42, 0x41, 0x8d, + 0x44, 0x43, 0x42, 0x41, 0x8d, 0x48, 0x49, 0x9d, 0x9e, 0xa2, 0xa3, ]); pub const ROOT_ARCH_ZONE_CONFIG: HvArchZoneConfig = HvArchZoneConfig { @@ -293,22 +332,22 @@ pub const ROOT_ARCH_ZONE_CONFIG: HvArchZoneConfig = HvArchZoneConfig { }), }; pub const ROOT_PCI_CONFIG: &[HvPciConfig] = &[ - // HvPciConfig { - // ecam_base: 0xfe260000, - // ecam_size: 0x400000, - // io_base: 0xf4100000, - // io_size: 0x100000, - // pci_io_base: 0xf4100000, - // mem32_base: 0xf4200000, - // mem32_size: 0x1e00000, - // pci_mem32_base: 0xf4200000, - // mem64_base: 0x300000000, - // mem64_size: 0x40000000, - // pci_mem64_base: 0x300000000, - // bus_range_begin: 0x0, - // bus_range_end: 0x10, - // domain: 0x0, - // }, + HvPciConfig { + ecam_base: 0x3c0000000, + ecam_size: 0x400000, + io_base: 0xf4100000, + io_size: 0x100000, + pci_io_base: 0xf4100000, + mem32_base: 0xf4200000, + mem32_size: 0x1e00000, + pci_mem32_base: 0xf4200000, + mem64_base: 0x300000000, + mem64_size: 0x40000000, + pci_mem64_base: 0x300000000, + bus_range_begin: 0x0, + bus_range_end: 0x10, + domain: 0x0, + }, HvPciConfig { ecam_base: 0x3c0400000, ecam_size: 0x400000, @@ -325,40 +364,68 @@ pub const ROOT_PCI_CONFIG: &[HvPciConfig] = &[ bus_range_end: 0x1f, domain: 0x1, }, - // HvPciConfig { - // ecam_base: 0xfe280000, - // ecam_size: 0x400000, - // io_base: 0xf0100000, - // io_size: 0x100000, - // pci_io_base: 0xf0100000, - // mem32_base: 0xf0200000, - // mem32_size: 0x1e00000, - // pci_mem32_base: 0xf0200000, - // mem64_base: 0x380000000, - // mem64_size: 0x40000000, - // pci_mem64_base: 0x380000000, - // bus_range_begin: 0x20, - // bus_range_end: 0x2f, - // domain: 0x2, - // } + HvPciConfig { + ecam_base: 0x3c0800000, + ecam_size: 0x400000, + io_base: 0xf0100000, + io_size: 0x100000, + pci_io_base: 0xf0100000, + mem32_base: 0xf0200000, + mem32_size: 0x1e00000, + pci_mem32_base: 0xf0200000, + mem64_base: 0x380000000, + mem64_size: 0x40000000, + pci_mem64_base: 0x380000000, + bus_range_begin: 0x20, + bus_range_end: 0x2f, + domain: 0x2, + }, ]; pub const ROOT_ZONE_IVC_CONFIG: [HvIvcConfig; 0] = []; -pub const ROOT_DWC_ATU_CONFIG: &[HvDwcAtuConfig] = &[HvDwcAtuConfig { - ecam_base: 0x3c0400000, - dbi_base: 0x3c0400000, - dbi_size: 0x10000, - apb_base: 0xfe270000, - apb_size: 0x10000, - cfg_base: 0xf2000000, - cfg_size: 0x80000 * 2, - io_cfg_atu_shared: 0, - io_atu_index: 0, - dw_msi_irq: 0, -}]; +pub const ROOT_DWC_ATU_CONFIG: &[HvDwcAtuConfig] = &[ + HvDwcAtuConfig { + ecam_base: 0x3c0000000, + dbi_base: 0x3c0000000, + dbi_size: 0x10000, + apb_base: 0xfe260000, + apb_size: 0x10000, + cfg_base: 0xf4000000, + cfg_size: 0x80000 * 2, + io_cfg_atu_shared: 0, + io_atu_index: 0, + dw_msi_irq: 0x49, // pcie@fe260000 msg irq + }, + HvDwcAtuConfig { + ecam_base: 0x3c0400000, + dbi_base: 0x3c0400000, + dbi_size: 0x10000, + apb_base: 0xfe270000, + apb_size: 0x10000, + cfg_base: 0xf2000000, + cfg_size: 0x80000 * 2, + io_cfg_atu_shared: 0, + io_atu_index: 0, + dw_msi_irq: 0x9e, // pcie@fe270000 msg irq + }, + HvDwcAtuConfig { + ecam_base: 0x3c0800000, + dbi_base: 0x3c0800000, + dbi_size: 0x10000, + apb_base: 0xfe280000, + apb_size: 0x10000, + cfg_base: 0xf0000000, + cfg_size: 0x80000 * 2, + io_cfg_atu_shared: 0, + io_atu_index: 0, + dw_msi_irq: 0xa3, // pcie@fe280000 msg irq + }, +]; -pub const ROOT_PCI_DEVS: [HvPciDevConfig; 2] = [ - pci_dev!(0x1, 0x00, 0x0, 0x0 => 0x00, 0x0, 0x0, VpciDevType::Physical), - pci_dev!(0x1, 0x01, 0x0, 0x0 => 0x01, 0x0, 0x0, VpciDevType::Physical), +pub const ROOT_PCI_DEVS: [HvPciDevConfig; 4] = [ + pci_dev!(0x1, 0x10, 0x0, 0x0 => 0x10, 0x0, 0x0, VpciDevType::Physical), + pci_dev!(0x1, 0x11, 0x0, 0x0 => 0x11, 0x0, 0x0, VpciDevType::Physical), + pci_dev!(0x2, 0x20, 0x0, 0x0 => 0x20, 0x0, 0x0, VpciDevType::Physical), + pci_dev!(0x2, 0x21, 0x0, 0x0 => 0x21, 0x0, 0x0, VpciDevType::Physical), ]; diff --git a/platform/aarch64/rk3568/configs/linux2.json b/platform/aarch64/rk3568/configs/linux2.json index 46d401055..0c64be5bb 100644 --- a/platform/aarch64/rk3568/configs/linux2.json +++ b/platform/aarch64/rk3568/configs/linux2.json @@ -44,5 +44,6 @@ "gicr_size": "0xc0000", "gits_base": "0x0", "gits_size": "0x0" - } + }, + "pci_config":[] } diff --git a/platform/aarch64/rk3568/image/dts/rk3568_limit_zone0.dts b/platform/aarch64/rk3568/image/dts/rk3568_limit_zone0.dts index 420426e81..3f872905b 100644 --- a/platform/aarch64/rk3568/image/dts/rk3568_limit_zone0.dts +++ b/platform/aarch64/rk3568/image/dts/rk3568_limit_zone0.dts @@ -140,47 +140,47 @@ }; - // pcie@fe260000 { - // power-domains = <0x22 0x0f>; - // #address-cells = <0x03>; - // phy-names = "pcie-phy"; - // bus-range = <0x00 0x0f>; - // clock-names = "aclk_mst\0aclk_slv\0aclk_dbi\0pclk\0aux"; - // reg-names = "pcie-dbi\0pcie-apb"; - // num-ob-windows = <0x02>; - // resets = <0x20 0xa1>; - // interrupts = <0x00 0x4b 0x04 0x00 0x4a 0x04 0x00 0x49 0x04 0x00 0x48 0x04 0x00 0x47 0x04>; - // clocks = <0x20 0x81 0x20 0x82 0x20 0x83 0x20 0x84 0x20 0x85>; - // interrupt-map = <0x00 0x00 0x00 0x01 0xac 0x00 0x00 0x00 0x00 0x02 0xac 0x01 0x00 0x00 0x00 0x03 0xac 0x02 0x00 0x00 0x00 0x04 0xac 0x03>; - // #size-cells = <0x02>; - // max-link-speed = <0x02>; - // device_type = "pci"; - // interrupt-map-mask = <0x00 0x00 0x00 0x07>; - // reset-gpios = <0x83 0x11 0x00>; - // num-lanes = <0x01>; - // compatible = "rockchip,rk3568-pcie\0snps,dw-pcie"; - // ranges = <0x800 0x00 0xf4000000 0x00 0xf4000000 0x00 0x100000 0x81000000 0x00 0xf4100000 0x00 0xf4100000 0x00 0x100000 0x82000000 0x00 0xf4200000 0x00 0xf4200000 0x00 0x1e00000 0xc3000000 0x03 0x00 0x03 0x00 0x00 0x40000000>; - // msi-map = <0x00 0xad 0x00 0x1000>; - // #interrupt-cells = <0x01>; - // status = "okay"; - // interrupt-names = "sys\0pmc\0msg\0legacy\0err"; - // phys = <0x24 0x02>; - // num-viewport = <0x08>; - // reg = <0x03 0xc0000000 0x00 0x400000 0x00 0xfe260000 0x00 0x10000>; - // linux,pci-domain = <0x00>; - // phandle = <0x19d>; - // reset-names = "pipe"; - // num-ib-windows = <0x06>; - - // legacy-interrupt-controller { - // #address-cells = <0x00>; - // interrupts = <0x00 0x48 0x01>; - // interrupt-parent = <0x01>; - // #interrupt-cells = <0x01>; - // phandle = <0xac>; - // interrupt-controller; - // }; - // }; + pcie@fe260000 { + power-domains = <0x22 0x0f>; + #address-cells = <0x03>; + phy-names = "pcie-phy"; + bus-range = <0x00 0x0f>; + clock-names = "aclk_mst\0aclk_slv\0aclk_dbi\0pclk\0aux"; + reg-names = "pcie-dbi\0pcie-apb"; + num-ob-windows = <0x02>; + resets = <0x20 0xa1>; + interrupts = <0x00 0x4b 0x04 0x00 0x4a 0x04 0x00 0x49 0x04 0x00 0x48 0x04 0x00 0x47 0x04>; + clocks = <0x20 0x81 0x20 0x82 0x20 0x83 0x20 0x84 0x20 0x85>; + interrupt-map = <0x00 0x00 0x00 0x01 0xac 0x00 0x00 0x00 0x00 0x02 0xac 0x01 0x00 0x00 0x00 0x03 0xac 0x02 0x00 0x00 0x00 0x04 0xac 0x03>; + #size-cells = <0x02>; + max-link-speed = <0x02>; + device_type = "pci"; + interrupt-map-mask = <0x00 0x00 0x00 0x07>; + reset-gpios = <0x83 0x11 0x00>; + num-lanes = <0x01>; + compatible = "rockchip,rk3568-pcie\0snps,dw-pcie"; + ranges = <0x800 0x00 0xf4000000 0x00 0xf4000000 0x00 0x100000 0x81000000 0x00 0xf4100000 0x00 0xf4100000 0x00 0x100000 0x82000000 0x00 0xf4200000 0x00 0xf4200000 0x00 0x1e00000 0xc3000000 0x03 0x00 0x03 0x00 0x00 0x40000000>; + msi-map = <0x00 0xad 0x00 0x1000>; + #interrupt-cells = <0x01>; + status = "okay"; + interrupt-names = "sys\0pmc\0msg\0legacy\0err"; + phys = <0x24 0x02>; + num-viewport = <0x08>; + reg = <0x03 0xc0000000 0x00 0x400000 0x00 0xfe260000 0x00 0x10000>; + linux,pci-domain = <0x00>; + phandle = <0x19d>; + reset-names = "pipe"; + num-ib-windows = <0x06>; + + legacy-interrupt-controller { + #address-cells = <0x00>; + interrupts = <0x00 0x48 0x01>; + interrupt-parent = <0x01>; + #interrupt-cells = <0x01>; + phandle = <0xac>; + interrupt-controller; + }; + }; syscon@fdcb8000 { compatible = "rockchip,pcie30-phy-grf\0syscon"; @@ -259,48 +259,48 @@ }; }; - // pcie@fe280000 { - // power-domains = <0x22 0x0f>; - // vpcie3v3-supply = <0xb0>; - // #address-cells = <0x03>; - // phy-names = "pcie-phy"; - // bus-range = <0x20 0x2f>; - // clock-names = "aclk_mst\0aclk_slv\0aclk_dbi\0pclk\0aux"; - // reg-names = "pcie-dbi\0pcie-apb"; - // num-ob-windows = <0x02>; - // resets = <0x20 0xc1>; - // interrupts = <0x00 0xa5 0x04 0x00 0xa4 0x04 0x00 0xa3 0x04 0x00 0xa2 0x04 0x00 0xa1 0x04>; - // clocks = <0x20 0x8f 0x20 0x90 0x20 0x91 0x20 0x92 0x20 0x93>; - // interrupt-map = <0x00 0x00 0x00 0x01 0xb1 0x00 0x00 0x00 0x00 0x02 0xb1 0x01 0x00 0x00 0x00 0x03 0xb1 0x02 0x00 0x00 0x00 0x04 0xb1 0x03>; - // #size-cells = <0x02>; - // max-link-speed = <0x03>; - // device_type = "pci"; - // interrupt-map-mask = <0x00 0x00 0x00 0x07>; - // reset-gpios = <0xb2 0x1e 0x00>; - // num-lanes = <0x02>; - // compatible = "rockchip,rk3568-pcie\0snps,dw-pcie"; - // ranges = <0x800 0x00 0xf0000000 0x00 0xf0000000 0x00 0x100000 0x81000000 0x00 0xf0100000 0x00 0xf0100000 0x00 0x100000 0x82000000 0x00 0xf0200000 0x00 0xf0200000 0x00 0x1e00000 0xc3000000 0x03 0x80000000 0x03 0x80000000 0x00 0x40000000>; - // msi-map = <0x2000 0xad 0x2000 0x1000>; - // #interrupt-cells = <0x01>; - // status = "okay"; - // interrupt-names = "sys\0pmc\0msg\0legacy\0err"; - // phys = <0xaf>; - // num-viewport = <0x08>; - // reg = <0x03 0xc0800000 0x00 0x400000 0x00 0xfe280000 0x00 0x10000>; - // linux,pci-domain = <0x02>; - // phandle = <0x19f>; - // reset-names = "pipe"; - // num-ib-windows = <0x06>; - - // legacy-interrupt-controller { - // #address-cells = <0x00>; - // interrupts = <0x00 0xa2 0x01>; - // interrupt-parent = <0x01>; - // #interrupt-cells = <0x01>; - // phandle = <0xb1>; - // interrupt-controller; - // }; - // }; + pcie@fe280000 { + power-domains = <0x22 0x0f>; + vpcie3v3-supply = <0xb0>; + #address-cells = <0x03>; + phy-names = "pcie-phy"; + bus-range = <0x20 0x2f>; + clock-names = "aclk_mst\0aclk_slv\0aclk_dbi\0pclk\0aux"; + reg-names = "pcie-dbi\0pcie-apb"; + num-ob-windows = <0x02>; + resets = <0x20 0xc1>; + interrupts = <0x00 0xa5 0x04 0x00 0xa4 0x04 0x00 0xa3 0x04 0x00 0xa2 0x04 0x00 0xa1 0x04>; + clocks = <0x20 0x8f 0x20 0x90 0x20 0x91 0x20 0x92 0x20 0x93>; + interrupt-map = <0x00 0x00 0x00 0x01 0xb1 0x00 0x00 0x00 0x00 0x02 0xb1 0x01 0x00 0x00 0x00 0x03 0xb1 0x02 0x00 0x00 0x00 0x04 0xb1 0x03>; + #size-cells = <0x02>; + max-link-speed = <0x03>; + device_type = "pci"; + interrupt-map-mask = <0x00 0x00 0x00 0x07>; + reset-gpios = <0xb2 0x1e 0x00>; + num-lanes = <0x02>; + compatible = "rockchip,rk3568-pcie\0snps,dw-pcie"; + ranges = <0x800 0x00 0xf0000000 0x00 0xf0000000 0x00 0x100000 0x81000000 0x00 0xf0100000 0x00 0xf0100000 0x00 0x100000 0x82000000 0x00 0xf0200000 0x00 0xf0200000 0x00 0x1e00000 0xc3000000 0x03 0x80000000 0x03 0x80000000 0x00 0x40000000>; + msi-map = <0x2000 0xad 0x2000 0x1000>; + #interrupt-cells = <0x01>; + status = "okay"; + interrupt-names = "sys\0pmc\0msg\0legacy\0err"; + phys = <0xaf>; + num-viewport = <0x08>; + reg = <0x03 0xc0800000 0x00 0x400000 0x00 0xfe280000 0x00 0x10000>; + linux,pci-domain = <0x02>; + phandle = <0x19f>; + reset-names = "pipe"; + num-ib-windows = <0x06>; + + legacy-interrupt-controller { + #address-cells = <0x00>; + interrupts = <0x00 0xa2 0x01>; + interrupt-parent = <0x01>; + #interrupt-cells = <0x01>; + phandle = <0xb1>; + interrupt-controller; + }; + }; sdhci@fe310000 { clock-names = "core\0bus\0axi\0block\0timer"; @@ -391,7 +391,7 @@ max-frequency = <0x8f0d180>; reset-names = "reset"; }; - + memory { device_type = "memory"; reg = <0x00 0x200000 0x00 0x8200000 0x00 0x9400000 0x00 0xe6c00000 0x01 0xf0000000 0x00 0x10000000>; @@ -652,13 +652,13 @@ phandle = <0x01>; interrupt-controller; - // interrupt-controller@fd440000 { - // msi-controller; - // compatible = "arm,gic-v3-its"; - // reg = <0x00 0xfd440000 0x00 0x20000>; - // phandle = <0xad>; - // #msi-cells = <0x01>; - // }; + interrupt-controller@fd440000 { + msi-controller; + compatible = "arm,gic-v3-its"; + reg = <0x00 0xfd440000 0x00 0x20000>; + phandle = <0xad>; + #msi-cells = <0x01>; + }; }; @@ -1652,4 +1652,4 @@ interrupt-parent = <0x01>; interrupts = <0x00 0x20 0x01>; }; -}; \ No newline at end of file +}; diff --git a/src/pci/pci_struct.rs b/src/pci/pci_struct.rs index 2ec58c853..a84327f27 100644 --- a/src/pci/pci_struct.rs +++ b/src/pci/pci_struct.rs @@ -1659,13 +1659,13 @@ impl PciIterator { let address = self.address(parent_bus, bdf); let pci_addr_base = self.get_pci_addr_base(bdf); - // info!("get node {:x} {:#?}", address, bdf); + info!("get node {:x} {:#?}", address, bdf); let region = PciConfigMmio::new(address, CONFIG_LENTH); let pci_header = PciConfigHeader::new_with_region(region); let (vender_id, device_id) = pci_header.id(); - // warn!("vender_id {:#x}", vender_id); + warn!("vender_id {:#x}", vender_id); // Check if device exists if vender_id == 0xffff || self.accessor.skip_device(bdf) { From 7909fcc34f2d6d8ca640959587c1580dcb85d430 Mon Sep 17 00:00:00 2001 From: ray Date: Tue, 7 Jul 2026 14:10:26 +0800 Subject: [PATCH 22/53] fix ci bug --- Jenkinsfile | 14 +++++++------- Makefile | 28 ++++++++++++++++++++-------- jenkins/ci.yaml | 1 - jenkins/ci_runner.py | 12 ++++++++---- 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 911d9f920..dc0248f69 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -205,6 +205,7 @@ pipeline { LOONGARCH64_TOOLCHAIN_PATH = '/home/light/DEMO/toolchain/loongarch_cross_tools' // All toolchain bins on PATH; same for every matrix cell (no per-arch selection). TOOLCHAIN_PATHS = "${env.RISCV_TOOLCHAIN_PATH}/bin:${env.AARCH64_TOOLCHAIN_PATH}/bin:${env.LOONGARCH64_TOOLCHAIN_PATH}/bin" + TFTP_DIR = '/home/light/tftp' } stages { @@ -385,15 +386,14 @@ pipeline { "${prepareScript}" """ } else if (mode == 'board') { - def tftpDir = (testsCfg.tftp_dir ?: '/home/light/tftp').toString() - echo "Deploy TFTP artifacts [BID=${env.BID}, TFTP_DIR=${tftpDir}]" + echo "Deploy TFTP artifacts [BID=${env.BID}, TFTP_DIR=${env.TFTP_DIR}]" sh """ - TFTP_DIR="${tftpDir}" - mkdir -p "\$TFTP_DIR" + export TERM=\${TERM:-xterm} ${toolchainPathShell()} - make cp ARCH=${arch} BOARD=${board} MODE=release TFTP_DIR="\$TFTP_DIR" - cp platform/${arch}/${board}/image/dts/rk3568_limit_zone0.dtb "\$TFTP_DIR/" - cp ${kdir}/arch/arm64/boot/Image "\$TFTP_DIR/" + mkdir -p "${env.TFTP_DIR}" + make cp ARCH=${arch} BOARD=${board} MODE=release TFTP_DIR="${env.TFTP_DIR}" + cp platform/${arch}/${board}/image/dts/rk3568_limit_zone0.dtb "${env.TFTP_DIR}/" + cp ${kdir}/arch/arm64/boot/Image "${env.TFTP_DIR}/" """ } else { error("jenkins/ci.yaml BID=${env.BID}: unsupported tests.mode='${mode}'") diff --git a/Makefile b/Makefile index 4fb832a86..d49f9b79d 100644 --- a/Makefile +++ b/Makefile @@ -66,13 +66,22 @@ ifeq ($(MODE), release) build_args += --release endif -# color code -COLOR_GREEN := $(shell tput setaf 2) -COLOR_RED := $(shell tput setaf 1) -COLOR_YELLOW := $(shell tput setaf 3) -COLOR_BLUE := $(shell tput setaf 4) -COLOR_BOLD := $(shell tput bold) -COLOR_RESET := $(shell tput sgr0) +# color code (skip tput when TERM is unset, e.g. Jenkins sh steps) +ifdef TERM +COLOR_GREEN := $(shell tput setaf 2 2>/dev/null) +COLOR_RED := $(shell tput setaf 1 2>/dev/null) +COLOR_YELLOW := $(shell tput setaf 3 2>/dev/null) +COLOR_BLUE := $(shell tput setaf 4 2>/dev/null) +COLOR_BOLD := $(shell tput bold 2>/dev/null) +COLOR_RESET := $(shell tput sgr0 2>/dev/null) +else +COLOR_GREEN := +COLOR_RED := +COLOR_YELLOW := +COLOR_BLUE := +COLOR_BOLD := +COLOR_RESET := +endif # Defconfig / menuconfig: see tools/kconfig/kconfig_cli.py kconfig_python := tools/kconfig/.venv/bin/python @@ -187,8 +196,11 @@ monitor: jlink-server: JLinkGDBServer -select USB -if JTAG -device Cortex-A53 -port 1234 +TFTP_DIR ?= $(HOME)/tftp + cp: - cp $(hvisor_bin) ~/tftp + @mkdir -p "$(TFTP_DIR)" + cp $(hvisor_bin) "$(TFTP_DIR)/" test-pre: download-test-img chmod +x platform/$(ARCH)/$(BOARD)/test/runner.sh diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index 1bf125b4f..66b8f3d85 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -29,7 +29,6 @@ bids: baudrate: 1500000 uboot_ready_pattern: => uboot_cmd: pci enum;setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.240; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:hvisor.bin; tftp ${fdt_addr} ${serverip}:rk3568_limit_zone0.dtb; tftp ${zone0_kernel_addr} ${serverip}:Image; bootm ${loadaddr} - ${fdt_addr}; - tftp_dir: /home/light/tftp cases: - zone0_start - zone1_start diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 1a0257b9e..7848746a1 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -17,6 +17,10 @@ CaseFunc = Callable[[dict[str, Any], Terminal | None], int] +# Wait for an interactive shell prompt, not login: (getty shows that before MOTD/shell). +ZONE0_READY_PATTERN = r"root@[^\r\n]*[#$]\s|(?:\r?\n)#\s" + + def bid_log_key(bid: str) -> str: return bid.replace("/", "__") @@ -163,8 +167,8 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: if not qemu_term.wait_pattern(uboot_ready, timeout=10.0): raise TerminalTimeoutError("timed out waiting for U-Boot prompt") qemu_term.send(uboot_cmd) - if not qemu_term.wait_pattern(r"buildroot|login:|# ", timeout=180.0): - raise TerminalTimeoutError("timed out waiting for zone0 boot") + if not qemu_term.wait_pattern(ZONE0_READY_PATTERN, timeout=180.0): + raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") return 0 if cfg["mode"] == "board": board_power_cycle(cfg) @@ -184,8 +188,8 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: if not board_term.wait_pattern(uboot_ready, timeout=60.0): raise TerminalTimeoutError("timed out waiting for U-Boot prompt") board_term.send(uboot_cmd) - if not board_term.wait_pattern(r"buildroot|login:|# ", timeout=180.0): - raise TerminalTimeoutError("timed out waiting for zone0 boot") + if not board_term.wait_pattern(ZONE0_READY_PATTERN, timeout=180.0): + raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") return 0 return 0 From 8474777a2b04c3748c8e87936928d9a794b55058 Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 7 Jul 2026 14:10:26 +0800 Subject: [PATCH 23/53] fix ci bug --- Jenkinsfile | 14 +++++++------- Makefile | 28 ++++++++++++++++++++-------- jenkins/ci.yaml | 1 - jenkins/ci_runner.py | 12 ++++++++---- 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 911d9f920..dc0248f69 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -205,6 +205,7 @@ pipeline { LOONGARCH64_TOOLCHAIN_PATH = '/home/light/DEMO/toolchain/loongarch_cross_tools' // All toolchain bins on PATH; same for every matrix cell (no per-arch selection). TOOLCHAIN_PATHS = "${env.RISCV_TOOLCHAIN_PATH}/bin:${env.AARCH64_TOOLCHAIN_PATH}/bin:${env.LOONGARCH64_TOOLCHAIN_PATH}/bin" + TFTP_DIR = '/home/light/tftp' } stages { @@ -385,15 +386,14 @@ pipeline { "${prepareScript}" """ } else if (mode == 'board') { - def tftpDir = (testsCfg.tftp_dir ?: '/home/light/tftp').toString() - echo "Deploy TFTP artifacts [BID=${env.BID}, TFTP_DIR=${tftpDir}]" + echo "Deploy TFTP artifacts [BID=${env.BID}, TFTP_DIR=${env.TFTP_DIR}]" sh """ - TFTP_DIR="${tftpDir}" - mkdir -p "\$TFTP_DIR" + export TERM=\${TERM:-xterm} ${toolchainPathShell()} - make cp ARCH=${arch} BOARD=${board} MODE=release TFTP_DIR="\$TFTP_DIR" - cp platform/${arch}/${board}/image/dts/rk3568_limit_zone0.dtb "\$TFTP_DIR/" - cp ${kdir}/arch/arm64/boot/Image "\$TFTP_DIR/" + mkdir -p "${env.TFTP_DIR}" + make cp ARCH=${arch} BOARD=${board} MODE=release TFTP_DIR="${env.TFTP_DIR}" + cp platform/${arch}/${board}/image/dts/rk3568_limit_zone0.dtb "${env.TFTP_DIR}/" + cp ${kdir}/arch/arm64/boot/Image "${env.TFTP_DIR}/" """ } else { error("jenkins/ci.yaml BID=${env.BID}: unsupported tests.mode='${mode}'") diff --git a/Makefile b/Makefile index 4fb832a86..d49f9b79d 100644 --- a/Makefile +++ b/Makefile @@ -66,13 +66,22 @@ ifeq ($(MODE), release) build_args += --release endif -# color code -COLOR_GREEN := $(shell tput setaf 2) -COLOR_RED := $(shell tput setaf 1) -COLOR_YELLOW := $(shell tput setaf 3) -COLOR_BLUE := $(shell tput setaf 4) -COLOR_BOLD := $(shell tput bold) -COLOR_RESET := $(shell tput sgr0) +# color code (skip tput when TERM is unset, e.g. Jenkins sh steps) +ifdef TERM +COLOR_GREEN := $(shell tput setaf 2 2>/dev/null) +COLOR_RED := $(shell tput setaf 1 2>/dev/null) +COLOR_YELLOW := $(shell tput setaf 3 2>/dev/null) +COLOR_BLUE := $(shell tput setaf 4 2>/dev/null) +COLOR_BOLD := $(shell tput bold 2>/dev/null) +COLOR_RESET := $(shell tput sgr0 2>/dev/null) +else +COLOR_GREEN := +COLOR_RED := +COLOR_YELLOW := +COLOR_BLUE := +COLOR_BOLD := +COLOR_RESET := +endif # Defconfig / menuconfig: see tools/kconfig/kconfig_cli.py kconfig_python := tools/kconfig/.venv/bin/python @@ -187,8 +196,11 @@ monitor: jlink-server: JLinkGDBServer -select USB -if JTAG -device Cortex-A53 -port 1234 +TFTP_DIR ?= $(HOME)/tftp + cp: - cp $(hvisor_bin) ~/tftp + @mkdir -p "$(TFTP_DIR)" + cp $(hvisor_bin) "$(TFTP_DIR)/" test-pre: download-test-img chmod +x platform/$(ARCH)/$(BOARD)/test/runner.sh diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index 1bf125b4f..66b8f3d85 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -29,7 +29,6 @@ bids: baudrate: 1500000 uboot_ready_pattern: => uboot_cmd: pci enum;setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.240; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:hvisor.bin; tftp ${fdt_addr} ${serverip}:rk3568_limit_zone0.dtb; tftp ${zone0_kernel_addr} ${serverip}:Image; bootm ${loadaddr} - ${fdt_addr}; - tftp_dir: /home/light/tftp cases: - zone0_start - zone1_start diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 1a0257b9e..7848746a1 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -17,6 +17,10 @@ CaseFunc = Callable[[dict[str, Any], Terminal | None], int] +# Wait for an interactive shell prompt, not login: (getty shows that before MOTD/shell). +ZONE0_READY_PATTERN = r"root@[^\r\n]*[#$]\s|(?:\r?\n)#\s" + + def bid_log_key(bid: str) -> str: return bid.replace("/", "__") @@ -163,8 +167,8 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: if not qemu_term.wait_pattern(uboot_ready, timeout=10.0): raise TerminalTimeoutError("timed out waiting for U-Boot prompt") qemu_term.send(uboot_cmd) - if not qemu_term.wait_pattern(r"buildroot|login:|# ", timeout=180.0): - raise TerminalTimeoutError("timed out waiting for zone0 boot") + if not qemu_term.wait_pattern(ZONE0_READY_PATTERN, timeout=180.0): + raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") return 0 if cfg["mode"] == "board": board_power_cycle(cfg) @@ -184,8 +188,8 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: if not board_term.wait_pattern(uboot_ready, timeout=60.0): raise TerminalTimeoutError("timed out waiting for U-Boot prompt") board_term.send(uboot_cmd) - if not board_term.wait_pattern(r"buildroot|login:|# ", timeout=180.0): - raise TerminalTimeoutError("timed out waiting for zone0 boot") + if not board_term.wait_pattern(ZONE0_READY_PATTERN, timeout=180.0): + raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") return 0 return 0 From 017f7d82810d7f4a35c67a0d68ea1881ef8b0466 Mon Sep 17 00:00:00 2001 From: ray Date: Tue, 7 Jul 2026 18:15:40 +0800 Subject: [PATCH 24/53] fix ci bug --- Jenkinsfile | 26 +++++++---- jenkins/check_serial.sh | 45 ++++++++++++++----- jenkins/ci.yaml | 2 +- jenkins/ci_runner.py | 18 +++++--- jenkins/prepare.sh | 0 jenkins/terminal.py | 21 +++++++++ .../configs/zone1-linux-passthrough.json | 8 +++- .../qemu-plic/configs/zone1-linux-virtio.json | 6 +++ 8 files changed, 97 insertions(+), 29 deletions(-) mode change 100644 => 100755 jenkins/prepare.sh diff --git a/Jenkinsfile b/Jenkinsfile index 5a54fe0a8..a985adb66 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -409,14 +409,24 @@ pipeline { steps { dir(matrixCellDir()) { script { - echo "Run tests via ci_runner [BID=${env.BID}]" - sh """ - export TERM=\${TERM:-xterm} - ${toolchainPathShell()} - ${qemuPathShell()} - python3 jenkins/ci_runner.py \ - --bid "${env.BID}" - """ + def bidCfg = getBidConfig(loadCiYaml(), env.BID) + def mode = (bidCfg.tests?.mode ?: '').toString().trim() + echo "Run tests via ci_runner [BID=${env.BID}, mode=${mode}]" + if (mode == 'board') { + sh """ + export TERM=\${TERM:-xterm} + sudo -E python3 jenkins/ci_runner.py \ + --bid "${env.BID}" + """ + } else { + sh """ + export TERM=\${TERM:-xterm} + ${toolchainPathShell()} + ${qemuPathShell()} + python3 jenkins/ci_runner.py \ + --bid "${env.BID}" + """ + } } } } diff --git a/jenkins/check_serial.sh b/jenkins/check_serial.sh index 2f2fdb7d8..130ea5122 100755 --- a/jenkins/check_serial.sh +++ b/jenkins/check_serial.sh @@ -1,6 +1,6 @@ #!/bin/sh -# Non-interactive serial check for inner zone virtio-console (/dev/pts/x). -# Usage: check_serial.sh +# Non-interactive virtio-console check for inner zone (/dev/pts/x). +# Usage: check_serial.sh [timeout_sec] [command...] set -eu @@ -8,21 +8,42 @@ pts_dev=${1:?pts device required} log_file=${2:?log file required} shift 2 +prompt_timeout=180 +if [ $# -gt 0 ] && [ "$1" -eq "$1" ] 2>/dev/null; then + prompt_timeout=$1 + shift +fi + : > "$log_file" -stty -F "$pts_dev" 115200 cs8 -cstopb -parenb -ixon -ixoff -echo 2>/dev/null \ - || stty -F "$pts_dev" raw -echo 2>/dev/null \ - || true +read_pts() { + timeout "${1:-2}" cat "$pts_dev" 2>/dev/null >> "$log_file" || true +} + +has_shell_prompt() { + tr -d '\r' < "$log_file" | grep -qE '^#[[:space:]]*$' \ + || tr -d '\r' < "$log_file" | grep -qE '^root@[^[:space:]]+#[[:space:]]*$' +} -{ - printf '%s\r\n' "$*" - sleep 3 - timeout 10 cat "$pts_dev" 2>/dev/null || true -} >> "$log_file" 2>&1 +deadline=$(( $(date +%s) + prompt_timeout )) +while [ "$(date +%s)" -lt "$deadline" ]; do + read_pts 2 + if has_shell_prompt; then + break + fi + sleep 0.2 +done -if [ ! -s "$log_file" ]; then - echo "check_serial: no output from $pts_dev" >> "$log_file" +if ! has_shell_prompt; then + echo "check_serial: timed out after ${prompt_timeout}s waiting for shell prompt on $pts_dev" \ + >> "$log_file" exit 1 fi +if [ $# -gt 0 ]; then + printf '%s\r\n' "$@" + sleep 1 + read_pts 10 +fi + exit 0 diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index 7a549f8ed..6084ba950 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -27,7 +27,7 @@ bids: serial: /dev/ttyUSB0 power_serial: /dev/ttyUSB1 baudrate: 1500000 - uboot_ready_pattern: => + uboot_ready_pattern: 'Net:.*\n=> ' uboot_cmd: pci enum;setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.240; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:hvisor.bin; tftp ${fdt_addr} ${serverip}:rk3568_limit_zone0.dtb; tftp ${zone0_kernel_addr} ${serverip}:Image; bootm ${loadaddr} - ${fdt_addr}; cases: - zone0_start diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 7848746a1..0cba92ff7 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -19,6 +19,7 @@ # Wait for an interactive shell prompt, not login: (getty shows that before MOTD/shell). ZONE0_READY_PATTERN = r"root@[^\r\n]*[#$]\s|(?:\r?\n)#\s" +ZONE1_INNER_PROMPT_TIMEOUT = 180.0 def bid_log_key(bid: str) -> str: @@ -171,22 +172,23 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") return 0 if cfg["mode"] == "board": - board_power_cycle(cfg) - log_path = logs_dir(cfg) / "zone0_console.log" log_path.write_text("", encoding="utf-8") board_term = build_terminal(cfg, log_path) board_term.open() cfg["_board_term"] = board_term + board_power_cycle(cfg) uboot_cmd = cfg.get("uboot_cmd", "") uboot_ready = cfg.get("uboot_ready_pattern", "") if uboot_cmd: if not uboot_ready: - uboot_ready = r"*=>" - if not board_term.wait_pattern(uboot_ready, timeout=60.0): + uboot_ready = r"Net:.*\n=> " + if not board_term.wait_pattern(uboot_ready, timeout=120.0): raise TerminalTimeoutError("timed out waiting for U-Boot prompt") + time.sleep(0.3) + board_term.flush_input() board_term.send(uboot_cmd) if not board_term.wait_pattern(ZONE0_READY_PATTERN, timeout=180.0): raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") @@ -210,8 +212,8 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: check_rc, _ = term.run( "zone1_serial_check", - f"./check_serial.sh /dev/pts/{max_pts} {inner_log} ls", - timeout=30.0, + f"./check_serial.sh /dev/pts/{max_pts} {inner_log} {int(ZONE1_INNER_PROMPT_TIMEOUT)}", + timeout=ZONE1_INNER_PROMPT_TIMEOUT + 30.0, ) _, inner_output = term.run("zone1_inner_log", f"cat {inner_log}", timeout=15.0) save_inner_serial_log(cfg, inner_output) @@ -219,7 +221,9 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: if boot_rc != 0: raise TerminalCommandError(f"command failed with rc={boot_rc}: ./boot_zone1.sh") if check_rc != 0: - raise TerminalCommandError(f"command failed with rc={check_rc}: check_serial.sh") + raise TerminalCommandError( + f"command failed with rc={check_rc}: check_serial.sh (no shell prompt)" + ) print("zone1_started successfully", flush=True) return 0 diff --git a/jenkins/prepare.sh b/jenkins/prepare.sh old mode 100644 new mode 100755 diff --git a/jenkins/terminal.py b/jenkins/terminal.py index ba273eb60..ff05841f3 100644 --- a/jenkins/terminal.py +++ b/jenkins/terminal.py @@ -47,6 +47,10 @@ def read(self, max_bytes: int = 4096) -> bytes: def write(self, data: bytes) -> None: pass + @abstractmethod + def flush_input(self) -> None: + pass + @dataclass class QemuSocketBackend(TerminalBackend): @@ -86,6 +90,14 @@ def write(self, data: bytes) -> None: raise RuntimeError("QEMU socket is not open") self._sock.sendall(data) + def flush_input(self) -> None: + if self._sock is None: + return + while True: + chunk = self.read() + if not chunk: + break + @dataclass class SerialBackend(TerminalBackend): @@ -121,6 +133,11 @@ def write(self, data: bytes) -> None: self._serial.write(data) self._serial.flush() + def flush_input(self) -> None: + if self._serial is None: + return + self._serial.reset_input_buffer() + class LogCollector: """Background reader that writes terminal output to a log file and console.""" @@ -267,6 +284,10 @@ def __enter__(self) -> "Terminal": def __exit__(self, exc_type, exc, tb) -> None: self.close() + def flush_input(self) -> None: + self._ensure_open() + self.backend.flush_input() + def send(self, command: str) -> None: self._ensure_open() payload = command.rstrip("\n") + "\n" diff --git a/platform/riscv64/qemu-plic/configs/zone1-linux-passthrough.json b/platform/riscv64/qemu-plic/configs/zone1-linux-passthrough.json index 11930624d..fd7a435c7 100644 --- a/platform/riscv64/qemu-plic/configs/zone1-linux-passthrough.json +++ b/platform/riscv64/qemu-plic/configs/zone1-linux-passthrough.json @@ -54,11 +54,14 @@ }], "num_pci_devs": 2, "alloc_pci_devs": [ - { + { "domain": "0x0", "bus": "0x0", "device": "0x0", "function": "0x0", + "v_bus": "0x0", + "v_device": "0x0", + "v_function": "0x0", "dev_type": "0" }, { @@ -66,6 +69,9 @@ "bus": "0x0", "device": "0x2", "function": "0x0", + "v_bus": "0x0", + "v_device": "0x2", + "v_function": "0x0", "dev_type": "0" } ] diff --git a/platform/riscv64/qemu-plic/configs/zone1-linux-virtio.json b/platform/riscv64/qemu-plic/configs/zone1-linux-virtio.json index 08bad2a15..564712984 100644 --- a/platform/riscv64/qemu-plic/configs/zone1-linux-virtio.json +++ b/platform/riscv64/qemu-plic/configs/zone1-linux-virtio.json @@ -53,6 +53,9 @@ "bus": "0x0", "device": "0x0", "function": "0x0", + "v_bus": "0x0", + "v_device": "0x0", + "v_function": "0x0", "dev_type": "0" }, { @@ -60,6 +63,9 @@ "bus": "0x0", "device": "0x2", "function": "0x0", + "v_bus": "0x0", + "v_device": "0x1", + "v_function": "0x0", "dev_type": "0" } ] From acf0c1819882b0c936db24a5048849401e67513a Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 7 Jul 2026 18:15:40 +0800 Subject: [PATCH 25/53] fix ci bug --- Jenkinsfile | 26 +++++++---- jenkins/check_serial.sh | 45 ++++++++++++++----- jenkins/ci.yaml | 2 +- jenkins/ci_runner.py | 18 +++++--- jenkins/prepare.sh | 0 jenkins/terminal.py | 21 +++++++++ .../configs/zone1-linux-passthrough.json | 8 +++- .../qemu-plic/configs/zone1-linux-virtio.json | 6 +++ 8 files changed, 97 insertions(+), 29 deletions(-) mode change 100644 => 100755 jenkins/prepare.sh diff --git a/Jenkinsfile b/Jenkinsfile index 5a54fe0a8..a985adb66 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -409,14 +409,24 @@ pipeline { steps { dir(matrixCellDir()) { script { - echo "Run tests via ci_runner [BID=${env.BID}]" - sh """ - export TERM=\${TERM:-xterm} - ${toolchainPathShell()} - ${qemuPathShell()} - python3 jenkins/ci_runner.py \ - --bid "${env.BID}" - """ + def bidCfg = getBidConfig(loadCiYaml(), env.BID) + def mode = (bidCfg.tests?.mode ?: '').toString().trim() + echo "Run tests via ci_runner [BID=${env.BID}, mode=${mode}]" + if (mode == 'board') { + sh """ + export TERM=\${TERM:-xterm} + sudo -E python3 jenkins/ci_runner.py \ + --bid "${env.BID}" + """ + } else { + sh """ + export TERM=\${TERM:-xterm} + ${toolchainPathShell()} + ${qemuPathShell()} + python3 jenkins/ci_runner.py \ + --bid "${env.BID}" + """ + } } } } diff --git a/jenkins/check_serial.sh b/jenkins/check_serial.sh index 2f2fdb7d8..130ea5122 100755 --- a/jenkins/check_serial.sh +++ b/jenkins/check_serial.sh @@ -1,6 +1,6 @@ #!/bin/sh -# Non-interactive serial check for inner zone virtio-console (/dev/pts/x). -# Usage: check_serial.sh +# Non-interactive virtio-console check for inner zone (/dev/pts/x). +# Usage: check_serial.sh [timeout_sec] [command...] set -eu @@ -8,21 +8,42 @@ pts_dev=${1:?pts device required} log_file=${2:?log file required} shift 2 +prompt_timeout=180 +if [ $# -gt 0 ] && [ "$1" -eq "$1" ] 2>/dev/null; then + prompt_timeout=$1 + shift +fi + : > "$log_file" -stty -F "$pts_dev" 115200 cs8 -cstopb -parenb -ixon -ixoff -echo 2>/dev/null \ - || stty -F "$pts_dev" raw -echo 2>/dev/null \ - || true +read_pts() { + timeout "${1:-2}" cat "$pts_dev" 2>/dev/null >> "$log_file" || true +} + +has_shell_prompt() { + tr -d '\r' < "$log_file" | grep -qE '^#[[:space:]]*$' \ + || tr -d '\r' < "$log_file" | grep -qE '^root@[^[:space:]]+#[[:space:]]*$' +} -{ - printf '%s\r\n' "$*" - sleep 3 - timeout 10 cat "$pts_dev" 2>/dev/null || true -} >> "$log_file" 2>&1 +deadline=$(( $(date +%s) + prompt_timeout )) +while [ "$(date +%s)" -lt "$deadline" ]; do + read_pts 2 + if has_shell_prompt; then + break + fi + sleep 0.2 +done -if [ ! -s "$log_file" ]; then - echo "check_serial: no output from $pts_dev" >> "$log_file" +if ! has_shell_prompt; then + echo "check_serial: timed out after ${prompt_timeout}s waiting for shell prompt on $pts_dev" \ + >> "$log_file" exit 1 fi +if [ $# -gt 0 ]; then + printf '%s\r\n' "$@" + sleep 1 + read_pts 10 +fi + exit 0 diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index 7a549f8ed..6084ba950 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -27,7 +27,7 @@ bids: serial: /dev/ttyUSB0 power_serial: /dev/ttyUSB1 baudrate: 1500000 - uboot_ready_pattern: => + uboot_ready_pattern: 'Net:.*\n=> ' uboot_cmd: pci enum;setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.240; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:hvisor.bin; tftp ${fdt_addr} ${serverip}:rk3568_limit_zone0.dtb; tftp ${zone0_kernel_addr} ${serverip}:Image; bootm ${loadaddr} - ${fdt_addr}; cases: - zone0_start diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 7848746a1..0cba92ff7 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -19,6 +19,7 @@ # Wait for an interactive shell prompt, not login: (getty shows that before MOTD/shell). ZONE0_READY_PATTERN = r"root@[^\r\n]*[#$]\s|(?:\r?\n)#\s" +ZONE1_INNER_PROMPT_TIMEOUT = 180.0 def bid_log_key(bid: str) -> str: @@ -171,22 +172,23 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") return 0 if cfg["mode"] == "board": - board_power_cycle(cfg) - log_path = logs_dir(cfg) / "zone0_console.log" log_path.write_text("", encoding="utf-8") board_term = build_terminal(cfg, log_path) board_term.open() cfg["_board_term"] = board_term + board_power_cycle(cfg) uboot_cmd = cfg.get("uboot_cmd", "") uboot_ready = cfg.get("uboot_ready_pattern", "") if uboot_cmd: if not uboot_ready: - uboot_ready = r"*=>" - if not board_term.wait_pattern(uboot_ready, timeout=60.0): + uboot_ready = r"Net:.*\n=> " + if not board_term.wait_pattern(uboot_ready, timeout=120.0): raise TerminalTimeoutError("timed out waiting for U-Boot prompt") + time.sleep(0.3) + board_term.flush_input() board_term.send(uboot_cmd) if not board_term.wait_pattern(ZONE0_READY_PATTERN, timeout=180.0): raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") @@ -210,8 +212,8 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: check_rc, _ = term.run( "zone1_serial_check", - f"./check_serial.sh /dev/pts/{max_pts} {inner_log} ls", - timeout=30.0, + f"./check_serial.sh /dev/pts/{max_pts} {inner_log} {int(ZONE1_INNER_PROMPT_TIMEOUT)}", + timeout=ZONE1_INNER_PROMPT_TIMEOUT + 30.0, ) _, inner_output = term.run("zone1_inner_log", f"cat {inner_log}", timeout=15.0) save_inner_serial_log(cfg, inner_output) @@ -219,7 +221,9 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: if boot_rc != 0: raise TerminalCommandError(f"command failed with rc={boot_rc}: ./boot_zone1.sh") if check_rc != 0: - raise TerminalCommandError(f"command failed with rc={check_rc}: check_serial.sh") + raise TerminalCommandError( + f"command failed with rc={check_rc}: check_serial.sh (no shell prompt)" + ) print("zone1_started successfully", flush=True) return 0 diff --git a/jenkins/prepare.sh b/jenkins/prepare.sh old mode 100644 new mode 100755 diff --git a/jenkins/terminal.py b/jenkins/terminal.py index ba273eb60..ff05841f3 100644 --- a/jenkins/terminal.py +++ b/jenkins/terminal.py @@ -47,6 +47,10 @@ def read(self, max_bytes: int = 4096) -> bytes: def write(self, data: bytes) -> None: pass + @abstractmethod + def flush_input(self) -> None: + pass + @dataclass class QemuSocketBackend(TerminalBackend): @@ -86,6 +90,14 @@ def write(self, data: bytes) -> None: raise RuntimeError("QEMU socket is not open") self._sock.sendall(data) + def flush_input(self) -> None: + if self._sock is None: + return + while True: + chunk = self.read() + if not chunk: + break + @dataclass class SerialBackend(TerminalBackend): @@ -121,6 +133,11 @@ def write(self, data: bytes) -> None: self._serial.write(data) self._serial.flush() + def flush_input(self) -> None: + if self._serial is None: + return + self._serial.reset_input_buffer() + class LogCollector: """Background reader that writes terminal output to a log file and console.""" @@ -267,6 +284,10 @@ def __enter__(self) -> "Terminal": def __exit__(self, exc_type, exc, tb) -> None: self.close() + def flush_input(self) -> None: + self._ensure_open() + self.backend.flush_input() + def send(self, command: str) -> None: self._ensure_open() payload = command.rstrip("\n") + "\n" diff --git a/platform/riscv64/qemu-plic/configs/zone1-linux-passthrough.json b/platform/riscv64/qemu-plic/configs/zone1-linux-passthrough.json index 11930624d..fd7a435c7 100644 --- a/platform/riscv64/qemu-plic/configs/zone1-linux-passthrough.json +++ b/platform/riscv64/qemu-plic/configs/zone1-linux-passthrough.json @@ -54,11 +54,14 @@ }], "num_pci_devs": 2, "alloc_pci_devs": [ - { + { "domain": "0x0", "bus": "0x0", "device": "0x0", "function": "0x0", + "v_bus": "0x0", + "v_device": "0x0", + "v_function": "0x0", "dev_type": "0" }, { @@ -66,6 +69,9 @@ "bus": "0x0", "device": "0x2", "function": "0x0", + "v_bus": "0x0", + "v_device": "0x2", + "v_function": "0x0", "dev_type": "0" } ] diff --git a/platform/riscv64/qemu-plic/configs/zone1-linux-virtio.json b/platform/riscv64/qemu-plic/configs/zone1-linux-virtio.json index 08bad2a15..564712984 100644 --- a/platform/riscv64/qemu-plic/configs/zone1-linux-virtio.json +++ b/platform/riscv64/qemu-plic/configs/zone1-linux-virtio.json @@ -53,6 +53,9 @@ "bus": "0x0", "device": "0x0", "function": "0x0", + "v_bus": "0x0", + "v_device": "0x0", + "v_function": "0x0", "dev_type": "0" }, { @@ -60,6 +63,9 @@ "bus": "0x0", "device": "0x2", "function": "0x0", + "v_bus": "0x0", + "v_device": "0x1", + "v_function": "0x0", "dev_type": "0" } ] From df2450c00ae2dda13eb09ccb3dd5d36a16793a63 Mon Sep 17 00:00:00 2001 From: ray Date: Tue, 7 Jul 2026 18:49:21 +0800 Subject: [PATCH 26/53] ci bug fix2 --- Jenkinsfile | 5 +- jenkins/check_serial.sh | 6 +- jenkins/ci_runner.py | 10 ++- jenkins/prepare.sh | 19 ++++++ jenkins/terminal.py | 26 ++++++- .../qemu-plic/configs/virtio-backend.json | 26 ++++--- .../qemu-plic/configs/zone1-linux.json | 68 +++++++++++++++++++ .../qemu-plic/image/dts/zone1-linux.dts | 7 ++ .../riscv64/qemu-plic/scripts/boot_zone1.sh | 2 +- 9 files changed, 150 insertions(+), 19 deletions(-) create mode 100644 platform/riscv64/qemu-plic/configs/zone1-linux.json diff --git a/Jenkinsfile b/Jenkinsfile index a985adb66..54f44bcd7 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -45,6 +45,8 @@ def syncWorkspaceTo(String destDir) { --exclude '.jenkins/' \\ --exclude '.matrix/' \\ --exclude '.jenkins-matrix/' \\ + --exclude '__pycache__/' \\ + --exclude '*.pyc' \\ '${env.WORKSPACE}/' '${destDir}/' """ } @@ -188,7 +190,7 @@ pipeline { always { echo "=== DEBUG: Branch ${env.BRANCH_NAME} ===" echo "=== DEBUG: Commit ${env.GIT_COMMIT} ===" - sh 'rm -rf .matrix' + sh 'sudo rm -rf .matrix 2>/dev/null || rm -rf .matrix || true' } } @@ -206,6 +208,7 @@ pipeline { // All toolchain bins on PATH; same for every matrix cell (no per-arch selection). TOOLCHAIN_PATHS = "${env.RISCV_TOOLCHAIN_PATH}/bin:${env.AARCH64_TOOLCHAIN_PATH}/bin:${env.LOONGARCH64_TOOLCHAIN_PATH}/bin" TFTP_DIR = '/home/light/tftp' + PYTHONDONTWRITEBYTECODE = '1' } stages { diff --git a/jenkins/check_serial.sh b/jenkins/check_serial.sh index 130ea5122..529abd74a 100755 --- a/jenkins/check_serial.sh +++ b/jenkins/check_serial.sh @@ -8,7 +8,7 @@ pts_dev=${1:?pts device required} log_file=${2:?log file required} shift 2 -prompt_timeout=180 +prompt_timeout=60 if [ $# -gt 0 ] && [ "$1" -eq "$1" ] 2>/dev/null; then prompt_timeout=$1 shift @@ -21,8 +21,8 @@ read_pts() { } has_shell_prompt() { - tr -d '\r' < "$log_file" | grep -qE '^#[[:space:]]*$' \ - || tr -d '\r' < "$log_file" | grep -qE '^root@[^[:space:]]+#[[:space:]]*$' + tr -d '\r' < "$log_file" | sed 's/\x1b\[[0-9;?]*[ -\/]*[@-~]//g' \ + | grep -qE 'root@[^[:space:]]*[#$][[:space:]]*$|^[[:space:]]*#[[:space:]]*$' } deadline=$(( $(date +%s) + prompt_timeout )) diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 0cba92ff7..aaea00168 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -19,7 +19,8 @@ # Wait for an interactive shell prompt, not login: (getty shows that before MOTD/shell). ZONE0_READY_PATTERN = r"root@[^\r\n]*[#$]\s|(?:\r?\n)#\s" -ZONE1_INNER_PROMPT_TIMEOUT = 180.0 +ZONE1_INNER_PROMPT_TIMEOUT = 60.0 +ZONE1_INNER_LOG_FETCH_TIMEOUT = 30.0 def bid_log_key(bid: str) -> str: @@ -215,7 +216,12 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: f"./check_serial.sh /dev/pts/{max_pts} {inner_log} {int(ZONE1_INNER_PROMPT_TIMEOUT)}", timeout=ZONE1_INNER_PROMPT_TIMEOUT + 30.0, ) - _, inner_output = term.run("zone1_inner_log", f"cat {inner_log}", timeout=15.0) + # Fetch via tail to limit serial traffic; generous timeout for slow Jenkins consoles. + _, inner_output = term.run( + "zone1_inner_log", + f"tail -c 131072 {inner_log}", + timeout=ZONE1_INNER_LOG_FETCH_TIMEOUT, + ) save_inner_serial_log(cfg, inner_output) if boot_rc != 0: diff --git a/jenkins/prepare.sh b/jenkins/prepare.sh index 56bd41af9..4e2f71e16 100755 --- a/jenkins/prepare.sh +++ b/jenkins/prepare.sh @@ -118,3 +118,22 @@ fi if [ -f "${ROOTFS_DIR}/root/screen_zone1.sh" ]; then chmod +x "${ROOTFS_DIR}/root/screen_zone1.sh" fi + +ROOTFS2_IMG="" +if [ -f "${VIRTDISK_DIR}/rootfs2.ext4" ]; then + ROOTFS2_IMG="${VIRTDISK_DIR}/rootfs2.ext4" +elif [ -f "${VIRTDISK_DIR}/rootfs2.img" ]; then + ROOTFS2_IMG="${VIRTDISK_DIR}/rootfs2.img" +fi + +if [ -n "${ROOTFS2_IMG}" ]; then + ROOTFS2_MNT="${VIRTDISK_DIR}/rootfs2" + mkdir -p "${ROOTFS2_MNT}" + mount -o loop "${ROOTFS2_IMG}" "${ROOTFS2_MNT}" + if [ -f "${ROOTFS2_MNT}/init" ]; then + chmod +x "${ROOTFS2_MNT}/init" + fi + umount "${ROOTFS2_MNT}" + rmdir "${ROOTFS2_MNT}" + cp "${ROOTFS2_IMG}" "${ROOTFS_DIR}/root/" +fi diff --git a/jenkins/terminal.py b/jenkins/terminal.py index ff05841f3..00852a982 100644 --- a/jenkins/terminal.py +++ b/jenkins/terminal.py @@ -3,6 +3,7 @@ from __future__ import annotations +import queue import re import select import socket @@ -159,9 +160,16 @@ def __init__( self._buffer = "" self._stop = threading.Event() self._thread: threading.Thread | None = None + self._console_queue: queue.SimpleQueue[str | None] = queue.SimpleQueue() + self._console_thread: threading.Thread | None = None def start(self) -> None: self.log_path.parent.mkdir(parents=True, exist_ok=True) + if self.console: + self._console_thread = threading.Thread( + target=self._console_loop, name="LogCollectorConsole", daemon=True + ) + self._console_thread.start() self._thread = threading.Thread(target=self._run_loop, name="LogCollector", daemon=True) self._thread.start() @@ -176,7 +184,12 @@ def stop(self) -> None: chunk = self.backend.read() if not chunk: break - self._append(chunk.decode(self.encoding, errors="replace")) + self._append(chunk.decode(self.encoding, errors="replace"), emit_console=False) + if self.console: + self._console_queue.put(None) + if self._console_thread is not None: + self._console_thread.join(timeout=5.0) + self._console_thread = None def offset(self) -> int: with self._lock: @@ -192,7 +205,7 @@ def text(self) -> str: with self._lock: return self._buffer - def _append(self, chunk: str) -> None: + def _append(self, chunk: str, *, emit_console: bool = True) -> None: if not chunk: return with self._lock: @@ -202,7 +215,14 @@ def _append(self, chunk: str) -> None: fh.write(chunk) except OSError: pass - if self.console: + if emit_console and self.console: + self._console_queue.put(chunk) + + def _console_loop(self) -> None: + while True: + chunk = self._console_queue.get() + if chunk is None: + return print(chunk, end="", flush=True) def _run_loop(self) -> None: diff --git a/platform/riscv64/qemu-plic/configs/virtio-backend.json b/platform/riscv64/qemu-plic/configs/virtio-backend.json index 7ab478162..5ce9f9bea 100644 --- a/platform/riscv64/qemu-plic/configs/virtio-backend.json +++ b/platform/riscv64/qemu-plic/configs/virtio-backend.json @@ -10,14 +10,22 @@ } ], "devices": [ - { - "type": "console", - "addr": "0x10007000", - "len": "0x1000", - "irq": 7, - "status": "enable" - } - ] + { + "type": "blk", + "addr": "0x10006000", + "len": "0x1000", + "irq": 6, + "img": "rootfs2.ext4", + "status": "enable" + }, + { + "type": "console", + "addr": "0x10007000", + "len": "0x1000", + "irq": 7, + "status": "enable" + } + ] } ] -} \ No newline at end of file +} diff --git a/platform/riscv64/qemu-plic/configs/zone1-linux.json b/platform/riscv64/qemu-plic/configs/zone1-linux.json new file mode 100644 index 000000000..a745e5338 --- /dev/null +++ b/platform/riscv64/qemu-plic/configs/zone1-linux.json @@ -0,0 +1,68 @@ +{ + "arch": "riscv", + "name": "linux2", + "zone_id": 1, + "cpus": [2, 3], + "memory_regions": [ + { + "type": "ram", + "physical_start": "0x85000000", + "virtual_start": "0x85000000", + "size": "0x0a000000" + }, + { + "type": "virtio", + "physical_start": "0x10006000", + "virtual_start": "0x10006000", + "size": "0x1000" + }, + { + "type": "virtio", + "physical_start": "0x10007000", + "virtual_start": "0x10007000", + "size": "0x1000" + } + ], + "interrupts": [6, 7], + "ivc_configs": [], + "kernel_filepath": "./Image", + "dtb_filepath": "./zone1-linux.dtb", + "kernel_load_paddr": "0x86000000", + "dtb_load_paddr": "0x85000000", + "entry_point": "0x86000000", + "arch_config": { + "plic_base": "0xc000000", + "plic_size": "0x4000000", + "aplic_base": "0xd000000", + "aplic_size": "0x8000" + }, + "pci_config": [{ + "ecam_base": "0x30000000", + "ecam_size": "0x10000000", + "io_base": "0x3000000", + "io_size": "0x10000", + "pci_io_base": "0x0", + "mem32_base": "0x40000000", + "mem32_size": "0x40000000", + "pci_mem32_base": "0x40000000", + "mem64_base": "0x400000000", + "mem64_size": "0x400000000", + "pci_mem64_base": "0x400000000", + "bus_range_begin": "0x0", + "bus_range_end": "0x1f", + "domain": "0x0" + }], + "num_pci_devs": 1, + "alloc_pci_devs": [ + { + "domain": "0x0", + "bus": "0x0", + "device": "0x0", + "function": "0x0", + "v_bus": "0x0", + "v_device": "0x0", + "v_function": "0x0", + "dev_type": "0" + } + ] +} diff --git a/platform/riscv64/qemu-plic/image/dts/zone1-linux.dts b/platform/riscv64/qemu-plic/image/dts/zone1-linux.dts index 9583df748..8087a6810 100644 --- a/platform/riscv64/qemu-plic/image/dts/zone1-linux.dts +++ b/platform/riscv64/qemu-plic/image/dts/zone1-linux.dts @@ -97,6 +97,13 @@ #address-cells = <0x03>; }; + virtio_mmio@10006000 { + interrupts = <0x06>; + interrupt-parent = <0x02>; + reg = <0x00 0x10006000 0x00 0x1000>; + compatible = "virtio,mmio"; + }; + virtio_mmio@10007000 { interrupts = <0x07>; interrupt-parent = <0x02>; diff --git a/platform/riscv64/qemu-plic/scripts/boot_zone1.sh b/platform/riscv64/qemu-plic/scripts/boot_zone1.sh index 00ec6b955..d3ffe3505 100644 --- a/platform/riscv64/qemu-plic/scripts/boot_zone1.sh +++ b/platform/riscv64/qemu-plic/scripts/boot_zone1.sh @@ -6,4 +6,4 @@ mount -t sysfs sysfs /sys mkdir -p /dev/pts mount -t devpts devpts /dev/pts ./hvisor virtio start virtio-backend.json & -./hvisor zone start zone1-linux-virtio.json \ No newline at end of file +./hvisor zone start zone1-linux.json From fa31c79b57ad5f555582ac73fb04da1b9c76e843 Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 7 Jul 2026 18:49:21 +0800 Subject: [PATCH 27/53] ci bug fix2 --- Jenkinsfile | 5 +- jenkins/check_serial.sh | 6 +- jenkins/ci_runner.py | 10 ++- jenkins/prepare.sh | 19 ++++++ jenkins/terminal.py | 26 ++++++- .../qemu-plic/configs/virtio-backend.json | 26 ++++--- .../qemu-plic/configs/zone1-linux.json | 68 +++++++++++++++++++ .../qemu-plic/image/dts/zone1-linux.dts | 7 ++ .../riscv64/qemu-plic/scripts/boot_zone1.sh | 2 +- 9 files changed, 150 insertions(+), 19 deletions(-) create mode 100644 platform/riscv64/qemu-plic/configs/zone1-linux.json diff --git a/Jenkinsfile b/Jenkinsfile index a985adb66..54f44bcd7 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -45,6 +45,8 @@ def syncWorkspaceTo(String destDir) { --exclude '.jenkins/' \\ --exclude '.matrix/' \\ --exclude '.jenkins-matrix/' \\ + --exclude '__pycache__/' \\ + --exclude '*.pyc' \\ '${env.WORKSPACE}/' '${destDir}/' """ } @@ -188,7 +190,7 @@ pipeline { always { echo "=== DEBUG: Branch ${env.BRANCH_NAME} ===" echo "=== DEBUG: Commit ${env.GIT_COMMIT} ===" - sh 'rm -rf .matrix' + sh 'sudo rm -rf .matrix 2>/dev/null || rm -rf .matrix || true' } } @@ -206,6 +208,7 @@ pipeline { // All toolchain bins on PATH; same for every matrix cell (no per-arch selection). TOOLCHAIN_PATHS = "${env.RISCV_TOOLCHAIN_PATH}/bin:${env.AARCH64_TOOLCHAIN_PATH}/bin:${env.LOONGARCH64_TOOLCHAIN_PATH}/bin" TFTP_DIR = '/home/light/tftp' + PYTHONDONTWRITEBYTECODE = '1' } stages { diff --git a/jenkins/check_serial.sh b/jenkins/check_serial.sh index 130ea5122..529abd74a 100755 --- a/jenkins/check_serial.sh +++ b/jenkins/check_serial.sh @@ -8,7 +8,7 @@ pts_dev=${1:?pts device required} log_file=${2:?log file required} shift 2 -prompt_timeout=180 +prompt_timeout=60 if [ $# -gt 0 ] && [ "$1" -eq "$1" ] 2>/dev/null; then prompt_timeout=$1 shift @@ -21,8 +21,8 @@ read_pts() { } has_shell_prompt() { - tr -d '\r' < "$log_file" | grep -qE '^#[[:space:]]*$' \ - || tr -d '\r' < "$log_file" | grep -qE '^root@[^[:space:]]+#[[:space:]]*$' + tr -d '\r' < "$log_file" | sed 's/\x1b\[[0-9;?]*[ -\/]*[@-~]//g' \ + | grep -qE 'root@[^[:space:]]*[#$][[:space:]]*$|^[[:space:]]*#[[:space:]]*$' } deadline=$(( $(date +%s) + prompt_timeout )) diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 0cba92ff7..aaea00168 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -19,7 +19,8 @@ # Wait for an interactive shell prompt, not login: (getty shows that before MOTD/shell). ZONE0_READY_PATTERN = r"root@[^\r\n]*[#$]\s|(?:\r?\n)#\s" -ZONE1_INNER_PROMPT_TIMEOUT = 180.0 +ZONE1_INNER_PROMPT_TIMEOUT = 60.0 +ZONE1_INNER_LOG_FETCH_TIMEOUT = 30.0 def bid_log_key(bid: str) -> str: @@ -215,7 +216,12 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: f"./check_serial.sh /dev/pts/{max_pts} {inner_log} {int(ZONE1_INNER_PROMPT_TIMEOUT)}", timeout=ZONE1_INNER_PROMPT_TIMEOUT + 30.0, ) - _, inner_output = term.run("zone1_inner_log", f"cat {inner_log}", timeout=15.0) + # Fetch via tail to limit serial traffic; generous timeout for slow Jenkins consoles. + _, inner_output = term.run( + "zone1_inner_log", + f"tail -c 131072 {inner_log}", + timeout=ZONE1_INNER_LOG_FETCH_TIMEOUT, + ) save_inner_serial_log(cfg, inner_output) if boot_rc != 0: diff --git a/jenkins/prepare.sh b/jenkins/prepare.sh index 56bd41af9..4e2f71e16 100755 --- a/jenkins/prepare.sh +++ b/jenkins/prepare.sh @@ -118,3 +118,22 @@ fi if [ -f "${ROOTFS_DIR}/root/screen_zone1.sh" ]; then chmod +x "${ROOTFS_DIR}/root/screen_zone1.sh" fi + +ROOTFS2_IMG="" +if [ -f "${VIRTDISK_DIR}/rootfs2.ext4" ]; then + ROOTFS2_IMG="${VIRTDISK_DIR}/rootfs2.ext4" +elif [ -f "${VIRTDISK_DIR}/rootfs2.img" ]; then + ROOTFS2_IMG="${VIRTDISK_DIR}/rootfs2.img" +fi + +if [ -n "${ROOTFS2_IMG}" ]; then + ROOTFS2_MNT="${VIRTDISK_DIR}/rootfs2" + mkdir -p "${ROOTFS2_MNT}" + mount -o loop "${ROOTFS2_IMG}" "${ROOTFS2_MNT}" + if [ -f "${ROOTFS2_MNT}/init" ]; then + chmod +x "${ROOTFS2_MNT}/init" + fi + umount "${ROOTFS2_MNT}" + rmdir "${ROOTFS2_MNT}" + cp "${ROOTFS2_IMG}" "${ROOTFS_DIR}/root/" +fi diff --git a/jenkins/terminal.py b/jenkins/terminal.py index ff05841f3..00852a982 100644 --- a/jenkins/terminal.py +++ b/jenkins/terminal.py @@ -3,6 +3,7 @@ from __future__ import annotations +import queue import re import select import socket @@ -159,9 +160,16 @@ def __init__( self._buffer = "" self._stop = threading.Event() self._thread: threading.Thread | None = None + self._console_queue: queue.SimpleQueue[str | None] = queue.SimpleQueue() + self._console_thread: threading.Thread | None = None def start(self) -> None: self.log_path.parent.mkdir(parents=True, exist_ok=True) + if self.console: + self._console_thread = threading.Thread( + target=self._console_loop, name="LogCollectorConsole", daemon=True + ) + self._console_thread.start() self._thread = threading.Thread(target=self._run_loop, name="LogCollector", daemon=True) self._thread.start() @@ -176,7 +184,12 @@ def stop(self) -> None: chunk = self.backend.read() if not chunk: break - self._append(chunk.decode(self.encoding, errors="replace")) + self._append(chunk.decode(self.encoding, errors="replace"), emit_console=False) + if self.console: + self._console_queue.put(None) + if self._console_thread is not None: + self._console_thread.join(timeout=5.0) + self._console_thread = None def offset(self) -> int: with self._lock: @@ -192,7 +205,7 @@ def text(self) -> str: with self._lock: return self._buffer - def _append(self, chunk: str) -> None: + def _append(self, chunk: str, *, emit_console: bool = True) -> None: if not chunk: return with self._lock: @@ -202,7 +215,14 @@ def _append(self, chunk: str) -> None: fh.write(chunk) except OSError: pass - if self.console: + if emit_console and self.console: + self._console_queue.put(chunk) + + def _console_loop(self) -> None: + while True: + chunk = self._console_queue.get() + if chunk is None: + return print(chunk, end="", flush=True) def _run_loop(self) -> None: diff --git a/platform/riscv64/qemu-plic/configs/virtio-backend.json b/platform/riscv64/qemu-plic/configs/virtio-backend.json index 7ab478162..5ce9f9bea 100644 --- a/platform/riscv64/qemu-plic/configs/virtio-backend.json +++ b/platform/riscv64/qemu-plic/configs/virtio-backend.json @@ -10,14 +10,22 @@ } ], "devices": [ - { - "type": "console", - "addr": "0x10007000", - "len": "0x1000", - "irq": 7, - "status": "enable" - } - ] + { + "type": "blk", + "addr": "0x10006000", + "len": "0x1000", + "irq": 6, + "img": "rootfs2.ext4", + "status": "enable" + }, + { + "type": "console", + "addr": "0x10007000", + "len": "0x1000", + "irq": 7, + "status": "enable" + } + ] } ] -} \ No newline at end of file +} diff --git a/platform/riscv64/qemu-plic/configs/zone1-linux.json b/platform/riscv64/qemu-plic/configs/zone1-linux.json new file mode 100644 index 000000000..a745e5338 --- /dev/null +++ b/platform/riscv64/qemu-plic/configs/zone1-linux.json @@ -0,0 +1,68 @@ +{ + "arch": "riscv", + "name": "linux2", + "zone_id": 1, + "cpus": [2, 3], + "memory_regions": [ + { + "type": "ram", + "physical_start": "0x85000000", + "virtual_start": "0x85000000", + "size": "0x0a000000" + }, + { + "type": "virtio", + "physical_start": "0x10006000", + "virtual_start": "0x10006000", + "size": "0x1000" + }, + { + "type": "virtio", + "physical_start": "0x10007000", + "virtual_start": "0x10007000", + "size": "0x1000" + } + ], + "interrupts": [6, 7], + "ivc_configs": [], + "kernel_filepath": "./Image", + "dtb_filepath": "./zone1-linux.dtb", + "kernel_load_paddr": "0x86000000", + "dtb_load_paddr": "0x85000000", + "entry_point": "0x86000000", + "arch_config": { + "plic_base": "0xc000000", + "plic_size": "0x4000000", + "aplic_base": "0xd000000", + "aplic_size": "0x8000" + }, + "pci_config": [{ + "ecam_base": "0x30000000", + "ecam_size": "0x10000000", + "io_base": "0x3000000", + "io_size": "0x10000", + "pci_io_base": "0x0", + "mem32_base": "0x40000000", + "mem32_size": "0x40000000", + "pci_mem32_base": "0x40000000", + "mem64_base": "0x400000000", + "mem64_size": "0x400000000", + "pci_mem64_base": "0x400000000", + "bus_range_begin": "0x0", + "bus_range_end": "0x1f", + "domain": "0x0" + }], + "num_pci_devs": 1, + "alloc_pci_devs": [ + { + "domain": "0x0", + "bus": "0x0", + "device": "0x0", + "function": "0x0", + "v_bus": "0x0", + "v_device": "0x0", + "v_function": "0x0", + "dev_type": "0" + } + ] +} diff --git a/platform/riscv64/qemu-plic/image/dts/zone1-linux.dts b/platform/riscv64/qemu-plic/image/dts/zone1-linux.dts index 9583df748..8087a6810 100644 --- a/platform/riscv64/qemu-plic/image/dts/zone1-linux.dts +++ b/platform/riscv64/qemu-plic/image/dts/zone1-linux.dts @@ -97,6 +97,13 @@ #address-cells = <0x03>; }; + virtio_mmio@10006000 { + interrupts = <0x06>; + interrupt-parent = <0x02>; + reg = <0x00 0x10006000 0x00 0x1000>; + compatible = "virtio,mmio"; + }; + virtio_mmio@10007000 { interrupts = <0x07>; interrupt-parent = <0x02>; diff --git a/platform/riscv64/qemu-plic/scripts/boot_zone1.sh b/platform/riscv64/qemu-plic/scripts/boot_zone1.sh index 00ec6b955..d3ffe3505 100644 --- a/platform/riscv64/qemu-plic/scripts/boot_zone1.sh +++ b/platform/riscv64/qemu-plic/scripts/boot_zone1.sh @@ -6,4 +6,4 @@ mount -t sysfs sysfs /sys mkdir -p /dev/pts mount -t devpts devpts /dev/pts ./hvisor virtio start virtio-backend.json & -./hvisor zone start zone1-linux-virtio.json \ No newline at end of file +./hvisor zone start zone1-linux.json From 442c5dbd6f706d97721b1529ea1f7b729c898b47 Mon Sep 17 00:00:00 2001 From: ray Date: Mon, 13 Jul 2026 19:19:30 +0800 Subject: [PATCH 28/53] add pcie test for rk3568 --- jenkins/ci.yaml | 14 ++- jenkins/ci_runner.py | 84 ++++++++++++- jenkins/terminal.py | 3 + platform/aarch64/rk3568/board.rs | 18 ++- .../rk3568/image/dts/rk3568_limit_zone0.dts | 79 +++++++++++- src/pci/pci_access.rs | 5 + src/pci/pci_config.rs | 4 +- src/pci/pci_handler.rs | 118 +++++++++++++----- src/pci/pci_struct.rs | 28 ++++- src/pci/pci_test.rs | 6 +- src/zone.rs | 10 +- 11 files changed, 318 insertions(+), 51 deletions(-) diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index 6084ba950..0124e3220 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -24,13 +24,21 @@ bids: - KDIR=/home/light/DEMO/sdk/rk356x-up4-2c/kernel tests: mode: board - serial: /dev/ttyUSB0 - power_serial: /dev/ttyUSB1 + serial: /dev/serial/by-id/usb-1a86_USB2.0-Ser_-if00-port0 + power_serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 baudrate: 1500000 - uboot_ready_pattern: 'Net:.*\n=> ' + uboot_ready_pattern: '=>' uboot_cmd: pci enum;setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.240; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:hvisor.bin; tftp ${fdt_addr} ${serverip}:rk3568_limit_zone0.dtb; tftp ${zone0_kernel_addr} ${serverip}:Image; bootm ${loadaddr} - ${fdt_addr}; + lspci: + expected_bdfs: + - "0001:10:00.0" + - "0001:11:00.0" + - "0002:20:00.0" + - "0002:21:00.0" + min_count: 4 cases: - zone0_start + - lspci - zone1_start diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index aaea00168..8a6cb3f2d 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -141,7 +141,21 @@ def board_power_cycle(cfg: dict[str, Any]) -> None: subprocess.run(["bash", str(script), "cycle", power_port], check=True, cwd=cfg["workspace"]) -def save_inner_serial_log(cfg: dict[str, Any], content: str) -> None: +def board_wake_console(term: Terminal, *, repeats: int = 3) -> None: + for _ in range(repeats): + term.send("") + time.sleep(0.2) + + +def board_wait_uboot_prompt(term: Terminal, pattern: str, timeout: float) -> None: + """Wait for U-Boot prompt, periodically waking an idle console.""" + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + remaining = deadline - time.monotonic() + if term.wait_pattern(pattern, timeout=min(5.0, remaining), from_offset=0): + return + board_wake_console(term) + raise TerminalTimeoutError(f"timed out waiting for U-Boot prompt (pattern={pattern!r})") if not content: return path = logs_dir(cfg) / "zone1_inner_serial.log" @@ -180,16 +194,16 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: board_term.open() cfg["_board_term"] = board_term board_power_cycle(cfg) + time.sleep(3.0) + board_wake_console(board_term) uboot_cmd = cfg.get("uboot_cmd", "") uboot_ready = cfg.get("uboot_ready_pattern", "") if uboot_cmd: if not uboot_ready: - uboot_ready = r"Net:.*\n=> " - if not board_term.wait_pattern(uboot_ready, timeout=120.0): - raise TerminalTimeoutError("timed out waiting for U-Boot prompt") + uboot_ready = r"=>" + board_wait_uboot_prompt(board_term, uboot_ready, timeout=120.0) time.sleep(0.3) - board_term.flush_input() board_term.send(uboot_cmd) if not board_term.wait_pattern(ZONE0_READY_PATTERN, timeout=180.0): raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") @@ -197,6 +211,64 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: return 0 +def lspci_expected_cfg(cfg: dict[str, Any]) -> dict[str, Any]: + raw = cfg.get("lspci") or {} + if not isinstance(raw, dict): + raw = {} + expected_bdfs = [str(x) for x in raw.get("expected_bdfs") or []] + min_count = int(raw.get("min_count", len(expected_bdfs) or 1)) + return {"expected_bdfs": expected_bdfs, "min_count": min_count} + + +def save_lspci_artifacts(cfg: dict[str, Any], name: str, content: str) -> None: + path = logs_dir(cfg) / name + path.write_text(content, encoding="utf-8") + print(f"[lspci] saved {path}", flush=True) + + +def lspci(cfg: dict[str, Any], term: Terminal | None) -> int: + print("————————————————\ncase: lspci\n————————————————\n", flush=True) + if term is None: + raise SystemExit("terminal backend is required (run zone0_start first)") + + expected = lspci_expected_cfg(cfg) + + _, output = term.run( + "lspci", + "timeout 20 lspci -D > /tmp/ci_lspci.log 2>&1; cat /tmp/ci_lspci.log", + timeout=60.0, + ) + save_lspci_artifacts(cfg, "lspci.log", f"=== lspci -D ===\n{output}\n") + + matched = [bdf for bdf in expected["expected_bdfs"] if bdf in output] + missing = [bdf for bdf in expected["expected_bdfs"] if bdf not in output] + line_count = len([line for line in output.splitlines() if line.strip()]) + + print(f"[lspci] devices listed: {line_count}", flush=True) + if expected["expected_bdfs"]: + print( + f"[lspci] matched expected BDFs ({len(matched)}/{len(expected['expected_bdfs'])}): {matched}", + flush=True, + ) + if missing: + print(f"[lspci] missing expected BDFs: {missing}", flush=True) + + if expected["expected_bdfs"]: + if len(matched) < expected["min_count"]: + raise TerminalCommandError( + "lspci found " + f"{len(matched)}/{expected['min_count']} expected devices; " + f"missing: {missing}" + ) + elif line_count < expected["min_count"]: + raise TerminalCommandError( + f"lspci listed {line_count} devices, expected at least {expected['min_count']}" + ) + + print("lspci verification passed", flush=True) + return 0 + + def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: print("————————————————\ncase: zone1_start\n————————————————\n", flush=True) if term is None: @@ -236,6 +308,7 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: CASE_HANDLERS: dict[str, CaseFunc] = { "zone0_start": zone0_start, + "lspci": lspci, "zone1_start": zone1_start, } @@ -277,6 +350,7 @@ def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: "uboot_cmd": str(tests.get("uboot_cmd", "")).strip(), "uboot_ready_pattern": str(tests.get("uboot_ready_pattern", "")).strip(), "tftp_dir": str(tests.get("tftp_dir", "/home/light/tftp")).strip(), + "lspci": tests.get("lspci") or {}, } diff --git a/jenkins/terminal.py b/jenkins/terminal.py index 00852a982..b31ee4f88 100644 --- a/jenkins/terminal.py +++ b/jenkins/terminal.py @@ -116,6 +116,9 @@ def open(self) -> None: timeout=self.timeout, write_timeout=self.timeout, ) + # CH340 adapters often need DTR asserted before the target UART TX is enabled. + self._serial.dtr = True + self._serial.rts = False def close(self) -> None: if self._serial is None: diff --git a/platform/aarch64/rk3568/board.rs b/platform/aarch64/rk3568/board.rs index b51035673..cc3b54742 100644 --- a/platform/aarch64/rk3568/board.rs +++ b/platform/aarch64/rk3568/board.rs @@ -312,12 +312,24 @@ pub const ROOT_ZONE_MEMORY_REGIONS: &[HvConfigMemoryRegion] = &[ virtual_start: 0xFE310000, size: 0x10000, }, //sdhci + HvConfigMemoryRegion { + mem_type: MEM_TYPE_IO, + physical_start: 0xfe010000, + virtual_start: 0xfe010000, + size: 0x10000, + }, // gmac1 ethernet@fe010000 + HvConfigMemoryRegion { + mem_type: MEM_TYPE_IO, + physical_start: 0xfda00000, + virtual_start: 0xfda00000, + size: 0x200000, + }, // xpcs syscon for gmac RGMII ]; -pub const IRQ_WAKEUP_VIRTIO_DEVICE: usize = 32 + 0x20; +pub const IRQ_WAKEUP_VIRTIO_DEVICE: usize = 32 + 0x26; pub const ROOT_ZONE_IRQS_BITMAP: &[BitmapWord] = &get_irqs_bitmap(&[ - 0x84, 0x98, 0x40, 0x104, 0x105, 0x106, 0x107, 0x2d, 0x2e, 0x2b, 0x2a, 0x29, 0x33, 0x96, 0x11c, - 0x44, 0x43, 0x42, 0x41, 0x8d, 0x48, 0x49, 0x9d, 0x9e, 0xa2, 0xa3, + 0x84, 0x98, 0x46, 0x20, 0x1d, 0x104, 0x105, 0x106, 0x107, 0x108, 0x2d, 0x2e, 0x2b, 0x2a, 0x29, + 0x33, 0x96, 0x11c, 0x44, 0x43, 0x42, 0x41, 0x8d, 0x48, 0x49, 0x9d, 0x9e, 0xa2, 0xa3, ]); pub const ROOT_ARCH_ZONE_CONFIG: HvArchZoneConfig = HvArchZoneConfig { diff --git a/platform/aarch64/rk3568/image/dts/rk3568_limit_zone0.dts b/platform/aarch64/rk3568/image/dts/rk3568_limit_zone0.dts index 3f872905b..0eac654e0 100644 --- a/platform/aarch64/rk3568/image/dts/rk3568_limit_zone0.dts +++ b/platform/aarch64/rk3568/image/dts/rk3568_limit_zone0.dts @@ -16,6 +16,7 @@ mmc1 = "/dwmmc@fe2b0000"; mmc2 = "/dwmmc@fe2c0000"; mmc3 = "/dwmmc@fe000000"; + ethernet1 = "/ethernet@fe010000"; }; syscon@fdc50000 { @@ -392,6 +393,82 @@ reset-names = "reset"; }; + syscon@fda00000 { + compatible = "rockchip,rk3568-xpcs\0syscon"; + status = "okay"; + reg = <0x00 0xfda00000 0x00 0x200000>; + phandle = <0x161>; + }; + + ethernet@fe010000 { + pinctrl-names = "default"; + phy-mode = "rgmii"; + snps,mixed-burst; + snps,mtl-rx-config = <0x81>; + snps,reset-active-low; + pinctrl-0 = <0x84 0x85 0x86 0x87 0x88>; + clock-names = "stmmaceth\0mac_clk_rx\0mac_clk_tx\0clk_mac_refout\0aclk_mac\0pclk_mac\0clk_mac_speed\0ptp_ref\0pclk_xpcs\0clk_xpcs_eee"; + assigned-clocks = <0x20 0x189 0x20 0x186>; + assigned-clock-parents = <0x20 0x187 0x20 0xc5>; + snps,mtl-tx-config = <0x82>; + local-mac-address = [2a fd 45 b0 3b 1a]; + assigned-clock-rates = <0x00 0x7735940>; + resets = <0x20 0xec>; + interrupts = <0x00 0x20 0x04 0x00 0x1d 0x04>; + clocks = <0x20 0x186 0x20 0x189 0x20 0x189 0x20 0xc7 0x20 0xc3 0x20 0xc4 0x20 0x189 0x20 0xc8 0x20 0xac 0x20 0xab>; + clock_in_out = "output"; + snps,tso; + compatible = "rockchip,rk3568-gmac\0snps,dwmac-4.20a"; + status = "okay"; + rockchip,grf = <0x34>; + interrupt-names = "macirq\0eth_wake_irq"; + snps,reset-gpio = <0x83 0x08 0x01>; + reg = <0x00 0xfe010000 0x00 0x10000>; + rx_delay = <0x26>; + phandle = <0x7f>; + phy-handle = <0x89>; + reset-names = "stmmaceth"; + tx_delay = <0x4f>; + snps,axi-config = <0x80>; + snps,reset-delays-us = <0x00 0x4e20 0x186a0>; + + mdio { + #address-cells = <0x01>; + #size-cells = <0x00>; + compatible = "snps,dwmac-mdio"; + phandle = <0x18a>; + + phy@0 { + compatible = "ethernet-phy-ieee802.3-c22"; + reg = <0x00>; + phandle = <0x89>; + }; + }; + + tx-queues-config { + phandle = <0x82>; + snps,tx-queues-to-use = <0x01>; + + queue0 { + }; + }; + + stmmac-axi-config { + snps,wr_osr_lmt = <0x04>; + phandle = <0x80>; + snps,blen = <0x00 0x00 0x00 0x00 0x10 0x08 0x04>; + snps,rd_osr_lmt = <0x08>; + }; + + rx-queues-config { + snps,rx-queues-to-use = <0x01>; + phandle = <0x81>; + + queue0 { + }; + }; + }; + memory { device_type = "memory"; reg = <0x00 0x200000 0x00 0x8200000 0x00 0x9400000 0x00 0xe6c00000 0x01 0xf0000000 0x00 0x10000000>; @@ -1650,6 +1727,6 @@ hvisor_virtio_device { compatible = "hvisor"; interrupt-parent = <0x01>; - interrupts = <0x00 0x20 0x01>; + interrupts = <0x00 0x26 0x01>; }; }; diff --git a/src/pci/pci_access.rs b/src/pci/pci_access.rs index f2c6c77ff..d7b115b3e 100644 --- a/src/pci/pci_access.rs +++ b/src/pci/pci_access.rs @@ -1072,6 +1072,7 @@ pub enum BridgeField { SecondaryBusNumber, SubordinateBusNumber, SecondaryLatencyTimer, + BusNumbers, IOBase, IOLimit, SecondaryStatus, @@ -1108,6 +1109,7 @@ impl Debug for BridgeField { BridgeField::SecondaryBusNumber => write!(f, "SecondaryBusNumber"), BridgeField::SubordinateBusNumber => write!(f, "SubordinateBusNumber"), BridgeField::SecondaryLatencyTimer => write!(f, "SecondaryLatencyTimer"), + BridgeField::BusNumbers => write!(f, "BusNumbers"), BridgeField::IOBase => write!(f, "IOBase"), BridgeField::IOLimit => write!(f, "IOLimit"), BridgeField::SecondaryStatus => write!(f, "SecondaryStatus"), @@ -1146,6 +1148,7 @@ impl PciField for BridgeField { BridgeField::SecondaryBusNumber => 0x19, BridgeField::SubordinateBusNumber => 0x1a, BridgeField::SecondaryLatencyTimer => 0x1b, + BridgeField::BusNumbers => 0x18, BridgeField::IOBase => 0x1c, BridgeField::IOLimit => 0x1d, BridgeField::SecondaryStatus => 0x1e, @@ -1181,6 +1184,7 @@ impl PciField for BridgeField { BridgeField::SecondaryBusNumber => 1, BridgeField::SubordinateBusNumber => 1, BridgeField::SecondaryLatencyTimer => 1, + BridgeField::BusNumbers => 4, BridgeField::IOBase => 1, BridgeField::IOLimit => 1, BridgeField::SecondaryStatus => 2, @@ -1219,6 +1223,7 @@ impl BridgeField { (0x19, 1) => BridgeField::SecondaryBusNumber, (0x1a, 1) => BridgeField::SubordinateBusNumber, (0x1b, 1) => BridgeField::SecondaryLatencyTimer, + (0x18, 4) => BridgeField::BusNumbers, (0x1c, 1) => BridgeField::IOBase, (0x1d, 1) => BridgeField::IOLimit, (0x1e, 2) => BridgeField::SecondaryStatus, diff --git a/src/pci/pci_config.rs b/src/pci/pci_config.rs index 9612ccf00..0e8029342 100644 --- a/src/pci/pci_config.rs +++ b/src/pci/pci_config.rs @@ -331,7 +331,7 @@ impl Zone { }) { let mut vdev = dev.read().config_space.clone(); - vdev.set_vbdf(vbdf); + vdev.set_vbdf(vbdf, target_pci_config.bus_range_end as u8); let msi_count = vdev.get_msi_count(); domain_msi_count += msi_count; inner.vpci_bus_mut().insert(vbdf, vdev); @@ -351,7 +351,7 @@ impl Zone { } else { dev.set_zone_id(Some(_zone_id as u32)); let mut vdev_inner = dev.read().config_space.clone(); - vdev_inner.set_vbdf(vbdf); + vdev_inner.set_vbdf(vbdf, target_pci_config.bus_range_end as u8); let msi_count = vdev_inner.get_msi_count(); domain_msi_count += msi_count; inner.vpci_bus_mut().insert(vbdf, vdev_inner); diff --git a/src/pci/pci_handler.rs b/src/pci/pci_handler.rs index 71cc787a1..445364086 100644 --- a/src/pci/pci_handler.rs +++ b/src/pci/pci_handler.rs @@ -34,7 +34,7 @@ use crate::zone::is_this_root_zone; use super::pci_access::{BridgeField, EndpointField, HeaderType, PciField, PciMemType}; use super::pci_config::GLOBAL_PCIE_LIST; -use super::pci_struct::{ArcRwLockVirtualPciConfigSpace, BIT_LENTH}; +use super::pci_struct::{ArcRwLockVirtualPciConfigSpace, Bdf, BIT_LENTH}; use super::vpci_dev::VpciDevType; use super::PciConfigAddress; @@ -1606,6 +1606,36 @@ fn handle_pci_bridge_access( Ok(None) } } + BridgeField::PrimaryBusNumber + | BridgeField::SecondaryBusNumber + | BridgeField::SubordinateBusNumber + | BridgeField::SecondaryLatencyTimer => { + let reg_offset = field.to_offset() as usize; + if is_write { + if is_dev_belong_to_zone || (is_direct && is_root) { + dev.with_config_value_mut(|cv| { + let mut reg = cv.get_bridge_bus_reg(); + let shift = (reg_offset - 0x18) * 8; + reg = (reg & !((0xffu32) << shift)) | (((value as u32) & 0xff) << shift); + cv.set_bridge_bus_reg(reg); + }); + } + Ok(None) + } else { + let reg = dev.with_config_value(|cv| cv.get_bridge_bus_reg()); + Ok(Some((((reg >> ((reg_offset - 0x18) * 8)) & 0xff)) as usize)) + } + } + BridgeField::BusNumbers => { + if is_write { + if is_dev_belong_to_zone || (is_direct && is_root) { + dev.with_config_value_mut(|cv| cv.set_bridge_bus_reg(value as u32)); + } + Ok(None) + } else { + Ok(Some(dev.with_config_value(|cv| cv.get_bridge_bus_reg()) as usize)) + } + } _ => Ok(None), } } @@ -1879,6 +1909,7 @@ pub fn mmio_dwc_cfg_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { if let Some((atu, ecam_base)) = atu_config { // Get dbi_base from platform config (usually dbi_base == ecam_base) use crate::platform; + let pci_target = atu.pci_target(); if let Some(extend_config) = platform::ROOT_DWC_ATU_CONFIG .iter() .find(|cfg| cfg.ecam_base == ecam_base as u64) @@ -1889,7 +1920,6 @@ pub fn mmio_dwc_cfg_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { let dbi_region = PciRegionMmio::new(dbi_base, dbi_size); let dbi_backend = DwcConfigRegionBackend::new(dbi_region); - let pci_target = atu.pci_target(); let target_bus = ((pci_target >> 24) & 0xff) as u8; let target_device = ((pci_target >> 19) & 0x1f) as u8; let target_function = ((pci_target >> 16) & 0x7) as u8; @@ -1903,7 +1933,7 @@ pub fn mmio_dwc_cfg_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { && vbdf.device() == target_device && vbdf.function() == target_function { - Some((dev.get_bdf(), dev.get_parent_bus())) + Some(dev.get_bdf()) } else { None } @@ -1914,40 +1944,39 @@ pub fn mmio_dwc_cfg_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { let mut atu_type = atu.atu_type(); let mut config_base = atu.cpu_base(); let mut cpu_limit = atu.cpu_limit(); - if let Some((host_bdf, parent_bus)) = mapped_target { + if let Some(host_bdf) = mapped_target { hw_pci_target = ((host_bdf.bus() as u64) << 24) + ((host_bdf.device() as u64) << 19) + ((host_bdf.function() as u64) << 16); - (config_base, atu_type) = if parent_bus == 0 { - (extend_config.cfg_base, AtuType::Cfg0) + let half = extend_config.cfg_size / 2; + config_base = _base as u64; + atu_type = if config_base >= extend_config.cfg_base + half { + AtuType::Cfg1 } else { - ( - extend_config.cfg_base + (extend_config.cfg_size / 2), - AtuType::Cfg1, - ) + AtuType::Cfg0 }; - cpu_limit = config_base + (extend_config.cfg_size / 2) - 1; + cpu_limit = config_base + half - 1; } - // Program hardware ATU with translated host target when remap exists. - let mut hw_atu = atu; - hw_atu.set_pci_target(hw_pci_target); - hw_atu.set_atu_type(atu_type); - hw_atu.set_cpu_base(config_base); - hw_atu.set_cpu_limit(cpu_limit); - AtuUnroll::dw_pcie_prog_outbound_atu_unroll(&dbi_backend, &hw_atu)?; + // Program hardware ATU only when host/guest BDF remapping is required. + if hw_pci_target != pci_target { + let mut hw_atu = atu; + hw_atu.set_pci_target(hw_pci_target); + hw_atu.set_atu_type(atu_type); + hw_atu.set_cpu_base(config_base); + hw_atu.set_cpu_limit(cpu_limit); + AtuUnroll::dw_pcie_prog_outbound_atu_unroll(&dbi_backend, &hw_atu)?; + } } let offset = (mmio.address & 0xfff) as PciConfigAddress; let zone = this_zone(); let mut is_dev_belong_to_zone = false; - let base = mmio.address as PciConfigAddress - offset + atu.pci_target(); - let dev: Option = { let mut guard = zone.write(); let vbus = guard.vpci_bus_mut(); - if let Some(dev) = vbus.get_device_by_base(base) { + if let Some(dev) = vbus.get_device_by_base(pci_target) { is_dev_belong_to_zone = true; Some(dev) } else { @@ -1956,13 +1985,29 @@ pub fn mmio_dwc_cfg_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { // This avoids holding multiple locks simultaneously let dev_clone = { let global_pcie_list = GLOBAL_PCIE_LIST.lock(); + let domain = platform::ROOT_PCI_CONFIG + .iter() + .find(|cfg| cfg.ecam_base as usize == ecam_base) + .map(|cfg| cfg.domain) + .unwrap_or(0); + let target_bdf = Bdf::new( + domain, + ((pci_target >> 24) & 0xff) as u8, + ((pci_target >> 19) & 0x1f) as u8, + ((pci_target >> 16) & 0x7) as u8, + ); global_pcie_list - .values() - .find(|dev| { - let dev_guard = dev.read(); - dev_guard.get_base() == base - }) + .get(&target_bdf) .cloned() + .or_else(|| { + global_pcie_list + .values() + .find(|dev| { + let dev_guard = dev.read(); + dev_guard.get_base() == pci_target + }) + .cloned() + }) }; dev_clone } @@ -2058,17 +2103,32 @@ pub fn mmio_vpci_handler_dbi(mmio: &mut MMIOAccess, _base: usize) -> HvResult { let root_config = platform::platform_root_zone_config(); let num_pci_bus = root_config.num_pci_bus as usize; - crate::pci::pci_config::hvisor_pci_init(&root_config.pci_config[..num_pci_bus])?; - let zone = crate::zone::root_zone(); let mut inner = zone.write(); - inner.virtual_pci_mmio_init_delay(&root_config.pci_config, num_pci_bus); + inner.virtual_pci_mmio_init_delay( + &root_config.pci_config, + num_pci_bus, + domain_id, + ); + drop(inner); + + if let Some(domain_cfg) = root_config.pci_config[..num_pci_bus] + .iter() + .find(|cfg| cfg.domain == domain_id && cfg.ecam_base != 0) + { + crate::pci::pci_config::hvisor_pci_init(core::slice::from_ref(domain_cfg))?; + } else { + warn!("No PCI config found for domain {}", domain_id); + } + + let mut inner = zone.write(); inner.guest_pci_init_delay( 0, &root_config.alloc_pci_devs, root_config.num_pci_devs, &root_config.pci_config, num_pci_bus, + domain_id, )?; #[cfg(dwc_msi)] diff --git a/src/pci/pci_struct.rs b/src/pci/pci_struct.rs index a84327f27..3a98f95ae 100644 --- a/src/pci/pci_struct.rs +++ b/src/pci/pci_struct.rs @@ -55,6 +55,7 @@ pub struct ConfigValue { class_and_revision_id: (BaseClass, SubClass, Interface, DeviceRevision), bar_value: [u32; 6], rom_value: u32, + bridge_bus_reg: u32, } impl Default for ConfigValue { @@ -64,6 +65,7 @@ impl Default for ConfigValue { class_and_revision_id: (0xFFu8, 0u8, 0u8, 0u8), bar_value: [0; 6], rom_value: 0, + bridge_bus_reg: 0, } } } @@ -78,6 +80,7 @@ impl ConfigValue { class_and_revision_id, bar_value: [0; 6], rom_value: 0, + bridge_bus_reg: 0, } } @@ -143,6 +146,14 @@ impl ConfigValue { pub fn set_rom_value(&mut self, value: u32) { self.rom_value = value; } + + pub fn get_bridge_bus_reg(&self) -> u32 { + self.bridge_bus_reg + } + + pub fn set_bridge_bus_reg(&mut self, value: u32) { + self.bridge_bus_reg = value; + } } const MAX_DEVICE: u8 = 31; @@ -361,6 +372,7 @@ impl VirtualPciAccessBits { pub fn bridge() -> Self { let mut bits = BitArray::ZERO; bits[0x10..0x18].fill(true); // BARs + bits[0x18..0x1c].fill(true); // Primary/Secondary/Subordinate bus + latency bits[0x38..0x3c].fill(true); // ROM bits[0x34..0x38].fill(true); // Capability Pointer bits[0x40..0x100].fill(true); // Capability region (caps start at 0x40) @@ -1394,8 +1406,20 @@ impl VirtualPciConfigSpace { self.config_type } - pub fn set_vbdf(&mut self, vbdf: Bdf) { + pub fn set_vbdf(&mut self, vbdf: Bdf, domain_bus_range_end: u8) { self.vbdf = vbdf; + if self.config_type == HeaderType::PciBridge { + self.init_bridge_bus_reg(domain_bus_range_end); + } + } + + fn init_bridge_bus_reg(&mut self, domain_bus_range_end: u8) { + let primary = self.vbdf.bus(); + let secondary = primary.saturating_add(1); + let subordinate = domain_bus_range_end; + self.config_value.set_bridge_bus_reg( + ((subordinate as u32) << 16) | ((secondary as u32) << 8) | (primary as u32), + ); } pub fn get_base(&self) -> PciConfigAddress { @@ -1734,6 +1758,7 @@ impl PciIterator { let _ = node.build_sriov_info(); // Build MSI/MSIX info once during device discovery node.build_msi_info(); + node.set_vbdf(bdf, self.bus_range.end as u8); Some(node) } @@ -1762,6 +1787,7 @@ impl PciIterator { let _ = node.build_sriov_info(); // Build MSI/MSIX info once during device discovery node.build_msi_info(); + node.set_vbdf(bdf, self.bus_range.end as u8); Some(node) } diff --git a/src/pci/pci_test.rs b/src/pci/pci_test.rs index c6d7b22d4..629697181 100644 --- a/src/pci/pci_test.rs +++ b/src/pci/pci_test.rs @@ -78,7 +78,7 @@ pub fn pcie_guest_init() { // let _ = dev.write_hw(0x24, 4, 0xffffffff); // let value2 = dev.read_hw(0x24, 4).unwrap(); // info!("{:#?} bar64 {:x}, {:x}", bdf, (value1 as u64), ((value2 as u64) << 32u64)); - dev.set_vbdf(vbdf); + dev.set_vbdf(vbdf, 0xff); vbus.insert(vbdf, dev); } else { warn!("can not find dev"); @@ -87,7 +87,7 @@ pub fn pcie_guest_init() { let vbdf = Bdf::from_str("0000:00:02.0").unwrap(); let bdf = Bdf::from_str("0000:00:02.0").unwrap(); if let Some(mut dev) = guard.remove(&bdf) { - dev.set_vbdf(vbdf); + dev.set_vbdf(vbdf, 0xff); vbus.insert(vbdf, dev); } else { warn!("can not find dev"); @@ -96,7 +96,7 @@ pub fn pcie_guest_init() { let vbdf = Bdf::from_str("0000:00:03.0").unwrap(); let bdf = Bdf::from_str("0000:00:03.0").unwrap(); if let Some(mut dev) = guard.remove(&bdf) { - dev.set_vbdf(vbdf); + dev.set_vbdf(vbdf, 0xff); vbus.insert(vbdf, dev); } else { warn!("can not find dev"); diff --git a/src/zone.rs b/src/zone.rs index 9aefa419a..ed9137113 100644 --- a/src/zone.rs +++ b/src/zone.rs @@ -326,11 +326,12 @@ impl ZoneInner { num_pci_devs: u64, pci_config: &[HvPciConfig], _num_pci_config: usize, + domain_id: u8, ) -> HvResult { let guard = GLOBAL_PCIE_LIST.lock(); for target_pci_config in pci_config { // Skip empty config - if target_pci_config.ecam_base == 0 { + if target_pci_config.ecam_base == 0 || target_pci_config.domain != domain_id { continue; } @@ -455,7 +456,7 @@ impl ZoneInner { }) { let mut vdev = dev.read().config_space.clone(); - vdev.set_vbdf(vbdf); + vdev.set_vbdf(vbdf, target_pci_config.bus_range_end as u8); let msi_count = vdev.get_msi_count(); domain_msi_count += msi_count; self.vpci_bus_mut().insert(vbdf, vdev); @@ -475,7 +476,7 @@ impl ZoneInner { } else { dev.set_zone_id(Some(_zone_id as u32)); let mut vdev_inner = dev.read().config_space.clone(); - vdev_inner.set_vbdf(vbdf); + vdev_inner.set_vbdf(vbdf, target_pci_config.bus_range_end as u8); let msi_count = vdev_inner.get_msi_count(); domain_msi_count += msi_count; self.vpci_bus_mut().insert(vbdf, vdev_inner); @@ -608,12 +609,13 @@ impl ZoneInner { &mut self, pci_rootcomplex_config: &[HvPciConfig; CONFIG_PCI_BUS_MAXNUM], _num_pci_config: usize, + domain_id: u8, ) { #[cfg(loongarch64_pcie)] let mut emergency_map_regions: alloc::vec::Vec<(usize, usize)> = alloc::vec::Vec::new(); for rootcomplex_config in pci_rootcomplex_config { - if rootcomplex_config.ecam_base == 0 { + if rootcomplex_config.ecam_base == 0 || rootcomplex_config.domain != domain_id { continue; } #[cfg(ecam_pcie)] From e046845740e921dc2bc5e686ae0795008462fd12 Mon Sep 17 00:00:00 2001 From: dallas Date: Mon, 13 Jul 2026 19:19:30 +0800 Subject: [PATCH 29/53] add pcie test for rk3568 --- jenkins/ci.yaml | 14 ++- jenkins/ci_runner.py | 84 ++++++++++++- jenkins/terminal.py | 3 + platform/aarch64/rk3568/board.rs | 18 ++- .../rk3568/image/dts/rk3568_limit_zone0.dts | 79 +++++++++++- src/pci/pci_access.rs | 5 + src/pci/pci_config.rs | 4 +- src/pci/pci_handler.rs | 118 +++++++++++++----- src/pci/pci_struct.rs | 28 ++++- src/pci/pci_test.rs | 6 +- src/zone.rs | 10 +- 11 files changed, 318 insertions(+), 51 deletions(-) diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index 6084ba950..0124e3220 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -24,13 +24,21 @@ bids: - KDIR=/home/light/DEMO/sdk/rk356x-up4-2c/kernel tests: mode: board - serial: /dev/ttyUSB0 - power_serial: /dev/ttyUSB1 + serial: /dev/serial/by-id/usb-1a86_USB2.0-Ser_-if00-port0 + power_serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 baudrate: 1500000 - uboot_ready_pattern: 'Net:.*\n=> ' + uboot_ready_pattern: '=>' uboot_cmd: pci enum;setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.240; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:hvisor.bin; tftp ${fdt_addr} ${serverip}:rk3568_limit_zone0.dtb; tftp ${zone0_kernel_addr} ${serverip}:Image; bootm ${loadaddr} - ${fdt_addr}; + lspci: + expected_bdfs: + - "0001:10:00.0" + - "0001:11:00.0" + - "0002:20:00.0" + - "0002:21:00.0" + min_count: 4 cases: - zone0_start + - lspci - zone1_start diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index aaea00168..8a6cb3f2d 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -141,7 +141,21 @@ def board_power_cycle(cfg: dict[str, Any]) -> None: subprocess.run(["bash", str(script), "cycle", power_port], check=True, cwd=cfg["workspace"]) -def save_inner_serial_log(cfg: dict[str, Any], content: str) -> None: +def board_wake_console(term: Terminal, *, repeats: int = 3) -> None: + for _ in range(repeats): + term.send("") + time.sleep(0.2) + + +def board_wait_uboot_prompt(term: Terminal, pattern: str, timeout: float) -> None: + """Wait for U-Boot prompt, periodically waking an idle console.""" + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + remaining = deadline - time.monotonic() + if term.wait_pattern(pattern, timeout=min(5.0, remaining), from_offset=0): + return + board_wake_console(term) + raise TerminalTimeoutError(f"timed out waiting for U-Boot prompt (pattern={pattern!r})") if not content: return path = logs_dir(cfg) / "zone1_inner_serial.log" @@ -180,16 +194,16 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: board_term.open() cfg["_board_term"] = board_term board_power_cycle(cfg) + time.sleep(3.0) + board_wake_console(board_term) uboot_cmd = cfg.get("uboot_cmd", "") uboot_ready = cfg.get("uboot_ready_pattern", "") if uboot_cmd: if not uboot_ready: - uboot_ready = r"Net:.*\n=> " - if not board_term.wait_pattern(uboot_ready, timeout=120.0): - raise TerminalTimeoutError("timed out waiting for U-Boot prompt") + uboot_ready = r"=>" + board_wait_uboot_prompt(board_term, uboot_ready, timeout=120.0) time.sleep(0.3) - board_term.flush_input() board_term.send(uboot_cmd) if not board_term.wait_pattern(ZONE0_READY_PATTERN, timeout=180.0): raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") @@ -197,6 +211,64 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: return 0 +def lspci_expected_cfg(cfg: dict[str, Any]) -> dict[str, Any]: + raw = cfg.get("lspci") or {} + if not isinstance(raw, dict): + raw = {} + expected_bdfs = [str(x) for x in raw.get("expected_bdfs") or []] + min_count = int(raw.get("min_count", len(expected_bdfs) or 1)) + return {"expected_bdfs": expected_bdfs, "min_count": min_count} + + +def save_lspci_artifacts(cfg: dict[str, Any], name: str, content: str) -> None: + path = logs_dir(cfg) / name + path.write_text(content, encoding="utf-8") + print(f"[lspci] saved {path}", flush=True) + + +def lspci(cfg: dict[str, Any], term: Terminal | None) -> int: + print("————————————————\ncase: lspci\n————————————————\n", flush=True) + if term is None: + raise SystemExit("terminal backend is required (run zone0_start first)") + + expected = lspci_expected_cfg(cfg) + + _, output = term.run( + "lspci", + "timeout 20 lspci -D > /tmp/ci_lspci.log 2>&1; cat /tmp/ci_lspci.log", + timeout=60.0, + ) + save_lspci_artifacts(cfg, "lspci.log", f"=== lspci -D ===\n{output}\n") + + matched = [bdf for bdf in expected["expected_bdfs"] if bdf in output] + missing = [bdf for bdf in expected["expected_bdfs"] if bdf not in output] + line_count = len([line for line in output.splitlines() if line.strip()]) + + print(f"[lspci] devices listed: {line_count}", flush=True) + if expected["expected_bdfs"]: + print( + f"[lspci] matched expected BDFs ({len(matched)}/{len(expected['expected_bdfs'])}): {matched}", + flush=True, + ) + if missing: + print(f"[lspci] missing expected BDFs: {missing}", flush=True) + + if expected["expected_bdfs"]: + if len(matched) < expected["min_count"]: + raise TerminalCommandError( + "lspci found " + f"{len(matched)}/{expected['min_count']} expected devices; " + f"missing: {missing}" + ) + elif line_count < expected["min_count"]: + raise TerminalCommandError( + f"lspci listed {line_count} devices, expected at least {expected['min_count']}" + ) + + print("lspci verification passed", flush=True) + return 0 + + def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: print("————————————————\ncase: zone1_start\n————————————————\n", flush=True) if term is None: @@ -236,6 +308,7 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: CASE_HANDLERS: dict[str, CaseFunc] = { "zone0_start": zone0_start, + "lspci": lspci, "zone1_start": zone1_start, } @@ -277,6 +350,7 @@ def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: "uboot_cmd": str(tests.get("uboot_cmd", "")).strip(), "uboot_ready_pattern": str(tests.get("uboot_ready_pattern", "")).strip(), "tftp_dir": str(tests.get("tftp_dir", "/home/light/tftp")).strip(), + "lspci": tests.get("lspci") or {}, } diff --git a/jenkins/terminal.py b/jenkins/terminal.py index 00852a982..b31ee4f88 100644 --- a/jenkins/terminal.py +++ b/jenkins/terminal.py @@ -116,6 +116,9 @@ def open(self) -> None: timeout=self.timeout, write_timeout=self.timeout, ) + # CH340 adapters often need DTR asserted before the target UART TX is enabled. + self._serial.dtr = True + self._serial.rts = False def close(self) -> None: if self._serial is None: diff --git a/platform/aarch64/rk3568/board.rs b/platform/aarch64/rk3568/board.rs index b51035673..cc3b54742 100644 --- a/platform/aarch64/rk3568/board.rs +++ b/platform/aarch64/rk3568/board.rs @@ -312,12 +312,24 @@ pub const ROOT_ZONE_MEMORY_REGIONS: &[HvConfigMemoryRegion] = &[ virtual_start: 0xFE310000, size: 0x10000, }, //sdhci + HvConfigMemoryRegion { + mem_type: MEM_TYPE_IO, + physical_start: 0xfe010000, + virtual_start: 0xfe010000, + size: 0x10000, + }, // gmac1 ethernet@fe010000 + HvConfigMemoryRegion { + mem_type: MEM_TYPE_IO, + physical_start: 0xfda00000, + virtual_start: 0xfda00000, + size: 0x200000, + }, // xpcs syscon for gmac RGMII ]; -pub const IRQ_WAKEUP_VIRTIO_DEVICE: usize = 32 + 0x20; +pub const IRQ_WAKEUP_VIRTIO_DEVICE: usize = 32 + 0x26; pub const ROOT_ZONE_IRQS_BITMAP: &[BitmapWord] = &get_irqs_bitmap(&[ - 0x84, 0x98, 0x40, 0x104, 0x105, 0x106, 0x107, 0x2d, 0x2e, 0x2b, 0x2a, 0x29, 0x33, 0x96, 0x11c, - 0x44, 0x43, 0x42, 0x41, 0x8d, 0x48, 0x49, 0x9d, 0x9e, 0xa2, 0xa3, + 0x84, 0x98, 0x46, 0x20, 0x1d, 0x104, 0x105, 0x106, 0x107, 0x108, 0x2d, 0x2e, 0x2b, 0x2a, 0x29, + 0x33, 0x96, 0x11c, 0x44, 0x43, 0x42, 0x41, 0x8d, 0x48, 0x49, 0x9d, 0x9e, 0xa2, 0xa3, ]); pub const ROOT_ARCH_ZONE_CONFIG: HvArchZoneConfig = HvArchZoneConfig { diff --git a/platform/aarch64/rk3568/image/dts/rk3568_limit_zone0.dts b/platform/aarch64/rk3568/image/dts/rk3568_limit_zone0.dts index 3f872905b..0eac654e0 100644 --- a/platform/aarch64/rk3568/image/dts/rk3568_limit_zone0.dts +++ b/platform/aarch64/rk3568/image/dts/rk3568_limit_zone0.dts @@ -16,6 +16,7 @@ mmc1 = "/dwmmc@fe2b0000"; mmc2 = "/dwmmc@fe2c0000"; mmc3 = "/dwmmc@fe000000"; + ethernet1 = "/ethernet@fe010000"; }; syscon@fdc50000 { @@ -392,6 +393,82 @@ reset-names = "reset"; }; + syscon@fda00000 { + compatible = "rockchip,rk3568-xpcs\0syscon"; + status = "okay"; + reg = <0x00 0xfda00000 0x00 0x200000>; + phandle = <0x161>; + }; + + ethernet@fe010000 { + pinctrl-names = "default"; + phy-mode = "rgmii"; + snps,mixed-burst; + snps,mtl-rx-config = <0x81>; + snps,reset-active-low; + pinctrl-0 = <0x84 0x85 0x86 0x87 0x88>; + clock-names = "stmmaceth\0mac_clk_rx\0mac_clk_tx\0clk_mac_refout\0aclk_mac\0pclk_mac\0clk_mac_speed\0ptp_ref\0pclk_xpcs\0clk_xpcs_eee"; + assigned-clocks = <0x20 0x189 0x20 0x186>; + assigned-clock-parents = <0x20 0x187 0x20 0xc5>; + snps,mtl-tx-config = <0x82>; + local-mac-address = [2a fd 45 b0 3b 1a]; + assigned-clock-rates = <0x00 0x7735940>; + resets = <0x20 0xec>; + interrupts = <0x00 0x20 0x04 0x00 0x1d 0x04>; + clocks = <0x20 0x186 0x20 0x189 0x20 0x189 0x20 0xc7 0x20 0xc3 0x20 0xc4 0x20 0x189 0x20 0xc8 0x20 0xac 0x20 0xab>; + clock_in_out = "output"; + snps,tso; + compatible = "rockchip,rk3568-gmac\0snps,dwmac-4.20a"; + status = "okay"; + rockchip,grf = <0x34>; + interrupt-names = "macirq\0eth_wake_irq"; + snps,reset-gpio = <0x83 0x08 0x01>; + reg = <0x00 0xfe010000 0x00 0x10000>; + rx_delay = <0x26>; + phandle = <0x7f>; + phy-handle = <0x89>; + reset-names = "stmmaceth"; + tx_delay = <0x4f>; + snps,axi-config = <0x80>; + snps,reset-delays-us = <0x00 0x4e20 0x186a0>; + + mdio { + #address-cells = <0x01>; + #size-cells = <0x00>; + compatible = "snps,dwmac-mdio"; + phandle = <0x18a>; + + phy@0 { + compatible = "ethernet-phy-ieee802.3-c22"; + reg = <0x00>; + phandle = <0x89>; + }; + }; + + tx-queues-config { + phandle = <0x82>; + snps,tx-queues-to-use = <0x01>; + + queue0 { + }; + }; + + stmmac-axi-config { + snps,wr_osr_lmt = <0x04>; + phandle = <0x80>; + snps,blen = <0x00 0x00 0x00 0x00 0x10 0x08 0x04>; + snps,rd_osr_lmt = <0x08>; + }; + + rx-queues-config { + snps,rx-queues-to-use = <0x01>; + phandle = <0x81>; + + queue0 { + }; + }; + }; + memory { device_type = "memory"; reg = <0x00 0x200000 0x00 0x8200000 0x00 0x9400000 0x00 0xe6c00000 0x01 0xf0000000 0x00 0x10000000>; @@ -1650,6 +1727,6 @@ hvisor_virtio_device { compatible = "hvisor"; interrupt-parent = <0x01>; - interrupts = <0x00 0x20 0x01>; + interrupts = <0x00 0x26 0x01>; }; }; diff --git a/src/pci/pci_access.rs b/src/pci/pci_access.rs index f2c6c77ff..d7b115b3e 100644 --- a/src/pci/pci_access.rs +++ b/src/pci/pci_access.rs @@ -1072,6 +1072,7 @@ pub enum BridgeField { SecondaryBusNumber, SubordinateBusNumber, SecondaryLatencyTimer, + BusNumbers, IOBase, IOLimit, SecondaryStatus, @@ -1108,6 +1109,7 @@ impl Debug for BridgeField { BridgeField::SecondaryBusNumber => write!(f, "SecondaryBusNumber"), BridgeField::SubordinateBusNumber => write!(f, "SubordinateBusNumber"), BridgeField::SecondaryLatencyTimer => write!(f, "SecondaryLatencyTimer"), + BridgeField::BusNumbers => write!(f, "BusNumbers"), BridgeField::IOBase => write!(f, "IOBase"), BridgeField::IOLimit => write!(f, "IOLimit"), BridgeField::SecondaryStatus => write!(f, "SecondaryStatus"), @@ -1146,6 +1148,7 @@ impl PciField for BridgeField { BridgeField::SecondaryBusNumber => 0x19, BridgeField::SubordinateBusNumber => 0x1a, BridgeField::SecondaryLatencyTimer => 0x1b, + BridgeField::BusNumbers => 0x18, BridgeField::IOBase => 0x1c, BridgeField::IOLimit => 0x1d, BridgeField::SecondaryStatus => 0x1e, @@ -1181,6 +1184,7 @@ impl PciField for BridgeField { BridgeField::SecondaryBusNumber => 1, BridgeField::SubordinateBusNumber => 1, BridgeField::SecondaryLatencyTimer => 1, + BridgeField::BusNumbers => 4, BridgeField::IOBase => 1, BridgeField::IOLimit => 1, BridgeField::SecondaryStatus => 2, @@ -1219,6 +1223,7 @@ impl BridgeField { (0x19, 1) => BridgeField::SecondaryBusNumber, (0x1a, 1) => BridgeField::SubordinateBusNumber, (0x1b, 1) => BridgeField::SecondaryLatencyTimer, + (0x18, 4) => BridgeField::BusNumbers, (0x1c, 1) => BridgeField::IOBase, (0x1d, 1) => BridgeField::IOLimit, (0x1e, 2) => BridgeField::SecondaryStatus, diff --git a/src/pci/pci_config.rs b/src/pci/pci_config.rs index 9612ccf00..0e8029342 100644 --- a/src/pci/pci_config.rs +++ b/src/pci/pci_config.rs @@ -331,7 +331,7 @@ impl Zone { }) { let mut vdev = dev.read().config_space.clone(); - vdev.set_vbdf(vbdf); + vdev.set_vbdf(vbdf, target_pci_config.bus_range_end as u8); let msi_count = vdev.get_msi_count(); domain_msi_count += msi_count; inner.vpci_bus_mut().insert(vbdf, vdev); @@ -351,7 +351,7 @@ impl Zone { } else { dev.set_zone_id(Some(_zone_id as u32)); let mut vdev_inner = dev.read().config_space.clone(); - vdev_inner.set_vbdf(vbdf); + vdev_inner.set_vbdf(vbdf, target_pci_config.bus_range_end as u8); let msi_count = vdev_inner.get_msi_count(); domain_msi_count += msi_count; inner.vpci_bus_mut().insert(vbdf, vdev_inner); diff --git a/src/pci/pci_handler.rs b/src/pci/pci_handler.rs index 71cc787a1..445364086 100644 --- a/src/pci/pci_handler.rs +++ b/src/pci/pci_handler.rs @@ -34,7 +34,7 @@ use crate::zone::is_this_root_zone; use super::pci_access::{BridgeField, EndpointField, HeaderType, PciField, PciMemType}; use super::pci_config::GLOBAL_PCIE_LIST; -use super::pci_struct::{ArcRwLockVirtualPciConfigSpace, BIT_LENTH}; +use super::pci_struct::{ArcRwLockVirtualPciConfigSpace, Bdf, BIT_LENTH}; use super::vpci_dev::VpciDevType; use super::PciConfigAddress; @@ -1606,6 +1606,36 @@ fn handle_pci_bridge_access( Ok(None) } } + BridgeField::PrimaryBusNumber + | BridgeField::SecondaryBusNumber + | BridgeField::SubordinateBusNumber + | BridgeField::SecondaryLatencyTimer => { + let reg_offset = field.to_offset() as usize; + if is_write { + if is_dev_belong_to_zone || (is_direct && is_root) { + dev.with_config_value_mut(|cv| { + let mut reg = cv.get_bridge_bus_reg(); + let shift = (reg_offset - 0x18) * 8; + reg = (reg & !((0xffu32) << shift)) | (((value as u32) & 0xff) << shift); + cv.set_bridge_bus_reg(reg); + }); + } + Ok(None) + } else { + let reg = dev.with_config_value(|cv| cv.get_bridge_bus_reg()); + Ok(Some((((reg >> ((reg_offset - 0x18) * 8)) & 0xff)) as usize)) + } + } + BridgeField::BusNumbers => { + if is_write { + if is_dev_belong_to_zone || (is_direct && is_root) { + dev.with_config_value_mut(|cv| cv.set_bridge_bus_reg(value as u32)); + } + Ok(None) + } else { + Ok(Some(dev.with_config_value(|cv| cv.get_bridge_bus_reg()) as usize)) + } + } _ => Ok(None), } } @@ -1879,6 +1909,7 @@ pub fn mmio_dwc_cfg_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { if let Some((atu, ecam_base)) = atu_config { // Get dbi_base from platform config (usually dbi_base == ecam_base) use crate::platform; + let pci_target = atu.pci_target(); if let Some(extend_config) = platform::ROOT_DWC_ATU_CONFIG .iter() .find(|cfg| cfg.ecam_base == ecam_base as u64) @@ -1889,7 +1920,6 @@ pub fn mmio_dwc_cfg_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { let dbi_region = PciRegionMmio::new(dbi_base, dbi_size); let dbi_backend = DwcConfigRegionBackend::new(dbi_region); - let pci_target = atu.pci_target(); let target_bus = ((pci_target >> 24) & 0xff) as u8; let target_device = ((pci_target >> 19) & 0x1f) as u8; let target_function = ((pci_target >> 16) & 0x7) as u8; @@ -1903,7 +1933,7 @@ pub fn mmio_dwc_cfg_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { && vbdf.device() == target_device && vbdf.function() == target_function { - Some((dev.get_bdf(), dev.get_parent_bus())) + Some(dev.get_bdf()) } else { None } @@ -1914,40 +1944,39 @@ pub fn mmio_dwc_cfg_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { let mut atu_type = atu.atu_type(); let mut config_base = atu.cpu_base(); let mut cpu_limit = atu.cpu_limit(); - if let Some((host_bdf, parent_bus)) = mapped_target { + if let Some(host_bdf) = mapped_target { hw_pci_target = ((host_bdf.bus() as u64) << 24) + ((host_bdf.device() as u64) << 19) + ((host_bdf.function() as u64) << 16); - (config_base, atu_type) = if parent_bus == 0 { - (extend_config.cfg_base, AtuType::Cfg0) + let half = extend_config.cfg_size / 2; + config_base = _base as u64; + atu_type = if config_base >= extend_config.cfg_base + half { + AtuType::Cfg1 } else { - ( - extend_config.cfg_base + (extend_config.cfg_size / 2), - AtuType::Cfg1, - ) + AtuType::Cfg0 }; - cpu_limit = config_base + (extend_config.cfg_size / 2) - 1; + cpu_limit = config_base + half - 1; } - // Program hardware ATU with translated host target when remap exists. - let mut hw_atu = atu; - hw_atu.set_pci_target(hw_pci_target); - hw_atu.set_atu_type(atu_type); - hw_atu.set_cpu_base(config_base); - hw_atu.set_cpu_limit(cpu_limit); - AtuUnroll::dw_pcie_prog_outbound_atu_unroll(&dbi_backend, &hw_atu)?; + // Program hardware ATU only when host/guest BDF remapping is required. + if hw_pci_target != pci_target { + let mut hw_atu = atu; + hw_atu.set_pci_target(hw_pci_target); + hw_atu.set_atu_type(atu_type); + hw_atu.set_cpu_base(config_base); + hw_atu.set_cpu_limit(cpu_limit); + AtuUnroll::dw_pcie_prog_outbound_atu_unroll(&dbi_backend, &hw_atu)?; + } } let offset = (mmio.address & 0xfff) as PciConfigAddress; let zone = this_zone(); let mut is_dev_belong_to_zone = false; - let base = mmio.address as PciConfigAddress - offset + atu.pci_target(); - let dev: Option = { let mut guard = zone.write(); let vbus = guard.vpci_bus_mut(); - if let Some(dev) = vbus.get_device_by_base(base) { + if let Some(dev) = vbus.get_device_by_base(pci_target) { is_dev_belong_to_zone = true; Some(dev) } else { @@ -1956,13 +1985,29 @@ pub fn mmio_dwc_cfg_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { // This avoids holding multiple locks simultaneously let dev_clone = { let global_pcie_list = GLOBAL_PCIE_LIST.lock(); + let domain = platform::ROOT_PCI_CONFIG + .iter() + .find(|cfg| cfg.ecam_base as usize == ecam_base) + .map(|cfg| cfg.domain) + .unwrap_or(0); + let target_bdf = Bdf::new( + domain, + ((pci_target >> 24) & 0xff) as u8, + ((pci_target >> 19) & 0x1f) as u8, + ((pci_target >> 16) & 0x7) as u8, + ); global_pcie_list - .values() - .find(|dev| { - let dev_guard = dev.read(); - dev_guard.get_base() == base - }) + .get(&target_bdf) .cloned() + .or_else(|| { + global_pcie_list + .values() + .find(|dev| { + let dev_guard = dev.read(); + dev_guard.get_base() == pci_target + }) + .cloned() + }) }; dev_clone } @@ -2058,17 +2103,32 @@ pub fn mmio_vpci_handler_dbi(mmio: &mut MMIOAccess, _base: usize) -> HvResult { let root_config = platform::platform_root_zone_config(); let num_pci_bus = root_config.num_pci_bus as usize; - crate::pci::pci_config::hvisor_pci_init(&root_config.pci_config[..num_pci_bus])?; - let zone = crate::zone::root_zone(); let mut inner = zone.write(); - inner.virtual_pci_mmio_init_delay(&root_config.pci_config, num_pci_bus); + inner.virtual_pci_mmio_init_delay( + &root_config.pci_config, + num_pci_bus, + domain_id, + ); + drop(inner); + + if let Some(domain_cfg) = root_config.pci_config[..num_pci_bus] + .iter() + .find(|cfg| cfg.domain == domain_id && cfg.ecam_base != 0) + { + crate::pci::pci_config::hvisor_pci_init(core::slice::from_ref(domain_cfg))?; + } else { + warn!("No PCI config found for domain {}", domain_id); + } + + let mut inner = zone.write(); inner.guest_pci_init_delay( 0, &root_config.alloc_pci_devs, root_config.num_pci_devs, &root_config.pci_config, num_pci_bus, + domain_id, )?; #[cfg(dwc_msi)] diff --git a/src/pci/pci_struct.rs b/src/pci/pci_struct.rs index a84327f27..3a98f95ae 100644 --- a/src/pci/pci_struct.rs +++ b/src/pci/pci_struct.rs @@ -55,6 +55,7 @@ pub struct ConfigValue { class_and_revision_id: (BaseClass, SubClass, Interface, DeviceRevision), bar_value: [u32; 6], rom_value: u32, + bridge_bus_reg: u32, } impl Default for ConfigValue { @@ -64,6 +65,7 @@ impl Default for ConfigValue { class_and_revision_id: (0xFFu8, 0u8, 0u8, 0u8), bar_value: [0; 6], rom_value: 0, + bridge_bus_reg: 0, } } } @@ -78,6 +80,7 @@ impl ConfigValue { class_and_revision_id, bar_value: [0; 6], rom_value: 0, + bridge_bus_reg: 0, } } @@ -143,6 +146,14 @@ impl ConfigValue { pub fn set_rom_value(&mut self, value: u32) { self.rom_value = value; } + + pub fn get_bridge_bus_reg(&self) -> u32 { + self.bridge_bus_reg + } + + pub fn set_bridge_bus_reg(&mut self, value: u32) { + self.bridge_bus_reg = value; + } } const MAX_DEVICE: u8 = 31; @@ -361,6 +372,7 @@ impl VirtualPciAccessBits { pub fn bridge() -> Self { let mut bits = BitArray::ZERO; bits[0x10..0x18].fill(true); // BARs + bits[0x18..0x1c].fill(true); // Primary/Secondary/Subordinate bus + latency bits[0x38..0x3c].fill(true); // ROM bits[0x34..0x38].fill(true); // Capability Pointer bits[0x40..0x100].fill(true); // Capability region (caps start at 0x40) @@ -1394,8 +1406,20 @@ impl VirtualPciConfigSpace { self.config_type } - pub fn set_vbdf(&mut self, vbdf: Bdf) { + pub fn set_vbdf(&mut self, vbdf: Bdf, domain_bus_range_end: u8) { self.vbdf = vbdf; + if self.config_type == HeaderType::PciBridge { + self.init_bridge_bus_reg(domain_bus_range_end); + } + } + + fn init_bridge_bus_reg(&mut self, domain_bus_range_end: u8) { + let primary = self.vbdf.bus(); + let secondary = primary.saturating_add(1); + let subordinate = domain_bus_range_end; + self.config_value.set_bridge_bus_reg( + ((subordinate as u32) << 16) | ((secondary as u32) << 8) | (primary as u32), + ); } pub fn get_base(&self) -> PciConfigAddress { @@ -1734,6 +1758,7 @@ impl PciIterator { let _ = node.build_sriov_info(); // Build MSI/MSIX info once during device discovery node.build_msi_info(); + node.set_vbdf(bdf, self.bus_range.end as u8); Some(node) } @@ -1762,6 +1787,7 @@ impl PciIterator { let _ = node.build_sriov_info(); // Build MSI/MSIX info once during device discovery node.build_msi_info(); + node.set_vbdf(bdf, self.bus_range.end as u8); Some(node) } diff --git a/src/pci/pci_test.rs b/src/pci/pci_test.rs index c6d7b22d4..629697181 100644 --- a/src/pci/pci_test.rs +++ b/src/pci/pci_test.rs @@ -78,7 +78,7 @@ pub fn pcie_guest_init() { // let _ = dev.write_hw(0x24, 4, 0xffffffff); // let value2 = dev.read_hw(0x24, 4).unwrap(); // info!("{:#?} bar64 {:x}, {:x}", bdf, (value1 as u64), ((value2 as u64) << 32u64)); - dev.set_vbdf(vbdf); + dev.set_vbdf(vbdf, 0xff); vbus.insert(vbdf, dev); } else { warn!("can not find dev"); @@ -87,7 +87,7 @@ pub fn pcie_guest_init() { let vbdf = Bdf::from_str("0000:00:02.0").unwrap(); let bdf = Bdf::from_str("0000:00:02.0").unwrap(); if let Some(mut dev) = guard.remove(&bdf) { - dev.set_vbdf(vbdf); + dev.set_vbdf(vbdf, 0xff); vbus.insert(vbdf, dev); } else { warn!("can not find dev"); @@ -96,7 +96,7 @@ pub fn pcie_guest_init() { let vbdf = Bdf::from_str("0000:00:03.0").unwrap(); let bdf = Bdf::from_str("0000:00:03.0").unwrap(); if let Some(mut dev) = guard.remove(&bdf) { - dev.set_vbdf(vbdf); + dev.set_vbdf(vbdf, 0xff); vbus.insert(vbdf, dev); } else { warn!("can not find dev"); diff --git a/src/zone.rs b/src/zone.rs index 9aefa419a..ed9137113 100644 --- a/src/zone.rs +++ b/src/zone.rs @@ -326,11 +326,12 @@ impl ZoneInner { num_pci_devs: u64, pci_config: &[HvPciConfig], _num_pci_config: usize, + domain_id: u8, ) -> HvResult { let guard = GLOBAL_PCIE_LIST.lock(); for target_pci_config in pci_config { // Skip empty config - if target_pci_config.ecam_base == 0 { + if target_pci_config.ecam_base == 0 || target_pci_config.domain != domain_id { continue; } @@ -455,7 +456,7 @@ impl ZoneInner { }) { let mut vdev = dev.read().config_space.clone(); - vdev.set_vbdf(vbdf); + vdev.set_vbdf(vbdf, target_pci_config.bus_range_end as u8); let msi_count = vdev.get_msi_count(); domain_msi_count += msi_count; self.vpci_bus_mut().insert(vbdf, vdev); @@ -475,7 +476,7 @@ impl ZoneInner { } else { dev.set_zone_id(Some(_zone_id as u32)); let mut vdev_inner = dev.read().config_space.clone(); - vdev_inner.set_vbdf(vbdf); + vdev_inner.set_vbdf(vbdf, target_pci_config.bus_range_end as u8); let msi_count = vdev_inner.get_msi_count(); domain_msi_count += msi_count; self.vpci_bus_mut().insert(vbdf, vdev_inner); @@ -608,12 +609,13 @@ impl ZoneInner { &mut self, pci_rootcomplex_config: &[HvPciConfig; CONFIG_PCI_BUS_MAXNUM], _num_pci_config: usize, + domain_id: u8, ) { #[cfg(loongarch64_pcie)] let mut emergency_map_regions: alloc::vec::Vec<(usize, usize)> = alloc::vec::Vec::new(); for rootcomplex_config in pci_rootcomplex_config { - if rootcomplex_config.ecam_base == 0 { + if rootcomplex_config.ecam_base == 0 || rootcomplex_config.domain != domain_id { continue; } #[cfg(ecam_pcie)] From 9185d7456033bd954919891d8e8696ffb46f460c Mon Sep 17 00:00:00 2001 From: ray Date: Mon, 13 Jul 2026 19:19:57 +0800 Subject: [PATCH 30/53] add network test for rk3568 --- .gitignore | 1 + jenkins/board_scp.sh | 104 ++++++++++++++++++++++++++++++++++++++++ jenkins/check_serial.sh | 10 ++-- jenkins/ci.yaml | 6 +++ jenkins/ci_runner.py | 102 +++++++++++++++++++++++++++++++++++++-- src/pci/pci_handler.rs | 33 ++++++------- 6 files changed, 229 insertions(+), 27 deletions(-) create mode 100755 jenkins/board_scp.sh diff --git a/.gitignore b/.gitignore index c5d0b3981..332a469d3 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ src/platform/__board.rs # systemtest rootfs1.zip* jenkins/__pycache__ +jenkins/logs/ /logs/ jenkins-cli.jar tools/kconfig/.venv/ diff --git a/jenkins/board_scp.sh b/jenkins/board_scp.sh new file mode 100755 index 000000000..8d7743861 --- /dev/null +++ b/jenkins/board_scp.sh @@ -0,0 +1,104 @@ +#!/bin/sh +# Stage zone1 test artifacts on the CI host for board pull via scp. +# Zone0 boot Image stays on TFTP; only zone1 Image/rootfs are staged here. + +set -eux + +ARCH=${ARCH:?ARCH is required} +BOARD=${BOARD:?BOARD is required} +WORKSPACE_ROOT=${WORKSPACE_ROOT:-$(pwd)} +HVISOR_TOOL_PATH=${HVISOR_TOOL_PATH:-${WORKSPACE_ROOT}/hvisor-tool} +MODE=${MODE:-release} +STAGING_DIR=${STAGING_DIR:-/home/light/tftp/ci_deploy} + +case "${HVISOR_TOOL_PATH}" in + /*) ;; + *) HVISOR_TOOL_PATH="${WORKSPACE_ROOT}/${HVISOR_TOOL_PATH}" ;; +esac + +PLATFORM_DIR="${WORKSPACE_ROOT}/platform/${ARCH}/${BOARD}" +CONFIGS_DIR="${PLATFORM_DIR}/configs" +IMAGE_DIR="${PLATFORM_DIR}/image" +SCRIPTS_DIR="${PLATFORM_DIR}/scripts" +ZONE1_BOOT_SCRIPT="${SCRIPTS_DIR}/boot_zone1.sh" +CHECK_SERIAL_SCRIPT="${WORKSPACE_ROOT}/jenkins/check_serial.sh" + +case "${ARCH}" in + x86_64) RUSTC_TARGET="x86_64-unknown-none" ;; + aarch64) RUSTC_TARGET="aarch64-unknown-none" ;; + riscv64) RUSTC_TARGET="riscv64gc-unknown-none-elf" ;; + *) + echo "error: unsupported ARCH: ${ARCH}" + exit 1 + ;; +esac + +BUILD_PATH="${WORKSPACE_ROOT}/target/${RUSTC_TARGET}/${MODE}" +HVISOR_BIN="${BUILD_PATH}/hvisor.bin" + +if [ -n "${ZONE1_DTB:-}" ]; then + : +elif [ -f "${IMAGE_DIR}/dts/rk3568_limit_zone1.dtb" ]; then + ZONE1_DTB="${IMAGE_DIR}/dts/rk3568_limit_zone1.dtb" +elif [ -f "${IMAGE_DIR}/dts/zone1-linux.dtb" ]; then + ZONE1_DTB="${IMAGE_DIR}/dts/zone1-linux.dtb" +else + ZONE1_DTB="${IMAGE_DIR}/dts/zone1-linux.dtb" +fi + +echo "ARCH: ${ARCH}" +echo "BOARD: ${BOARD}" +echo "HVISOR_TOOL_PATH: ${HVISOR_TOOL_PATH}" +echo "STAGING_DIR: ${STAGING_DIR}" + +if [ ! -f "${HVISOR_TOOL_PATH}/output/hvisor" ]; then + echo "error: hvisor tool binary not found: ${HVISOR_TOOL_PATH}/output/hvisor" + exit 1 +fi +if [ ! -f "${HVISOR_TOOL_PATH}/output/hvisor.ko" ]; then + echo "error: hvisor.ko not found: ${HVISOR_TOOL_PATH}/output/hvisor.ko" + exit 1 +fi +if [ ! -f "${HVISOR_BIN}" ]; then + echo "error: hvisor.bin not found: ${HVISOR_BIN}" + exit 1 +fi +if [ ! -f "${ZONE1_BOOT_SCRIPT}" ]; then + echo "error: boot script not found: ${ZONE1_BOOT_SCRIPT}" + exit 1 +fi + +if [ ! -f "${ZONE1_DTB}" ] && [ -d "${IMAGE_DIR}/dts" ]; then + echo "zone1 dtb is missing, building from ${IMAGE_DIR}/dts" + make -C "${IMAGE_DIR}/dts" all || true +fi + +rm -rf "${STAGING_DIR}" +mkdir -p "${STAGING_DIR}" + +cp "${HVISOR_TOOL_PATH}/output/hvisor" "${HVISOR_TOOL_PATH}/output/hvisor.ko" "${STAGING_DIR}/" +cp "${HVISOR_BIN}" "${STAGING_DIR}/" +cp "${CONFIGS_DIR}/"* "${STAGING_DIR}/" +cp "${ZONE1_BOOT_SCRIPT}" "${STAGING_DIR}/" + +if [ -f "${ZONE1_DTB}" ]; then + cp "${ZONE1_DTB}" "${STAGING_DIR}/" +else + echo "warning: zone1 dtb unavailable, skip copying ${ZONE1_DTB}" +fi + +if [ -f "${CHECK_SERIAL_SCRIPT}" ]; then + cp "${CHECK_SERIAL_SCRIPT}" "${STAGING_DIR}/" +fi + +ZONE1_ASSETS_DIR=${ZONE1_ASSETS_DIR:-/home/light/tftp/zone1_assets} +if [ -f "${ZONE1_ASSETS_DIR}/Image" ]; then + cp "${ZONE1_ASSETS_DIR}/Image" "${STAGING_DIR}/" +fi +if [ -f "${ZONE1_ASSETS_DIR}/rootfs2.ext4" ]; then + cp "${ZONE1_ASSETS_DIR}/rootfs2.ext4" "${STAGING_DIR}/" +fi + +chmod -R a+rX "${STAGING_DIR}" +echo "board staging completed: ${STAGING_DIR}" +ls -la "${STAGING_DIR}" diff --git a/jenkins/check_serial.sh b/jenkins/check_serial.sh index 529abd74a..3cb9911fa 100755 --- a/jenkins/check_serial.sh +++ b/jenkins/check_serial.sh @@ -20,22 +20,22 @@ read_pts() { timeout "${1:-2}" cat "$pts_dev" 2>/dev/null >> "$log_file" || true } -has_shell_prompt() { +has_console_ready() { tr -d '\r' < "$log_file" | sed 's/\x1b\[[0-9;?]*[ -\/]*[@-~]//g' \ - | grep -qE 'root@[^[:space:]]*[#$][[:space:]]*$|^[[:space:]]*#[[:space:]]*$' + | grep -qE 'root@[^[:space:]]*[#$][[:space:]]*$|^[[:space:]]*#[[:space:]]*$|login:[[:space:]]*$' } deadline=$(( $(date +%s) + prompt_timeout )) while [ "$(date +%s)" -lt "$deadline" ]; do read_pts 2 - if has_shell_prompt; then + if has_console_ready; then break fi sleep 0.2 done -if ! has_shell_prompt; then - echo "check_serial: timed out after ${prompt_timeout}s waiting for shell prompt on $pts_dev" \ +if ! has_console_ready; then + echo "check_serial: timed out after ${prompt_timeout}s waiting for console prompt on $pts_dev" \ >> "$log_file" exit 1 fi diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index 0124e3220..356d4d835 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -29,6 +29,11 @@ bids: baudrate: 1500000 uboot_ready_pattern: '=>' uboot_cmd: pci enum;setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.240; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:hvisor.bin; tftp ${fdt_addr} ${serverip}:rk3568_limit_zone0.dtb; tftp ${zone0_kernel_addr} ${serverip}:Image; bootm ${loadaddr} - ${fdt_addr}; + network: + host_ip: 192.168.1.181 + host_user: light + staging_dir: /home/light/tftp/ci_deploy + zone1_dtb: platform/aarch64/rk3568/image/dts/rk3568_limit_zone1.dtb lspci: expected_bdfs: - "0001:10:00.0" @@ -38,6 +43,7 @@ bids: min_count: 4 cases: - zone0_start + - network - lspci - zone1_start diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 8a6cb3f2d..7d36769be 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -17,7 +17,7 @@ CaseFunc = Callable[[dict[str, Any], Terminal | None], int] -# Wait for an interactive shell prompt, not login: (getty shows that before MOTD/shell). +# Wait for zone1 inner console: shell prompt (#/$) or login:. ZONE0_READY_PATTERN = r"root@[^\r\n]*[#$]\s|(?:\r?\n)#\s" ZONE1_INNER_PROMPT_TIMEOUT = 60.0 ZONE1_INNER_LOG_FETCH_TIMEOUT = 30.0 @@ -156,6 +156,9 @@ def board_wait_uboot_prompt(term: Terminal, pattern: str, timeout: float) -> Non return board_wake_console(term) raise TerminalTimeoutError(f"timed out waiting for U-Boot prompt (pattern={pattern!r})") + + +def save_inner_serial_log(cfg: dict[str, Any], content: str) -> None: if not content: return path = logs_dir(cfg) / "zone1_inner_serial.log" @@ -202,7 +205,7 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: if uboot_cmd: if not uboot_ready: uboot_ready = r"=>" - board_wait_uboot_prompt(board_term, uboot_ready, timeout=120.0) + board_wait_uboot_prompt(board_term, uboot_ready, timeout=10.0) time.sleep(0.3) board_term.send(uboot_cmd) if not board_term.wait_pattern(ZONE0_READY_PATTERN, timeout=180.0): @@ -269,6 +272,80 @@ def lspci(cfg: dict[str, Any], term: Terminal | None) -> int: return 0 +def ping_success(output: str) -> bool: + if re.search(r"\b0% (?:packet )?loss\b", output): + return True + match = re.search(r"(\d+) packets? received", output) + return bool(match and int(match.group(1)) > 0) + + +def network(cfg: dict[str, Any], term: Terminal | None) -> int: + print("————————————————\ncase: network\n————————————————\n", flush=True) + if cfg["mode"] != "board": + print("[network] skipped (not board mode)", flush=True) + return 0 + if term is None: + raise SystemExit("terminal backend is required (run zone0_start first)") + + host_ip = cfg["network_host_ip"] + ping_count = cfg["network_ping_count"] + _, output = term.run( + "network_ping", + f"ping -c {ping_count} -W 5 {host_ip}", + timeout=30.0, + ) + save_lspci_artifacts(cfg, "network_ping.log", f"=== ping {host_ip} ===\n{output}\n") + + if not ping_success(output): + raise TerminalCommandError(f"ping {host_ip} failed") + + print(f"[network] ping {host_ip} ok, staging files on host", flush=True) + script = cfg["workspace"] / "jenkins" / "board_scp.sh" + if not script.is_file(): + raise SystemExit(f"board scp script not found: {script}") + + env = os.environ.copy() + env["ARCH"] = cfg["arch"] + env["BOARD"] = cfg["board"] + if cfg["kdir"]: + env["KDIR"] = cfg["kdir"] + env["WORKSPACE_ROOT"] = str(cfg["workspace"]) + env["HVISOR_TOOL_PATH"] = cfg["hvisor_tool_path"] + env["STAGING_DIR"] = cfg["network_staging_dir"] + if cfg.get("zone1_dtb"): + zone1_dtb = Path(cfg["zone1_dtb"]) + if not zone1_dtb.is_absolute(): + zone1_dtb = cfg["workspace"] / zone1_dtb + env["ZONE1_DTB"] = str(zone1_dtb.resolve()) + + subprocess.run(["bash", str(script)], check=True, cwd=cfg["workspace"], env=env) + + # Let host staging finish and drain any buffered serial output before scp. + time.sleep(3.0) + + host_user = cfg["network_host_user"] + staging_dir = cfg["network_staging_dir"] + term.run( + "network_scp_env", + f"export CI_H={host_ip} CI_U={host_user} CI_D={staging_dir}", + timeout=30.0, + ) + # Keep the scp line short: serial consoles truncate long commands. + scp_cmd = "scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $CI_U@$CI_H:$CI_D/* /root/" + print(f"[network] pulling staged files from {host_user}@{host_ip}:{staging_dir}", flush=True) + pull_rc, _ = term.run("network_scp_pull", scp_cmd, timeout=300.0) + if pull_rc != 0: + raise TerminalCommandError(f"board scp pull failed with rc={pull_rc}") + + term.run( + "network_chmod", + "chmod +x /root/boot_zone1.sh /root/check_serial.sh 2>/dev/null || true", + timeout=15.0, + ) + print("network test and file deploy passed", flush=True) + return 0 + + def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: print("————————————————\ncase: zone1_start\n————————————————\n", flush=True) if term is None: @@ -300,7 +377,7 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: raise TerminalCommandError(f"command failed with rc={boot_rc}: ./boot_zone1.sh") if check_rc != 0: raise TerminalCommandError( - f"command failed with rc={check_rc}: check_serial.sh (no shell prompt)" + f"command failed with rc={check_rc}: check_serial.sh (no console prompt)" ) print("zone1_started successfully", flush=True) return 0 @@ -308,6 +385,7 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: CASE_HANDLERS: dict[str, CaseFunc] = { "zone0_start": zone0_start, + "network": network, "lspci": lspci, "zone1_start": zone1_start, } @@ -336,6 +414,15 @@ def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: raise SystemExit(f"no test cases configured for bid '{args.bid}'") cell_root = Path.cwd() + build_args = bid_entry.get("build_args") or {} + network_cfg = tests.get("network") or {} + if not isinstance(network_cfg, dict): + network_cfg = {} + hvisor_tool_path = os.environ.get("HVISOR_TOOL_PATH", "").strip() + if not hvisor_tool_path: + hvisor_tool_path = str((cell_root / "hvisor-tool").resolve()) + elif not Path(hvisor_tool_path).is_absolute(): + hvisor_tool_path = str((cell_root / hvisor_tool_path).resolve()) return { "bid": args.bid, "arch": arch, @@ -343,6 +430,8 @@ def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: "mode": mode, "cases": cases, "workspace": cell_root, + "kdir": str(build_args.get("KDIR", "")).strip(), + "hvisor_tool_path": hvisor_tool_path, "socket_path": str((cell_root / ".qemu" / "qemu.sock").resolve()), "serial_port": str(tests.get("serial", "/dev/null")), "power_serial": str(tests.get("power_serial", "")).strip(), @@ -351,6 +440,13 @@ def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: "uboot_ready_pattern": str(tests.get("uboot_ready_pattern", "")).strip(), "tftp_dir": str(tests.get("tftp_dir", "/home/light/tftp")).strip(), "lspci": tests.get("lspci") or {}, + "network_host_ip": str(network_cfg.get("host_ip", "192.168.1.181")).strip(), + "network_host_user": str(network_cfg.get("host_user", "light")).strip(), + "network_staging_dir": str( + network_cfg.get("staging_dir", "/home/light/tftp/ci_deploy") + ).strip(), + "network_ping_count": int(network_cfg.get("ping_count", 3)), + "zone1_dtb": str(network_cfg.get("zone1_dtb", "")).strip(), } diff --git a/src/pci/pci_handler.rs b/src/pci/pci_handler.rs index 445364086..6787c0877 100644 --- a/src/pci/pci_handler.rs +++ b/src/pci/pci_handler.rs @@ -1623,7 +1623,7 @@ fn handle_pci_bridge_access( Ok(None) } else { let reg = dev.with_config_value(|cv| cv.get_bridge_bus_reg()); - Ok(Some((((reg >> ((reg_offset - 0x18) * 8)) & 0xff)) as usize)) + Ok(Some(((reg >> ((reg_offset - 0x18) * 8)) & 0xff) as usize)) } } BridgeField::BusNumbers => { @@ -1633,7 +1633,9 @@ fn handle_pci_bridge_access( } Ok(None) } else { - Ok(Some(dev.with_config_value(|cv| cv.get_bridge_bus_reg()) as usize)) + Ok(Some( + dev.with_config_value(|cv| cv.get_bridge_bus_reg()) as usize + )) } } _ => Ok(None), @@ -1996,18 +1998,15 @@ pub fn mmio_dwc_cfg_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { ((pci_target >> 19) & 0x1f) as u8, ((pci_target >> 16) & 0x7) as u8, ); - global_pcie_list - .get(&target_bdf) - .cloned() - .or_else(|| { - global_pcie_list - .values() - .find(|dev| { - let dev_guard = dev.read(); - dev_guard.get_base() == pci_target - }) - .cloned() - }) + global_pcie_list.get(&target_bdf).cloned().or_else(|| { + global_pcie_list + .values() + .find(|dev| { + let dev_guard = dev.read(); + dev_guard.get_base() == pci_target + }) + .cloned() + }) }; dev_clone } @@ -2105,11 +2104,7 @@ pub fn mmio_vpci_handler_dbi(mmio: &mut MMIOAccess, _base: usize) -> HvResult { let zone = crate::zone::root_zone(); let mut inner = zone.write(); - inner.virtual_pci_mmio_init_delay( - &root_config.pci_config, - num_pci_bus, - domain_id, - ); + inner.virtual_pci_mmio_init_delay(&root_config.pci_config, num_pci_bus, domain_id); drop(inner); if let Some(domain_cfg) = root_config.pci_config[..num_pci_bus] From 8a683222a2eaec31a6dee26d6bc03ab7d4389161 Mon Sep 17 00:00:00 2001 From: dallas Date: Mon, 13 Jul 2026 19:19:57 +0800 Subject: [PATCH 31/53] add network test for rk3568 --- .gitignore | 1 + jenkins/board_scp.sh | 104 ++++++++++++++++++++++++++++++++++++++++ jenkins/check_serial.sh | 10 ++-- jenkins/ci.yaml | 6 +++ jenkins/ci_runner.py | 102 +++++++++++++++++++++++++++++++++++++-- src/pci/pci_handler.rs | 33 ++++++------- 6 files changed, 229 insertions(+), 27 deletions(-) create mode 100755 jenkins/board_scp.sh diff --git a/.gitignore b/.gitignore index c5d0b3981..332a469d3 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ src/platform/__board.rs # systemtest rootfs1.zip* jenkins/__pycache__ +jenkins/logs/ /logs/ jenkins-cli.jar tools/kconfig/.venv/ diff --git a/jenkins/board_scp.sh b/jenkins/board_scp.sh new file mode 100755 index 000000000..8d7743861 --- /dev/null +++ b/jenkins/board_scp.sh @@ -0,0 +1,104 @@ +#!/bin/sh +# Stage zone1 test artifacts on the CI host for board pull via scp. +# Zone0 boot Image stays on TFTP; only zone1 Image/rootfs are staged here. + +set -eux + +ARCH=${ARCH:?ARCH is required} +BOARD=${BOARD:?BOARD is required} +WORKSPACE_ROOT=${WORKSPACE_ROOT:-$(pwd)} +HVISOR_TOOL_PATH=${HVISOR_TOOL_PATH:-${WORKSPACE_ROOT}/hvisor-tool} +MODE=${MODE:-release} +STAGING_DIR=${STAGING_DIR:-/home/light/tftp/ci_deploy} + +case "${HVISOR_TOOL_PATH}" in + /*) ;; + *) HVISOR_TOOL_PATH="${WORKSPACE_ROOT}/${HVISOR_TOOL_PATH}" ;; +esac + +PLATFORM_DIR="${WORKSPACE_ROOT}/platform/${ARCH}/${BOARD}" +CONFIGS_DIR="${PLATFORM_DIR}/configs" +IMAGE_DIR="${PLATFORM_DIR}/image" +SCRIPTS_DIR="${PLATFORM_DIR}/scripts" +ZONE1_BOOT_SCRIPT="${SCRIPTS_DIR}/boot_zone1.sh" +CHECK_SERIAL_SCRIPT="${WORKSPACE_ROOT}/jenkins/check_serial.sh" + +case "${ARCH}" in + x86_64) RUSTC_TARGET="x86_64-unknown-none" ;; + aarch64) RUSTC_TARGET="aarch64-unknown-none" ;; + riscv64) RUSTC_TARGET="riscv64gc-unknown-none-elf" ;; + *) + echo "error: unsupported ARCH: ${ARCH}" + exit 1 + ;; +esac + +BUILD_PATH="${WORKSPACE_ROOT}/target/${RUSTC_TARGET}/${MODE}" +HVISOR_BIN="${BUILD_PATH}/hvisor.bin" + +if [ -n "${ZONE1_DTB:-}" ]; then + : +elif [ -f "${IMAGE_DIR}/dts/rk3568_limit_zone1.dtb" ]; then + ZONE1_DTB="${IMAGE_DIR}/dts/rk3568_limit_zone1.dtb" +elif [ -f "${IMAGE_DIR}/dts/zone1-linux.dtb" ]; then + ZONE1_DTB="${IMAGE_DIR}/dts/zone1-linux.dtb" +else + ZONE1_DTB="${IMAGE_DIR}/dts/zone1-linux.dtb" +fi + +echo "ARCH: ${ARCH}" +echo "BOARD: ${BOARD}" +echo "HVISOR_TOOL_PATH: ${HVISOR_TOOL_PATH}" +echo "STAGING_DIR: ${STAGING_DIR}" + +if [ ! -f "${HVISOR_TOOL_PATH}/output/hvisor" ]; then + echo "error: hvisor tool binary not found: ${HVISOR_TOOL_PATH}/output/hvisor" + exit 1 +fi +if [ ! -f "${HVISOR_TOOL_PATH}/output/hvisor.ko" ]; then + echo "error: hvisor.ko not found: ${HVISOR_TOOL_PATH}/output/hvisor.ko" + exit 1 +fi +if [ ! -f "${HVISOR_BIN}" ]; then + echo "error: hvisor.bin not found: ${HVISOR_BIN}" + exit 1 +fi +if [ ! -f "${ZONE1_BOOT_SCRIPT}" ]; then + echo "error: boot script not found: ${ZONE1_BOOT_SCRIPT}" + exit 1 +fi + +if [ ! -f "${ZONE1_DTB}" ] && [ -d "${IMAGE_DIR}/dts" ]; then + echo "zone1 dtb is missing, building from ${IMAGE_DIR}/dts" + make -C "${IMAGE_DIR}/dts" all || true +fi + +rm -rf "${STAGING_DIR}" +mkdir -p "${STAGING_DIR}" + +cp "${HVISOR_TOOL_PATH}/output/hvisor" "${HVISOR_TOOL_PATH}/output/hvisor.ko" "${STAGING_DIR}/" +cp "${HVISOR_BIN}" "${STAGING_DIR}/" +cp "${CONFIGS_DIR}/"* "${STAGING_DIR}/" +cp "${ZONE1_BOOT_SCRIPT}" "${STAGING_DIR}/" + +if [ -f "${ZONE1_DTB}" ]; then + cp "${ZONE1_DTB}" "${STAGING_DIR}/" +else + echo "warning: zone1 dtb unavailable, skip copying ${ZONE1_DTB}" +fi + +if [ -f "${CHECK_SERIAL_SCRIPT}" ]; then + cp "${CHECK_SERIAL_SCRIPT}" "${STAGING_DIR}/" +fi + +ZONE1_ASSETS_DIR=${ZONE1_ASSETS_DIR:-/home/light/tftp/zone1_assets} +if [ -f "${ZONE1_ASSETS_DIR}/Image" ]; then + cp "${ZONE1_ASSETS_DIR}/Image" "${STAGING_DIR}/" +fi +if [ -f "${ZONE1_ASSETS_DIR}/rootfs2.ext4" ]; then + cp "${ZONE1_ASSETS_DIR}/rootfs2.ext4" "${STAGING_DIR}/" +fi + +chmod -R a+rX "${STAGING_DIR}" +echo "board staging completed: ${STAGING_DIR}" +ls -la "${STAGING_DIR}" diff --git a/jenkins/check_serial.sh b/jenkins/check_serial.sh index 529abd74a..3cb9911fa 100755 --- a/jenkins/check_serial.sh +++ b/jenkins/check_serial.sh @@ -20,22 +20,22 @@ read_pts() { timeout "${1:-2}" cat "$pts_dev" 2>/dev/null >> "$log_file" || true } -has_shell_prompt() { +has_console_ready() { tr -d '\r' < "$log_file" | sed 's/\x1b\[[0-9;?]*[ -\/]*[@-~]//g' \ - | grep -qE 'root@[^[:space:]]*[#$][[:space:]]*$|^[[:space:]]*#[[:space:]]*$' + | grep -qE 'root@[^[:space:]]*[#$][[:space:]]*$|^[[:space:]]*#[[:space:]]*$|login:[[:space:]]*$' } deadline=$(( $(date +%s) + prompt_timeout )) while [ "$(date +%s)" -lt "$deadline" ]; do read_pts 2 - if has_shell_prompt; then + if has_console_ready; then break fi sleep 0.2 done -if ! has_shell_prompt; then - echo "check_serial: timed out after ${prompt_timeout}s waiting for shell prompt on $pts_dev" \ +if ! has_console_ready; then + echo "check_serial: timed out after ${prompt_timeout}s waiting for console prompt on $pts_dev" \ >> "$log_file" exit 1 fi diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index 0124e3220..356d4d835 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -29,6 +29,11 @@ bids: baudrate: 1500000 uboot_ready_pattern: '=>' uboot_cmd: pci enum;setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.240; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:hvisor.bin; tftp ${fdt_addr} ${serverip}:rk3568_limit_zone0.dtb; tftp ${zone0_kernel_addr} ${serverip}:Image; bootm ${loadaddr} - ${fdt_addr}; + network: + host_ip: 192.168.1.181 + host_user: light + staging_dir: /home/light/tftp/ci_deploy + zone1_dtb: platform/aarch64/rk3568/image/dts/rk3568_limit_zone1.dtb lspci: expected_bdfs: - "0001:10:00.0" @@ -38,6 +43,7 @@ bids: min_count: 4 cases: - zone0_start + - network - lspci - zone1_start diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 8a6cb3f2d..7d36769be 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -17,7 +17,7 @@ CaseFunc = Callable[[dict[str, Any], Terminal | None], int] -# Wait for an interactive shell prompt, not login: (getty shows that before MOTD/shell). +# Wait for zone1 inner console: shell prompt (#/$) or login:. ZONE0_READY_PATTERN = r"root@[^\r\n]*[#$]\s|(?:\r?\n)#\s" ZONE1_INNER_PROMPT_TIMEOUT = 60.0 ZONE1_INNER_LOG_FETCH_TIMEOUT = 30.0 @@ -156,6 +156,9 @@ def board_wait_uboot_prompt(term: Terminal, pattern: str, timeout: float) -> Non return board_wake_console(term) raise TerminalTimeoutError(f"timed out waiting for U-Boot prompt (pattern={pattern!r})") + + +def save_inner_serial_log(cfg: dict[str, Any], content: str) -> None: if not content: return path = logs_dir(cfg) / "zone1_inner_serial.log" @@ -202,7 +205,7 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: if uboot_cmd: if not uboot_ready: uboot_ready = r"=>" - board_wait_uboot_prompt(board_term, uboot_ready, timeout=120.0) + board_wait_uboot_prompt(board_term, uboot_ready, timeout=10.0) time.sleep(0.3) board_term.send(uboot_cmd) if not board_term.wait_pattern(ZONE0_READY_PATTERN, timeout=180.0): @@ -269,6 +272,80 @@ def lspci(cfg: dict[str, Any], term: Terminal | None) -> int: return 0 +def ping_success(output: str) -> bool: + if re.search(r"\b0% (?:packet )?loss\b", output): + return True + match = re.search(r"(\d+) packets? received", output) + return bool(match and int(match.group(1)) > 0) + + +def network(cfg: dict[str, Any], term: Terminal | None) -> int: + print("————————————————\ncase: network\n————————————————\n", flush=True) + if cfg["mode"] != "board": + print("[network] skipped (not board mode)", flush=True) + return 0 + if term is None: + raise SystemExit("terminal backend is required (run zone0_start first)") + + host_ip = cfg["network_host_ip"] + ping_count = cfg["network_ping_count"] + _, output = term.run( + "network_ping", + f"ping -c {ping_count} -W 5 {host_ip}", + timeout=30.0, + ) + save_lspci_artifacts(cfg, "network_ping.log", f"=== ping {host_ip} ===\n{output}\n") + + if not ping_success(output): + raise TerminalCommandError(f"ping {host_ip} failed") + + print(f"[network] ping {host_ip} ok, staging files on host", flush=True) + script = cfg["workspace"] / "jenkins" / "board_scp.sh" + if not script.is_file(): + raise SystemExit(f"board scp script not found: {script}") + + env = os.environ.copy() + env["ARCH"] = cfg["arch"] + env["BOARD"] = cfg["board"] + if cfg["kdir"]: + env["KDIR"] = cfg["kdir"] + env["WORKSPACE_ROOT"] = str(cfg["workspace"]) + env["HVISOR_TOOL_PATH"] = cfg["hvisor_tool_path"] + env["STAGING_DIR"] = cfg["network_staging_dir"] + if cfg.get("zone1_dtb"): + zone1_dtb = Path(cfg["zone1_dtb"]) + if not zone1_dtb.is_absolute(): + zone1_dtb = cfg["workspace"] / zone1_dtb + env["ZONE1_DTB"] = str(zone1_dtb.resolve()) + + subprocess.run(["bash", str(script)], check=True, cwd=cfg["workspace"], env=env) + + # Let host staging finish and drain any buffered serial output before scp. + time.sleep(3.0) + + host_user = cfg["network_host_user"] + staging_dir = cfg["network_staging_dir"] + term.run( + "network_scp_env", + f"export CI_H={host_ip} CI_U={host_user} CI_D={staging_dir}", + timeout=30.0, + ) + # Keep the scp line short: serial consoles truncate long commands. + scp_cmd = "scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $CI_U@$CI_H:$CI_D/* /root/" + print(f"[network] pulling staged files from {host_user}@{host_ip}:{staging_dir}", flush=True) + pull_rc, _ = term.run("network_scp_pull", scp_cmd, timeout=300.0) + if pull_rc != 0: + raise TerminalCommandError(f"board scp pull failed with rc={pull_rc}") + + term.run( + "network_chmod", + "chmod +x /root/boot_zone1.sh /root/check_serial.sh 2>/dev/null || true", + timeout=15.0, + ) + print("network test and file deploy passed", flush=True) + return 0 + + def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: print("————————————————\ncase: zone1_start\n————————————————\n", flush=True) if term is None: @@ -300,7 +377,7 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: raise TerminalCommandError(f"command failed with rc={boot_rc}: ./boot_zone1.sh") if check_rc != 0: raise TerminalCommandError( - f"command failed with rc={check_rc}: check_serial.sh (no shell prompt)" + f"command failed with rc={check_rc}: check_serial.sh (no console prompt)" ) print("zone1_started successfully", flush=True) return 0 @@ -308,6 +385,7 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: CASE_HANDLERS: dict[str, CaseFunc] = { "zone0_start": zone0_start, + "network": network, "lspci": lspci, "zone1_start": zone1_start, } @@ -336,6 +414,15 @@ def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: raise SystemExit(f"no test cases configured for bid '{args.bid}'") cell_root = Path.cwd() + build_args = bid_entry.get("build_args") or {} + network_cfg = tests.get("network") or {} + if not isinstance(network_cfg, dict): + network_cfg = {} + hvisor_tool_path = os.environ.get("HVISOR_TOOL_PATH", "").strip() + if not hvisor_tool_path: + hvisor_tool_path = str((cell_root / "hvisor-tool").resolve()) + elif not Path(hvisor_tool_path).is_absolute(): + hvisor_tool_path = str((cell_root / hvisor_tool_path).resolve()) return { "bid": args.bid, "arch": arch, @@ -343,6 +430,8 @@ def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: "mode": mode, "cases": cases, "workspace": cell_root, + "kdir": str(build_args.get("KDIR", "")).strip(), + "hvisor_tool_path": hvisor_tool_path, "socket_path": str((cell_root / ".qemu" / "qemu.sock").resolve()), "serial_port": str(tests.get("serial", "/dev/null")), "power_serial": str(tests.get("power_serial", "")).strip(), @@ -351,6 +440,13 @@ def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: "uboot_ready_pattern": str(tests.get("uboot_ready_pattern", "")).strip(), "tftp_dir": str(tests.get("tftp_dir", "/home/light/tftp")).strip(), "lspci": tests.get("lspci") or {}, + "network_host_ip": str(network_cfg.get("host_ip", "192.168.1.181")).strip(), + "network_host_user": str(network_cfg.get("host_user", "light")).strip(), + "network_staging_dir": str( + network_cfg.get("staging_dir", "/home/light/tftp/ci_deploy") + ).strip(), + "network_ping_count": int(network_cfg.get("ping_count", 3)), + "zone1_dtb": str(network_cfg.get("zone1_dtb", "")).strip(), } diff --git a/src/pci/pci_handler.rs b/src/pci/pci_handler.rs index 445364086..6787c0877 100644 --- a/src/pci/pci_handler.rs +++ b/src/pci/pci_handler.rs @@ -1623,7 +1623,7 @@ fn handle_pci_bridge_access( Ok(None) } else { let reg = dev.with_config_value(|cv| cv.get_bridge_bus_reg()); - Ok(Some((((reg >> ((reg_offset - 0x18) * 8)) & 0xff)) as usize)) + Ok(Some(((reg >> ((reg_offset - 0x18) * 8)) & 0xff) as usize)) } } BridgeField::BusNumbers => { @@ -1633,7 +1633,9 @@ fn handle_pci_bridge_access( } Ok(None) } else { - Ok(Some(dev.with_config_value(|cv| cv.get_bridge_bus_reg()) as usize)) + Ok(Some( + dev.with_config_value(|cv| cv.get_bridge_bus_reg()) as usize + )) } } _ => Ok(None), @@ -1996,18 +1998,15 @@ pub fn mmio_dwc_cfg_handler(mmio: &mut MMIOAccess, _base: usize) -> HvResult { ((pci_target >> 19) & 0x1f) as u8, ((pci_target >> 16) & 0x7) as u8, ); - global_pcie_list - .get(&target_bdf) - .cloned() - .or_else(|| { - global_pcie_list - .values() - .find(|dev| { - let dev_guard = dev.read(); - dev_guard.get_base() == pci_target - }) - .cloned() - }) + global_pcie_list.get(&target_bdf).cloned().or_else(|| { + global_pcie_list + .values() + .find(|dev| { + let dev_guard = dev.read(); + dev_guard.get_base() == pci_target + }) + .cloned() + }) }; dev_clone } @@ -2105,11 +2104,7 @@ pub fn mmio_vpci_handler_dbi(mmio: &mut MMIOAccess, _base: usize) -> HvResult { let zone = crate::zone::root_zone(); let mut inner = zone.write(); - inner.virtual_pci_mmio_init_delay( - &root_config.pci_config, - num_pci_bus, - domain_id, - ); + inner.virtual_pci_mmio_init_delay(&root_config.pci_config, num_pci_bus, domain_id); drop(inner); if let Some(domain_cfg) = root_config.pci_config[..num_pci_bus] From 1b3a6ec65b5fa1fcbc19fe8238c0b2fc7b66d178 Mon Sep 17 00:00:00 2001 From: ray Date: Tue, 21 Jul 2026 12:14:31 +0800 Subject: [PATCH 32/53] Fixed the permissions issue with the logs folder --- .gitignore | 2 +- Jenkinsfile | 35 ++++++++++++++++++++++++++++++++++- jenkins/ci_runner.py | 23 +++++++---------------- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 332a469d3..9039abf35 100644 --- a/.gitignore +++ b/.gitignore @@ -39,7 +39,7 @@ src/platform/__board.rs rootfs1.zip* jenkins/__pycache__ jenkins/logs/ -/logs/ +logs/ jenkins-cli.jar tools/kconfig/.venv/ tools/kconfig/__pycache__/ diff --git a/Jenkinsfile b/Jenkinsfile index 54f44bcd7..c633f7ae7 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -32,6 +32,33 @@ def matrixCellDir() { return "${env.WORKSPACE}/.matrix/${bid.replace('/', '__')}" } +/** Stable directory name for a BID under ``$WORKSPACE/logs/``. */ +def bidLogKey(String bid = env.BID) { + return (bid ?: '').replace('/', '__') +} + +/** Copy per-cell logs out to ``$WORKSPACE/logs/`` before ``.matrix`` is removed. */ +def collectCellLogs() { + def src = "${matrixCellDir()}/logs" + def dest = "${env.WORKSPACE}/logs/${bidLogKey()}" + def logsRoot = "${env.WORKSPACE}/logs" + sh """ + if [ ! -d '${src}' ]; then + echo "No cell logs to collect: ${src}" + exit 0 + fi + mkdir -p '${dest}' 2>/dev/null || sudo mkdir -p '${dest}' + sudo chown -R "\$(id -u):\$(id -g)" '${logsRoot}' 2>/dev/null || true + # Source may be root-owned after board sudo ci_runner. + if ! cp -a '${src}/.' '${dest}/' 2>/dev/null; then + sudo cp -a '${src}/.' '${dest}/' + sudo chown -R "\$(id -u):\$(id -g)" '${dest}' + fi + echo "Collected cell logs -> ${dest}" + ls -la '${dest}' || true + """ +} + /** Isolated workspace for top-level CI jobs (linter, license-checker, …). */ def jenkinsJobDir(String name) { return "${env.WORKSPACE}/.jenkins/${name}" @@ -45,6 +72,7 @@ def syncWorkspaceTo(String destDir) { --exclude '.jenkins/' \\ --exclude '.matrix/' \\ --exclude '.jenkins-matrix/' \\ + --exclude 'logs/' \\ --exclude '__pycache__/' \\ --exclude '*.pyc' \\ '${env.WORKSPACE}/' '${destDir}/' @@ -190,7 +218,9 @@ pipeline { always { echo "=== DEBUG: Branch ${env.BRANCH_NAME} ===" echo "=== DEBUG: Commit ${env.GIT_COMMIT} ===" + // Keep $WORKSPACE/logs (collected from cells); only tear down sandboxes. sh 'sudo rm -rf .matrix 2>/dev/null || rm -rf .matrix || true' + archiveArtifacts artifacts: 'logs/**/*', allowEmptyArchive: true } } @@ -438,7 +468,10 @@ pipeline { post { always { - script { finishGithubCheck(matrixCheckName(), currentBuild.currentResult) } + script { + collectCellLogs() + finishGithubCheck(matrixCheckName(), currentBuild.currentResult) + } } } } diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 7d36769be..f310ff3f1 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -23,20 +23,9 @@ ZONE1_INNER_LOG_FETCH_TIMEOUT = 30.0 -def bid_log_key(bid: str) -> str: - return bid.replace("/", "__") - - -def jenkins_workspace_root(fallback: Path) -> Path: - workspace = os.environ.get("WORKSPACE", "").strip() - if workspace: - return Path(workspace) - return fallback - - def logs_dir(cfg: dict[str, Any]) -> Path: - root = jenkins_workspace_root(cfg["workspace"]) - path = root / "logs" / bid_log_key(cfg["bid"]) + """Per-cell log directory: ``/logs``.""" + path = Path(cfg["workspace"]) / "logs" path.mkdir(parents=True, exist_ok=True) return path @@ -168,15 +157,17 @@ def save_inner_serial_log(cfg: dict[str, Any], content: str) -> None: def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: print("————————————————\ncase: zone0_start\n————————————————\n", flush=True) if cfg["mode"] == "qemu": + # Create log dir before starting QEMU so a permission failure does not + # leave a running guest that must be SIGTERM'd from finally. + log_path = logs_dir(cfg) / "zone0_console.log" + log_path.write_text("", encoding="utf-8") + cmd = ["make", f"ARCH={cfg['arch']}", f"BOARD={cfg['board']}", "MODE=release", "ci-run"] proc = subprocess.Popen(cmd, cwd=cfg["workspace"], start_new_session=True) cfg["_managed_proc"] = proc cfg["_managed_proc_name"] = "qemu ci-run" wait_qemu_socket(cfg["socket_path"], timeout=30.0) - log_path = logs_dir(cfg) / "zone0_console.log" - log_path.write_text("", encoding="utf-8") - qemu_term = ensure_qemu_terminal(cfg, log_path) uboot_cmd = cfg.get("uboot_cmd", "") uboot_ready = cfg.get("uboot_ready_pattern", "") From e4baae74348f0124ae02cc125b49e15177796271 Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 21 Jul 2026 12:14:31 +0800 Subject: [PATCH 33/53] Fixed the permissions issue with the logs folder --- .gitignore | 2 +- Jenkinsfile | 35 ++++++++++++++++++++++++++++++++++- jenkins/ci_runner.py | 23 +++++++---------------- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 332a469d3..9039abf35 100644 --- a/.gitignore +++ b/.gitignore @@ -39,7 +39,7 @@ src/platform/__board.rs rootfs1.zip* jenkins/__pycache__ jenkins/logs/ -/logs/ +logs/ jenkins-cli.jar tools/kconfig/.venv/ tools/kconfig/__pycache__/ diff --git a/Jenkinsfile b/Jenkinsfile index 54f44bcd7..c633f7ae7 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -32,6 +32,33 @@ def matrixCellDir() { return "${env.WORKSPACE}/.matrix/${bid.replace('/', '__')}" } +/** Stable directory name for a BID under ``$WORKSPACE/logs/``. */ +def bidLogKey(String bid = env.BID) { + return (bid ?: '').replace('/', '__') +} + +/** Copy per-cell logs out to ``$WORKSPACE/logs/`` before ``.matrix`` is removed. */ +def collectCellLogs() { + def src = "${matrixCellDir()}/logs" + def dest = "${env.WORKSPACE}/logs/${bidLogKey()}" + def logsRoot = "${env.WORKSPACE}/logs" + sh """ + if [ ! -d '${src}' ]; then + echo "No cell logs to collect: ${src}" + exit 0 + fi + mkdir -p '${dest}' 2>/dev/null || sudo mkdir -p '${dest}' + sudo chown -R "\$(id -u):\$(id -g)" '${logsRoot}' 2>/dev/null || true + # Source may be root-owned after board sudo ci_runner. + if ! cp -a '${src}/.' '${dest}/' 2>/dev/null; then + sudo cp -a '${src}/.' '${dest}/' + sudo chown -R "\$(id -u):\$(id -g)" '${dest}' + fi + echo "Collected cell logs -> ${dest}" + ls -la '${dest}' || true + """ +} + /** Isolated workspace for top-level CI jobs (linter, license-checker, …). */ def jenkinsJobDir(String name) { return "${env.WORKSPACE}/.jenkins/${name}" @@ -45,6 +72,7 @@ def syncWorkspaceTo(String destDir) { --exclude '.jenkins/' \\ --exclude '.matrix/' \\ --exclude '.jenkins-matrix/' \\ + --exclude 'logs/' \\ --exclude '__pycache__/' \\ --exclude '*.pyc' \\ '${env.WORKSPACE}/' '${destDir}/' @@ -190,7 +218,9 @@ pipeline { always { echo "=== DEBUG: Branch ${env.BRANCH_NAME} ===" echo "=== DEBUG: Commit ${env.GIT_COMMIT} ===" + // Keep $WORKSPACE/logs (collected from cells); only tear down sandboxes. sh 'sudo rm -rf .matrix 2>/dev/null || rm -rf .matrix || true' + archiveArtifacts artifacts: 'logs/**/*', allowEmptyArchive: true } } @@ -438,7 +468,10 @@ pipeline { post { always { - script { finishGithubCheck(matrixCheckName(), currentBuild.currentResult) } + script { + collectCellLogs() + finishGithubCheck(matrixCheckName(), currentBuild.currentResult) + } } } } diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 7d36769be..f310ff3f1 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -23,20 +23,9 @@ ZONE1_INNER_LOG_FETCH_TIMEOUT = 30.0 -def bid_log_key(bid: str) -> str: - return bid.replace("/", "__") - - -def jenkins_workspace_root(fallback: Path) -> Path: - workspace = os.environ.get("WORKSPACE", "").strip() - if workspace: - return Path(workspace) - return fallback - - def logs_dir(cfg: dict[str, Any]) -> Path: - root = jenkins_workspace_root(cfg["workspace"]) - path = root / "logs" / bid_log_key(cfg["bid"]) + """Per-cell log directory: ``/logs``.""" + path = Path(cfg["workspace"]) / "logs" path.mkdir(parents=True, exist_ok=True) return path @@ -168,15 +157,17 @@ def save_inner_serial_log(cfg: dict[str, Any], content: str) -> None: def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: print("————————————————\ncase: zone0_start\n————————————————\n", flush=True) if cfg["mode"] == "qemu": + # Create log dir before starting QEMU so a permission failure does not + # leave a running guest that must be SIGTERM'd from finally. + log_path = logs_dir(cfg) / "zone0_console.log" + log_path.write_text("", encoding="utf-8") + cmd = ["make", f"ARCH={cfg['arch']}", f"BOARD={cfg['board']}", "MODE=release", "ci-run"] proc = subprocess.Popen(cmd, cwd=cfg["workspace"], start_new_session=True) cfg["_managed_proc"] = proc cfg["_managed_proc_name"] = "qemu ci-run" wait_qemu_socket(cfg["socket_path"], timeout=30.0) - log_path = logs_dir(cfg) / "zone0_console.log" - log_path.write_text("", encoding="utf-8") - qemu_term = ensure_qemu_terminal(cfg, log_path) uboot_cmd = cfg.get("uboot_cmd", "") uboot_ready = cfg.get("uboot_ready_pattern", "") From 4dd86487db6209cbfeed1953ddaa0e169d446c56 Mon Sep 17 00:00:00 2001 From: ray Date: Tue, 21 Jul 2026 13:09:27 +0800 Subject: [PATCH 34/53] remove scp for rootfs2 and add time for timeout --- jenkins/board_scp.sh | 4 +--- jenkins/ci_runner.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/jenkins/board_scp.sh b/jenkins/board_scp.sh index 8d7743861..a8aab9139 100755 --- a/jenkins/board_scp.sh +++ b/jenkins/board_scp.sh @@ -95,9 +95,7 @@ ZONE1_ASSETS_DIR=${ZONE1_ASSETS_DIR:-/home/light/tftp/zone1_assets} if [ -f "${ZONE1_ASSETS_DIR}/Image" ]; then cp "${ZONE1_ASSETS_DIR}/Image" "${STAGING_DIR}/" fi -if [ -f "${ZONE1_ASSETS_DIR}/rootfs2.ext4" ]; then - cp "${ZONE1_ASSETS_DIR}/rootfs2.ext4" "${STAGING_DIR}/" -fi +# rootfs2.ext4 is large and persistent on the board; do not re-stage/pull every run. chmod -R a+rX "${STAGING_DIR}" echo "board staging completed: ${STAGING_DIR}" diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index f310ff3f1..e628f4459 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -331,7 +331,7 @@ def network(cfg: dict[str, Any], term: Terminal | None) -> int: term.run( "network_chmod", "chmod +x /root/boot_zone1.sh /root/check_serial.sh 2>/dev/null || true", - timeout=15.0, + timeout=60.0, ) print("network test and file deploy passed", flush=True) return 0 From 48d5b97c95530ba5a94ac25e3605eef45fc77bc0 Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 21 Jul 2026 13:09:27 +0800 Subject: [PATCH 35/53] remove scp for rootfs2 and add time for timeout --- jenkins/board_scp.sh | 4 +--- jenkins/ci_runner.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/jenkins/board_scp.sh b/jenkins/board_scp.sh index 8d7743861..a8aab9139 100755 --- a/jenkins/board_scp.sh +++ b/jenkins/board_scp.sh @@ -95,9 +95,7 @@ ZONE1_ASSETS_DIR=${ZONE1_ASSETS_DIR:-/home/light/tftp/zone1_assets} if [ -f "${ZONE1_ASSETS_DIR}/Image" ]; then cp "${ZONE1_ASSETS_DIR}/Image" "${STAGING_DIR}/" fi -if [ -f "${ZONE1_ASSETS_DIR}/rootfs2.ext4" ]; then - cp "${ZONE1_ASSETS_DIR}/rootfs2.ext4" "${STAGING_DIR}/" -fi +# rootfs2.ext4 is large and persistent on the board; do not re-stage/pull every run. chmod -R a+rX "${STAGING_DIR}" echo "board staging completed: ${STAGING_DIR}" diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index f310ff3f1..e628f4459 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -331,7 +331,7 @@ def network(cfg: dict[str, Any], term: Terminal | None) -> int: term.run( "network_chmod", "chmod +x /root/boot_zone1.sh /root/check_serial.sh 2>/dev/null || true", - timeout=15.0, + timeout=60.0, ) print("network test and file deploy passed", flush=True) return 0 From 188b1260568dfc9117d4a16d8680bdddabb06c7a Mon Sep 17 00:00:00 2001 From: w101yyds <1022377521@qq.com> Date: Mon, 27 Jul 2026 18:40:34 +0800 Subject: [PATCH 36/53] fix serial rk3568 --- jenkins/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index 356d4d835..9ff99586f 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -24,7 +24,7 @@ bids: - KDIR=/home/light/DEMO/sdk/rk356x-up4-2c/kernel tests: mode: board - serial: /dev/serial/by-id/usb-1a86_USB2.0-Ser_-if00-port0 + serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 power_serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 baudrate: 1500000 uboot_ready_pattern: '=>' From 0235ba495c3a36c123dc130004666acd3a3f0437 Mon Sep 17 00:00:00 2001 From: w101yyds <1022377521@qq.com> Date: Mon, 27 Jul 2026 20:45:22 +0800 Subject: [PATCH 37/53] restore serial device --- jenkins/ci.yaml | 2 +- platform/aarch64/phytium-pi/configs/zone1-linux.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index 9ff99586f..356d4d835 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -24,7 +24,7 @@ bids: - KDIR=/home/light/DEMO/sdk/rk356x-up4-2c/kernel tests: mode: board - serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 + serial: /dev/serial/by-id/usb-1a86_USB2.0-Ser_-if00-port0 power_serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 baudrate: 1500000 uboot_ready_pattern: '=>' diff --git a/platform/aarch64/phytium-pi/configs/zone1-linux.json b/platform/aarch64/phytium-pi/configs/zone1-linux.json index 5bf7d154b..259d6fbab 100644 --- a/platform/aarch64/phytium-pi/configs/zone1-linux.json +++ b/platform/aarch64/phytium-pi/configs/zone1-linux.json @@ -50,5 +50,6 @@ "gicr_size": "0x80000", "gits_base": "0x0", "gits_size": "0x0" - } + }, + "pci_config":[] } From 897ffaa2896c8980aaa242360cdeded07026ebb1 Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 28 Jul 2026 20:44:58 +0800 Subject: [PATCH 38/53] remove the copy of image to increase speed --- jenkins/board_scp.sh | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/jenkins/board_scp.sh b/jenkins/board_scp.sh index a8aab9139..12a568740 100755 --- a/jenkins/board_scp.sh +++ b/jenkins/board_scp.sh @@ -1,6 +1,7 @@ #!/bin/sh # Stage zone1 test artifacts on the CI host for board pull via scp. -# Zone0 boot Image stays on TFTP; only zone1 Image/rootfs are staged here. +# Zone0 boot Image, Image and rootfs2.ext4 are large and +# persistent on the board; do not re-stage them every CI run. set -eux @@ -91,12 +92,6 @@ if [ -f "${CHECK_SERIAL_SCRIPT}" ]; then cp "${CHECK_SERIAL_SCRIPT}" "${STAGING_DIR}/" fi -ZONE1_ASSETS_DIR=${ZONE1_ASSETS_DIR:-/home/light/tftp/zone1_assets} -if [ -f "${ZONE1_ASSETS_DIR}/Image" ]; then - cp "${ZONE1_ASSETS_DIR}/Image" "${STAGING_DIR}/" -fi -# rootfs2.ext4 is large and persistent on the board; do not re-stage/pull every run. - chmod -R a+rX "${STAGING_DIR}" echo "board staging completed: ${STAGING_DIR}" ls -la "${STAGING_DIR}" From b242f794b0b7bb9eb136416d8ad226c109c806ea Mon Sep 17 00:00:00 2001 From: dallas Date: Wed, 29 Jul 2026 15:05:05 +0800 Subject: [PATCH 39/53] fix mem overlap --- platform/aarch64/rk3568/board.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/platform/aarch64/rk3568/board.rs b/platform/aarch64/rk3568/board.rs index cc3b54742..2c76f10e1 100644 --- a/platform/aarch64/rk3568/board.rs +++ b/platform/aarch64/rk3568/board.rs @@ -204,12 +204,12 @@ pub const ROOT_ZONE_MEMORY_REGIONS: &[HvConfigMemoryRegion] = &[ // virtual_start: 0x10f000, // size: 0x1000, // }, //scmi-shmem - HvConfigMemoryRegion { - mem_type: MEM_TYPE_RAM, - physical_start: 0xfd440000, - virtual_start: 0xfd440000, - size: 0x20000, - }, // its + // HvConfigMemoryRegion { + // mem_type: MEM_TYPE_RAM, + // physical_start: 0xfd440000, + // virtual_start: 0xfd440000, + // size: 0x20000, + // }, // its HvConfigMemoryRegion { mem_type: MEM_TYPE_RAM, physical_start: 0x1f0000000, From 19a6c65cd1e60816eed49fdaa212e6863ba5229e Mon Sep 17 00:00:00 2001 From: w101yyds <1022377521@qq.com> Date: Thu, 30 Jul 2026 14:21:49 +0800 Subject: [PATCH 40/53] phytium-pi ci --- Jenkinsfile | 27 +++++-- jenkins/board_power.sh | 69 +++++++++++++---- jenkins/board_scp.sh | 32 ++++++-- jenkins/ci.yaml | 42 ++++++++++- jenkins/ci_runner.py | 166 +++++++++++++++++++++++++++++++++++------ 5 files changed, 286 insertions(+), 50 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index c633f7ae7..6a7850c28 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -419,13 +419,30 @@ pipeline { "${prepareScript}" """ } else if (mode == 'board') { - echo "Deploy TFTP artifacts [BID=${env.BID}, TFTP_DIR=${env.TFTP_DIR}]" + def tftpDir = (testsCfg.tftp_dir ?: env.TFTP_DIR).toString() + def zone0Dtbs = testsCfg.zone0_dtbs ?: [] + if (testsCfg.zone0_dtb) { + zone0Dtbs = [testsCfg.zone0_dtb] + } + def zone0Image = (testsCfg.zone0_image ?: "${kdir}/arch/arm64/boot/Image").toString() + echo "Deploy TFTP artifacts [BID=${env.BID}, TFTP_DIR=${tftpDir}]" sh """ export TERM=\${TERM:-xterm} - sudo mkdir -p "${env.TFTP_DIR}" - sudo make cp ARCH=${arch} BOARD=${board} MODE=release TFTP_DIR="${env.TFTP_DIR}" - sudo cp platform/${arch}/${board}/image/dts/rk3568_limit_zone0.dtb "${env.TFTP_DIR}/" - sudo cp ${kdir}/arch/arm64/boot/Image "${env.TFTP_DIR}/" + sudo mkdir -p "${tftpDir}" + sudo find "${tftpDir}" -mindepth 1 -maxdepth 1 -type f -delete + sudo make cp ARCH=${arch} BOARD=${board} MODE=release TFTP_DIR="${tftpDir}" + """ + zone0Dtbs.each { dtb -> + sh """ + test -f "${dtb}" + sudo cp "${dtb}" "${tftpDir}/" + """ + } + sh """ + test -f "${zone0Image}" + sudo cp "${zone0Image}" "${tftpDir}/Image" + sudo chmod -R a+rX "${tftpDir}" + ls -la "${tftpDir}" """ } else { error("jenkins/ci.yaml BID=${env.BID}: unsupported tests.mode='${mode}'") diff --git a/jenkins/board_power.sh b/jenkins/board_power.sh index 0eb7a28bc..bb6d3915d 100755 --- a/jenkins/board_power.sh +++ b/jenkins/board_power.sh @@ -1,32 +1,73 @@ #!/bin/sh # Relay power control for board CI (socat hex frames). -# Usage: board_power.sh off|on|cycle +# Usage: board_power.sh off|on|cycle [channel] # cycle: off, wait 3s, on set -eu action=${1:?action required (off|on|cycle)} port=${2:?serial port required, e.g. /dev/ttyUSB1} +channel=${3:-4} +lock_file=${RELAY_LOCK_FILE:-/var/lock/hvisor-relay.lock} +lock_timeout=${RELAY_LOCK_TIMEOUT:-300} + +if ! command -v flock >/dev/null 2>&1; then + echo "error: flock is required for relay serial locking" >&2 + exit 1 +fi + +case "${channel}" in + 1|2|3|4) ;; + *) + echo "invalid relay channel: ${channel}" >&2 + exit 1 + ;; +esac + +checksum() { + state=$1 + printf '%02X' "$((0xA0 + channel + state))" +} send_frame() { - frame=$1 - printf '%b' "$frame" | sudo socat - "$port" + state=$1 + frame=$(printf '%s%02X%s%02X%s%s' '\\xA0\\x' "${channel}" '\\x' "${state}" '\\x' "$(checksum "${state}")") + printf '%b' "$frame" | sudo socat - "$port,b9600,raw,echo=0" } +run_locked() { + if ! flock -w "${lock_timeout}" 9; then + echo "error: timed out waiting for relay lock: ${lock_file}" >&2 + exit 1 + fi + + case "$action" in + off) + send_frame 0 + ;; + on) + send_frame 1 + ;; + cycle) + send_frame 0 + sleep 3 + send_frame 1 + ;; + *) + echo "usage: $0 off|on|cycle [channel]" >&2 + exit 1 + ;; + esac +} + +exec 9>"${lock_file}" + case "$action" in - off) - send_frame '\xA0\x04\x00\xA4' - ;; - on) - send_frame '\xA0\x04\x01\xA5' - ;; - cycle) - send_frame '\xA0\x04\x00\xA4' - sleep 3 - send_frame '\xA0\x04\x01\xA5' + off|on|cycle) + run_locked ;; *) - echo "usage: $0 off|on|cycle " >&2 + echo "usage: $0 off|on|cycle [channel]" >&2 exit 1 ;; esac diff --git a/jenkins/board_scp.sh b/jenkins/board_scp.sh index a8aab9139..2a7377e12 100755 --- a/jenkins/board_scp.sh +++ b/jenkins/board_scp.sh @@ -22,6 +22,9 @@ IMAGE_DIR="${PLATFORM_DIR}/image" SCRIPTS_DIR="${PLATFORM_DIR}/scripts" ZONE1_BOOT_SCRIPT="${SCRIPTS_DIR}/boot_zone1.sh" CHECK_SERIAL_SCRIPT="${WORKSPACE_ROOT}/jenkins/check_serial.sh" +EXTERNAL_DIR=${EXTERNAL_DIR:-} +START_SCRIPT=${START_SCRIPT:-} +COPY_HVISOR_BIN=${COPY_HVISOR_BIN:-true} case "${ARCH}" in x86_64) RUSTC_TARGET="x86_64-unknown-none" ;; @@ -59,12 +62,21 @@ if [ ! -f "${HVISOR_TOOL_PATH}/output/hvisor.ko" ]; then echo "error: hvisor.ko not found: ${HVISOR_TOOL_PATH}/output/hvisor.ko" exit 1 fi -if [ ! -f "${HVISOR_BIN}" ]; then +if [ "${COPY_HVISOR_BIN}" != "false" ] && [ ! -f "${HVISOR_BIN}" ]; then echo "error: hvisor.bin not found: ${HVISOR_BIN}" exit 1 fi +if [ -n "${START_SCRIPT}" ]; then + ZONE1_BOOT_SCRIPT="${START_SCRIPT}" + ZONE1_SCRIPT_DEST="start.sh" +elif [ -n "${EXTERNAL_DIR}" ] && [ -f "${EXTERNAL_DIR}/start.sh" ]; then + ZONE1_BOOT_SCRIPT="${EXTERNAL_DIR}/start.sh" + ZONE1_SCRIPT_DEST="start.sh" +else + ZONE1_SCRIPT_DEST="$(basename "${ZONE1_BOOT_SCRIPT}")" +fi if [ ! -f "${ZONE1_BOOT_SCRIPT}" ]; then - echo "error: boot script not found: ${ZONE1_BOOT_SCRIPT}" + echo "error: zone1 start script not found: ${ZONE1_BOOT_SCRIPT}" exit 1 fi @@ -77,9 +89,11 @@ rm -rf "${STAGING_DIR}" mkdir -p "${STAGING_DIR}" cp "${HVISOR_TOOL_PATH}/output/hvisor" "${HVISOR_TOOL_PATH}/output/hvisor.ko" "${STAGING_DIR}/" -cp "${HVISOR_BIN}" "${STAGING_DIR}/" +if [ "${COPY_HVISOR_BIN}" != "false" ]; then + cp "${HVISOR_BIN}" "${STAGING_DIR}/" +fi cp "${CONFIGS_DIR}/"* "${STAGING_DIR}/" -cp "${ZONE1_BOOT_SCRIPT}" "${STAGING_DIR}/" +cp "${ZONE1_BOOT_SCRIPT}" "${STAGING_DIR}/${ZONE1_SCRIPT_DEST}" if [ -f "${ZONE1_DTB}" ]; then cp "${ZONE1_DTB}" "${STAGING_DIR}/" @@ -92,11 +106,17 @@ if [ -f "${CHECK_SERIAL_SCRIPT}" ]; then fi ZONE1_ASSETS_DIR=${ZONE1_ASSETS_DIR:-/home/light/tftp/zone1_assets} -if [ -f "${ZONE1_ASSETS_DIR}/Image" ]; then +if [ -n "${EXTERNAL_DIR}" ] && [ -f "${EXTERNAL_DIR}/Image" ]; then + cp "${EXTERNAL_DIR}/Image" "${STAGING_DIR}/" +elif [ -f "${ZONE1_ASSETS_DIR}/Image" ]; then cp "${ZONE1_ASSETS_DIR}/Image" "${STAGING_DIR}/" fi -# rootfs2.ext4 is large and persistent on the board; do not re-stage/pull every run. +if [ -n "${EXTERNAL_DIR}" ] && [ -f "${EXTERNAL_DIR}/rootfs2.ext4" ]; then + cp "${EXTERNAL_DIR}/rootfs2.ext4" "${STAGING_DIR}/" +fi +# rk3568 rootfs2.ext4 is large and persistent on the board; do not re-stage/pull every run. chmod -R a+rX "${STAGING_DIR}" +chmod +x "${STAGING_DIR}/${ZONE1_SCRIPT_DEST}" echo "board staging completed: ${STAGING_DIR}" ls -la "${STAGING_DIR}" diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index 356d4d835..cb9b8b41b 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -26,9 +26,13 @@ bids: mode: board serial: /dev/serial/by-id/usb-1a86_USB2.0-Ser_-if00-port0 power_serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 + power_channel: 4 baudrate: 1500000 + tftp_dir: /home/light/tftp/rk3568 + zone0_dtbs: + - platform/aarch64/rk3568/image/dts/rk3568_limit_zone0.dtb uboot_ready_pattern: '=>' - uboot_cmd: pci enum;setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.240; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:hvisor.bin; tftp ${fdt_addr} ${serverip}:rk3568_limit_zone0.dtb; tftp ${zone0_kernel_addr} ${serverip}:Image; bootm ${loadaddr} - ${fdt_addr}; + uboot_cmd: pci enum;setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.240; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:rk3568/hvisor.bin && tftp ${fdt_addr} ${serverip}:rk3568/rk3568_limit_zone0.dtb && tftp ${zone0_kernel_addr} ${serverip}:rk3568/Image && bootm ${loadaddr} - ${fdt_addr}; network: host_ip: 192.168.1.181 host_user: light @@ -47,6 +51,40 @@ bids: - lspci - zone1_start + - bid: aarch64/phytium-pi + build_args: + - KDIR=/home/light/DEMO/linux/kernel-5.10 + tests: + mode: board + serial: /dev/serial/by-id/phytium-pi-uart + power_serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 + power_channel: 3 + baudrate: 115200 + tftp_dir: /home/light/tftp/phytium-pi + zone0_dtbs: + - platform/aarch64/phytium-pi/image/dts/linux1.dtb + - platform/aarch64/phytium-pi/image/dts/phytium-pi-board-v2.dtb + zone0_image: /home/light/DEMO/phytium-pi-external/Image + uboot_ready_pattern: 'Phytium-Pi#' + uboot_cmd: setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.178; setenv loadaddr 0x90100000; setenv fdt_addr 0x90000000; setenv zone0_kernel_addr 0xa0400000; setenv zone0_fdt_addr 0xa0000000; tftp ${loadaddr} ${serverip}:phytium-pi/hvisor.bin && tftp ${fdt_addr} ${serverip}:phytium-pi/phytium-pi-board-v2.dtb && tftp ${zone0_kernel_addr} ${serverip}:phytium-pi/Image && tftp ${zone0_fdt_addr} ${serverip}:phytium-pi/linux1.dtb && bootm ${loadaddr} - ${fdt_addr}; + board_ip: 192.168.1.178 + board_iface: eth0 + board_user: user + board_pass: user + scp_dst: /home/user/zone1 + network: + host_ip: 192.168.1.181 + host_user: light + staging_dir: /home/light/DEMO/tmp + zone1_dtb: platform/aarch64/phytium-pi/image/dts/linux2.dtb + external_dir: /home/light/DEMO/phytium-pi-external + start_script: /home/light/DEMO/phytium-pi-external/start.sh + copy_hvisor_bin: false + cases: + - phytium_deploy_zone1 + - zone0_start + - phytium_start_zone1 + - bid: x86_64/qemu build_args: @@ -56,5 +94,3 @@ bids: cases: - zone0_start - zone1_start - - diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index e628f4459..3b131b36d 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -4,6 +4,7 @@ import argparse import os import re +import shutil import socket import signal import subprocess @@ -127,7 +128,11 @@ def board_power_cycle(cfg: dict[str, Any]) -> None: script = board_power_script(cfg) if not script.is_file(): raise SystemExit(f"board power script not found: {script}") - subprocess.run(["bash", str(script), "cycle", power_port], check=True, cwd=cfg["workspace"]) + subprocess.run( + ["bash", str(script), "cycle", power_port, str(cfg.get("power_channel", 4))], + check=True, + cwd=cfg["workspace"], + ) def board_wake_console(term: Terminal, *, repeats: int = 3) -> None: @@ -154,6 +159,109 @@ def save_inner_serial_log(cfg: dict[str, Any], content: str) -> None: path.write_text(content, encoding="utf-8") +def stage_board_files(cfg: dict[str, Any]) -> None: + script = cfg["workspace"] / "jenkins" / "board_scp.sh" + if not script.is_file(): + raise SystemExit(f"board scp script not found: {script}") + + env = os.environ.copy() + env["ARCH"] = cfg["arch"] + env["BOARD"] = cfg["board"] + if cfg["kdir"]: + env["KDIR"] = cfg["kdir"] + env["WORKSPACE_ROOT"] = str(cfg["workspace"]) + env["HVISOR_TOOL_PATH"] = cfg["hvisor_tool_path"] + env["STAGING_DIR"] = cfg["network_staging_dir"] + if cfg.get("zone1_dtb"): + zone1_dtb = Path(cfg["zone1_dtb"]) + if not zone1_dtb.is_absolute(): + zone1_dtb = cfg["workspace"] / zone1_dtb + env["ZONE1_DTB"] = str(zone1_dtb.resolve()) + if cfg.get("external_dir"): + env["EXTERNAL_DIR"] = cfg["external_dir"] + if cfg.get("start_script"): + env["START_SCRIPT"] = cfg["start_script"] + env["COPY_HVISOR_BIN"] = "true" if cfg.get("copy_hvisor_bin", True) else "false" + + subprocess.run(["bash", str(script)], check=True, cwd=cfg["workspace"], env=env) + + +def board_login_if_needed(cfg: dict[str, Any], term: Terminal, timeout: float = 120.0) -> None: + term.send("") + if term.wait_pattern(r"[$#]\s", timeout=5.0): + return + if not term.wait_pattern(r"Phytium-Pi login:", timeout=timeout, from_offset=0): + raise TerminalTimeoutError("timed out waiting for Phytium-Pi login prompt") + term.send(cfg["board_user"]) + if not term.wait_pattern(r"Password:", timeout=20.0): + raise TerminalTimeoutError("timed out waiting for Phytium-Pi password prompt") + term.send(cfg["board_pass"]) + if not term.wait_pattern(r"[$#]\s", timeout=30.0): + raise TerminalTimeoutError("timed out waiting for Phytium-Pi shell prompt") + + +def host_ssh_prefix(cfg: dict[str, Any]) -> list[str]: + target = f"{cfg['board_user']}@{cfg['board_ip']}" + base = [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + target, + ] + if cfg["board_pass"]: + if shutil.which("sshpass") is None: + raise SystemExit("sshpass is required for password-based board ssh/scp") + return ["sshpass", "-p", cfg["board_pass"], *base] + return base + + +def host_scp_prefix(cfg: dict[str, Any]) -> list[str]: + base = [ + "scp", + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + ] + if cfg["board_pass"]: + if shutil.which("sshpass") is None: + raise SystemExit("sshpass is required for password-based board ssh/scp") + return ["sshpass", "-p", cfg["board_pass"], *base] + return base + + +def run_host_command(cmd: list[str], timeout: float) -> None: + print("+ " + " ".join(cmd), flush=True) + subprocess.run(cmd, check=True, timeout=timeout) + + +def phytium_deploy_zone1(cfg: dict[str, Any], term: Terminal | None) -> int: + print("————————————————\ncase: phytium_deploy_zone1\n————————————————\n", flush=True) + if term is None: + raise SystemExit("terminal backend is required") + board_login_if_needed(cfg, term) + term.run( + "phytium_set_ip", + f"echo {cfg['board_pass']} | sudo -S ifconfig {cfg['board_iface']} {cfg['board_ip']}", + timeout=30.0, + ) + stage_board_files(cfg) + remote_clean = ( + f"mkdir -p '{cfg['scp_dst']}' && " + f"find '{cfg['scp_dst']}' -mindepth 1 -maxdepth 1 -exec rm -rf {{}} +" + ) + run_host_command([*host_ssh_prefix(cfg), remote_clean], timeout=60.0) + src = Path(cfg["network_staging_dir"]) + run_host_command( + [*host_scp_prefix(cfg), *[str(p) for p in sorted(src.iterdir())], f"{cfg['board_user']}@{cfg['board_ip']}:{cfg['scp_dst']}/"], + timeout=300.0, + ) + print("phytium zone1 files deployed successfully", flush=True) + return 0 + + def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: print("————————————————\ncase: zone0_start\n————————————————\n", flush=True) if cfg["mode"] == "qemu": @@ -189,14 +297,14 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: cfg["_board_term"] = board_term board_power_cycle(cfg) time.sleep(3.0) - board_wake_console(board_term) + board_wake_console(board_term, repeats=8 if cfg["board"] == "phytium-pi" else 3) uboot_cmd = cfg.get("uboot_cmd", "") uboot_ready = cfg.get("uboot_ready_pattern", "") if uboot_cmd: if not uboot_ready: uboot_ready = r"=>" - board_wait_uboot_prompt(board_term, uboot_ready, timeout=10.0) + board_wait_uboot_prompt(board_term, uboot_ready, timeout=float(cfg["uboot_prompt_timeout"])) time.sleep(0.3) board_term.send(uboot_cmd) if not board_term.wait_pattern(ZONE0_READY_PATTERN, timeout=180.0): @@ -291,25 +399,7 @@ def network(cfg: dict[str, Any], term: Terminal | None) -> int: raise TerminalCommandError(f"ping {host_ip} failed") print(f"[network] ping {host_ip} ok, staging files on host", flush=True) - script = cfg["workspace"] / "jenkins" / "board_scp.sh" - if not script.is_file(): - raise SystemExit(f"board scp script not found: {script}") - - env = os.environ.copy() - env["ARCH"] = cfg["arch"] - env["BOARD"] = cfg["board"] - if cfg["kdir"]: - env["KDIR"] = cfg["kdir"] - env["WORKSPACE_ROOT"] = str(cfg["workspace"]) - env["HVISOR_TOOL_PATH"] = cfg["hvisor_tool_path"] - env["STAGING_DIR"] = cfg["network_staging_dir"] - if cfg.get("zone1_dtb"): - zone1_dtb = Path(cfg["zone1_dtb"]) - if not zone1_dtb.is_absolute(): - zone1_dtb = cfg["workspace"] / zone1_dtb - env["ZONE1_DTB"] = str(zone1_dtb.resolve()) - - subprocess.run(["bash", str(script)], check=True, cwd=cfg["workspace"], env=env) + stage_board_files(cfg) # Let host staging finish and drain any buffered serial output before scp. time.sleep(3.0) @@ -337,6 +427,22 @@ def network(cfg: dict[str, Any], term: Terminal | None) -> int: return 0 +def phytium_start_zone1(cfg: dict[str, Any], term: Terminal | None) -> int: + print("————————————————\ncase: phytium_start_zone1\n————————————————\n", flush=True) + if term is None: + raise SystemExit("terminal backend is required") + + term.run("phytium_zone1_cd", f"cd {cfg['scp_dst']}", timeout=15.0) + term.run("phytium_zone1_chmod", "chmod +x start.sh hvisor 2>/dev/null || true", timeout=30.0) + term.run("phytium_zone1_start", "./start.sh", timeout=120.0) + time.sleep(float(cfg["zone1_start_wait"])) + _, output = term.run("phytium_zone_list", "./hvisor zone list", timeout=30.0) + if not re.search(r"(?m)^\s*\|?\s*1\s*(?:\||\s)", output): + raise TerminalCommandError("zone id 1 was not found in './hvisor zone list' output") + print("phytium-pi zone1 check passed", flush=True) + return 0 + + def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: print("————————————————\ncase: zone1_start\n————————————————\n", flush=True) if term is None: @@ -375,6 +481,8 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: CASE_HANDLERS: dict[str, CaseFunc] = { + "phytium_deploy_zone1": phytium_deploy_zone1, + "phytium_start_zone1": phytium_start_zone1, "zone0_start": zone0_start, "network": network, "lspci": lspci, @@ -409,6 +517,9 @@ def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: network_cfg = tests.get("network") or {} if not isinstance(network_cfg, dict): network_cfg = {} + copy_hvisor_bin = network_cfg.get("copy_hvisor_bin", True) + if isinstance(copy_hvisor_bin, str): + copy_hvisor_bin = copy_hvisor_bin.lower() not in ("0", "false", "no") hvisor_tool_path = os.environ.get("HVISOR_TOOL_PATH", "").strip() if not hvisor_tool_path: hvisor_tool_path = str((cell_root / "hvisor-tool").resolve()) @@ -426,11 +537,18 @@ def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: "socket_path": str((cell_root / ".qemu" / "qemu.sock").resolve()), "serial_port": str(tests.get("serial", "/dev/null")), "power_serial": str(tests.get("power_serial", "")).strip(), + "power_channel": int(tests.get("power_channel", 4)), "baudrate": int(tests.get("baudrate", 1500000)), "uboot_cmd": str(tests.get("uboot_cmd", "")).strip(), "uboot_ready_pattern": str(tests.get("uboot_ready_pattern", "")).strip(), + "uboot_prompt_timeout": float(tests.get("uboot_prompt_timeout", 20.0)), "tftp_dir": str(tests.get("tftp_dir", "/home/light/tftp")).strip(), "lspci": tests.get("lspci") or {}, + "board_ip": str(tests.get("board_ip", "")).strip(), + "board_iface": str(tests.get("board_iface", "eth0")).strip(), + "board_user": str(tests.get("board_user", "root")).strip(), + "board_pass": str(tests.get("board_pass", "")).strip(), + "scp_dst": str(tests.get("scp_dst", "/root")).strip(), "network_host_ip": str(network_cfg.get("host_ip", "192.168.1.181")).strip(), "network_host_user": str(network_cfg.get("host_user", "light")).strip(), "network_staging_dir": str( @@ -438,6 +556,10 @@ def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: ).strip(), "network_ping_count": int(network_cfg.get("ping_count", 3)), "zone1_dtb": str(network_cfg.get("zone1_dtb", "")).strip(), + "external_dir": str(network_cfg.get("external_dir", "")).strip(), + "start_script": str(network_cfg.get("start_script", "")).strip(), + "copy_hvisor_bin": bool(copy_hvisor_bin), + "zone1_start_wait": float(tests.get("zone1_start_wait", 30.0)), } From 749bbe19a5c13f6470f2f1c733cc64cd03da6db8 Mon Sep 17 00:00:00 2001 From: w101yyds <1022377521@qq.com> Date: Thu, 30 Jul 2026 16:47:05 +0800 Subject: [PATCH 41/53] fix power on/off and zone1 launch success judgement --- jenkins/ci_runner.py | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 3b131b36d..9d3991e95 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -186,11 +186,22 @@ def stage_board_files(cfg: dict[str, Any]) -> None: subprocess.run(["bash", str(script)], check=True, cwd=cfg["workspace"], env=env) -def board_login_if_needed(cfg: dict[str, Any], term: Terminal, timeout: float = 120.0) -> None: - term.send("") - if term.wait_pattern(r"[$#]\s", timeout=5.0): - return - if not term.wait_pattern(r"Phytium-Pi login:", timeout=timeout, from_offset=0): +def board_login_if_needed( + cfg: dict[str, Any], + term: Terminal, + timeout: float = 120.0, + *, + wake: bool = True, +) -> None: + if wake: + term.send("") + if term.wait_pattern(r"[$#]\s", timeout=5.0): + return + if wake: + login_ready = term.wait_pattern(r"Phytium-Pi login:", timeout=timeout, from_offset=0) + else: + login_ready = term.wait_pattern(r"Phytium-Pi login:", timeout=timeout) + if not login_ready: raise TerminalTimeoutError("timed out waiting for Phytium-Pi login prompt") term.send(cfg["board_user"]) if not term.wait_pattern(r"Password:", timeout=20.0): @@ -241,7 +252,8 @@ def phytium_deploy_zone1(cfg: dict[str, Any], term: Terminal | None) -> int: print("————————————————\ncase: phytium_deploy_zone1\n————————————————\n", flush=True) if term is None: raise SystemExit("terminal backend is required") - board_login_if_needed(cfg, term) + board_power_cycle(cfg) + board_login_if_needed(cfg, term, wake=False) term.run( "phytium_set_ip", f"echo {cfg['board_pass']} | sudo -S ifconfig {cfg['board_iface']} {cfg['board_ip']}", @@ -436,10 +448,20 @@ def phytium_start_zone1(cfg: dict[str, Any], term: Terminal | None) -> int: term.run("phytium_zone1_chmod", "chmod +x start.sh hvisor 2>/dev/null || true", timeout=30.0) term.run("phytium_zone1_start", "./start.sh", timeout=120.0) time.sleep(float(cfg["zone1_start_wait"])) - _, output = term.run("phytium_zone_list", "./hvisor zone list", timeout=30.0) - if not re.search(r"(?m)^\s*\|?\s*1\s*(?:\||\s)", output): - raise TerminalCommandError("zone id 1 was not found in './hvisor zone list' output") - print("phytium-pi zone1 check passed", flush=True) + + term.send("script -q /dev/null") + if not term.wait_pattern(r"[$#]\s", timeout=15.0): + raise TerminalTimeoutError("timed out waiting for shell after starting script") + + term.send("screen /dev/pts/0") + if not term.wait_pattern(r"(?:login:|[$#]\s)", timeout=60.0): + raise TerminalTimeoutError("timed out waiting for Phytium-Pi zone1 console") + + term.send("su") + if not term.wait_pattern(r"root@\(none\):/#\s", timeout=30.0): + raise TerminalTimeoutError("timed out waiting for Phytium-Pi zone1 root prompt") + + print("phytium-pi zone1 root console check passed", flush=True) return 0 From 8e2051390db9230f96b3b0b04023b379c1688260 Mon Sep 17 00:00:00 2001 From: w101yyds <1022377521@qq.com> Date: Thu, 30 Jul 2026 18:04:01 +0800 Subject: [PATCH 42/53] not sure if OK --- jenkins/board_power.sh | 63 +++++++++++++++++------------------------- jenkins/ci_runner.py | 26 ++++++++++++----- 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/jenkins/board_power.sh b/jenkins/board_power.sh index bb6d3915d..7b776aa50 100755 --- a/jenkins/board_power.sh +++ b/jenkins/board_power.sh @@ -11,11 +11,6 @@ channel=${3:-4} lock_file=${RELAY_LOCK_FILE:-/var/lock/hvisor-relay.lock} lock_timeout=${RELAY_LOCK_TIMEOUT:-300} -if ! command -v flock >/dev/null 2>&1; then - echo "error: flock is required for relay serial locking" >&2 - exit 1 -fi - case "${channel}" in 1|2|3|4) ;; *) @@ -24,47 +19,41 @@ case "${channel}" in ;; esac -checksum() { - state=$1 - printf '%02X' "$((0xA0 + channel + state))" -} - send_frame() { state=$1 - frame=$(printf '%s%02X%s%02X%s%s' '\\xA0\\x' "${channel}" '\\x' "${state}" '\\x' "$(checksum "${state}")") - printf '%b' "$frame" | sudo socat - "$port,b9600,raw,echo=0" -} - -run_locked() { - if ! flock -w "${lock_timeout}" 9; then - echo "error: timed out waiting for relay lock: ${lock_file}" >&2 - exit 1 + checksum=$((0xA0 + channel + state)) + printf '[power] send port=%s channel=%s state=%s frame=A0%02X%02X%02X\n' \ + "$port" "$channel" "$state" "$channel" "$state" "$checksum" + frame=$(printf '\\%03o\\%03o\\%03o\\%03o' 160 "$channel" "$state" "$checksum") + if [ "$(id -u)" -eq 0 ]; then + printf '%b' "$frame" | socat - "$port,b9600,raw,echo=0" + else + printf '%b' "$frame" | sudo socat - "$port,b9600,raw,echo=0" fi +} - case "$action" in - off) - send_frame 0 - ;; - on) - send_frame 1 - ;; - cycle) - send_frame 0 - sleep 3 - send_frame 1 - ;; - *) - echo "usage: $0 off|on|cycle [channel]" >&2 - exit 1 - ;; - esac +command -v flock >/dev/null 2>&1 || { + echo "error: flock is required for relay serial locking" >&2 + exit 1 } exec 9>"${lock_file}" +flock -w "${lock_timeout}" 9 || { + echo "error: timed out waiting for relay lock: ${lock_file}" >&2 + exit 1 +} case "$action" in - off|on|cycle) - run_locked + off) + send_frame 0 + ;; + on) + send_frame 1 + ;; + cycle) + send_frame 0 + sleep 3 + send_frame 1 ;; *) echo "usage: $0 off|on|cycle [channel]" >&2 diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 9d3991e95..31c81e03a 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -124,15 +124,19 @@ def board_power_script(cfg: dict[str, Any]) -> Path: def board_power_cycle(cfg: dict[str, Any]) -> None: power_port = str(cfg.get("power_serial", "")).strip() if not power_port: + print("[power] skip cycle: power_serial is empty", flush=True) return script = board_power_script(cfg) if not script.is_file(): raise SystemExit(f"board power script not found: {script}") + power_channel = str(cfg.get("power_channel", 4)) + print(f"[power] cycle port={power_port} channel={power_channel}", flush=True) subprocess.run( - ["bash", str(script), "cycle", power_port, str(cfg.get("power_channel", 4))], + ["bash", str(script), "cycle", power_port, power_channel], check=True, cwd=cfg["workspace"], ) + print("[power] cycle completed", flush=True) def board_wake_console(term: Terminal, *, repeats: int = 3) -> None: @@ -248,11 +252,7 @@ def run_host_command(cmd: list[str], timeout: float) -> None: subprocess.run(cmd, check=True, timeout=timeout) -def phytium_deploy_zone1(cfg: dict[str, Any], term: Terminal | None) -> int: - print("————————————————\ncase: phytium_deploy_zone1\n————————————————\n", flush=True) - if term is None: - raise SystemExit("terminal backend is required") - board_power_cycle(cfg) +def phytium_deploy_zone1_with_terminal(cfg: dict[str, Any], term: Terminal) -> int: board_login_if_needed(cfg, term, wake=False) term.run( "phytium_set_ip", @@ -274,6 +274,18 @@ def phytium_deploy_zone1(cfg: dict[str, Any], term: Terminal | None) -> int: return 0 +def phytium_deploy_zone1(cfg: dict[str, Any], term: Terminal | None) -> int: + print("————————————————\ncase: phytium_deploy_zone1\n————————————————\n", flush=True) + board_power_cycle(cfg) + if term is not None: + return phytium_deploy_zone1_with_terminal(cfg, term) + + log_path = logs_dir(cfg) / "zone1_console.log" + log_path.write_text("", encoding="utf-8") + with build_terminal(cfg, log_path) as board_term: + return phytium_deploy_zone1_with_terminal(cfg, board_term) + + def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: print("————————————————\ncase: zone0_start\n————————————————\n", flush=True) if cfg["mode"] == "qemu": @@ -601,7 +613,7 @@ def main() -> int: available = ", ".join(sorted(CASE_HANDLERS.keys())) raise SystemExit(f"unknown case '{case_name}', available: {available}") - if case_name == "zone0_start": + if case_name in ("phytium_deploy_zone1", "zone0_start"): rc = case_fn(cfg, None) if rc != 0: return rc From c000359f4df38f648143a21ff2b72fadcee7752b Mon Sep 17 00:00:00 2001 From: w101yyds <1022377521@qq.com> Date: Fri, 31 Jul 2026 17:53:49 +0800 Subject: [PATCH 43/53] both boards passed local test --- jenkins/check_serial.sh | 1 + jenkins/ci.yaml | 2 +- jenkins/ci_runner.py | 70 +++++++++++++++++++++++++++++------------ jenkins/terminal.py | 21 ++++++++----- 4 files changed, 66 insertions(+), 28 deletions(-) diff --git a/jenkins/check_serial.sh b/jenkins/check_serial.sh index 3cb9911fa..fb2cd6316 100755 --- a/jenkins/check_serial.sh +++ b/jenkins/check_serial.sh @@ -27,6 +27,7 @@ has_console_ready() { deadline=$(( $(date +%s) + prompt_timeout )) while [ "$(date +%s)" -lt "$deadline" ]; do + printf '\r\n' > "$pts_dev" 2>/dev/null || true read_pts 2 if has_console_ready; then break diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index cb9b8b41b..15faa51de 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -66,7 +66,7 @@ bids: - platform/aarch64/phytium-pi/image/dts/phytium-pi-board-v2.dtb zone0_image: /home/light/DEMO/phytium-pi-external/Image uboot_ready_pattern: 'Phytium-Pi#' - uboot_cmd: setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.178; setenv loadaddr 0x90100000; setenv fdt_addr 0x90000000; setenv zone0_kernel_addr 0xa0400000; setenv zone0_fdt_addr 0xa0000000; tftp ${loadaddr} ${serverip}:phytium-pi/hvisor.bin && tftp ${fdt_addr} ${serverip}:phytium-pi/phytium-pi-board-v2.dtb && tftp ${zone0_kernel_addr} ${serverip}:phytium-pi/Image && tftp ${zone0_fdt_addr} ${serverip}:phytium-pi/linux1.dtb && bootm ${loadaddr} - ${fdt_addr}; + uboot_cmd: run boot_root_linux board_ip: 192.168.1.178 board_iface: eth0 board_user: user diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 31c81e03a..a2e3f6c98 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -4,6 +4,7 @@ import argparse import os import re +import shlex import shutil import socket import signal @@ -208,7 +209,7 @@ def board_login_if_needed( if not login_ready: raise TerminalTimeoutError("timed out waiting for Phytium-Pi login prompt") term.send(cfg["board_user"]) - if not term.wait_pattern(r"Password:", timeout=20.0): + if not term.wait_pattern(r"(?:Password|密码)[::]", timeout=20.0): raise TerminalTimeoutError("timed out waiting for Phytium-Pi password prompt") term.send(cfg["board_pass"]) if not term.wait_pattern(r"[$#]\s", timeout=30.0): @@ -252,6 +253,20 @@ def run_host_command(cmd: list[str], timeout: float) -> None: subprocess.run(cmd, check=True, timeout=timeout) +def run_host_command_with_pty(cmd: list[str], timeout: float) -> None: + shell_cmd = shlex.join(cmd) + print("+ " + shell_cmd, flush=True) + subprocess.run(["script", "-qfec", shell_cmd, "/dev/null"], check=True, timeout=timeout) + + +def describe_staged_files(src: Path) -> list[Path]: + files = sorted(p for p in src.iterdir() if p.is_file()) + print(f"[scp] staging dir: {src}", flush=True) + for path in files: + print(f"[scp] {path.name} ({path.stat().st_size} bytes)", flush=True) + return files + + def phytium_deploy_zone1_with_terminal(cfg: dict[str, Any], term: Terminal) -> int: board_login_if_needed(cfg, term, wake=False) term.run( @@ -260,16 +275,23 @@ def phytium_deploy_zone1_with_terminal(cfg: dict[str, Any], term: Terminal) -> i timeout=30.0, ) stage_board_files(cfg) + src = Path(cfg["network_staging_dir"]) + staged_files = describe_staged_files(src) + remote_clean = ( f"mkdir -p '{cfg['scp_dst']}' && " f"find '{cfg['scp_dst']}' -mindepth 1 -maxdepth 1 -exec rm -rf {{}} +" ) + print(f"[scp] cleaning {cfg['board_user']}@{cfg['board_ip']}:{cfg['scp_dst']}", flush=True) run_host_command([*host_ssh_prefix(cfg), remote_clean], timeout=60.0) - src = Path(cfg["network_staging_dir"]) - run_host_command( - [*host_scp_prefix(cfg), *[str(p) for p in sorted(src.iterdir())], f"{cfg['board_user']}@{cfg['board_ip']}:{cfg['scp_dst']}/"], + print(f"[scp] copying {len(staged_files)} files to {cfg['board_ip']}:{cfg['scp_dst']}", flush=True) + run_host_command_with_pty( + [*host_scp_prefix(cfg), *[str(p) for p in staged_files], f"{cfg['board_user']}@{cfg['board_ip']}:{cfg['scp_dst']}/"], timeout=300.0, ) + print(f"[scp] syncing remote files on {cfg['board_ip']} before power cycle", flush=True) + run_host_command([*host_ssh_prefix(cfg), "sync; sleep 5; sync"], timeout=120.0) + print("[scp] remote sync completed", flush=True) print("phytium zone1 files deployed successfully", flush=True) return 0 @@ -430,15 +452,13 @@ def network(cfg: dict[str, Any], term: Terminal | None) -> int: host_user = cfg["network_host_user"] staging_dir = cfg["network_staging_dir"] - term.run( - "network_scp_env", - f"export CI_H={host_ip} CI_U={host_user} CI_D={staging_dir}", - timeout=30.0, - ) + term.run("network_scp_host", f"CI_H={host_ip}", timeout=15.0) + term.run("network_scp_user", f"CI_U={host_user}", timeout=15.0) + term.run("network_scp_dir", f"CI_D={staging_dir}", timeout=15.0) # Keep the scp line short: serial consoles truncate long commands. scp_cmd = "scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $CI_U@$CI_H:$CI_D/* /root/" print(f"[network] pulling staged files from {host_user}@{host_ip}:{staging_dir}", flush=True) - pull_rc, _ = term.run("network_scp_pull", scp_cmd, timeout=300.0) + pull_rc, _ = term.run("network_scp_pull", scp_cmd, timeout=900.0) if pull_rc != 0: raise TerminalCommandError(f"board scp pull failed with rc={pull_rc}") @@ -456,21 +476,31 @@ def phytium_start_zone1(cfg: dict[str, Any], term: Terminal | None) -> int: if term is None: raise SystemExit("terminal backend is required") + print("[phytium] sending zone0 su", flush=True) + term.send("su") + if not term.wait_pattern(r"root@\(none\):[^\r\n]*#\s", timeout=30.0): + raise TerminalTimeoutError("timed out waiting for Phytium-Pi zone0 root prompt") + term.run("phytium_zone1_cd", f"cd {cfg['scp_dst']}", timeout=15.0) term.run("phytium_zone1_chmod", "chmod +x start.sh hvisor 2>/dev/null || true", timeout=30.0) - term.run("phytium_zone1_start", "./start.sh", timeout=120.0) - time.sleep(float(cfg["zone1_start_wait"])) - - term.send("script -q /dev/null") - if not term.wait_pattern(r"[$#]\s", timeout=15.0): - raise TerminalTimeoutError("timed out waiting for shell after starting script") - + term.send("./start.sh") + print("[phytium] start.sh sent, wait 20s before opening zone1 console", flush=True) + time.sleep(20.0) + + print("[phytium] sending enter before script", flush=True) + term.send("") + time.sleep(1.0) + print("[phytium] sending: script /dev/null", flush=True) + term.send("script /dev/null") + time.sleep(2.0) + print("[phytium] sending: screen /dev/pts/0", flush=True) term.send("screen /dev/pts/0") - if not term.wait_pattern(r"(?:login:|[$#]\s)", timeout=60.0): - raise TerminalTimeoutError("timed out waiting for Phytium-Pi zone1 console") + time.sleep(3.0) + print("[phytium] sending zone1 su", flush=True) + su_offset = term.offset() term.send("su") - if not term.wait_pattern(r"root@\(none\):/#\s", timeout=30.0): + if not term.wait_pattern(r"root@\(none\):[^\r\n]*#\s", timeout=30.0, from_offset=su_offset): raise TerminalTimeoutError("timed out waiting for Phytium-Pi zone1 root prompt") print("phytium-pi zone1 root console check passed", flush=True) diff --git a/jenkins/terminal.py b/jenkins/terminal.py index b31ee4f88..f94c286c5 100644 --- a/jenkins/terminal.py +++ b/jenkins/terminal.py @@ -16,9 +16,7 @@ import serial -RUN_RESULT_RE = re.compile( - r"__CI_RUN_RESULT__ case=(?P[^\s]+) run_id=(?P[a-f0-9]+) rc=(?P\d+)" -) +RUN_RESULT_RE = re.compile(r"__R__(?P[a-f0-9]+):(?P\d+)") class TerminalTimeoutError(TimeoutError): @@ -311,6 +309,10 @@ def flush_input(self) -> None: self._ensure_open() self.backend.flush_input() + def offset(self) -> int: + self._ensure_open() + return self._collector.offset() + def send(self, command: str) -> None: self._ensure_open() payload = command.rstrip("\n") + "\n" @@ -322,16 +324,18 @@ def run( command: str, timeout: float = 30.0, poll_interval: float = 0.05, + wake_interval: float | None = None, ) -> tuple[int, str]: - """Run a shell command and wait for __CI_RUN_RESULT__ in the log.""" + """Run a shell command and wait for the compact result marker in the log.""" self._ensure_open() - run_id = uuid.uuid4().hex + run_id = uuid.uuid4().hex[:8] offset = self._collector.offset() - wrapped = f"{command}; echo __CI_RUN_RESULT__ case={case} run_id={run_id} rc=$?" + wrapped = f"{command}; echo __R__{run_id}:$?" self.send(wrapped) deadline = time.monotonic() + timeout - run_id_needle = f"run_id={run_id}" + next_wake = time.monotonic() + wake_interval if wake_interval else None + run_id_needle = f"__R__{run_id}:" while time.monotonic() < deadline: chunk = self._collector.tail_since(offset) if run_id_needle in chunk: @@ -341,6 +345,9 @@ def run( rc = int(match.group("rc")) output = chunk[: match.start()] return rc, output + if next_wake is not None and time.monotonic() >= next_wake: + self.send("") + next_wake = time.monotonic() + wake_interval time.sleep(poll_interval) raise TerminalTimeoutError( From 8819a30d88194e3d4e3f5cc0ffef6fb62a7b56f3 Mon Sep 17 00:00:00 2001 From: ray Date: Tue, 4 Aug 2026 23:46:19 +0800 Subject: [PATCH 44/53] remove its mem in mem region and add power down after test --- jenkins/ci_runner.py | 20 +++++++++++++++----- platform/aarch64/rk3568/board.rs | 12 ++++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index a2e3f6c98..c48a9e7f5 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -122,22 +122,30 @@ def board_power_script(cfg: dict[str, Any]) -> Path: return cfg["workspace"] / "jenkins" / "board_power.sh" -def board_power_cycle(cfg: dict[str, Any]) -> None: +def board_power(cfg: dict[str, Any], action: str) -> None: power_port = str(cfg.get("power_serial", "")).strip() if not power_port: - print("[power] skip cycle: power_serial is empty", flush=True) + print(f"[power] skip {action}: power_serial is empty", flush=True) return script = board_power_script(cfg) if not script.is_file(): raise SystemExit(f"board power script not found: {script}") power_channel = str(cfg.get("power_channel", 4)) - print(f"[power] cycle port={power_port} channel={power_channel}", flush=True) + print(f"[power] {action} port={power_port} channel={power_channel}", flush=True) subprocess.run( - ["bash", str(script), "cycle", power_port, power_channel], + ["bash", str(script), action, power_port, power_channel], check=True, cwd=cfg["workspace"], ) - print("[power] cycle completed", flush=True) + print(f"[power] {action} completed", flush=True) + + +def board_power_cycle(cfg: dict[str, Any]) -> None: + board_power(cfg, "cycle") + + +def board_power_off(cfg: dict[str, Any]) -> None: + board_power(cfg, "off") def board_wake_console(term: Terminal, *, repeats: int = 3) -> None: @@ -675,6 +683,8 @@ def main() -> int: finally: close_active_terminal(cfg) terminate_managed_process(cfg) + if cfg.get("mode") == "board": + board_power_off(cfg) if __name__ == "__main__": diff --git a/platform/aarch64/rk3568/board.rs b/platform/aarch64/rk3568/board.rs index cc3b54742..2c76f10e1 100644 --- a/platform/aarch64/rk3568/board.rs +++ b/platform/aarch64/rk3568/board.rs @@ -204,12 +204,12 @@ pub const ROOT_ZONE_MEMORY_REGIONS: &[HvConfigMemoryRegion] = &[ // virtual_start: 0x10f000, // size: 0x1000, // }, //scmi-shmem - HvConfigMemoryRegion { - mem_type: MEM_TYPE_RAM, - physical_start: 0xfd440000, - virtual_start: 0xfd440000, - size: 0x20000, - }, // its + // HvConfigMemoryRegion { + // mem_type: MEM_TYPE_RAM, + // physical_start: 0xfd440000, + // virtual_start: 0xfd440000, + // size: 0x20000, + // }, // its HvConfigMemoryRegion { mem_type: MEM_TYPE_RAM, physical_start: 0x1f0000000, From adb72b2cc394a61d89bc44fc085843381189114a Mon Sep 17 00:00:00 2001 From: dallas Date: Mon, 10 Aug 2026 21:31:50 +0800 Subject: [PATCH 45/53] refactor the ci of phytium-pi --- jenkins/board_scp.sh | 45 +-- jenkins/ci.yaml | 32 +- jenkins/ci_runner.py | 367 +++++++----------- .../aarch64/phytium-pi/scripts/boot_zone1.sh | 10 + platform/aarch64/rk3568/board.rs | 16 +- platform/aarch64/rk3568/scripts/boot_zone1.sh | 10 +- 6 files changed, 206 insertions(+), 274 deletions(-) create mode 100755 platform/aarch64/phytium-pi/scripts/boot_zone1.sh diff --git a/jenkins/board_scp.sh b/jenkins/board_scp.sh index 3953b506d..ecf37ec7e 100755 --- a/jenkins/board_scp.sh +++ b/jenkins/board_scp.sh @@ -1,7 +1,5 @@ #!/bin/sh # Stage zone1 test artifacts on the CI host for board pull via scp. -# Zone0 boot Image, Image and rootfs2.ext4 are large and -# persistent on the board; do not re-stage them every CI run. set -eux @@ -11,6 +9,7 @@ WORKSPACE_ROOT=${WORKSPACE_ROOT:-$(pwd)} HVISOR_TOOL_PATH=${HVISOR_TOOL_PATH:-${WORKSPACE_ROOT}/hvisor-tool} MODE=${MODE:-release} STAGING_DIR=${STAGING_DIR:-/home/light/tftp/ci_deploy} +TFTP_DIR=${TFTP_DIR:-} case "${HVISOR_TOOL_PATH}" in /*) ;; @@ -23,8 +22,6 @@ IMAGE_DIR="${PLATFORM_DIR}/image" SCRIPTS_DIR="${PLATFORM_DIR}/scripts" ZONE1_BOOT_SCRIPT="${SCRIPTS_DIR}/boot_zone1.sh" CHECK_SERIAL_SCRIPT="${WORKSPACE_ROOT}/jenkins/check_serial.sh" -EXTERNAL_DIR=${EXTERNAL_DIR:-} -START_SCRIPT=${START_SCRIPT:-} COPY_HVISOR_BIN=${COPY_HVISOR_BIN:-true} case "${ARCH}" in @@ -40,14 +37,16 @@ esac BUILD_PATH="${WORKSPACE_ROOT}/target/${RUSTC_TARGET}/${MODE}" HVISOR_BIN="${BUILD_PATH}/hvisor.bin" -if [ -n "${ZONE1_DTB:-}" ]; then - : -elif [ -f "${IMAGE_DIR}/dts/rk3568_limit_zone1.dtb" ]; then - ZONE1_DTB="${IMAGE_DIR}/dts/rk3568_limit_zone1.dtb" -elif [ -f "${IMAGE_DIR}/dts/zone1-linux.dtb" ]; then - ZONE1_DTB="${IMAGE_DIR}/dts/zone1-linux.dtb" -else - ZONE1_DTB="${IMAGE_DIR}/dts/zone1-linux.dtb" +if [ -z "${ZONE1_DTB:-}" ]; then + for candidate in \ + "${IMAGE_DIR}/dts/zone1-linux.dtb" \ + "${IMAGE_DIR}/dts/linux2.dtb"; do + if [ -f "${candidate}" ]; then + ZONE1_DTB="${candidate}" + break + fi + done + ZONE1_DTB=${ZONE1_DTB:-${IMAGE_DIR}/dts/zone1-linux.dtb} fi echo "ARCH: ${ARCH}" @@ -67,15 +66,6 @@ if [ "${COPY_HVISOR_BIN}" != "false" ] && [ ! -f "${HVISOR_BIN}" ]; then echo "error: hvisor.bin not found: ${HVISOR_BIN}" exit 1 fi -if [ -n "${START_SCRIPT}" ]; then - ZONE1_BOOT_SCRIPT="${START_SCRIPT}" - ZONE1_SCRIPT_DEST="start.sh" -elif [ -n "${EXTERNAL_DIR}" ] && [ -f "${EXTERNAL_DIR}/start.sh" ]; then - ZONE1_BOOT_SCRIPT="${EXTERNAL_DIR}/start.sh" - ZONE1_SCRIPT_DEST="start.sh" -else - ZONE1_SCRIPT_DEST="$(basename "${ZONE1_BOOT_SCRIPT}")" -fi if [ ! -f "${ZONE1_BOOT_SCRIPT}" ]; then echo "error: zone1 start script not found: ${ZONE1_BOOT_SCRIPT}" exit 1 @@ -94,7 +84,7 @@ if [ "${COPY_HVISOR_BIN}" != "false" ]; then cp "${HVISOR_BIN}" "${STAGING_DIR}/" fi cp "${CONFIGS_DIR}/"* "${STAGING_DIR}/" -cp "${ZONE1_BOOT_SCRIPT}" "${STAGING_DIR}/${ZONE1_SCRIPT_DEST}" +cp "${ZONE1_BOOT_SCRIPT}" "${STAGING_DIR}/" if [ -f "${ZONE1_DTB}" ]; then cp "${ZONE1_DTB}" "${STAGING_DIR}/" @@ -107,17 +97,14 @@ if [ -f "${CHECK_SERIAL_SCRIPT}" ]; then fi ZONE1_ASSETS_DIR=${ZONE1_ASSETS_DIR:-/home/light/tftp/zone1_assets} -if [ -n "${EXTERNAL_DIR}" ] && [ -f "${EXTERNAL_DIR}/Image" ]; then - cp "${EXTERNAL_DIR}/Image" "${STAGING_DIR}/" +if [ -n "${TFTP_DIR}" ] && [ -f "${TFTP_DIR}/Image" ]; then + cp "${TFTP_DIR}/Image" "${STAGING_DIR}/" elif [ -f "${ZONE1_ASSETS_DIR}/Image" ]; then cp "${ZONE1_ASSETS_DIR}/Image" "${STAGING_DIR}/" fi -if [ -n "${EXTERNAL_DIR}" ] && [ -f "${EXTERNAL_DIR}/rootfs2.ext4" ]; then - cp "${EXTERNAL_DIR}/rootfs2.ext4" "${STAGING_DIR}/" -fi -# rk3568 rootfs2.ext4 is large and persistent on the board; do not re-stage/pull every run. +# rootfs2.ext4 is large and persistent on the board; do not stage in CI. chmod -R a+rX "${STAGING_DIR}" -chmod +x "${STAGING_DIR}/${ZONE1_SCRIPT_DEST}" +chmod +x "${STAGING_DIR}/boot_zone1.sh" echo "board staging completed: ${STAGING_DIR}" ls -la "${STAGING_DIR}" diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index 15faa51de..dccd9b481 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -32,10 +32,16 @@ bids: zone0_dtbs: - platform/aarch64/rk3568/image/dts/rk3568_limit_zone0.dtb uboot_ready_pattern: '=>' + zone0_shell_timeout: 60 uboot_cmd: pci enum;setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.240; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:rk3568/hvisor.bin && tftp ${fdt_addr} ${serverip}:rk3568/rk3568_limit_zone0.dtb && tftp ${zone0_kernel_addr} ${serverip}:rk3568/Image && bootm ${loadaddr} - ${fdt_addr}; - network: + deploy: + board_ip: 192.168.1.240 + board_iface: eth0 + link_wait: 3 + ping_retries: 3 host_ip: 192.168.1.181 host_user: light + board_ssh_key: /home/light/.ssh/hvisor_board_pull staging_dir: /home/light/tftp/ci_deploy zone1_dtb: platform/aarch64/rk3568/image/dts/rk3568_limit_zone1.dtb lspci: @@ -47,7 +53,7 @@ bids: min_count: 4 cases: - zone0_start - - network + - deploy - lspci - zone1_start @@ -66,24 +72,22 @@ bids: - platform/aarch64/phytium-pi/image/dts/phytium-pi-board-v2.dtb zone0_image: /home/light/DEMO/phytium-pi-external/Image uboot_ready_pattern: 'Phytium-Pi#' + zone0_shell_timeout: 60 uboot_cmd: run boot_root_linux - board_ip: 192.168.1.178 - board_iface: eth0 - board_user: user - board_pass: user - scp_dst: /home/user/zone1 - network: + deploy: + board_ip: 192.168.1.178 + board_iface: eth0 + link_wait: 3 + ping_retries: 3 host_ip: 192.168.1.181 host_user: light - staging_dir: /home/light/DEMO/tmp + board_ssh_key: /home/light/.ssh/hvisor_board_pull + staging_dir: /home/light/tftp/ci_deploy zone1_dtb: platform/aarch64/phytium-pi/image/dts/linux2.dtb - external_dir: /home/light/DEMO/phytium-pi-external - start_script: /home/light/DEMO/phytium-pi-external/start.sh - copy_hvisor_bin: false cases: - - phytium_deploy_zone1 - zone0_start - - phytium_start_zone1 + - deploy + - zone1_start - bid: x86_64/qemu diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index c48a9e7f5..807c1a994 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -2,10 +2,9 @@ from __future__ import annotations import argparse +import base64 import os import re -import shlex -import shutil import socket import signal import subprocess @@ -185,135 +184,102 @@ def stage_board_files(cfg: dict[str, Any]) -> None: env["WORKSPACE_ROOT"] = str(cfg["workspace"]) env["HVISOR_TOOL_PATH"] = cfg["hvisor_tool_path"] env["STAGING_DIR"] = cfg["network_staging_dir"] + env["TFTP_DIR"] = cfg["tftp_dir"] if cfg.get("zone1_dtb"): zone1_dtb = Path(cfg["zone1_dtb"]) if not zone1_dtb.is_absolute(): zone1_dtb = cfg["workspace"] / zone1_dtb env["ZONE1_DTB"] = str(zone1_dtb.resolve()) - if cfg.get("external_dir"): - env["EXTERNAL_DIR"] = cfg["external_dir"] - if cfg.get("start_script"): - env["START_SCRIPT"] = cfg["start_script"] env["COPY_HVISOR_BIN"] = "true" if cfg.get("copy_hvisor_bin", True) else "false" subprocess.run(["bash", str(script)], check=True, cwd=cfg["workspace"], env=env) -def board_login_if_needed( - cfg: dict[str, Any], - term: Terminal, - timeout: float = 120.0, - *, - wake: bool = True, -) -> None: - if wake: - term.send("") - if term.wait_pattern(r"[$#]\s", timeout=5.0): - return - if wake: - login_ready = term.wait_pattern(r"Phytium-Pi login:", timeout=timeout, from_offset=0) - else: - login_ready = term.wait_pattern(r"Phytium-Pi login:", timeout=timeout) - if not login_ready: - raise TerminalTimeoutError("timed out waiting for Phytium-Pi login prompt") - term.send(cfg["board_user"]) - if not term.wait_pattern(r"(?:Password|密码)[::]", timeout=20.0): - raise TerminalTimeoutError("timed out waiting for Phytium-Pi password prompt") - term.send(cfg["board_pass"]) - if not term.wait_pattern(r"[$#]\s", timeout=30.0): - raise TerminalTimeoutError("timed out waiting for Phytium-Pi shell prompt") - - -def host_ssh_prefix(cfg: dict[str, Any]) -> list[str]: - target = f"{cfg['board_user']}@{cfg['board_ip']}" - base = [ - "ssh", - "-o", - "StrictHostKeyChecking=no", - "-o", - "UserKnownHostsFile=/dev/null", - target, - ] - if cfg["board_pass"]: - if shutil.which("sshpass") is None: - raise SystemExit("sshpass is required for password-based board ssh/scp") - return ["sshpass", "-p", cfg["board_pass"], *base] - return base - - -def host_scp_prefix(cfg: dict[str, Any]) -> list[str]: - base = [ - "scp", - "-o", - "StrictHostKeyChecking=no", - "-o", - "UserKnownHostsFile=/dev/null", - ] - if cfg["board_pass"]: - if shutil.which("sshpass") is None: - raise SystemExit("sshpass is required for password-based board ssh/scp") - return ["sshpass", "-p", cfg["board_pass"], *base] - return base - - -def run_host_command(cmd: list[str], timeout: float) -> None: - print("+ " + " ".join(cmd), flush=True) - subprocess.run(cmd, check=True, timeout=timeout) - - -def run_host_command_with_pty(cmd: list[str], timeout: float) -> None: - shell_cmd = shlex.join(cmd) - print("+ " + shell_cmd, flush=True) - subprocess.run(["script", "-qfec", shell_cmd, "/dev/null"], check=True, timeout=timeout) - - -def describe_staged_files(src: Path) -> list[Path]: - files = sorted(p for p in src.iterdir() if p.is_file()) - print(f"[scp] staging dir: {src}", flush=True) - for path in files: - print(f"[scp] {path.name} ({path.stat().st_size} bytes)", flush=True) - return files - - -def phytium_deploy_zone1_with_terminal(cfg: dict[str, Any], term: Terminal) -> int: - board_login_if_needed(cfg, term, wake=False) +def deploy_chmod(cfg: dict[str, Any], term: Terminal) -> None: + work_dir = cfg["zone1_work_dir"] + boot_script = cfg["zone1_boot_script"] term.run( - "phytium_set_ip", - f"echo {cfg['board_pass']} | sudo -S ifconfig {cfg['board_iface']} {cfg['board_ip']}", - timeout=30.0, + "deploy_chmod", + f"chmod +x {work_dir}/{boot_script} {work_dir}/check_serial.sh 2>/dev/null || true", + timeout=60.0, ) + + +def deploy_board_pull(cfg: dict[str, Any], term: Terminal) -> int: + """Board pulls staged zone1 files from the CI host via scp (zone0 serial console).""" + board_ip = cfg.get("board_ip") + board_iface = cfg.get("board_iface") + if board_ip and board_iface: + term.run( + "deploy_set_ip", + f"ifconfig {board_iface} {board_ip} netmask 255.255.255.0 up", + timeout=30.0, + ) + link_wait = float(cfg.get("deploy_link_wait", 0.0)) + if link_wait > 0: + print( + f"[deploy] waiting {link_wait}s for {board_iface} link on {board_ip}", + flush=True, + ) + time.sleep(link_wait) + + host_ip = cfg["network_host_ip"] + ping_count = cfg["network_ping_count"] + ping_retries = int(cfg.get("deploy_ping_retries", 1)) + ping_output = "" + for attempt in range(ping_retries): + if attempt > 0: + retry_wait = float(cfg.get("deploy_link_wait", 5.0)) or 5.0 + print(f"[deploy] ping retry {attempt + 1}/{ping_retries} after {retry_wait}s", flush=True) + time.sleep(retry_wait) + _, ping_output = term.run( + "deploy_ping", + f"ping -c {ping_count} -W 5 {host_ip}", + timeout=30.0 + ping_count * 5.0, + ) + if ping_success(ping_output): + break + save_lspci_artifacts(cfg, "deploy_ping.log", f"=== ping {host_ip} ===\n{ping_output}\n") + + if not ping_success(ping_output): + raise TerminalCommandError(f"ping {host_ip} failed") + + print(f"[deploy] ping {host_ip} ok, staging files on host", flush=True) stage_board_files(cfg) - src = Path(cfg["network_staging_dir"]) - staged_files = describe_staged_files(src) + time.sleep(3.0) - remote_clean = ( - f"mkdir -p '{cfg['scp_dst']}' && " - f"find '{cfg['scp_dst']}' -mindepth 1 -maxdepth 1 -exec rm -rf {{}} +" - ) - print(f"[scp] cleaning {cfg['board_user']}@{cfg['board_ip']}:{cfg['scp_dst']}", flush=True) - run_host_command([*host_ssh_prefix(cfg), remote_clean], timeout=60.0) - print(f"[scp] copying {len(staged_files)} files to {cfg['board_ip']}:{cfg['scp_dst']}", flush=True) - run_host_command_with_pty( - [*host_scp_prefix(cfg), *[str(p) for p in staged_files], f"{cfg['board_user']}@{cfg['board_ip']}:{cfg['scp_dst']}/"], - timeout=300.0, + board_key = install_board_ssh_key(cfg, term) + identity = board_scp_identity(cfg, board_key) + + host_user = cfg["network_host_user"] + staging_dir = cfg["network_staging_dir"] + work_dir = cfg["zone1_work_dir"] + term.run("deploy_scp_host", f"CI_H={host_ip}", timeout=15.0) + term.run("deploy_scp_user", f"CI_U={host_user}", timeout=15.0) + term.run("deploy_scp_dir", f"CI_D={staging_dir}", timeout=15.0) + scp_cmd = ( + f"scp {identity}-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null " + f"$CI_U@$CI_H:$CI_D/* {work_dir}/" ) - print(f"[scp] syncing remote files on {cfg['board_ip']} before power cycle", flush=True) - run_host_command([*host_ssh_prefix(cfg), "sync; sleep 5; sync"], timeout=120.0) - print("[scp] remote sync completed", flush=True) - print("phytium zone1 files deployed successfully", flush=True) + print(f"[deploy] pulling staged files from {host_user}@{host_ip}:{staging_dir}", flush=True) + pull_rc, _ = term.run("deploy_scp_pull", scp_cmd, timeout=900.0) + if pull_rc != 0: + raise TerminalCommandError(f"board scp pull failed with rc={pull_rc}") + + deploy_chmod(cfg, term) + print("[deploy] board_pull completed", flush=True) return 0 -def phytium_deploy_zone1(cfg: dict[str, Any], term: Terminal | None) -> int: - print("————————————————\ncase: phytium_deploy_zone1\n————————————————\n", flush=True) - board_power_cycle(cfg) - if term is not None: - return phytium_deploy_zone1_with_terminal(cfg, term) +def deploy(cfg: dict[str, Any], term: Terminal | None) -> int: + print("————————————————\ncase: deploy\n————————————————\n", flush=True) + if cfg["mode"] != "board": + print("[deploy] skipped (not board mode)", flush=True) + return 0 - log_path = logs_dir(cfg) / "zone1_console.log" - log_path.write_text("", encoding="utf-8") - with build_terminal(cfg, log_path) as board_term: - return phytium_deploy_zone1_with_terminal(cfg, board_term) + if term is None: + raise SystemExit("terminal backend is required (run zone0_start first)") + return deploy_board_pull(cfg, term) def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: @@ -351,7 +317,7 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: cfg["_board_term"] = board_term board_power_cycle(cfg) time.sleep(3.0) - board_wake_console(board_term, repeats=8 if cfg["board"] == "phytium-pi" else 3) + board_wake_console(board_term) uboot_cmd = cfg.get("uboot_cmd", "") uboot_ready = cfg.get("uboot_ready_pattern", "") @@ -361,7 +327,8 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: board_wait_uboot_prompt(board_term, uboot_ready, timeout=float(cfg["uboot_prompt_timeout"])) time.sleep(0.3) board_term.send(uboot_cmd) - if not board_term.wait_pattern(ZONE0_READY_PATTERN, timeout=180.0): + zone0_timeout = float(cfg.get("zone0_shell_timeout", 180.0)) + if not board_term.wait_pattern(ZONE0_READY_PATTERN, timeout=zone0_timeout): raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") return 0 return 0 @@ -432,87 +399,30 @@ def ping_success(output: str) -> bool: return bool(match and int(match.group(1)) > 0) -def network(cfg: dict[str, Any], term: Terminal | None) -> int: - print("————————————————\ncase: network\n————————————————\n", flush=True) - if cfg["mode"] != "board": - print("[network] skipped (not board mode)", flush=True) - return 0 - if term is None: - raise SystemExit("terminal backend is required (run zone0_start first)") - - host_ip = cfg["network_host_ip"] - ping_count = cfg["network_ping_count"] - _, output = term.run( - "network_ping", - f"ping -c {ping_count} -W 5 {host_ip}", - timeout=30.0, +def install_board_ssh_key(cfg: dict[str, Any], term: Terminal) -> str | None: + """Install a host-side private key on the board for scp pull. Returns key path on board.""" + key_path = cfg.get("deploy_board_ssh_key", "").strip() + if not key_path: + return None + host_key = Path(key_path).expanduser() + if not host_key.is_file(): + raise SystemExit(f"deploy board_ssh_key not found: {host_key}") + + board_key = f"/root/.ssh/{host_key.name}" + key_b64 = base64.b64encode(host_key.read_bytes()).decode("ascii") + cmd = ( + "mkdir -p /root/.ssh && chmod 700 /root/.ssh && " + f"echo {key_b64} | base64 -d > {board_key} && chmod 600 {board_key}" ) - save_lspci_artifacts(cfg, "network_ping.log", f"=== ping {host_ip} ===\n{output}\n") - - if not ping_success(output): - raise TerminalCommandError(f"ping {host_ip} failed") + term.run("deploy_install_ssh_key", cmd, timeout=30.0) + print(f"[deploy] installed board ssh key at {board_key}", flush=True) + return board_key - print(f"[network] ping {host_ip} ok, staging files on host", flush=True) - stage_board_files(cfg) - # Let host staging finish and drain any buffered serial output before scp. - time.sleep(3.0) - - host_user = cfg["network_host_user"] - staging_dir = cfg["network_staging_dir"] - term.run("network_scp_host", f"CI_H={host_ip}", timeout=15.0) - term.run("network_scp_user", f"CI_U={host_user}", timeout=15.0) - term.run("network_scp_dir", f"CI_D={staging_dir}", timeout=15.0) - # Keep the scp line short: serial consoles truncate long commands. - scp_cmd = "scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $CI_U@$CI_H:$CI_D/* /root/" - print(f"[network] pulling staged files from {host_user}@{host_ip}:{staging_dir}", flush=True) - pull_rc, _ = term.run("network_scp_pull", scp_cmd, timeout=900.0) - if pull_rc != 0: - raise TerminalCommandError(f"board scp pull failed with rc={pull_rc}") - - term.run( - "network_chmod", - "chmod +x /root/boot_zone1.sh /root/check_serial.sh 2>/dev/null || true", - timeout=60.0, - ) - print("network test and file deploy passed", flush=True) - return 0 - - -def phytium_start_zone1(cfg: dict[str, Any], term: Terminal | None) -> int: - print("————————————————\ncase: phytium_start_zone1\n————————————————\n", flush=True) - if term is None: - raise SystemExit("terminal backend is required") - - print("[phytium] sending zone0 su", flush=True) - term.send("su") - if not term.wait_pattern(r"root@\(none\):[^\r\n]*#\s", timeout=30.0): - raise TerminalTimeoutError("timed out waiting for Phytium-Pi zone0 root prompt") - - term.run("phytium_zone1_cd", f"cd {cfg['scp_dst']}", timeout=15.0) - term.run("phytium_zone1_chmod", "chmod +x start.sh hvisor 2>/dev/null || true", timeout=30.0) - term.send("./start.sh") - print("[phytium] start.sh sent, wait 20s before opening zone1 console", flush=True) - time.sleep(20.0) - - print("[phytium] sending enter before script", flush=True) - term.send("") - time.sleep(1.0) - print("[phytium] sending: script /dev/null", flush=True) - term.send("script /dev/null") - time.sleep(2.0) - print("[phytium] sending: screen /dev/pts/0", flush=True) - term.send("screen /dev/pts/0") - time.sleep(3.0) - - print("[phytium] sending zone1 su", flush=True) - su_offset = term.offset() - term.send("su") - if not term.wait_pattern(r"root@\(none\):[^\r\n]*#\s", timeout=30.0, from_offset=su_offset): - raise TerminalTimeoutError("timed out waiting for Phytium-Pi zone1 root prompt") - - print("phytium-pi zone1 root console check passed", flush=True) - return 0 +def board_scp_identity(cfg: dict[str, Any], board_key: str | None) -> str: + if board_key: + return f"-i {board_key} " + return "" def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: @@ -520,13 +430,25 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: if term is None: raise SystemExit("terminal backend is required") + work_dir = cfg["zone1_work_dir"] + boot_script = cfg["zone1_boot_script"] inner_log = "/tmp/zone1_inner.log" - _, _ = term.run("zone1_cd", "cd /root", timeout=15.0) + _, _ = term.run("zone1_cd", f"cd {work_dir}", timeout=15.0) _, _ = term.run("zone1_ls", "ls", timeout=15.0) - _, _ = term.run("zone1_cat_boot", "cat boot_zone1.sh", timeout=15.0) - boot_rc, _ = term.run("zone1_boot", "./boot_zone1.sh", timeout=120.0) + _, _ = term.run( + "zone1_chmod", + f"chmod +x {boot_script} hvisor check_serial.sh 2>/dev/null || true", + timeout=30.0, + ) + _, _ = term.run("zone1_cat_boot", f"cat {boot_script}", timeout=15.0) + boot_rc, _ = term.run("zone1_boot", f"./{boot_script}", timeout=120.0) _, _ = term.run("zone1_list", "./hvisor zone list", timeout=15.0) + start_wait = float(cfg.get("zone1_start_wait", 0.0)) + if start_wait > 0: + print(f"[zone1] waiting {start_wait}s for zone1 console", flush=True) + time.sleep(start_wait) + max_pts = find_zone1_pts(term) check_rc, _ = term.run( @@ -543,7 +465,7 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: save_inner_serial_log(cfg, inner_output) if boot_rc != 0: - raise TerminalCommandError(f"command failed with rc={boot_rc}: ./boot_zone1.sh") + raise TerminalCommandError(f"command failed with rc={boot_rc}: ./{boot_script}") if check_rc != 0: raise TerminalCommandError( f"command failed with rc={check_rc}: check_serial.sh (no console prompt)" @@ -553,10 +475,8 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: CASE_HANDLERS: dict[str, CaseFunc] = { - "phytium_deploy_zone1": phytium_deploy_zone1, - "phytium_start_zone1": phytium_start_zone1, + "deploy": deploy, "zone0_start": zone0_start, - "network": network, "lspci": lspci, "zone1_start": zone1_start, } @@ -586,10 +506,19 @@ def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: cell_root = Path.cwd() build_args = bid_entry.get("build_args") or {} - network_cfg = tests.get("network") or {} - if not isinstance(network_cfg, dict): - network_cfg = {} - copy_hvisor_bin = network_cfg.get("copy_hvisor_bin", True) + deploy_cfg = tests.get("deploy") or tests.get("network") or {} + if not isinstance(deploy_cfg, dict): + deploy_cfg = {} + + def cfg_str(key: str, default: str = "") -> str: + raw = deploy_cfg.get(key, tests.get(key, default)) + return str(raw).strip() if raw is not None else default + + deploy_method = str(deploy_cfg.get("method", "")).strip() + if deploy_method and deploy_method != "board_pull": + raise SystemExit(f"unsupported deploy method '{deploy_method}', only board_pull is supported") + board_ip = cfg_str("board_ip") + copy_hvisor_bin = deploy_cfg.get("copy_hvisor_bin", True) if isinstance(copy_hvisor_bin, str): copy_hvisor_bin = copy_hvisor_bin.lower() not in ("0", "false", "no") hvisor_tool_path = os.environ.get("HVISOR_TOOL_PATH", "").strip() @@ -614,22 +543,23 @@ def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: "uboot_cmd": str(tests.get("uboot_cmd", "")).strip(), "uboot_ready_pattern": str(tests.get("uboot_ready_pattern", "")).strip(), "uboot_prompt_timeout": float(tests.get("uboot_prompt_timeout", 20.0)), + "zone0_shell_timeout": float(tests.get("zone0_shell_timeout", 300.0 if mode == "board" else 180.0)), "tftp_dir": str(tests.get("tftp_dir", "/home/light/tftp")).strip(), "lspci": tests.get("lspci") or {}, - "board_ip": str(tests.get("board_ip", "")).strip(), - "board_iface": str(tests.get("board_iface", "eth0")).strip(), - "board_user": str(tests.get("board_user", "root")).strip(), - "board_pass": str(tests.get("board_pass", "")).strip(), - "scp_dst": str(tests.get("scp_dst", "/root")).strip(), - "network_host_ip": str(network_cfg.get("host_ip", "192.168.1.181")).strip(), - "network_host_user": str(network_cfg.get("host_user", "light")).strip(), - "network_staging_dir": str( - network_cfg.get("staging_dir", "/home/light/tftp/ci_deploy") - ).strip(), - "network_ping_count": int(network_cfg.get("ping_count", 3)), - "zone1_dtb": str(network_cfg.get("zone1_dtb", "")).strip(), - "external_dir": str(network_cfg.get("external_dir", "")).strip(), - "start_script": str(network_cfg.get("start_script", "")).strip(), + "board_ip": board_ip, + "board_iface": cfg_str("board_iface", "eth0"), + "zone1_work_dir": cfg_str("zone1_work_dir", "/root"), + "zone1_boot_script": cfg_str("zone1_boot_script", "boot_zone1.sh"), + "network_host_ip": cfg_str("host_ip", "192.168.1.181"), + "network_host_user": cfg_str("host_user", "light"), + "network_staging_dir": cfg_str("staging_dir", "/home/light/tftp/ci_deploy"), + "network_ping_count": int(deploy_cfg.get("ping_count", tests.get("ping_count", 3))), + "deploy_link_wait": float(deploy_cfg.get("link_wait", 0.0)), + "deploy_ping_retries": int(deploy_cfg.get("ping_retries", 1)), + "deploy_board_ssh_key": cfg_str( + "board_ssh_key", "/home/light/.ssh/hvisor_board_pull" + ), + "zone1_dtb": cfg_str("zone1_dtb"), "copy_hvisor_bin": bool(copy_hvisor_bin), "zone1_start_wait": float(tests.get("zone1_start_wait", 30.0)), } @@ -651,7 +581,8 @@ def main() -> int: available = ", ".join(sorted(CASE_HANDLERS.keys())) raise SystemExit(f"unknown case '{case_name}', available: {available}") - if case_name in ("phytium_deploy_zone1", "zone0_start"): + standalone = case_name == "zone0_start" + if standalone: rc = case_fn(cfg, None) if rc != 0: return rc diff --git a/platform/aarch64/phytium-pi/scripts/boot_zone1.sh b/platform/aarch64/phytium-pi/scripts/boot_zone1.sh new file mode 100755 index 000000000..ab255dc74 --- /dev/null +++ b/platform/aarch64/phytium-pi/scripts/boot_zone1.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +insmod hvisor.ko 2>/dev/null || true +mount -t proc proc /proc 2>/dev/null || true +mount -t sysfs sysfs /sys 2>/dev/null || true +rm -f nohup.out +mkdir -p /dev/pts +mount -t devpts devpts /dev/pts 2>/dev/null || true +nohup ./hvisor virtio start zone1-linux-virtio.json & +./hvisor zone start zone1-linux.json diff --git a/platform/aarch64/rk3568/board.rs b/platform/aarch64/rk3568/board.rs index fa009e3d2..adaf8923e 100644 --- a/platform/aarch64/rk3568/board.rs +++ b/platform/aarch64/rk3568/board.rs @@ -207,12 +207,12 @@ pub const ROOT_ZONE_MEMORY_REGIONS: &[HvConfigMemoryRegion] = &[ // virtual_start: 0x10f000, // size: 0x1000, // }, //scmi-shmem - // HvConfigMemoryRegion { - // mem_type: MEM_TYPE_RAM, - // physical_start: 0xfd440000, - // virtual_start: 0xfd440000, - // size: 0x20000, - // }, // its + HvConfigMemoryRegion { + mem_type: MEM_TYPE_IO, + physical_start: 0xfd440000, + virtual_start: 0xfd440000, + size: 0x20000, + }, // its passthrough HvConfigMemoryRegion { mem_type: MEM_TYPE_RAM, physical_start: 0x1f0000000, @@ -342,8 +342,8 @@ pub const ROOT_ARCH_ZONE_CONFIG: HvArchZoneConfig = HvArchZoneConfig { gicd_size: 0x10000, gicr_base: 0xfd460000, gicr_size: 0xc0000, - gits_base: 0xfd440000, - gits_size: 0x20000, + gits_base: 0, + gits_size: 0, }), uefi_config: UefiConfig::NoUefi }; diff --git a/platform/aarch64/rk3568/scripts/boot_zone1.sh b/platform/aarch64/rk3568/scripts/boot_zone1.sh index b881d4954..2275db262 100644 --- a/platform/aarch64/rk3568/scripts/boot_zone1.sh +++ b/platform/aarch64/rk3568/scripts/boot_zone1.sh @@ -1,10 +1,10 @@ #!/bin/bash -insmod hvisor.ko -mount -t proc proc /proc -mount -t sysfs sysfs /sys -rm nohup.out +insmod hvisor.ko 2>/dev/null || true +mount -t proc proc /proc 2>/dev/null || true +mount -t sysfs sysfs /sys 2>/dev/null || true +rm -f nohup.out mkdir -p /dev/pts -mount -t devpts devpts /dev/pts +mount -t devpts devpts /dev/pts 2>/dev/null || true nohup ./hvisor virtio start virtio.json & ./hvisor zone start linux2.json \ No newline at end of file From 29331a4a5a70e9c0f3735b0ba8bcb413fb0685db Mon Sep 17 00:00:00 2001 From: dallas Date: Mon, 10 Aug 2026 22:41:23 +0800 Subject: [PATCH 46/53] add uboot cmd for pi --- jenkins/ci.yaml | 16 +++++----- jenkins/ci_runner.py | 71 +++++++++++++++++++++++++++++++++----------- 2 files changed, 62 insertions(+), 25 deletions(-) diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index dccd9b481..9f4cccb21 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -32,13 +32,10 @@ bids: zone0_dtbs: - platform/aarch64/rk3568/image/dts/rk3568_limit_zone0.dtb uboot_ready_pattern: '=>' + zone0_ready_pattern: '(?:\r?\n)#\s' zone0_shell_timeout: 60 uboot_cmd: pci enum;setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.240; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:rk3568/hvisor.bin && tftp ${fdt_addr} ${serverip}:rk3568/rk3568_limit_zone0.dtb && tftp ${zone0_kernel_addr} ${serverip}:rk3568/Image && bootm ${loadaddr} - ${fdt_addr}; deploy: - board_ip: 192.168.1.240 - board_iface: eth0 - link_wait: 3 - ping_retries: 3 host_ip: 192.168.1.181 host_user: light board_ssh_key: /home/light/.ssh/hvisor_board_pull @@ -72,10 +69,15 @@ bids: - platform/aarch64/phytium-pi/image/dts/phytium-pi-board-v2.dtb zone0_image: /home/light/DEMO/phytium-pi-external/Image uboot_ready_pattern: 'Phytium-Pi#' - zone0_shell_timeout: 60 - uboot_cmd: run boot_root_linux + zone0_ready_pattern: '(?:\r?\n)#\s' + zone0_shell_timeout: 120 + uboot_prompt_timeout: 30 + uboot_step_timeout: 90 + uboot_cmds: + - setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.179; setenv loadaddr 0x90100000; setenv fdt_addr 0x90000000; setenv zone0_kernel_addr 0xa0400000; setenv zone0_fdt_addr 0xa0000000; tftp ${loadaddr} ${serverip}:hvisor.bin; + - tftp ${fdt_addr} ${serverip}:phytium-pi-board-v2.dtb; tftp ${zone0_kernel_addr} ${serverip}:Image; tftp ${zone0_fdt_addr} ${serverip}:linux1.dtb; bootm ${loadaddr} - ${fdt_addr}; deploy: - board_ip: 192.168.1.178 + board_ip: 192.168.1.179 board_iface: eth0 link_wait: 3 ping_retries: 3 diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 807c1a994..92fcd2386 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -160,7 +160,8 @@ def board_wait_uboot_prompt(term: Terminal, pattern: str, timeout: float) -> Non remaining = deadline - time.monotonic() if term.wait_pattern(pattern, timeout=min(5.0, remaining), from_offset=0): return - board_wake_console(term) + term.send(" ") + time.sleep(0.2) raise TerminalTimeoutError(f"timed out waiting for U-Boot prompt (pattern={pattern!r})") @@ -282,6 +283,41 @@ def deploy(cfg: dict[str, Any], term: Terminal | None) -> int: return deploy_board_pull(cfg, term) +def zone0_ready_pattern(cfg: dict[str, Any]) -> str: + custom = str(cfg.get("zone0_ready_pattern", "")).strip() + if custom: + return custom + return ZONE0_READY_PATTERN + + +def uboot_commands(cfg: dict[str, Any]) -> list[str]: + raw = cfg.get("uboot_cmds") + if isinstance(raw, list): + cmds = [str(item).strip() for item in raw if str(item).strip()] + if cmds: + return cmds + cmd = str(cfg.get("uboot_cmd", "")).strip() + return [cmd] if cmd else [] + + +def send_board_uboot_commands(cfg: dict[str, Any], term: Terminal) -> None: + cmds = uboot_commands(cfg) + if not cmds: + return + uboot_ready = cfg.get("uboot_ready_pattern", "") or r"=>" + initial_timeout = float(cfg.get("uboot_prompt_timeout", 20.0)) + step_timeout = float(cfg.get("uboot_step_timeout", 0.0)) + if step_timeout <= 0: + step_timeout = max(initial_timeout, 60.0) + board_wait_uboot_prompt(term, uboot_ready, timeout=initial_timeout) + for index, cmd in enumerate(cmds): + if index > 0: + time.sleep(0.3) + board_wait_uboot_prompt(term, uboot_ready, timeout=step_timeout) + time.sleep(0.3) + term.send(cmd) + + def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: print("————————————————\ncase: zone0_start\n————————————————\n", flush=True) if cfg["mode"] == "qemu": @@ -297,15 +333,14 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: wait_qemu_socket(cfg["socket_path"], timeout=30.0) qemu_term = ensure_qemu_terminal(cfg, log_path) - uboot_cmd = cfg.get("uboot_cmd", "") - uboot_ready = cfg.get("uboot_ready_pattern", "") - if uboot_cmd: - if not uboot_ready: - uboot_ready = r"*=>" + cmds = uboot_commands(cfg) + if cmds: + uboot_ready = cfg.get("uboot_ready_pattern", "") or r"*=>" if not qemu_term.wait_pattern(uboot_ready, timeout=10.0): raise TerminalTimeoutError("timed out waiting for U-Boot prompt") - qemu_term.send(uboot_cmd) - if not qemu_term.wait_pattern(ZONE0_READY_PATTERN, timeout=180.0): + for cmd in cmds: + qemu_term.send(cmd) + if not qemu_term.wait_pattern(zone0_ready_pattern(cfg), timeout=180.0): raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") return 0 if cfg["mode"] == "board": @@ -319,16 +354,9 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: time.sleep(3.0) board_wake_console(board_term) - uboot_cmd = cfg.get("uboot_cmd", "") - uboot_ready = cfg.get("uboot_ready_pattern", "") - if uboot_cmd: - if not uboot_ready: - uboot_ready = r"=>" - board_wait_uboot_prompt(board_term, uboot_ready, timeout=float(cfg["uboot_prompt_timeout"])) - time.sleep(0.3) - board_term.send(uboot_cmd) + send_board_uboot_commands(cfg, board_term) zone0_timeout = float(cfg.get("zone0_shell_timeout", 180.0)) - if not board_term.wait_pattern(ZONE0_READY_PATTERN, timeout=zone0_timeout): + if not board_term.wait_pattern(zone0_ready_pattern(cfg), timeout=zone0_timeout): raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") return 0 return 0 @@ -541,8 +569,11 @@ def cfg_str(key: str, default: str = "") -> str: "power_channel": int(tests.get("power_channel", 4)), "baudrate": int(tests.get("baudrate", 1500000)), "uboot_cmd": str(tests.get("uboot_cmd", "")).strip(), + "uboot_cmds": tests.get("uboot_cmds") or [], "uboot_ready_pattern": str(tests.get("uboot_ready_pattern", "")).strip(), "uboot_prompt_timeout": float(tests.get("uboot_prompt_timeout", 20.0)), + "uboot_step_timeout": float(tests.get("uboot_step_timeout", 0.0)), + "zone0_ready_pattern": str(tests.get("zone0_ready_pattern", "")).strip(), "zone0_shell_timeout": float(tests.get("zone0_shell_timeout", 300.0 if mode == "board" else 180.0)), "tftp_dir": str(tests.get("tftp_dir", "/home/light/tftp")).strip(), "lspci": tests.get("lspci") or {}, @@ -583,7 +614,11 @@ def main() -> int: standalone = case_name == "zone0_start" if standalone: - rc = case_fn(cfg, None) + try: + rc = case_fn(cfg, None) + except (TerminalTimeoutError, TerminalCommandError) as exc: + print(f"[ci_runner] terminal command failed in case '{case_name}': {exc}", flush=True) + return 1 if rc != 0: return rc time.sleep(5.0) From 0d3514d52488e5eb89f43c8da9fc547a5a1f92b4 Mon Sep 17 00:00:00 2001 From: dallas Date: Mon, 10 Aug 2026 22:44:13 +0800 Subject: [PATCH 47/53] for qemu-gicv3 aarch64 remove cpu 2,3 in zone0 --- platform/aarch64/qemu-gicv3/board.rs | 2 +- platform/aarch64/qemu-gicv3/configs/zone1-linux.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/platform/aarch64/qemu-gicv3/board.rs b/platform/aarch64/qemu-gicv3/board.rs index 331a46cc4..9048090a4 100644 --- a/platform/aarch64/qemu-gicv3/board.rs +++ b/platform/aarch64/qemu-gicv3/board.rs @@ -53,7 +53,7 @@ pub const BOARD_PHYSMEM_LIST: &[(u64, u64, MemoryType)] = &[ pub const ROOT_ZONE_DTB_ADDR: u64 = 0xa0000000; pub const ROOT_ZONE_KERNEL_ADDR: u64 = 0xa0400000; pub const ROOT_ZONE_ENTRY: u64 = 0xa0400000; -pub const ROOT_ZONE_CPUS: u64 = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3); +pub const ROOT_ZONE_CPUS: u64 = (1 << 0) | (1 << 1); pub const ROOT_ZONE_NAME: &str = "root-linux"; diff --git a/platform/aarch64/qemu-gicv3/configs/zone1-linux.json b/platform/aarch64/qemu-gicv3/configs/zone1-linux.json index 514c16f91..b73f0bc98 100644 --- a/platform/aarch64/qemu-gicv3/configs/zone1-linux.json +++ b/platform/aarch64/qemu-gicv3/configs/zone1-linux.json @@ -2,8 +2,8 @@ "name": "linux2", "zone_id": 1, "cpus": [ - 2, - 3 + 3, + 4 ], "memory_regions": [ { From 5aba85ea523b6329b7099c2b145f6bab555b3003 Mon Sep 17 00:00:00 2001 From: dallas Date: Mon, 10 Aug 2026 23:41:16 +0800 Subject: [PATCH 48/53] fix ci bug for pi --- jenkins/{board_scp.sh => board_stage.sh} | 39 +- jenkins/board_zone0_start.py | 62 +++ jenkins/ci.yaml | 46 +- jenkins/ci_runner.py | 565 +++++++++++++++++------ jenkins/terminal.py | 12 +- 5 files changed, 544 insertions(+), 180 deletions(-) rename jenkins/{board_scp.sh => board_stage.sh} (63%) create mode 100755 jenkins/board_zone0_start.py diff --git a/jenkins/board_scp.sh b/jenkins/board_stage.sh similarity index 63% rename from jenkins/board_scp.sh rename to jenkins/board_stage.sh index ecf37ec7e..509d43fe9 100755 --- a/jenkins/board_scp.sh +++ b/jenkins/board_stage.sh @@ -1,5 +1,6 @@ #!/bin/sh -# Stage zone1 test artifacts on the CI host for board pull via scp. +# Stage zone1 deploy artifacts on the CI host; the board pulls them via scp. +# Zone0 boot artifacts (hvisor.bin, Image, dtbs) belong in tftp_dir, not here. set -eux @@ -7,9 +8,7 @@ ARCH=${ARCH:?ARCH is required} BOARD=${BOARD:?BOARD is required} WORKSPACE_ROOT=${WORKSPACE_ROOT:-$(pwd)} HVISOR_TOOL_PATH=${HVISOR_TOOL_PATH:-${WORKSPACE_ROOT}/hvisor-tool} -MODE=${MODE:-release} -STAGING_DIR=${STAGING_DIR:-/home/light/tftp/ci_deploy} -TFTP_DIR=${TFTP_DIR:-} +STAGING_DIR=${STAGING_DIR:-/home/light/ci_deploy} case "${HVISOR_TOOL_PATH}" in /*) ;; @@ -22,20 +21,6 @@ IMAGE_DIR="${PLATFORM_DIR}/image" SCRIPTS_DIR="${PLATFORM_DIR}/scripts" ZONE1_BOOT_SCRIPT="${SCRIPTS_DIR}/boot_zone1.sh" CHECK_SERIAL_SCRIPT="${WORKSPACE_ROOT}/jenkins/check_serial.sh" -COPY_HVISOR_BIN=${COPY_HVISOR_BIN:-true} - -case "${ARCH}" in - x86_64) RUSTC_TARGET="x86_64-unknown-none" ;; - aarch64) RUSTC_TARGET="aarch64-unknown-none" ;; - riscv64) RUSTC_TARGET="riscv64gc-unknown-none-elf" ;; - *) - echo "error: unsupported ARCH: ${ARCH}" - exit 1 - ;; -esac - -BUILD_PATH="${WORKSPACE_ROOT}/target/${RUSTC_TARGET}/${MODE}" -HVISOR_BIN="${BUILD_PATH}/hvisor.bin" if [ -z "${ZONE1_DTB:-}" ]; then for candidate in \ @@ -62,10 +47,6 @@ if [ ! -f "${HVISOR_TOOL_PATH}/output/hvisor.ko" ]; then echo "error: hvisor.ko not found: ${HVISOR_TOOL_PATH}/output/hvisor.ko" exit 1 fi -if [ "${COPY_HVISOR_BIN}" != "false" ] && [ ! -f "${HVISOR_BIN}" ]; then - echo "error: hvisor.bin not found: ${HVISOR_BIN}" - exit 1 -fi if [ ! -f "${ZONE1_BOOT_SCRIPT}" ]; then echo "error: zone1 start script not found: ${ZONE1_BOOT_SCRIPT}" exit 1 @@ -79,10 +60,8 @@ fi rm -rf "${STAGING_DIR}" mkdir -p "${STAGING_DIR}" -cp "${HVISOR_TOOL_PATH}/output/hvisor" "${HVISOR_TOOL_PATH}/output/hvisor.ko" "${STAGING_DIR}/" -if [ "${COPY_HVISOR_BIN}" != "false" ]; then - cp "${HVISOR_BIN}" "${STAGING_DIR}/" -fi +cp "${HVISOR_TOOL_PATH}/output/hvisor.ko" "${STAGING_DIR}/" +gzip -c "${HVISOR_TOOL_PATH}/output/hvisor" > "${STAGING_DIR}/hvisor.gz" cp "${CONFIGS_DIR}/"* "${STAGING_DIR}/" cp "${ZONE1_BOOT_SCRIPT}" "${STAGING_DIR}/" @@ -96,14 +75,6 @@ if [ -f "${CHECK_SERIAL_SCRIPT}" ]; then cp "${CHECK_SERIAL_SCRIPT}" "${STAGING_DIR}/" fi -ZONE1_ASSETS_DIR=${ZONE1_ASSETS_DIR:-/home/light/tftp/zone1_assets} -if [ -n "${TFTP_DIR}" ] && [ -f "${TFTP_DIR}/Image" ]; then - cp "${TFTP_DIR}/Image" "${STAGING_DIR}/" -elif [ -f "${ZONE1_ASSETS_DIR}/Image" ]; then - cp "${ZONE1_ASSETS_DIR}/Image" "${STAGING_DIR}/" -fi -# rootfs2.ext4 is large and persistent on the board; do not stage in CI. - chmod -R a+rX "${STAGING_DIR}" chmod +x "${STAGING_DIR}/boot_zone1.sh" echo "board staging completed: ${STAGING_DIR}" diff --git a/jenkins/board_zone0_start.py b/jenkins/board_zone0_start.py new file mode 100755 index 000000000..0b8f2e5bd --- /dev/null +++ b/jenkins/board_zone0_start.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Power-cycle a board and boot zone0 via U-Boot; no network/SSH/deploy/zone1.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from ci_runner import ( + board_power_off, + boot_board_zone0_shell, + build_terminal, + load_runtime_config, + logs_dir, +) +from terminal import TerminalCommandError, TerminalTimeoutError + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Boot board zone0 and stop.") + parser.add_argument("--bid", required=True, help="BID key in jenkins/ci.yaml") + parser.add_argument( + "--no-power-cycle", + action="store_true", + help="Skip relay power cycle (board already off or at U-Boot).", + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + cfg = load_runtime_config(args) + if cfg["mode"] != "board": + raise SystemExit(f"bid '{args.bid}' is mode={cfg['mode']!r}, board mode required") + + log_path = logs_dir(cfg) / "zone0_console.log" + log_path.write_text("", encoding="utf-8") + term = build_terminal(cfg, log_path) + term.open() + ok = False + try: + boot_board_zone0_shell(cfg, term, power_cycle=not args.no_power_cycle) + ok = True + print( + f"[board_zone0_start] zone0 ready (serial={cfg['serial_port']}, log={log_path})", + flush=True, + ) + return 0 + except (TerminalTimeoutError, TerminalCommandError) as exc: + print(f"[board_zone0_start] failed: {exc}", flush=True) + return 1 + finally: + term.close() + if not ok: + board_power_off(cfg) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index 9f4cccb21..1a50b01ec 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -32,14 +32,24 @@ bids: zone0_dtbs: - platform/aarch64/rk3568/image/dts/rk3568_limit_zone0.dtb uboot_ready_pattern: '=>' - zone0_ready_pattern: '(?:\r?\n)#\s' - zone0_shell_timeout: 60 - uboot_cmd: pci enum;setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.240; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:rk3568/hvisor.bin && tftp ${fdt_addr} ${serverip}:rk3568/rk3568_limit_zone0.dtb && tftp ${zone0_kernel_addr} ${serverip}:rk3568/Image && bootm ${loadaddr} - ${fdt_addr}; + zone0_ready_pattern: 'root@linux:[^\r\n]*#\s?' + zone0_shell_timeout: 180 + uboot_prompt_timeout: 60 + uboot_step_timeout: 90 + uboot_cmds: + - pci enum; setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.240; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000 + - tftp ${loadaddr} ${serverip}:rk3568/hvisor.bin + - tftp ${fdt_addr} ${serverip}:rk3568/rk3568_limit_zone0.dtb + - tftp ${zone0_kernel_addr} ${serverip}:rk3568/Image + - bootm ${loadaddr} - ${fdt_addr} deploy: + board_ip: 192.168.1.240 + board_iface: eth0 + link_wait: 3 host_ip: 192.168.1.181 host_user: light - board_ssh_key: /home/light/.ssh/hvisor_board_pull - staging_dir: /home/light/tftp/ci_deploy + staging_dir: /home/light/ci_deploy + ssh_connect_timeout: 60 zone1_dtb: platform/aarch64/rk3568/image/dts/rk3568_limit_zone1.dtb lspci: expected_bdfs: @@ -69,22 +79,30 @@ bids: - platform/aarch64/phytium-pi/image/dts/phytium-pi-board-v2.dtb zone0_image: /home/light/DEMO/phytium-pi-external/Image uboot_ready_pattern: 'Phytium-Pi#' - zone0_ready_pattern: '(?:\r?\n)#\s' - zone0_shell_timeout: 120 - uboot_prompt_timeout: 30 - uboot_step_timeout: 90 + zone0_ready_pattern: 'user@[^\r\n]*[$#]\s?|root@[^\r\n]*:\/#\s?|(?:\r?\n)#\s*(?:\r?\n|$)' + board_shell_su_cmd: 'su - user -P' + board_shell_ready_pattern: 'user@[^\r\n]*[$#]\s?' + zone0_shell_timeout: 300 + uboot_prompt_timeout: 60 + uboot_step_timeout: 600 + uboot_autoboot_window: 15 uboot_cmds: - - setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.179; setenv loadaddr 0x90100000; setenv fdt_addr 0x90000000; setenv zone0_kernel_addr 0xa0400000; setenv zone0_fdt_addr 0xa0000000; tftp ${loadaddr} ${serverip}:hvisor.bin; - - tftp ${fdt_addr} ${serverip}:phytium-pi-board-v2.dtb; tftp ${zone0_kernel_addr} ${serverip}:Image; tftp ${zone0_fdt_addr} ${serverip}:linux1.dtb; bootm ${loadaddr} - ${fdt_addr}; + - setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.179; setenv loadaddr 0x90100000; setenv fdt_addr 0x90000000; setenv zone0_kernel_addr 0xa0400000; setenv zone0_fdt_addr 0xa0000000 + - tftp ${loadaddr} ${serverip}:phytium-pi/hvisor.bin + - tftp ${fdt_addr} ${serverip}:phytium-pi/phytium-pi-board-v2.dtb + - tftp ${zone0_kernel_addr} ${serverip}:phytium-pi/Image + - tftp ${zone0_fdt_addr} ${serverip}:phytium-pi/linux1.dtb + - bootm ${loadaddr} - ${fdt_addr} deploy: + board_user: user board_ip: 192.168.1.179 board_iface: eth0 - link_wait: 3 + link_wait: 8 ping_retries: 3 host_ip: 192.168.1.181 host_user: light - board_ssh_key: /home/light/.ssh/hvisor_board_pull - staging_dir: /home/light/tftp/ci_deploy + staging_dir: /home/light/ci_deploy + pull_timeout: 900 zone1_dtb: platform/aarch64/phytium-pi/image/dts/linux2.dtb cases: - zone0_start diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 92fcd2386..3780001b0 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -2,9 +2,9 @@ from __future__ import annotations import argparse -import base64 import os import re +import shlex import socket import signal import subprocess @@ -19,9 +19,14 @@ CaseFunc = Callable[[dict[str, Any], Terminal | None], int] # Wait for zone1 inner console: shell prompt (#/$) or login:. -ZONE0_READY_PATTERN = r"root@[^\r\n]*[#$]\s|(?:\r?\n)#\s" +ZONE0_READY_PATTERN = r"root@[^\r\n]*#\s?|(?:\r?\n)#\s*(?:\r?\n|$)" ZONE1_INNER_PROMPT_TIMEOUT = 60.0 ZONE1_INNER_LOG_FETCH_TIMEOUT = 30.0 +DEPLOY_GUNZIP_ARTIFACTS = {"hvisor.gz": "hvisor"} +DEPLOY_LARGE_PULL_BYTES = 512 * 1024 +BOARD_PUBKEY_LINE = re.compile(r"^ssh-(?:ed25519|rsa)\s+\S+") +BOARD_ROOT_SHELL_PATTERN = r"root@[^\r\n]*:\/#\s?|(?:\r?\n)#\s?(?:\r?\n|$)" +BOARD_SU_PASSWORD_PROMPT = r"Password:|password:" def logs_dir(cfg: dict[str, Any]) -> Path: @@ -147,22 +152,30 @@ def board_power_off(cfg: dict[str, Any]) -> None: board_power(cfg, "off") -def board_wake_console(term: Terminal, *, repeats: int = 3) -> None: - for _ in range(repeats): +def board_wait_uboot_prompt( + term: Terminal, + pattern: str, + timeout: float, + *, + after_offset: int | None = None, + autoboot_window: float = 0.0, +) -> None: + """Wake the console and wait for a U-Boot prompt.""" + offset = term.offset() if after_offset is None else after_offset + if autoboot_window > 0 and after_offset is None: + deadline = time.monotonic() + autoboot_window + while time.monotonic() < deadline: + if term.wait_pattern(pattern, timeout=0.2, from_offset=offset): + return + term.send(" ") + time.sleep(0.1) + elif after_offset is None: term.send("") time.sleep(0.2) - - -def board_wait_uboot_prompt(term: Terminal, pattern: str, timeout: float) -> None: - """Wait for U-Boot prompt, periodically waking an idle console.""" - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - remaining = deadline - time.monotonic() - if term.wait_pattern(pattern, timeout=min(5.0, remaining), from_offset=0): - return - term.send(" ") + term.send("") time.sleep(0.2) - raise TerminalTimeoutError(f"timed out waiting for U-Boot prompt (pattern={pattern!r})") + if not term.wait_pattern(pattern, timeout=timeout, from_offset=offset): + raise TerminalTimeoutError(f"timed out waiting for U-Boot prompt (pattern={pattern!r})") def save_inner_serial_log(cfg: dict[str, Any], content: str) -> None: @@ -173,9 +186,9 @@ def save_inner_serial_log(cfg: dict[str, Any], content: str) -> None: def stage_board_files(cfg: dict[str, Any]) -> None: - script = cfg["workspace"] / "jenkins" / "board_scp.sh" + script = cfg["workspace"] / "jenkins" / "board_stage.sh" if not script.is_file(): - raise SystemExit(f"board scp script not found: {script}") + raise SystemExit(f"board stage script not found: {script}") env = os.environ.copy() env["ARCH"] = cfg["arch"] @@ -185,45 +198,94 @@ def stage_board_files(cfg: dict[str, Any]) -> None: env["WORKSPACE_ROOT"] = str(cfg["workspace"]) env["HVISOR_TOOL_PATH"] = cfg["hvisor_tool_path"] env["STAGING_DIR"] = cfg["network_staging_dir"] - env["TFTP_DIR"] = cfg["tftp_dir"] if cfg.get("zone1_dtb"): zone1_dtb = Path(cfg["zone1_dtb"]) if not zone1_dtb.is_absolute(): zone1_dtb = cfg["workspace"] / zone1_dtb env["ZONE1_DTB"] = str(zone1_dtb.resolve()) - env["COPY_HVISOR_BIN"] = "true" if cfg.get("copy_hvisor_bin", True) else "false" subprocess.run(["bash", str(script)], check=True, cwd=cfg["workspace"], env=env) -def deploy_chmod(cfg: dict[str, Any], term: Terminal) -> None: - work_dir = cfg["zone1_work_dir"] - boot_script = cfg["zone1_boot_script"] +def save_case_log(cfg: dict[str, Any], name: str, content: str) -> None: + path = logs_dir(cfg) / name + path.write_text(content, encoding="utf-8") + print(f"[ci_runner] saved {path}", flush=True) + + +def netmask_prefix(netmask: str) -> int: + try: + return sum(bin(int(part)).count("1") for part in netmask.split(".")) + except (ValueError, AttributeError): + return 24 + + +def board_sudo(cmd: str) -> str: + """Wrap a board shell command to run with sudo -E (preserve HOME etc.).""" + cmd = cmd.strip() + if cmd.startswith("sudo "): + return cmd + if cmd.startswith("export "): + return cmd + return f"sudo -E sh -c {shlex.quote(cmd)}" + + +def board_priv(cfg: dict[str, Any], cmd: str) -> str: + """Run privileged board commands; skip sudo when the shell is already root.""" + if cfg.get("board_is_root"): + return cmd.strip() + return board_sudo(cmd) + + +def board_ssh_paths(cfg: dict[str, Any]) -> tuple[str, str, str]: + """Absolute SSH key paths from the prepared board shell home.""" + home = str(cfg.get("board_home") or "/root") + ssh_dir = f"{home}/.ssh" + key = f"{ssh_dir}/id_ed25519" + return key, f"{key}.pub", ssh_dir + + +def _setup_board_interface(cfg: dict[str, Any], term: Terminal) -> None: + """Assign a static IP and cycle the interface.""" + board_ip = str(cfg.get("board_ip", "")).strip() + board_iface = str(cfg.get("board_iface", "")).strip() + if not board_ip or not board_iface: + return + + netmask = str(cfg.get("board_netmask", "255.255.255.0")).strip() or "255.255.255.0" + prefix = netmask_prefix(netmask) term.run( - "deploy_chmod", - f"chmod +x {work_dir}/{boot_script} {work_dir}/check_serial.sh 2>/dev/null || true", - timeout=60.0, + "deploy_set_ip", + board_priv( + cfg, + f"ip addr flush dev {board_iface} 2>/dev/null || true; " + f"ip addr add {board_ip}/{prefix} dev {board_iface}", + ), + timeout=30.0, + ) + term.run( + "deploy_iface_down", + board_priv(cfg, f"ip link set {board_iface} down"), + timeout=15.0, + ) + term.run( + "deploy_iface_up", + board_priv(cfg, f"ip link set {board_iface} up"), + timeout=15.0, ) - -def deploy_board_pull(cfg: dict[str, Any], term: Terminal) -> int: - """Board pulls staged zone1 files from the CI host via scp (zone0 serial console).""" - board_ip = cfg.get("board_ip") - board_iface = cfg.get("board_iface") - if board_ip and board_iface: - term.run( - "deploy_set_ip", - f"ifconfig {board_iface} {board_ip} netmask 255.255.255.0 up", - timeout=30.0, + link_wait = float(cfg.get("deploy_link_wait", 0.0)) + if link_wait > 0: + print( + f"[deploy] waiting {link_wait}s for {board_iface} link on {board_ip}", + flush=True, ) - link_wait = float(cfg.get("deploy_link_wait", 0.0)) - if link_wait > 0: - print( - f"[deploy] waiting {link_wait}s for {board_iface} link on {board_ip}", - flush=True, - ) - time.sleep(link_wait) + time.sleep(link_wait) + time.sleep(2.0) + +def _board_ping_host(cfg: dict[str, Any], term: Terminal) -> None: + """Verify layer-3 reachability to the CI host before SSH setup.""" host_ip = cfg["network_host_ip"] ping_count = cfg["network_ping_count"] ping_retries = int(cfg.get("deploy_ping_retries", 1)) @@ -235,39 +297,194 @@ def deploy_board_pull(cfg: dict[str, Any], term: Terminal) -> int: time.sleep(retry_wait) _, ping_output = term.run( "deploy_ping", - f"ping -c {ping_count} -W 5 {host_ip}", + board_priv( + cfg, + f"ping -c {ping_count} -W 5 {host_ip} > /tmp/ci_ping.log 2>&1; cat /tmp/ci_ping.log", + ), timeout=30.0 + ping_count * 5.0, + wake_interval=2.0, ) if ping_success(ping_output): break - save_lspci_artifacts(cfg, "deploy_ping.log", f"=== ping {host_ip} ===\n{ping_output}\n") - + save_case_log(cfg, "deploy_ping.log", f"=== ping {host_ip} ===\n{ping_output}\n") if not ping_success(ping_output): raise TerminalCommandError(f"ping {host_ip} failed") + print(f"[deploy] ping {host_ip} ok", flush=True) - print(f"[deploy] ping {host_ip} ok, staging files on host", flush=True) - stage_board_files(cfg) - time.sleep(3.0) - board_key = install_board_ssh_key(cfg, term) - identity = board_scp_identity(cfg, board_key) +def ensure_board_net(cfg: dict[str, Any], term: Terminal) -> None: + """Configure network, verify ping, and probe SSH to the CI host.""" + prepare_board_shell(cfg, term) + _setup_board_interface(cfg, term) + _board_ping_host(cfg, term) + _board_ssh_probe(cfg, term) + + +def board_pubkey_from_output(output: str) -> str | None: + for line in output.splitlines(): + candidate = line.strip() + if BOARD_PUBKEY_LINE.match(candidate): + return candidate + return None + + +def _board_ssh_probe(cfg: dict[str, Any], term: Terminal) -> None: + key, pub, _ = board_ssh_paths(cfg) + key_rc, _ = term.run( + "board_ssh_check", + board_priv(cfg, f"test -f {shlex.quote(key)}"), + timeout=15.0, + ) + if key_rc != 0: + raise TerminalCommandError( + f"board ssh private key not found: {key}\n" + "copy or generate ~/.ssh/id_ed25519 on the board manually." + ) + + host_user = cfg["network_host_user"] + host_ip = cfg["network_host_ip"] + host = shlex.quote(f"{host_user}@{host_ip}") + opts = board_ssh_opts(cfg, key) + probe_timeout = float(int(cfg.get("deploy_ssh_connect_timeout", 30)) + 30) + probe_rc, probe_out = term.run( + "deploy_ssh_probe", + board_priv(cfg, f"ssh {opts} {host} true"), + timeout=probe_timeout, + ) + if probe_rc == 0: + print("[deploy] ssh to CI host ok", flush=True) + return + + pub_rc, pub_out = term.run( + "board_ssh_pubkey", + board_priv(cfg, f"cat {shlex.quote(pub)}"), + timeout=15.0, + ) + pubkey = board_pubkey_from_output(pub_out) if pub_rc == 0 else None + if not pubkey: + raise TerminalCommandError( + f"ssh probe to {host_user}@{host_ip} failed and board public key not found: {pub}\n" + f"{probe_out.strip()}" + ) + + auth_keys = f"/home/{host_user}/.ssh/authorized_keys" + raise TerminalCommandError( + f"ssh probe to {host_user}@{host_ip} failed; add this board public key to " + f"{auth_keys} on the CI host:\n{pubkey}" + ) + + +def boot_board_zone0_shell(cfg: dict[str, Any], term: Terminal, *, power_cycle: bool = True) -> None: + """Power-cycle (optional), run U-Boot commands, and prepare the zone0 shell.""" + if power_cycle: + board_power_cycle(cfg) + send_board_uboot_commands(cfg, term) + timeout = float(cfg.get("zone0_shell_timeout", 180.0)) + if not term.wait_pattern(zone0_ready_pattern(cfg), timeout=timeout): + raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") + prepare_board_shell(cfg, term) + +def deploy_board_pull(cfg: dict[str, Any], term: Terminal) -> int: + """Board pulls staged zone1 files from the CI host over ssh.""" + ensure_board_net(cfg, term) + + host_ip = cfg["network_host_ip"] host_user = cfg["network_host_user"] staging_dir = cfg["network_staging_dir"] work_dir = cfg["zone1_work_dir"] - term.run("deploy_scp_host", f"CI_H={host_ip}", timeout=15.0) - term.run("deploy_scp_user", f"CI_U={host_user}", timeout=15.0) - term.run("deploy_scp_dir", f"CI_D={staging_dir}", timeout=15.0) - scp_cmd = ( - f"scp {identity}-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null " - f"$CI_U@$CI_H:$CI_D/* {work_dir}/" + + print(f"[deploy] staging files on host", flush=True) + stage_board_files(cfg) + + staging_path = Path(staging_dir) + if not staging_path.is_dir(): + raise TerminalCommandError(f"staging dir not found: {staging_path}") + staged_files = sorted( + (p.name for p in staging_path.iterdir() if p.is_file()), + key=lambda name: staging_path.joinpath(name).stat().st_size, ) - print(f"[deploy] pulling staged files from {host_user}@{host_ip}:{staging_dir}", flush=True) - pull_rc, _ = term.run("deploy_scp_pull", scp_cmd, timeout=900.0) - if pull_rc != 0: - raise TerminalCommandError(f"board scp pull failed with rc={pull_rc}") + if not staged_files: + raise TerminalCommandError(f"no staged files in {staging_path}") + + key, _, _ = board_ssh_paths(cfg) + pull_timeout = float(cfg.get("deploy_pull_timeout", 600.0)) + deadline = time.monotonic() + pull_timeout + all_logs: list[str] = [] + + stale_names = list(staged_files) + for gz_name, raw_name in DEPLOY_GUNZIP_ARTIFACTS.items(): + if gz_name in stale_names: + stale_names.append(raw_name) + stale_paths = " ".join(shlex.quote(f"{work_dir}/{name}") for name in stale_names) + print(f"[deploy] removing stale files in {work_dir}", flush=True) + term.run("deploy_clean", board_priv(cfg, f"rm -f {stale_paths}"), timeout=30.0) + + print( + f"[deploy] pulling {len(staged_files)} file(s) via ssh from {host_user}@{host_ip}:{staging_dir}", + flush=True, + ) + for index, name in enumerate(staged_files, start=1): + remaining = deadline - time.monotonic() + if remaining <= 0: + save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) + raise TerminalTimeoutError( + f"deploy pull timed out before {name!r}", + partial_output="".join(all_logs), + ) - deploy_chmod(cfg, term) + file_size = staging_path.joinpath(name).stat().st_size + file_timeout = deploy_file_pull_timeout(file_size, remaining) + large_transfer = file_size >= DEPLOY_LARGE_PULL_BYTES + pull_cmd = board_ssh_pull_file_cmd( + cfg, key, staging_dir, name, work_dir, file_size, large_transfer=large_transfer + ) + print( + f"[deploy] pull ({index}/{len(staged_files)}): {name} ({file_size} bytes, timeout={file_timeout:.0f}s)", + flush=True, + ) + try: + pull_rc, pull_out = term.run(f"deploy_pull_{name}", pull_cmd, timeout=file_timeout) + except TerminalTimeoutError as exc: + pull_out = exc.partial_output or "" + all_logs.append(f"=== deploy_pull_{name} (timed out) ===\n{pull_out}\n") + save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) + raise + all_logs.append(f"=== deploy_pull_{name} ===\n{pull_out}\n") + if pull_rc != 0: + save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) + raise TerminalCommandError( + f"board pull failed for {name!r} with rc={pull_rc}: {pull_out.strip()}" + ) + + raw_name = DEPLOY_GUNZIP_ARTIFACTS.get(name) + if raw_name: + raw_path = Path(cfg["hvisor_tool_path"]) / "output" / raw_name + raw_size = raw_path.stat().st_size + gunzip_cmd = ( + f"gunzip -f {shlex.quote(f'{work_dir}/{name}')} && " + f"test $(wc -c < {shlex.quote(f'{work_dir}/{raw_name}')}) -eq {raw_size}" + ) + print(f"[deploy] gunzip: {name} -> {raw_name}", flush=True) + gz_rc, gz_out = term.run(f"deploy_gunzip_{raw_name}", gunzip_cmd, timeout=60.0) + all_logs.append(f"=== deploy_gunzip_{raw_name} ===\n{gz_out}\n") + if gz_rc != 0: + save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) + raise TerminalCommandError( + f"board gunzip failed for {name!r} with rc={gz_rc}: {gz_out.strip()}" + ) + + save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) + + boot_script = cfg["zone1_boot_script"] + term.run( + "deploy_chmod", + board_priv( + cfg, + f"chmod +x {work_dir}/{boot_script} {work_dir}/check_serial.sh 2>/dev/null || true", + ), + timeout=60.0, + ) print("[deploy] board_pull completed", flush=True) return 0 @@ -290,6 +507,16 @@ def zone0_ready_pattern(cfg: dict[str, Any]) -> str: return ZONE0_READY_PATTERN +def board_shell_ready_pattern(cfg: dict[str, Any]) -> str: + custom = str(cfg.get("board_shell_ready_pattern", "")).strip() + if custom: + return custom + board_user = str(cfg.get("board_user", "root")).strip() or "root" + if board_user == "root": + return BOARD_ROOT_SHELL_PATTERN + return rf"{re.escape(board_user)}@[^\r\n]*[$#]\s?" + + def uboot_commands(cfg: dict[str, Any]) -> list[str]: raw = cfg.get("uboot_cmds") if isinstance(raw, list): @@ -309,13 +536,24 @@ def send_board_uboot_commands(cfg: dict[str, Any], term: Terminal) -> None: step_timeout = float(cfg.get("uboot_step_timeout", 0.0)) if step_timeout <= 0: step_timeout = max(initial_timeout, 60.0) - board_wait_uboot_prompt(term, uboot_ready, timeout=initial_timeout) + + autoboot_window = float(cfg.get("uboot_autoboot_window", 0.0)) + board_wait_uboot_prompt( + term, + uboot_ready, + timeout=initial_timeout, + autoboot_window=autoboot_window, + ) for index, cmd in enumerate(cmds): - if index > 0: - time.sleep(0.3) - board_wait_uboot_prompt(term, uboot_ready, timeout=step_timeout) - time.sleep(0.3) + send_offset = term.offset() term.send(cmd) + if index < len(cmds) - 1: + board_wait_uboot_prompt( + term, + uboot_ready, + timeout=step_timeout, + after_offset=send_offset, + ) def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: @@ -350,14 +588,7 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: board_term = build_terminal(cfg, log_path) board_term.open() cfg["_board_term"] = board_term - board_power_cycle(cfg) - time.sleep(3.0) - board_wake_console(board_term) - - send_board_uboot_commands(cfg, board_term) - zone0_timeout = float(cfg.get("zone0_shell_timeout", 180.0)) - if not board_term.wait_pattern(zone0_ready_pattern(cfg), timeout=zone0_timeout): - raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") + boot_board_zone0_shell(cfg, board_term, power_cycle=True) return 0 return 0 @@ -371,12 +602,6 @@ def lspci_expected_cfg(cfg: dict[str, Any]) -> dict[str, Any]: return {"expected_bdfs": expected_bdfs, "min_count": min_count} -def save_lspci_artifacts(cfg: dict[str, Any], name: str, content: str) -> None: - path = logs_dir(cfg) / name - path.write_text(content, encoding="utf-8") - print(f"[lspci] saved {path}", flush=True) - - def lspci(cfg: dict[str, Any], term: Terminal | None) -> int: print("————————————————\ncase: lspci\n————————————————\n", flush=True) if term is None: @@ -386,10 +611,10 @@ def lspci(cfg: dict[str, Any], term: Terminal | None) -> int: _, output = term.run( "lspci", - "timeout 20 lspci -D > /tmp/ci_lspci.log 2>&1; cat /tmp/ci_lspci.log", + board_priv(cfg, "timeout 20 lspci -D > /tmp/ci_lspci.log 2>&1; cat /tmp/ci_lspci.log"), timeout=60.0, ) - save_lspci_artifacts(cfg, "lspci.log", f"=== lspci -D ===\n{output}\n") + save_case_log(cfg, "lspci.log", f"=== lspci -D ===\n{output}\n") matched = [bdf for bdf in expected["expected_bdfs"] if bdf in output] missing = [bdf for bdf in expected["expected_bdfs"] if bdf not in output] @@ -421,36 +646,119 @@ def lspci(cfg: dict[str, Any], term: Terminal | None) -> int: def ping_success(output: str) -> bool: - if re.search(r"\b0% (?:packet )?loss\b", output): + if re.search(r"0%\s*(?:packet\s*)?loss\b", output, re.I): + return True + if re.search(r"0%\s*包丢失", output): return True - match = re.search(r"(\d+) packets? received", output) + match = re.search(r"(\d+)\s*packets?\s*received", output, re.I) + if match and int(match.group(1)) > 0: + return True + match = re.search(r"已接收\s*(\d+)\s*个包", output) return bool(match and int(match.group(1)) > 0) -def install_board_ssh_key(cfg: dict[str, Any], term: Terminal) -> str | None: - """Install a host-side private key on the board for scp pull. Returns key path on board.""" - key_path = cfg.get("deploy_board_ssh_key", "").strip() - if not key_path: - return None - host_key = Path(key_path).expanduser() - if not host_key.is_file(): - raise SystemExit(f"deploy board_ssh_key not found: {host_key}") - - board_key = f"/root/.ssh/{host_key.name}" - key_b64 = base64.b64encode(host_key.read_bytes()).decode("ascii") - cmd = ( - "mkdir -p /root/.ssh && chmod 700 /root/.ssh && " - f"echo {key_b64} | base64 -d > {board_key} && chmod 600 {board_key}" - ) - term.run("deploy_install_ssh_key", cmd, timeout=30.0) - print(f"[deploy] installed board ssh key at {board_key}", flush=True) - return board_key +def board_login_user(term: Terminal) -> str: + _, out = term.run("board_id", "id -un", timeout=15.0) + for line in out.splitlines(): + candidate = line.strip() + if candidate and "__R__" not in candidate and re.fullmatch(r"[\w.-]+", candidate): + return candidate + raise TerminalCommandError(f"failed to detect board login user: {out.strip()!r}") -def board_scp_identity(cfg: dict[str, Any], board_key: str | None) -> str: - if board_key: - return f"-i {board_key} " - return "" +def ensure_board_shell_user(cfg: dict[str, Any], term: Terminal) -> None: + """Switch shell user when board_shell_su_cmd is set in ci.yaml.""" + su_cmd = str(cfg.get("board_shell_su_cmd", "")).strip() + if not su_cmd: + return + target = str(cfg.get("board_user", "root")).strip() or "root" + if board_login_user(term) == target: + return + + shell_prompt = board_shell_ready_pattern(cfg) + password = str(cfg.get("board_su_password", "")) + print(f"[deploy] {su_cmd}", flush=True) + offset = term.offset() + term.send(su_cmd) + deadline = time.monotonic() + 20.0 + sent_password = False + while time.monotonic() < deadline: + chunk = term.tail_since(offset) + if not sent_password and re.search(BOARD_SU_PASSWORD_PROMPT, chunk): + term.send(password) + sent_password = True + if re.search(shell_prompt, chunk): + break + time.sleep(0.1) + else: + raise TerminalCommandError(f"timed out waiting for shell after {su_cmd!r}") + + # Serial su may print job-control errors; an extra enter wakes the ready shell. + time.sleep(0.3) + term.send("") + time.sleep(0.3) + if board_login_user(term) != target: + raise TerminalCommandError(f"failed to switch shell user to {target}") + + +def prepare_board_shell(cfg: dict[str, Any], term: Terminal) -> None: + """Enter board shell once: optional su, detect root/user, export HOME.""" + if cfg.get("_board_shell_ready"): + return + + ensure_board_shell_user(cfg, term) + user = board_login_user(term) + cfg["board_is_root"] = user == "root" + home = "/root" if cfg["board_is_root"] else f"/home/{user}" + cfg["board_home"] = home + term.run("board_home_export", f"export HOME={shlex.quote(home)}", timeout=15.0) + cfg["_board_shell_ready"] = True + print(f"[board] shell ready: user={user} home={home}", flush=True) + + +def board_ssh_opts(cfg: dict[str, Any], key: str, *, large_transfer: bool = False) -> str: + timeout = int(cfg.get("deploy_ssh_connect_timeout", 30)) + opts = ( + f"-i {key} -o BatchMode=yes -o ConnectTimeout={timeout} " + "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + ) + if large_transfer: + # ServerAlive probes can drop bulk transfers ("server not responding"). + opts += " -o ServerAliveInterval=0 -o TCPKeepAlive=yes" + else: + opts += " -o ServerAliveInterval=15 -o ServerAliveCountMax=8" + return opts + + +def deploy_file_pull_timeout(file_size: int, remaining: float) -> float: + """Per-file timeout from size (8 KiB/s floor) capped by the deploy deadline.""" + min_bps = 8192.0 + needed = max(90.0, file_size / min_bps + 45.0) + return min(remaining, needed) + + +def board_ssh_pull_file_cmd( + cfg: dict[str, Any], + key: str, + staging_dir: str, + name: str, + work_dir: str, + expected_size: int, + *, + large_transfer: bool = False, +) -> str: + """Pull one staged file over ssh cat (more reliable than scp on some boards).""" + host_ip = cfg["network_host_ip"] + host_user = cfg["network_host_user"] + opts = board_ssh_opts(cfg, key, large_transfer=large_transfer) + remote = f"{host_user}@{host_ip}" + remote_file = f"{staging_dir}/{name}" + local_file = f"{work_dir}/{name}" + return ( + f"ssh {opts} {shlex.quote(remote)} " + f"'cat {shlex.quote(remote_file)}' > {shlex.quote(local_file)} && " + f"test $(wc -c < {shlex.quote(local_file)}) -eq {expected_size}" + ) def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: @@ -458,19 +766,12 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: if term is None: raise SystemExit("terminal backend is required") - work_dir = cfg["zone1_work_dir"] + work_dir = shlex.quote(cfg["zone1_work_dir"]) boot_script = cfg["zone1_boot_script"] inner_log = "/tmp/zone1_inner.log" - _, _ = term.run("zone1_cd", f"cd {work_dir}", timeout=15.0) - _, _ = term.run("zone1_ls", "ls", timeout=15.0) - _, _ = term.run( - "zone1_chmod", - f"chmod +x {boot_script} hvisor check_serial.sh 2>/dev/null || true", - timeout=30.0, - ) - _, _ = term.run("zone1_cat_boot", f"cat {boot_script}", timeout=15.0) - boot_rc, _ = term.run("zone1_boot", f"./{boot_script}", timeout=120.0) - _, _ = term.run("zone1_list", "./hvisor zone list", timeout=15.0) + _, _ = term.run("zone1_ls", f"cd {work_dir} && ls", timeout=15.0) + boot_rc, _ = term.run("zone1_boot", f"cd {work_dir} && ./{boot_script}", timeout=120.0) + _, _ = term.run("zone1_list", f"cd {work_dir} && ./hvisor zone list", timeout=15.0) start_wait = float(cfg.get("zone1_start_wait", 0.0)) if start_wait > 0: @@ -481,10 +782,12 @@ def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: check_rc, _ = term.run( "zone1_serial_check", - f"./check_serial.sh /dev/pts/{max_pts} {inner_log} {int(ZONE1_INNER_PROMPT_TIMEOUT)}", + ( + f"cd {work_dir} && ./check_serial.sh /dev/pts/{max_pts} {inner_log} " + f"{int(ZONE1_INNER_PROMPT_TIMEOUT)}" + ), timeout=ZONE1_INNER_PROMPT_TIMEOUT + 30.0, ) - # Fetch via tail to limit serial traffic; generous timeout for slow Jenkins consoles. _, inner_output = term.run( "zone1_inner_log", f"tail -c 131072 {inner_log}", @@ -542,13 +845,9 @@ def cfg_str(key: str, default: str = "") -> str: raw = deploy_cfg.get(key, tests.get(key, default)) return str(raw).strip() if raw is not None else default - deploy_method = str(deploy_cfg.get("method", "")).strip() - if deploy_method and deploy_method != "board_pull": - raise SystemExit(f"unsupported deploy method '{deploy_method}', only board_pull is supported") board_ip = cfg_str("board_ip") - copy_hvisor_bin = deploy_cfg.get("copy_hvisor_bin", True) - if isinstance(copy_hvisor_bin, str): - copy_hvisor_bin = copy_hvisor_bin.lower() not in ("0", "false", "no") + board_user = cfg_str("board_user", "root") or "root" + default_work_dir = "/root" if board_user == "root" else f"/home/{board_user}" hvisor_tool_path = os.environ.get("HVISOR_TOOL_PATH", "").strip() if not hvisor_tool_path: hvisor_tool_path = str((cell_root / "hvisor-tool").resolve()) @@ -573,25 +872,29 @@ def cfg_str(key: str, default: str = "") -> str: "uboot_ready_pattern": str(tests.get("uboot_ready_pattern", "")).strip(), "uboot_prompt_timeout": float(tests.get("uboot_prompt_timeout", 20.0)), "uboot_step_timeout": float(tests.get("uboot_step_timeout", 0.0)), + "uboot_autoboot_window": float(tests.get("uboot_autoboot_window", 0.0)), "zone0_ready_pattern": str(tests.get("zone0_ready_pattern", "")).strip(), + "board_shell_ready_pattern": str(tests.get("board_shell_ready_pattern", "")).strip(), + "board_shell_su_cmd": str(tests.get("board_shell_su_cmd", "")).strip(), "zone0_shell_timeout": float(tests.get("zone0_shell_timeout", 300.0 if mode == "board" else 180.0)), "tftp_dir": str(tests.get("tftp_dir", "/home/light/tftp")).strip(), "lspci": tests.get("lspci") or {}, "board_ip": board_ip, + "board_user": board_user, "board_iface": cfg_str("board_iface", "eth0"), - "zone1_work_dir": cfg_str("zone1_work_dir", "/root"), + "board_netmask": cfg_str("netmask", "255.255.255.0"), + "board_su_password": cfg_str("su_password", ""), + "zone1_work_dir": cfg_str("zone1_work_dir", default_work_dir), "zone1_boot_script": cfg_str("zone1_boot_script", "boot_zone1.sh"), "network_host_ip": cfg_str("host_ip", "192.168.1.181"), "network_host_user": cfg_str("host_user", "light"), - "network_staging_dir": cfg_str("staging_dir", "/home/light/tftp/ci_deploy"), + "network_staging_dir": cfg_str("staging_dir", "/home/light/ci_deploy"), "network_ping_count": int(deploy_cfg.get("ping_count", tests.get("ping_count", 3))), "deploy_link_wait": float(deploy_cfg.get("link_wait", 0.0)), "deploy_ping_retries": int(deploy_cfg.get("ping_retries", 1)), - "deploy_board_ssh_key": cfg_str( - "board_ssh_key", "/home/light/.ssh/hvisor_board_pull" - ), + "deploy_ssh_connect_timeout": int(deploy_cfg.get("ssh_connect_timeout", 30)), + "deploy_pull_timeout": float(deploy_cfg.get("pull_timeout", 600.0)), "zone1_dtb": cfg_str("zone1_dtb"), - "copy_hvisor_bin": bool(copy_hvisor_bin), "zone1_start_wait": float(tests.get("zone1_start_wait", 30.0)), } diff --git a/jenkins/terminal.py b/jenkins/terminal.py index f94c286c5..d4591d93a 100644 --- a/jenkins/terminal.py +++ b/jenkins/terminal.py @@ -22,6 +22,10 @@ class TerminalTimeoutError(TimeoutError): """Raised when terminal command wait times out.""" + def __init__(self, message: str, *, partial_output: str = "") -> None: + super().__init__(message) + self.partial_output = partial_output + class TerminalCommandError(RuntimeError): """Raised when a terminal command exits with non-zero status.""" @@ -313,6 +317,10 @@ def offset(self) -> int: self._ensure_open() return self._collector.offset() + def tail_since(self, offset: int) -> str: + self._ensure_open() + return self._collector.tail_since(offset) + def send(self, command: str) -> None: self._ensure_open() payload = command.rstrip("\n") + "\n" @@ -350,8 +358,10 @@ def run( next_wake = time.monotonic() + wake_interval time.sleep(poll_interval) + partial = self._collector.tail_since(offset) raise TerminalTimeoutError( - f"timed out waiting for run result (case={case}, run_id={run_id}): {command}" + f"timed out waiting for run result (case={case}, run_id={run_id}): {command}", + partial_output=partial, ) def wait_pattern( From fd7f09b051f4db48bea054cbb9eff9c6e6d734ad Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 11 Aug 2026 19:04:08 +0800 Subject: [PATCH 49/53] fix ci bug --- jenkins/ci.yaml | 7 +- jenkins/ci_runner.py | 74 +++++++++++-------- .../aarch64/phytium-pi/scripts/boot_zone1.sh | 2 +- 3 files changed, 46 insertions(+), 37 deletions(-) diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index 1a50b01ec..1da24daf6 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -49,6 +49,7 @@ bids: host_ip: 192.168.1.181 host_user: light staging_dir: /home/light/ci_deploy + zone1_work_dir: /root ssh_connect_timeout: 60 zone1_dtb: platform/aarch64/rk3568/image/dts/rk3568_limit_zone1.dtb lspci: @@ -79,9 +80,7 @@ bids: - platform/aarch64/phytium-pi/image/dts/phytium-pi-board-v2.dtb zone0_image: /home/light/DEMO/phytium-pi-external/Image uboot_ready_pattern: 'Phytium-Pi#' - zone0_ready_pattern: 'user@[^\r\n]*[$#]\s?|root@[^\r\n]*:\/#\s?|(?:\r?\n)#\s*(?:\r?\n|$)' - board_shell_su_cmd: 'su - user -P' - board_shell_ready_pattern: 'user@[^\r\n]*[$#]\s?' + zone0_ready_pattern: 'root@[^\r\n]*:\/#\s?|(?:\r?\n)#\s*(?:\r?\n|$)' zone0_shell_timeout: 300 uboot_prompt_timeout: 60 uboot_step_timeout: 600 @@ -94,7 +93,6 @@ bids: - tftp ${zone0_fdt_addr} ${serverip}:phytium-pi/linux1.dtb - bootm ${loadaddr} - ${fdt_addr} deploy: - board_user: user board_ip: 192.168.1.179 board_iface: eth0 link_wait: 8 @@ -102,6 +100,7 @@ bids: host_ip: 192.168.1.181 host_user: light staging_dir: /home/light/ci_deploy + zone1_work_dir: /root pull_timeout: 900 zone1_dtb: platform/aarch64/phytium-pi/image/dts/linux2.dtb cases: diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 3780001b0..65c155a0c 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -232,7 +232,7 @@ def board_sudo(cmd: str) -> str: def board_priv(cfg: dict[str, Any], cmd: str) -> str: """Run privileged board commands; skip sudo when the shell is already root.""" - if cfg.get("board_is_root"): + if cfg.get("board_is_root") or cfg.get("mode") == "qemu": return cmd.strip() return board_sudo(cmd) @@ -328,6 +328,27 @@ def board_pubkey_from_output(output: str) -> str | None: return None +def _export_board_ssh_env(cfg: dict[str, Any], term: Terminal) -> None: + if cfg.get("_board_ssh_env"): + return + key, _, _ = board_ssh_paths(cfg) + host = f"{cfg['network_host_user']}@{cfg['network_host_ip']}" + term.run( + "board_ssh_env", + f"export CI_SSH_KEY={shlex.quote(key)} CI_SSH_HOST={shlex.quote(host)}", + timeout=15.0, + ) + cfg["_board_ssh_env"] = True + + +def _board_ssh_opts_short(cfg: dict[str, Any], *, large_transfer: bool = False) -> str: + timeout = int(cfg.get("deploy_ssh_connect_timeout", 30)) + opts = f"-o BatchMode=yes -o ConnectTimeout={timeout} -o StrictHostKeyChecking=no" + if large_transfer: + opts += " -o ServerAliveInterval=0 -o TCPKeepAlive=yes" + return opts + + def _board_ssh_probe(cfg: dict[str, Any], term: Terminal) -> None: key, pub, _ = board_ssh_paths(cfg) key_rc, _ = term.run( @@ -341,14 +362,14 @@ def _board_ssh_probe(cfg: dict[str, Any], term: Terminal) -> None: "copy or generate ~/.ssh/id_ed25519 on the board manually." ) + _export_board_ssh_env(cfg, term) host_user = cfg["network_host_user"] host_ip = cfg["network_host_ip"] - host = shlex.quote(f"{host_user}@{host_ip}") - opts = board_ssh_opts(cfg, key) + opts = _board_ssh_opts_short(cfg) probe_timeout = float(int(cfg.get("deploy_ssh_connect_timeout", 30)) + 30) probe_rc, probe_out = term.run( "deploy_ssh_probe", - board_priv(cfg, f"ssh {opts} {host} true"), + board_priv(cfg, f"ssh -i $CI_SSH_KEY {opts} $CI_SSH_HOST true int: if not staged_files: raise TerminalCommandError(f"no staged files in {staging_path}") - key, _, _ = board_ssh_paths(cfg) pull_timeout = float(cfg.get("deploy_pull_timeout", 600.0)) deadline = time.monotonic() + pull_timeout all_logs: list[str] = [] @@ -416,9 +436,10 @@ def deploy_board_pull(cfg: dict[str, Any], term: Terminal) -> int: for gz_name, raw_name in DEPLOY_GUNZIP_ARTIFACTS.items(): if gz_name in stale_names: stale_names.append(raw_name) - stale_paths = " ".join(shlex.quote(f"{work_dir}/{name}") for name in stale_names) print(f"[deploy] removing stale files in {work_dir}", flush=True) - term.run("deploy_clean", board_priv(cfg, f"rm -f {stale_paths}"), timeout=30.0) + for name in stale_names: + stale_path = shlex.quote(f"{work_dir}/{name}") + term.run(f"deploy_clean_{name}", board_priv(cfg, f"rm -f {stale_path}"), timeout=15.0) print( f"[deploy] pulling {len(staged_files)} file(s) via ssh from {host_user}@{host_ip}:{staging_dir}", @@ -437,7 +458,7 @@ def deploy_board_pull(cfg: dict[str, Any], term: Terminal) -> int: file_timeout = deploy_file_pull_timeout(file_size, remaining) large_transfer = file_size >= DEPLOY_LARGE_PULL_BYTES pull_cmd = board_ssh_pull_file_cmd( - cfg, key, staging_dir, name, work_dir, file_size, large_transfer=large_transfer + cfg, staging_dir, name, work_dir, file_size, large_transfer=large_transfer ) print( f"[deploy] pull ({index}/{len(staged_files)}): {name} ({file_size} bytes, timeout={file_timeout:.0f}s)", @@ -481,7 +502,7 @@ def deploy_board_pull(cfg: dict[str, Any], term: Terminal) -> int: "deploy_chmod", board_priv( cfg, - f"chmod +x {work_dir}/{boot_script} {work_dir}/check_serial.sh 2>/dev/null || true", + f"chmod +x {work_dir}/{boot_script} {work_dir}/hvisor {work_dir}/check_serial.sh 2>/dev/null || true", ), timeout=60.0, ) @@ -580,6 +601,9 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: qemu_term.send(cmd) if not qemu_term.wait_pattern(zone0_ready_pattern(cfg), timeout=180.0): raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") + cfg["board_is_root"] = True + cfg["board_home"] = "/root" + cfg["_board_shell_ready"] = True return 0 if cfg["mode"] == "board": log_path = logs_dir(cfg) / "zone0_console.log" @@ -716,20 +740,6 @@ def prepare_board_shell(cfg: dict[str, Any], term: Terminal) -> None: print(f"[board] shell ready: user={user} home={home}", flush=True) -def board_ssh_opts(cfg: dict[str, Any], key: str, *, large_transfer: bool = False) -> str: - timeout = int(cfg.get("deploy_ssh_connect_timeout", 30)) - opts = ( - f"-i {key} -o BatchMode=yes -o ConnectTimeout={timeout} " - "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" - ) - if large_transfer: - # ServerAlive probes can drop bulk transfers ("server not responding"). - opts += " -o ServerAliveInterval=0 -o TCPKeepAlive=yes" - else: - opts += " -o ServerAliveInterval=15 -o ServerAliveCountMax=8" - return opts - - def deploy_file_pull_timeout(file_size: int, remaining: float) -> float: """Per-file timeout from size (8 KiB/s floor) capped by the deploy deadline.""" min_bps = 8192.0 @@ -739,7 +749,6 @@ def deploy_file_pull_timeout(file_size: int, remaining: float) -> float: def board_ssh_pull_file_cmd( cfg: dict[str, Any], - key: str, staging_dir: str, name: str, work_dir: str, @@ -747,16 +756,13 @@ def board_ssh_pull_file_cmd( *, large_transfer: bool = False, ) -> str: - """Pull one staged file over ssh cat (more reliable than scp on some boards).""" - host_ip = cfg["network_host_ip"] - host_user = cfg["network_host_user"] - opts = board_ssh_opts(cfg, key, large_transfer=large_transfer) - remote = f"{host_user}@{host_ip}" + """Pull one staged file over ssh cat using CI_SSH_KEY/CI_SSH_HOST env vars.""" remote_file = f"{staging_dir}/{name}" local_file = f"{work_dir}/{name}" + opts = _board_ssh_opts_short(cfg, large_transfer=large_transfer) return ( - f"ssh {opts} {shlex.quote(remote)} " - f"'cat {shlex.quote(remote_file)}' > {shlex.quote(local_file)} && " + f"ssh -i $CI_SSH_KEY {opts} $CI_SSH_HOST " + f"'cat {shlex.quote(remote_file)}' > {shlex.quote(local_file)} int: inner_log = "/tmp/zone1_inner.log" _, _ = term.run("zone1_ls", f"cd {work_dir} && ls", timeout=15.0) boot_rc, _ = term.run("zone1_boot", f"cd {work_dir} && ./{boot_script}", timeout=120.0) - _, _ = term.run("zone1_list", f"cd {work_dir} && ./hvisor zone list", timeout=15.0) + if str(cfg.get("board_user", "root")).strip() not in ("", "root"): + zone_list_cmd = f"cd {work_dir} && sudo ./hvisor zone list" + else: + zone_list_cmd = f"cd {work_dir} && ./hvisor zone list" + _, _ = term.run("zone1_list", zone_list_cmd, timeout=15.0) start_wait = float(cfg.get("zone1_start_wait", 0.0)) if start_wait > 0: diff --git a/platform/aarch64/phytium-pi/scripts/boot_zone1.sh b/platform/aarch64/phytium-pi/scripts/boot_zone1.sh index ab255dc74..442b107d2 100755 --- a/platform/aarch64/phytium-pi/scripts/boot_zone1.sh +++ b/platform/aarch64/phytium-pi/scripts/boot_zone1.sh @@ -6,5 +6,5 @@ mount -t sysfs sysfs /sys 2>/dev/null || true rm -f nohup.out mkdir -p /dev/pts mount -t devpts devpts /dev/pts 2>/dev/null || true -nohup ./hvisor virtio start zone1-linux-virtio.json & +./hvisor virtio start zone1-linux-virtio.json & ./hvisor zone start zone1-linux.json From 610acb39d4a2d31506f8fd2a72d242e8a7a86384 Mon Sep 17 00:00:00 2001 From: dallas Date: Tue, 11 Aug 2026 23:40:13 +0800 Subject: [PATCH 50/53] allow ci use hvisor-tool from test branch and add warning --- Jenkinsfile | 93 +++++++++++++++++++++++++++++++++------- jenkins/ci_runner.py | 75 +++++++++++++++++++++++++------- jenkins/hvisor-tool.yaml | 23 ++++++++++ tools/license_checker.sh | 3 +- 4 files changed, 162 insertions(+), 32 deletions(-) create mode 100644 jenkins/hvisor-tool.yaml diff --git a/Jenkinsfile b/Jenkinsfile index 56f47631c..d7505b087 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -88,6 +88,55 @@ def loadCiYaml() { return data } +def loadHvisorToolConfig() { + def path = 'jenkins/hvisor-tool.yaml' + def defaults = [ + repo: env.HVISOR_TOOL_URL ?: 'https://github.com/syswonder/hvisor-tool.git', + ref : 'main', + ] + if (!fileExists(path)) { + return defaults + } + def data = readYaml file: path + def repo = (data?.repo ?: defaults.repo).toString().trim() + def ref = (data?.ref ?: defaults.ref).toString().trim() + if (!repo || !ref) { + error("${path}: 'repo' and 'ref' must be non-empty") + } + return [repo: repo, ref: ref] +} + +def checkoutHvisorTool(Map toolCfg, String targetDir) { + def url = toolCfg.repo.toString().trim() + def ref = (toolCfg.ref ?: '').trim() + + if (ref ==~ /^[0-9a-fA-F]{7,40}$/) { + sh """ + set -eux + rm -rf '${targetDir}' + git init '${targetDir}' + git -C '${targetDir}' remote add origin '${url}' + git -C '${targetDir}' fetch --depth 50 origin '${ref}' + git -C '${targetDir}' checkout FETCH_HEAD + """ + } else if (ref.startsWith('refs/')) { + sh """ + set -eux + rm -rf '${targetDir}' + git init '${targetDir}' + git -C '${targetDir}' remote add origin '${url}' + git -C '${targetDir}' fetch --depth 1 origin '${ref}:ci-ref' + git -C '${targetDir}' checkout ci-ref + """ + } else { + sh """ + set -eux + rm -rf '${targetDir}' + git clone --depth 1 --branch '${ref}' '${url}' '${targetDir}' + """ + } +} + def getBidConfig(ci, String bid) { return (ci.bids ?: []).find { entry -> return (entry?.bid ?: '').toString().trim() == bid @@ -159,10 +208,6 @@ def publishMatrixCheckInProgress() { publishGithubCheckInProgress(matrixCheckName()) } -def publishMatrixCheckCompleted(String conclusion) { - publishGithubCheckCompleted(matrixCheckName(), conclusion) -} - def finishGithubCheck(String checkName, String buildResult) { if (!isGithubCheckStarted(checkName)) { echo "Skip GitHub check completion for '${checkName}' (in-progress was never published)" @@ -262,6 +307,16 @@ pipeline { } } + stage('Checkout hvisor-tool') { + steps { + script { + def toolCfg = loadHvisorToolConfig() + echo "Checkout hvisor-tool [repo=${toolCfg.repo}, ref=${toolCfg.ref}]" + checkoutHvisorTool(toolCfg, env.HVISOR_TOOL_PATH) + } + } + } + stage('License checker') { steps { script { @@ -364,17 +419,6 @@ pipeline { } echo "Build hvisor-tool [BID=${env.BID}, TARCH=${tarch}, KDIR=${kdir}]" - if (!fileExists(env.HVISOR_TOOL_PATH)) { - sh "mkdir -p ${env.HVISOR_TOOL_PATH}" - } - dir(env.HVISOR_TOOL_PATH) { - checkout([ - $class: 'GitSCM', - branches: [[name: '*/main']], - extensions: [[$class: 'CloneOption', depth: 1, noTags: true]], - userRemoteConfigs: [[url: env.HVISOR_TOOL_URL]] - ]) - } sh """ export PATH=${env.TOOLCHAIN_PATHS}:\$PATH make -C ${env.HVISOR_TOOL_PATH} all ARCH=${tarch} KDIR=${kdir} @@ -494,5 +538,24 @@ pipeline { } } } + + stage('hvisor-tool on the branch of test') { + steps { + script { + def checkName = 'hvisor-tool on the branch of test' + publishGithubCheckInProgress(checkName) + def ref = loadHvisorToolConfig().ref + echo "Verify hvisor-tool pin [ref=${ref}]" + if (ref != 'main') { + error("hvisor-tool ref must be main for merge (current ref=${ref}). Restore ref: main in jenkins/hvisor-tool.yaml.") + } + } + } + post { + always { + script { finishGithubCheck('hvisor-tool on the branch of test', currentBuild.currentResult) } + } + } + } } } diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 65c155a0c..470c03098 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -406,8 +406,31 @@ def boot_board_zone0_shell(cfg: dict[str, Any], term: Terminal, *, power_cycle: prepare_board_shell(cfg, term) +def board_term_run( + cfg: dict[str, Any], + term: Terminal, + case_id: str, + command: str, + timeout: float, + *, + retries: int = 1, +) -> tuple[int, str]: + last_exc: TerminalTimeoutError | None = None + for attempt in range(retries + 1): + if attempt > 0: + print(f"[deploy] retry {case_id} ({attempt}/{retries})", flush=True) + time.sleep(3.0) + run_id = case_id if attempt == 0 else f"{case_id}_retry{attempt}" + try: + return term.run(run_id, board_priv(cfg, command), timeout=timeout) + except TerminalTimeoutError as exc: + last_exc = exc + assert last_exc is not None + raise last_exc + + def deploy_board_pull(cfg: dict[str, Any], term: Terminal) -> int: - """Board pulls staged zone1 files from the CI host over ssh.""" + """Board pulls staged zone1 files from the CI host over scp.""" ensure_board_net(cfg, term) host_ip = cfg["network_host_ip"] @@ -415,7 +438,7 @@ def deploy_board_pull(cfg: dict[str, Any], term: Terminal) -> int: staging_dir = cfg["network_staging_dir"] work_dir = cfg["zone1_work_dir"] - print(f"[deploy] staging files on host", flush=True) + print("[deploy] staging files on host", flush=True) stage_board_files(cfg) staging_path = Path(staging_dir) @@ -442,10 +465,12 @@ def deploy_board_pull(cfg: dict[str, Any], term: Terminal) -> int: term.run(f"deploy_clean_{name}", board_priv(cfg, f"rm -f {stale_path}"), timeout=15.0) print( - f"[deploy] pulling {len(staged_files)} file(s) via ssh from {host_user}@{host_ip}:{staging_dir}", + f"[deploy] pulling {len(staged_files)} file(s) via scp from {host_user}@{host_ip}:{staging_dir}", flush=True, ) for index, name in enumerate(staged_files, start=1): + if index > 1: + time.sleep(2.0) remaining = deadline - time.monotonic() if remaining <= 0: save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) @@ -457,25 +482,44 @@ def deploy_board_pull(cfg: dict[str, Any], term: Terminal) -> int: file_size = staging_path.joinpath(name).stat().st_size file_timeout = deploy_file_pull_timeout(file_size, remaining) large_transfer = file_size >= DEPLOY_LARGE_PULL_BYTES - pull_cmd = board_ssh_pull_file_cmd( - cfg, staging_dir, name, work_dir, file_size, large_transfer=large_transfer + pull_cmd = board_ssh_scp_pull_file_cmd( + cfg, staging_dir, name, work_dir, large_transfer=large_transfer ) print( - f"[deploy] pull ({index}/{len(staged_files)}): {name} ({file_size} bytes, timeout={file_timeout:.0f}s)", + f"[deploy] scp ({index}/{len(staged_files)}): {name} ({file_size} bytes, timeout={file_timeout:.0f}s)", flush=True, ) try: - pull_rc, pull_out = term.run(f"deploy_pull_{name}", pull_cmd, timeout=file_timeout) + pull_rc, pull_out = board_term_run( + cfg, term, f"deploy_scp_{name}", pull_cmd, file_timeout, retries=1 + ) except TerminalTimeoutError as exc: pull_out = exc.partial_output or "" - all_logs.append(f"=== deploy_pull_{name} (timed out) ===\n{pull_out}\n") + all_logs.append(f"=== deploy_scp_{name} (timed out) ===\n{pull_out}\n") save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) raise - all_logs.append(f"=== deploy_pull_{name} ===\n{pull_out}\n") + all_logs.append(f"=== deploy_scp_{name} ===\n{pull_out}\n") if pull_rc != 0: save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) raise TerminalCommandError( - f"board pull failed for {name!r} with rc={pull_rc}: {pull_out.strip()}" + f"board scp failed for {name!r} with rc={pull_rc}: {pull_out.strip()}" + ) + + verify_cmd = f"test $(wc -c < {shlex.quote(f'{work_dir}/{name}')}) -eq {file_size}" + try: + verify_rc, verify_out = board_term_run( + cfg, term, f"deploy_verify_{name}", verify_cmd, 30.0, retries=1 + ) + except TerminalTimeoutError as exc: + verify_out = exc.partial_output or "" + all_logs.append(f"=== deploy_verify_{name} (timed out) ===\n{verify_out}\n") + save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) + raise + all_logs.append(f"=== deploy_verify_{name} ===\n{verify_out}\n") + if verify_rc != 0: + save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) + raise TerminalCommandError( + f"board scp verify failed for {name!r} with rc={verify_rc}: {verify_out.strip()}" ) raw_name = DEPLOY_GUNZIP_ARTIFACTS.get(name) @@ -747,23 +791,22 @@ def deploy_file_pull_timeout(file_size: int, remaining: float) -> float: return min(remaining, needed) -def board_ssh_pull_file_cmd( +def board_ssh_scp_pull_file_cmd( cfg: dict[str, Any], staging_dir: str, name: str, work_dir: str, - expected_size: int, *, large_transfer: bool = False, ) -> str: - """Pull one staged file over ssh cat using CI_SSH_KEY/CI_SSH_HOST env vars.""" + """Pull one staged file over scp using CI_SSH_KEY/CI_SSH_HOST env vars.""" remote_file = f"{staging_dir}/{name}" local_file = f"{work_dir}/{name}" opts = _board_ssh_opts_short(cfg, large_transfer=large_transfer) return ( - f"ssh -i $CI_SSH_KEY {opts} $CI_SSH_HOST " - f"'cat {shlex.quote(remote_file)}' > {shlex.quote(local_file)} /head, refs/tags/, or commit SHA. +# +# Examples (uncomment one repo/ref pair): +# +# repo: https://github.com/syswonder/hvisor-tool.git +# ref: main +# +# repo: https://github.com/syswonder/hvisor-tool.git +# ref: feature/my-change +# +# repo: https://github.com/syswonder/hvisor-tool.git +# ref: refs/pull/123/head +# +# repo: https://github.com/syswonder/hvisor-tool.git +# ref: v0.3.1 +# +# repo: https://github.com/alice/hvisor-tool.git +# ref: my-branch +# +# "hvisor-tool on the branch of test" fails unless ref is main. +repo: https://github.com/syswonder/hvisor-tool.git +ref: refs/pull/117/head diff --git a/tools/license_checker.sh b/tools/license_checker.sh index 5a4fdb5d4..e9f004887 100755 --- a/tools/license_checker.sh +++ b/tools/license_checker.sh @@ -12,7 +12,8 @@ while read -r file; do fi done < <(find . -type f -name "*.rs" \ -not \( -path "./vendor/*" -prune \) \ - -not \( -path "./target/*" -prune \)) + -not \( -path "./target/*" -prune \) \ + -not \( -path "./hvisor-tool/*" -prune \)) echo $ec exit $ec \ No newline at end of file From 555c2a847a253bb86221682f215d358c41b60815 Mon Sep 17 00:00:00 2001 From: dallas Date: Wed, 12 Aug 2026 02:34:19 +0800 Subject: [PATCH 51/53] spilt file when scp --- jenkins/board_stage.sh | 20 +++ jenkins/ci_runner.py | 370 ++++++++++++++++++++++++++++------------- 2 files changed, 275 insertions(+), 115 deletions(-) diff --git a/jenkins/board_stage.sh b/jenkins/board_stage.sh index 509d43fe9..782448388 100755 --- a/jenkins/board_stage.sh +++ b/jenkins/board_stage.sh @@ -77,5 +77,25 @@ fi chmod -R a+rX "${STAGING_DIR}" chmod +x "${STAGING_DIR}/boot_zone1.sh" + +if [ "$(id -u)" -eq 0 ] && id light >/dev/null 2>&1; then + chown -R light:light "${STAGING_DIR}" +fi + +DEPLOY_SPLIT_CHUNK_BYTES=${DEPLOY_SPLIT_CHUNK_BYTES:-262144} +for staged_file in "${STAGING_DIR}"/*; do + [ -f "${staged_file}" ] || continue + staged_name=$(basename "${staged_file}") + case "${staged_name}" in + *.part.*) continue ;; + esac + size=$(stat -c%s "${staged_file}") + if [ "${size}" -gt "${DEPLOY_SPLIT_CHUNK_BYTES}" ]; then + split -b "${DEPLOY_SPLIT_CHUNK_BYTES}" "${staged_file}" "${STAGING_DIR}/${staged_name}.part." + rm -f "${staged_file}" + echo "split ${staged_name}: ${size} bytes into ${DEPLOY_SPLIT_CHUNK_BYTES}-byte chunks" + fi +done + echo "board staging completed: ${STAGING_DIR}" ls -la "${STAGING_DIR}" diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 470c03098..5bc7e313c 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -24,6 +24,9 @@ ZONE1_INNER_LOG_FETCH_TIMEOUT = 30.0 DEPLOY_GUNZIP_ARTIFACTS = {"hvisor.gz": "hvisor"} DEPLOY_LARGE_PULL_BYTES = 512 * 1024 +DEPLOY_BOARD_TMP_DIR = "/tmp/ci_pull" +DEPLOY_SPLIT_PART_RE = re.compile(r"^(.+)\.part\.[a-z]{2}$") +BOARD_HOME = "~" BOARD_PUBKEY_LINE = re.compile(r"^ssh-(?:ed25519|rsa)\s+\S+") BOARD_ROOT_SHELL_PATTERN = r"root@[^\r\n]*:\/#\s?|(?:\r?\n)#\s?(?:\r?\n|$)" BOARD_SU_PASSWORD_PROMPT = r"Password:|password:" @@ -237,12 +240,20 @@ def board_priv(cfg: dict[str, Any], cmd: str) -> str: return board_sudo(cmd) -def board_ssh_paths(cfg: dict[str, Any]) -> tuple[str, str, str]: - """Absolute SSH key paths from the prepared board shell home.""" - home = str(cfg.get("board_home") or "/root") - ssh_dir = f"{home}/.ssh" - key = f"{ssh_dir}/id_ed25519" - return key, f"{key}.pub", ssh_dir +def board_path(work_dir: str, *parts: str) -> str: + """Build a shell path; ~ is left for the board shell to expand.""" + rel = "/".join(parts) + if work_dir == BOARD_HOME: + return f"{BOARD_HOME}/{rel}" if rel else BOARD_HOME + return f"{work_dir}/{rel}" if rel else work_dir + + +def deploy_pull_tmp_path(name: str) -> str: + return f"{DEPLOY_BOARD_TMP_DIR}/{name.replace('/', '_')}" + + +def board_ssh_host(cfg: dict[str, Any]) -> str: + return f"{cfg['network_host_user']}@{cfg['network_host_ip']}" def _setup_board_interface(cfg: dict[str, Any], term: Terminal) -> None: @@ -331,11 +342,9 @@ def board_pubkey_from_output(output: str) -> str | None: def _export_board_ssh_env(cfg: dict[str, Any], term: Terminal) -> None: if cfg.get("_board_ssh_env"): return - key, _, _ = board_ssh_paths(cfg) - host = f"{cfg['network_host_user']}@{cfg['network_host_ip']}" term.run( "board_ssh_env", - f"export CI_SSH_KEY={shlex.quote(key)} CI_SSH_HOST={shlex.quote(host)}", + f"export CI_SSH_HOST={shlex.quote(board_ssh_host(cfg))}", timeout=15.0, ) cfg["_board_ssh_env"] = True @@ -350,18 +359,6 @@ def _board_ssh_opts_short(cfg: dict[str, Any], *, large_transfer: bool = False) def _board_ssh_probe(cfg: dict[str, Any], term: Terminal) -> None: - key, pub, _ = board_ssh_paths(cfg) - key_rc, _ = term.run( - "board_ssh_check", - board_priv(cfg, f"test -f {shlex.quote(key)}"), - timeout=15.0, - ) - if key_rc != 0: - raise TerminalCommandError( - f"board ssh private key not found: {key}\n" - "copy or generate ~/.ssh/id_ed25519 on the board manually." - ) - _export_board_ssh_env(cfg, term) host_user = cfg["network_host_user"] host_ip = cfg["network_host_ip"] @@ -369,7 +366,7 @@ def _board_ssh_probe(cfg: dict[str, Any], term: Terminal) -> None: probe_timeout = float(int(cfg.get("deploy_ssh_connect_timeout", 30)) + 30) probe_rc, probe_out = term.run( "deploy_ssh_probe", - board_priv(cfg, f"ssh -i $CI_SSH_KEY {opts} $CI_SSH_HOST true None: pub_rc, pub_out = term.run( "board_ssh_pubkey", - board_priv(cfg, f"cat {shlex.quote(pub)}"), + board_priv( + cfg, + "cat ~/.ssh/id_ed25519.pub 2>/dev/null || cat ~/.ssh/id_rsa.pub 2>/dev/null || true", + ), timeout=15.0, ) pubkey = board_pubkey_from_output(pub_out) if pub_rc == 0 else None if not pubkey: raise TerminalCommandError( - f"ssh probe to {host_user}@{host_ip} failed and board public key not found: {pub}\n" + f"ssh probe to {host_user}@{host_ip} failed and board public key not found\n" f"{probe_out.strip()}" ) @@ -406,6 +406,194 @@ def boot_board_zone0_shell(cfg: dict[str, Any], term: Terminal, *, power_cycle: prepare_board_shell(cfg, term) +def resolve_staging_dir(raw: str, arch: str, board: str) -> str: + """Per-board staging path so parallel matrix cells do not clobber each other.""" + base = (raw or "/home/light/ci_deploy").rstrip("/") + suffix = f"{arch}__{board.replace('/', '__')}" + if base.endswith(suffix): + return base + return f"{base}/{suffix}" + + +def deploy_ensure_work_dir(cfg: dict[str, Any], term: Terminal) -> str: + """Create zone1 work dir; fall back to ~ when configured dir is not writable.""" + preferred = cfg["zone1_work_dir"] + candidates = [preferred] + if preferred != BOARD_HOME: + candidates.append(BOARD_HOME) + + board_term_run( + cfg, term, "deploy_tmpdir", f"mkdir -p {DEPLOY_BOARD_TMP_DIR}", 15.0, retries=0 + ) + + last_out = "" + for path in candidates: + probe = board_path(path, ".ci_probe") + cmd = f"mkdir -p {board_path(path)} && touch {probe} && rm -f {probe}" + rc, last_out = board_term_run(cfg, term, "deploy_mkdir", cmd, 30.0, retries=1) + if rc == 0: + if path != preferred: + print(f"[deploy] warning: {preferred} unavailable, using {BOARD_HOME}", flush=True) + else: + print(f"[deploy] work dir ready: {path}", flush=True) + cfg["zone1_work_dir"] = path + return path + raise TerminalCommandError(f"board work dir setup failed for {candidates!r}: {last_out.strip()}") + + +def parse_staging(staging_path: Path) -> tuple[list[str], dict[str, list[str]], list[str]]: + regular: list[str] = [] + split_groups: dict[str, list[str]] = {} + + for entry in staging_path.iterdir(): + if not entry.is_file(): + continue + name = entry.name + match = DEPLOY_SPLIT_PART_RE.match(name) + if match: + split_groups.setdefault(match.group(1), []).append(name) + continue + regular.append(name) + + for base in split_groups: + split_groups[base] = sorted(split_groups[base]) + parts = [part for part_names in split_groups.values() for part in part_names] + pull_files = sorted( + regular + parts, + key=lambda name: staging_path.joinpath(name).stat().st_size, + ) + return regular, split_groups, pull_files + + +def deploy_fail(cfg: dict[str, Any], all_logs: list[str], section: str, output: str) -> None: + all_logs.append(f"=== {section} ===\n{output}\n") + save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) + + +def deploy_board_cmd( + cfg: dict[str, Any], + term: Terminal, + all_logs: list[str], + case_id: str, + command: str, + timeout: float, + error: str, + *, + retries: int = 1, +) -> str: + try: + rc, out = board_term_run(cfg, term, case_id, command, timeout, retries=retries) + except TerminalTimeoutError as exc: + deploy_fail(cfg, all_logs, f"{case_id} (timed out)", exc.partial_output or "") + raise + if rc != 0: + deploy_fail(cfg, all_logs, case_id, out) + raise TerminalCommandError(f"{error}: {out.strip()}") + all_logs.append(f"=== {case_id} ===\n{out}\n") + return out + + +def deploy_board_pull_file( + cfg: dict[str, Any], + term: Terminal, + staging_path: Path, + staging_dir: str, + work_dir: str, + name: str, + deadline: float, + all_logs: list[str], + *, + index: int, + total: int, +) -> None: + remaining = deadline - time.monotonic() + if remaining <= 0: + save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) + raise TerminalTimeoutError( + f"deploy pull timed out before {name!r}", + partial_output="".join(all_logs), + ) + + file_size = staging_path.joinpath(name).stat().st_size + file_timeout = deploy_file_pull_timeout(file_size, remaining) + tmp_path = deploy_pull_tmp_path(name) + final_path = board_path(work_dir, name) + pull_cmd = board_ssh_scp_pull_file_cmd( + cfg, + staging_dir, + name, + tmp_path, + large_transfer=file_size >= DEPLOY_LARGE_PULL_BYTES, + ) + print( + f"[deploy] scp ({index}/{total}): {name} ({file_size} bytes, timeout={file_timeout:.0f}s)", + flush=True, + ) + deploy_board_cmd( + cfg, term, all_logs, f"deploy_scp_{name}", pull_cmd, file_timeout, + f"board scp failed for {name!r}", retries=1, + ) + deploy_board_cmd( + cfg, term, all_logs, f"deploy_verify_{name}", + f"test $(wc -c < {tmp_path}) -eq {file_size}", 30.0, + f"board scp verify failed for {name!r}", retries=1, + ) + deploy_board_cmd( + cfg, term, all_logs, f"deploy_install_{name}", + f"mkdir -p {board_path(work_dir)} && mv {tmp_path} {final_path}", 30.0, + f"board install failed for {name!r}", retries=1, + ) + + +def deploy_assemble_split_file( + cfg: dict[str, Any], + term: Terminal, + staging_path: Path, + work_dir: str, + base: str, + parts: list[str], + all_logs: list[str], +) -> None: + expected_size = sum(staging_path.joinpath(part).stat().st_size for part in parts) + part_args = " ".join(board_path(work_dir, part) for part in parts) + out_path = board_path(work_dir, base) + cat_cmd = f"cat {part_args} > {out_path}" + verify_cmd = f"test $(wc -c < {out_path}) -eq {expected_size}" + print(f"[deploy] assemble split file {base} from {len(parts)} part(s)", flush=True) + deploy_board_cmd( + cfg, term, all_logs, f"deploy_cat_{base}", cat_cmd, 120.0, + f"board assemble failed for {base!r}", retries=1, + ) + deploy_board_cmd( + cfg, term, all_logs, f"deploy_verify_{base}", verify_cmd, 30.0, + f"board assemble verify failed for {base!r}: expected {expected_size} bytes", + retries=1, + ) + rm_parts = " ".join(board_path(work_dir, part) for part in parts) + term.run(f"deploy_rm_parts_{base}", board_priv(cfg, f"rm -f {rm_parts}"), timeout=30.0) + + +def deploy_gunzip_artifact( + cfg: dict[str, Any], + term: Terminal, + work_dir: str, + gz_name: str, + all_logs: list[str], +) -> None: + raw_name = DEPLOY_GUNZIP_ARTIFACTS[gz_name] + raw_path = Path(cfg["hvisor_tool_path"]) / "output" / raw_name + raw_size = raw_path.stat().st_size + gunzip_cmd = ( + f"gunzip -f {board_path(work_dir, gz_name)} && " + f"test $(wc -c < {board_path(work_dir, raw_name)}) -eq {raw_size}" + ) + print(f"[deploy] gunzip: {gz_name} -> {raw_name}", flush=True) + deploy_board_cmd( + cfg, term, all_logs, f"deploy_gunzip_{raw_name}", gunzip_cmd, 120.0, + f"board gunzip failed for {gz_name!r}", + ) + + def board_term_run( cfg: dict[str, Any], term: Terminal, @@ -436,7 +624,6 @@ def deploy_board_pull(cfg: dict[str, Any], term: Terminal) -> int: host_ip = cfg["network_host_ip"] host_user = cfg["network_host_user"] staging_dir = cfg["network_staging_dir"] - work_dir = cfg["zone1_work_dir"] print("[deploy] staging files on host", flush=True) stage_board_files(cfg) @@ -444,100 +631,52 @@ def deploy_board_pull(cfg: dict[str, Any], term: Terminal) -> int: staging_path = Path(staging_dir) if not staging_path.is_dir(): raise TerminalCommandError(f"staging dir not found: {staging_path}") - staged_files = sorted( - (p.name for p in staging_path.iterdir() if p.is_file()), - key=lambda name: staging_path.joinpath(name).stat().st_size, - ) - if not staged_files: + + work_dir = deploy_ensure_work_dir(cfg, term) + + regular_files, split_groups, pull_files = parse_staging(staging_path) + if not pull_files and not split_groups: raise TerminalCommandError(f"no staged files in {staging_path}") pull_timeout = float(cfg.get("deploy_pull_timeout", 600.0)) deadline = time.monotonic() + pull_timeout all_logs: list[str] = [] - stale_names = list(staged_files) - for gz_name, raw_name in DEPLOY_GUNZIP_ARTIFACTS.items(): - if gz_name in stale_names: - stale_names.append(raw_name) + stale = sorted(set(pull_files) | set(split_groups) | set(DEPLOY_GUNZIP_ARTIFACTS.values())) print(f"[deploy] removing stale files in {work_dir}", flush=True) - for name in stale_names: - stale_path = shlex.quote(f"{work_dir}/{name}") + for name in stale: + stale_path = board_path(work_dir, name) term.run(f"deploy_clean_{name}", board_priv(cfg, f"rm -f {stale_path}"), timeout=15.0) print( - f"[deploy] pulling {len(staged_files)} file(s) via scp from {host_user}@{host_ip}:{staging_dir}", + f"[deploy] pulling {len(pull_files)} file(s) via scp from {host_user}@{host_ip}:{staging_dir}", flush=True, ) - for index, name in enumerate(staged_files, start=1): + if split_groups: + print(f"[deploy] split artifacts: {', '.join(sorted(split_groups))}", flush=True) + + for index, name in enumerate(pull_files, start=1): if index > 1: time.sleep(2.0) - remaining = deadline - time.monotonic() - if remaining <= 0: - save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) - raise TerminalTimeoutError( - f"deploy pull timed out before {name!r}", - partial_output="".join(all_logs), - ) - - file_size = staging_path.joinpath(name).stat().st_size - file_timeout = deploy_file_pull_timeout(file_size, remaining) - large_transfer = file_size >= DEPLOY_LARGE_PULL_BYTES - pull_cmd = board_ssh_scp_pull_file_cmd( - cfg, staging_dir, name, work_dir, large_transfer=large_transfer - ) - print( - f"[deploy] scp ({index}/{len(staged_files)}): {name} ({file_size} bytes, timeout={file_timeout:.0f}s)", - flush=True, + deploy_board_pull_file( + cfg, + term, + staging_path, + staging_dir, + work_dir, + name, + deadline, + all_logs, + index=index, + total=len(pull_files), ) - try: - pull_rc, pull_out = board_term_run( - cfg, term, f"deploy_scp_{name}", pull_cmd, file_timeout, retries=1 - ) - except TerminalTimeoutError as exc: - pull_out = exc.partial_output or "" - all_logs.append(f"=== deploy_scp_{name} (timed out) ===\n{pull_out}\n") - save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) - raise - all_logs.append(f"=== deploy_scp_{name} ===\n{pull_out}\n") - if pull_rc != 0: - save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) - raise TerminalCommandError( - f"board scp failed for {name!r} with rc={pull_rc}: {pull_out.strip()}" - ) - verify_cmd = f"test $(wc -c < {shlex.quote(f'{work_dir}/{name}')}) -eq {file_size}" - try: - verify_rc, verify_out = board_term_run( - cfg, term, f"deploy_verify_{name}", verify_cmd, 30.0, retries=1 - ) - except TerminalTimeoutError as exc: - verify_out = exc.partial_output or "" - all_logs.append(f"=== deploy_verify_{name} (timed out) ===\n{verify_out}\n") - save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) - raise - all_logs.append(f"=== deploy_verify_{name} ===\n{verify_out}\n") - if verify_rc != 0: - save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) - raise TerminalCommandError( - f"board scp verify failed for {name!r} with rc={verify_rc}: {verify_out.strip()}" - ) + for base, parts in sorted(split_groups.items()): + deploy_assemble_split_file(cfg, term, staging_path, work_dir, base, parts, all_logs) - raw_name = DEPLOY_GUNZIP_ARTIFACTS.get(name) - if raw_name: - raw_path = Path(cfg["hvisor_tool_path"]) / "output" / raw_name - raw_size = raw_path.stat().st_size - gunzip_cmd = ( - f"gunzip -f {shlex.quote(f'{work_dir}/{name}')} && " - f"test $(wc -c < {shlex.quote(f'{work_dir}/{raw_name}')}) -eq {raw_size}" - ) - print(f"[deploy] gunzip: {name} -> {raw_name}", flush=True) - gz_rc, gz_out = term.run(f"deploy_gunzip_{raw_name}", gunzip_cmd, timeout=60.0) - all_logs.append(f"=== deploy_gunzip_{raw_name} ===\n{gz_out}\n") - if gz_rc != 0: - save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) - raise TerminalCommandError( - f"board gunzip failed for {name!r} with rc={gz_rc}: {gz_out.strip()}" - ) + for gz_name in DEPLOY_GUNZIP_ARTIFACTS: + if gz_name in regular_files or gz_name in split_groups: + deploy_gunzip_artifact(cfg, term, work_dir, gz_name, all_logs) save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) @@ -546,7 +685,8 @@ def deploy_board_pull(cfg: dict[str, Any], term: Terminal) -> int: "deploy_chmod", board_priv( cfg, - f"chmod +x {work_dir}/{boot_script} {work_dir}/hvisor {work_dir}/check_serial.sh 2>/dev/null || true", + f"chmod +x {board_path(work_dir, boot_script)} " + f"{board_path(work_dir, 'hvisor')} {board_path(work_dir, 'check_serial.sh')} 2>/dev/null || true", ), timeout=60.0, ) @@ -646,7 +786,6 @@ def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: if not qemu_term.wait_pattern(zone0_ready_pattern(cfg), timeout=180.0): raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") cfg["board_is_root"] = True - cfg["board_home"] = "/root" cfg["_board_shell_ready"] = True return 0 if cfg["mode"] == "board": @@ -777,11 +916,9 @@ def prepare_board_shell(cfg: dict[str, Any], term: Terminal) -> None: ensure_board_shell_user(cfg, term) user = board_login_user(term) cfg["board_is_root"] = user == "root" - home = "/root" if cfg["board_is_root"] else f"/home/{user}" - cfg["board_home"] = home - term.run("board_home_export", f"export HOME={shlex.quote(home)}", timeout=15.0) + term.run("board_home_export", "export HOME=$(cd ~ && pwd)", timeout=15.0) cfg["_board_shell_ready"] = True - print(f"[board] shell ready: user={user} home={home}", flush=True) + print(f"[board] shell ready: user={user} home={BOARD_HOME}", flush=True) def deploy_file_pull_timeout(file_size: int, remaining: float) -> float: @@ -795,18 +932,17 @@ def board_ssh_scp_pull_file_cmd( cfg: dict[str, Any], staging_dir: str, name: str, - work_dir: str, + dest_path: str, *, large_transfer: bool = False, ) -> str: - """Pull one staged file over scp using CI_SSH_KEY/CI_SSH_HOST env vars.""" + """Pull one staged file over scp using default board SSH identity.""" remote_file = f"{staging_dir}/{name}" - local_file = f"{work_dir}/{name}" opts = _board_ssh_opts_short(cfg, large_transfer=large_transfer) return ( - f"scp -i $CI_SSH_KEY {opts} " + f"scp {opts} " f"$CI_SSH_HOST:{shlex.quote(remote_file)} " - f"{shlex.quote(local_file)} int: if term is None: raise SystemExit("terminal backend is required") - work_dir = shlex.quote(cfg["zone1_work_dir"]) + work_dir = board_path(cfg["zone1_work_dir"]) boot_script = cfg["zone1_boot_script"] inner_log = "/tmp/zone1_inner.log" _, _ = term.run("zone1_ls", f"cd {work_dir} && ls", timeout=15.0) @@ -941,7 +1077,11 @@ def cfg_str(key: str, default: str = "") -> str: "zone1_boot_script": cfg_str("zone1_boot_script", "boot_zone1.sh"), "network_host_ip": cfg_str("host_ip", "192.168.1.181"), "network_host_user": cfg_str("host_user", "light"), - "network_staging_dir": cfg_str("staging_dir", "/home/light/ci_deploy"), + "network_staging_dir": resolve_staging_dir( + cfg_str("staging_dir", "/home/light/ci_deploy"), + arch, + board, + ), "network_ping_count": int(deploy_cfg.get("ping_count", tests.get("ping_count", 3))), "deploy_link_wait": float(deploy_cfg.get("link_wait", 0.0)), "deploy_ping_retries": int(deploy_cfg.get("ping_retries", 1)), From e99d80e77e86c152e01cfecaef7d17542558a891 Mon Sep 17 00:00:00 2001 From: dallas Date: Wed, 12 Aug 2026 11:38:01 +0800 Subject: [PATCH 52/53] bug fix --- jenkins/ci_runner.py | 150 ++++++++++++++++++++++++++----------------- 1 file changed, 91 insertions(+), 59 deletions(-) diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index 5bc7e313c..bcc525e67 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -25,6 +25,7 @@ DEPLOY_GUNZIP_ARTIFACTS = {"hvisor.gz": "hvisor"} DEPLOY_LARGE_PULL_BYTES = 512 * 1024 DEPLOY_BOARD_TMP_DIR = "/tmp/ci_pull" +DEPLOY_BOARD_TMP_FILE = f"{DEPLOY_BOARD_TMP_DIR}/f" DEPLOY_SPLIT_PART_RE = re.compile(r"^(.+)\.part\.[a-z]{2}$") BOARD_HOME = "~" BOARD_PUBKEY_LINE = re.compile(r"^ssh-(?:ed25519|rsa)\s+\S+") @@ -248,10 +249,6 @@ def board_path(work_dir: str, *parts: str) -> str: return f"{work_dir}/{rel}" if rel else work_dir -def deploy_pull_tmp_path(name: str) -> str: - return f"{DEPLOY_BOARD_TMP_DIR}/{name.replace('/', '_')}" - - def board_ssh_host(cfg: dict[str, Any]) -> str: return f"{cfg['network_host_user']}@{cfg['network_host_ip']}" @@ -342,31 +339,29 @@ def board_pubkey_from_output(output: str) -> str | None: def _export_board_ssh_env(cfg: dict[str, Any], term: Terminal) -> None: if cfg.get("_board_ssh_env"): return + connect = int(cfg.get("deploy_ssh_connect_timeout", 30)) term.run( "board_ssh_env", - f"export CI_SSH_HOST={shlex.quote(board_ssh_host(cfg))}", + ( + f"export CI_SSH_HOST={shlex.quote(board_ssh_host(cfg))} " + f"CI_SSH_OPTS='-o BatchMode=yes -o ConnectTimeout={connect} " + f"-o StrictHostKeyChecking=no'" + ), timeout=15.0, ) cfg["_board_ssh_env"] = True -def _board_ssh_opts_short(cfg: dict[str, Any], *, large_transfer: bool = False) -> str: - timeout = int(cfg.get("deploy_ssh_connect_timeout", 30)) - opts = f"-o BatchMode=yes -o ConnectTimeout={timeout} -o StrictHostKeyChecking=no" - if large_transfer: - opts += " -o ServerAliveInterval=0 -o TCPKeepAlive=yes" - return opts - - def _board_ssh_probe(cfg: dict[str, Any], term: Terminal) -> None: _export_board_ssh_env(cfg, term) host_user = cfg["network_host_user"] host_ip = cfg["network_host_ip"] - opts = _board_ssh_opts_short(cfg) - probe_timeout = float(int(cfg.get("deploy_ssh_connect_timeout", 30)) + 30) + connect_timeout = int(cfg.get("deploy_ssh_connect_timeout", 30)) + board_limit = connect_timeout + 15 + probe_timeout = float(board_limit + 30) probe_rc, probe_out = term.run( "deploy_ssh_probe", - board_priv(cfg, f"ssh {opts} $CI_SSH_HOST true str: return f"{base}/{suffix}" +def deploy_board_staging_alias(cfg: dict[str, Any]) -> str: + """Symlink under /tmp so board scp commands fit the serial line limit.""" + alias = Path("/tmp/s") / cfg["board"].replace("/", "_") + target = Path(cfg["network_staging_dir"]) + alias.parent.mkdir(parents=True, exist_ok=True) + if alias.is_symlink() or alias.exists(): + alias.unlink() + alias.symlink_to(target, target_is_directory=True) + return str(alias) + + def deploy_ensure_work_dir(cfg: dict[str, Any], term: Terminal) -> str: """Create zone1 work dir; fall back to ~ when configured dir is not writable.""" preferred = cfg["zone1_work_dir"] @@ -493,6 +499,60 @@ def deploy_board_cmd( return out +def deploy_scp_to_tmp( + cfg: dict[str, Any], + term: Terminal, + all_logs: list[str], + name: str, + tmp_path: str, + file_size: int, + file_timeout: float, +) -> None: + """Scp one file to tmp_path; recover when serial loses the result marker.""" + scp_cmd = ( + f"scp -q $CI_SSH_OPTS $CI_SSH_HOST:$CI_D/{name} {tmp_path} /dev/null" + ) + case_id = f"deploy_scp_{name}" + try: + deploy_board_cmd( + cfg, term, all_logs, case_id, scp_cmd, file_timeout, + f"board scp failed for {name!r}", retries=0, + ) + except TerminalTimeoutError: + check = f"test $(wc -c < {tmp_path}) -eq {file_size}" + rc, out = board_term_run(cfg, term, f"{case_id}_recover", check, 20.0, retries=0) + if rc != 0: + deploy_fail(cfg, all_logs, f"{case_id} (timed out)", out) + raise + all_logs.append(f"=== {case_id} (recovered) ===\n{out}\n") + print(f"[deploy] scp recovered after serial timeout: {name}", flush=True) + + +def deploy_install_from_tmp( + cfg: dict[str, Any], + term: Terminal, + all_logs: list[str], + name: str, + final_path: str, + file_size: int, +) -> None: + install_cmd = f"cp $CI_F {final_path} && rm -f $CI_F" + case_id = f"deploy_install_{name}" + try: + deploy_board_cmd( + cfg, term, all_logs, case_id, install_cmd, 30.0, + f"board install failed for {name!r}", retries=0, + ) + except TerminalTimeoutError: + check = f"test $(wc -c < {final_path}) -eq {file_size}" + rc, out = board_term_run(cfg, term, f"{case_id}_recover", check, 20.0, retries=0) + if rc != 0: + deploy_fail(cfg, all_logs, f"{case_id} (timed out)", out) + raise + all_logs.append(f"=== {case_id} (recovered) ===\n{out}\n") + print(f"[deploy] install recovered after serial timeout: {name}", flush=True) + + def deploy_board_pull_file( cfg: dict[str, Any], term: Terminal, @@ -516,33 +576,14 @@ def deploy_board_pull_file( file_size = staging_path.joinpath(name).stat().st_size file_timeout = deploy_file_pull_timeout(file_size, remaining) - tmp_path = deploy_pull_tmp_path(name) + tmp_path = DEPLOY_BOARD_TMP_FILE final_path = board_path(work_dir, name) - pull_cmd = board_ssh_scp_pull_file_cmd( - cfg, - staging_dir, - name, - tmp_path, - large_transfer=file_size >= DEPLOY_LARGE_PULL_BYTES, - ) print( f"[deploy] scp ({index}/{total}): {name} ({file_size} bytes, timeout={file_timeout:.0f}s)", flush=True, ) - deploy_board_cmd( - cfg, term, all_logs, f"deploy_scp_{name}", pull_cmd, file_timeout, - f"board scp failed for {name!r}", retries=1, - ) - deploy_board_cmd( - cfg, term, all_logs, f"deploy_verify_{name}", - f"test $(wc -c < {tmp_path}) -eq {file_size}", 30.0, - f"board scp verify failed for {name!r}", retries=1, - ) - deploy_board_cmd( - cfg, term, all_logs, f"deploy_install_{name}", - f"mkdir -p {board_path(work_dir)} && mv {tmp_path} {final_path}", 30.0, - f"board install failed for {name!r}", retries=1, - ) + deploy_scp_to_tmp(cfg, term, all_logs, name, tmp_path, file_size, file_timeout) + deploy_install_from_tmp(cfg, term, all_logs, name, final_path, file_size) def deploy_assemble_split_file( @@ -623,12 +664,12 @@ def deploy_board_pull(cfg: dict[str, Any], term: Terminal) -> int: host_ip = cfg["network_host_ip"] host_user = cfg["network_host_user"] - staging_dir = cfg["network_staging_dir"] print("[deploy] staging files on host", flush=True) stage_board_files(cfg) - staging_path = Path(staging_dir) + staging_dir = deploy_board_staging_alias(cfg) + staging_path = Path(cfg["network_staging_dir"]) if not staging_path.is_dir(): raise TerminalCommandError(f"staging dir not found: {staging_path}") @@ -655,9 +696,18 @@ def deploy_board_pull(cfg: dict[str, Any], term: Terminal) -> int: if split_groups: print(f"[deploy] split artifacts: {', '.join(sorted(split_groups))}", flush=True) + board_term_run( + cfg, + term, + "deploy_pull_env", + f"export CI_D={shlex.quote(staging_dir)} CI_F={DEPLOY_BOARD_TMP_FILE}", + 30.0, + retries=0, + ) + for index, name in enumerate(pull_files, start=1): if index > 1: - time.sleep(2.0) + time.sleep(3.0) deploy_board_pull_file( cfg, term, @@ -928,24 +978,6 @@ def deploy_file_pull_timeout(file_size: int, remaining: float) -> float: return min(remaining, needed) -def board_ssh_scp_pull_file_cmd( - cfg: dict[str, Any], - staging_dir: str, - name: str, - dest_path: str, - *, - large_transfer: bool = False, -) -> str: - """Pull one staged file over scp using default board SSH identity.""" - remote_file = f"{staging_dir}/{name}" - opts = _board_ssh_opts_short(cfg, large_transfer=large_transfer) - return ( - f"scp {opts} " - f"$CI_SSH_HOST:{shlex.quote(remote_file)} " - f"{dest_path} int: print("————————————————\ncase: zone1_start\n————————————————\n", flush=True) if term is None: From 26180bb5c0ee3abd55e537c7cf984fac38cb3c46 Mon Sep 17 00:00:00 2001 From: dallas Date: Wed, 12 Aug 2026 13:22:20 +0800 Subject: [PATCH 53/53] remove all changes about board ci --- Jenkinsfile | 187 +-- Makefile | 31 +- jenkins/board_power.sh | 62 - jenkins/board_stage.sh | 101 -- jenkins/board_zone0_start.py | 62 - jenkins/check_serial.sh | 50 - jenkins/ci.yaml | 104 +- jenkins/ci_runner.py | 1125 ++--------------- jenkins/hvisor-tool.yaml | 23 - jenkins/prepare.sh | 25 - jenkins/terminal.py | 238 +++- .../aarch64/phytium-pi/scripts/boot_zone1.sh | 10 - 12 files changed, 334 insertions(+), 1684 deletions(-) delete mode 100755 jenkins/board_power.sh delete mode 100755 jenkins/board_stage.sh delete mode 100755 jenkins/board_zone0_start.py delete mode 100755 jenkins/check_serial.sh delete mode 100644 jenkins/hvisor-tool.yaml mode change 100755 => 100644 jenkins/prepare.sh delete mode 100755 platform/aarch64/phytium-pi/scripts/boot_zone1.sh diff --git a/Jenkinsfile b/Jenkinsfile index d7505b087..cad720e4d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -32,33 +32,6 @@ def matrixCellDir() { return "${env.WORKSPACE}/.matrix/${bid.replace('/', '__')}" } -/** Stable directory name for a BID under ``$WORKSPACE/logs/``. */ -def bidLogKey(String bid = env.BID) { - return (bid ?: '').replace('/', '__') -} - -/** Copy per-cell logs out to ``$WORKSPACE/logs/`` before ``.matrix`` is removed. */ -def collectCellLogs() { - def src = "${matrixCellDir()}/logs" - def dest = "${env.WORKSPACE}/logs/${bidLogKey()}" - def logsRoot = "${env.WORKSPACE}/logs" - sh """ - if [ ! -d '${src}' ]; then - echo "No cell logs to collect: ${src}" - exit 0 - fi - mkdir -p '${dest}' 2>/dev/null || sudo mkdir -p '${dest}' - sudo chown -R "\$(id -u):\$(id -g)" '${logsRoot}' 2>/dev/null || true - # Source may be root-owned after board sudo ci_runner. - if ! cp -a '${src}/.' '${dest}/' 2>/dev/null; then - sudo cp -a '${src}/.' '${dest}/' - sudo chown -R "\$(id -u):\$(id -g)" '${dest}' - fi - echo "Collected cell logs -> ${dest}" - ls -la '${dest}' || true - """ -} - /** Isolated workspace for top-level CI jobs (linter, license-checker, …). */ def jenkinsJobDir(String name) { return "${env.WORKSPACE}/.jenkins/${name}" @@ -72,9 +45,6 @@ def syncWorkspaceTo(String destDir) { --exclude '.jenkins/' \\ --exclude '.matrix/' \\ --exclude '.jenkins-matrix/' \\ - --exclude 'logs/' \\ - --exclude '__pycache__/' \\ - --exclude '*.pyc' \\ '${env.WORKSPACE}/' '${destDir}/' """ } @@ -88,55 +58,6 @@ def loadCiYaml() { return data } -def loadHvisorToolConfig() { - def path = 'jenkins/hvisor-tool.yaml' - def defaults = [ - repo: env.HVISOR_TOOL_URL ?: 'https://github.com/syswonder/hvisor-tool.git', - ref : 'main', - ] - if (!fileExists(path)) { - return defaults - } - def data = readYaml file: path - def repo = (data?.repo ?: defaults.repo).toString().trim() - def ref = (data?.ref ?: defaults.ref).toString().trim() - if (!repo || !ref) { - error("${path}: 'repo' and 'ref' must be non-empty") - } - return [repo: repo, ref: ref] -} - -def checkoutHvisorTool(Map toolCfg, String targetDir) { - def url = toolCfg.repo.toString().trim() - def ref = (toolCfg.ref ?: '').trim() - - if (ref ==~ /^[0-9a-fA-F]{7,40}$/) { - sh """ - set -eux - rm -rf '${targetDir}' - git init '${targetDir}' - git -C '${targetDir}' remote add origin '${url}' - git -C '${targetDir}' fetch --depth 50 origin '${ref}' - git -C '${targetDir}' checkout FETCH_HEAD - """ - } else if (ref.startsWith('refs/')) { - sh """ - set -eux - rm -rf '${targetDir}' - git init '${targetDir}' - git -C '${targetDir}' remote add origin '${url}' - git -C '${targetDir}' fetch --depth 1 origin '${ref}:ci-ref' - git -C '${targetDir}' checkout ci-ref - """ - } else { - sh """ - set -eux - rm -rf '${targetDir}' - git clone --depth 1 --branch '${ref}' '${url}' '${targetDir}' - """ - } -} - def getBidConfig(ci, String bid) { return (ci.bids ?: []).find { entry -> return (entry?.bid ?: '').toString().trim() == bid @@ -208,6 +129,10 @@ def publishMatrixCheckInProgress() { publishGithubCheckInProgress(matrixCheckName()) } +def publishMatrixCheckCompleted(String conclusion) { + publishGithubCheckCompleted(matrixCheckName(), conclusion) +} + def finishGithubCheck(String checkName, String buildResult) { if (!isGithubCheckStarted(checkName)) { echo "Skip GitHub check completion for '${checkName}' (in-progress was never published)" @@ -263,9 +188,7 @@ pipeline { always { echo "=== DEBUG: Branch ${env.BRANCH_NAME} ===" echo "=== DEBUG: Commit ${env.GIT_COMMIT} ===" - // Keep $WORKSPACE/logs (collected from cells); only tear down sandboxes. - sh 'sudo rm -rf .matrix 2>/dev/null || rm -rf .matrix || true' - archiveArtifacts artifacts: 'logs/**/*', allowEmptyArchive: true + deleteDir() } } @@ -282,8 +205,6 @@ pipeline { LOONGARCH64_TOOLCHAIN_PATH = '/home/light/DEMO/toolchain/loongarch_cross_tools' // All toolchain bins on PATH; same for every matrix cell (no per-arch selection). TOOLCHAIN_PATHS = "${env.RISCV_TOOLCHAIN_PATH}/bin:${env.AARCH64_TOOLCHAIN_PATH}/bin:${env.LOONGARCH64_TOOLCHAIN_PATH}/bin" - TFTP_DIR = '/home/light/tftp' - PYTHONDONTWRITEBYTECODE = '1' } stages { @@ -307,16 +228,6 @@ pipeline { } } - stage('Checkout hvisor-tool') { - steps { - script { - def toolCfg = loadHvisorToolConfig() - echo "Checkout hvisor-tool [repo=${toolCfg.repo}, ref=${toolCfg.ref}]" - checkoutHvisorTool(toolCfg, env.HVISOR_TOOL_PATH) - } - } - } - stage('License checker') { steps { script { @@ -419,6 +330,17 @@ pipeline { } echo "Build hvisor-tool [BID=${env.BID}, TARCH=${tarch}, KDIR=${kdir}]" + if (!fileExists(env.HVISOR_TOOL_PATH)) { + sh "mkdir -p ${env.HVISOR_TOOL_PATH}" + } + dir(env.HVISOR_TOOL_PATH) { + checkout([ + $class: 'GitSCM', + branches: [[name: '*/main']], + extensions: [[$class: 'CloneOption', depth: 1, noTags: true]], + userRemoteConfigs: [[url: env.HVISOR_TOOL_URL]] + ]) + } sh """ export PATH=${env.TOOLCHAIN_PATHS}:\$PATH make -C ${env.HVISOR_TOOL_PATH} all ARCH=${tarch} KDIR=${kdir} @@ -464,31 +386,8 @@ pipeline { "${prepareScript}" """ } else if (mode == 'board') { - def tftpDir = (testsCfg.tftp_dir ?: env.TFTP_DIR).toString() - def zone0Dtbs = testsCfg.zone0_dtbs ?: [] - if (testsCfg.zone0_dtb) { - zone0Dtbs = [testsCfg.zone0_dtb] - } - def zone0Image = (testsCfg.zone0_image ?: "${kdir}/arch/arm64/boot/Image").toString() - echo "Deploy TFTP artifacts [BID=${env.BID}, TFTP_DIR=${tftpDir}]" - sh """ - export TERM=\${TERM:-xterm} - sudo mkdir -p "${tftpDir}" - sudo find "${tftpDir}" -mindepth 1 -maxdepth 1 -type f -delete - sudo make cp ARCH=${arch} BOARD=${board} MODE=release TFTP_DIR="${tftpDir}" - """ - zone0Dtbs.each { dtb -> - sh """ - test -f "${dtb}" - sudo cp "${dtb}" "${tftpDir}/" - """ - } - sh """ - test -f "${zone0Image}" - sudo cp "${zone0Image}" "${tftpDir}/Image" - sudo chmod -R a+rX "${tftpDir}" - ls -la "${tftpDir}" - """ + // Placeholder for future board artifact distribution by network. + echo "Board prepare placeholder [BID=${env.BID}]" } else { error("jenkins/ci.yaml BID=${env.BID}: unsupported tests.mode='${mode}'") } @@ -504,24 +403,14 @@ pipeline { steps { dir(matrixCellDir()) { script { - def bidCfg = getBidConfig(loadCiYaml(), env.BID) - def mode = (bidCfg.tests?.mode ?: '').toString().trim() - echo "Run tests via ci_runner [BID=${env.BID}, mode=${mode}]" - if (mode == 'board') { - sh """ - export TERM=\${TERM:-xterm} - sudo -E python3 jenkins/ci_runner.py \ - --bid "${env.BID}" - """ - } else { - sh """ - export TERM=\${TERM:-xterm} - ${toolchainPathShell()} - ${qemuPathShell()} - python3 jenkins/ci_runner.py \ - --bid "${env.BID}" - """ - } + echo "Run tests via ci_runner [BID=${env.BID}]" + sh """ + export TERM=\${TERM:-xterm} + ${toolchainPathShell()} + ${qemuPathShell()} + python3 jenkins/ci_runner.py \ + --bid "${env.BID}" + """ } } } @@ -530,32 +419,10 @@ pipeline { post { always { - script { - collectCellLogs() - finishGithubCheck(matrixCheckName(), currentBuild.currentResult) - } + script { finishGithubCheck(matrixCheckName(), currentBuild.currentResult) } } } } } - - stage('hvisor-tool on the branch of test') { - steps { - script { - def checkName = 'hvisor-tool on the branch of test' - publishGithubCheckInProgress(checkName) - def ref = loadHvisorToolConfig().ref - echo "Verify hvisor-tool pin [ref=${ref}]" - if (ref != 'main') { - error("hvisor-tool ref must be main for merge (current ref=${ref}). Restore ref: main in jenkins/hvisor-tool.yaml.") - } - } - } - post { - always { - script { finishGithubCheck('hvisor-tool on the branch of test', currentBuild.currentResult) } - } - } - } } } diff --git a/Makefile b/Makefile index 684f3d2b0..4fb832a86 100644 --- a/Makefile +++ b/Makefile @@ -66,25 +66,13 @@ ifeq ($(MODE), release) build_args += --release endif -# color code (skip tput when TERM is unset, e.g. Jenkins sh steps) -ifdef TERM -COLOR_GREEN := $(shell tput setaf 2 2>/dev/null) -COLOR_RED := $(shell tput setaf 1 2>/dev/null) -COLOR_YELLOW := $(shell tput setaf 3 2>/dev/null) -COLOR_BLUE := $(shell tput setaf 4 2>/dev/null) -COLOR_BOLD := $(shell tput bold 2>/dev/null) -COLOR_RESET := $(shell tput sgr0 2>/dev/null) -else -COLOR_GREEN := -COLOR_RED := -COLOR_YELLOW := -COLOR_BLUE := -COLOR_BOLD := -COLOR_RESET := -endif - -# Defconfig / menuconfig: see tools/kconfig/kconfig_cli.py -kconfig_python := tools/kconfig/.venv/bin/python +# color code +COLOR_GREEN := $(shell tput setaf 2) +COLOR_RED := $(shell tput setaf 1) +COLOR_YELLOW := $(shell tput setaf 3) +COLOR_BLUE := $(shell tput setaf 4) +COLOR_BOLD := $(shell tput bold) +COLOR_RESET := $(shell tput sgr0) # Defconfig / menuconfig: see tools/kconfig/kconfig_cli.py kconfig_python := tools/kconfig/.venv/bin/python @@ -199,11 +187,8 @@ monitor: jlink-server: JLinkGDBServer -select USB -if JTAG -device Cortex-A53 -port 1234 -TFTP_DIR ?= $(HOME)/tftp - cp: - @mkdir -p "$(TFTP_DIR)" - cp $(hvisor_bin) "$(TFTP_DIR)/" + cp $(hvisor_bin) ~/tftp test-pre: download-test-img chmod +x platform/$(ARCH)/$(BOARD)/test/runner.sh diff --git a/jenkins/board_power.sh b/jenkins/board_power.sh deleted file mode 100755 index 7b776aa50..000000000 --- a/jenkins/board_power.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/sh -# Relay power control for board CI (socat hex frames). -# Usage: board_power.sh off|on|cycle [channel] -# cycle: off, wait 3s, on - -set -eu - -action=${1:?action required (off|on|cycle)} -port=${2:?serial port required, e.g. /dev/ttyUSB1} -channel=${3:-4} -lock_file=${RELAY_LOCK_FILE:-/var/lock/hvisor-relay.lock} -lock_timeout=${RELAY_LOCK_TIMEOUT:-300} - -case "${channel}" in - 1|2|3|4) ;; - *) - echo "invalid relay channel: ${channel}" >&2 - exit 1 - ;; -esac - -send_frame() { - state=$1 - checksum=$((0xA0 + channel + state)) - printf '[power] send port=%s channel=%s state=%s frame=A0%02X%02X%02X\n' \ - "$port" "$channel" "$state" "$channel" "$state" "$checksum" - frame=$(printf '\\%03o\\%03o\\%03o\\%03o' 160 "$channel" "$state" "$checksum") - if [ "$(id -u)" -eq 0 ]; then - printf '%b' "$frame" | socat - "$port,b9600,raw,echo=0" - else - printf '%b' "$frame" | sudo socat - "$port,b9600,raw,echo=0" - fi -} - -command -v flock >/dev/null 2>&1 || { - echo "error: flock is required for relay serial locking" >&2 - exit 1 -} - -exec 9>"${lock_file}" -flock -w "${lock_timeout}" 9 || { - echo "error: timed out waiting for relay lock: ${lock_file}" >&2 - exit 1 -} - -case "$action" in - off) - send_frame 0 - ;; - on) - send_frame 1 - ;; - cycle) - send_frame 0 - sleep 3 - send_frame 1 - ;; - *) - echo "usage: $0 off|on|cycle [channel]" >&2 - exit 1 - ;; -esac diff --git a/jenkins/board_stage.sh b/jenkins/board_stage.sh deleted file mode 100755 index 782448388..000000000 --- a/jenkins/board_stage.sh +++ /dev/null @@ -1,101 +0,0 @@ -#!/bin/sh -# Stage zone1 deploy artifacts on the CI host; the board pulls them via scp. -# Zone0 boot artifacts (hvisor.bin, Image, dtbs) belong in tftp_dir, not here. - -set -eux - -ARCH=${ARCH:?ARCH is required} -BOARD=${BOARD:?BOARD is required} -WORKSPACE_ROOT=${WORKSPACE_ROOT:-$(pwd)} -HVISOR_TOOL_PATH=${HVISOR_TOOL_PATH:-${WORKSPACE_ROOT}/hvisor-tool} -STAGING_DIR=${STAGING_DIR:-/home/light/ci_deploy} - -case "${HVISOR_TOOL_PATH}" in - /*) ;; - *) HVISOR_TOOL_PATH="${WORKSPACE_ROOT}/${HVISOR_TOOL_PATH}" ;; -esac - -PLATFORM_DIR="${WORKSPACE_ROOT}/platform/${ARCH}/${BOARD}" -CONFIGS_DIR="${PLATFORM_DIR}/configs" -IMAGE_DIR="${PLATFORM_DIR}/image" -SCRIPTS_DIR="${PLATFORM_DIR}/scripts" -ZONE1_BOOT_SCRIPT="${SCRIPTS_DIR}/boot_zone1.sh" -CHECK_SERIAL_SCRIPT="${WORKSPACE_ROOT}/jenkins/check_serial.sh" - -if [ -z "${ZONE1_DTB:-}" ]; then - for candidate in \ - "${IMAGE_DIR}/dts/zone1-linux.dtb" \ - "${IMAGE_DIR}/dts/linux2.dtb"; do - if [ -f "${candidate}" ]; then - ZONE1_DTB="${candidate}" - break - fi - done - ZONE1_DTB=${ZONE1_DTB:-${IMAGE_DIR}/dts/zone1-linux.dtb} -fi - -echo "ARCH: ${ARCH}" -echo "BOARD: ${BOARD}" -echo "HVISOR_TOOL_PATH: ${HVISOR_TOOL_PATH}" -echo "STAGING_DIR: ${STAGING_DIR}" - -if [ ! -f "${HVISOR_TOOL_PATH}/output/hvisor" ]; then - echo "error: hvisor tool binary not found: ${HVISOR_TOOL_PATH}/output/hvisor" - exit 1 -fi -if [ ! -f "${HVISOR_TOOL_PATH}/output/hvisor.ko" ]; then - echo "error: hvisor.ko not found: ${HVISOR_TOOL_PATH}/output/hvisor.ko" - exit 1 -fi -if [ ! -f "${ZONE1_BOOT_SCRIPT}" ]; then - echo "error: zone1 start script not found: ${ZONE1_BOOT_SCRIPT}" - exit 1 -fi - -if [ ! -f "${ZONE1_DTB}" ] && [ -d "${IMAGE_DIR}/dts" ]; then - echo "zone1 dtb is missing, building from ${IMAGE_DIR}/dts" - make -C "${IMAGE_DIR}/dts" all || true -fi - -rm -rf "${STAGING_DIR}" -mkdir -p "${STAGING_DIR}" - -cp "${HVISOR_TOOL_PATH}/output/hvisor.ko" "${STAGING_DIR}/" -gzip -c "${HVISOR_TOOL_PATH}/output/hvisor" > "${STAGING_DIR}/hvisor.gz" -cp "${CONFIGS_DIR}/"* "${STAGING_DIR}/" -cp "${ZONE1_BOOT_SCRIPT}" "${STAGING_DIR}/" - -if [ -f "${ZONE1_DTB}" ]; then - cp "${ZONE1_DTB}" "${STAGING_DIR}/" -else - echo "warning: zone1 dtb unavailable, skip copying ${ZONE1_DTB}" -fi - -if [ -f "${CHECK_SERIAL_SCRIPT}" ]; then - cp "${CHECK_SERIAL_SCRIPT}" "${STAGING_DIR}/" -fi - -chmod -R a+rX "${STAGING_DIR}" -chmod +x "${STAGING_DIR}/boot_zone1.sh" - -if [ "$(id -u)" -eq 0 ] && id light >/dev/null 2>&1; then - chown -R light:light "${STAGING_DIR}" -fi - -DEPLOY_SPLIT_CHUNK_BYTES=${DEPLOY_SPLIT_CHUNK_BYTES:-262144} -for staged_file in "${STAGING_DIR}"/*; do - [ -f "${staged_file}" ] || continue - staged_name=$(basename "${staged_file}") - case "${staged_name}" in - *.part.*) continue ;; - esac - size=$(stat -c%s "${staged_file}") - if [ "${size}" -gt "${DEPLOY_SPLIT_CHUNK_BYTES}" ]; then - split -b "${DEPLOY_SPLIT_CHUNK_BYTES}" "${staged_file}" "${STAGING_DIR}/${staged_name}.part." - rm -f "${staged_file}" - echo "split ${staged_name}: ${size} bytes into ${DEPLOY_SPLIT_CHUNK_BYTES}-byte chunks" - fi -done - -echo "board staging completed: ${STAGING_DIR}" -ls -la "${STAGING_DIR}" diff --git a/jenkins/board_zone0_start.py b/jenkins/board_zone0_start.py deleted file mode 100755 index 0b8f2e5bd..000000000 --- a/jenkins/board_zone0_start.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python3 -"""Power-cycle a board and boot zone0 via U-Boot; no network/SSH/deploy/zone1.""" - -from __future__ import annotations - -import argparse -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parent)) - -from ci_runner import ( - board_power_off, - boot_board_zone0_shell, - build_terminal, - load_runtime_config, - logs_dir, -) -from terminal import TerminalCommandError, TerminalTimeoutError - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Boot board zone0 and stop.") - parser.add_argument("--bid", required=True, help="BID key in jenkins/ci.yaml") - parser.add_argument( - "--no-power-cycle", - action="store_true", - help="Skip relay power cycle (board already off or at U-Boot).", - ) - return parser.parse_args() - - -def main() -> int: - args = parse_args() - cfg = load_runtime_config(args) - if cfg["mode"] != "board": - raise SystemExit(f"bid '{args.bid}' is mode={cfg['mode']!r}, board mode required") - - log_path = logs_dir(cfg) / "zone0_console.log" - log_path.write_text("", encoding="utf-8") - term = build_terminal(cfg, log_path) - term.open() - ok = False - try: - boot_board_zone0_shell(cfg, term, power_cycle=not args.no_power_cycle) - ok = True - print( - f"[board_zone0_start] zone0 ready (serial={cfg['serial_port']}, log={log_path})", - flush=True, - ) - return 0 - except (TerminalTimeoutError, TerminalCommandError) as exc: - print(f"[board_zone0_start] failed: {exc}", flush=True) - return 1 - finally: - term.close() - if not ok: - board_power_off(cfg) - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/jenkins/check_serial.sh b/jenkins/check_serial.sh deleted file mode 100755 index fb2cd6316..000000000 --- a/jenkins/check_serial.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/sh -# Non-interactive virtio-console check for inner zone (/dev/pts/x). -# Usage: check_serial.sh [timeout_sec] [command...] - -set -eu - -pts_dev=${1:?pts device required} -log_file=${2:?log file required} -shift 2 - -prompt_timeout=60 -if [ $# -gt 0 ] && [ "$1" -eq "$1" ] 2>/dev/null; then - prompt_timeout=$1 - shift -fi - -: > "$log_file" - -read_pts() { - timeout "${1:-2}" cat "$pts_dev" 2>/dev/null >> "$log_file" || true -} - -has_console_ready() { - tr -d '\r' < "$log_file" | sed 's/\x1b\[[0-9;?]*[ -\/]*[@-~]//g' \ - | grep -qE 'root@[^[:space:]]*[#$][[:space:]]*$|^[[:space:]]*#[[:space:]]*$|login:[[:space:]]*$' -} - -deadline=$(( $(date +%s) + prompt_timeout )) -while [ "$(date +%s)" -lt "$deadline" ]; do - printf '\r\n' > "$pts_dev" 2>/dev/null || true - read_pts 2 - if has_console_ready; then - break - fi - sleep 0.2 -done - -if ! has_console_ready; then - echo "check_serial: timed out after ${prompt_timeout}s waiting for console prompt on $pts_dev" \ - >> "$log_file" - exit 1 -fi - -if [ $# -gt 0 ]; then - printf '%s\r\n' "$@" - sleep 1 - read_pts 10 -fi - -exit 0 diff --git a/jenkins/ci.yaml b/jenkins/ci.yaml index 1da24daf6..b23c396e0 100644 --- a/jenkins/ci.yaml +++ b/jenkins/ci.yaml @@ -14,99 +14,21 @@ bids: tests: mode: qemu uboot_cmd: bootm 0x40400000 - 0x40000000 - uboot_ready_pattern: => cases: - zone0_start - zone1_start - - bid: aarch64/rk3568 - build_args: - - KDIR=/home/light/DEMO/sdk/rk356x-up4-2c/kernel - tests: - mode: board - serial: /dev/serial/by-id/usb-1a86_USB2.0-Ser_-if00-port0 - power_serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 - power_channel: 4 - baudrate: 1500000 - tftp_dir: /home/light/tftp/rk3568 - zone0_dtbs: - - platform/aarch64/rk3568/image/dts/rk3568_limit_zone0.dtb - uboot_ready_pattern: '=>' - zone0_ready_pattern: 'root@linux:[^\r\n]*#\s?' - zone0_shell_timeout: 180 - uboot_prompt_timeout: 60 - uboot_step_timeout: 90 - uboot_cmds: - - pci enum; setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.240; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000 - - tftp ${loadaddr} ${serverip}:rk3568/hvisor.bin - - tftp ${fdt_addr} ${serverip}:rk3568/rk3568_limit_zone0.dtb - - tftp ${zone0_kernel_addr} ${serverip}:rk3568/Image - - bootm ${loadaddr} - ${fdt_addr} - deploy: - board_ip: 192.168.1.240 - board_iface: eth0 - link_wait: 3 - host_ip: 192.168.1.181 - host_user: light - staging_dir: /home/light/ci_deploy - zone1_work_dir: /root - ssh_connect_timeout: 60 - zone1_dtb: platform/aarch64/rk3568/image/dts/rk3568_limit_zone1.dtb - lspci: - expected_bdfs: - - "0001:10:00.0" - - "0001:11:00.0" - - "0002:20:00.0" - - "0002:21:00.0" - min_count: 4 - cases: - - zone0_start - - deploy - - lspci - - zone1_start - - - bid: aarch64/phytium-pi - build_args: - - KDIR=/home/light/DEMO/linux/kernel-5.10 - tests: - mode: board - serial: /dev/serial/by-id/phytium-pi-uart - power_serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 - power_channel: 3 - baudrate: 115200 - tftp_dir: /home/light/tftp/phytium-pi - zone0_dtbs: - - platform/aarch64/phytium-pi/image/dts/linux1.dtb - - platform/aarch64/phytium-pi/image/dts/phytium-pi-board-v2.dtb - zone0_image: /home/light/DEMO/phytium-pi-external/Image - uboot_ready_pattern: 'Phytium-Pi#' - zone0_ready_pattern: 'root@[^\r\n]*:\/#\s?|(?:\r?\n)#\s*(?:\r?\n|$)' - zone0_shell_timeout: 300 - uboot_prompt_timeout: 60 - uboot_step_timeout: 600 - uboot_autoboot_window: 15 - uboot_cmds: - - setenv serverip 192.168.1.181; setenv ipaddr 192.168.1.179; setenv loadaddr 0x90100000; setenv fdt_addr 0x90000000; setenv zone0_kernel_addr 0xa0400000; setenv zone0_fdt_addr 0xa0000000 - - tftp ${loadaddr} ${serverip}:phytium-pi/hvisor.bin - - tftp ${fdt_addr} ${serverip}:phytium-pi/phytium-pi-board-v2.dtb - - tftp ${zone0_kernel_addr} ${serverip}:phytium-pi/Image - - tftp ${zone0_fdt_addr} ${serverip}:phytium-pi/linux1.dtb - - bootm ${loadaddr} - ${fdt_addr} - deploy: - board_ip: 192.168.1.179 - board_iface: eth0 - link_wait: 8 - ping_retries: 3 - host_ip: 192.168.1.181 - host_user: light - staging_dir: /home/light/ci_deploy - zone1_work_dir: /root - pull_timeout: 900 - zone1_dtb: platform/aarch64/phytium-pi/image/dts/linux2.dtb - cases: - - zone0_start - - deploy - - zone1_start + # - bid: aarch64/rk3568 + # build_args: + # - KDIR=/home/light/DEMO/sdk/rk356x-up4-2c/kernel + # tests: + # mode: board + # serial: /dev/serial/by-id/usb-1a86_USB2.0-Ser_-if00-port0 + # baudrate: 115200 + # uboot_cmd: pci enum;setenv serverip 192.168.0.1; setenv ipaddr 192.168.0.2; setenv loadaddr 0x60800000; setenv fdt_addr 0xa0000000; setenv zone0_kernel_addr 0x00280000; tftp ${loadaddr} ${serverip}:hvisor.bin; tftp ${fdt_addr} ${serverip}:rk3568_limit_zone0.dtb; tftp ${zone0_kernel_addr} ${serverip}:Image_test5; bootm ${loadaddr} - ${fdt_addr}; + # cases: + # - zone0_start + # - zone1_start - bid: x86_64/qemu @@ -116,4 +38,6 @@ bids: mode: qemu cases: - zone0_start - - zone1_start + - zone1_start + + diff --git a/jenkins/ci_runner.py b/jenkins/ci_runner.py index bcc525e67..6036ff9c4 100755 --- a/jenkins/ci_runner.py +++ b/jenkins/ci_runner.py @@ -4,7 +4,6 @@ import argparse import os import re -import shlex import socket import signal import subprocess @@ -18,27 +17,6 @@ CaseFunc = Callable[[dict[str, Any], Terminal | None], int] -# Wait for zone1 inner console: shell prompt (#/$) or login:. -ZONE0_READY_PATTERN = r"root@[^\r\n]*#\s?|(?:\r?\n)#\s*(?:\r?\n|$)" -ZONE1_INNER_PROMPT_TIMEOUT = 60.0 -ZONE1_INNER_LOG_FETCH_TIMEOUT = 30.0 -DEPLOY_GUNZIP_ARTIFACTS = {"hvisor.gz": "hvisor"} -DEPLOY_LARGE_PULL_BYTES = 512 * 1024 -DEPLOY_BOARD_TMP_DIR = "/tmp/ci_pull" -DEPLOY_BOARD_TMP_FILE = f"{DEPLOY_BOARD_TMP_DIR}/f" -DEPLOY_SPLIT_PART_RE = re.compile(r"^(.+)\.part\.[a-z]{2}$") -BOARD_HOME = "~" -BOARD_PUBKEY_LINE = re.compile(r"^ssh-(?:ed25519|rsa)\s+\S+") -BOARD_ROOT_SHELL_PATTERN = r"root@[^\r\n]*:\/#\s?|(?:\r?\n)#\s?(?:\r?\n|$)" -BOARD_SU_PASSWORD_PROMPT = r"Password:|password:" - - -def logs_dir(cfg: dict[str, Any]) -> Path: - """Per-cell log directory: ``/logs``.""" - path = Path(cfg["workspace"]) / "logs" - path.mkdir(parents=True, exist_ok=True) - return path - def wait_qemu_socket(path: str, timeout: float = 30.0) -> None: deadline = time.monotonic() + timeout @@ -73,963 +51,147 @@ def terminate_managed_process(cfg: dict[str, Any]) -> None: pass -def parse_pts(output: str) -> list[int]: - return sorted({int(match) for match in re.findall(r"/dev/pts/(\d+)", output)}) - - -def find_zone1_pts(term: Terminal) -> int: - """List virtio-console pts devices; retry once on failure.""" - for attempt in range(2): - _, pts_output = term.run( - f"zone1_pts_{attempt}", - "ls -1 /dev/pts/[0-9]*", - timeout=15.0, - ) - pts_numbers = parse_pts(pts_output) - if pts_numbers: - return pts_numbers[-1] - if attempt == 0: - time.sleep(2.0) - raise TerminalCommandError("failed to find numeric pts from 'ls -1 /dev/pts/[0-9]*'") - - -def ensure_qemu_terminal(cfg: dict[str, Any], log_path: Path) -> Terminal: - term = cfg.get("_qemu_term") - if term is None: - term = build_terminal(cfg, log_path) - term.open() - cfg["_qemu_term"] = term - cfg["_zone0_log_path"] = log_path - return term - - -def close_qemu_terminal(cfg: dict[str, Any]) -> None: - term = cfg.get("_qemu_term") - if term is not None: - term.close() - cfg["_qemu_term"] = None - - -def close_board_terminal(cfg: dict[str, Any]) -> None: - term = cfg.get("_board_term") - if term is not None: - term.close() - cfg["_board_term"] = None - - -def get_active_terminal(cfg: dict[str, Any]) -> Terminal | None: - return cfg.get("_qemu_term") or cfg.get("_board_term") - - -def close_active_terminal(cfg: dict[str, Any]) -> None: - close_qemu_terminal(cfg) - close_board_terminal(cfg) - - -def board_power_script(cfg: dict[str, Any]) -> Path: - return cfg["workspace"] / "jenkins" / "board_power.sh" - - -def board_power(cfg: dict[str, Any], action: str) -> None: - power_port = str(cfg.get("power_serial", "")).strip() - if not power_port: - print(f"[power] skip {action}: power_serial is empty", flush=True) - return - script = board_power_script(cfg) - if not script.is_file(): - raise SystemExit(f"board power script not found: {script}") - power_channel = str(cfg.get("power_channel", 4)) - print(f"[power] {action} port={power_port} channel={power_channel}", flush=True) - subprocess.run( - ["bash", str(script), action, power_port, power_channel], - check=True, - cwd=cfg["workspace"], - ) - print(f"[power] {action} completed", flush=True) - - -def board_power_cycle(cfg: dict[str, Any]) -> None: - board_power(cfg, "cycle") - - -def board_power_off(cfg: dict[str, Any]) -> None: - board_power(cfg, "off") +def run_and_print(term: Terminal, command: str) -> str: + output = term.send_until_quiet(command, quiet_seconds=1.0, max_duration=40.0) + if output: + print(output, end="", flush=True) + return output -def board_wait_uboot_prompt( +def run_and_print_quiet( term: Terminal, - pattern: str, - timeout: float, - *, - after_offset: int | None = None, - autoboot_window: float = 0.0, -) -> None: - """Wake the console and wait for a U-Boot prompt.""" - offset = term.offset() if after_offset is None else after_offset - if autoboot_window > 0 and after_offset is None: - deadline = time.monotonic() + autoboot_window - while time.monotonic() < deadline: - if term.wait_pattern(pattern, timeout=0.2, from_offset=offset): - return - term.send(" ") - time.sleep(0.1) - elif after_offset is None: - term.send("") - time.sleep(0.2) - term.send("") - time.sleep(0.2) - if not term.wait_pattern(pattern, timeout=timeout, from_offset=offset): - raise TerminalTimeoutError(f"timed out waiting for U-Boot prompt (pattern={pattern!r})") - - -def save_inner_serial_log(cfg: dict[str, Any], content: str) -> None: - if not content: - return - path = logs_dir(cfg) / "zone1_inner_serial.log" - path.write_text(content, encoding="utf-8") - - -def stage_board_files(cfg: dict[str, Any]) -> None: - script = cfg["workspace"] / "jenkins" / "board_stage.sh" - if not script.is_file(): - raise SystemExit(f"board stage script not found: {script}") - - env = os.environ.copy() - env["ARCH"] = cfg["arch"] - env["BOARD"] = cfg["board"] - if cfg["kdir"]: - env["KDIR"] = cfg["kdir"] - env["WORKSPACE_ROOT"] = str(cfg["workspace"]) - env["HVISOR_TOOL_PATH"] = cfg["hvisor_tool_path"] - env["STAGING_DIR"] = cfg["network_staging_dir"] - if cfg.get("zone1_dtb"): - zone1_dtb = Path(cfg["zone1_dtb"]) - if not zone1_dtb.is_absolute(): - zone1_dtb = cfg["workspace"] / zone1_dtb - env["ZONE1_DTB"] = str(zone1_dtb.resolve()) - - subprocess.run(["bash", str(script)], check=True, cwd=cfg["workspace"], env=env) - - -def save_case_log(cfg: dict[str, Any], name: str, content: str) -> None: - path = logs_dir(cfg) / name - path.write_text(content, encoding="utf-8") - print(f"[ci_runner] saved {path}", flush=True) - - -def netmask_prefix(netmask: str) -> int: - try: - return sum(bin(int(part)).count("1") for part in netmask.split(".")) - except (ValueError, AttributeError): - return 24 - - -def board_sudo(cmd: str) -> str: - """Wrap a board shell command to run with sudo -E (preserve HOME etc.).""" - cmd = cmd.strip() - if cmd.startswith("sudo "): - return cmd - if cmd.startswith("export "): - return cmd - return f"sudo -E sh -c {shlex.quote(cmd)}" - - -def board_priv(cfg: dict[str, Any], cmd: str) -> str: - """Run privileged board commands; skip sudo when the shell is already root.""" - if cfg.get("board_is_root") or cfg.get("mode") == "qemu": - return cmd.strip() - return board_sudo(cmd) - - -def board_path(work_dir: str, *parts: str) -> str: - """Build a shell path; ~ is left for the board shell to expand.""" - rel = "/".join(parts) - if work_dir == BOARD_HOME: - return f"{BOARD_HOME}/{rel}" if rel else BOARD_HOME - return f"{work_dir}/{rel}" if rel else work_dir - - -def board_ssh_host(cfg: dict[str, Any]) -> str: - return f"{cfg['network_host_user']}@{cfg['network_host_ip']}" - - -def _setup_board_interface(cfg: dict[str, Any], term: Terminal) -> None: - """Assign a static IP and cycle the interface.""" - board_ip = str(cfg.get("board_ip", "")).strip() - board_iface = str(cfg.get("board_iface", "")).strip() - if not board_ip or not board_iface: - return - - netmask = str(cfg.get("board_netmask", "255.255.255.0")).strip() or "255.255.255.0" - prefix = netmask_prefix(netmask) - term.run( - "deploy_set_ip", - board_priv( - cfg, - f"ip addr flush dev {board_iface} 2>/dev/null || true; " - f"ip addr add {board_ip}/{prefix} dev {board_iface}", - ), - timeout=30.0, - ) - term.run( - "deploy_iface_down", - board_priv(cfg, f"ip link set {board_iface} down"), - timeout=15.0, - ) - term.run( - "deploy_iface_up", - board_priv(cfg, f"ip link set {board_iface} up"), - timeout=15.0, - ) - - link_wait = float(cfg.get("deploy_link_wait", 0.0)) - if link_wait > 0: - print( - f"[deploy] waiting {link_wait}s for {board_iface} link on {board_ip}", - flush=True, - ) - time.sleep(link_wait) - time.sleep(2.0) - - -def _board_ping_host(cfg: dict[str, Any], term: Terminal) -> None: - """Verify layer-3 reachability to the CI host before SSH setup.""" - host_ip = cfg["network_host_ip"] - ping_count = cfg["network_ping_count"] - ping_retries = int(cfg.get("deploy_ping_retries", 1)) - ping_output = "" - for attempt in range(ping_retries): - if attempt > 0: - retry_wait = float(cfg.get("deploy_link_wait", 5.0)) or 5.0 - print(f"[deploy] ping retry {attempt + 1}/{ping_retries} after {retry_wait}s", flush=True) - time.sleep(retry_wait) - _, ping_output = term.run( - "deploy_ping", - board_priv( - cfg, - f"ping -c {ping_count} -W 5 {host_ip} > /tmp/ci_ping.log 2>&1; cat /tmp/ci_ping.log", - ), - timeout=30.0 + ping_count * 5.0, - wake_interval=2.0, - ) - if ping_success(ping_output): - break - save_case_log(cfg, "deploy_ping.log", f"=== ping {host_ip} ===\n{ping_output}\n") - if not ping_success(ping_output): - raise TerminalCommandError(f"ping {host_ip} failed") - print(f"[deploy] ping {host_ip} ok", flush=True) - - -def ensure_board_net(cfg: dict[str, Any], term: Terminal) -> None: - """Configure network, verify ping, and probe SSH to the CI host.""" - prepare_board_shell(cfg, term) - _setup_board_interface(cfg, term) - _board_ping_host(cfg, term) - _board_ssh_probe(cfg, term) - - -def board_pubkey_from_output(output: str) -> str | None: - for line in output.splitlines(): - candidate = line.strip() - if BOARD_PUBKEY_LINE.match(candidate): - return candidate - return None - - -def _export_board_ssh_env(cfg: dict[str, Any], term: Terminal) -> None: - if cfg.get("_board_ssh_env"): - return - connect = int(cfg.get("deploy_ssh_connect_timeout", 30)) - term.run( - "board_ssh_env", - ( - f"export CI_SSH_HOST={shlex.quote(board_ssh_host(cfg))} " - f"CI_SSH_OPTS='-o BatchMode=yes -o ConnectTimeout={connect} " - f"-o StrictHostKeyChecking=no'" - ), - timeout=15.0, - ) - cfg["_board_ssh_env"] = True - - -def _board_ssh_probe(cfg: dict[str, Any], term: Terminal) -> None: - _export_board_ssh_env(cfg, term) - host_user = cfg["network_host_user"] - host_ip = cfg["network_host_ip"] - connect_timeout = int(cfg.get("deploy_ssh_connect_timeout", 30)) - board_limit = connect_timeout + 15 - probe_timeout = float(board_limit + 30) - probe_rc, probe_out = term.run( - "deploy_ssh_probe", - board_priv(cfg, f"timeout {board_limit} ssh $CI_SSH_OPTS $CI_SSH_HOST true /dev/null || cat ~/.ssh/id_rsa.pub 2>/dev/null || true", - ), - timeout=15.0, - ) - pubkey = board_pubkey_from_output(pub_out) if pub_rc == 0 else None - if not pubkey: - raise TerminalCommandError( - f"ssh probe to {host_user}@{host_ip} failed and board public key not found\n" - f"{probe_out.strip()}" - ) - - auth_keys = f"/home/{host_user}/.ssh/authorized_keys" - raise TerminalCommandError( - f"ssh probe to {host_user}@{host_ip} failed; add this board public key to " - f"{auth_keys} on the CI host:\n{pubkey}" - ) - - -def boot_board_zone0_shell(cfg: dict[str, Any], term: Terminal, *, power_cycle: bool = True) -> None: - """Power-cycle (optional), run U-Boot commands, and prepare the zone0 shell.""" - if power_cycle: - board_power_cycle(cfg) - send_board_uboot_commands(cfg, term) - timeout = float(cfg.get("zone0_shell_timeout", 180.0)) - if not term.wait_pattern(zone0_ready_pattern(cfg), timeout=timeout): - raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") - prepare_board_shell(cfg, term) - - -def resolve_staging_dir(raw: str, arch: str, board: str) -> str: - """Per-board staging path so parallel matrix cells do not clobber each other.""" - base = (raw or "/home/light/ci_deploy").rstrip("/") - suffix = f"{arch}__{board.replace('/', '__')}" - if base.endswith(suffix): - return base - return f"{base}/{suffix}" - - -def deploy_board_staging_alias(cfg: dict[str, Any]) -> str: - """Symlink under /tmp so board scp commands fit the serial line limit.""" - alias = Path("/tmp/s") / cfg["board"].replace("/", "_") - target = Path(cfg["network_staging_dir"]) - alias.parent.mkdir(parents=True, exist_ok=True) - if alias.is_symlink() or alias.exists(): - alias.unlink() - alias.symlink_to(target, target_is_directory=True) - return str(alias) - - -def deploy_ensure_work_dir(cfg: dict[str, Any], term: Terminal) -> str: - """Create zone1 work dir; fall back to ~ when configured dir is not writable.""" - preferred = cfg["zone1_work_dir"] - candidates = [preferred] - if preferred != BOARD_HOME: - candidates.append(BOARD_HOME) - - board_term_run( - cfg, term, "deploy_tmpdir", f"mkdir -p {DEPLOY_BOARD_TMP_DIR}", 15.0, retries=0 - ) - - last_out = "" - for path in candidates: - probe = board_path(path, ".ci_probe") - cmd = f"mkdir -p {board_path(path)} && touch {probe} && rm -f {probe}" - rc, last_out = board_term_run(cfg, term, "deploy_mkdir", cmd, 30.0, retries=1) - if rc == 0: - if path != preferred: - print(f"[deploy] warning: {preferred} unavailable, using {BOARD_HOME}", flush=True) - else: - print(f"[deploy] work dir ready: {path}", flush=True) - cfg["zone1_work_dir"] = path - return path - raise TerminalCommandError(f"board work dir setup failed for {candidates!r}: {last_out.strip()}") - - -def parse_staging(staging_path: Path) -> tuple[list[str], dict[str, list[str]], list[str]]: - regular: list[str] = [] - split_groups: dict[str, list[str]] = {} - - for entry in staging_path.iterdir(): - if not entry.is_file(): - continue - name = entry.name - match = DEPLOY_SPLIT_PART_RE.match(name) - if match: - split_groups.setdefault(match.group(1), []).append(name) - continue - regular.append(name) - - for base in split_groups: - split_groups[base] = sorted(split_groups[base]) - parts = [part for part_names in split_groups.values() for part in part_names] - pull_files = sorted( - regular + parts, - key=lambda name: staging_path.joinpath(name).stat().st_size, + command: str, + quiet_seconds: float = 1.0, + max_duration: float = 30.0, + check_exit: bool = True, +) -> tuple[str, int]: + # Send a leading Enter to synchronize shell prompt state. + # term.send("\n") + # _ = term.read_for(duration=0.2) + + output, rc = term.run_until_quiet_with_status( + command, + quiet_seconds=quiet_seconds, + max_duration=max_duration, ) - return regular, split_groups, pull_files + if output: + print(output, end="", flush=True) + if check_exit and rc != 0: + raise TerminalCommandError(f"command failed with rc={rc}: {command}") + return output, rc -def deploy_fail(cfg: dict[str, Any], all_logs: list[str], section: str, output: str) -> None: - all_logs.append(f"=== {section} ===\n{output}\n") - save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) - - -def deploy_board_cmd( - cfg: dict[str, Any], +def run_and_print_quiet_raw( term: Terminal, - all_logs: list[str], - case_id: str, command: str, - timeout: float, - error: str, - *, - retries: int = 1, + quiet_seconds: float = 1.0, + max_duration: float = 30.0, ) -> str: - try: - rc, out = board_term_run(cfg, term, case_id, command, timeout, retries=retries) - except TerminalTimeoutError as exc: - deploy_fail(cfg, all_logs, f"{case_id} (timed out)", exc.partial_output or "") - raise - if rc != 0: - deploy_fail(cfg, all_logs, case_id, out) - raise TerminalCommandError(f"{error}: {out.strip()}") - all_logs.append(f"=== {case_id} ===\n{out}\n") - return out - - -def deploy_scp_to_tmp( - cfg: dict[str, Any], - term: Terminal, - all_logs: list[str], - name: str, - tmp_path: str, - file_size: int, - file_timeout: float, -) -> None: - """Scp one file to tmp_path; recover when serial loses the result marker.""" - scp_cmd = ( - f"scp -q $CI_SSH_OPTS $CI_SSH_HOST:$CI_D/{name} {tmp_path} /dev/null" - ) - case_id = f"deploy_scp_{name}" - try: - deploy_board_cmd( - cfg, term, all_logs, case_id, scp_cmd, file_timeout, - f"board scp failed for {name!r}", retries=0, - ) - except TerminalTimeoutError: - check = f"test $(wc -c < {tmp_path}) -eq {file_size}" - rc, out = board_term_run(cfg, term, f"{case_id}_recover", check, 20.0, retries=0) - if rc != 0: - deploy_fail(cfg, all_logs, f"{case_id} (timed out)", out) - raise - all_logs.append(f"=== {case_id} (recovered) ===\n{out}\n") - print(f"[deploy] scp recovered after serial timeout: {name}", flush=True) - - -def deploy_install_from_tmp( - cfg: dict[str, Any], - term: Terminal, - all_logs: list[str], - name: str, - final_path: str, - file_size: int, -) -> None: - install_cmd = f"cp $CI_F {final_path} && rm -f $CI_F" - case_id = f"deploy_install_{name}" - try: - deploy_board_cmd( - cfg, term, all_logs, case_id, install_cmd, 30.0, - f"board install failed for {name!r}", retries=0, - ) - except TerminalTimeoutError: - check = f"test $(wc -c < {final_path}) -eq {file_size}" - rc, out = board_term_run(cfg, term, f"{case_id}_recover", check, 20.0, retries=0) - if rc != 0: - deploy_fail(cfg, all_logs, f"{case_id} (timed out)", out) - raise - all_logs.append(f"=== {case_id} (recovered) ===\n{out}\n") - print(f"[deploy] install recovered after serial timeout: {name}", flush=True) - - -def deploy_board_pull_file( - cfg: dict[str, Any], - term: Terminal, - staging_path: Path, - staging_dir: str, - work_dir: str, - name: str, - deadline: float, - all_logs: list[str], - *, - index: int, - total: int, -) -> None: - remaining = deadline - time.monotonic() - if remaining <= 0: - save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) - raise TerminalTimeoutError( - f"deploy pull timed out before {name!r}", - partial_output="".join(all_logs), - ) - - file_size = staging_path.joinpath(name).stat().st_size - file_timeout = deploy_file_pull_timeout(file_size, remaining) - tmp_path = DEPLOY_BOARD_TMP_FILE - final_path = board_path(work_dir, name) - print( - f"[deploy] scp ({index}/{total}): {name} ({file_size} bytes, timeout={file_timeout:.0f}s)", - flush=True, + # For non-shell environments (e.g. U-Boot), do not append shell-style + # status markers; just send and wait for output to go quiet. + output = term.send_until_quiet( + command, + quiet_seconds=quiet_seconds, + max_duration=max_duration, ) - deploy_scp_to_tmp(cfg, term, all_logs, name, tmp_path, file_size, file_timeout) - deploy_install_from_tmp(cfg, term, all_logs, name, final_path, file_size) + if output: + print(output, end="", flush=True) + return output -def deploy_assemble_split_file( - cfg: dict[str, Any], +def read_and_print_until_quiet( term: Terminal, - staging_path: Path, - work_dir: str, - base: str, - parts: list[str], - all_logs: list[str], -) -> None: - expected_size = sum(staging_path.joinpath(part).stat().st_size for part in parts) - part_args = " ".join(board_path(work_dir, part) for part in parts) - out_path = board_path(work_dir, base) - cat_cmd = f"cat {part_args} > {out_path}" - verify_cmd = f"test $(wc -c < {out_path}) -eq {expected_size}" - print(f"[deploy] assemble split file {base} from {len(parts)} part(s)", flush=True) - deploy_board_cmd( - cfg, term, all_logs, f"deploy_cat_{base}", cat_cmd, 120.0, - f"board assemble failed for {base!r}", retries=1, - ) - deploy_board_cmd( - cfg, term, all_logs, f"deploy_verify_{base}", verify_cmd, 30.0, - f"board assemble verify failed for {base!r}: expected {expected_size} bytes", - retries=1, - ) - rm_parts = " ".join(board_path(work_dir, part) for part in parts) - term.run(f"deploy_rm_parts_{base}", board_priv(cfg, f"rm -f {rm_parts}"), timeout=30.0) - - -def deploy_gunzip_artifact( - cfg: dict[str, Any], - term: Terminal, - work_dir: str, - gz_name: str, - all_logs: list[str], -) -> None: - raw_name = DEPLOY_GUNZIP_ARTIFACTS[gz_name] - raw_path = Path(cfg["hvisor_tool_path"]) / "output" / raw_name - raw_size = raw_path.stat().st_size - gunzip_cmd = ( - f"gunzip -f {board_path(work_dir, gz_name)} && " - f"test $(wc -c < {board_path(work_dir, raw_name)}) -eq {raw_size}" - ) - print(f"[deploy] gunzip: {gz_name} -> {raw_name}", flush=True) - deploy_board_cmd( - cfg, term, all_logs, f"deploy_gunzip_{raw_name}", gunzip_cmd, 120.0, - f"board gunzip failed for {gz_name!r}", + quiet_seconds: float = 3.0, + max_duration: float = 120.0, +) -> str: + # Read side is decoupled from send side for interactive boot flows. + output = term.read_until_quiet( + quiet_seconds=quiet_seconds, + max_duration=max_duration, ) + if output: + print(output, end="", flush=True) + return output -def board_term_run( - cfg: dict[str, Any], +def run_and_print_send_only( term: Terminal, - case_id: str, command: str, - timeout: float, - *, - retries: int = 1, -) -> tuple[int, str]: - last_exc: TerminalTimeoutError | None = None - for attempt in range(retries + 1): - if attempt > 0: - print(f"[deploy] retry {case_id} ({attempt}/{retries})", flush=True) - time.sleep(3.0) - run_id = case_id if attempt == 0 else f"{case_id}_retry{attempt}" - try: - return term.run(run_id, board_priv(cfg, command), timeout=timeout) - except TerminalTimeoutError as exc: - last_exc = exc - assert last_exc is not None - raise last_exc - - -def deploy_board_pull(cfg: dict[str, Any], term: Terminal) -> int: - """Board pulls staged zone1 files from the CI host over scp.""" - ensure_board_net(cfg, term) - - host_ip = cfg["network_host_ip"] - host_user = cfg["network_host_user"] - - print("[deploy] staging files on host", flush=True) - stage_board_files(cfg) - - staging_dir = deploy_board_staging_alias(cfg) - staging_path = Path(cfg["network_staging_dir"]) - if not staging_path.is_dir(): - raise TerminalCommandError(f"staging dir not found: {staging_path}") - - work_dir = deploy_ensure_work_dir(cfg, term) - - regular_files, split_groups, pull_files = parse_staging(staging_path) - if not pull_files and not split_groups: - raise TerminalCommandError(f"no staged files in {staging_path}") - - pull_timeout = float(cfg.get("deploy_pull_timeout", 600.0)) - deadline = time.monotonic() + pull_timeout - all_logs: list[str] = [] - - stale = sorted(set(pull_files) | set(split_groups) | set(DEPLOY_GUNZIP_ARTIFACTS.values())) - print(f"[deploy] removing stale files in {work_dir}", flush=True) - for name in stale: - stale_path = board_path(work_dir, name) - term.run(f"deploy_clean_{name}", board_priv(cfg, f"rm -f {stale_path}"), timeout=15.0) - - print( - f"[deploy] pulling {len(pull_files)} file(s) via scp from {host_user}@{host_ip}:{staging_dir}", - flush=True, - ) - if split_groups: - print(f"[deploy] split artifacts: {', '.join(sorted(split_groups))}", flush=True) - - board_term_run( - cfg, - term, - "deploy_pull_env", - f"export CI_D={shlex.quote(staging_dir)} CI_F={DEPLOY_BOARD_TMP_FILE}", - 30.0, - retries=0, - ) - - for index, name in enumerate(pull_files, start=1): - if index > 1: - time.sleep(3.0) - deploy_board_pull_file( - cfg, - term, - staging_path, - staging_dir, - work_dir, - name, - deadline, - all_logs, - index=index, - total=len(pull_files), - ) - - for base, parts in sorted(split_groups.items()): - deploy_assemble_split_file(cfg, term, staging_path, work_dir, base, parts, all_logs) - - for gz_name in DEPLOY_GUNZIP_ARTIFACTS: - if gz_name in regular_files or gz_name in split_groups: - deploy_gunzip_artifact(cfg, term, work_dir, gz_name, all_logs) - - save_case_log(cfg, "deploy_scp.log", "".join(all_logs)) - - boot_script = cfg["zone1_boot_script"] - term.run( - "deploy_chmod", - board_priv( - cfg, - f"chmod +x {board_path(work_dir, boot_script)} " - f"{board_path(work_dir, 'hvisor')} {board_path(work_dir, 'check_serial.sh')} 2>/dev/null || true", - ), - timeout=60.0, - ) - print("[deploy] board_pull completed", flush=True) - return 0 - - -def deploy(cfg: dict[str, Any], term: Terminal | None) -> int: - print("————————————————\ncase: deploy\n————————————————\n", flush=True) - if cfg["mode"] != "board": - print("[deploy] skipped (not board mode)", flush=True) - return 0 - - if term is None: - raise SystemExit("terminal backend is required (run zone0_start first)") - return deploy_board_pull(cfg, term) - - -def zone0_ready_pattern(cfg: dict[str, Any]) -> str: - custom = str(cfg.get("zone0_ready_pattern", "")).strip() - if custom: - return custom - return ZONE0_READY_PATTERN - - -def board_shell_ready_pattern(cfg: dict[str, Any]) -> str: - custom = str(cfg.get("board_shell_ready_pattern", "")).strip() - if custom: - return custom - board_user = str(cfg.get("board_user", "root")).strip() or "root" - if board_user == "root": - return BOARD_ROOT_SHELL_PATTERN - return rf"{re.escape(board_user)}@[^\r\n]*[$#]\s?" - - -def uboot_commands(cfg: dict[str, Any]) -> list[str]: - raw = cfg.get("uboot_cmds") - if isinstance(raw, list): - cmds = [str(item).strip() for item in raw if str(item).strip()] - if cmds: - return cmds - cmd = str(cfg.get("uboot_cmd", "")).strip() - return [cmd] if cmd else [] - - -def send_board_uboot_commands(cfg: dict[str, Any], term: Terminal) -> None: - cmds = uboot_commands(cfg) - if not cmds: - return - uboot_ready = cfg.get("uboot_ready_pattern", "") or r"=>" - initial_timeout = float(cfg.get("uboot_prompt_timeout", 20.0)) - step_timeout = float(cfg.get("uboot_step_timeout", 0.0)) - if step_timeout <= 0: - step_timeout = max(initial_timeout, 60.0) - - autoboot_window = float(cfg.get("uboot_autoboot_window", 0.0)) - board_wait_uboot_prompt( - term, - uboot_ready, - timeout=initial_timeout, - autoboot_window=autoboot_window, - ) - for index, cmd in enumerate(cmds): - send_offset = term.offset() - term.send(cmd) - if index < len(cmds) - 1: - board_wait_uboot_prompt( - term, - uboot_ready, - timeout=step_timeout, - after_offset=send_offset, - ) + read_duration: float = 0.5, +) -> str: + # For commands that switch interactive context (e.g. screen attach), + # only send and collect a short best-effort echo. + output = term.send_and_drain(command, read_duration=read_duration) + if output: + print(output, end="", flush=True) + return output def zone0_start(cfg: dict[str, Any], term: Terminal | None) -> int: print("————————————————\ncase: zone0_start\n————————————————\n", flush=True) if cfg["mode"] == "qemu": - # Create log dir before starting QEMU so a permission failure does not - # leave a running guest that must be SIGTERM'd from finally. - log_path = logs_dir(cfg) / "zone0_console.log" - log_path.write_text("", encoding="utf-8") - cmd = ["make", f"ARCH={cfg['arch']}", f"BOARD={cfg['board']}", "MODE=release", "ci-run"] proc = subprocess.Popen(cmd, cwd=cfg["workspace"], start_new_session=True) cfg["_managed_proc"] = proc cfg["_managed_proc_name"] = "qemu ci-run" wait_qemu_socket(cfg["socket_path"], timeout=30.0) - - qemu_term = ensure_qemu_terminal(cfg, log_path) - cmds = uboot_commands(cfg) - if cmds: - uboot_ready = cfg.get("uboot_ready_pattern", "") or r"*=>" - if not qemu_term.wait_pattern(uboot_ready, timeout=10.0): - raise TerminalTimeoutError("timed out waiting for U-Boot prompt") - for cmd in cmds: - qemu_term.send(cmd) - if not qemu_term.wait_pattern(zone0_ready_pattern(cfg), timeout=180.0): - raise TerminalTimeoutError("timed out waiting for zone0 shell prompt") - cfg["board_is_root"] = True - cfg["_board_shell_ready"] = True + with build_terminal(cfg) as qemu_term: + bid = cfg["bid"] + if bid == "aarch64/qemu-gicv3": + _ = read_and_print_until_quiet( + qemu_term, + quiet_seconds=3.0, + max_duration=10.0, + ) + qemu_term.send("bootm 0x40400000 - 0x40000000") + if bid == "x86_64/qemu": + time.sleep(10.0) + _ = read_and_print_until_quiet( + qemu_term, + quiet_seconds=5, + max_duration=180.0, + ) return 0 if cfg["mode"] == "board": - log_path = logs_dir(cfg) / "zone0_console.log" - log_path.write_text("", encoding="utf-8") - - board_term = build_terminal(cfg, log_path) - board_term.open() - cfg["_board_term"] = board_term - boot_board_zone0_shell(cfg, board_term, power_cycle=True) + # TODO: reboot board return 0 return 0 -def lspci_expected_cfg(cfg: dict[str, Any]) -> dict[str, Any]: - raw = cfg.get("lspci") or {} - if not isinstance(raw, dict): - raw = {} - expected_bdfs = [str(x) for x in raw.get("expected_bdfs") or []] - min_count = int(raw.get("min_count", len(expected_bdfs) or 1)) - return {"expected_bdfs": expected_bdfs, "min_count": min_count} - - -def lspci(cfg: dict[str, Any], term: Terminal | None) -> int: - print("————————————————\ncase: lspci\n————————————————\n", flush=True) - if term is None: - raise SystemExit("terminal backend is required (run zone0_start first)") - - expected = lspci_expected_cfg(cfg) - - _, output = term.run( - "lspci", - board_priv(cfg, "timeout 20 lspci -D > /tmp/ci_lspci.log 2>&1; cat /tmp/ci_lspci.log"), - timeout=60.0, - ) - save_case_log(cfg, "lspci.log", f"=== lspci -D ===\n{output}\n") - - matched = [bdf for bdf in expected["expected_bdfs"] if bdf in output] - missing = [bdf for bdf in expected["expected_bdfs"] if bdf not in output] - line_count = len([line for line in output.splitlines() if line.strip()]) - - print(f"[lspci] devices listed: {line_count}", flush=True) - if expected["expected_bdfs"]: - print( - f"[lspci] matched expected BDFs ({len(matched)}/{len(expected['expected_bdfs'])}): {matched}", - flush=True, - ) - if missing: - print(f"[lspci] missing expected BDFs: {missing}", flush=True) - - if expected["expected_bdfs"]: - if len(matched) < expected["min_count"]: - raise TerminalCommandError( - "lspci found " - f"{len(matched)}/{expected['min_count']} expected devices; " - f"missing: {missing}" - ) - elif line_count < expected["min_count"]: - raise TerminalCommandError( - f"lspci listed {line_count} devices, expected at least {expected['min_count']}" - ) - - print("lspci verification passed", flush=True) - return 0 - - -def ping_success(output: str) -> bool: - if re.search(r"0%\s*(?:packet\s*)?loss\b", output, re.I): - return True - if re.search(r"0%\s*包丢失", output): - return True - match = re.search(r"(\d+)\s*packets?\s*received", output, re.I) - if match and int(match.group(1)) > 0: - return True - match = re.search(r"已接收\s*(\d+)\s*个包", output) - return bool(match and int(match.group(1)) > 0) - - -def board_login_user(term: Terminal) -> str: - _, out = term.run("board_id", "id -un", timeout=15.0) - for line in out.splitlines(): - candidate = line.strip() - if candidate and "__R__" not in candidate and re.fullmatch(r"[\w.-]+", candidate): - return candidate - raise TerminalCommandError(f"failed to detect board login user: {out.strip()!r}") - - -def ensure_board_shell_user(cfg: dict[str, Any], term: Terminal) -> None: - """Switch shell user when board_shell_su_cmd is set in ci.yaml.""" - su_cmd = str(cfg.get("board_shell_su_cmd", "")).strip() - if not su_cmd: - return - target = str(cfg.get("board_user", "root")).strip() or "root" - if board_login_user(term) == target: - return - - shell_prompt = board_shell_ready_pattern(cfg) - password = str(cfg.get("board_su_password", "")) - print(f"[deploy] {su_cmd}", flush=True) - offset = term.offset() - term.send(su_cmd) - deadline = time.monotonic() + 20.0 - sent_password = False - while time.monotonic() < deadline: - chunk = term.tail_since(offset) - if not sent_password and re.search(BOARD_SU_PASSWORD_PROMPT, chunk): - term.send(password) - sent_password = True - if re.search(shell_prompt, chunk): - break - time.sleep(0.1) - else: - raise TerminalCommandError(f"timed out waiting for shell after {su_cmd!r}") - - # Serial su may print job-control errors; an extra enter wakes the ready shell. - time.sleep(0.3) - term.send("") - time.sleep(0.3) - if board_login_user(term) != target: - raise TerminalCommandError(f"failed to switch shell user to {target}") - - -def prepare_board_shell(cfg: dict[str, Any], term: Terminal) -> None: - """Enter board shell once: optional su, detect root/user, export HOME.""" - if cfg.get("_board_shell_ready"): - return - - ensure_board_shell_user(cfg, term) - user = board_login_user(term) - cfg["board_is_root"] = user == "root" - term.run("board_home_export", "export HOME=$(cd ~ && pwd)", timeout=15.0) - cfg["_board_shell_ready"] = True - print(f"[board] shell ready: user={user} home={BOARD_HOME}", flush=True) - - -def deploy_file_pull_timeout(file_size: int, remaining: float) -> float: - """Per-file timeout from size (8 KiB/s floor) capped by the deploy deadline.""" - min_bps = 8192.0 - needed = max(90.0, file_size / min_bps + 45.0) - return min(remaining, needed) - - def zone1_start(cfg: dict[str, Any], term: Terminal | None) -> int: print("————————————————\ncase: zone1_start\n————————————————\n", flush=True) if term is None: raise SystemExit("terminal backend is required") - - work_dir = board_path(cfg["zone1_work_dir"]) - boot_script = cfg["zone1_boot_script"] - inner_log = "/tmp/zone1_inner.log" - _, _ = term.run("zone1_ls", f"cd {work_dir} && ls", timeout=15.0) - boot_rc, _ = term.run("zone1_boot", f"cd {work_dir} && ./{boot_script}", timeout=120.0) - if str(cfg.get("board_user", "root")).strip() not in ("", "root"): - zone_list_cmd = f"cd {work_dir} && sudo ./hvisor zone list" - else: - zone_list_cmd = f"cd {work_dir} && ./hvisor zone list" - _, _ = term.run("zone1_list", zone_list_cmd, timeout=15.0) - - start_wait = float(cfg.get("zone1_start_wait", 0.0)) - if start_wait > 0: - print(f"[zone1] waiting {start_wait}s for zone1 console", flush=True) - time.sleep(start_wait) - - max_pts = find_zone1_pts(term) - - check_rc, _ = term.run( - "zone1_serial_check", - ( - f"cd {work_dir} && ./check_serial.sh /dev/pts/{max_pts} {inner_log} " - f"{int(ZONE1_INNER_PROMPT_TIMEOUT)}" - ), - timeout=ZONE1_INNER_PROMPT_TIMEOUT + 30.0, - ) - _, inner_output = term.run( - "zone1_inner_log", - f"tail -c 131072 {inner_log}", - timeout=ZONE1_INNER_LOG_FETCH_TIMEOUT, + # _ = run_and_print_quiet_raw(term, "bash", quiet_seconds=1.0, max_duration=15.0) + _, _ = run_and_print_quiet(term, "cd /root", quiet_seconds=1.0, max_duration=15.0) + _, _ = run_and_print_quiet(term, "ls", quiet_seconds=1.0, max_duration=15.0) + _, _ = run_and_print_quiet(term, "cat boot_zone1.sh", quiet_seconds=1.0, max_duration=15.0) + _, boot_rc = run_and_print_quiet( + term, + "./boot_zone1.sh", + quiet_seconds=15, + max_duration=30.0, ) - save_inner_serial_log(cfg, inner_output) - + _, _ = run_and_print_quiet(term, "./hvisor zone list", quiet_seconds=1.0, max_duration=15.0) + if cfg["arch"] != "x86_64": + _ = run_and_print_quiet_raw(term, "script /dev/null", quiet_seconds=1.0, max_duration=15.0) + pts_output, _ = run_and_print_quiet(term, "ls -1 /dev/pts/[0-9]*", quiet_seconds=1.0, max_duration=15.0) + pts_numbers = sorted(int(match) for match in re.findall(r"/dev/pts/(\d+)", pts_output)) + if not pts_numbers: + raise TerminalCommandError("failed to find numeric pts from 'ls -1 /dev/pts/[0-9]*'") + max_pts = pts_numbers[-1] + _ = run_and_print_send_only(term, f"screen /dev/pts/{max_pts}", read_duration=20.0) + _ = run_and_print_send_only(term, "\n", read_duration=2.0) + _, _ = run_and_print_quiet(term, "ls", quiet_seconds=1.0, max_duration=15.0) if boot_rc != 0: - raise TerminalCommandError(f"command failed with rc={boot_rc}: ./{boot_script}") - if check_rc != 0: - raise TerminalCommandError( - f"command failed with rc={check_rc}: check_serial.sh (no console prompt)" - ) - print("zone1_started successfully", flush=True) + raise TerminalCommandError(f"command failed with rc={boot_rc}: sh ./boot_zone1.sh") + else: + print("zone1_started successfully", flush=True) return 0 CASE_HANDLERS: dict[str, CaseFunc] = { - "deploy": deploy, "zone0_start": zone0_start, - "lspci": lspci, "zone1_start": zone1_start, } @@ -1056,78 +218,23 @@ def load_runtime_config(args: argparse.Namespace) -> dict[str, Any]: if not cases: raise SystemExit(f"no test cases configured for bid '{args.bid}'") - cell_root = Path.cwd() - build_args = bid_entry.get("build_args") or {} - deploy_cfg = tests.get("deploy") or tests.get("network") or {} - if not isinstance(deploy_cfg, dict): - deploy_cfg = {} - - def cfg_str(key: str, default: str = "") -> str: - raw = deploy_cfg.get(key, tests.get(key, default)) - return str(raw).strip() if raw is not None else default - - board_ip = cfg_str("board_ip") - board_user = cfg_str("board_user", "root") or "root" - default_work_dir = "/root" if board_user == "root" else f"/home/{board_user}" - hvisor_tool_path = os.environ.get("HVISOR_TOOL_PATH", "").strip() - if not hvisor_tool_path: - hvisor_tool_path = str((cell_root / "hvisor-tool").resolve()) - elif not Path(hvisor_tool_path).is_absolute(): - hvisor_tool_path = str((cell_root / hvisor_tool_path).resolve()) return { "bid": args.bid, "arch": arch, "board": board, "mode": mode, "cases": cases, - "workspace": cell_root, - "kdir": str(build_args.get("KDIR", "")).strip(), - "hvisor_tool_path": hvisor_tool_path, - "socket_path": str((cell_root / ".qemu" / "qemu.sock").resolve()), + "workspace": Path(__file__).resolve().parent.parent, + "socket_path": str((Path(__file__).resolve().parent.parent / ".qemu" / "qemu.sock").resolve()), "serial_port": str(tests.get("serial", "/dev/null")), - "power_serial": str(tests.get("power_serial", "")).strip(), - "power_channel": int(tests.get("power_channel", 4)), "baudrate": int(tests.get("baudrate", 1500000)), - "uboot_cmd": str(tests.get("uboot_cmd", "")).strip(), - "uboot_cmds": tests.get("uboot_cmds") or [], - "uboot_ready_pattern": str(tests.get("uboot_ready_pattern", "")).strip(), - "uboot_prompt_timeout": float(tests.get("uboot_prompt_timeout", 20.0)), - "uboot_step_timeout": float(tests.get("uboot_step_timeout", 0.0)), - "uboot_autoboot_window": float(tests.get("uboot_autoboot_window", 0.0)), - "zone0_ready_pattern": str(tests.get("zone0_ready_pattern", "")).strip(), - "board_shell_ready_pattern": str(tests.get("board_shell_ready_pattern", "")).strip(), - "board_shell_su_cmd": str(tests.get("board_shell_su_cmd", "")).strip(), - "zone0_shell_timeout": float(tests.get("zone0_shell_timeout", 300.0 if mode == "board" else 180.0)), - "tftp_dir": str(tests.get("tftp_dir", "/home/light/tftp")).strip(), - "lspci": tests.get("lspci") or {}, - "board_ip": board_ip, - "board_user": board_user, - "board_iface": cfg_str("board_iface", "eth0"), - "board_netmask": cfg_str("netmask", "255.255.255.0"), - "board_su_password": cfg_str("su_password", ""), - "zone1_work_dir": cfg_str("zone1_work_dir", default_work_dir), - "zone1_boot_script": cfg_str("zone1_boot_script", "boot_zone1.sh"), - "network_host_ip": cfg_str("host_ip", "192.168.1.181"), - "network_host_user": cfg_str("host_user", "light"), - "network_staging_dir": resolve_staging_dir( - cfg_str("staging_dir", "/home/light/ci_deploy"), - arch, - board, - ), - "network_ping_count": int(deploy_cfg.get("ping_count", tests.get("ping_count", 3))), - "deploy_link_wait": float(deploy_cfg.get("link_wait", 0.0)), - "deploy_ping_retries": int(deploy_cfg.get("ping_retries", 1)), - "deploy_ssh_connect_timeout": int(deploy_cfg.get("ssh_connect_timeout", 30)), - "deploy_pull_timeout": float(deploy_cfg.get("pull_timeout", 600.0)), - "zone1_dtb": cfg_str("zone1_dtb"), - "zone1_start_wait": float(tests.get("zone1_start_wait", 30.0)), } -def build_terminal(cfg: dict[str, Any], log_path: Path) -> Terminal: +def build_terminal(cfg: dict[str, Any]) -> Terminal: if cfg["mode"] == "qemu": - return Terminal.from_qemu_socket(path=cfg["socket_path"], log_path=log_path) - return Terminal.from_serial(port=cfg["serial_port"], baudrate=cfg["baudrate"], log_path=log_path) + return Terminal.from_qemu_socket(path=cfg["socket_path"]) + return Terminal.from_serial(port=cfg["serial_port"], baudrate=cfg["baudrate"]) def main() -> int: @@ -1140,31 +247,14 @@ def main() -> int: available = ", ".join(sorted(CASE_HANDLERS.keys())) raise SystemExit(f"unknown case '{case_name}', available: {available}") - standalone = case_name == "zone0_start" - if standalone: - try: - rc = case_fn(cfg, None) - except (TerminalTimeoutError, TerminalCommandError) as exc: - print(f"[ci_runner] terminal command failed in case '{case_name}': {exc}", flush=True) - return 1 + if case_name == "zone0_start": + rc = case_fn(cfg, None) if rc != 0: return rc time.sleep(5.0) continue - - term = get_active_terminal(cfg) - if term is None: - log_path = logs_dir(cfg) / "zone1_console.log" - log_path.write_text("", encoding="utf-8") - with build_terminal(cfg, log_path) as term: - try: - rc = case_fn(cfg, term) - except (TerminalTimeoutError, TerminalCommandError) as exc: - print(f"[ci_runner] terminal command failed in case '{case_name}': {exc}", flush=True) - return 1 - if rc != 0: - return rc - else: + + with build_terminal(cfg) as term: try: rc = case_fn(cfg, term) except (TerminalTimeoutError, TerminalCommandError) as exc: @@ -1172,13 +262,10 @@ def main() -> int: return 1 if rc != 0: return rc - time.sleep(5.0) + time.sleep(5.0) return 0 finally: - close_active_terminal(cfg) terminate_managed_process(cfg) - if cfg.get("mode") == "board": - board_power_off(cfg) if __name__ == "__main__": diff --git a/jenkins/hvisor-tool.yaml b/jenkins/hvisor-tool.yaml deleted file mode 100644 index 4ef4876cc..000000000 --- a/jenkins/hvisor-tool.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# CI checkout source for hvisor-tool (Jenkins stage "Checkout hvisor-tool", after Linter). -# ref: branch name, refs/pull//head, refs/tags/, or commit SHA. -# -# Examples (uncomment one repo/ref pair): -# -# repo: https://github.com/syswonder/hvisor-tool.git -# ref: main -# -# repo: https://github.com/syswonder/hvisor-tool.git -# ref: feature/my-change -# -# repo: https://github.com/syswonder/hvisor-tool.git -# ref: refs/pull/123/head -# -# repo: https://github.com/syswonder/hvisor-tool.git -# ref: v0.3.1 -# -# repo: https://github.com/alice/hvisor-tool.git -# ref: my-branch -# -# "hvisor-tool on the branch of test" fails unless ref is main. -repo: https://github.com/syswonder/hvisor-tool.git -ref: refs/pull/117/head diff --git a/jenkins/prepare.sh b/jenkins/prepare.sh old mode 100755 new mode 100644 index 4e2f71e16..ad0f3d1a9 --- a/jenkins/prepare.sh +++ b/jenkins/prepare.sh @@ -106,34 +106,9 @@ fi cp "${ZONE1_BOOT_SCRIPT}" "${ROOTFS_DIR}/root/" -CHECK_SERIAL_SCRIPT="${WORKSPACE_ROOT}/jenkins/check_serial.sh" -if [ -f "${CHECK_SERIAL_SCRIPT}" ]; then - cp "${CHECK_SERIAL_SCRIPT}" "${ROOTFS_DIR}/root/" - chmod +x "${ROOTFS_DIR}/root/check_serial.sh" -fi - if [ -f "${ROOTFS_DIR}/root/boot_zone1.sh" ]; then chmod +x "${ROOTFS_DIR}/root/boot_zone1.sh" fi if [ -f "${ROOTFS_DIR}/root/screen_zone1.sh" ]; then chmod +x "${ROOTFS_DIR}/root/screen_zone1.sh" fi - -ROOTFS2_IMG="" -if [ -f "${VIRTDISK_DIR}/rootfs2.ext4" ]; then - ROOTFS2_IMG="${VIRTDISK_DIR}/rootfs2.ext4" -elif [ -f "${VIRTDISK_DIR}/rootfs2.img" ]; then - ROOTFS2_IMG="${VIRTDISK_DIR}/rootfs2.img" -fi - -if [ -n "${ROOTFS2_IMG}" ]; then - ROOTFS2_MNT="${VIRTDISK_DIR}/rootfs2" - mkdir -p "${ROOTFS2_MNT}" - mount -o loop "${ROOTFS2_IMG}" "${ROOTFS2_MNT}" - if [ -f "${ROOTFS2_MNT}/init" ]; then - chmod +x "${ROOTFS2_MNT}/init" - fi - umount "${ROOTFS2_MNT}" - rmdir "${ROOTFS2_MNT}" - cp "${ROOTFS2_IMG}" "${ROOTFS_DIR}/root/" -fi diff --git a/jenkins/terminal.py b/jenkins/terminal.py index d4591d93a..dabf31cef 100644 --- a/jenkins/terminal.py +++ b/jenkins/terminal.py @@ -3,6 +3,7 @@ from __future__ import annotations +import os import queue import re import select @@ -16,8 +17,6 @@ import serial -RUN_RESULT_RE = re.compile(r"__R__(?P[a-f0-9]+):(?P\d+)") - class TerminalTimeoutError(TimeoutError): """Raised when terminal command wait times out.""" @@ -146,7 +145,7 @@ def flush_input(self) -> None: class LogCollector: - """Background reader that writes terminal output to a log file and console.""" + """Background reader that continuously captures terminal output.""" def __init__( self, @@ -183,7 +182,6 @@ def stop(self) -> None: if self._thread is not None: self._thread.join(timeout=5.0) self._thread = None - # Drain any bytes still buffered in the backend after the reader exits. deadline = time.monotonic() + 1.0 while time.monotonic() < deadline: chunk = self.backend.read() @@ -206,10 +204,6 @@ def tail_since(self, offset: int) -> str: return self._buffer return self._buffer[offset:] - def text(self) -> str: - with self._lock: - return self._buffer - def _append(self, chunk: str, *, emit_console: bool = True) -> None: if not chunk: return @@ -239,35 +233,42 @@ def _run_loop(self) -> None: time.sleep(self.poll_interval) +def _default_log_path() -> Path: + return Path(f"/tmp/hvisor-terminal-{os.getpid()}.log") + + class Terminal: - """High level terminal wrapper with log-file-driven command helpers.""" + """High level terminal wrapper with command helpers.""" def __init__( self, backend: TerminalBackend, - log_path: Path, encoding: str = "utf-8", + log_path: Path | None = None, console: bool = True, ) -> None: self.backend = backend self.encoding = encoding - self._collector = LogCollector(backend, log_path, encoding=encoding, console=console) + self._log_path = log_path or _default_log_path() + self._collector = LogCollector( + backend, self._log_path, encoding=encoding, console=console + ) self._opened = False @classmethod def from_qemu_socket( cls, path: str, - log_path: Path, connect_timeout: float = 10.0, io_timeout: float = 0.2, encoding: str = "utf-8", + log_path: Path | None = None, console: bool = True, ) -> "Terminal": return cls( QemuSocketBackend(path=path, connect_timeout=connect_timeout, io_timeout=io_timeout), - log_path=log_path, encoding=encoding, + log_path=log_path, console=console, ) @@ -275,16 +276,16 @@ def from_qemu_socket( def from_serial( cls, port: str, - log_path: Path, baudrate: int = 115200, timeout: float = 0.2, encoding: str = "utf-8", + log_path: Path | None = None, console: bool = True, ) -> "Terminal": return cls( SerialBackend(port=port, baudrate=baudrate, timeout=timeout), - log_path=log_path, encoding=encoding, + log_path=log_path, console=console, ) @@ -313,76 +314,195 @@ def flush_input(self) -> None: self._ensure_open() self.backend.flush_input() - def offset(self) -> int: - self._ensure_open() - return self._collector.offset() - - def tail_since(self, offset: int) -> str: + def wait_pattern( + self, + pattern: str, + timeout: float = 120.0, + poll_interval: float = 0.05, + from_offset: int | None = None, + ) -> bool: + """Wait until regex pattern appears in the collected log.""" self._ensure_open() - return self._collector.tail_since(offset) + offset = self._collector.offset() if from_offset is None else from_offset + compiled = re.compile(pattern) + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + chunk = self._collector.tail_since(offset) + if compiled.search(chunk): + return True + time.sleep(poll_interval) + return False def send(self, command: str) -> None: self._ensure_open() payload = command.rstrip("\n") + "\n" self.backend.write(payload.encode(self.encoding, errors="replace")) - def run( + def read_for( + self, + duration: float = 2.0, + poll_interval: float = 0.05, + ) -> str: + self._ensure_open() + offset = self._collector.offset() + deadline = time.monotonic() + duration + while time.monotonic() < deadline: + time.sleep(poll_interval) + return self._collector.tail_since(offset) + + def send_until_get( self, - case: str, command: str, timeout: float = 30.0, poll_interval: float = 0.05, - wake_interval: float | None = None, - ) -> tuple[int, str]: - """Run a shell command and wait for the compact result marker in the log.""" + include_marker_line: bool = False, + ) -> str: self._ensure_open() - run_id = uuid.uuid4().hex[:8] + marker = f"__HV_TERMINAL_DONE_{uuid.uuid4().hex}__" offset = self._collector.offset() - wrapped = f"{command}; echo __R__{run_id}:$?" - self.send(wrapped) + self.send(f"{command}; echo {marker}") deadline = time.monotonic() + timeout - next_wake = time.monotonic() + wake_interval if wake_interval else None - run_id_needle = f"__R__{run_id}:" while time.monotonic() < deadline: - chunk = self._collector.tail_since(offset) - if run_id_needle in chunk: - matches = list(RUN_RESULT_RE.finditer(chunk)) - for match in reversed(matches): - if match.group("run_id") == run_id: - rc = int(match.group("rc")) - output = chunk[: match.start()] - return rc, output - if next_wake is not None and time.monotonic() >= next_wake: - self.send("") - next_wake = time.monotonic() + wake_interval + buf = self._collector.tail_since(offset) + if marker in buf: + if include_marker_line: + return buf + return self._trim_after_marker(buf, marker) time.sleep(poll_interval) - - partial = self._collector.tail_since(offset) raise TerminalTimeoutError( - f"timed out waiting for run result (case={case}, run_id={run_id}): {command}", - partial_output=partial, + f"timed out waiting for terminal marker: {marker}", + partial_output=self._collector.tail_since(offset), ) - def wait_pattern( + def send_until_quiet( self, - pattern: str, - timeout: float = 120.0, + command: str, + quiet_seconds: float = 1.0, + max_duration: float = 30.0, poll_interval: float = 0.05, - from_offset: int | None = None, - ) -> bool: - """Wait until regex pattern appears in the collected log.""" + ) -> str: self._ensure_open() - offset = self._collector.offset() if from_offset is None else from_offset - compiled = re.compile(pattern) - deadline = time.monotonic() + timeout + offset = self._collector.offset() + self.send(command) + return self._wait_quiet(offset, quiet_seconds, max_duration, poll_interval, context=command) + + def send_and_drain( + self, + command: str, + read_duration: float = 0.5, + poll_interval: float = 0.05, + ) -> str: + """Send command and collect best-effort output for a fixed duration.""" + self._ensure_open() + offset = self._collector.offset() + self.send(command) + deadline = time.monotonic() + read_duration while time.monotonic() < deadline: - chunk = self._collector.tail_since(offset) - if compiled.search(chunk): - return True time.sleep(poll_interval) - return False + return self._collector.tail_since(offset) + + def read_until_quiet( + self, + quiet_seconds: float = 3.0, + max_duration: float = 120.0, + poll_interval: float = 0.05, + ) -> str: + """Continuously read until quiet for x seconds or total timeout.""" + self._ensure_open() + offset = self._collector.offset() + try: + return self._wait_quiet( + offset, quiet_seconds, max_duration, poll_interval, context="read" + ) + except TerminalTimeoutError: + return self._collector.tail_since(offset) + + def run_until_quiet_with_status( + self, + command: str, + quiet_seconds: float = 1.0, + max_duration: float = 30.0, + poll_interval: float = 0.05, + ) -> tuple[str, int]: + marker = f"__HV_TERMINAL_RC_{uuid.uuid4().hex}__" + wrapped = f"{command}; echo {marker}0" + self._ensure_open() + offset = self._collector.offset() + self.send(wrapped) + + deadline = time.monotonic() + max_duration + marker_pattern = re.compile(re.escape(marker) + r"(\d+)") + rc = -1 + marker_seen_at = 0.0 + + while time.monotonic() < deadline: + buf = self._collector.tail_since(offset) + matches = list(marker_pattern.finditer(buf)) + if matches: + last = matches[-1] + rc = int(last.group(1)) + if marker_seen_at <= 0.0: + marker_seen_at = time.monotonic() + elif marker_seen_at <= 0.0: + time.sleep(poll_interval) + continue + + if marker_seen_at > 0.0 and (time.monotonic() - marker_seen_at) >= quiet_seconds: + cleaned = self._strip_status_marker(buf, marker) + return cleaned, rc + + time.sleep(poll_interval) + + raise TerminalTimeoutError( + f"timed out waiting for command status marker: {marker}", + partial_output=self._collector.tail_since(offset), + ) + + def _wait_quiet( + self, + offset: int, + quiet_seconds: float, + max_duration: float, + poll_interval: float, + *, + context: str, + ) -> str: + start = time.monotonic() + deadline = start + max_duration + last_len = self._collector.offset() + last_output_at = start + + while time.monotonic() < deadline: + current_len = self._collector.offset() + if current_len > last_len: + last_len = current_len + last_output_at = time.monotonic() + elif (time.monotonic() - last_output_at) >= quiet_seconds: + return self._collector.tail_since(offset) + + time.sleep(poll_interval) + + raise TerminalTimeoutError( + f"timed out waiting for terminal quiet period after command: {context}", + partial_output=self._collector.tail_since(offset), + ) def _ensure_open(self) -> None: if not self._opened: self.open() + + @staticmethod + def _trim_after_marker(output: str, marker: str) -> str: + idx = output.find(marker) + if idx < 0: + return output + return output[:idx] + + @staticmethod + def _strip_status_marker(output: str, marker: str) -> str: + pattern = re.compile(re.escape(marker) + r"\d+") + matches = list(pattern.finditer(output)) + if not matches: + return output + return output[: matches[-1].start()] diff --git a/platform/aarch64/phytium-pi/scripts/boot_zone1.sh b/platform/aarch64/phytium-pi/scripts/boot_zone1.sh deleted file mode 100755 index 442b107d2..000000000 --- a/platform/aarch64/phytium-pi/scripts/boot_zone1.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -insmod hvisor.ko 2>/dev/null || true -mount -t proc proc /proc 2>/dev/null || true -mount -t sysfs sysfs /sys 2>/dev/null || true -rm -f nohup.out -mkdir -p /dev/pts -mount -t devpts devpts /dev/pts 2>/dev/null || true -./hvisor virtio start zone1-linux-virtio.json & -./hvisor zone start zone1-linux.json