#![allow(clippy::result_large_err)]
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::cm_tools::cargo_metadata::cargo_metadata_command;
use crate::cm_tools::tool_result::ToolError;
use super::ToolContext;
use super::test_result_cache::{
TestCacheKey, TestCacheKind, cargo_test_args_fingerprint, fingerprint_rust_workspace_sources,
store_cached, try_get_cached, wrap_cache_hit,
};
const MAX_OUTPUT_LINES: usize = 800;
pub fn cargo_check(args_json: &str, workspace_root: &Path, max_output_len: usize) -> String {
cargo_check_try(args_json, workspace_root, max_output_len).unwrap_or_else(|e| e.message)
}
pub fn cargo_check_try(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
) -> Result<String, ToolError> {
run_cargo_subcommand_str_try("check", args_json, workspace_root, max_output_len, None)
}
const RUST_RUSTC_MAX_ARGS: usize = 64;
const RUST_RUSTC_MAX_ARG_BYTES: usize = 8192;
fn rustc_arg_is_safe(arg: &str) -> bool {
let a = arg.trim();
!a.contains("..") && !a.starts_with('/')
}
pub fn rust_rustc(args_json: &str, workspace_root: &Path, max_output_len: usize) -> String {
rust_rustc_try(args_json, workspace_root, max_output_len).unwrap_or_else(|e| e.message)
}
pub fn rust_rustc_try(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
) -> Result<String, ToolError> {
let v = crate::cm_tools::tools::parse_args_json(args_json).map_err(ToolError::invalid_args)?;
let args: Vec<String> = match v.get("args") {
Some(serde_json::Value::Array(arr)) => arr
.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect(),
Some(_) => {
return Err(ToolError::invalid_args(
"错误:args 必须是字符串数组".to_string(),
));
}
None => Vec::new(),
};
if args.len() > RUST_RUSTC_MAX_ARGS {
return Err(ToolError::invalid_args(format!(
"错误:rustc 参数个数超过上限 {}",
RUST_RUSTC_MAX_ARGS
)));
}
for a in &args {
if a.len() > RUST_RUSTC_MAX_ARG_BYTES {
return Err(ToolError::invalid_args(format!(
"错误:单参数长度超过 {} 字节",
RUST_RUSTC_MAX_ARG_BYTES
)));
}
if !rustc_arg_is_safe(a) {
return Err(ToolError::invalid_args(
"错误:参数不允许包含 \"..\" 或绝对路径(以 / 开头)".to_string(),
));
}
}
let mut cmd = Command::new("rustc");
cmd.args(&args).current_dir(workspace_root);
run_and_format_try(cmd, max_output_len, "rustc", "rust_rustc", None)
}
pub fn cargo_test(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
ctx: Option<&ToolContext<'_>>,
) -> String {
cargo_test_try(args_json, workspace_root, max_output_len, ctx).unwrap_or_else(|e| e.message)
}
pub fn cargo_test_try(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
ctx: Option<&ToolContext<'_>>,
) -> Result<String, ToolError> {
let v = crate::cm_tools::tools::parse_args_json(args_json).map_err(ToolError::invalid_args)?;
let Some(c) = ctx else {
return run_cargo_subcommand_value_try("test", &v, workspace_root, max_output_len, None);
};
maybe_cache_cargo_test_try(&v, workspace_root, c, || {
run_cargo_subcommand_value_try(
"test",
&v,
workspace_root,
max_output_len,
Some(c.command_timeout_secs),
)
})
}
pub fn cargo_clippy(args_json: &str, workspace_root: &Path, max_output_len: usize) -> String {
cargo_clippy_try(args_json, workspace_root, max_output_len).unwrap_or_else(|e| e.message)
}
pub fn cargo_clippy_try(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
) -> Result<String, ToolError> {
run_cargo_subcommand_str_try("clippy", args_json, workspace_root, max_output_len, None)
}
pub fn cargo_run(args_json: &str, workspace_root: &Path, max_output_len: usize) -> String {
cargo_run_try(args_json, workspace_root, max_output_len).unwrap_or_else(|e| e.message)
}
pub fn cargo_run_try(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
) -> Result<String, ToolError> {
run_cargo_subcommand_str_try("run", args_json, workspace_root, max_output_len, None)
}
pub fn rust_test_one(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
ctx: Option<&ToolContext<'_>>,
) -> String {
rust_test_one_try(args_json, workspace_root, max_output_len, ctx).unwrap_or_else(|e| e.message)
}
pub fn rust_test_one_try(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
ctx: Option<&ToolContext<'_>>,
) -> Result<String, ToolError> {
let v = crate::cm_tools::tools::parse_args_json(args_json).map_err(ToolError::invalid_args)?;
let filter = match v.get("test_name").and_then(|x| x.as_str()).map(str::trim) {
Some(s) if !s.is_empty() => s.to_string(),
_ => {
return Err(ToolError::invalid_args(
"错误:缺少 test_name 参数".to_string(),
));
}
};
let mut merged = v;
if let Some(obj) = merged.as_object_mut() {
obj.insert("test_filter".to_string(), serde_json::Value::String(filter));
}
let Some(c) = ctx else {
return run_cargo_subcommand_value_try("test", &merged, workspace_root, max_output_len, None);
};
maybe_cache_cargo_test_try(&merged, workspace_root, c, || {
run_cargo_subcommand_value_try(
"test",
&merged,
workspace_root,
max_output_len,
Some(c.command_timeout_secs),
)
})
}
fn maybe_cache_cargo_test_try(
v: &serde_json::Value,
workspace_root: &Path,
ctx: &ToolContext<'_>,
run: impl FnOnce() -> Result<String, ToolError>,
) -> Result<String, ToolError> {
if ctx.test_result_cache_enabled
&& let Some(inputs_fp) = fingerprint_rust_workspace_sources(workspace_root)
{
let args_fp = cargo_test_args_fingerprint(v);
let root = workspace_root.to_path_buf();
let key = TestCacheKey {
workspace_root: root,
kind: TestCacheKind::CargoTest,
args_fingerprint: args_fp,
inputs_fingerprint: inputs_fp.clone(),
};
if let Some(hit) = try_get_cached(
ctx.test_result_cache_enabled,
ctx.test_result_cache_max_entries,
&key,
) {
return Ok(wrap_cache_hit(&inputs_fp, &hit));
}
let out = run()?;
store_cached(
ctx.test_result_cache_enabled,
ctx.test_result_cache_max_entries,
key,
out.clone(),
);
return Ok(out);
}
run()
}
pub fn cargo_metadata(args_json: &str, workspace_root: &Path, max_output_len: usize) -> String {
cargo_metadata_try(args_json, workspace_root, max_output_len).unwrap_or_else(|e| e.message)
}
pub fn cargo_metadata_try(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
) -> Result<String, ToolError> {
let v = crate::cm_tools::tools::parse_args_json(args_json).map_err(ToolError::invalid_args)?;
let no_deps = v.get("no_deps").and_then(|x| x.as_bool()).unwrap_or(true);
let format_version = v
.get("format_version")
.and_then(|x| x.as_u64())
.unwrap_or(1);
if !workspace_root.join("Cargo.toml").is_file() {
return Err(ToolError::workspace(
"workspace_no_cargo_toml",
"错误:当前工作目录未找到 Cargo.toml".to_string(),
));
}
let cmd = cargo_metadata_command(workspace_root, no_deps, format_version);
run_and_format_try(cmd, max_output_len, "cargo metadata", "cargo_metadata", None)
}
pub fn cargo_tree(args_json: &str, workspace_root: &Path, max_output_len: usize) -> String {
cargo_tree_try(args_json, workspace_root, max_output_len).unwrap_or_else(|e| e.message)
}
pub fn cargo_tree_try(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
) -> Result<String, ToolError> {
let v = crate::cm_tools::tools::parse_args_json(args_json).map_err(ToolError::invalid_args)?;
if !workspace_root.join("Cargo.toml").is_file() {
return Err(ToolError::workspace(
"workspace_no_cargo_toml",
"错误:当前工作目录未找到 Cargo.toml".to_string(),
));
}
let package = v.get("package").and_then(|x| x.as_str()).map(str::trim);
let invert = v.get("invert").and_then(|x| x.as_str()).map(str::trim);
let depth = v.get("depth").and_then(|x| x.as_u64());
let edges = v.get("edges").and_then(|x| x.as_str()).map(str::trim);
let mut cmd = Command::new("cargo");
cmd.arg("tree");
if let Some(p) = package.filter(|s| !s.is_empty()) {
cmd.arg("--package").arg(p);
}
if let Some(i) = invert.filter(|s| !s.is_empty()) {
cmd.arg("--invert").arg(i);
}
if let Some(d) = depth {
cmd.arg("--depth").arg(d.to_string());
}
if let Some(e) = edges.filter(|s| !s.is_empty()) {
cmd.arg("--edges").arg(e);
}
cmd.current_dir(workspace_root);
run_and_format_try(cmd, max_output_len, "cargo tree", "cargo_tree", None)
}
pub fn cargo_clean(args_json: &str, workspace_root: &Path, max_output_len: usize) -> String {
cargo_clean_try(args_json, workspace_root, max_output_len).unwrap_or_else(|e| e.message)
}
pub fn cargo_clean_try(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
) -> Result<String, ToolError> {
let v = crate::cm_tools::tools::parse_args_json(args_json).map_err(ToolError::invalid_args)?;
if !workspace_root.join("Cargo.toml").is_file() {
return Err(ToolError::workspace(
"workspace_no_cargo_toml",
"错误:当前工作目录未找到 Cargo.toml".to_string(),
));
}
let package = v.get("package").and_then(|x| x.as_str()).map(str::trim);
let release = v.get("release").and_then(|x| x.as_bool()).unwrap_or(false);
let doc = v.get("doc").and_then(|x| x.as_bool()).unwrap_or(false);
let dry_run = v.get("dry_run").and_then(|x| x.as_bool()).unwrap_or(true);
let mut cmd = Command::new("cargo");
cmd.arg("clean");
if let Some(p) = package.filter(|s| !s.is_empty()) {
cmd.arg("--package").arg(p);
}
if release {
cmd.arg("--release");
}
if doc {
cmd.arg("--doc");
}
if dry_run {
cmd.arg("--dry-run");
}
cmd.current_dir(workspace_root);
run_and_format_try(cmd, max_output_len, "cargo clean", "cargo_clean", None)
}
pub fn cargo_doc(args_json: &str, workspace_root: &Path, max_output_len: usize) -> String {
cargo_doc_try(args_json, workspace_root, max_output_len).unwrap_or_else(|e| e.message)
}
pub fn cargo_doc_try(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
) -> Result<String, ToolError> {
let v = crate::cm_tools::tools::parse_args_json(args_json).map_err(ToolError::invalid_args)?;
if !workspace_root.join("Cargo.toml").is_file() {
return Err(ToolError::workspace(
"workspace_no_cargo_toml",
"错误:当前工作目录未找到 Cargo.toml".to_string(),
));
}
let package = v.get("package").and_then(|x| x.as_str()).map(str::trim);
let no_deps = v.get("no_deps").and_then(|x| x.as_bool()).unwrap_or(true);
let open = v.get("open").and_then(|x| x.as_bool()).unwrap_or(false);
let mut cmd = Command::new("cargo");
cmd.arg("doc");
if let Some(p) = package.filter(|s| !s.is_empty()) {
cmd.arg("--package").arg(p);
}
if no_deps {
cmd.arg("--no-deps");
}
if open {
cmd.arg("--open");
}
cmd.current_dir(workspace_root);
run_and_format_try(cmd, max_output_len, "cargo doc", "cargo_doc", None)
}
pub fn cargo_nextest(args_json: &str, workspace_root: &Path, max_output_len: usize) -> String {
cargo_nextest_try(args_json, workspace_root, max_output_len, None).unwrap_or_else(|e| e.message)
}
pub fn cargo_nextest_try(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
wall_secs: Option<u64>,
) -> Result<String, ToolError> {
let v = crate::cm_tools::tools::parse_args_json(args_json).map_err(ToolError::invalid_args)?;
if !workspace_root.join("Cargo.toml").is_file() {
return Err(ToolError::workspace(
"workspace_no_cargo_toml",
"错误:当前工作目录未找到 Cargo.toml".to_string(),
));
}
let test_filter = v
.get("test_filter")
.and_then(|x| x.as_str())
.map(str::trim)
.filter(|s| !s.is_empty());
let package = v.get("package").and_then(|x| x.as_str()).map(str::trim);
let profile = v
.get("profile")
.and_then(|x| x.as_str())
.map(str::trim)
.filter(|s| !s.is_empty());
let no_capture = v
.get("nocapture")
.and_then(|x| x.as_bool())
.unwrap_or(false);
let mut cmd = Command::new("cargo");
cmd.arg("nextest").arg("run");
if let Some(p) = package.filter(|s| !s.is_empty()) {
cmd.arg("--package").arg(p);
}
if let Some(p) = profile {
cmd.arg("--profile").arg(p);
}
if let Some(f) = test_filter {
cmd.arg(f);
}
if no_capture {
cmd.arg("--").arg("--nocapture");
}
cmd.current_dir(workspace_root);
let out = run_and_format_try(cmd, max_output_len, "cargo nextest run", "cargo_nextest", wall_secs)?;
if out.contains("no such command: `nextest`") {
return Err(ToolError::invalid_args(
"cargo nextest: 未安装 cargo-nextest,请先运行 `cargo install cargo-nextest`"
.to_string(),
));
}
Ok(out)
}
pub fn cargo_outdated(args_json: &str, workspace_root: &Path, max_output_len: usize) -> String {
cargo_outdated_try(args_json, workspace_root, max_output_len).unwrap_or_else(|e| e.message)
}
pub fn cargo_outdated_try(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
) -> Result<String, ToolError> {
let v = crate::cm_tools::tools::parse_args_json(args_json).map_err(ToolError::invalid_args)?;
if !workspace_root.join("Cargo.toml").is_file() {
return Err(ToolError::workspace(
"workspace_no_cargo_toml",
"错误:当前工作目录未找到 Cargo.toml".to_string(),
));
}
let workspace = v
.get("workspace")
.and_then(|x| x.as_bool())
.unwrap_or(false);
let depth = v.get("depth").and_then(|x| x.as_u64());
let mut cmd = Command::new("cargo");
cmd.arg("outdated");
if workspace {
cmd.arg("--workspace");
}
if let Some(d) = depth {
cmd.arg("--depth").arg(d.to_string());
}
cmd.current_dir(workspace_root);
let out = run_and_format_try(cmd, max_output_len, "cargo outdated", "cargo_outdated", None)?;
if out.contains("no such command: `outdated`") || out.contains("no such command: outdated") {
return Err(ToolError::invalid_args(
"cargo outdated: 未安装 cargo-outdated,请先运行 `cargo install cargo-outdated`"
.to_string(),
));
}
Ok(out)
}
pub fn cargo_machete(args_json: &str, workspace_root: &Path, max_output_len: usize) -> String {
cargo_machete_try(args_json, workspace_root, max_output_len).unwrap_or_else(|e| e.message)
}
fn cargo_machete_resolved_path(
workspace_root: &Path,
path_rel: Option<&str>,
) -> Result<Option<PathBuf>, ToolError> {
let Some(p) = path_rel else {
return Ok(None);
};
if p.is_empty() || p.contains("..") {
return Err(ToolError::invalid_args(
"错误:path 无效(不可为空或含 ..)".to_string(),
));
}
let root_canon = workspace_root
.canonicalize()
.unwrap_or_else(|_| workspace_root.to_path_buf());
let joined = workspace_root.join(p);
match joined.canonicalize() {
Ok(abs) => {
if !abs.starts_with(&root_canon) {
return Err(ToolError::invalid_args(
"错误:path 必须位于工作区内".to_string(),
));
}
Ok(Some(abs))
}
Err(e) => Err(ToolError::invalid_args(format!(
"错误:无法解析 path({})",
e
))),
}
}
pub fn cargo_machete_try(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
) -> Result<String, ToolError> {
let v = crate::cm_tools::tools::parse_args_json(args_json).map_err(ToolError::invalid_args)?;
if !workspace_root.join("Cargo.toml").is_file() {
return Err(ToolError::workspace(
"workspace_no_cargo_toml",
"错误:当前工作目录未找到 Cargo.toml".to_string(),
));
}
let with_metadata = v
.get("with_metadata")
.and_then(|x| x.as_bool())
.unwrap_or(false);
let path_rel = v.get("path").and_then(|x| x.as_str()).map(str::trim);
let resolved = cargo_machete_resolved_path(workspace_root, path_rel)?;
let mut cmd = Command::new("cargo");
cmd.arg("machete");
if with_metadata {
cmd.arg("--with-metadata");
}
if let Some(abs) = resolved {
cmd.arg(abs);
}
cmd.current_dir(workspace_root);
let out = run_and_format_try(cmd, max_output_len, "cargo machete", "cargo_machete", None)?;
if out.contains("no such command: `machete`") || out.contains("no such command: machete") {
return Err(ToolError::invalid_args(
"cargo machete: 未安装 cargo-machete,请先运行 `cargo install cargo-machete`"
.to_string(),
));
}
Ok(out)
}
pub fn cargo_udeps(args_json: &str, workspace_root: &Path, max_output_len: usize) -> String {
cargo_udeps_try(args_json, workspace_root, max_output_len).unwrap_or_else(|e| e.message)
}
pub fn cargo_udeps_try(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
) -> Result<String, ToolError> {
let v = crate::cm_tools::tools::parse_args_json(args_json).map_err(ToolError::invalid_args)?;
if !workspace_root.join("Cargo.toml").is_file() {
return Err(ToolError::workspace(
"workspace_no_cargo_toml",
"错误:当前工作目录未找到 Cargo.toml".to_string(),
));
}
let nightly = v.get("nightly").and_then(|x| x.as_bool()).unwrap_or(false);
let mut cmd = Command::new("cargo");
if nightly {
cmd.arg("+nightly");
}
cmd.arg("udeps");
cmd.current_dir(workspace_root);
let out = run_and_format_try(cmd, max_output_len, "cargo udeps", "cargo_udeps", None)?;
if out.contains("no such command: `udeps`") || out.contains("no such command: udeps") {
return Err(ToolError::invalid_args(
"cargo udeps: 未安装 cargo-udeps,请先运行 `cargo install cargo-udeps`(运行期通常需 nightly,可传 nightly: true)"
.to_string(),
));
}
Ok(out)
}
pub fn cargo_publish_dry_run(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
) -> String {
cargo_publish_dry_run_try(args_json, workspace_root, max_output_len)
.unwrap_or_else(|e| e.message)
}
pub fn cargo_publish_dry_run_try(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
) -> Result<String, ToolError> {
let v = crate::cm_tools::tools::parse_args_json(args_json).map_err(ToolError::invalid_args)?;
if !workspace_root.join("Cargo.toml").is_file() {
return Err(ToolError::workspace(
"workspace_no_cargo_toml",
"错误:当前工作目录未找到 Cargo.toml".to_string(),
));
}
let package = v
.get("package")
.and_then(|x| x.as_str())
.map(str::trim)
.filter(|s| !s.is_empty());
let allow_dirty = v
.get("allow_dirty")
.and_then(|x| x.as_bool())
.unwrap_or(false);
let no_verify = v
.get("no_verify")
.and_then(|x| x.as_bool())
.unwrap_or(false);
let features = v
.get("features")
.and_then(|x| x.as_str())
.map(str::trim)
.filter(|s| !s.is_empty());
let all_features = v
.get("all_features")
.and_then(|x| x.as_bool())
.unwrap_or(false);
let mut cmd = Command::new("cargo");
cmd.arg("publish").arg("--dry-run");
if let Some(p) = package {
cmd.arg("--package").arg(p);
}
if allow_dirty {
cmd.arg("--allow-dirty");
}
if no_verify {
cmd.arg("--no-verify");
}
if let Some(f) = features {
cmd.arg("--features").arg(f);
}
if all_features {
cmd.arg("--all-features");
}
cmd.current_dir(workspace_root);
run_and_format_try(
cmd,
max_output_len,
"cargo publish --dry-run",
"cargo_publish_dry_run",
None,
)
}
pub fn cargo_fix(args_json: &str, workspace_root: &Path, max_output_len: usize) -> String {
cargo_fix_try(args_json, workspace_root, max_output_len).unwrap_or_else(|e| e.message)
}
pub fn cargo_fix_try(
args_json: &str,
workspace_root: &Path,
max_output_len: usize,
) -> Result<String, ToolError> {
let v = crate::cm_tools::tools::parse_args_json(args_json).map_err(ToolError::invalid_args)?;
if !workspace_root.join("Cargo.toml").is_file() {
return Err(ToolError::workspace(
"workspace_no_cargo_toml",
"错误:当前工作目录未找到 Cargo.toml".to_string(),
));
}
let confirm = v.get("confirm").and_then(|x| x.as_bool()).unwrap_or(false);
if !confirm {
return Err(ToolError::invalid_args(
"拒绝执行:cargo_fix 需要 confirm=true 才会真正应用修复(避免误改代码)。".to_string(),
));
}
let mut cmd = Command::new("cargo");
cmd.arg("fix");
push_cargo_fix_flags(&mut cmd, &v);
cmd.current_dir(workspace_root);
run_and_format_try(cmd, max_output_len, "cargo fix", "cargo_fix", None)
}
const CARGO_FIX_BOOL_FLAGS: &[(&str, &str)] = &[
("broken_code", "--broken-code"),
("all_targets", "--all-targets"),
("all_features", "--all-features"),
("edition_idioms", "--edition-idioms"),
("allow_dirty", "--allow-dirty"),
("allow_staged", "--allow-staged"),
("allow_no_vcs", "--allow-no-vcs"),
];
fn push_cargo_fix_flags(cmd: &mut Command, v: &serde_json::Value) {
for (key, flag) in CARGO_FIX_BOOL_FLAGS {
if v.get(*key).and_then(|x| x.as_bool()).unwrap_or(false) {
cmd.arg(*flag);
}
}
for (key, flag) in [("package", "--package"), ("features", "--features"), ("edition", "--edition")] {
if let Some(s) = v
.get(key)
.and_then(|x| x.as_str())
.map(str::trim)
.filter(|s| !s.is_empty())
{
cmd.arg(flag).arg(s);
}
}
}
#[path = "cargo_subcommand.rs"]
mod cargo_subcommand;
use cargo_subcommand::{
run_and_format_try, run_cargo_subcommand_str_try, run_cargo_subcommand_value_try,
};
#[path = "cargo_tools_run_tests.rs"]
mod tests;