diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fed0be0..361d1f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,8 +24,10 @@ jobs: targets: ${{ matrix.targets }} - uses: Swatinem/rust-cache@v2 - name: Install qemu - run: sudo apt install qemu-system-aarch64 -y - - run: sudo apt install u-boot-tools -y + run: | + sudo apt-get update + sudo apt install qemu-system-aarch64 -y + sudo apt install u-boot-tools -y - name: Install lib libudev-dev run: sudo apt install libudev-dev -y - name: Check rust version diff --git a/Cargo.lock b/Cargo.lock index 2961cd7..47b3618 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1859,7 +1859,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "ostool" -version = "0.8.3" +version = "0.8.4" dependencies = [ "anyhow", "byte-unit", diff --git a/jkconfig/tests/schema.rs b/jkconfig/tests/schema.rs index 1e63e6a..bbb2210 100644 --- a/jkconfig/tests/schema.rs +++ b/jkconfig/tests/schema.rs @@ -63,7 +63,7 @@ fn test_object() { #[test] fn test_value() { - env_logger::builder().is_test(true).init(); + let _ = env_logger::builder().is_test(true).try_init(); let schema = schema_for!(AnimalObject); diff --git a/ostool/Cargo.toml b/ostool/Cargo.toml index 239a864..f6bf18f 100644 --- a/ostool/Cargo.toml +++ b/ostool/Cargo.toml @@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0" name = "ostool" readme = "../README.md" repository = "https://github.com/ZR233/ostool" -version = "0.8.3" +version = "0.8.4" [[bin]] name = "ostool" diff --git a/ostool/src/bin/cargo-osrun.rs b/ostool/src/bin/cargo-osrun.rs index af21ed3..b21de6e 100644 --- a/ostool/src/bin/cargo-osrun.rs +++ b/ostool/src/bin/cargo-osrun.rs @@ -3,7 +3,7 @@ use std::{env, path::PathBuf, process::exit}; use clap::{Parser, Subcommand}; use log::{LevelFilter, debug}; use ostool::{ - ctx::AppContext, + ctx::{AppContext, OutputConfig, PathConfig}, run::{ qemu, uboot::{self, RunUbootArgs}, @@ -53,6 +53,12 @@ struct RunnerArgs { #[arg(allow_hyphen_values = true)] /// Arguments to be run runner_args: Vec, + + #[arg(long)] + build_dir: Option, + + #[arg(long)] + bin_dir: Option, } #[derive(Debug, Subcommand, Clone)] @@ -90,9 +96,18 @@ async fn main() -> anyhow::Result<()> { Err(_) => manifest_dir.clone(), }; + let bin_dir: Option = args.bin_dir.map(PathBuf::from); + let build_dir: Option = args.build_dir.map(PathBuf::from); + + let output_config = OutputConfig { build_dir, bin_dir }; + let mut app = AppContext { - workspace_folder, - manifest_dir, + paths: PathConfig { + workspace: workspace_folder, + manifest: manifest_dir, + config: output_config, + ..Default::default() + }, ..Default::default() }; diff --git a/ostool/src/build/cargo_builder.rs b/ostool/src/build/cargo_builder.rs new file mode 100644 index 0000000..108ca8b --- /dev/null +++ b/ostool/src/build/cargo_builder.rs @@ -0,0 +1,345 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use colored::Colorize; + +use crate::{build::config::Cargo, ctx::AppContext, utils::Command}; + +pub struct CargoBuilder<'a> { + ctx: &'a mut AppContext, + config: &'a Cargo, + command: String, + extra_args: Vec, + extra_envs: HashMap, + skip_objcopy: bool, + config_path: Option, +} + +impl<'a> CargoBuilder<'a> { + pub fn build(ctx: &'a mut AppContext, config: &'a Cargo, config_path: Option) -> Self { + Self { + ctx, + config, + command: "build".to_string(), + extra_args: Vec::new(), + extra_envs: HashMap::new(), + skip_objcopy: false, + config_path, + } + } + + pub fn run(ctx: &'a mut AppContext, config: &'a Cargo, config_path: Option) -> Self { + Self { + ctx, + config, + command: "run".to_string(), + extra_args: Vec::new(), + extra_envs: HashMap::new(), + skip_objcopy: true, + config_path, + } + } + + pub fn debug(self, debug: bool) -> Self { + self.ctx.debug = debug; + self + } + + pub fn build_auto(ctx: &'a mut AppContext, config: &'a Cargo) -> Self { + let config_path = ctx.build_config_path.clone(); + Self::build(ctx, config, config_path) + } + + pub fn run_auto(ctx: &'a mut AppContext, config: &'a Cargo) -> Self { + let config_path = ctx.build_config_path.clone(); + Self::run(ctx, config, config_path) + } + + pub fn arg(mut self, arg: impl Into) -> Self { + self.extra_args.push(arg.into()); + self + } + + pub fn args(mut self, args: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.extra_args.extend(args.into_iter().map(|s| s.into())); + self + } + + pub fn env(mut self, key: impl Into, value: impl Into) -> Self { + self.extra_envs.insert(key.into(), value.into()); + self + } + + pub fn skip_objcopy(mut self, skip: bool) -> Self { + self.skip_objcopy = skip; + self + } + + pub async fn execute(mut self) -> anyhow::Result<()> { + // 1. Pre-build commands + self.run_pre_build_cmds()?; + + // 2. Build and run cargo + self.run_cargo().await?; + + // 3. Handle output + self.handle_output().await?; + + // 4. Post-build commands + self.run_post_build_cmds()?; + + Ok(()) + } + + fn run_pre_build_cmds(&mut self) -> anyhow::Result<()> { + for cmd in &self.config.pre_build_cmds { + self.ctx.shell_run_cmd(cmd)?; + } + Ok(()) + } + + async fn run_cargo(&mut self) -> anyhow::Result<()> { + let mut cmd = self.build_cargo_command().await?; + cmd.run()?; + Ok(()) + } + + async fn build_cargo_command(&mut self) -> anyhow::Result { + let mut cmd = self.ctx.command("cargo"); + + cmd.arg(&self.command); + + for (k, v) in &self.config.env { + println!("{}", format!("{k}={v}").cyan()); + cmd.env(k, v); + } + for (k, v) in &self.extra_envs { + println!("{}", format!("{k}={v}").cyan()); + cmd.env(k, v); + } + + // Extra config + if let Some(extra_config_path) = self.cargo_extra_config().await? { + cmd.arg("--config"); + cmd.arg(extra_config_path.display().to_string()); + } + + // Package and target + cmd.arg("-p"); + cmd.arg(&self.config.package); + cmd.arg("--target"); + cmd.arg(&self.config.target); + cmd.arg("-Z"); + cmd.arg("unstable-options"); + + if let Some(build_dir) = &self.ctx.paths.config.build_dir { + cmd.arg("--target-dir"); + cmd.arg(build_dir.display().to_string()); + } + + // Features + let features = self.build_features(); + if !features.is_empty() { + cmd.arg("--features"); + cmd.arg(features.join(",")); + } + + // Config args + for arg in &self.config.args { + cmd.arg(arg); + } + + // Release mode + if !self.ctx.debug { + cmd.arg("--release"); + } + + // Extra args + for arg in &self.extra_args { + cmd.arg(arg); + } + + Ok(cmd) + } + + async fn handle_output(&mut self) -> anyhow::Result<()> { + let target_dir = self.ctx.paths.build_dir(); + + let elf_path = target_dir + .join(&self.config.target) + .join(if self.ctx.debug { "debug" } else { "release" }) + .join(&self.config.package); + + self.ctx.set_elf_path(elf_path).await; + + if self.config.to_bin && !self.skip_objcopy { + self.ctx.objcopy_output_bin()?; + } + + Ok(()) + } + + fn run_post_build_cmds(&mut self) -> anyhow::Result<()> { + for cmd in &self.config.post_build_cmds { + self.ctx.shell_run_cmd(cmd)?; + } + Ok(()) + } + + fn build_features(&self) -> Vec { + let mut features = self.config.features.clone(); + if let Some(log_level) = self.log_level_feature() { + features.push(log_level); + } + features + } + + fn log_level_feature(&self) -> Option { + let level = self.config.log.clone()?; + + let meta = self.ctx.metadata().ok()?; + let pkg = meta + .packages + .iter() + .find(|p| p.name == self.config.package)?; + + let has_log = pkg.dependencies.iter().any(|dep| dep.name == "log"); + + if has_log { + Some(format!( + "log/{}max_level_{}", + if self.ctx.debug { "" } else { "release_" }, + format!("{:?}", level).to_lowercase() + )) + } else { + None + } + } + + async fn cargo_extra_config(&self) -> anyhow::Result> { + let s = match self.config.extra_config.as_ref() { + Some(s) => s, + None => return Ok(None), + }; + + // Check if it's a URL (starts with http:// or https://) + if s.starts_with("http://") || s.starts_with("https://") { + // Convert GitHub URL to raw content URL if needed + let download_url = Self::convert_to_raw_url(s); + + // Download to temp directory + match self.download_config_to_temp(&download_url).await { + Ok(path) => Ok(Some(path)), + Err(e) => { + eprintln!("Failed to download config from {}: {}", s, e); + Err(e) + } + } + } else { + // It's a local path + let extra = Path::new(s); + + if extra.is_relative() { + if let Some(ref config_path) = self.config_path { + let combined = config_path + .parent() + .ok_or_else(|| anyhow::anyhow!("Invalid config path"))? + .join(extra); + Ok(Some(combined)) + } else { + Ok(Some(extra.to_path_buf())) + } + } else { + Ok(Some(extra.to_path_buf())) + } + } + } + + /// Convert GitHub URL to raw content URL + /// Supports: + /// - https://github.com/user/repo/blob/branch/path/file -> https://raw.githubusercontent.com/user/repo/branch/path/file + /// - https://raw.githubusercontent.com/... (already raw, no change) + /// - Other URLs: no change + fn convert_to_raw_url(url: &str) -> String { + // Already a raw URL + if url.contains("raw.githubusercontent.com") || url.contains("raw.github.com") { + return url.to_string(); + } + + // Convert github.com/user/repo/blob/... to raw.githubusercontent.com/user/repo/... + if url.contains("github.com") && url.contains("/blob/") { + let converted = url + .replace("github.com", "raw.githubusercontent.com") + .replace("/blob/", "/"); + println!("Converting GitHub URL to raw: {} -> {}", url, converted); + return converted; + } + + // Not a GitHub URL or already in correct format + url.to_string() + } + + async fn download_config_to_temp(&self, url: &str) -> anyhow::Result { + use std::time::SystemTime; + + println!("Downloading cargo config from: {}", url); + + // Get system temp directory + let temp_dir = std::env::temp_dir(); + + // Generate filename with timestamp + let timestamp = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + // Extract filename from URL or use default + let url_path = url.split('/').next_back().unwrap_or("config.toml"); + let filename = format!("cargo_config_{}_{}", timestamp, url_path); + let target_path = temp_dir.join(filename); + + // Create reqwest client + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {}", e))?; + + // Build request with User-Agent for GitHub + let mut request = client.get(url); + + if url.contains("github.com") || url.contains("githubusercontent.com") { + // GitHub requires User-Agent + request = request.header("User-Agent", "ostool-cargo-downloader"); + } + + // Download the file + let response = request + .send() + .await + .map_err(|e| anyhow::anyhow!("Failed to download from {}: {}", url, e))?; + + if !response.status().is_success() { + return Err(anyhow::anyhow!("HTTP error {}: {}", response.status(), url)); + } + + let content = response + .bytes() + .await + .map_err(|e| anyhow::anyhow!("Failed to read response body: {}", e))?; + + // Write to temp file + tokio::fs::write(&target_path, content) + .await + .map_err(|e| anyhow::anyhow!("Failed to write to temp file: {}", e))?; + + println!("Config downloaded to: {}", target_path.display()); + + Ok(target_path) + } +} diff --git a/ostool/src/build/mod.rs b/ostool/src/build/mod.rs index e846805..df783de 100644 --- a/ostool/src/build/mod.rs +++ b/ostool/src/build/mod.rs @@ -1,226 +1,140 @@ use std::path::PathBuf; -use colored::Colorize; +use anyhow::Context; use crate::{ - build::config::{Cargo, Custom}, + build::{ + cargo_builder::CargoBuilder, + config::{Cargo, Custom}, + }, ctx::AppContext, }; +pub mod cargo_builder; pub mod config; +pub enum CargoRunnerKind { + Qemu { + qemu_config: Option, + debug: bool, + dtb_dump: bool, + }, + Uboot { + uboot_config: Option, + }, +} + impl AppContext { pub async fn build_with_config(&mut self, config: &config::BuildConfig) -> anyhow::Result<()> { match &config.system { - config::BuildSystem::Custom(custom) => self.build_custrom(custom)?, + config::BuildSystem::Custom(custom) => self.build_custom(custom)?, config::BuildSystem::Cargo(cargo) => { - self.build_cargo(cargo).await?; + self.cargo_build(cargo).await?; } } Ok(()) } pub async fn build(&mut self, config_path: Option) -> anyhow::Result<()> { - let build_config = self.perpare_build_config(config_path, false).await?; + let build_config = self.prepare_build_config(config_path, false).await?; println!("Build configuration: {:?}", build_config); self.build_with_config(&build_config).await } - pub fn build_custrom(&mut self, config: &Custom) -> anyhow::Result<()> { + pub fn build_custom(&mut self, config: &Custom) -> anyhow::Result<()> { self.shell_run_cmd(&config.build_cmd)?; Ok(()) } - pub async fn build_cargo(&mut self, config: &Cargo) -> anyhow::Result<()> { - for cmd in &config.pre_build_cmds { - self.shell_run_cmd(cmd)?; - } + pub async fn cargo_build(&mut self, config: &Cargo) -> anyhow::Result<()> { + cargo_builder::CargoBuilder::build_auto(self, config) + .execute() + .await + } - let mut features = config.features.clone(); - if let Some(log_level) = &self.log_level_feature(config) { - features.push(log_level.to_string()); - } + pub async fn cargo_run( + &mut self, + config: &Cargo, + runner: &CargoRunnerKind, + ) -> anyhow::Result<()> { + let build_config_path = self.build_config_path.clone(); + + let normalize = |dir: &PathBuf| -> anyhow::Result { + let bin_path = if dir.is_relative() { + self.paths.manifest.join(dir) + } else { + dir.clone() + }; + + bin_path + .canonicalize() + .or_else(|_| { + if let Some(parent) = bin_path.parent() { + parent + .canonicalize() + .map(|p| p.join(bin_path.file_name().unwrap())) + } else { + Ok(bin_path.clone()) + } + }) + .context("Failed to normalize path") + }; - let mut cmd = self.command("cargo"); - cmd.arg("build"); + let build_dir = self + .paths + .config + .build_dir + .as_ref() + .map(|d| normalize(d)) + .transpose()?; - for (k, v) in &config.env { - cmd.env(k, v); - } + let bin_dir = self + .paths + .config + .bin_dir + .as_ref() + .map(|d| normalize(d)) + .transpose()?; - if let Some(extra_config_path) = self.cargo_extra_config(config).await? { - cmd.arg("--config"); - cmd.arg(extra_config_path); - } + let mut builder = CargoBuilder::run(self, config, build_config_path); - cmd.arg("-p"); - cmd.arg(&config.package); - cmd.arg("--target"); - cmd.arg(&config.target); - cmd.arg("-Z"); - cmd.arg("unstable-options"); - if !features.is_empty() { - cmd.arg("--features"); - cmd.arg(features.join(",")); - } - for arg in &config.args { - cmd.arg(arg); - } - if !self.debug { - cmd.arg("--release"); - } - for (k, v) in cmd.get_envs() { - println!("{}", format!("{k:?}={v:?}").cyan()); - } - cmd.run()?; + builder = builder.arg("--"); - let elf_path = self - .manifest_dir - .join("target") - .join(&config.target) - .join(if self.debug { "debug" } else { "release" }) - .join(&config.package); - - self.set_elf_path(elf_path.clone()).await; - - if config.to_bin { - self.objcopy_output_bin()?; + if let Some(build_dir) = build_dir { + builder = builder + .arg("--build-dir") + .arg(build_dir.display().to_string()) } - for cmd in &config.post_build_cmds { - self.shell_run_cmd(cmd)?; + if let Some(bin_dir) = bin_dir { + builder = builder.arg("--bin-dir").arg(bin_dir.display().to_string()) } - Ok(()) - } + match runner { + CargoRunnerKind::Qemu { + qemu_config, + debug, + dtb_dump, + } => { + if let Some(cfg) = qemu_config { + builder = builder.arg("--config").arg(cfg.display().to_string()); + } - fn log_level_feature(&self, config: &Cargo) -> Option { - let level = config.log.clone()?; + builder = builder.debug(*debug); - let meta = self.metadata().ok()?; - let pkg = meta.packages.iter().find(|p| p.name == config.package)?; - let mut has_log = false; - for dep in &pkg.dependencies { - if dep.name == "log" { - has_log = true; - break; + if *dtb_dump { + builder = builder.arg("--dtb-dump"); + } + builder = builder.arg("qemu"); } - } - if has_log { - Some(format!( - "log/{}max_level_{}", - if self.debug { "" } else { "release_" }, - format!("{:?}", level).to_lowercase() - )) - } else { - None - } - } - - async fn cargo_extra_config(&self, config: &Cargo) -> anyhow::Result> { - let s = match config.extra_config.as_ref() { - Some(s) => s, - None => return Ok(None), - }; - - // Check if it's a URL (starts with http:// or https://) - if s.starts_with("http://") || s.starts_with("https://") { - // Convert GitHub URL to raw content URL if needed - let download_url = Self::convert_to_raw_url(s); - - // Download to temp directory - match self.download_config_to_temp(&download_url).await { - Ok(path) => Ok(Some(path)), - Err(e) => { - eprintln!("Failed to download config from {}: {}", s, e); - Err(e) + CargoRunnerKind::Uboot { uboot_config } => { + if let Some(cfg) = uboot_config { + builder = builder.arg("--config").arg(cfg.display().to_string()); } + builder = builder.arg("uboot"); } - } else { - // It's a local path, return as is - Ok(Some(s.clone())) } - } - - /// Convert GitHub URL to raw content URL - /// Supports: - /// - https://github.com/user/repo/blob/branch/path/file -> https://raw.githubusercontent.com/user/repo/branch/path/file - /// - https://raw.githubusercontent.com/... (already raw, no change) - /// - Other URLs: no change - fn convert_to_raw_url(url: &str) -> String { - // Already a raw URL - if url.contains("raw.githubusercontent.com") || url.contains("raw.github.com") { - return url.to_string(); - } - - // Convert github.com/user/repo/blob/... to raw.githubusercontent.com/user/repo/... - if url.contains("github.com") && url.contains("/blob/") { - let converted = url - .replace("github.com", "raw.githubusercontent.com") - .replace("/blob/", "/"); - println!("Converting GitHub URL to raw: {} -> {}", url, converted); - return converted; - } - - // Not a GitHub URL or already in correct format - url.to_string() - } - - async fn download_config_to_temp(&self, url: &str) -> anyhow::Result { - use std::time::SystemTime; - - println!("Downloading cargo config from: {}", url); - - // Get system temp directory - let temp_dir = std::env::temp_dir(); - - // Generate filename with timestamp - let timestamp = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs(); - - // Extract filename from URL or use default - let url_path = url.split('/').next_back().unwrap_or("config.toml"); - let filename = format!("cargo_config_{}_{}", timestamp, url_path); - let target_path = temp_dir.join(filename); - - // Create reqwest client - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(30)) - .build() - .map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?; - - // Build request with User-Agent for GitHub - let mut request = client.get(url); - - if url.contains("github.com") || url.contains("githubusercontent.com") { - // GitHub requires User-Agent - request = request.header("User-Agent", "ostool-cargo-downloader"); - } - - // Download the file - let response = request - .send() - .await - .map_err(|e| anyhow!("Failed to download from {}: {}", url, e))?; - - if !response.status().is_success() { - return Err(anyhow!("HTTP error {}: {}", response.status(), url)); - } - - let content = response - .bytes() - .await - .map_err(|e| anyhow!("Failed to read response body: {}", e))?; - - // Write to temp file - tokio::fs::write(&target_path, content) - .await - .map_err(|e| anyhow!("Failed to write to temp file: {}", e))?; - - println!("Config downloaded to: {}", target_path.display()); - Ok(target_path.to_string_lossy().to_string()) + builder.execute().await } } diff --git a/ostool/src/ctx.rs b/ostool/src/ctx.rs index b9fb528..e427dd7 100644 --- a/ostool/src/ctx.rs +++ b/ostool/src/ctx.rs @@ -15,13 +15,48 @@ use tokio::fs; use crate::build::config::BuildConfig; +/// Configuration for output directories (set from external config) +#[derive(Default, Clone)] +pub struct OutputConfig { + pub build_dir: Option, + pub bin_dir: Option, +} + +/// Build artifacts (generated during build) +#[derive(Default, Clone)] +pub struct OutputArtifacts { + pub elf: Option, + pub bin: Option, +} + +/// Path configuration grouping all path-related fields +#[derive(Default, Clone)] +pub struct PathConfig { + pub workspace: PathBuf, + pub manifest: PathBuf, + pub config: OutputConfig, + pub artifacts: OutputArtifacts, +} + +impl PathConfig { + /// Get build directory, defaulting to manifest/target if not configured + pub fn build_dir(&self) -> PathBuf { + self.config + .build_dir + .clone() + .unwrap_or_else(|| self.manifest.join("target")) + } + + /// Get bin directory, defaulting to build_dir if not configured + pub fn bin_dir(&self) -> Option { + self.config.bin_dir.clone() + } +} + #[derive(Default, Clone)] pub struct AppContext { - pub workspace_folder: PathBuf, - pub manifest_dir: PathBuf, + pub paths: PathConfig, pub debug: bool, - pub elf_path: Option, - pub bin_path: Option, pub arch: Option, pub build_config: Option, pub build_config_path: Option, @@ -33,7 +68,7 @@ impl AppContext { command.arg("-c"); command.arg(cmd); - if let Some(elf) = &self.elf_path { + if let Some(elf) = &self.paths.artifacts.elf { command.env("KERNEL_ELF", elf.display().to_string()); } @@ -44,21 +79,21 @@ impl AppContext { pub fn command(&self, program: &str) -> crate::utils::Command { let this = self.clone(); - crate::utils::Command::new(program, &self.manifest_dir, move |s| { + crate::utils::Command::new(program, &self.paths.manifest, move |s| { this.value_replace_with_var(s) }) } pub fn metadata(&self) -> anyhow::Result { let res = cargo_metadata::MetadataCommand::new() - .current_dir(&self.manifest_dir) + .current_dir(&self.paths.manifest) .no_deps() .exec()?; Ok(res) } pub async fn set_elf_path(&mut self, path: PathBuf) { - self.elf_path = Some(path.clone()); + self.paths.artifacts.elf = Some(path.clone()); let binary_data = match fs::read(path).await { Ok(data) => data, Err(e) => { @@ -78,7 +113,9 @@ impl AppContext { pub fn objcopy_elf(&mut self) -> anyhow::Result { let elf_path = self - .elf_path + .paths + .artifacts + .elf .as_ref() .ok_or(anyhow!("elf not exist"))? .canonicalize()?; @@ -112,32 +149,42 @@ impl AppContext { objcopy.arg(&stripped_elf_path); objcopy.run()?; - self.elf_path = Some(stripped_elf_path.clone()); + self.paths.artifacts.elf = Some(stripped_elf_path.clone()); Ok(stripped_elf_path) } pub fn objcopy_output_bin(&mut self) -> anyhow::Result { - if self.bin_path.is_some() { - debug!("BIN file already exists: {:?}", self.bin_path); - return Ok(self.bin_path.as_ref().unwrap().clone()); + if self.paths.artifacts.bin.is_some() { + debug!("BIN file already exists: {:?}", self.paths.artifacts.bin); + return Ok(self.paths.artifacts.bin.as_ref().unwrap().clone()); } let elf_path = self - .elf_path + .paths + .artifacts + .elf .as_ref() .ok_or(anyhow!("elf not exist"))? .canonicalize()?; - // 去掉原文件的扩展名后添加 .bin - let bin_path = elf_path.with_file_name( - elf_path - .file_stem() - .ok_or(anyhow!("Invalid file path"))? - .to_string_lossy() - .to_string() - + ".bin", - ); + let bin_name = elf_path + .file_stem() + .ok_or(anyhow!("Invalid file path"))? + .to_string_lossy() + .to_string() + + ".bin"; + + let bin_path = if let Some(bin_dir) = self.paths.config.bin_dir.clone() { + bin_dir.join(bin_name) + } else { + elf_path.with_file_name(bin_name) + }; + + if let Some(parent) = bin_path.parent() { + std::fs::create_dir_all(parent)?; + } + println!( "{}", format!( @@ -162,85 +209,19 @@ impl AppContext { .arg(&bin_path); objcopy.run()?; - self.bin_path = Some(bin_path.clone()); + self.paths.artifacts.bin = Some(bin_path.clone()); Ok(bin_path) } - // pub fn objcopy_output_bin(&mut self) -> anyhow::Result { - // let elf_path = self.elf_path.as_ref().ok_or(anyhow!("elf not exist"))?; - // let bin_path = elf_path.with_extension("bin"); - // println!( - // "{}", - // format!( - // "Converting ELF to BIN format...\r\n elf: {}\r\n bin: {}", - // elf_path.display(), - // bin_path.display() - // ) - // .bold() - // .purple() - // ); - - // // Read ELF file - // let binary_data = - // std::fs::read(elf_path).map_err(|e| anyhow!("Failed to read ELF file: {}", e))?; - - // // Parse ELF file - // let obj_file = object::File::parse(binary_data.as_slice()) - // .map_err(|e| anyhow!("Failed to parse ELF file: {}", e))?; - - // // Extract loadable segments and write to binary file - // let mut binary_output = Vec::new(); - // let mut min_addr = u64::MAX; - // let mut max_addr = 0u64; - - // // First pass: find memory range - // for segment in obj_file.segments() { - // // Only include loadable segments - // if segment.size() > 0 { - // let addr = segment.address(); - // min_addr = min_addr.min(addr); - // max_addr = max_addr.max(addr + segment.size()); - // } - // } - - // if min_addr == u64::MAX { - // return Err(anyhow!("No loadable segments found in ELF file")); - // } - - // // Allocate buffer for binary output - // let total_size = (max_addr - min_addr) as usize; - // binary_output.resize(total_size, 0u8); - - // // Second pass: copy segment data - // for segment in obj_file.segments() { - // if let Ok(data) = segment.data() - // && !data.is_empty() - // { - // let addr = segment.address(); - // let offset = (addr - min_addr) as usize; - // if offset + data.len() <= binary_output.len() { - // binary_output[offset..offset + data.len()].copy_from_slice(data); - // } - // } - // } - - // // Write binary file - // std::fs::write(&bin_path, binary_output) - // .map_err(|e| anyhow!("Failed to write binary file: {}", e))?; - - // self.bin_path = Some(bin_path.clone()); - // Ok(bin_path) - // } - - pub async fn perpare_build_config( + pub async fn prepare_build_config( &mut self, config_path: Option, menu: bool, ) -> anyhow::Result { let config_path = match config_path { Some(path) => path, - None => self.workspace_folder.join(".build.toml"), + None => self.paths.workspace.join(".build.toml"), }; self.build_config_path = Some(config_path.clone()); @@ -258,13 +239,6 @@ impl AppContext { Ok(c) } - pub fn is_cargo_build(&self) -> bool { - match &self.build_config { - Some(cfg) => matches!(cfg.system, crate::build::config::BuildSystem::Cargo(_)), - None => false, - } - } - pub fn value_replace_with_var(&self, value: S) -> String where S: AsRef, @@ -272,7 +246,7 @@ impl AppContext { let raw = value.as_ref().to_string_lossy(); raw.replace( "${workspaceFolder}", - format!("{}", self.workspace_folder.display()).as_ref(), + format!("{}", self.paths.workspace.display()).as_ref(), ) } @@ -282,7 +256,7 @@ impl AppContext { fn ui_hock_feature_select(&self) -> ElemHock { let path = "system.features"; - let cargo_toml = self.workspace_folder.join("Cargo.toml"); + let cargo_toml = self.paths.workspace.join("Cargo.toml"); ElemHock { path: path.to_string(), callback: Arc::new(move |siv: &mut Cursive, _path: &str| { @@ -303,7 +277,7 @@ impl AppContext { fn ui_hock_pacage_select(&self) -> ElemHock { let path = "system.package"; - let cargo_toml = self.workspace_folder.join("Cargo.toml"); + let cargo_toml = self.paths.workspace.join("Cargo.toml"); ElemHock { path: path.to_string(), diff --git a/ostool/src/main.rs b/ostool/src/main.rs index 48901d1..945de00 100644 --- a/ostool/src/main.rs +++ b/ostool/src/main.rs @@ -5,10 +5,10 @@ use clap::*; use log::info; use ostool::{ - build, + build::{self, CargoRunnerKind}, ctx::AppContext, menuconfig::{MenuConfigHandler, MenuConfigMode}, - run::{cargo::CargoRunnerKind, qemu::RunQemuArgs, uboot::RunUbootArgs}, + run::{qemu::RunQemuArgs, uboot::RunUbootArgs}, }; #[derive(Parser)] @@ -89,8 +89,11 @@ async fn main() -> Result<()> { }; let mut ctx = AppContext { - manifest_dir: workspace_folder.clone(), - workspace_folder, + paths: ostool::ctx::PathConfig { + workspace: workspace_folder.clone(), + manifest: workspace_folder.clone(), + ..Default::default() + }, ..Default::default() }; @@ -99,7 +102,7 @@ async fn main() -> Result<()> { ctx.build(config).await?; } SubCommands::Run(args) => { - let config = ctx.perpare_build_config(args.config, false).await?; + let config = ctx.prepare_build_config(args.config, false).await?; match config.system { build::config::BuildSystem::Cargo(config) => { let kind = match args.command { @@ -120,7 +123,7 @@ async fn main() -> Result<()> { info!( "ELF {:?}: {}", ctx.arch, - ctx.elf_path.as_ref().unwrap().display() + ctx.paths.artifacts.elf.as_ref().unwrap().display() ); if custom_cfg.to_bin { diff --git a/ostool/src/menuconfig.rs b/ostool/src/menuconfig.rs index 1ca1242..5de237c 100644 --- a/ostool/src/menuconfig.rs +++ b/ostool/src/menuconfig.rs @@ -36,14 +36,14 @@ impl MenuConfigHandler { } async fn handle_default_config(ctx: &mut AppContext) -> Result<()> { - ctx.perpare_build_config(None, true).await?; + ctx.prepare_build_config(None, true).await?; Ok(()) } async fn handle_qemu_config(ctx: &mut AppContext) -> Result<()> { info!("配置 QEMU 运行参数"); - let config_path = ctx.workspace_folder.join(".qemu.toml"); + let config_path = ctx.paths.workspace.join(".qemu.toml"); if config_path.exists() { println!("\n当前 U-Boot 配置文件: {}", config_path.display()); // 这里可以读取并显示当前的 U-Boot 配置 @@ -55,7 +55,7 @@ impl MenuConfigHandler { if let Some(c) = config { fs::write( - ctx.value_replace_with_var(ctx.workspace_folder.join(".qemu.toml")), + ctx.value_replace_with_var(ctx.paths.workspace.join(".qemu.toml")), toml::to_string_pretty(&c)?, ) .await?; @@ -73,7 +73,7 @@ impl MenuConfigHandler { println!("=== U-Boot 配置模式 ==="); // 检查是否存在 U-Boot 配置文件 - let uboot_config_path = ctx.workspace_folder.join(".uboot.toml"); + let uboot_config_path = ctx.paths.workspace.join(".uboot.toml"); if uboot_config_path.exists() { println!("\n当前 U-Boot 配置文件: {}", uboot_config_path.display()); // 这里可以读取并显示当前的 U-Boot 配置 @@ -83,7 +83,7 @@ impl MenuConfigHandler { let config = jkconfig::run::(uboot_config_path, true, &[]).await?; if let Some(c) = config { fs::write( - ctx.value_replace_with_var(ctx.workspace_folder.join(".uboot.toml")), + ctx.value_replace_with_var(ctx.paths.workspace.join(".uboot.toml")), toml::to_string_pretty(&c)?, ) .await?; diff --git a/ostool/src/run/cargo.rs b/ostool/src/run/cargo.rs deleted file mode 100644 index be5a377..0000000 --- a/ostool/src/run/cargo.rs +++ /dev/null @@ -1,300 +0,0 @@ -use std::{ - collections::HashMap, - path::{Path, PathBuf}, -}; - -use crossterm::style::Stylize; - -use crate::{build::config::Cargo, ctx::AppContext}; - -pub enum CargoRunnerKind { - Qemu { - qemu_config: Option, - debug: bool, - dtb_dump: bool, - }, - Uboot { - uboot_config: Option, - }, -} - -impl AppContext { - pub async fn cargo_run( - &mut self, - config: &Cargo, - runner: &CargoRunnerKind, - ) -> anyhow::Result<()> { - let build_config_path = self.build_config_path.clone().unwrap(); - - let mut cargo = CargoRunner::new("run", true, &build_config_path); - for (k, v) in &config.env { - cargo.env(k, v); - } - - cargo.arg("--"); - - if config.to_bin { - cargo.arg("--to-bin"); - } - - match runner { - CargoRunnerKind::Qemu { - qemu_config, - debug, - dtb_dump, - } => { - if let Some(cfg) = qemu_config { - cargo.arg("--config"); - cargo.arg(cfg.display().to_string()); - } - self.debug = *debug; - - if *dtb_dump { - cargo.arg("--dtb-dump"); - } - cargo.arg("qemu"); - } - CargoRunnerKind::Uboot { uboot_config } => { - if let Some(cfg) = uboot_config { - cargo.arg("--config"); - cargo.arg(cfg.display().to_string()); - } - cargo.arg("uboot"); - } - } - cargo.run(self, config).await - } -} - -pub struct CargoRunner { - pub cmd: String, - pub args: Vec, - - skip_objcopy: bool, - - envs: HashMap, - - config_path: PathBuf, -} - -impl CargoRunner { - pub fn new(cmd: &str, skip_objcopy: bool, build_config_path: &Path) -> Self { - Self { - cmd: cmd.to_string(), - args: vec![], - skip_objcopy, - envs: HashMap::new(), - config_path: build_config_path.to_path_buf(), - } - } - - pub fn arg(&mut self, arg: impl Into) { - self.args.push(arg.into()); - } - - pub fn env(&mut self, key: impl Into, value: impl Into) { - self.envs.insert(key.into(), value.into()); - } - - pub async fn run(&mut self, ctx: &mut AppContext, config: &Cargo) -> anyhow::Result<()> { - for cmd in &config.pre_build_cmds { - ctx.shell_run_cmd(cmd)?; - } - - let mut features = config.features.clone(); - if let Some(log_level) = &self.log_level_feature(ctx, config) { - features.push(log_level.to_string()); - } - - let mut cmd = ctx.command("cargo"); - for (k, v) in &self.envs { - println!("{}", format!("{k}={v}").cyan()); - cmd.env(k, v); - } - - cmd.arg(&self.cmd); - - if let Some(extra_config_path) = self.cargo_extra_config(config).await? { - cmd.arg("--config"); - cmd.arg(extra_config_path); - } - - cmd.arg("-p"); - cmd.arg(&config.package); - cmd.arg("--target"); - cmd.arg(&config.target); - cmd.arg("-Z"); - cmd.arg("unstable-options"); - if !features.is_empty() { - cmd.arg("--features"); - cmd.arg(features.join(",")); - } - for arg in &config.args { - cmd.arg(arg); - } - if !ctx.debug { - cmd.arg("--release"); - } - - for arg in &self.args { - cmd.arg(arg); - } - - cmd.run()?; - - let elf_path = ctx - .manifest_dir - .join("target") - .join(&config.target) - .join(if ctx.debug { "debug" } else { "release" }) - .join(&config.package); - - ctx.set_elf_path(elf_path.clone()).await; - - if config.to_bin && !self.skip_objcopy { - ctx.objcopy_output_bin()?; - } - - for cmd in &config.post_build_cmds { - ctx.shell_run_cmd(cmd)?; - } - - Ok(()) - } - - fn log_level_feature(&self, ctx: &AppContext, config: &Cargo) -> Option { - let level = config.log.clone()?; - - let meta = ctx.metadata().ok()?; - let pkg = meta.packages.iter().find(|p| p.name == config.package)?; - let mut has_log = false; - for dep in &pkg.dependencies { - if dep.name == "log" { - has_log = true; - break; - } - } - if has_log { - Some(format!( - "log/{}max_level_{}", - if ctx.debug { "" } else { "release_" }, - format!("{:?}", level).to_lowercase() - )) - } else { - None - } - } - - async fn cargo_extra_config(&self, config: &Cargo) -> anyhow::Result> { - let s = match config.extra_config.as_ref() { - Some(s) => s, - None => return Ok(None), - }; - - // Check if it's a URL (starts with http:// or https://) - if s.starts_with("http://") || s.starts_with("https://") { - // Convert GitHub URL to raw content URL if needed - let download_url = Self::convert_to_raw_url(s); - - // Download to temp directory - match self.download_config_to_temp(&download_url).await { - Ok(path) => Ok(Some(path)), - Err(e) => { - eprintln!("Failed to download config from {}: {}", s, e); - Err(e) - } - } - } else { - // It's a local path, return as is - let extra = Path::new(s); - if extra.is_relative() { - let combined = self.config_path.parent().unwrap().join(extra); - Ok(Some(combined)) - } else { - Ok(Some(s.into())) - } - } - } - - /// Convert GitHub URL to raw content URL - /// Supports: - /// - https://github.com/user/repo/blob/branch/path/file -> https://raw.githubusercontent.com/user/repo/branch/path/file - /// - https://raw.githubusercontent.com/... (already raw, no change) - /// - Other URLs: no change - fn convert_to_raw_url(url: &str) -> String { - // Already a raw URL - if url.contains("raw.githubusercontent.com") || url.contains("raw.github.com") { - return url.to_string(); - } - - // Convert github.com/user/repo/blob/... to raw.githubusercontent.com/user/repo/... - if url.contains("github.com") && url.contains("/blob/") { - let converted = url - .replace("github.com", "raw.githubusercontent.com") - .replace("/blob/", "/"); - println!("Converting GitHub URL to raw: {} -> {}", url, converted); - return converted; - } - - // Not a GitHub URL or already in correct format - url.to_string() - } - - async fn download_config_to_temp(&self, url: &str) -> anyhow::Result { - use std::time::SystemTime; - - println!("Downloading cargo config from: {}", url); - - // Get system temp directory - let temp_dir = std::env::temp_dir(); - - // Generate filename with timestamp - let timestamp = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs(); - - // Extract filename from URL or use default - let url_path = url.split('/').next_back().unwrap_or("config.toml"); - let filename = format!("cargo_config_{}_{}", timestamp, url_path); - let target_path = temp_dir.join(filename); - - // Create reqwest client - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(30)) - .build() - .map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?; - - // Build request with User-Agent for GitHub - let mut request = client.get(url); - - if url.contains("github.com") || url.contains("githubusercontent.com") { - // GitHub requires User-Agent - request = request.header("User-Agent", "ostool-cargo-downloader"); - } - - // Download the file - let response = request - .send() - .await - .map_err(|e| anyhow!("Failed to download from {}: {}", url, e))?; - - if !response.status().is_success() { - return Err(anyhow!("HTTP error {}: {}", response.status(), url)); - } - - let content = response - .bytes() - .await - .map_err(|e| anyhow!("Failed to read response body: {}", e))?; - - // Write to temp file - tokio::fs::write(&target_path, content) - .await - .map_err(|e| anyhow!("Failed to write to temp file: {}", e))?; - - println!("Config downloaded to: {}", target_path.display()); - - Ok(target_path) - } -} diff --git a/ostool/src/run/mod.rs b/ostool/src/run/mod.rs index 06a70dc..41aefae 100644 --- a/ostool/src/run/mod.rs +++ b/ostool/src/run/mod.rs @@ -1,4 +1,3 @@ -pub mod cargo; pub mod qemu; pub mod tftp; pub mod uboot; diff --git a/ostool/src/run/qemu.rs b/ostool/src/run/qemu.rs index 2f19b9b..641ae94 100644 --- a/ostool/src/run/qemu.rs +++ b/ostool/src/run/qemu.rs @@ -41,7 +41,7 @@ pub async fn run_qemu(ctx: AppContext, args: RunQemuArgs) -> anyhow::Result<()> // Build logic will be implemented here let config_path = match args.qemu_config.clone() { Some(path) => path, - None => ctx.manifest_dir.join(".qemu.toml"), + None => ctx.paths.manifest.join(".qemu.toml"), }; let schema_path = default_schema_by_init(&config_path); @@ -166,9 +166,9 @@ impl QemuRunner { cmd.arg("-bios").arg(bios); } - if let Some(bin_path) = &self.ctx.bin_path { + if let Some(bin_path) = &self.ctx.paths.artifacts.bin { cmd.arg("-kernel").arg(bin_path); - } else if let Some(elf_path) = &self.ctx.elf_path { + } else if let Some(elf_path) = &self.ctx.paths.artifacts.elf { cmd.arg("-kernel").arg(elf_path); } cmd.stdout(Stdio::piped()); diff --git a/ostool/src/run/tftp.rs b/ostool/src/run/tftp.rs index ea0140a..23719d6 100644 --- a/ostool/src/run/tftp.rs +++ b/ostool/src/run/tftp.rs @@ -7,8 +7,8 @@ use crate::ctx::AppContext; pub fn run_tftp_server(app: &AppContext) -> anyhow::Result<()> { // TFTP server implementation goes here - let mut file_dir = app.manifest_dir.clone(); - if let Some(elf_path) = &app.elf_path { + let mut file_dir = app.paths.manifest.clone(); + if let Some(elf_path) = &app.paths.artifacts.elf { file_dir = elf_path .parent() .ok_or(anyhow!("{} no parent dir", elf_path.display()))? diff --git a/ostool/src/run/uboot.rs b/ostool/src/run/uboot.rs index 1da1ee5..14c136f 100644 --- a/ostool/src/run/uboot.rs +++ b/ostool/src/run/uboot.rs @@ -70,6 +70,7 @@ pub struct Net { pub board_ip: Option, pub gatewayip: Option, pub netmask: Option, + pub tftp_dir: Option, } #[derive(Debug, Clone)] @@ -82,7 +83,7 @@ pub async fn run_uboot(ctx: AppContext, args: RunUbootArgs) -> anyhow::Result<() // Build logic will be implemented here let config_path = match args.config.clone() { Some(path) => path, - None => ctx.workspace_folder.join(".uboot.toml"), + None => ctx.paths.workspace.join(".uboot.toml"), }; let schema_path = default_schema_by_init(&config_path); @@ -285,7 +286,13 @@ impl Runner { self.preper_regex()?; self.ctx.objcopy_output_bin()?; - let kernel = self.ctx.bin_path.as_ref().ok_or(anyhow!("bin not exist"))?; + let kernel = self + .ctx + .paths + .artifacts + .bin + .as_ref() + .ok_or(anyhow!("bin not exist"))?; info!("Starting U-Boot runner..."); @@ -293,9 +300,18 @@ impl Runner { let ip_string = self.detect_tftp_ip(); - if let Some(ip) = ip_string.as_ref() { - info!("TFTP server IP: {}", ip); - tftp::run_tftp_server(&self.ctx)?; + let is_tftp = self + .config + .net + .as_ref() + .and_then(|net| net.tftp_dir.as_ref()) + .is_some(); + + if !is_tftp { + if let Some(ip) = ip_string.as_ref() { + info!("TFTP server IP: {}", ip); + tftp::run_tftp_server(&self.ctx)?; + } } info!( @@ -417,7 +433,28 @@ impl Runner { ) .await?; - let fitname = fitimage.file_name().unwrap().to_str().unwrap(); + let fitname = if is_tftp { + let tftp_dir = self + .config + .net + .as_ref() + .and_then(|net| net.tftp_dir.as_ref()) + .unwrap(); + + let fitimage = fitimage.file_name().unwrap(); + let tftp_path = PathBuf::from(tftp_dir).join(fitimage); + + info!("Setting TFTP file path: {}", tftp_path.display()); + tftp_path.display().to_string() + } else { + let name = fitimage + .file_name() + .and_then(|n| n.to_str()) + .ok_or(anyhow!("Invalid fitimage filename"))?; + + info!("Using fitimage filename: {}", name); + name.to_string() + }; let bootcmd = if let Some(ref board_ip) = self.config.net.as_ref().and_then(|e| e.board_ip.clone()) {