From b4f85dde75162b531ac13e975d6a2f3c6a3689cc Mon Sep 17 00:00:00 2001 From: Ockham Liu Date: Wed, 29 Apr 2026 23:48:16 +0800 Subject: [PATCH 1/4] refactor: new progress bar and new param of build command --- AGENTS.md | 2 +- Cargo.lock | 247 +++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + README.md | 7 +- src/adapters/hvigor.rs | 181 +++++++++++++++--------------- src/adapters/ohpm.rs | 86 +++++++++++++- src/build.rs | 227 +++++++++++++++++++++---------------- src/clean.rs | 8 +- src/main.rs | 1 + src/progress.rs | 99 +++++++++++++++++ src/run.rs | 2 +- 11 files changed, 654 insertions(+), 207 deletions(-) create mode 100644 src/progress.rs diff --git a/AGENTS.md b/AGENTS.md index 15a9649..2c6a11f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ cargo run -- - `add` - Add a DevEco Studio path (auto-extracts apiVersion/version) - `remove` - Remove a DevEco Studio path or version - `list` - List current environment configurations -- `build` - Build modules(s) and product(s) (supports `-m/--module` and `--products`) +- `build` - Build modules(s) and product(s) (supports `-m/--modules` and `--products`) - `clean` - Clean build artifacts and uninstall application from devices (supports `--with-devices` and `--with-all-devices`) - `lint` - Run code linter (codelinter) and fix issues (supports `--fix` and `--products`) - `emulator` - Manage emulator instances (`start`, `stop`, `list`) diff --git a/Cargo.lock b/Cargo.lock index 0e6ee17..405c473 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,12 @@ dependencies = [ "objc2", ] +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + [[package]] name = "cfg-if" version = "1.0.4" @@ -152,6 +158,19 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + [[package]] name = "console" version = "0.16.3" @@ -200,7 +219,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" dependencies = [ - "console", + "console 0.16.3", "fuzzy-matcher", "shell-words", "tempfile", @@ -284,6 +303,30 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "fuzzy-matcher" version = "0.3.7" @@ -360,6 +403,7 @@ dependencies = [ "dialoguer", "dirs", "generic-array", + "indicatif", "os_pipe", "owo-colors", "serde", @@ -386,6 +430,19 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console 0.15.11", + "number_prefix", + "portable-atomic", + "unicode-width", + "web-time", +] + [[package]] name = "is_executable" version = "1.0.5" @@ -407,6 +464,18 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -458,6 +527,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "objc2" version = "0.6.4" @@ -550,6 +625,18 @@ dependencies = [ "sha2", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "prettyplease" version = "0.2.37" @@ -608,6 +695,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "semver" version = "1.0.28" @@ -700,6 +793,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "strsim" version = "0.11.1" @@ -864,6 +963,51 @@ dependencies = [ "wit-bindgen 0.51.0", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -898,19 +1042,38 @@ dependencies = [ "semver", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -922,6 +1085,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -929,58 +1108,106 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" diff --git a/Cargo.toml b/Cargo.toml index b76ae88..50261fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,4 @@ ctrlc = "3.5.2" generic-array = "0.14.9" clap_complete = { version = "4.5", features = ["unstable-dynamic"] } serde_json = "1.0.149" +indicatif = "0.17" diff --git a/README.md b/README.md index 80e84d3..5cc854a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ## ✨ 核心特性 -- 🚀 **极速构建 (`build`)**:自动推断工程结构,支持 `module@target` 粒度构建,完美集成 `hvigor` 且支持多产品 (`--products`) 一键循环构建 `assembleApp`。 +- 🚀 **极速构建 (`build`)**:自动推断工程结构,支持 `module@target` 粒度构建及多模块同时构建,完美集成 `hvigor` 且支持多产品 (`--products`) 一键循环构建 `assembleApp`。 - 📦 **智能运行 (`run`)**:内置日志追踪,自动解析依赖并在推送 HAP 的同时连带安装相关 HSP,过滤 `hilog` 及 `FaultLogger` 精准捕获崩溃。 - 🧹 **一键清理 (`clean`)**:支持工程或单模块清理,同时提供 `--with-devices` / `--with-all-devices` 参数快速卸载远端设备上的应用。 - 📱 **设备与模拟器管理 (`device`** **/** **`emulator`)**:跨平台(macOS/Windows)快速启动、停止模拟器实例,列出可用物理设备及模拟器。 @@ -71,7 +71,10 @@ heco env --help heco build # 构建特定模块及其 target -heco build --module entry@default +heco build --modules entry@default + +# 构建多个模块 +heco build --modules entry,feature # 一键构建所有 product 的 APP 包 heco build --products diff --git a/src/adapters/hvigor.rs b/src/adapters/hvigor.rs index d3c8ba8..1b79f36 100644 --- a/src/adapters/hvigor.rs +++ b/src/adapters/hvigor.rs @@ -2,6 +2,7 @@ use crate::build::BuildArgs; use crate::clean::CleanArgs; use crate::command::CommandRunner; use crate::config::Config; +use crate::progress::StatusBar; use crate::project::{ModuleType, load_project}; use anstream::println; use owo_colors::OwoColorize; @@ -49,106 +50,87 @@ fn parse_log_type(line: &str) -> Option<(LogType, String)> { None } -/// 处理 hvigor 日志块的函数 -fn process_log_block(block: &[String], width: usize) { - if block.is_empty() { - return; - } - - // 标记上一行是否是 Warning/Error,用于处理多行日志的缩进延续 +/// 运行命令并处理日志块 +fn run_command_with_log_handling( + runner: &CommandRunner, + node_path_str: &str, + program_args: &[&str], + width: usize, + bar: Option<&StatusBar>, +) -> anyhow::Result<()> { + // 追踪连续行的状态 let mut last_log_type: Option = None; + let mut first_line = true; - // 处理每一行 - for (i, line) in block.iter().enumerate() { - // 去掉颜色控制字符及其他 ANSI 转义序列 + runner.run_with_handler(node_path_str, program_args, |line| { + // 立即处理这一行 let mut processed_line = anstream::adapter::strip_str(line).to_string(); - // 忽略空行 if processed_line.trim().is_empty() { - continue; + return; } - // 处理第一行(块的头部) - if i == 0 { - // 去掉 > hvigor 前缀 - if processed_line.starts_with("> hvigor ") { - processed_line = processed_line - .trim_start_matches("> hvigor") - .trim_start() - .to_string(); - } + // 如果是新块开头,重置为非延续状态,并去掉 "> hvigor " 前缀 + let is_block_header = processed_line.trim().starts_with("> hvigor "); + + if is_block_header { + processed_line = processed_line + .trim_start_matches("> hvigor ") + .trim_start() + .to_string(); + last_log_type = None; } - // 尝试解析当前行是否是 Warning 或 Error + // 尝试解析为警告/错误 if let Some((log_type, content)) = parse_log_type(&processed_line) { last_log_type = Some(log_type); - match log_type { - LogType::Warning => { - println!("{}: {}", "warning".yellow().bold(), content); - } - LogType::Error => { - println!("{}: {}", "error".red().bold(), content); - } + let output = match log_type { + LogType::Warning => format!("{}: {}", "warning".yellow().bold(), content), + LogType::Error => format!("{}: {}", "error".red().bold(), content), + }; + if let Some(b) = bar { + b.println(&output); + } else { + println!("{}", output); } } else { - // 如果当前行不是显式的 Warning/Error 前缀开头 - - // 如果这一行是以空白字符开头,且上一行是 Warning/Error,我们认为这是多行日志的延续 + // 检查是否是延续行(以空白开头且上一行是警告/错误) if line.starts_with(char::is_whitespace) - && let Some(log_type) = last_log_type + && let Some(_log_type) = last_log_type { - // 延续上一行的颜色风格,但保持其原有的缩进格式 - match log_type { - LogType::Warning => println!("{}", processed_line), - LogType::Error => println!("{}", processed_line), + // 延续,直接打印原始内容 + if let Some(b) = bar { + b.println(&processed_line); + } else { + println!("{}", processed_line); } - continue; - } - - // 否则这是一个普通的日志行 - last_log_type = None; // 断开延续状态 - - if i == 0 { - // 第一行,添加绿色的 hvigor: 前缀 - println!( - "{:>width$} {}", - "hvigor".green().bold(), - processed_line, - width = width - ); } else { - // 后续行,直接打印原始信息 - println!("{}", processed_line); - } - } - } -} - -/// 运行命令并处理日志块 -fn run_command_with_log_handling( - runner: &CommandRunner, - node_path_str: &str, - program_args: &[&str], - width: usize, -) -> anyhow::Result<()> { - // 维护日志块 - let mut current_block = Vec::new(); - runner.run_with_handler(node_path_str, program_args, |line| { - // 检查是否是新的日志块开头 - if line.trim().starts_with("> hvigor ") { - // 处理上一个日志块 - if !current_block.is_empty() { - process_log_block(¤t_block, width); - current_block.clear(); + // 普通行,重置延续状态 + last_log_type = None; + + let output = if is_block_header || first_line { + // 块的第一行,添加 hvigor 前缀 + first_line = false; + format!( + "{:>width$} {}", + "hvigor".green().bold(), + processed_line, + width = width + ) + } else { + // 普通延续行 + processed_line.clone() + }; + + if let Some(b) = bar { + b.println(&output); + } else { + println!("{}", output); + } } } - // 添加当前行到日志块 - current_block.push(line.to_string()); })?; - // 处理最后一个日志块 - if !current_block.is_empty() { - process_log_block(¤t_block, width); - } + Ok(()) } @@ -171,14 +153,27 @@ impl BuildArgs { args.push(format!("product={}", p)); } } else { - let (module_name, target_name) = self.parse_module().unwrap_or((String::new(), None)); + let parsed_modules = self.parse_modules().unwrap_or_default(); + + if parsed_modules.is_empty() { + let mut tasks = resolve_tasks("", &None, project_root)?; + args.append(&mut tasks); + } else { + let mut all_tasks = Vec::new(); + let mut module_names = Vec::new(); + + for (module_name, target_name) in parsed_modules { + let mut tasks = resolve_tasks(&module_name, &target_name, project_root)?; + all_tasks.append(&mut tasks); + module_names.push(module_name); + } - let mut tasks = resolve_tasks(&module_name, &target_name, project_root)?; - args.append(&mut tasks); + all_tasks.sort(); + all_tasks.dedup(); + args.append(&mut all_tasks); - if let Some(module) = &self.module { args.push("-p".to_string()); - args.push(format!("module={}", module)); + args.push(format!("module={}", module_names.join(","))); } } @@ -193,7 +188,13 @@ impl BuildArgs { } } -pub fn sync(project_root: &Path, config: &Config, quiet: bool, width: usize) -> anyhow::Result<()> { +pub fn sync( + project_root: &Path, + config: &Config, + quiet: bool, + width: usize, + bar: Option<&StatusBar>, +) -> anyhow::Result<()> { let node_path = config .node_path() .ok_or_else(|| anyhow::anyhow!("未找到 Node 路径"))?; @@ -253,7 +254,7 @@ pub fn sync(project_root: &Path, config: &Config, quiet: bool, width: usize) -> } Ok(()) } else { - run_command_with_log_handling(&runner, node_path_str, &program_args, width) + run_command_with_log_handling(&runner, node_path_str, &program_args, width, bar) } } @@ -262,6 +263,7 @@ pub fn build( project_root: &PathBuf, config: &Config, width: usize, + bar: Option<&StatusBar>, ) -> anyhow::Result<()> { let node_path = config .node_path() @@ -305,7 +307,7 @@ pub fn build( } Ok(()) } else { - run_command_with_log_handling(&runner, node_path_str, &program_args, width) + run_command_with_log_handling(&runner, node_path_str, &program_args, width, bar) } } @@ -380,6 +382,7 @@ pub fn clean( project_root: &Path, config: &Config, width: usize, + bar: Option<&StatusBar>, ) -> anyhow::Result<()> { let node_path = config .node_path() @@ -416,6 +419,6 @@ pub fn clean( } Ok(()) } else { - run_command_with_log_handling(&runner, node_path_str, &program_args, width) + run_command_with_log_handling(&runner, node_path_str, &program_args, width, bar) } } diff --git a/src/adapters/ohpm.rs b/src/adapters/ohpm.rs index 769ac02..d74a9ab 100644 --- a/src/adapters/ohpm.rs +++ b/src/adapters/ohpm.rs @@ -1,10 +1,47 @@ use crate::command::CommandRunner; use crate::config::Config; +use crate::progress::StatusBar; use anstream::println; use owo_colors::OwoColorize; +use std::collections::HashMap; use std::path::Path; +use std::sync::LazyLock; -pub fn install(project_root: &Path, config: &Config, quiet: bool) -> anyhow::Result<()> { +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum LogType { + Warning, + Error, +} + +// 预定义日志前缀映射 +static LOG_PREFIX_MAP: LazyLock>> = LazyLock::new(|| { + let mut m = HashMap::new(); + m.insert(LogType::Warning, vec!["WARN:", "warning:", "ohpm WARN:"]); + m.insert(LogType::Error, vec!["ERROR:", "ERR!", "error:"]); + m +}); + +/// 识别行是否匹配指定的日志类型,如果匹配则返回 (LogType, 剥离前缀后的内容) +fn parse_log_type(line: &str) -> Option<(LogType, String)> { + let line_trim = line.trim(); + for (log_type, prefixes) in LOG_PREFIX_MAP.iter() { + for prefix in prefixes { + if line_trim.starts_with(prefix) { + // 如果找到匹配,截取掉前缀并返回 + let content = line_trim.strip_prefix(prefix).unwrap().trim().to_string(); + return Some((*log_type, content)); + } + } + } + None +} + +pub fn install( + project_root: &Path, + config: &Config, + quiet: bool, + bar: Option<&StatusBar>, +) -> anyhow::Result<()> { let ohpm_path = config .ohpm_path() .ok_or_else(|| anyhow::anyhow!("未找到 ohpm 路径"))?; @@ -36,9 +73,52 @@ pub fn install(project_root: &Path, config: &Config, quiet: bool) -> anyhow::Res anyhow::bail!("{}", "error: ohpm install failed".red()); } } else { - println!("{:>9} dependencies", "Installing".green().bold()); + let mut last_log_type: Option = None; + runner.run_with_handler(ohpm_path_str, &program_args, |line| { - println!(" {}", line); + let processed = anstream::adapter::strip_str(line).to_string(); + if processed.trim().is_empty() { + return; + } + + let mut content_line = processed.trim(); + // 去除所有连续的 "ohpm " 前缀,解决 "ohpm ohpm WARN:" 这种重复情况 + while content_line.starts_with("ohpm ") { + content_line = content_line.strip_prefix("ohpm ").unwrap().trim_start(); + } + + // 尝试解析为警告/错误 + if let Some((log_type, content)) = parse_log_type(content_line) { + last_log_type = Some(log_type); + let output = match log_type { + LogType::Warning => format!("{}: {}", "warning".yellow().bold(), content), + LogType::Error => format!("{}: {}", "error".red().bold(), content), + }; + if let Some(b) = bar { + b.println(&output); + } else { + println!("{}", output); + } + } else { + // 检查是否是延续行(以空白开头且上一行是警告/错误) + if line.starts_with(char::is_whitespace) && last_log_type.is_some() { + // 延续,直接打印原始内容 + if let Some(b) = bar { + b.println(&processed); + } else { + println!("{}", processed); + } + } else { + // 普通行,重置延续状态 + last_log_type = None; + let output = format!("{:>12} {}", "ohpm".green().bold(), content_line); + if let Some(b) = bar { + b.println(&output); + } else { + println!("{}", output); + } + } + } })?; } diff --git a/src/build.rs b/src/build.rs index bcd4649..c7d3ce2 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1,7 +1,8 @@ use crate::adapters::hvigor; use crate::config::Config; +use crate::progress::StatusBar; use crate::project::find_project_root; -use anstream::{eprintln, println}; +use anstream::eprintln; use clap::Parser; use clap_complete::engine::ArgValueCompleter; use owo_colors::OwoColorize; @@ -9,9 +10,9 @@ use std::time::Instant; #[derive(Parser, Debug)] pub struct BuildArgs { - /// Module name (format: module or module@target) - #[arg(short, long, add = ArgValueCompleter::new(crate::completion::complete_modules))] - pub module: Option, + /// Module names (format: module or module@target), separated by commas. If passed without values, builds all modules. + #[arg(short, long, num_args = 0.., value_delimiter = ',', add = ArgValueCompleter::new(crate::completion::complete_modules), conflicts_with = "products")] + pub modules: Option>, /// Debug build mode #[arg(long, conflicts_with = "release")] pub debug: bool, @@ -22,20 +23,25 @@ pub struct BuildArgs { #[arg(long, short)] pub quiet: bool, /// Specify one or more product names to build app, separated by commas. If passed without values, builds all products. - #[arg(long, num_args = 0.., value_delimiter = ',', add = ArgValueCompleter::new(crate::completion::complete_products))] + #[arg(long, num_args = 0.., value_delimiter = ',', add = ArgValueCompleter::new(crate::completion::complete_products), conflicts_with = "modules")] pub products: Option>, } impl BuildArgs { - pub fn parse_module(&self) -> Option<(String, Option)> { - self.module.as_ref().map(|m| { - if let Some(idx) = m.find('@') { - let module_name = m[..idx].to_string(); - let target_name = m[idx + 1..].to_string(); - (module_name, Some(target_name)) - } else { - (m.clone(), None) - } + pub fn parse_modules(&self) -> Option)>> { + self.modules.as_ref().map(|modules| { + modules + .iter() + .map(|m| { + if let Some(idx) = m.find('@') { + let module_name = m[..idx].to_string(); + let target_name = m[idx + 1..].to_string(); + (module_name, Some(target_name)) + } else { + (m.clone(), None) + } + }) + .collect() }) } } @@ -60,22 +66,6 @@ pub(crate) fn handle_build(args: BuildArgs) { } }; - if !args.quiet { - println!("{:>9} project", "Syncing".green().bold()); - } - if let Err(e) = hvigor::sync(&project_root, &config, args.quiet, 9) { - eprintln!("{}", format!("error: sync failed: {}", e).red()); - std::process::exit(1); - } - - if let Err(e) = crate::adapters::ohpm::install(&project_root, &config, args.quiet) { - eprintln!("{}", format!("error: install failed: {}", e).red()); - std::process::exit(1); - } - - let start = Instant::now(); - let build_type = if args.release { "release" } else { "debug" }; - let project = match crate::project::load_project() { Ok(p) => p, Err(e) => { @@ -84,27 +74,93 @@ pub(crate) fn handle_build(args: BuildArgs) { } }; - let args = if args.module.is_none() && args.products.is_none() { - if let Ok(current_dir) = std::env::current_dir() { - if let Some(module) = project.find_module_by_path(¤t_dir) { - BuildArgs { - module: Some(module.name.clone()), - debug: args.debug, - release: args.release, - quiet: args.quiet, - products: args.products.clone(), - } - } else { - args + // 先预处理 args,确定有多少个 build 任务 + let args = if let Some(modules) = &args.modules { + if modules.is_empty() { + // 如果传入了 --modules 但没有值,收集所有模块名 + let all_modules: Vec = project.modules.iter().map(|m| m.name.clone()).collect(); + BuildArgs { + modules: Some(all_modules), + debug: args.debug, + release: args.release, + quiet: args.quiet, + products: args.products.clone(), } } else { args } + } else if args.products.is_none() { + let entry_modules: Vec<_> = project + .modules + .iter() + .filter(|m| m.module_type == crate::project::ModuleType::Entry) + .collect(); + + let target_module = if entry_modules.len() == 1 { + Some(entry_modules[0].name.clone()) + } else if project.modules.len() == 1 { + Some(project.modules[0].name.clone()) + } else { + None + }; + + if let Some(module_name) = target_module { + BuildArgs { + modules: Some(vec![module_name]), + debug: args.debug, + release: args.release, + quiet: args.quiet, + products: args.products.clone(), + } + } else { + eprintln!( + "{}", + "error: no modules specified. Please specify modules using --modules or --products. \ + (e.g., `heco build --modules entry` or `heco build --products`)".red() + ); + std::process::exit(1); + } } else { args }; - let (module_name, target_name) = args.parse_module().unwrap_or((String::new(), None)); + // 计算总任务数 + let num_build_tasks = if let Some(products) = &args.products { + if products.is_empty() { + project.products.len() + } else { + products.len() + } + } else { + 1 + }; + let total_tasks = 2 + num_build_tasks; // sync + ohpm + build(s) + + let bar = StatusBar::new(total_tasks, args.quiet); + let total_start = Instant::now(); + let build_type = if args.release { "release" } else { "debug" }; + + // 任务 1: Sync + { + let _task = bar.task("Syncing", "project"); + if let Err(e) = hvigor::sync(&project_root, &config, args.quiet, 12, Some(&bar)) { + eprintln!("{}", format!("error: sync failed: {}", e).red()); + std::process::exit(1); + } + } + + // 任务 2: Ohpm Install + { + let _task = bar.task("Installing", "dependencies"); + if let Err(e) = + crate::adapters::ohpm::install(&project_root, &config, args.quiet, Some(&bar)) + { + eprintln!("{}", format!("error: install failed: {}", e).red()); + std::process::exit(1); + } + } + + let parsed_modules = args.parse_modules().unwrap_or_default(); if let Some(products) = &args.products { let target_products = if products.is_empty() { @@ -118,38 +174,21 @@ pub(crate) fn handle_build(args: BuildArgs) { std::process::exit(1); } - let total_start = Instant::now(); for product in &target_products { - if !args.quiet { - println!( - "{:>9} product {} ({})", - "Compiling".green().bold(), - product, - project_root.display() - ); - } + let desc = format!("product {} ({})", product, project_root.display()); + let _task = bar.task("Compiling", &desc); // Create a temporary args just for this product let single_product_args = BuildArgs { - module: args.module.clone(), + modules: args.modules.clone(), debug: args.debug, release: args.release, quiet: args.quiet, products: Some(vec![product.clone()]), }; - match hvigor::build(&single_product_args, &project_root, &config, 9) { - Ok(_) => { - if !args.quiet { - println!( - "{:>9} {} product {} in {:.2?}", - "Finished".green().bold(), - build_type, - product, - start.elapsed() - ); - } - } + match hvigor::build(&single_product_args, &project_root, &config, 12, Some(&bar)) { + Ok(_) => {} Err(e) => { eprintln!( "{}", @@ -159,48 +198,42 @@ pub(crate) fn handle_build(args: BuildArgs) { } } } - - if target_products.len() > 1 && !args.quiet { - println!( - "{:>9} {} product(s) in {:.2?}", - "Finished".green().bold(), - build_type, - total_start.elapsed() - ); - } } else { - let display_name = if module_name.is_empty() { + let display_name = if parsed_modules.is_empty() { "project".to_string() - } else if let Some(target) = target_name { - format!("{}@{}", module_name, target) } else { - module_name.clone() + parsed_modules + .iter() + .map(|(m, t)| { + if let Some(target) = t { + format!("{}@{}", m, target) + } else { + m.clone() + } + }) + .collect::>() + .join(",") }; + let desc = format!("{} ({})", display_name, project_root.display()); - if !args.quiet { - println!( - "{:>9} {} ({})", - "Compiling".green().bold(), - display_name, - project_root.display() - ); - } + let _task = bar.task("Compiling", &desc); - match hvigor::build(&args, &project_root, &config, 9) { - Ok(_) => { - if !args.quiet { - println!( - "{:>9} {} module(s) in {:.2?}", - "Finished".green().bold(), - build_type, - start.elapsed() - ); - } - } + match hvigor::build(&args, &project_root, &config, 12, Some(&bar)) { + Ok(_) => {} Err(e) => { eprintln!("{}", format!("error: build failed: {}", e).red()); std::process::exit(1); } } } + + // 结束,显示总完成信息 + if !args.quiet { + bar.finish_with_message(&format!( + "{:>12} {} in {:.2?}", + "Finished".green().bold(), + build_type, + total_start.elapsed() + )); + } } diff --git a/src/clean.rs b/src/clean.rs index 478c8ed..a5a87f7 100644 --- a/src/clean.rs +++ b/src/clean.rs @@ -81,18 +81,18 @@ pub(crate) fn handle_clean(args: CleanArgs) { None => "project".to_string(), }; println!( - "{:>9} {} ({})", + "{:>12} {} ({})", "Cleaning".green().bold(), target_display, project_root.display() ); } - match hvigor::clean(&args, &project_root, &config, 9) { + match hvigor::clean(&args, &project_root, &config, 12, None) { Ok(_) => { if !args.quiet { println!( - "{:>9} in {:.2?}", + "{:>12} in {:.2?}", "Finished".green().bold(), start.elapsed() ); @@ -154,7 +154,7 @@ fn handle_uninstall( for (device_name, device_id) in target_devices { if !args.quiet { println!( - "{:>9} {} from {} ({})", + "{:>12} {} from {} ({})", "Uninstall".blue().bold(), bundle_name, device_name, diff --git a/src/main.rs b/src/main.rs index 95235f1..c53f063 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod device; mod emulator; mod env; mod lint; +mod progress; mod project; mod run; diff --git a/src/progress.rs b/src/progress.rs new file mode 100644 index 0000000..212c8b3 --- /dev/null +++ b/src/progress.rs @@ -0,0 +1,99 @@ +use anstream::println; +use indicatif::{ProgressBar, ProgressStyle}; +use owo_colors::OwoColorize; +use std::cell::RefCell; + +/// 底部状态栏管理器 - 用 indicatif +pub struct StatusBar { + bar: ProgressBar, + quiet: bool, + state: RefCell, +} + +struct State { + total: usize, + current: usize, +} + +impl StatusBar { + /// 创建新的状态栏 + pub fn new(total: usize, quiet: bool) -> Self { + let bar = ProgressBar::new(total as u64); + bar.set_style( + ProgressStyle::default_spinner() + .template("{wide_msg} {spinner:.green}") + .unwrap(), + ); + bar.enable_steady_tick(std::time::Duration::from_millis(100)); + + Self { + bar, + quiet, + state: RefCell::new(State { total, current: 0 }), + } + } + + /// 更新总任务数 + #[allow(dead_code)] + pub fn set_total(&self, total: usize) { + self.state.borrow_mut().total = total; + self.bar.set_length(total as u64); + } + + /// 打印内容,自动处理状态栏 + pub fn println(&self, content: &str) { + if self.quiet { + return; + } + self.bar.println(content); + } + + /// 开始一个任务,返回任务guard + pub fn task(&self, name: &str, description: &str) -> TaskGuard<'_> { + self.begin_task(name, description); + TaskGuard { _bar: self } + } + + /// 内部:开始任务 + fn begin_task(&self, name: &str, description: &str) { + if self.quiet { + return; + } + + let mut state = self.state.borrow_mut(); + state.current += 1; + + // 统一宽度 12,右对齐(模仿 Cargo) + let start_msg = format!("{:>12} {}", name.green().bold(), description); + self.bar.println(start_msg); + + // 设置状态栏消息,进度数字放在对齐文字后面 + let status_line = format!( + "{:>12} {} [{}/{}]", + name.green().bold(), + description, + state.current, + state.total + ); + + self.bar.set_message(status_line); + self.bar.set_position(state.current as u64); + } + + /// 完成并显示最终信息(清除进度条后打印,避免模板前缀干扰对齐) + pub fn finish_with_message(&self, msg: &str) { + if !self.quiet { + self.bar.finish_and_clear(); + println!("{}", msg); + } + } +} + +/// 任务 guard +pub struct TaskGuard<'a> { + _bar: &'a StatusBar, +} + +impl<'a> Drop for TaskGuard<'a> { + fn drop(&mut self) {} +} diff --git a/src/run.rs b/src/run.rs index cd5a91a..f30f9a3 100644 --- a/src/run.rs +++ b/src/run.rs @@ -144,7 +144,7 @@ pub fn handle_run(args: RunArgs) -> Result<()> { // 3. Build (call heco build) println!("Building module {}...", module_name); let build_args = BuildArgs { - module: Some(format!("{}@{}", module_name, target_name)), + modules: Some(vec![format!("{}@{}", module_name, target_name)]), debug: false, release: false, quiet: false, From 8f1d96be69c784ef82ea0bad6e7af5a29dfa4dad Mon Sep 17 00:00:00 2001 From: Ockham Liu Date: Sat, 2 May 2026 23:03:52 +0800 Subject: [PATCH 2/4] fix(project): support building modules with product name Make ModuleTarget.name optional to handle modules without target names Remove conflict between --modules and --products flags to allow building specific modules for a product, with validation to ensure only one product is specified when using both parameters --- src/adapters/hvigor.rs | 42 ++++++++++++++++++----- src/build.rs | 75 +++++++++++++++++++++++++----------------- src/project.rs | 8 +++-- 3 files changed, 84 insertions(+), 41 deletions(-) diff --git a/src/adapters/hvigor.rs b/src/adapters/hvigor.rs index 1b79f36..3d95c31 100644 --- a/src/adapters/hvigor.rs +++ b/src/adapters/hvigor.rs @@ -137,22 +137,40 @@ fn run_command_with_log_handling( impl BuildArgs { pub fn to_command_args(&self, project_root: &PathBuf) -> anyhow::Result> { let mut args = Vec::new(); + let project = load_project()?; + if project.root != *project_root { + anyhow::bail!("project root mismatch"); + } - if let Some(products) = &self.products { - args.push("assembleApp".to_string()); - - let project = load_project()?; - if project.root != *project_root { - anyhow::bail!("project root mismatch"); + // Handle product parameter + let product = if let Some(products) = &self.products { + if !products.is_empty() { + if self.modules.is_some() && products.len() > 1 { + anyhow::bail!("only one product is allowed when using --modules parameter"); + } + let p = &products[0]; + project.validate_product(p)?; + Some(p) + } else { + None } + } else { + None + }; + + if self.products.is_some() && self.modules.is_none() { + // Only products specified, use assembleApp + args.push("assembleApp".to_string()); // Since build.rs now handles loop logic, self.products should only contain exactly 1 product - if let Some(p) = products.first() { - project.validate_product(p)?; + if let Some(p) = product { args.push("-p".to_string()); args.push(format!("product={}", p)); } - } else { + } + + // Handle modules parameter (whether products are specified or not) + if self.modules.is_some() { let parsed_modules = self.parse_modules().unwrap_or_default(); if parsed_modules.is_empty() { @@ -175,6 +193,12 @@ impl BuildArgs { args.push("-p".to_string()); args.push(format!("module={}", module_names.join(","))); } + + // Add product parameter if specified + if let Some(p) = product { + args.push("-p".to_string()); + args.push(format!("product={}", p)); + } } let mode = if self.release { "release" } else { "debug" }; diff --git a/src/build.rs b/src/build.rs index c7d3ce2..9e61645 100644 --- a/src/build.rs +++ b/src/build.rs @@ -11,7 +11,7 @@ use std::time::Instant; #[derive(Parser, Debug)] pub struct BuildArgs { /// Module names (format: module or module@target), separated by commas. If passed without values, builds all modules. - #[arg(short, long, num_args = 0.., value_delimiter = ',', add = ArgValueCompleter::new(crate::completion::complete_modules), conflicts_with = "products")] + #[arg(short, long, num_args = 0.., value_delimiter = ',', add = ArgValueCompleter::new(crate::completion::complete_modules))] pub modules: Option>, /// Debug build mode #[arg(long, conflicts_with = "release")] @@ -22,8 +22,8 @@ pub struct BuildArgs { /// Quiet mode, reduce output #[arg(long, short)] pub quiet: bool, - /// Specify one or more product names to build app, separated by commas. If passed without values, builds all products. - #[arg(long, num_args = 0.., value_delimiter = ',', add = ArgValueCompleter::new(crate::completion::complete_products), conflicts_with = "modules")] + /// Build .app product packages, or specify the product to use when building modules. Separated by commas. If passed without values, builds all products. + #[arg(long, num_args = 0.., value_delimiter = ',', add = ArgValueCompleter::new(crate::completion::complete_products))] pub products: Option>, } @@ -162,7 +162,45 @@ pub(crate) fn handle_build(args: BuildArgs) { let parsed_modules = args.parse_modules().unwrap_or_default(); - if let Some(products) = &args.products { + if args.modules.is_some() { + // When modules are specified, build them directly (product parameter will be handled in hvigor.rs) + let display_name = if parsed_modules.is_empty() { + "project".to_string() + } else { + parsed_modules + .iter() + .map(|(m, t)| { + if let Some(target) = t { + format!("{}@{}", m, target) + } else { + m.clone() + } + }) + .collect::>() + .join(",") + }; + + let desc = if let Some(products) = &args.products { + if !products.is_empty() { + format!("{} for product {} ({})", display_name, products[0], project_root.display()) + } else { + format!("{} ({})", display_name, project_root.display()) + } + } else { + format!("{} ({})", display_name, project_root.display()) + }; + + let _task = bar.task("Compiling", &desc); + + match hvigor::build(&args, &project_root, &config, 12, Some(&bar)) { + Ok(_) => {} + Err(e) => { + eprintln!("{}", format!("error: build failed: {}", e).red()); + std::process::exit(1); + } + } + } else if let Some(products) = &args.products { + // Only products specified, loop through them let target_products = if products.is_empty() { project.products.clone() } else { @@ -199,32 +237,9 @@ pub(crate) fn handle_build(args: BuildArgs) { } } } else { - let display_name = if parsed_modules.is_empty() { - "project".to_string() - } else { - parsed_modules - .iter() - .map(|(m, t)| { - if let Some(target) = t { - format!("{}@{}", m, target) - } else { - m.clone() - } - }) - .collect::>() - .join(",") - }; - let desc = format!("{} ({})", display_name, project_root.display()); - - let _task = bar.task("Compiling", &desc); - - match hvigor::build(&args, &project_root, &config, 12, Some(&bar)) { - Ok(_) => {} - Err(e) => { - eprintln!("{}", format!("error: build failed: {}", e).red()); - std::process::exit(1); - } - } + // No modules or products specified (should not happen due to earlier validation) + eprintln!("{}", "error: no modules or products specified".red()); + std::process::exit(1); } // 结束,显示总完成信息 diff --git a/src/project.rs b/src/project.rs index 405d464..7ff0a6c 100644 --- a/src/project.rs +++ b/src/project.rs @@ -69,7 +69,7 @@ struct ProjectModuleInfo { #[derive(Debug, Deserialize)] struct ModuleTarget { - name: String, + name: Option, } #[derive(Debug, Deserialize)] @@ -114,7 +114,11 @@ impl Module { let targets: Vec = info .targets .as_ref() - .map(|t| t.iter().map(|target| target.name.clone()).collect()) + .map(|t| { + t.iter() + .filter_map(|target| target.name.clone()) + .collect() + }) .unwrap_or_default(); let module_type = if module_path.join("src/main/module.json5").exists() { From 6cd198aa35d3ef76ef562c05091df127a2d77bf5 Mon Sep 17 00:00:00 2001 From: Ockham Liu Date: Sun, 3 May 2026 22:07:09 +0800 Subject: [PATCH 3/4] feat: improve color output control and add NO_COLOR support - Add console dependency for consistent color handling - Update color choice handling to sync console library settings - Add NO_COLOR environment variable detection in progress bar - Strip ANSI color codes when NO_COLOR is set - Minor code formatting improvements --- Cargo.lock | 1 + Cargo.toml | 1 + src/build.rs | 7 ++++++- src/main.rs | 12 ++++++++++-- src/progress.rs | 21 ++++++++++++++++++--- src/project.rs | 6 +----- 6 files changed, 37 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 405c473..b1ff864 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -399,6 +399,7 @@ dependencies = [ "anyhow", "clap", "clap_complete", + "console 0.15.11", "ctrlc", "dialoguer", "dirs", diff --git a/Cargo.toml b/Cargo.toml index 50261fd..f7b943e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,3 +26,4 @@ generic-array = "0.14.9" clap_complete = { version = "4.5", features = ["unstable-dynamic"] } serde_json = "1.0.149" indicatif = "0.17" +console = "0.15.8" diff --git a/src/build.rs b/src/build.rs index 9e61645..082d1e2 100644 --- a/src/build.rs +++ b/src/build.rs @@ -182,7 +182,12 @@ pub(crate) fn handle_build(args: BuildArgs) { let desc = if let Some(products) = &args.products { if !products.is_empty() { - format!("{} for product {} ({})", display_name, products[0], project_root.display()) + format!( + "{} for product {} ({})", + display_name, + products[0], + project_root.display() + ) } else { format!("{} ({})", display_name, project_root.display()) } diff --git a/src/main.rs b/src/main.rs index c53f063..5f07266 100644 --- a/src/main.rs +++ b/src/main.rs @@ -98,8 +98,16 @@ fn main() -> anyhow::Result<()> { let cli = Cli::parse(); match cli.color { - ColorChoice::Always => unsafe { std::env::set_var("CLICOLOR_FORCE", "1") }, - ColorChoice::Never => unsafe { std::env::set_var("NO_COLOR", "1") }, + ColorChoice::Always => { + unsafe { std::env::set_var("CLICOLOR_FORCE", "1") }; + console::set_colors_enabled(true); + console::set_colors_enabled_stderr(true); + } + ColorChoice::Never => { + unsafe { std::env::set_var("NO_COLOR", "1") }; + console::set_colors_enabled(false); + console::set_colors_enabled_stderr(false); + } ColorChoice::Auto => {} } diff --git a/src/progress.rs b/src/progress.rs index 212c8b3..b06fe33 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -45,7 +45,12 @@ impl StatusBar { if self.quiet { return; } - self.bar.println(content); + if std::env::var("NO_COLOR").is_ok() { + self.bar + .println(anstream::adapter::strip_str(content).to_string()); + } else { + self.bar.println(content); + } } /// 开始一个任务,返回任务guard @@ -65,7 +70,12 @@ impl StatusBar { // 统一宽度 12,右对齐(模仿 Cargo) let start_msg = format!("{:>12} {}", name.green().bold(), description); - self.bar.println(start_msg); + if std::env::var("NO_COLOR").is_ok() { + self.bar + .println(anstream::adapter::strip_str(&start_msg).to_string()); + } else { + self.bar.println(start_msg); + } // 设置状态栏消息,进度数字放在对齐文字后面 let status_line = format!( @@ -76,7 +86,12 @@ impl StatusBar { state.total ); - self.bar.set_message(status_line); + if std::env::var("NO_COLOR").is_ok() { + self.bar + .set_message(anstream::adapter::strip_str(&status_line).to_string()); + } else { + self.bar.set_message(status_line); + } self.bar.set_position(state.current as u64); } diff --git a/src/project.rs b/src/project.rs index 7ff0a6c..04ddef5 100644 --- a/src/project.rs +++ b/src/project.rs @@ -114,11 +114,7 @@ impl Module { let targets: Vec = info .targets .as_ref() - .map(|t| { - t.iter() - .filter_map(|target| target.name.clone()) - .collect() - }) + .map(|t| t.iter().filter_map(|target| target.name.clone()).collect()) .unwrap_or_default(); let module_type = if module_path.join("src/main/module.json5").exists() { From 78b5f686bcdbfb19bf137fbe27ca024b99b06d2b Mon Sep 17 00:00:00 2001 From: Ockham Liu Date: Sun, 3 May 2026 22:27:50 +0800 Subject: [PATCH 4/4] fix(run): resolve missing HSP build artifacts when running app - Adjust build flow: recursively resolve all HSP dependencies of the main module first - Build main module + all dependent HSPs together to ensure all artifacts are generated - Keep correct installation order: install all HSPs first, then main HAP (follows HarmonyOS requirement) Fixes error: "Build outputs directory not found for module 'xxx'" --- src/run.rs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/run.rs b/src/run.rs index f30f9a3..1d82443 100644 --- a/src/run.rs +++ b/src/run.rs @@ -141,10 +141,23 @@ pub fn handle_run(args: RunArgs) -> Result<()> { let is_emulator = target_device_id.contains("127.0.0.1") || target_device_id.contains("localhost"); - // 3. Build (call heco build) - println!("Building module {}...", module_name); + // 3. Resolve dependencies and build + let bundle_name = project.get_bundle_name()?; + let mut artifacts_to_install = Vec::new(); + let mut hsp_modules = Vec::new(); + + // 3.1 Resolve all HSP dependencies first (recursive) + project.resolve_hsp_dependencies(main_module, &mut hsp_modules)?; + + // 3.2 Build main module + all dependent HSP modules together + let mut build_modules = vec![format!("{}@{}", module_name, target_name)]; + for hsp_mod in &hsp_modules { + build_modules.push(format!("{}@{}", hsp_mod.name, target_name)); + } + + println!("Building modules: {}...", build_modules.join(", ")); let build_args = BuildArgs { - modules: Some(vec![format!("{}@{}", module_name, target_name)]), + modules: Some(build_modules), debug: false, release: false, quiet: false, @@ -152,13 +165,7 @@ pub fn handle_run(args: RunArgs) -> Result<()> { }; handle_build(build_args); // Assume this succeeds and generates the artifacts - // 4. Resolve artifacts and dependencies - let bundle_name = project.get_bundle_name()?; - let mut artifacts_to_install = Vec::new(); - let mut hsp_modules = Vec::new(); - - // 4.1 Resolve HSP dependencies - project.resolve_hsp_dependencies(main_module, &mut hsp_modules)?; + // 4. Resolve artifacts // 4.2 Find paths for HSPs for hsp_mod in &hsp_modules {