use std::path::{Path, PathBuf};
use anyhow::Context;
use crate::{channel::DEFAULT_NETWORK, paths};
const LEGACY_LINK: &str = "stable";
#[derive(Debug, PartialEq, Eq)]
pub enum Outcome {
NothingToDo,
Migrated,
}
pub fn migrate_if_needed(home: &Path) -> anyhow::Result<Outcome> {
let mut migrated = false;
let legacy = paths::network_link(home, LEGACY_LINK);
let mainnet = paths::network_link(home, DEFAULT_NETWORK);
if std::fs::symlink_metadata(&legacy).is_ok() && std::fs::symlink_metadata(&mainnet).is_err() {
if adopt_var_for_default_network(home, &legacy)? {
migrated = true;
}
match std::fs::rename(&legacy, &mainnet) {
Ok(()) => {
drop_stale_manifest_cache(home)?;
migrated = true;
},
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {},
Err(err) => {
return Err(err).context("failed to rename the legacy 'stable' link");
},
}
}
if repoint_default(home)? {
migrated = true;
}
Ok(if migrated {
Outcome::Migrated
} else {
Outcome::NothingToDo
})
}
fn repoint_default(home: &Path) -> anyhow::Result<bool> {
let default = paths::toolchains_dir(home).join("default");
let Ok(target) = std::fs::read_link(&default) else {
return Ok(false);
};
if target.file_name().and_then(|name| name.to_str()) != Some(LEGACY_LINK) {
return Ok(false);
}
let replacement = if target.is_absolute() {
paths::network_link(home, DEFAULT_NETWORK)
} else {
PathBuf::from(DEFAULT_NETWORK)
};
crate::utils::fs::replace_symlink(&default, &replacement)?;
Ok(true)
}
fn adopt_var_for_default_network(home: &Path, legacy: &Path) -> anyhow::Result<bool> {
let Ok(named) = std::fs::read_link(legacy) else {
return Ok(false);
};
let Some(named) = named.file_name() else {
return Ok(false);
};
if default_pins_a_version(home) {
return Ok(false);
}
let source = home.join("var").join(named);
let destination = home.join("var").join(DEFAULT_NETWORK);
if !source.is_dir() || destination.exists() {
return Ok(false);
}
match std::fs::rename(&source, &destination) {
Ok(()) => {
crate::info!(
"moved your {DEFAULT_NETWORK} data from {} to {}: it is now keyed by the network \
rather than by the toolchain version.\nIf a project of yours pins {} in \
miden-toolchain.toml, move it back with: mv {} {}",
source.display(),
destination.display(),
named.to_string_lossy(),
destination.display(),
source.display(),
);
Ok(true)
},
Err(err)
if matches!(
err.kind(),
std::io::ErrorKind::NotFound
| std::io::ErrorKind::AlreadyExists
| std::io::ErrorKind::DirectoryNotEmpty
) =>
{
Ok(false)
},
Err(err) => Err(err).context("failed to move the default network's data"),
}
}
fn default_pins_a_version(home: &Path) -> bool {
let default = paths::toolchains_dir(home).join("default");
let Ok(target) = std::fs::read_link(&default) else {
return false;
};
target.file_name().and_then(|name| name.to_str()) != Some(LEGACY_LINK)
}
fn drop_stale_manifest_cache(home: &Path) -> anyhow::Result<()> {
let cache = paths::manifest_cache(home);
let Ok(contents) = std::fs::read_to_string(&cache) else {
return Ok(());
};
let readable = crate::manifest::version::read_version_header(&contents, "manifest_version")
.is_ok_and(|header| {
header.version.major == crate::manifest::v3::MANIFEST_VERSION.major
|| header.version == crate::manifest::v1::MANIFEST_VERSION
});
if readable {
return Ok(());
}
match std::fs::remove_file(&cache) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(err).context("failed to remove the stale manifest cache"),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn alpha_home() -> (tempdir::TempDir, PathBuf) {
let temp = tempdir::TempDir::new("migrate-networks").unwrap();
let home = temp.path().join("midenup");
let toolchains = crate::paths::toolchains_dir(&home);
std::fs::create_dir_all(&toolchains).unwrap();
std::fs::create_dir_all(toolchains.join("0.15.0")).unwrap();
std::os::unix::fs::symlink("0.15.0", toolchains.join("stable")).unwrap();
std::os::unix::fs::symlink(toolchains.join("stable"), toolchains.join("default")).unwrap();
std::fs::write(crate::paths::manifest_cache(&home), r#"{"manifest_version":"2.0.0"}"#)
.unwrap();
(temp, home)
}
#[test]
fn the_stable_link_becomes_mainnet() {
let (_temp, home) = alpha_home();
assert_eq!(migrate_if_needed(&home).unwrap(), Outcome::Migrated);
assert_eq!(
std::fs::read_link(crate::paths::network_link(&home, "mainnet")).unwrap(),
PathBuf::from("0.15.0")
);
assert!(
std::fs::symlink_metadata(crate::paths::network_link(&home, "stable")).is_err(),
"the old name must not be left behind"
);
}
#[test]
fn the_default_link_follows_the_rename() {
let (_temp, home) = alpha_home();
migrate_if_needed(&home).unwrap();
let default = crate::paths::toolchains_dir(&home).join("default");
let target = std::fs::read_link(&default).expect("default must still be a symlink");
assert_eq!(target.file_name().unwrap(), "mainnet");
assert!(default.canonicalize().is_ok(), "default must not dangle");
}
#[test]
fn a_stale_manifest_cache_is_dropped() {
let (_temp, home) = alpha_home();
migrate_if_needed(&home).unwrap();
assert!(!crate::paths::manifest_cache(&home).exists());
}
#[test]
fn a_current_manifest_cache_is_kept() {
let (_temp, home) = alpha_home();
std::fs::write(crate::paths::manifest_cache(&home), r#"{"manifest_version":"3.0.0"}"#)
.unwrap();
migrate_if_needed(&home).unwrap();
assert!(crate::paths::manifest_cache(&home).exists());
}
#[test]
fn a_v1_manifest_cache_is_kept() {
let (_temp, home) = alpha_home();
std::fs::write(crate::paths::manifest_cache(&home), r#"{"manifest_version":"1.0.1"}"#)
.unwrap();
migrate_if_needed(&home).unwrap();
assert!(crate::paths::manifest_cache(&home).exists());
}
#[test]
fn a_dangling_legacy_link_is_still_renamed() {
let temp = tempdir::TempDir::new("migrate-networks-dangling").unwrap();
let home = temp.path().join("midenup");
let toolchains = crate::paths::toolchains_dir(&home);
std::fs::create_dir_all(&toolchains).unwrap();
std::os::unix::fs::symlink("0.15.0", toolchains.join("stable")).unwrap();
assert_eq!(migrate_if_needed(&home).unwrap(), Outcome::Migrated);
assert!(
std::fs::symlink_metadata(crate::paths::network_link(&home, "mainnet")).is_ok(),
"the link must be renamed even though its target is gone"
);
}
#[test]
fn a_relative_default_link_follows_the_rename() {
let (_temp, home) = alpha_home();
let default = crate::paths::toolchains_dir(&home).join("default");
std::fs::remove_file(&default).unwrap();
std::os::unix::fs::symlink("stable", &default).unwrap();
migrate_if_needed(&home).unwrap();
assert_eq!(std::fs::read_link(&default).unwrap(), PathBuf::from("mainnet"));
}
#[test]
fn a_default_link_naming_a_version_is_left_alone() {
let (_temp, home) = alpha_home();
let default = crate::paths::toolchains_dir(&home).join("default");
std::fs::remove_file(&default).unwrap();
std::os::unix::fs::symlink("0.15.0", &default).unwrap();
migrate_if_needed(&home).unwrap();
assert_eq!(std::fs::read_link(&default).unwrap(), PathBuf::from("0.15.0"));
}
#[test]
fn migration_is_idempotent() {
let (_temp, home) = alpha_home();
migrate_if_needed(&home).unwrap();
let after_first = std::fs::read_link(crate::paths::network_link(&home, "mainnet")).unwrap();
assert_eq!(migrate_if_needed(&home).unwrap(), Outcome::NothingToDo);
assert_eq!(
std::fs::read_link(crate::paths::network_link(&home, "mainnet")).unwrap(),
after_first
);
}
#[test]
fn an_existing_mainnet_link_is_not_overwritten() {
let (_temp, home) = alpha_home();
let toolchains = crate::paths::toolchains_dir(&home);
std::fs::create_dir_all(toolchains.join("0.16.0")).unwrap();
std::os::unix::fs::symlink("0.16.0", toolchains.join("mainnet")).unwrap();
migrate_if_needed(&home).unwrap();
assert_eq!(
std::fs::read_link(crate::paths::network_link(&home, "mainnet")).unwrap(),
PathBuf::from("0.16.0")
);
}
fn seed_var(home: &Path, key: &str) {
let dir = home.join("var").join(key);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("store.sqlite3"), key.as_bytes()).unwrap();
}
fn var_store(home: &Path, key: &str) -> Option<Vec<u8>> {
std::fs::read(home.join("var").join(key).join("store.sqlite3")).ok()
}
#[test]
fn the_single_store_becomes_the_default_networks() {
let (_temp, home) = alpha_home();
seed_var(&home, "0.15.0");
assert_eq!(migrate_if_needed(&home).unwrap(), Outcome::Migrated);
assert_eq!(var_store(&home, DEFAULT_NETWORK).as_deref(), Some(&b"0.15.0"[..]));
assert!(
!home.join("var").join("0.15.0").exists(),
"the old key must not be left holding a second copy"
);
}
#[test]
fn the_other_networks_start_empty() {
let (_temp, home) = alpha_home();
seed_var(&home, "0.15.0");
migrate_if_needed(&home).unwrap();
for network in ["testnet", "devnet"] {
assert!(
!home.join("var").join(network).exists(),
"{network} must not be given a copy of the default network's store"
);
}
}
#[test]
fn adopting_the_store_is_idempotent() {
let (_temp, home) = alpha_home();
seed_var(&home, "0.15.0");
migrate_if_needed(&home).unwrap();
assert_eq!(migrate_if_needed(&home).unwrap(), Outcome::NothingToDo);
assert_eq!(var_store(&home, DEFAULT_NETWORK).as_deref(), Some(&b"0.15.0"[..]));
}
#[test]
fn an_existing_network_store_is_not_overwritten() {
let (_temp, home) = alpha_home();
seed_var(&home, "0.15.0");
seed_var(&home, DEFAULT_NETWORK);
migrate_if_needed(&home).unwrap();
assert_eq!(var_store(&home, DEFAULT_NETWORK).as_deref(), Some(&b"mainnet"[..]));
assert_eq!(
var_store(&home, "0.15.0").as_deref(),
Some(&b"0.15.0"[..]),
"and the one that could not move stays where the user can find it"
);
}
#[test]
fn a_store_under_a_pinned_default_is_left_alone() {
let (_temp, home) = alpha_home();
let default = crate::paths::toolchains_dir(&home).join("default");
std::fs::remove_file(&default).unwrap();
std::os::unix::fs::symlink("0.15.0", &default).unwrap();
seed_var(&home, "0.15.0");
migrate_if_needed(&home).unwrap();
assert_eq!(var_store(&home, "0.15.0").as_deref(), Some(&b"0.15.0"[..]));
assert!(
!home.join("var").join(DEFAULT_NETWORK).exists(),
"a deliberately pinned store must not be moved out from under the pin"
);
}
#[test]
fn a_pinned_store_is_left_alone() {
let (_temp, home) = alpha_home();
seed_var(&home, "0.9.0");
migrate_if_needed(&home).unwrap();
assert_eq!(var_store(&home, "0.9.0").as_deref(), Some(&b"0.9.0"[..]));
assert!(!home.join("var").join(DEFAULT_NETWORK).exists());
}
#[test]
fn a_home_that_does_not_exist_is_not_an_error() {
let temp = tempdir::TempDir::new("migrate-networks-empty").unwrap();
assert_eq!(migrate_if_needed(temp.path()).unwrap(), Outcome::NothingToDo);
}
}