use clap::Parser;
use std::fs;
use std::path::Path;
use std::process::Command;
use toml_edit::value;
#[derive(Debug)]
enum MsvcError {
Io(std::io::Error),
TomlParse(String),
CommandFailed(String),
InvalidProject(String),
}
impl std::fmt::Display for MsvcError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
MsvcError::Io(err) => write!(f, "IO error: {}", err),
MsvcError::TomlParse(err) => write!(f, "TOML parse error: {}", err),
MsvcError::CommandFailed(msg) => write!(f, "Command failed: {}", msg),
MsvcError::InvalidProject(msg) => write!(f, "Invalid project: {}", msg),
}
}
}
impl std::error::Error for MsvcError {}
impl From<std::io::Error> for MsvcError {
fn from(err: std::io::Error) -> Self {
MsvcError::Io(err)
}
}
type Result<T> = std::result::Result<T, MsvcError>;
fn validate_project() -> Result<()> {
let cargo_file = Path::new("Cargo.toml");
if !cargo_file.exists() {
return Err(MsvcError::InvalidProject(
"Not in a Cargo project directory. Please run this command from a directory containing Cargo.toml".to_string()
));
}
Ok(())
}
fn load_cargo_toml() -> Result<toml_edit::DocumentMut> {
let cargo_file = Path::new("Cargo.toml");
let content = fs::read_to_string(cargo_file)?;
let doc = content.parse::<toml_edit::DocumentMut>()
.map_err(|e| MsvcError::TomlParse(e.to_string()))?;
Ok(doc)
}
fn has_dependency(doc: &toml_edit::DocumentMut, section: &str, dep_name: &str) -> bool {
doc.get(section)
.and_then(|item| item.as_table())
.map(|table| table.contains_key(dep_name))
.unwrap_or(false)
}
fn run_cargo_command(args: &[&str]) -> Result<bool> {
let mut child = Command::new("cargo")
.args(args)
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.spawn()
.map_err(|e| MsvcError::CommandFailed(format!("Failed to execute cargo command: {}", e)))?;
let status = child
.wait()
.map_err(|e| MsvcError::CommandFailed(format!("Failed to wait for cargo command: {}", e)))?;
Ok(status.success())
}
#[derive(Parser, Debug)]
#[command(
name = "msvc",
author = "Baoge <baoge@live.cn>",
version,
about = "A tool to automate setup for MSVC projects with vc-ltl and thunk-rs",
long_about = "msvc is a command-line tool that helps automate the setup of MSVC (Microsoft Visual C++) projects in Rust. It can manage vc-ltl dependencies, thunk-rs integration, and target configurations."
)]
struct Args {
#[arg(short, long)]
thunk: bool,
#[arg(short = 'x', long = "x86-64")]
x86_64: bool,
#[arg(short = 'a', long = "add-all")]
add_all: bool,
#[arg(short = 'r', long = "remove-all")]
remove_all: bool,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(clap::Subcommand, Debug)]
enum Commands {
#[command(alias = "b")]
Build {
#[arg(short = 'r', long)]
release: bool,
},
}
fn main() -> Result<()> {
let args = Args::parse();
validate_project()?;
println!("Working on project directory: {:?}", std::env::current_dir()?);
if let Some(command) = args.command {
match command {
Commands::Build { release } => {
let mut build_args = vec!["build", "--target", "x86_64-pc-windows-msvc"];
if release {
build_args.push("--release");
}
if !run_cargo_command(&build_args)? {
return Err(MsvcError::CommandFailed("Build failed".to_string()));
}
println!("✓ Build completed successfully with MSVC target");
return Ok(());
}
}
}
if args.remove_all {
remove_all()?;
return Ok(());
}
if args.add_all {
add_all()?;
return Ok(());
}
let doc = load_cargo_toml()?;
let has_vc_ltl = has_dependency(&doc, "dependencies", "vc-ltl");
let has_thunk = has_dependency(&doc, "build-dependencies", "thunk-rs");
if !args.thunk && !args.x86_64 {
if has_vc_ltl {
remove_vc_ltl()?;
} else {
add_vc_ltl()?;
}
}
if args.thunk {
if has_thunk {
remove_thunk_rs()?;
remove_build_script()?;
} else {
add_thunk_rs()?;
add_build_script()?;
}
}
if args.x86_64 {
toggle_target_config()?;
}
Ok(())
}
fn remove_vc_ltl() -> Result<()> {
if run_cargo_command(&["remove", "vc-ltl"])? {
println!("✓ Removed vc-ltl dependency");
} else {
println!("⚠ vc-ltl not found in dependencies");
}
Ok(())
}
fn add_vc_ltl() -> Result<()> {
if run_cargo_command(&["add", "vc-ltl"])? {
println!("✓ Added vc-ltl dependency");
} else {
println!("✗ Failed to add vc-ltl dependency");
}
Ok(())
}
fn remove_thunk_rs() -> Result<()> {
if run_cargo_command(&["remove", "--build", "thunk-rs"])? {
println!("✓ Removed thunk-rs build dependency");
} else {
println!("⚠ thunk-rs not found in build dependencies");
}
Ok(())
}
fn add_thunk_rs() -> Result<()> {
if run_cargo_command(&["add", "--build", "thunk-rs"])? {
println!("✓ Added thunk-rs build dependency");
} else {
println!("✗ Failed to add thunk-rs build dependency");
}
Ok(())
}
fn add_all() -> Result<()> {
add_vc_ltl()?;
add_thunk_rs()?;
add_target_config()?;
add_build_script()?;
println!("✓ Added all MSVC configurations");
Ok(())
}
fn remove_all() -> Result<()> {
remove_vc_ltl()?;
remove_thunk_rs()?;
remove_target_config()?;
remove_build_script()?;
println!("✓ Removed all MSVC configurations");
Ok(())
}
fn add_build_script() -> Result<()> {
let build_rs_path = Path::new("build.rs");
const BUILD_RS_CONTENT: &str = r#"// This file is automatically generated/managed by cargo-msvc.
fn main() {
if std::env::var("CARGO_CFG_TARGET_ENV").map_or(false, |env| env == "msvc") {
thunk::thunk();
}
}
"#;
if !build_rs_path.exists() {
fs::write(build_rs_path, BUILD_RS_CONTENT)?;
println!("✓ Created build.rs file with thunk-rs integration");
return Ok(());
}
let content = fs::read_to_string(&build_rs_path)?;
if content.contains("thunk::thunk();") {
println!("⚠ thunk-rs integration already exists in build.rs");
return Ok(());
}
if content.contains("// This file is automatically generated/managed by cargo-msvc.") {
fs::write(build_rs_path, BUILD_RS_CONTENT)?;
} else {
if let Some(last_brace_pos) = content.rfind('}') {
let new_content = format!(
"{} if std::env::var(\"CARGO_CFG_TARGET_ENV\").map_or(false, |env| env == \"msvc\") {{\n thunk::thunk();\n }}\n{}",
&content[..last_brace_pos],
&content[last_brace_pos..]
);
fs::write(build_rs_path, new_content)?;
} else {
println!("⚠ Could not find suitable location to add thunk-rs integration");
return Ok(());
}
}
println!("✓ Added thunk-rs integration to build.rs");
Ok(())
}
fn remove_build_script() -> Result<()> {
let build_rs_path = Path::new("build.rs");
if !build_rs_path.exists() {
println!("⚠ build.rs file not found");
return Ok(());
}
let content = fs::read_to_string(&build_rs_path)?;
if !content.contains("thunk::thunk();") {
println!("⚠ No thunk-rs integration found in build.rs");
return Ok(());
}
let new_content = if content.contains("// This file is automatically generated/managed by cargo-msvc.") {
"fn main() {}\n".to_string()
} else {
content.replace(
" if std::env::var(\"CARGO_CFG_TARGET_ENV\").map_or(false, |env| env == \"msvc\") {\n thunk::thunk();\n }\n",
""
)
};
fs::write(build_rs_path, new_content)?;
println!("✓ Removed thunk-rs integration from build.rs");
Ok(())
}
fn add_target_config() -> Result<()> {
let config_toml_path = Path::new(".cargo/config.toml");
if let Some(parent) = config_toml_path.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
let mut doc = if config_toml_path.exists() {
let content = fs::read_to_string(&config_toml_path)?;
if content.trim().is_empty() {
toml_edit::DocumentMut::new()
} else {
content.parse::<toml_edit::DocumentMut>()
.map_err(|e| MsvcError::TomlParse(e.to_string()))?
}
} else {
toml_edit::DocumentMut::new()
};
let build_table = doc["build"].or_insert(toml_edit::table());
if let Some(table) = build_table.as_table_mut() {
table["target"] = value("x86_64-pc-windows-msvc");
}
fs::write(config_toml_path, doc.to_string())?;
println!("✓ Added target configuration to .cargo/config.toml");
Ok(())
}
fn toggle_target_config() -> Result<()> {
let config_toml_path = Path::new(".cargo/config.toml");
if config_toml_path.exists() {
let content = fs::read_to_string(&config_toml_path)?;
let doc = if content.trim().is_empty() {
toml_edit::DocumentMut::new()
} else {
content.parse::<toml_edit::DocumentMut>()
.map_err(|e| MsvcError::TomlParse(e.to_string()))?
};
let has_target = doc
.get("build")
.and_then(|item| item.as_table())
.and_then(|table| table.get("target"))
.and_then(|target| target.as_str())
.map(|target_str| target_str == "x86_64-pc-windows-msvc")
.unwrap_or(false);
if has_target {
remove_target_config()?;
} else {
add_target_config()?;
}
} else {
println!("⚠ Config file not found, creating one");
add_target_config()?;
}
Ok(())
}
fn remove_target_config() -> Result<()> {
let config_toml_path = Path::new(".cargo/config.toml");
if !config_toml_path.exists() {
return Ok(());
}
let content = fs::read_to_string(&config_toml_path)?;
let mut doc = if content.trim().is_empty() {
toml_edit::DocumentMut::new()
} else {
content.parse::<toml_edit::DocumentMut>()
.map_err(|e| MsvcError::TomlParse(e.to_string()))?
};
if let Some(build_table) = doc.get_mut("build").and_then(|item| item.as_table_mut()) {
if build_table.contains_key("target") {
build_table.remove("target");
if build_table.is_empty() {
doc.remove("build");
}
}
}
if doc.to_string().trim().is_empty() {
fs::remove_file(config_toml_path)?;
println!("✓ Removed .cargo/config.toml file");
} else {
fs::write(config_toml_path, doc.to_string())?;
println!("✓ Removed target configuration from .cargo/config.toml");
}
Ok(())
}