use color_eyre::eyre::{Context, Result};
use semver::Version;
use serde::{Deserialize, Serialize};
use std::fs as sync_fs;
use std::path::Path;
use tokio::fs;
use yansi::Paint;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZvConfig {
pub version: String,
pub active_zig: Option<ActiveZig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActiveZig {
pub version: String,
pub path: String,
pub is_master: bool,
}
#[derive(Debug, thiserror::Error)]
pub enum MigrationError {
#[error("Failed to read zv.toml: {0}")]
ReadConfig(#[source] std::io::Error),
#[error("Failed to write zv.toml: {0}")]
WriteConfig(#[source] std::io::Error),
#[error("Failed to parse zv.toml: {0}")]
ParseConfig(#[source] toml::de::Error),
}
pub async fn migrate(zv_root: &Path) -> Result<()> {
let current_version = env!("CARGO_PKG_VERSION");
let current_version_parsed =
Version::parse(current_version).expect("CARGO_PKG_VERSION should be valid semver");
let zv_toml_path = zv_root.join("zv.toml");
let needs_migration = if !zv_toml_path.exists() {
tracing::debug!("zv.toml not found, migration needed");
true
} else {
match load_zv_config(&zv_toml_path) {
Ok(config) => {
let config_version =
Version::parse(&config.version).unwrap_or_else(|_| Version::new(0, 8, 0));
if config_version < current_version_parsed {
tracing::debug!(
"Config version {} < current version {}, migration needed",
config_version,
current_version
);
true
} else {
tracing::debug!(
"Config version {} >= current version {}, no migration needed",
config_version,
current_version
);
false
}
}
Err(e) => {
tracing::warn!("Failed to load zv.toml, will recreate: {}", e);
true
}
}
};
if needs_migration {
println!(
"Performing zv -> {} migrations",
Paint::green(current_version)
);
let migrated_active_zig = migrate_0_8_0_to_0_9_0(zv_root).await?;
let config = ZvConfig {
version: current_version.to_string(),
active_zig: migrated_active_zig,
};
save_zv_config(&zv_toml_path, &config)?;
}
Ok(())
}
pub fn load_zv_config(path: &Path) -> Result<ZvConfig, MigrationError> {
let contents = sync_fs::read_to_string(path).map_err(MigrationError::ReadConfig)?;
toml::from_str(&contents).map_err(MigrationError::ParseConfig)
}
pub fn save_zv_config(path: &Path, config: &ZvConfig) -> Result<(), MigrationError> {
let contents = toml::to_string_pretty(config).map_err(|e| {
MigrationError::WriteConfig(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to serialize config: {}", e),
))
})?;
sync_fs::write(path, contents).map_err(MigrationError::WriteConfig)?;
Ok(())
}
async fn migrate_0_8_0_to_0_9_0(zv_root: &Path) -> Result<Option<ActiveZig>> {
tracing::info!("Running v0.9.0 migrations");
let versions_path = zv_root.join("versions");
let master_dir = versions_path.join("master");
let active_json_path = zv_root.join("active.json");
let mut migrated_active_zig = None;
if master_dir.exists() {
flatten_master_to_versions(&versions_path, &master_dir).await?;
}
if active_json_path.exists() {
migrated_active_zig = Some(migrate_active_json(&active_json_path).await?);
}
if master_dir.exists() {
fs::remove_dir_all(&master_dir)
.await
.wrap_err("Failed to remove versions/master directory after migration")?;
tracing::info!("Removed versions/master directory");
}
tracing::info!("Migration from 0.8.0 to 0.9.0 completed successfully");
Ok(migrated_active_zig)
}
async fn flatten_master_to_versions(versions_path: &Path, master_dir: &Path) -> Result<()> {
use walkdir::WalkDir;
let zig_exe = if cfg!(windows) { "zig.exe" } else { "zig" };
println!(
" {} Migrating versions from master/ directory...",
"→".blue()
);
let mut migrated_count = 0;
let mut skipped_count = 0;
for entry in WalkDir::new(master_dir)
.min_depth(1)
.max_depth(1)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_dir())
{
let master_version_path = entry.path();
let version_str = master_version_path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
let target_version_path = versions_path.join(version_str);
let target_zig_path = target_version_path.join(zig_exe);
if target_zig_path.is_file() {
tracing::debug!(
"Version {} already exists in versions/, skipping master version",
version_str
);
skipped_count += 1;
continue;
}
let master_zig_path = master_version_path.join(zig_exe);
if !master_zig_path.is_file() {
tracing::debug!(
"Invalid Zig installation at {}, skipping",
master_version_path.display()
);
skipped_count += 1;
continue;
}
tracing::debug!("Moving {} to versions/{}", version_str, version_str);
fs::rename(master_version_path, &target_version_path)
.await
.wrap_err_with(|| {
format!(
"Failed to move {} to {}",
master_version_path.display(),
target_version_path.display()
)
})?;
migrated_count += 1;
tracing::info!("Migrated version {}", version_str);
}
println!(
" {} Migrated {} versions{}",
"✓".green(),
migrated_count,
if skipped_count > 0 {
format!(" (skipped {} that already exist)", skipped_count)
} else {
String::new()
}
);
Ok(())
}
async fn migrate_active_json(active_json_path: &Path) -> Result<ActiveZig> {
tracing::debug!("Migrating active.json to zv.toml");
let active_json = fs::read_to_string(active_json_path)
.await
.wrap_err("Failed to read active.json")?;
#[derive(Debug, Deserialize)]
struct LegacyZigInstall {
version: semver::Version,
path: std::path::PathBuf,
#[serde(default)]
is_master: bool,
}
let zig_install: LegacyZigInstall =
serde_json::from_str(&active_json).wrap_err("Failed to parse active.json")?;
let active_zig = ActiveZig {
version: zig_install.version.to_string(),
path: zig_install.path.to_string_lossy().to_string(),
is_master: zig_install.is_master,
};
tracing::info!(
"Migrated active Zig version {} (master: {}) from active.json",
active_zig.version,
active_zig.is_master
);
fs::remove_file(active_json_path)
.await
.wrap_err("Failed to remove active.json")?;
tracing::debug!("Removed active.json after migration");
Ok(active_zig)
}
pub async fn update_master_file(zv_root: &Path, version: &str) {
let master_file_path = zv_root.join("master");
match sync_fs::write(&master_file_path, version) {
Ok(_) => {
tracing::debug!("Updated master file with version: {}", version);
}
Err(e) => {
tracing::error!("Failed to update master file: {}", e);
}
}
}