use std::path::PathBuf;
use clap::{CommandFactory, Parser, Subcommand};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use msvc_kit::bundle::{generate_bundle_scripts, save_bundle_scripts, BundleLayout};
use msvc_kit::env::generate_activation_script;
use msvc_kit::version::{list_installed_msvc, list_installed_sdk, Architecture};
use msvc_kit::{
download_msvc, download_sdk, generate_script, get_env_vars, load_config, save_config,
setup_environment, DownloadOptions, MsvcComponent, MsvcKitConfig, ScriptContext, ShellType,
};
#[derive(Parser)]
#[command(name = "msvc-kit")]
#[command(author = "loonghao <hal.long@outlook.com>")]
#[command(version)]
#[command(about = "Download and manage MSVC compiler and Windows SDK", long_about = None)]
struct Cli {
#[arg(short, long, global = true)]
verbose: bool,
#[arg(short, long, global = true)]
config: Option<PathBuf>,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
Download {
#[arg(long)]
msvc_version: Option<String>,
#[arg(long)]
sdk_version: Option<String>,
#[arg(short, long)]
target: Option<PathBuf>,
#[arg(short, long, default_value = "x64")]
arch: String,
#[arg(long)]
no_msvc: bool,
#[arg(long)]
no_sdk: bool,
#[arg(long)]
no_verify: bool,
#[arg(long)]
parallel_downloads: Option<usize>,
#[arg(long = "include-component", value_name = "COMPONENT")]
include_components: Vec<String>,
#[arg(long = "exclude-pattern", value_name = "PATTERN")]
exclude_patterns: Vec<String>,
},
Setup {
#[arg(short, long)]
dir: Option<PathBuf>,
#[arg(short, long, default_value = "x64")]
arch: String,
#[arg(long)]
script: bool,
#[arg(long, default_value = "powershell")]
shell: String,
#[arg(long, requires = "script", value_name = "PORTABLE_ROOT")]
portable_root: Option<String>,
#[arg(long)]
persistent: bool,
},
List {
#[arg(short, long)]
dir: Option<PathBuf>,
#[arg(long)]
available: bool,
},
Clean {
#[arg(short, long)]
dir: Option<PathBuf>,
#[arg(long)]
msvc_version: Option<String>,
#[arg(long)]
sdk_version: Option<String>,
#[arg(long)]
all: bool,
#[arg(long)]
cache: bool,
},
Config {
#[arg(long)]
set_dir: Option<PathBuf>,
#[arg(long)]
set_msvc: Option<String>,
#[arg(long)]
set_sdk: Option<String>,
#[arg(long)]
reset: bool,
},
Env {
#[arg(short, long)]
dir: Option<PathBuf>,
#[arg(short, long, default_value = "shell")]
format: String,
},
Bundle {
#[arg(short, long, default_value = "./msvc-bundle")]
output: PathBuf,
#[arg(short, long, default_value = "x64")]
arch: String,
#[arg(long)]
host_arch: Option<String>,
#[arg(long)]
msvc_version: Option<String>,
#[arg(long)]
sdk_version: Option<String>,
#[arg(long)]
accept_license: bool,
#[arg(long)]
zip: bool,
},
#[cfg(feature = "self-update")]
Update {
#[arg(long)]
check: bool,
#[arg(long)]
version: Option<String>,
},
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let filter = if cli.verbose {
EnvFilter::new("debug")
} else {
EnvFilter::new("info")
};
tracing_subscriber::registry()
.with(fmt::layer())
.with(filter)
.init();
let mut config = load_config().unwrap_or_default();
let command = match cli.command {
Some(cmd) => cmd,
None => {
Cli::command().print_help().unwrap();
return Ok(());
}
};
match command {
Commands::Download {
msvc_version,
sdk_version,
target,
arch,
no_msvc,
no_sdk,
no_verify,
parallel_downloads,
include_components,
exclude_patterns,
} => {
let target_dir = target.unwrap_or_else(|| config.install_dir.clone());
let arch: Architecture = arch.parse().map_err(|e: String| anyhow::anyhow!(e))?;
let components = include_components
.iter()
.filter_map(|s| {
s.parse::<MsvcComponent>()
.map_err(|e| eprintln!("⚠️ Warning: {}", e))
.ok()
})
.collect();
let options = DownloadOptions {
msvc_version,
sdk_version,
target_dir: target_dir.clone(),
arch,
host_arch: Some(Architecture::host()),
verify_hashes: !no_verify,
parallel_downloads: parallel_downloads.unwrap_or(config.parallel_downloads),
http_client: None,
progress_handler: None,
cache_manager: None,
dry_run: false,
include_components: components,
exclude_patterns,
};
println!("📦 msvc-kit - Downloading MSVC Build Tools\n");
println!("Target directory: {}", target_dir.display());
println!("Architecture: {}", arch);
println!();
if !no_msvc {
println!("⬇️ Downloading MSVC compiler...");
let mut msvc_info = download_msvc(&options).await?;
println!("📁 Extracting MSVC packages...");
msvc_kit::extract_and_finalize_msvc(&mut msvc_info).await?;
println!(
"✅ MSVC {} installed to {}",
msvc_info.version,
target_dir.display()
);
}
if !no_sdk {
println!("\n⬇️ Downloading Windows SDK...");
let sdk_info = download_sdk(&options).await?;
println!("📁 Extracting SDK packages...");
msvc_kit::extract_and_finalize_sdk(&sdk_info).await?;
println!(
"✅ Windows SDK {} installed to {}",
sdk_info.version,
target_dir.display()
);
}
println!("\n🎉 Download complete!");
println!("\nRun 'msvc-kit setup' to configure environment variables.");
}
Commands::Setup {
dir,
arch,
script,
shell,
portable_root,
persistent,
} => {
let install_dir = dir.unwrap_or_else(|| config.install_dir.clone());
let arch: Architecture = arch.parse().map_err(|e: String| anyhow::anyhow!(e))?;
let msvc_versions = list_installed_msvc(&install_dir);
let sdk_versions = list_installed_sdk(&install_dir);
if msvc_versions.is_empty() {
anyhow::bail!("No MSVC installation found. Run 'msvc-kit download' first.");
}
let msvc_version = &msvc_versions[0];
let sdk_version = sdk_versions.first();
let msvc_info = msvc_kit::installer::InstallInfo {
component_type: "msvc".to_string(),
version: msvc_version.version.clone(),
install_path: msvc_version.install_path.clone().unwrap(),
downloaded_files: vec![],
arch,
};
let sdk_info = sdk_version.map(|v| msvc_kit::installer::InstallInfo {
component_type: "sdk".to_string(),
version: v.version.clone(),
install_path: v.install_path.clone().unwrap(),
downloaded_files: vec![],
arch,
});
let env = setup_environment(&msvc_info, sdk_info.as_ref())?;
if script {
let shell_type = match shell.to_lowercase().as_str() {
"cmd" | "bat" => ShellType::Cmd,
"powershell" | "ps1" | "pwsh" => ShellType::PowerShell,
"bash" | "sh" => ShellType::Bash,
_ => ShellType::detect(),
};
let ctx = if let Some(ref _portable_root) = portable_root {
ScriptContext::portable(
&env.vc_tools_version,
&env.windows_sdk_version,
arch,
arch,
)
} else {
ScriptContext::absolute(
install_dir.clone(),
&env.vc_tools_version,
&env.windows_sdk_version,
arch,
arch,
)
};
let script_content = generate_script(&ctx, shell_type)?;
println!("{}", script_content);
} else if persistent {
#[cfg(windows)]
{
msvc_kit::env::write_to_registry(&env)?;
println!("✅ Environment variables written to registry.");
println!("Please restart your terminal for changes to take effect.");
}
#[cfg(not(windows))]
{
anyhow::bail!("Persistent environment setup is only supported on Windows.");
}
} else {
let shell_type = ShellType::detect();
let _script = generate_activation_script(&env, shell_type)?;
println!("📋 MSVC Environment Setup\n");
println!("To activate the MSVC environment, run:\n");
match shell_type {
ShellType::Cmd => {
println!(" msvc-kit setup --script --shell cmd > activate.bat");
println!(" activate.bat");
}
ShellType::PowerShell => {
println!(
" msvc-kit setup --script --shell powershell | Invoke-Expression"
);
println!("\nOr save to a file:");
println!(" msvc-kit setup --script --shell powershell > activate.ps1");
println!(" . .\\activate.ps1");
}
ShellType::Bash => {
println!(" eval \"$(msvc-kit setup --script --shell bash)\"");
}
}
println!("\nFor persistent setup (Windows only):");
println!(" msvc-kit setup --persistent");
}
}
Commands::List { dir, available } => {
let install_dir = dir.unwrap_or_else(|| config.install_dir.clone());
if available {
println!("📋 Fetching available versions from Microsoft...\n");
let manifest = msvc_kit::downloader::VsManifest::fetch().await?;
if let Some(msvc) = manifest.get_latest_msvc_version() {
println!("Latest MSVC version: {}", msvc);
}
if let Some(sdk) = manifest.get_latest_sdk_version() {
println!("Latest Windows SDK version: {}", sdk);
}
} else {
println!("📋 Installed versions in {}\n", install_dir.display());
let msvc_versions = list_installed_msvc(&install_dir);
let sdk_versions = list_installed_sdk(&install_dir);
if msvc_versions.is_empty() && sdk_versions.is_empty() {
println!("No installations found.");
println!("\nRun 'msvc-kit download' to install MSVC and Windows SDK.");
} else {
if !msvc_versions.is_empty() {
println!("MSVC Compiler:");
for v in &msvc_versions {
println!(" - {}", v);
}
}
if !sdk_versions.is_empty() {
println!("\nWindows SDK:");
for v in &sdk_versions {
println!(" - {}", v);
}
}
}
}
}
Commands::Clean {
dir,
msvc_version,
sdk_version,
all,
cache,
} => {
let install_dir = dir.unwrap_or_else(|| config.install_dir.clone());
if all {
println!("🗑️ Removing all installed versions...");
if install_dir.exists() {
tokio::fs::remove_dir_all(&install_dir).await?;
println!("✅ Removed {}", install_dir.display());
}
} else {
if let Some(version) = msvc_version {
let msvc_path = install_dir
.join("VC")
.join("Tools")
.join("MSVC")
.join(&version);
if msvc_path.exists() {
tokio::fs::remove_dir_all(&msvc_path).await?;
println!("✅ Removed MSVC {}", version);
} else {
println!("⚠️ MSVC {} not found", version);
}
}
if let Some(version) = sdk_version {
let sdk_path = install_dir
.join("Windows Kits")
.join("10")
.join("Include")
.join(&version);
if sdk_path.exists() {
for subdir in ["Include", "Lib", "bin"] {
let path = install_dir
.join("Windows Kits")
.join("10")
.join(subdir)
.join(&version);
if path.exists() {
tokio::fs::remove_dir_all(&path).await?;
}
}
println!("✅ Removed Windows SDK {}", version);
} else {
println!("⚠️ Windows SDK {} not found", version);
}
}
}
if cache {
let cache_dir = install_dir.join("downloads");
if cache_dir.exists() {
tokio::fs::remove_dir_all(&cache_dir).await?;
println!("✅ Removed download cache");
}
}
}
Commands::Config {
set_dir,
set_msvc,
set_sdk,
reset,
} => {
if reset {
config = MsvcKitConfig::default();
save_config(&config)?;
println!("✅ Configuration reset to defaults");
} else if set_dir.is_some() || set_msvc.is_some() || set_sdk.is_some() {
if let Some(dir) = set_dir {
config.install_dir = dir;
}
if let Some(msvc) = set_msvc {
config.default_msvc_version = Some(msvc);
}
if let Some(sdk) = set_sdk {
config.default_sdk_version = Some(sdk);
}
save_config(&config)?;
println!("✅ Configuration updated");
}
println!("📋 Current configuration:\n");
println!(" Install directory: {}", config.install_dir.display());
println!(
" Default MSVC version: {}",
config.default_msvc_version.as_deref().unwrap_or("latest")
);
println!(
" Default SDK version: {}",
config.default_sdk_version.as_deref().unwrap_or("latest")
);
println!(" Default architecture: {}", config.default_arch);
println!(" Verify hashes: {}", config.verify_hashes);
println!(" Parallel downloads: {}", config.parallel_downloads);
}
Commands::Bundle {
output,
arch,
host_arch,
msvc_version,
sdk_version,
accept_license,
zip,
} => {
if !accept_license {
println!("⚠️ License Agreement Required\n");
println!(
"The MSVC compiler and Windows SDK are subject to Microsoft's license terms:"
);
println!(" https://visualstudio.microsoft.com/license-terms/\n");
println!("By using --accept-license, you confirm that you have read and accepted");
println!("Microsoft's Visual Studio License Terms.\n");
println!("Usage:");
println!(" msvc-kit bundle --accept-license [--output <dir>] [--arch <arch>]\n");
anyhow::bail!(
"You must accept the license terms with --accept-license to proceed."
);
}
let arch: Architecture = arch.parse().map_err(|e: String| anyhow::anyhow!(e))?;
let host_arch: Architecture = host_arch
.map(|s| s.parse().map_err(|e: String| anyhow::anyhow!(e)))
.transpose()?
.unwrap_or_else(Architecture::host);
println!("📦 msvc-kit - Creating Portable MSVC Bundle\n");
println!("Output directory: {}", output.display());
println!("Target architecture: {}", arch);
println!("Host architecture: {}", host_arch);
println!();
tokio::fs::create_dir_all(&output).await?;
let options = DownloadOptions {
msvc_version: msvc_version.clone(),
sdk_version: sdk_version.clone(),
target_dir: output.clone(),
arch,
host_arch: Some(host_arch),
verify_hashes: true,
parallel_downloads: config.parallel_downloads,
http_client: None,
progress_handler: None,
cache_manager: None,
dry_run: false,
include_components: Default::default(),
exclude_patterns: Default::default(),
};
println!("⬇️ Downloading MSVC compiler...");
let mut msvc_info = download_msvc(&options).await?;
println!("📁 Extracting MSVC packages...");
msvc_kit::extract_and_finalize_msvc(&mut msvc_info).await?;
let msvc_ver = msvc_info.version.clone();
println!("✅ MSVC {} installed", msvc_ver);
println!("\n⬇️ Downloading Windows SDK...");
let sdk_info = download_sdk(&options).await?;
println!("📁 Extracting SDK packages...");
msvc_kit::extract_and_finalize_sdk(&sdk_info).await?;
let sdk_ver = sdk_info.version.clone();
println!("✅ Windows SDK {} installed", sdk_ver);
let layout = BundleLayout::from_root_with_versions(
&output, &msvc_ver, &sdk_ver, arch, host_arch,
)?;
let scripts = generate_bundle_scripts(&layout)?;
save_bundle_scripts(&layout, &scripts).await?;
let exe_name = if cfg!(windows) {
"msvc-kit.exe"
} else {
"msvc-kit"
};
let current_exe = std::env::current_exe()?;
let target_exe = output.join(exe_name);
tokio::fs::copy(¤t_exe, &target_exe).await?;
println!("\n✅ Bundle created successfully!");
println!("\nContents:");
println!(" {}/", output.display());
println!(" ├── {}", exe_name);
println!(" ├── setup.bat");
println!(" ├── setup.ps1");
println!(" ├── setup.sh");
println!(" ├── README.txt");
println!(" ├── VC/Tools/MSVC/{}/", msvc_ver);
println!(" └── Windows Kits/10/");
if zip {
println!("\n📦 Creating zip archive...");
let zip_name = format!(
"msvc-kit-bundle-{}-{}-{}.zip",
msvc_ver.replace('.', "_"),
sdk_ver.replace('.', "_"),
arch
);
let zip_path = output.parent().unwrap_or(&output).join(&zip_name);
#[cfg(windows)]
{
let output_str = output.display().to_string();
let zip_str = zip_path.display().to_string();
let status = std::process::Command::new("powershell")
.args([
"-NoProfile",
"-Command",
&format!(
"Compress-Archive -Path '{}\\*' -DestinationPath '{}' -Force",
output_str, zip_str
),
])
.status()?;
if status.success() {
println!("✅ Created: {}", zip_path.display());
} else {
println!("⚠️ Failed to create zip archive");
}
}
#[cfg(not(windows))]
{
println!("⚠️ Zip creation is only supported on Windows");
}
}
println!("\n🎉 Done! Run setup.bat (cmd) or .\\setup.ps1 (PowerShell) to activate.");
}
Commands::Env { dir, format } => {
let install_dir = dir.unwrap_or_else(|| config.install_dir.clone());
let msvc_versions = list_installed_msvc(&install_dir);
if msvc_versions.is_empty() {
anyhow::bail!("No MSVC installation found. Run 'msvc-kit download' first.");
}
let msvc_version = &msvc_versions[0];
let sdk_versions = list_installed_sdk(&install_dir);
let sdk_version = sdk_versions.first();
let msvc_info = msvc_kit::installer::InstallInfo {
component_type: "msvc".to_string(),
version: msvc_version.version.clone(),
install_path: msvc_version.install_path.clone().unwrap(),
downloaded_files: vec![],
arch: config.default_arch,
};
let sdk_info = sdk_version.map(|v| msvc_kit::installer::InstallInfo {
component_type: "sdk".to_string(),
version: v.version.clone(),
install_path: v.install_path.clone().unwrap(),
downloaded_files: vec![],
arch: config.default_arch,
});
let env = setup_environment(&msvc_info, sdk_info.as_ref())?;
let vars = get_env_vars(&env);
match format.as_str() {
"json" => {
println!("{}", serde_json::to_string_pretty(&vars)?);
}
_ => {
for (key, value) in &vars {
println!("{}={}", key, value);
}
}
}
}
#[cfg(feature = "self-update")]
Commands::Update { check, version } => {
let current_version = env!("CARGO_PKG_VERSION");
let source = axoupdater::ReleaseSource {
release_type: axoupdater::ReleaseSourceType::GitHub,
owner: "loonghao".to_string(),
name: "msvc-kit".to_string(),
app_name: "msvc-kit".to_string(),
};
let mut updater = axoupdater::AxoUpdater::new_for("msvc-kit");
updater.set_release_source(source);
updater
.set_current_version(
current_version
.parse()
.map_err(|e| anyhow::anyhow!("Failed to parse current version: {}", e))?,
)
.map_err(|e| anyhow::anyhow!("{}", e))?;
updater.disable_installer_output();
if let Some(ref target_version) = version {
updater.configure_version_specifier(axoupdater::UpdateRequest::SpecificVersion(
target_version.clone(),
));
updater.always_update(true);
}
if check {
println!("🔍 Checking for updates...\n");
println!("Current version: v{}", current_version);
match updater.query_new_version().await {
Ok(Some(new_version)) => {
println!("Latest version: v{}", new_version);
println!("\n📦 A new version is available!");
println!("Run 'msvc-kit update' to upgrade.");
}
Ok(None) => {
println!("\n✅ You are running the latest version.");
}
Err(e) => {
println!("⚠️ Failed to check for updates: {}", e);
}
}
} else {
println!("🔄 Updating msvc-kit...\n");
println!("Current version: v{}", current_version);
match updater.run().await {
Ok(Some(result)) => {
println!("\n✅ Updated to v{}!", result.new_version);
println!("Please restart msvc-kit to use the new version.");
}
Ok(None) => {
println!(
"\n✅ Already running the latest version (v{}).",
current_version
);
}
Err(e) => {
anyhow::bail!("Failed to update: {}", e);
}
}
}
}
}
Ok(())
}