From 3db09595f31bd9f2f211d43d96f0acb887a68991 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Mon, 23 Sep 2024 02:15:52 +0200 Subject: [PATCH] Add migration support for TOML config changes This patch allows running `alacritty migrate` to automatically apply configuration changes made to the TOML format, like moving `ipc_socket` to `general.ipc_socket`. This should reduce the friction of moving around individual options significantly, while also persisting the format of the existing TOML file thanks to `toml_edit`. The YAML migration has been simplified significantly to only switch the format of the file from YAML to TOML. The new TOML features are used for everything else. --- CHANGELOG.md | 1 + Cargo.lock | 48 ++- alacritty/Cargo.toml | 4 +- alacritty/src/config/mod.rs | 33 +- alacritty/src/migrate.rs | 274 --------------- alacritty/src/migrate/mod.rs | 318 ++++++++++++++++++ alacritty/src/migrate/yaml.rs | 87 +++++ .../src/config_deserialize/de_struct.rs | 1 + alacritty_config_derive/tests/config.rs | 19 +- 9 files changed, 476 insertions(+), 309 deletions(-) delete mode 100644 alacritty/src/migrate.rs create mode 100644 alacritty/src/migrate/mod.rs create mode 100644 alacritty/src/migrate/yaml.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b767eef..dc858911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Notable changes to the `alacritty_terminal` crate are documented in its ### Added - Support relative path imports from config files +- `alacritty migrate` support for TOML configuration changes ### Changed diff --git a/Cargo.lock b/Cargo.lock index 566dbc08..39a672fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,7 +58,9 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "tempfile", "toml", + "toml_edit 0.22.21", "unicode-width", "windows-sys 0.52.0", "winit", @@ -891,9 +893,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown", @@ -1749,9 +1751,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -1877,6 +1879,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "thiserror" version = "1.0.62" @@ -1931,14 +1946,14 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.15", + "toml_edit 0.22.21", ] [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] @@ -1956,15 +1971,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.15" +version = "0.22.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" +checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.13", + "winnow 0.6.18", ] [[package]] @@ -2332,6 +2347,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -2574,9 +2598,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.13" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] diff --git a/alacritty/Cargo.toml b/alacritty/Cargo.toml index e8739049..ab44b046 100644 --- a/alacritty/Cargo.toml +++ b/alacritty/Cargo.toml @@ -34,10 +34,12 @@ libc = "0.2" log = { version = "0.4", features = ["std", "serde"] } notify = "6.1.1" parking_lot = "0.12.0" -serde = { version = "1", features = ["derive"] } serde_json = "1" +serde = { version = "1", features = ["derive"] } serde_yaml = "0.9.25" +tempfile = "3.12.0" toml = "0.8.2" +toml_edit = "0.22.21" unicode-width = "0.1" winit = { version = "0.30.4", default-features = false, features = ["rwh_06", "serde"] } diff --git a/alacritty/src/config/mod.rs b/alacritty/src/config/mod.rs index 9ee5215c..ba9d674d 100644 --- a/alacritty/src/config/mod.rs +++ b/alacritty/src/config/mod.rs @@ -301,7 +301,7 @@ pub fn imports( let mut import_paths = Vec::new(); for import in imports { - let mut path = match import { + let path = match import { Value::String(path) => PathBuf::from(path), _ => { import_paths.push(Err("Invalid import element type: expected path string".into())); @@ -309,23 +309,32 @@ pub fn imports( }, }; - // Resolve paths relative to user's home directory. - if let (Ok(stripped), Some(home_dir)) = (path.strip_prefix("~/"), home::home_dir()) { - path = home_dir.join(stripped); - } + let normalized = normalize_import(base_path, path); - if path.is_relative() { - if let Some(base_path) = base_path.parent() { - path = base_path.join(path) - } - } - - import_paths.push(Ok(path)); + import_paths.push(Ok(normalized)); } Ok(import_paths) } +/// Normalize import paths. +pub fn normalize_import(base_config_path: &Path, import_path: impl Into) -> PathBuf { + let mut import_path = import_path.into(); + + // Resolve paths relative to user's home directory. + if let (Ok(stripped), Some(home_dir)) = (import_path.strip_prefix("~/"), home::home_dir()) { + import_path = home_dir.join(stripped); + } + + if import_path.is_relative() { + if let Some(base_config_dir) = base_config_path.parent() { + import_path = base_config_dir.join(import_path) + } + } + + import_path +} + /// Prune the nulls from the YAML to ensure TOML compatibility. fn prune_yaml_nulls(value: &mut serde_yaml::Value, warn_pruned: bool) { fn walk(value: &mut serde_yaml::Value, warn_pruned: bool) -> bool { diff --git a/alacritty/src/migrate.rs b/alacritty/src/migrate.rs deleted file mode 100644 index 39779ba2..00000000 --- a/alacritty/src/migrate.rs +++ /dev/null @@ -1,274 +0,0 @@ -//! Configuration file migration. - -use std::fs; -use std::path::Path; - -use toml::map::Entry; -use toml::{Table, Value}; - -use crate::cli::MigrateOptions; -use crate::config; - -/// Handle migration. -pub fn migrate(options: MigrateOptions) { - // Find configuration file path. - let config_path = options - .config_file - .clone() - .or_else(|| config::installed_config("toml")) - .or_else(|| config::installed_config("yml")); - - // Abort if system has no installed configuration. - let config_path = match config_path { - Some(config_path) => config_path, - None => { - eprintln!("No configuration file found"); - std::process::exit(1); - }, - }; - - // If we're doing a wet run, perform a dry run first for safety. - if !options.dry_run { - #[allow(clippy::redundant_clone)] - let mut options = options.clone(); - options.silent = true; - options.dry_run = true; - if let Err(err) = migrate_config(&options, &config_path, config::IMPORT_RECURSION_LIMIT) { - eprintln!("Configuration file migration failed:"); - eprintln!(" {config_path:?}: {err}"); - std::process::exit(1); - } - } - - // Migrate the root config. - match migrate_config(&options, &config_path, config::IMPORT_RECURSION_LIMIT) { - Ok(new_path) => { - if !options.silent { - println!("Successfully migrated {config_path:?} to {new_path:?}"); - } - }, - Err(err) => { - eprintln!("Configuration file migration failed:"); - eprintln!(" {config_path:?}: {err}"); - std::process::exit(1); - }, - } -} - -/// Migrate a specific configuration file. -fn migrate_config( - options: &MigrateOptions, - path: &Path, - recursion_limit: usize, -) -> Result { - // Ensure configuration file has an extension. - let path_str = path.to_string_lossy(); - let (prefix, suffix) = match path_str.rsplit_once('.') { - Some((prefix, suffix)) => (prefix, suffix), - None => return Err("missing file extension".to_string()), - }; - - // Abort if config is already toml. - if suffix == "toml" { - return Err("already in TOML format".to_string()); - } - - // Try to parse the configuration file. - let mut config = match config::deserialize_config(path, !options.dry_run) { - Ok(config) => config, - Err(err) => return Err(format!("parsing error: {err}")), - }; - - // Migrate config imports. - if !options.skip_imports { - migrate_imports(options, &mut config, path, recursion_limit)?; - } - - // Migrate deprecated field names to their new location. - if !options.skip_renames { - migrate_renames(&mut config)?; - } - - // Convert to TOML format. - let toml = toml::to_string(&config).map_err(|err| format!("conversion error: {err}"))?; - let new_path = format!("{prefix}.toml"); - - if options.dry_run && !options.silent { - // Output new content to STDOUT. - println!( - "\nv-----Start TOML for {path:?}-----v\n\n{toml}\n^-----End TOML for {path:?}-----^\n" - ); - } else if !options.dry_run { - // Write the new toml configuration. - fs::write(&new_path, toml).map_err(|err| format!("filesystem error: {err}"))?; - } - - Ok(new_path) -} - -/// Migrate the imports of a config. -fn migrate_imports( - options: &MigrateOptions, - config: &mut Value, - base_path: &Path, - recursion_limit: usize, -) -> Result<(), String> { - let imports = match config::imports(config, base_path, recursion_limit) { - Ok(imports) => imports, - Err(err) => return Err(format!("import error: {err}")), - }; - - // Migrate the individual imports. - let mut new_imports = Vec::new(); - for import in imports { - let import = match import { - Ok(import) => import, - Err(err) => return Err(format!("import error: {err}")), - }; - - // Keep yaml import if path does not exist. - if !import.exists() { - if options.dry_run { - eprintln!("Keeping yaml config for nonexistent import: {import:?}"); - } - new_imports.push(Value::String(import.to_string_lossy().into())); - continue; - } - - let new_path = migrate_config(options, &import, recursion_limit - 1)?; - - // Print new import path. - if options.dry_run { - println!("Successfully migrated import {import:?} to {new_path:?}"); - } - - new_imports.push(Value::String(new_path)); - } - - // Update the imports field. - if let Some(import) = config.get_mut("import") { - *import = Value::Array(new_imports); - } - - Ok(()) -} - -/// Migrate deprecated fields. -fn migrate_renames(config: &mut Value) -> Result<(), String> { - let config_table = match config.as_table_mut() { - Some(config_table) => config_table, - None => return Ok(()), - }; - - // draw_bold_text_with_bright_colors -> colors.draw_bold_text_with_bright_colors - move_value(config_table, &["draw_bold_text_with_bright_colors"], &[ - "colors", - "draw_bold_text_with_bright_colors", - ])?; - - // key_bindings -> keyboard.bindings - move_value(config_table, &["key_bindings"], &["keyboard", "bindings"])?; - - // mouse_bindings -> mouse.bindings - move_value(config_table, &["mouse_bindings"], &["mouse", "bindings"])?; - - // Avoid warnings due to introduction of the new `general` section. - move_value(config_table, &["live_config_reload"], &["general", "live_config_reload"])?; - move_value(config_table, &["working_directory"], &["general", "working_directory"])?; - move_value(config_table, &["ipc_socket"], &["general", "ipc_socket"])?; - move_value(config_table, &["import"], &["general", "import"])?; - move_value(config_table, &["shell"], &["terminal", "shell"])?; - - Ok(()) -} - -/// Move a toml value from one map to another. -fn move_value(config_table: &mut Table, origin: &[&str], target: &[&str]) -> Result<(), String> { - if let Some(value) = remove_node(config_table, origin)? { - if !insert_node_if_empty(config_table, target, value)? { - return Err(format!( - "conflict: both `{}` and `{}` are set", - origin.join("."), - target.join(".") - )); - } - } - - Ok(()) -} - -/// Remove a node from a tree of tables. -fn remove_node(table: &mut Table, path: &[&str]) -> Result, String> { - if path.len() == 1 { - Ok(table.remove(path[0])) - } else { - let next_table_value = match table.get_mut(path[0]) { - Some(next_table_value) => next_table_value, - None => return Ok(None), - }; - - let next_table = match next_table_value.as_table_mut() { - Some(next_table) => next_table, - None => return Err(format!("invalid `{}` table", path[0])), - }; - - remove_node(next_table, &path[1..]) - } -} - -/// Try to insert a node into a tree of tables. -/// -/// Returns `false` if the node already exists. -fn insert_node_if_empty(table: &mut Table, path: &[&str], node: Value) -> Result { - if path.len() == 1 { - match table.entry(path[0]) { - Entry::Vacant(vacant_entry) => { - vacant_entry.insert(node); - Ok(true) - }, - Entry::Occupied(_) => Ok(false), - } - } else { - let next_table_value = table.entry(path[0]).or_insert_with(|| Value::Table(Table::new())); - - let next_table = match next_table_value.as_table_mut() { - Some(next_table) => next_table, - None => return Err(format!("invalid `{}` table", path[0])), - }; - - insert_node_if_empty(next_table, &path[1..], node) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn move_values() { - let input = r#" -root_value = 3 - -[table] -table_value = 5 - -[preexisting] -not_moved = 9 - "#; - - let mut value: Value = toml::from_str(input).unwrap(); - let table = value.as_table_mut().unwrap(); - - move_value(table, &["root_value"], &["new_table", "root_value"]).unwrap(); - move_value(table, &["table", "table_value"], &["preexisting", "subtable", "new_name"]) - .unwrap(); - - let output = toml::to_string(table).unwrap(); - - assert_eq!( - output, - "[new_table]\nroot_value = 3\n\n[preexisting]\nnot_moved = \ - 9\n\n[preexisting.subtable]\nnew_name = 5\n\n[table]\n" - ); - } -} diff --git a/alacritty/src/migrate/mod.rs b/alacritty/src/migrate/mod.rs new file mode 100644 index 00000000..ffd0d4b4 --- /dev/null +++ b/alacritty/src/migrate/mod.rs @@ -0,0 +1,318 @@ +//! Configuration file migration. + +use std::fmt::Debug; +use std::path::Path; +use std::{fs, mem}; + +use tempfile::NamedTempFile; +use toml_edit::{DocumentMut, Item}; + +use crate::cli::MigrateOptions; +use crate::config; + +mod yaml; + +/// Handle migration. +pub fn migrate(options: MigrateOptions) { + // Find configuration file path. + let config_path = options + .config_file + .clone() + .or_else(|| config::installed_config("toml")) + .or_else(|| config::installed_config("yml")); + + // Abort if system has no installed configuration. + let config_path = match config_path { + Some(config_path) => config_path, + None => { + eprintln!("No configuration file found"); + std::process::exit(1); + }, + }; + + // If we're doing a wet run, perform a dry run first for safety. + if !options.dry_run { + #[allow(clippy::redundant_clone)] + let mut options = options.clone(); + options.silent = true; + options.dry_run = true; + if let Err(err) = migrate_config(&options, &config_path, config::IMPORT_RECURSION_LIMIT) { + eprintln!("Configuration file migration failed:"); + eprintln!(" {config_path:?}: {err}"); + std::process::exit(1); + } + } + + // Migrate the root config. + match migrate_config(&options, &config_path, config::IMPORT_RECURSION_LIMIT) { + Ok(migration) => { + if !options.silent { + println!("{}", migration.success_message(false)); + } + }, + Err(err) => { + eprintln!("Configuration file migration failed:"); + eprintln!(" {config_path:?}: {err}"); + std::process::exit(1); + }, + } +} + +/// Migrate a specific configuration file. +fn migrate_config<'a>( + options: &MigrateOptions, + path: &'a Path, + recursion_limit: usize, +) -> Result, String> { + // Ensure configuration file has an extension. + let path_str = path.to_string_lossy(); + let (prefix, suffix) = match path_str.rsplit_once('.') { + Some((prefix, suffix)) => (prefix, suffix), + None => return Err("missing file extension".to_string()), + }; + + // Handle legacy YAML files. + if suffix == "yml" { + let new_path = yaml::migrate(options, path, recursion_limit, prefix)?; + return Ok(Migration::Yaml((path, new_path))); + } + + // TOML only does renames, so return early if they are disabled. + if options.skip_renames { + if options.dry_run { + eprintln!("Ignoring TOML file {path:?} since `--skip-renames` was supplied"); + } + return Ok(Migration::Toml(path)); + } + + // Read TOML file and perform all in-file migrations. + let toml = fs::read_to_string(path).map_err(|err| format!("{err}"))?; + let mut migrated = migrate_toml(toml)?; + + // Recursively migrate imports. + migrate_imports(options, path, &mut migrated, recursion_limit)?; + + // Write migrated TOML file. + write_results(options, path, &migrated.to_string())?; + + Ok(Migration::Toml(path)) +} + +/// Migrate TOML config to the latest version. +fn migrate_toml(toml: String) -> Result { + // Parse TOML file. + let mut document = match toml.parse::() { + Ok(document) => document, + Err(err) => return Err(format!("TOML parsing error: {err}")), + }; + + // Move `draw_bold_text_with_bright_colors` to its own section. + move_value(&mut document, &["draw_bold_text_with_bright_colors"], &[ + "colors", + "draw_bold_text_with_bright_colors", + ])?; + + // Move bindings to their own section. + move_value(&mut document, &["key_bindings"], &["keyboard", "bindings"])?; + move_value(&mut document, &["mouse_bindings"], &["mouse", "bindings"])?; + + // Avoid warnings due to introduction of the new `general` section. + move_value(&mut document, &["live_config_reload"], &["general", "live_config_reload"])?; + move_value(&mut document, &["working_directory"], &["general", "working_directory"])?; + move_value(&mut document, &["ipc_socket"], &["general", "ipc_socket"])?; + move_value(&mut document, &["import"], &["general", "import"])?; + move_value(&mut document, &["shell"], &["terminal", "shell"])?; + + Ok(document) +} + +/// Migrate TOML imports to the latest version. +fn migrate_imports( + options: &MigrateOptions, + path: &Path, + document: &mut DocumentMut, + recursion_limit: usize, +) -> Result<(), String> { + // Check if any imports need to be processed. + let imports = match document["general"].get("import").and_then(|i| i.as_array()) { + Some(array) if !array.is_empty() => array, + _ => return Ok(()), + }; + + // Abort once recursion limit is exceeded. + if recursion_limit == 0 { + return Err("Exceeded maximum configuration import depth".into()); + } + + // Migrate each import. + for import in imports.into_iter().filter_map(|item| item.as_str()) { + let normalized_path = config::normalize_import(path, import); + let migration = migrate_config(options, &normalized_path, recursion_limit)?; + if options.dry_run { + println!("{}", migration.success_message(true)); + } + } + + Ok(()) +} + +/// Move a TOML value from one map to another. +fn move_value(document: &mut DocumentMut, origin: &[&str], target: &[&str]) -> Result<(), String> { + // Find and remove the original item. + let (mut origin_key, mut origin_item) = (None, document.as_item_mut()); + for element in origin { + let table = match origin_item.as_table_like_mut() { + Some(table) => table, + None => panic!("Moving from unsupported TOML structure"), + }; + + let (key, item) = match table.get_key_value_mut(element) { + Some((key, item)) => (key, item), + None => return Ok(()), + }; + + dbg!(&key); + origin_key = Some(key); + origin_item = item; + + // Ensure no empty tables are left behind. + if let Some(table) = origin_item.as_table_mut() { + table.set_implicit(true) + } + } + + let origin_key_decor = + origin_key.map(|key| (key.leaf_decor().clone(), key.dotted_decor().clone())); + let origin_item = mem::replace(origin_item, Item::None); + + // Create all dependencies for the new location. + let mut target_item = document.as_item_mut(); + for (i, element) in target.iter().enumerate() { + let table = match target_item.as_table_like_mut() { + Some(table) => table, + None => panic!("Moving into unsupported TOML structure"), + }; + + if i + 1 == target.len() { + table.insert(element, origin_item); + // Move original key decorations. + if let Some((leaf, dotted)) = origin_key_decor { + let mut key = table.key_mut(element).unwrap(); + *key.leaf_decor_mut() = leaf; + *key.dotted_decor_mut() = dotted; + } + + break; + } else { + // Create missing parent tables. + target_item = target_item[element].or_insert(toml_edit::table()); + } + } + + Ok(()) +} + +/// Write migrated TOML to its target location. +fn write_results

(options: &MigrateOptions, path: P, toml: &str) -> Result<(), String> +where + P: AsRef + Debug, +{ + let path = path.as_ref(); + if options.dry_run && !options.silent { + // Output new content to STDOUT. + println!( + "\nv-----Start TOML for {path:?}-----v\n\n{toml}\n^-----End TOML for {path:?}-----^\n" + ); + } else if !options.dry_run { + // Atomically replace the configuration file. + let tmp = NamedTempFile::new_in(path.parent().unwrap()) + .map_err(|err| format!("could not create temporary file: {err}"))?; + fs::write(tmp.path(), toml).map_err(|err| format!("filesystem error: {err}"))?; + tmp.persist(path).map_err(|err| format!("atomic replacement failed: {err}"))?; + } + Ok(()) +} + +/// Performed migration mode. +enum Migration<'a> { + /// In-place TOML migration. + Toml(&'a Path), + /// YAML to TOML migration. + Yaml((&'a Path, String)), +} + +impl<'a> Migration<'a> { + /// Get the success message for this migration. + fn success_message(&self, import: bool) -> String { + match self { + Self::Yaml((original_path, new_path)) if import => { + format!("Successfully migrated import {original_path:?} to {new_path:?}") + }, + Self::Yaml((original_path, new_path)) => { + format!("Successfully migrated {original_path:?} to {new_path:?}") + }, + Self::Toml(original_path) if import => { + format!("Successfully migrated import {original_path:?}") + }, + Self::Toml(original_path) => format!("Successfully migrated {original_path:?}"), + } + } + + /// Get the file path after migration. + fn new_path(&self) -> String { + match self { + Self::Toml(path) => path.to_string_lossy().into(), + Self::Yaml((_, path)) => path.into(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn move_values() { + let input = r#" +# This is a root_value. +# +# Use it with care. +root_value = 3 + +[table] +table_value = 5 + +[preexisting] +not_moved = 9 + "#; + + let mut document = input.parse::().unwrap(); + + move_value(&mut document, &["root_value"], &["new_table", "root_value"]).unwrap(); + move_value(&mut document, &["table", "table_value"], &[ + "preexisting", + "subtable", + "new_name", + ]) + .unwrap(); + + let output = document.to_string(); + + let expected = r#" +[preexisting] +not_moved = 9 + +[preexisting.subtable] +new_name = 5 + +[new_table] + +# This is a root_value. +# +# Use it with care. +root_value = 3 + "#; + + assert_eq!(output, expected); + } +} diff --git a/alacritty/src/migrate/yaml.rs b/alacritty/src/migrate/yaml.rs new file mode 100644 index 00000000..9607e95e --- /dev/null +++ b/alacritty/src/migrate/yaml.rs @@ -0,0 +1,87 @@ +//! Migration of legacy YAML files to TOML. + +use std::path::Path; + +use toml::Value; + +use crate::cli::MigrateOptions; +use crate::config; +use crate::migrate::{migrate_config, migrate_toml, write_results}; + +/// Migrate a legacy YAML config to TOML. +pub fn migrate( + options: &MigrateOptions, + path: &Path, + recursion_limit: usize, + prefix: &str, +) -> Result { + // Try to parse the configuration file. + let mut config = match config::deserialize_config(path, !options.dry_run) { + Ok(config) => config, + Err(err) => return Err(format!("YAML parsing error: {err}")), + }; + + // Migrate config imports. + if !options.skip_imports { + migrate_imports(options, &mut config, path, recursion_limit)?; + } + + // Convert to TOML format. + let mut toml = toml::to_string(&config).map_err(|err| format!("conversion error: {err}"))?; + let new_path = format!("{prefix}.toml"); + + // Apply TOML migration, without recursing through imports. + toml = migrate_toml(toml)?.to_string(); + + // Write migrated TOML config. + write_results(options, &new_path, &toml)?; + + Ok(new_path) +} + +/// Migrate the imports of a config. +fn migrate_imports( + options: &MigrateOptions, + config: &mut Value, + base_path: &Path, + recursion_limit: usize, +) -> Result<(), String> { + let imports = match config::imports(config, base_path, recursion_limit) { + Ok(imports) => imports, + Err(err) => return Err(format!("import error: {err}")), + }; + + // Migrate the individual imports. + let mut new_imports = Vec::new(); + for import in imports { + let import = match import { + Ok(import) => import, + Err(err) => return Err(format!("import error: {err}")), + }; + + // Keep yaml import if path does not exist. + if !import.exists() { + if options.dry_run { + eprintln!("Keeping yaml config for nonexistent import: {import:?}"); + } + new_imports.push(Value::String(import.to_string_lossy().into())); + continue; + } + + let migration = migrate_config(options, &import, recursion_limit - 1)?; + + // Print success message. + if options.dry_run { + println!("{}", migration.success_message(true)); + } + + new_imports.push(Value::String(migration.new_path())); + } + + // Update the imports field. + if let Some(import) = config.get_mut("import") { + *import = Value::Array(new_imports); + } + + Ok(()) +} diff --git a/alacritty_config_derive/src/config_deserialize/de_struct.rs b/alacritty_config_derive/src/config_deserialize/de_struct.rs index d2a7dd82..ad38863e 100644 --- a/alacritty_config_derive/src/config_deserialize/de_struct.rs +++ b/alacritty_config_derive/src/config_deserialize/de_struct.rs @@ -155,6 +155,7 @@ fn field_deserializer(field_streams: &mut FieldStreams, field: &Field) -> Result if let Some(warning) = parsed.param { message = format!("{}; {}", message, warning.value()); } + message.push_str("\nUse `alacritty migrate` to automatically resolve it"); // Append stream to log deprecation/removal warning. match_assignment_stream.extend(quote! { diff --git a/alacritty_config_derive/tests/config.rs b/alacritty_config_derive/tests/config.rs index 27a968ed..be140cbe 100644 --- a/alacritty_config_derive/tests/config.rs +++ b/alacritty_config_derive/tests/config.rs @@ -1,4 +1,4 @@ -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, OnceLock}; use log::{Level, Log, Metadata, Record}; use serde::Deserialize; @@ -83,10 +83,8 @@ struct NewType(usize); #[test] fn config_deserialize() { - let logger = unsafe { - LOGGER = Some(Logger::default()); - LOGGER.as_mut().unwrap() - }; + static LOGGER: OnceLock = OnceLock::new(); + let logger = LOGGER.get_or_init(Logger::default); log::set_logger(logger).unwrap(); log::set_max_level(log::LevelFilter::Warn); @@ -134,15 +132,16 @@ fn config_deserialize() { ]); let warn_logs = logger.warn_logs.lock().unwrap(); assert_eq!(warn_logs.as_slice(), [ - "Config warning: field1 has been deprecated; use field2 instead", - "Config warning: enom_error has been deprecated", - "Config warning: gone has been removed; it's gone", + "Config warning: field1 has been deprecated; use field2 instead\nUse `alacritty migrate` \ + to automatically resolve it", + "Config warning: enom_error has been deprecated\nUse `alacritty migrate` to automatically \ + resolve it", + "Config warning: gone has been removed; it's gone\nUse `alacritty migrate` to \ + automatically resolve it", "Unused config key: field3", ]); } -static mut LOGGER: Option = None; - /// Logger storing all messages for later validation. #[derive(Default)] struct Logger {