use std::fs;
use std::path::Path;
use crate::config::{Config, DEFAULT_PROFILE};
use crate::error::Result;
use crate::flake::template::regenerate_flake_from_profile;
use crate::nixy_config::{NixyConfig, ProfileConfig, NIXY_CONFIG_VERSION};
use crate::state::PackageState;
pub fn needs_migration(config: &Config) -> bool {
if config.nixy_json.exists() {
return false;
}
if config.profiles_dir.exists() && config.profiles_dir.is_dir() {
if let Ok(entries) = fs::read_dir(&config.profiles_dir) {
for entry in entries.flatten() {
if entry.path().is_dir() {
return true;
}
}
}
}
if config.active_file.exists() {
return true;
}
if config.legacy_flake.exists() {
return true;
}
false
}
pub fn migrate_to_nixy_json(config: &Config) -> Result<NixyConfig> {
let mut nixy_config = NixyConfig {
version: NIXY_CONFIG_VERSION,
active_profile: DEFAULT_PROFILE.to_string(),
profiles: std::collections::BTreeMap::new(),
};
if config.active_file.exists() {
if let Ok(active) = fs::read_to_string(&config.active_file) {
let active = active.trim().to_string();
if !active.is_empty() {
nixy_config.active_profile = active;
}
}
}
let mut profile_dirs: Vec<(String, std::path::PathBuf, ProfileConfig)> = Vec::new();
if config.profiles_dir.exists() {
if let Ok(entries) = fs::read_dir(&config.profiles_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
let profile_config = migrate_profile(&path)?;
profile_dirs.push((name.to_string(), path, profile_config));
}
}
}
}
}
for (_, path, _) in &profile_dirs {
let legacy_packages_dir = path.join("packages");
if legacy_packages_dir.exists() {
merge_local_packages(&legacy_packages_dir, &config.global_packages_dir)?;
}
}
if config.legacy_flake.exists() {
let legacy_packages_dir = config.config_dir.join("packages");
if legacy_packages_dir.exists() {
merge_local_packages(&legacy_packages_dir, &config.global_packages_dir)?;
}
}
let global_packages_dir = if config.global_packages_dir.exists() {
Some(config.global_packages_dir.as_path())
} else {
None
};
for (name, path, profile_config) in profile_dirs {
nixy_config
.profiles
.insert(name.clone(), profile_config.clone());
let state_profile_dir = config.profiles_state_dir.join(&name);
fs::create_dir_all(&state_profile_dir)?;
let legacy_lock = path.join("flake.lock");
if legacy_lock.exists() {
fs::copy(&legacy_lock, state_profile_dir.join("flake.lock"))?;
}
regenerate_flake_from_profile(&state_profile_dir, &profile_config, global_packages_dir)?;
}
if config.legacy_flake.exists() && !nixy_config.profiles.contains_key(DEFAULT_PROFILE) {
let profile_config = ProfileConfig::default();
nixy_config
.profiles
.insert(DEFAULT_PROFILE.to_string(), profile_config.clone());
let state_profile_dir = config.profiles_state_dir.join(DEFAULT_PROFILE);
fs::create_dir_all(&state_profile_dir)?;
let legacy_lock = config.config_dir.join("flake.lock");
if legacy_lock.exists() {
fs::copy(&legacy_lock, state_profile_dir.join("flake.lock"))?;
}
regenerate_flake_from_profile(&state_profile_dir, &profile_config, global_packages_dir)?;
}
if !nixy_config.profiles.contains_key(DEFAULT_PROFILE) {
nixy_config
.profiles
.insert(DEFAULT_PROFILE.to_string(), ProfileConfig::default());
}
if !nixy_config
.profiles
.contains_key(&nixy_config.active_profile)
{
nixy_config.active_profile = DEFAULT_PROFILE.to_string();
}
Ok(nixy_config)
}
fn migrate_profile(profile_dir: &Path) -> Result<ProfileConfig> {
let state_path = profile_dir.join("packages.json");
if state_path.exists() {
let state = PackageState::load(&state_path)?;
Ok(ProfileConfig::from(&state))
} else {
Ok(ProfileConfig::default())
}
}
fn merge_local_packages(src_dir: &Path, dst_dir: &Path) -> Result<()> {
if !src_dir.exists() {
return Ok(());
}
fs::create_dir_all(dst_dir)?;
if let Ok(entries) = fs::read_dir(src_dir) {
for entry in entries.flatten() {
let src_path = entry.path();
let file_name = entry.file_name();
let dst_path = dst_dir.join(&file_name);
if dst_path.exists() {
continue;
}
let metadata = match fs::symlink_metadata(&src_path) {
Ok(m) => m,
Err(_) => continue,
};
if metadata.file_type().is_symlink() {
continue;
}
if metadata.is_dir() {
copy_dir_recursive(&src_path, &dst_path)?;
} else if src_path.exists() {
fs::copy(&src_path, &dst_path)?;
}
}
}
Ok(())
}
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
let metadata = match fs::symlink_metadata(&src_path) {
Ok(m) => m,
Err(_) => continue,
};
if metadata.file_type().is_symlink() {
continue;
}
if metadata.is_dir() {
copy_dir_recursive(&src_path, &dst_path)?;
} else {
fs::copy(&src_path, &dst_path)?;
}
}
Ok(())
}
pub fn run_migration_if_needed(config: &Config) -> Result<()> {
if config.nixy_json.exists() {
return Ok(());
}
if needs_migration(config) {
crate::commands::info("Migrating to new nixy.json configuration format...");
let nixy_config = migrate_to_nixy_json(config)?;
nixy_config.save(config)?;
crate::commands::success("Migration complete! Your configuration has been updated.");
crate::commands::info(&format!(
"Configuration is now stored in: {}",
config.nixy_json.display()
));
crate::commands::info(&format!(
"Generated files are now in: {}",
config.profiles_state_dir.display()
));
} else {
let nixy_config = NixyConfig::default();
nixy_config.save(config)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::{CustomPackage, ResolvedNixpkgPackage};
use tempfile::TempDir;
fn test_config(temp: &TempDir) -> Config {
Config {
config_dir: temp.path().join("config"),
nixy_json: temp.path().join("config/nixy.json"),
global_packages_dir: temp.path().join("config/packages"),
state_dir: temp.path().join("state"),
profiles_state_dir: temp.path().join("state/profiles"),
profiles_dir: temp.path().join("config/profiles"),
active_file: temp.path().join("config/active"),
env_link: temp.path().join("state/env"),
legacy_flake: temp.path().join("config/flake.nix"),
}
}
#[test]
fn test_needs_migration_false_when_nixy_json_exists() {
let temp = TempDir::new().unwrap();
let config = test_config(&temp);
fs::create_dir_all(&config.config_dir).unwrap();
fs::write(&config.nixy_json, "{}").unwrap();
assert!(!needs_migration(&config));
}
#[test]
fn test_needs_migration_false_when_nothing_exists() {
let temp = TempDir::new().unwrap();
let config = test_config(&temp);
assert!(!needs_migration(&config));
}
#[test]
fn test_needs_migration_true_when_legacy_profile_exists() {
let temp = TempDir::new().unwrap();
let config = test_config(&temp);
fs::create_dir_all(config.profiles_dir.join("default")).unwrap();
assert!(needs_migration(&config));
}
#[test]
fn test_needs_migration_true_when_active_file_exists() {
let temp = TempDir::new().unwrap();
let config = test_config(&temp);
fs::create_dir_all(&config.config_dir).unwrap();
fs::write(&config.active_file, "default").unwrap();
assert!(needs_migration(&config));
}
#[test]
fn test_needs_migration_true_when_legacy_flake_exists() {
let temp = TempDir::new().unwrap();
let config = test_config(&temp);
fs::create_dir_all(&config.config_dir).unwrap();
fs::write(&config.legacy_flake, "{}").unwrap();
assert!(needs_migration(&config));
}
#[test]
fn test_migrate_empty_profile() {
let temp = TempDir::new().unwrap();
let config = test_config(&temp);
let profile_dir = config.profiles_dir.join("default");
fs::create_dir_all(&profile_dir).unwrap();
let nixy_config = migrate_to_nixy_json(&config).unwrap();
assert!(nixy_config.profiles.contains_key("default"));
assert_eq!(nixy_config.active_profile, "default");
}
#[test]
fn test_migrate_profile_with_packages() {
let temp = TempDir::new().unwrap();
let config = test_config(&temp);
let profile_dir = config.profiles_dir.join("default");
fs::create_dir_all(&profile_dir).unwrap();
let state = PackageState {
version: 2,
packages: vec!["hello".to_string()],
resolved_packages: vec![ResolvedNixpkgPackage {
name: "nodejs".to_string(),
version_spec: Some("20".to_string()),
resolved_version: "20.11.0".to_string(),
attribute_path: "nodejs_20".to_string(),
commit_hash: "abc123".to_string(),
platforms: None,
}],
custom_packages: vec![CustomPackage {
name: "neovim".to_string(),
input_name: "neovim-nightly".to_string(),
input_url: "github:nix-community/neovim-nightly-overlay".to_string(),
package_output: "packages".to_string(),
source_name: None,
platforms: None,
}],
};
state.save(&profile_dir.join("packages.json")).unwrap();
let nixy_config = migrate_to_nixy_json(&config).unwrap();
let profile = nixy_config.profiles.get("default").unwrap();
assert!(profile.has_package("hello"));
assert!(profile.has_package("nodejs"));
assert!(profile.has_package("neovim"));
}
#[test]
fn test_migrate_preserves_active_profile() {
let temp = TempDir::new().unwrap();
let config = test_config(&temp);
fs::create_dir_all(config.profiles_dir.join("default")).unwrap();
fs::create_dir_all(config.profiles_dir.join("work")).unwrap();
fs::create_dir_all(&config.config_dir).unwrap();
fs::write(&config.active_file, "work").unwrap();
let nixy_config = migrate_to_nixy_json(&config).unwrap();
assert_eq!(nixy_config.active_profile, "work");
}
#[test]
fn test_migrate_copies_flake_files() {
let temp = TempDir::new().unwrap();
let config = test_config(&temp);
let profile_dir = config.profiles_dir.join("default");
fs::create_dir_all(&profile_dir).unwrap();
fs::write(profile_dir.join("flake.nix"), "{ }").unwrap();
fs::write(profile_dir.join("flake.lock"), "{}").unwrap();
let _ = migrate_to_nixy_json(&config).unwrap();
let state_profile_dir = config.profiles_state_dir.join("default");
assert!(state_profile_dir.join("flake.nix").exists());
assert!(state_profile_dir.join("flake.lock").exists());
}
#[test]
fn test_migrate_merges_local_packages() {
let temp = TempDir::new().unwrap();
let config = test_config(&temp);
let profile_dir = config.profiles_dir.join("default");
let packages_dir = profile_dir.join("packages");
fs::create_dir_all(&packages_dir).unwrap();
fs::write(packages_dir.join("my-pkg.nix"), "{ }").unwrap();
let _ = migrate_to_nixy_json(&config).unwrap();
assert!(config.global_packages_dir.join("my-pkg.nix").exists());
}
#[test]
fn test_migrate_handles_multiple_profiles() {
let temp = TempDir::new().unwrap();
let config = test_config(&temp);
for name in &["default", "work", "personal"] {
let profile_dir = config.profiles_dir.join(name);
fs::create_dir_all(&profile_dir).unwrap();
let mut state = PackageState::default();
state.packages.push(format!("{}-pkg", name));
state.save(&profile_dir.join("packages.json")).unwrap();
}
let nixy_config = migrate_to_nixy_json(&config).unwrap();
assert!(nixy_config.profiles.contains_key("default"));
assert!(nixy_config.profiles.contains_key("work"));
assert!(nixy_config.profiles.contains_key("personal"));
assert!(nixy_config
.profiles
.get("default")
.unwrap()
.has_package("default-pkg"));
assert!(nixy_config
.profiles
.get("work")
.unwrap()
.has_package("work-pkg"));
assert!(nixy_config
.profiles
.get("personal")
.unwrap()
.has_package("personal-pkg"));
}
#[test]
fn test_migrate_legacy_flake_only() {
let temp = TempDir::new().unwrap();
let config = test_config(&temp);
fs::create_dir_all(&config.config_dir).unwrap();
fs::write(&config.legacy_flake, "{ }").unwrap();
let nixy_config = migrate_to_nixy_json(&config).unwrap();
assert!(nixy_config.profiles.contains_key("default"));
let state_profile_dir = config.profiles_state_dir.join("default");
assert!(state_profile_dir.join("flake.nix").exists());
}
#[test]
#[cfg(unix)]
fn test_migrate_skips_broken_symlinks() {
use std::os::unix::fs::symlink;
let temp = TempDir::new().unwrap();
let config = test_config(&temp);
let profile_dir = config.profiles_dir.join("default");
let packages_dir = profile_dir.join("packages");
fs::create_dir_all(&packages_dir).unwrap();
fs::write(packages_dir.join("valid.nix"), "{ }").unwrap();
symlink(
"/nonexistent/path/broken.nix",
packages_dir.join("broken.nix"),
)
.unwrap();
let nixy_config = migrate_to_nixy_json(&config).unwrap();
assert!(nixy_config.profiles.contains_key("default"));
assert!(config.global_packages_dir.join("valid.nix").exists());
assert!(!config.global_packages_dir.join("broken.nix").exists());
}
}