diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-07-10 16:54:57 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-07-10 16:54:57 +0800 |
| commit | 5d258482fb84d58c966a7ccc1b467708278d28ad (patch) | |
| tree | 1a0c9d2e245d43ff7bc11d9088cf88a5264d293f /dev_tools/src/verify.rs | |
| parent | a8c8d50b6c59b103da7fd4bbb5e7ca3ff7b2f49a (diff) | |
refactor(devtools): rename dev_tools to .run and replace run-tools
scripts
Replace the old run-tools.ps1/run-tools.sh scripts with a new
unified run.ps1/run.sh launcher that supports multiple languages
(PowerShell, Rust, Python, Go, C#, Nim, Perl, Ruby, Zig, and
arbitrary binaries) from the `.run/src/bin` directory.
Move all development tools from `dev_tools/` to `.run/`, update
Cargo workspace config and relevant documentation references.
Diffstat (limited to 'dev_tools/src/verify.rs')
| -rw-r--r-- | dev_tools/src/verify.rs | 511 |
1 files changed, 0 insertions, 511 deletions
diff --git a/dev_tools/src/verify.rs b/dev_tools/src/verify.rs deleted file mode 100644 index 2ddff0c..0000000 --- a/dev_tools/src/verify.rs +++ /dev/null @@ -1,511 +0,0 @@ -use std::path::Path; - -use crate::println_cargo_style; - -/// Represents a parsed code block from a markdown file -#[derive(Debug, Clone)] -pub struct CodeBlock { - /// Source file path (for reporting) - pub source_file: String, - /// The line number in source file where this block starts - pub line: usize, - /// The raw Rust source code - pub code: String, - /// Feature flags extracted from `// Features: [...]` comment - pub features: Vec<String>, - /// Whether the block had an explicit `// Features:` header - pub has_features_header: bool, - /// Whether the block has `// NOT VERIFIED` to opt out of testing - pub not_verified: bool, - /// External dependencies extracted from `// Dependencies:` comments - pub external_deps: Vec<(String, String)>, - /// Whether this block has a `fn main` entry point - pub has_main: bool, - /// Whether this block has `gen_program!()` call - pub has_gen_program: bool, - /// Whether this block has `// BUILD TIME` annotation (write to build.rs, not main.rs) - pub is_build_time: bool, -} - -/// Parse all ```rust code blocks from markdown content -pub fn parse_code_blocks(content: &str, source_file: &str) -> Vec<CodeBlock> { - let mut blocks = Vec::new(); - let lines: Vec<&str> = content.lines().collect(); - let mut i = 0; - - while i < lines.len() { - if lines[i].trim() == "```rust" { - if let Some(block) = parse_single_block(&lines, i, source_file) { - blocks.push(block); - } - i += 1; - while i < lines.len() && lines[i].trim() != "```" { - i += 1; - } - } - i += 1; - } - - blocks -} - -/// Parse a single code block starting at the ```rust line -fn parse_single_block(lines: &[&str], start: usize, source_file: &str) -> Option<CodeBlock> { - let line_num = start + 1; // 1-based line number - - let mut code_lines: Vec<String> = Vec::new(); - let mut features: Vec<String> = Vec::new(); - let mut has_features_header = false; - let mut not_verified = false; - let mut external_deps: Vec<(String, String)> = Vec::new(); - let mut has_main = false; - let mut has_gen_program = false; - let mut is_build_time = false; - - let mut idx = start + 1; - let mut in_header = true; - - while idx < lines.len() { - let raw_line = lines[idx]; - let trimmed = raw_line.trim(); - - if trimmed == "```" { - break; - } - - // @@@ lines: strip the prefix and treat as regular Rust code - // These lines are hidden in the rendered docs (filtered by a docsify plugin) - // but must still compile. - if trimmed.starts_with("@@@") { - in_header = false; - // Strip @@@ and optionally one following space - let code = trimmed[3..].trim_start(); - if code.contains("fn main") { - has_main = true; - } - if code.contains("gen_program!") { - has_gen_program = true; - } - code_lines.push(code.to_string()); - idx += 1; - continue; - } - - // Parse header comments - // Check for NOT VERIFIED marker - if in_header && trimmed == "// NOT VERIFIED" { - not_verified = true; - idx += 1; - continue; - } - - if in_header && trimmed == "// BUILD TIME" { - is_build_time = true; - idx += 1; - continue; - } - - if in_header && trimmed.starts_with("// ") { - if trimmed.starts_with("// Features:") { - has_features_header = true; - let feat_str = trimmed.trim_start_matches("// Features:").trim(); - if feat_str.starts_with('[') && feat_str.ends_with(']') { - let inner = &feat_str[1..feat_str.len() - 1]; - if !inner.is_empty() { - features = inner - .split(',') - .map(|s| s.trim().trim_matches('"').to_string()) - .filter(|s| !s.is_empty()) - .collect(); - } - } - idx += 1; - continue; - } - if trimmed == "// Dependencies:" { - idx += 1; - // Collect subsequent `// crate = "version"` lines - while idx < lines.len() { - let next = lines[idx].trim(); - if next == "```" { - break; - } - if next.starts_with("// ") { - let dep_line = next.trim_start_matches("// ").trim(); - if let Some((name, ver)) = dep_line.split_once(" = ") { - external_deps.push(( - name.trim().to_string(), - ver.trim().trim_matches('"').to_string(), - )); - } - idx += 1; - } else { - break; - } - } - continue; - } - } - - in_header = false; - - if raw_line.contains("fn main") { - has_main = true; - } - if raw_line.contains("gen_program!") { - has_gen_program = true; - } - - code_lines.push(raw_line.to_string()); - idx += 1; - } - - if code_lines.is_empty() { - return None; - } - - Some(CodeBlock { - source_file: source_file.to_string(), - line: line_num, - code: code_lines.join("\n"), - features, - has_features_header, - not_verified, - external_deps, - has_main, - has_gen_program, - is_build_time, - }) -} - -/// Generate a Cargo.toml for a block -/// -/// `manifest_path` is the full path to the Cargo.toml file being written; it is used to -/// compute the relative path to the `mingling` crate. -pub fn generate_cargo_toml(block: &CodeBlock, package_name: &str, manifest_path: &Path) -> String { - let features_str = if !block.features.is_empty() { - let feats: Vec<String> = block.features.iter().map(|f| format!("\"{f}\"")).collect(); - format!("features = [{}]", feats.join(", ")) - } else { - String::new() - }; - - let mut extra_deps = String::new(); - for (name, version) in &block.external_deps { - if !version.starts_with('{') { - if name == "serde" || name == "clap" { - extra_deps.push_str(&format!( - "{name} = {{ version = \"{version}\", features = [\"derive\"] }}\n" - )); - } else { - extra_deps.push_str(&format!("{name} = \"{version}\"\n")); - } - } else { - extra_deps.push_str(&format!("{name} = {version}\n")); - } - } - - let mingling_path = find_mingling_relative_path(manifest_path); - - let deps_section = if features_str.is_empty() { - format!("[dependencies]\nmingling = {{ path = \"{mingling_path}\" }}\n{extra_deps}",) - } else { - format!( - "[dependencies]\nmingling = {{ path = \"{mingling_path}\", {features_str} }}\n{extra_deps}", - ) - }; - - // Build-time blocks: add `builds` by default, merge with explicit features - let build_deps_section = if block.is_build_time { - let mut all_feats = vec!["builds".to_string()]; - for f in &block.features { - if f != "builds" { - all_feats.push(f.clone()); - } - } - let feats_str: Vec<String> = all_feats.iter().map(|f| format!("\"{f}\"")).collect(); - let build_feats = format!("features = [{}]", feats_str.join(", ")); - format!( - "\n[build-dependencies]\nmingling = {{ path = \"{mingling_path}\", {build_feats} }}\n" - ) - } else { - String::new() - }; - - format!( - r#"[package] - name = "{package_name}" - version = "0.0.0" - edition = "2024" - -{deps_section}{build_deps_section} -[workspace] -"# - ) -} - -/// Compute the relative path from a Cargo.toml's parent directory to the `mingling` crate. -/// -/// The process current directory is expected to be the project root (where `mingling/` lives). -/// Returns a forward-slash path safe for embedding in TOML strings. -fn find_mingling_relative_path(manifest_path: &Path) -> String { - let manifest_dir = manifest_path - .parent() - .expect("manifest_path has no parent directory"); - let cwd = std::env::current_dir().expect("failed to get current directory"); - - // Strip cwd prefix to get the relative components of the manifest directory - let relative_to_root = manifest_dir.strip_prefix(&cwd).unwrap_or(manifest_dir); - let depth = relative_to_root.components().count(); - - let mut result = String::new(); - for _ in 0..depth { - result.push_str("../"); - } - result.push_str("mingling"); - result -} - -/// Generate main.rs for a block -/// -/// Automatically prepends `use mingling::prelude::*;` if the block doesn't already have it. -pub fn generate_main_rs(block: &CodeBlock) -> String { - let mut output = String::from("#![allow(dead_code)]\n#![allow(unused)]\n"); - - if !block.code.contains("use mingling::prelude::*;") { - output.push_str("#[allow(unused_imports)]\nuse mingling::prelude::*;\n\n"); - } - - output.push_str(&block.code); - output.push('\n'); - - if !block.has_main { - output.push_str("\nfn main() {}\n"); - } - - if !block.has_gen_program { - output.push_str("\nmingling::macros::gen_program!();\n"); - } - - output -} - -/// Generate build.rs for a build-time block -/// -/// Default: `use mingling::builds::*;`, code wrapped in `fn main() { }`. -pub fn generate_build_rs(block: &CodeBlock) -> String { - let mut output = String::from("#![allow(dead_code)]\n#![allow(unused)]\n"); - - if !block.code.contains("use mingling::build::*;") { - output.push_str("#[allow(unused_imports)]\nuse mingling::build::*;\n\n"); - } - - if block.has_main { - output.push_str(&block.code); - } else { - output.push_str("fn main() {\n"); - for line in block.code.lines() { - output.push_str(" "); - output.push_str(line); - output.push('\n'); - } - output.push_str("}\n"); - } - - output -} - -/// Build a single code block as a Cargo project. -/// -/// When `is_build_time` is true, `src_content` is written to `build.rs` instead of `src/main.rs`, -/// and a minimal `src/main.rs` stub (`fn main() {}`) is created. -pub fn build_block( - src_dir: &Path, - manifest_path: &Path, - cargo_toml: &str, - src_content: &str, - is_build_time: bool, -) -> (bool, String) { - if let Err(e) = std::fs::create_dir_all(src_dir) { - return (false, format!("mkdir: {e}")); - } - - // Write Cargo.toml - if let Err(e) = std::fs::write(manifest_path, cargo_toml) { - return (false, format!("write Cargo.toml: {e}")); - } - - if is_build_time { - // Write build.rs and a stub main.rs - let crate_dir = manifest_path.parent().unwrap(); - if let Err(e) = std::fs::write(crate_dir.join("build.rs"), src_content) { - return (false, format!("write build.rs: {e}")); - } - if let Err(e) = std::fs::write(src_dir.join("main.rs"), "fn main() {}\n") { - return (false, format!("write main.rs: {e}")); - } - } else { - // Normal: write src/main.rs - if let Err(e) = std::fs::write(src_dir.join("main.rs"), src_content) { - return (false, format!("write main.rs: {e}")); - } - } - - // Check code — inherit stderr so cargo output is real-time and colored - let shell = if cfg!(target_os = "windows") { - "powershell" - } else { - "sh" - }; - let cmd = format!( - "cargo check --color=always --manifest-path {}", - manifest_path.to_string_lossy() - ); - - let mut child = match std::process::Command::new(shell) - .arg("-c") - .arg(&cmd) - .stdout(std::process::Stdio::inherit()) - .stderr(std::process::Stdio::piped()) - .current_dir(std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))) - .spawn() - { - Ok(c) => c, - Err(e) => return (false, format!("spawn: {e}")), - }; - - // Read stderr (buffered, not forwarded — groups print their own output contiguously) - use std::io::BufRead; - let stderr_handle = child.stderr.take().unwrap(); - let reader = std::io::BufReader::new(stderr_handle); - let mut captured = String::new(); - for line in reader.lines() { - match line { - Ok(l) => { - captured.push_str(&l); - captured.push('\n'); - } - Err(_) => break, - } - } - - let status = child.wait().unwrap_or_else(|_| std::process::exit(1)); - let exit_code = status.code().unwrap_or(1); - - if exit_code == 0 { - (true, String::new()) - } else { - let mut last_lines: Vec<&str> = captured.lines().rev().take(20).collect(); - last_lines.reverse(); - let detail = last_lines.join("\n"); - (false, format!("exit code {exit_code}\n{detail}")) - } -} - -/// Compute a stable hash for a code block based on its dependency configuration. -/// -/// Blocks with the same features and external dependencies produce the same hash, -/// allowing them to share a compiled crate and avoid redundant recompilation. -/// -/// Hash input (all sorted for stability): -/// - Sorted mingling feature strings -/// - Sorted external dependency names -/// - Sorted external dependency versions -/// - Sorted external deps as `name=version` pairs -pub fn compute_block_hash(block: &CodeBlock) -> String { - let mut features: Vec<&str> = block.features.iter().map(|s| s.as_str()).collect(); - features.sort(); - let features_str = features.join(","); - - let mut dep_names: Vec<&str> = block - .external_deps - .iter() - .map(|(n, _)| n.as_str()) - .collect(); - dep_names.sort(); - let dep_names_str = dep_names.join(","); - - let mut dep_versions: Vec<&str> = block - .external_deps - .iter() - .map(|(_, v)| v.as_str()) - .collect(); - dep_versions.sort(); - let dep_versions_str = dep_versions.join(","); - - let mut deps: Vec<String> = block - .external_deps - .iter() - .map(|(n, v)| format!("{n}={v}")) - .collect(); - deps.sort(); - let deps_str = deps.join(","); - - let canonical = format!("{features_str}\n{dep_names_str}\n{dep_versions_str}\n{deps_str}"); - - // FNV-1a 64-bit hash — stable across runs (no random seed) - let mut hash: u64 = 0xcbf29ce484222325; - for &byte in canonical.as_bytes() { - hash ^= byte as u64; - hash = hash.wrapping_mul(0x100000001b3); - } - - format!("{:016x}", hash) -} - -/// Determine if a block should be treated as a test candidate. -/// A block is NOT testable only if it has `// NOT VERIFIED` marker. -pub fn is_block_testable(block: &CodeBlock) -> bool { - !block.not_verified -} - -/// Write a summary report -pub fn write_summary_report( - path: &Path, - title: &str, - results: &[(String, usize, bool, String)], - total: usize, - passed: usize, - failed: usize, -) { - let mut content = String::new(); - content.push_str(&format!("# {title}\n\n")); - content.push_str(&format!( - "Tested **{total}** code blocks: **{passed}** passed, **{failed}** failed.\n\n" - )); - content.push_str("## Results\n\n"); - content.push_str("| Block | File | Line | Status |\n"); - content.push_str("|-------|------|------|--------|\n"); - - for (i, (file, line, ok, _)) in results.iter().enumerate() { - let status = if *ok { "PASS" } else { "FAIL" }; - let short_file = file.rsplit('/').next().unwrap_or(file); - content.push_str(&format!( - "| {} | {} | {} | {status} |\n", - i + 1, - short_file, - line - )); - } - - let has_failures = results.iter().any(|(_, _, ok, _)| !ok); - if has_failures { - content.push_str("\n## Failed Blocks\n\n"); - for (i, (file, line, ok, err)) in results.iter().enumerate() { - if !ok { - content.push_str(&format!( - "### Block {} (`{}`, line {})\n\n```\n{err}\n```\n\n", - i + 1, - file, - line - )); - } - } - } - - std::fs::write(path, &content).unwrap_or_else(|e| { - eprintln!("Warning: failed to write {path:?}: {e}"); - }); - - println_cargo_style!("Report: written to {}", path.display()); -} |
