Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions ostool/src/run/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,35 @@ pub struct QemuConfig {
}

impl QemuConfig {
fn replace_strings(&mut self, tool: &Tool) -> anyhow::Result<()> {
self.args = self
.args
.iter()
.map(|arg| tool.replace_string(arg))
.collect::<anyhow::Result<Vec<_>>>()?;
self.success_regex = self
.success_regex
.iter()
.map(|arg| tool.replace_string(arg))
.collect::<anyhow::Result<Vec<_>>>()?;
self.fail_regex = self
.fail_regex
.iter()
.map(|arg| tool.replace_string(arg))
.collect::<anyhow::Result<Vec<_>>>()?;
self.shell_prefix = self
.shell_prefix
.as_deref()
.map(|value| tool.replace_string(value))
.transpose()?;
self.shell_init_cmd = self
.shell_init_cmd
.as_deref()
.map(|value| tool.replace_string(value))
.transpose()?;
Ok(())
}

fn normalize(&mut self, config_name: &str) -> anyhow::Result<()> {
normalize_shell_init_config(
&mut self.shell_prefix,
Expand Down Expand Up @@ -171,6 +200,9 @@ async fn load_or_create_qemu_config(
explicit_config_path: Option<PathBuf>,
overrides: QemuDefaultOverrides,
) -> anyhow::Result<QemuConfig> {
let explicit_config_path = explicit_config_path
.map(|path| tool.replace_path_variables(path))
.transpose()?;
let config_path = resolve_qemu_config_path(tool, explicit_config_path)?;

info!("Using QEMU config file: {}", config_path.display());
Expand All @@ -180,6 +212,7 @@ async fn load_or_create_qemu_config(
let mut config: QemuConfig = toml::from_str(&content).with_context(|| {
format!("failed to parse QEMU config: {}", config_path.display())
})?;
config.replace_strings(tool)?;
config.normalize(&format!("QEMU config {}", config_path.display()))?;
Comment on lines 212 to 216
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

config.replace_strings(tool)? is only called when a config file is successfully loaded. If the config file is missing and we build a default config from overrides (including success_regex/fail_regex), any ${...} placeholders in those override strings will remain unresolved and can break output matching. Consider also applying replace_strings in the NotFound/default-config branch before normalize()/writing.

Copilot uses AI. Check for mistakes.
return Ok(config);
}
Expand Down Expand Up @@ -720,11 +753,13 @@ mod tests {

use crate::{
Tool, ToolConfig,
build::config::{BuildConfig, BuildSystem, Cargo},
run::{
output_matcher::{ByteStreamMatcher, StreamMatchKind},
shell_init::ShellAutoInitMatcher,
},
};
use std::collections::HashMap;

fn write_single_crate_manifest(dir: &std::path::Path) {
std::fs::write(
Expand Down Expand Up @@ -931,6 +966,65 @@ timeout = 0
assert_eq!(result, manifest.join("qemu-aarch64.toml"));
}

#[test]
fn qemu_config_replaces_string_fields() {
let tmp = TempDir::new().unwrap();
write_single_crate_manifest(tmp.path());
let mut tool = make_tool(tmp.path());
tool.ctx.build_config = Some(BuildConfig {
system: BuildSystem::Cargo(Cargo {
env: HashMap::new(),
target: "aarch64-unknown-none".into(),
package: "sample".into(),
features: vec![],
log: None,
extra_config: None,
args: vec![],
pre_build_cmds: vec![],
post_build_cmds: vec![],
to_bin: false,
}),
});
unsafe {
std::env::set_var("OSTOOL_QEMU_TEST_ENV", "env-ok");
}

let mut config = QemuConfig {
args: vec!["${workspace}".into(), "${package}".into()],
success_regex: vec!["${env:OSTOOL_QEMU_TEST_ENV}".into()],
fail_regex: vec!["${workspaceFolder}".into()],
shell_prefix: Some("${workspace}".into()),
shell_init_cmd: Some("${package}".into()),
..Default::default()
};

config.replace_strings(&tool).unwrap();

let expected = tmp.path().display().to_string();
assert_eq!(config.args, vec![expected.clone(), expected.clone()]);
assert_eq!(config.success_regex, vec!["env-ok"]);
assert_eq!(config.fail_regex, vec![expected.clone()]);
assert_eq!(config.shell_prefix.as_deref(), Some(expected.as_str()));
assert_eq!(config.shell_init_cmd.as_deref(), Some(expected.as_str()));
}

#[test]
fn qemu_config_explicit_path_supports_variables() {
let tmp = TempDir::new().unwrap();
write_single_crate_manifest(tmp.path());
let tool = make_tool(tmp.path());

let result = resolve_qemu_config_path(
&tool,
Some(
tool.replace_path_variables("${workspace}/qemu.toml".into())
.unwrap(),
),
)
.unwrap();
assert_eq!(result, tmp.path().join("qemu.toml"));
}

#[test]
fn qemu_config_default_path_with_search_dir() {
let tmp = TempDir::new().unwrap();
Expand Down
203 changes: 195 additions & 8 deletions ostool/src/run/uboot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use crate::{
tftp,
},
sterm::SerialTerm,
utils::{PathResultExt, replace_env_placeholders},
utils::PathResultExt,
};

/// FIT image 生成相关的错误消息常量
Expand Down Expand Up @@ -73,6 +73,70 @@ pub struct UbootConfig {
}

impl UbootConfig {
fn replace_strings(&mut self, tool: &Tool) -> anyhow::Result<()> {
self.serial = tool.replace_string(&self.serial)?;
self.baud_rate = tool.replace_string(&self.baud_rate)?;
self.dtb_file = self
.dtb_file
.as_deref()
.map(|value| tool.replace_string(value))
.transpose()?;
self.kernel_load_addr = self
.kernel_load_addr
.as_deref()
.map(|value| tool.replace_string(value))
.transpose()?;
self.fit_load_addr = self
.fit_load_addr
.as_deref()
.map(|value| tool.replace_string(value))
.transpose()?;
self.board_reset_cmd = self
.board_reset_cmd
.as_deref()
.map(|value| tool.replace_string(value))
.transpose()?;
self.board_power_off_cmd = self
.board_power_off_cmd
.as_deref()
.map(|value| tool.replace_string(value))
.transpose()?;
self.success_regex = self
.success_regex
.iter()
.map(|value| tool.replace_string(value))
.collect::<anyhow::Result<Vec<_>>>()?;
self.fail_regex = self
.fail_regex
.iter()
.map(|value| tool.replace_string(value))
.collect::<anyhow::Result<Vec<_>>>()?;
self.uboot_cmd = self
.uboot_cmd
.as_ref()
.map(|values| {
values
.iter()
.map(|value| tool.replace_string(value))
.collect::<anyhow::Result<Vec<_>>>()
})
.transpose()?;
self.shell_prefix = self
.shell_prefix
.as_deref()
.map(|value| tool.replace_string(value))
.transpose()?;
self.shell_init_cmd = self
.shell_init_cmd
.as_deref()
.map(|value| tool.replace_string(value))
.transpose()?;
if let Some(net) = &mut self.net {
net.replace_strings(tool)?;
}
Ok(())
}

pub fn kernel_load_addr_int(&self) -> Option<u64> {
self.addr_int(self.kernel_load_addr.as_ref())
}
Expand Down Expand Up @@ -115,6 +179,33 @@ pub struct Net {
pub tftp_dir: Option<String>,
}

impl Net {
fn replace_strings(&mut self, tool: &Tool) -> anyhow::Result<()> {
self.interface = tool.replace_string(&self.interface)?;
self.board_ip = self
.board_ip
.as_deref()
.map(|value| tool.replace_string(value))
.transpose()?;
self.gatewayip = self
.gatewayip
.as_deref()
.map(|value| tool.replace_string(value))
.transpose()?;
self.netmask = self
.netmask
.as_deref()
.map(|value| tool.replace_string(value))
.transpose()?;
self.tftp_dir = self
.tftp_dir
.as_deref()
.map(|value| tool.replace_string(value))
.transpose()?;
Ok(())
}
}

#[derive(Debug, Clone)]
pub struct RunUbootArgs {
pub config: Option<PathBuf>,
Expand All @@ -124,18 +215,17 @@ pub struct RunUbootArgs {
impl Tool {
pub async fn run_uboot(&mut self, args: RunUbootArgs) -> anyhow::Result<()> {
let config_path = match args.config.clone() {
Some(path) => path,
Some(path) => self.replace_path_variables(path)?,
None => self.workspace_dir().join(".uboot.toml"),
};

let config = match fs::read_to_string(&config_path).await {
Ok(content) => {
println!("Using U-Boot config: {}", config_path.display());
let config_content = replace_env_placeholders(&content)?;
let mut config: UbootConfig =
toml::from_str(&config_content).with_context(|| {
format!("failed to parse U-Boot config: {}", config_path.display())
})?;
let mut config: UbootConfig = toml::from_str(&content).with_context(|| {
format!("failed to parse U-Boot config: {}", config_path.display())
})?;
config.replace_strings(self)?;
config.normalize(&format!("U-Boot config {}", config_path.display()))?;
config
}
Expand Down Expand Up @@ -718,7 +808,12 @@ fn build_network_boot_request(

#[cfg(test)]
mod tests {
use super::{UbootConfig, build_network_boot_request, timeout_duration};
use super::{Net, UbootConfig, build_network_boot_request, timeout_duration};
use crate::{
Tool, ToolConfig,
build::config::{BuildConfig, BuildSystem, Cargo},
};
use std::collections::HashMap;
use std::time::Duration;

#[test]
Expand Down Expand Up @@ -806,4 +901,96 @@ timeout = 0

assert_eq!(config.timeout, Some(0));
}

#[test]
fn uboot_config_replaces_string_fields() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("Cargo.toml"),
"[package]\nname = \"sample\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("src")).unwrap();
std::fs::write(tmp.path().join("src/lib.rs"), "").unwrap();

let mut tool = Tool::new(ToolConfig {
manifest: Some(tmp.path().to_path_buf()),
..Default::default()
})
.unwrap();
tool.ctx.build_config = Some(BuildConfig {
system: BuildSystem::Cargo(Cargo {
env: HashMap::new(),
target: "aarch64-unknown-none".into(),
package: "sample".into(),
features: vec![],
log: None,
extra_config: None,
args: vec![],
pre_build_cmds: vec![],
post_build_cmds: vec![],
to_bin: false,
}),
});
unsafe {
std::env::set_var("OSTOOL_UBOOT_TEST_ENV", "env-ok");
}

let mut config = UbootConfig {
serial: "${workspace}/tty".into(),
baud_rate: "${env:OSTOOL_UBOOT_TEST_ENV}".into(),
dtb_file: Some("${package}/board.dtb".into()),
kernel_load_addr: Some("${workspaceFolder}".into()),
fit_load_addr: Some("${package}".into()),
board_reset_cmd: Some("${workspace}".into()),
board_power_off_cmd: Some("${package}".into()),
success_regex: vec!["${workspace}".into()],
fail_regex: vec!["${package}".into()],
uboot_cmd: Some(vec!["setenv boot ${workspace}".into()]),
shell_prefix: Some("${workspace}".into()),
shell_init_cmd: Some("${package}".into()),
net: Some(Net {
interface: "${env:OSTOOL_UBOOT_TEST_ENV}".into(),
board_ip: Some("${workspace}".into()),
gatewayip: Some("${package}".into()),
netmask: Some("${workspaceFolder}".into()),
tftp_dir: Some("${package}/tftp".into()),
}),
..Default::default()
};

config.replace_strings(&tool).unwrap();

let expected = tmp.path().display().to_string();
assert_eq!(config.serial, format!("{expected}/tty"));
assert_eq!(config.baud_rate, "env-ok");
assert_eq!(
config.dtb_file.as_deref(),
Some(format!("{expected}/board.dtb").as_str())
);
assert_eq!(config.kernel_load_addr.as_deref(), Some(expected.as_str()));
assert_eq!(config.fit_load_addr.as_deref(), Some(expected.as_str()));
assert_eq!(config.board_reset_cmd.as_deref(), Some(expected.as_str()));
assert_eq!(
config.board_power_off_cmd.as_deref(),
Some(expected.as_str())
);
assert_eq!(config.success_regex, vec![expected.clone()]);
assert_eq!(config.fail_regex, vec![expected.clone()]);
assert_eq!(
config.uboot_cmd,
Some(vec![format!("setenv boot {expected}")])
);
assert_eq!(config.shell_prefix.as_deref(), Some(expected.as_str()));
assert_eq!(config.shell_init_cmd.as_deref(), Some(expected.as_str()));
let net = config.net.unwrap();
assert_eq!(net.interface, "env-ok");
assert_eq!(net.board_ip.as_deref(), Some(expected.as_str()));
assert_eq!(net.gatewayip.as_deref(), Some(expected.as_str()));
assert_eq!(net.netmask.as_deref(), Some(expected.as_str()));
assert_eq!(
net.tftp_dir.as_deref(),
Some(format!("{expected}/tftp").as_str())
);
}
}
Loading
Loading