├── clippy.toml ├── .cargo └── config.toml ├── docs └── YAML.jpg ├── rust-toolchain.toml ├── src ├── cmd │ ├── mod.rs │ ├── dump.rs │ └── apply.rs ├── errors.rs ├── main.rs └── defaults.rs ├── rustfmt.toml ├── .gitignore ├── Justfile ├── dist-workspace.toml ├── LICENSE ├── Cargo.toml ├── README.md ├── .github └── workflows │ └── release.yml └── Cargo.lock /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.70.0" 2 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-Ctarget-cpu=native"] 3 | -------------------------------------------------------------------------------- /docs/YAML.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsully/macos-defaults/HEAD/docs/YAML.jpg -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = [ "rust-analyzer" ] 4 | -------------------------------------------------------------------------------- /src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod apply; 2 | pub mod dump; 3 | 4 | pub use apply::{apply_defaults, process_path}; 5 | pub use dump::dump; 6 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Unstable 2 | # blank_lines_lower_bound = 0 3 | edition = "2024" 4 | group_imports = "StdExternalCrate" 5 | imports_granularity = "Module" 6 | max_width = 180 7 | use_field_init_shorthand = true 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | default: build test 2 | 3 | build: 4 | @cargo build --all 5 | 6 | check: 7 | @cargo check --all 8 | 9 | format: 10 | @cargo fmt --all 11 | 12 | format-check: 13 | @cargo fmt --all -- --check 14 | 15 | lint: 16 | @cargo clippy --all -- -D clippy::dbg-macro -D warnings 17 | 18 | test: 19 | @cargo test --all 20 | 21 | patch: 22 | @cargo release version patch --execute 23 | 24 | minor: 25 | @cargo release version minor --execute 26 | 27 | major: 28 | @cargo release version major --execute 29 | 30 | udeps: 31 | RUSTC_BOOTSTRAP=1 cargo +nightly udeps --all-targets --backend depinfo 32 | -------------------------------------------------------------------------------- /dist-workspace.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["cargo:."] 3 | 4 | # Config for 'dist' 5 | [dist] 6 | # The preferred dist version to use in CI (Cargo.toml SemVer syntax) 7 | cargo-dist-version = "0.30.1" 8 | # CI backends to support 9 | ci = "github" 10 | # The installers to generate for each app 11 | installers = ["homebrew"] 12 | # A GitHub repo to push Homebrew formulas to 13 | tap = "dsully/homebrew-tap" 14 | # Target platforms to build apps for (Rust target-triple syntax) 15 | targets = ["aarch64-apple-darwin"] 16 | # Publish jobs to run in CI 17 | publish-jobs = ["homebrew"] 18 | # Which actions to run on pull requests 19 | pr-run-mode = "plan" 20 | 21 | # Use Apple Silicon runners. 22 | [dist.github-custom-runners] 23 | aarch64-apple-darwin = "macos-14" 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dan Sully 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | // NB: Most of this code originated from: https://github.com/gibfahn/up-rs, MIT & Apache 2.0 licensed. 2 | 3 | use camino::Utf8PathBuf; 4 | use thiserror::Error; 5 | 6 | #[derive(Error, Debug)] 7 | pub enum DefaultsError { 8 | #[error("Unable to create dir at: {path}.")] 9 | DirCreation { path: Utf8PathBuf, source: std::io::Error }, 10 | 11 | #[error("Unable to copy file. From: {from_path} To: {to_path}")] 12 | FileCopy { 13 | from_path: Utf8PathBuf, 14 | to_path: Utf8PathBuf, 15 | source: std::io::Error, 16 | }, 17 | 18 | #[error("Failed to read bytes from path {path}")] 19 | FileRead { path: Utf8PathBuf, source: std::io::Error }, 20 | 21 | #[error("Expected to find a plist dictionary, but found a {plist_type} instead.\nDomain: {domain:?}\nKey: {key:?}")] 22 | NotADictionary { domain: String, key: String, plist_type: &'static str }, 23 | 24 | #[error("Failed to read Plist file {path}.")] 25 | PlistRead { path: Utf8PathBuf, source: plist::Error }, 26 | 27 | #[error("Failed to write value to plist file {path}")] 28 | PlistWrite { path: Utf8PathBuf, source: plist::Error }, 29 | 30 | #[error("Failed to write a value to plist file {path} as sudo.")] 31 | PlistSudoWrite { path: Utf8PathBuf, source: std::io::Error }, 32 | 33 | #[error("Invalid YAML at '{path}'")] 34 | InvalidYaml { path: Utf8PathBuf, source: serde_yaml::Error }, 35 | 36 | #[error("Failed to serialize plist to YAML. Domain: {domain:?}")] 37 | SerializationFailed { domain: String, source: serde_yaml::Error }, 38 | 39 | #[error("Failed to deserialize the YAML file or string.")] 40 | DeserializationFailed { source: serde_yaml::Error }, 41 | 42 | #[error("Expected a domain, but didn't find one.")] 43 | MissingDomain {}, 44 | 45 | #[error("Unexpectedly empty option found.")] 46 | UnexpectedNone, 47 | 48 | #[error("Eyre error.")] 49 | EyreError { source: color_eyre::Report }, 50 | 51 | #[error("failed to split YAML file {path}")] 52 | YamlSplitError { path: Utf8PathBuf, source: yaml_split::YamlSplitError }, 53 | } 54 | -------------------------------------------------------------------------------- /src/cmd/dump.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::prelude::*; 3 | 4 | use camino::Utf8PathBuf; 5 | use color_eyre::eyre::Result; 6 | use log::{debug, trace, warn}; 7 | use plist::{Dictionary, Value}; 8 | use yaml_rust::{YamlEmitter, YamlLoader}; 9 | 10 | use crate::defaults::{get_plist_value_type, plist_path, replace_data_in_plist, MacOSDefaults, NS_GLOBAL_DOMAIN}; 11 | use crate::errors::DefaultsError as E; 12 | 13 | /// `dump` command. 14 | pub fn dump(current_host: bool, output: Option, global_domain: bool, domain: Option) -> Result<()> { 15 | // 16 | let domain = if global_domain { 17 | NS_GLOBAL_DOMAIN.to_owned() 18 | } else { 19 | domain.ok_or(E::MissingDomain {})? 20 | }; 21 | 22 | debug!("Domain: {domain:?}"); 23 | let plist_path = plist_path(&domain, current_host)?; 24 | debug!("Plist path: {plist_path}"); 25 | 26 | // TODO: Nicer error. 27 | let plist: Value = plist::from_file(&plist_path).map_err(|e| E::PlistRead { path: plist_path, source: e })?; 28 | 29 | trace!("Plist: {plist:?}"); 30 | 31 | // First pass. 32 | let plist = if serde_yaml::to_string(&plist).is_ok() { 33 | plist 34 | } else { 35 | warn!( 36 | "Serializing plist value to YAML failed, assuming this is because it contained binary \ 37 | data and replacing that with hex-encoded binary data. This is incorrect, but allows \ 38 | the output to be printed." 39 | ); 40 | let mut value = plist.clone(); 41 | 42 | replace_data_in_plist(&mut value).map_err(|e| E::EyreError { source: e })?; 43 | 44 | serde_yaml::to_string(&value).map_err(|e| E::SerializationFailed { 45 | domain: domain.clone(), 46 | source: e, 47 | })?; 48 | value 49 | }; 50 | 51 | // Sort the top level keys. 52 | let mut value = plist 53 | .as_dictionary() 54 | .ok_or_else(|| E::NotADictionary { 55 | domain: domain.clone(), 56 | key: "Unknown".to_owned(), 57 | plist_type: get_plist_value_type(&plist), 58 | })? 59 | .clone(); 60 | 61 | value.sort_keys(); 62 | 63 | let data = serde_yaml::to_value(Dictionary::from_iter(vec![(domain.clone(), Value::Dictionary(value))]))?; 64 | 65 | // Wrap in the container struct. 66 | let defaults = MacOSDefaults { 67 | description: Some(domain), 68 | current_host, 69 | kill: None, 70 | sudo: false, 71 | data: Some(data), 72 | }; 73 | 74 | // Round-trip for yamllint valid YAML. 75 | let yaml = round_trip_yaml(&defaults)?; 76 | 77 | match output { 78 | Some(path) => File::create(path)?.write(&yaml), 79 | None => std::io::stdout().write(&yaml), 80 | }?; 81 | 82 | Ok(()) 83 | } 84 | 85 | fn round_trip_yaml(defaults: &MacOSDefaults) -> Result> { 86 | // 87 | let mut buffer = Vec::new(); 88 | 89 | for doc in YamlLoader::load_from_str(&serde_yaml::to_string(&defaults)?)? { 90 | let mut content = String::new(); 91 | 92 | let mut emitter = YamlEmitter::new(&mut content); 93 | emitter.compact(false); 94 | emitter.dump(&doc).ok(); 95 | 96 | buffer.write_all(content.as_ref())?; 97 | } 98 | 99 | Ok(buffer) 100 | } 101 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all, clippy::pedantic, clippy::unwrap_used)] 2 | #![allow( 3 | clippy::module_name_repetitions, 4 | clippy::missing_errors_doc, 5 | clippy::missing_panics_doc, 6 | 7 | // Ignore clippy for the generated file from shadow-rs. 8 | // https://github.com/baoyachi/shadow-rs/issues/151 9 | clippy::non_ascii_literal, 10 | clippy::print_stdout, 11 | clippy::needless_raw_strings, 12 | clippy::needless_raw_string_hashes 13 | )] 14 | 15 | use std::fs; 16 | use std::io; 17 | 18 | use camino::Utf8PathBuf; 19 | use clap::crate_authors; 20 | use clap::{ArgGroup, CommandFactory, Parser, Subcommand, ValueHint}; 21 | use clap_complete::{generate, Shell as CompletionShell}; 22 | use color_eyre::eyre::Result; 23 | use shadow_rs::shadow; 24 | 25 | // https://crates.io/crates/shadow-rs 26 | shadow!(build); 27 | 28 | mod cmd; 29 | mod defaults; 30 | mod errors; 31 | 32 | use self::cmd::{apply_defaults, dump, process_path}; 33 | use crate::errors::DefaultsError as E; 34 | 35 | #[derive(Parser, Debug)] 36 | #[clap( 37 | author=crate_authors!(), 38 | version=build::PKG_VERSION, 39 | long_version=build::CLAP_LONG_VERSION, 40 | about="Generate and apply macOS defaults.", 41 | subcommand_required=true, 42 | arg_required_else_help=true, 43 | )] 44 | #[allow(clippy::upper_case_acronyms)] 45 | struct CLI { 46 | /// Don’t actually run anything. 47 | #[arg(short, long)] 48 | dry_run: bool, 49 | 50 | #[clap(flatten)] 51 | verbose: clap_verbosity_flag::Verbosity, 52 | 53 | /// Clap subcommand to run. 54 | #[clap(subcommand)] 55 | command: Commands, 56 | } 57 | 58 | #[derive(Debug, Subcommand)] 59 | pub(crate) enum Commands { 60 | /// Set macOS defaults in plist files. 61 | Apply { 62 | /// Sets the input file or path to use. 63 | #[arg(required = true, value_hint = ValueHint::FilePath)] 64 | path: Utf8PathBuf, 65 | 66 | /// If changes were applied, exit with this return code. 67 | #[clap(short, long, default_value = "0")] 68 | exit_code: i32, 69 | }, 70 | 71 | /// Generate shell completions to stdout. 72 | Completions { 73 | #[clap(value_enum)] 74 | shell: CompletionShell, 75 | }, 76 | 77 | /// Dump existing defaults as YAML. 78 | #[clap(group( 79 | ArgGroup::new("dump") 80 | .required(true) 81 | .args(&["domain", "global_domain"]), 82 | ))] 83 | Dump { 84 | /// Read from the current host. 85 | #[arg(short, long)] 86 | current_host: bool, 87 | 88 | /// Read from the global domain. 89 | #[clap(short, long)] 90 | global_domain: bool, 91 | 92 | /// Domain to generate. 93 | #[clap(short, long)] 94 | domain: Option, 95 | 96 | /// Path to YAML file for dump output. 97 | #[arg(value_hint = ValueHint::FilePath)] 98 | path: Option, 99 | }, 100 | } 101 | 102 | fn main() -> Result<()> { 103 | color_eyre::install()?; 104 | 105 | let cli = CLI::parse(); 106 | 107 | env_logger::Builder::new().filter_level(cli.verbose.log_level_filter()).init(); 108 | 109 | match cli.command { 110 | Commands::Apply { path, exit_code } => { 111 | // 112 | let mut changed = false; 113 | 114 | for p in process_path(path)? { 115 | fs::metadata(&p).map_err(|e| E::FileRead { path: p.clone(), source: e })?; 116 | 117 | if apply_defaults(&p)? { 118 | changed = true; 119 | } 120 | } 121 | 122 | std::process::exit(if changed { exit_code } else { 0 }); 123 | } 124 | Commands::Completions { shell } => { 125 | generate(shell, &mut CLI::command(), "macos-defaults", &mut io::stdout().lock()); 126 | Ok(()) 127 | } 128 | Commands::Dump { 129 | current_host, 130 | path, 131 | global_domain, 132 | domain, 133 | } => dump(current_host, path, global_domain, domain), 134 | }?; 135 | 136 | std::process::exit(0); 137 | } 138 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Dan Sully "] 3 | build = "build.rs" 4 | categories = [ 5 | "command-line-utilities", 6 | "config", 7 | "os::macos-apis", 8 | ] 9 | description = "Defaults setting for macOS" 10 | edition = "2024" 11 | homepage = "https://github.com/dsully/macos-defaults" 12 | keywords = ["declarative", "defaults", "macos", "user", "yaml"] 13 | license = "MIT" 14 | name = "macos-defaults" 15 | readme = "README.md" 16 | repository = "https://github.com/dsully/macos-defaults" 17 | version = "0.3.0" 18 | 19 | [dependencies] 20 | camino = "1.2.1" 21 | clap = { version = "~4.5.51", features = [ 22 | "cargo", 23 | "color", 24 | "derive", 25 | "suggestions", 26 | "wrap_help", 27 | ] } 28 | clap_complete = "4.5.60" 29 | clap-verbosity-flag = "3.0.4" 30 | color-eyre = "0.6.5" 31 | colored = "3.0.0" 32 | dirs = "6.0.0" 33 | duct = "1.1.0" 34 | env_logger = "0.11.8" 35 | hex = "0.4.3" 36 | itertools = "0.14.0" 37 | log = "0.4.28" 38 | plist = "1.8.0" 39 | serde = { version = "1.0.228", features = ["derive"] } 40 | serde_yaml = "0.9.34" 41 | shadow-rs = { version = "1.4.0", default-features = false } 42 | sysinfo = "0.37.2" 43 | thiserror = "2.0.17" 44 | yaml-rust = "0.4.5" 45 | yaml-split = "0.4.0" 46 | 47 | [dev-dependencies] 48 | testresult = "0.4.1" 49 | 50 | [build-dependencies] 51 | shadow-rs = { version = "1.4.0", default-features = false, features = ["build"] } 52 | 53 | [profile.dev] 54 | debug = 0 55 | 56 | [profile.release] 57 | lto = true 58 | panic = "abort" 59 | codegen-units = 1 60 | 61 | # The profile that 'cargo dist' will build with 62 | [profile.dist] 63 | inherits = "release" 64 | opt-level = 3 65 | debug = false 66 | strip = "none" 67 | lto = true 68 | panic = "abort" 69 | incremental = false 70 | codegen-units = 1 71 | 72 | # https://stackoverflow.com/a/74545562/81120 73 | [lints.clippy] 74 | all = { level = "deny", priority = -1 } 75 | # Warn-level lints 76 | await_holding_lock = "warn" 77 | # Allowed lints 78 | cargo_common_metadata = { level = "allow", priority = 1 } 79 | char_lit_as_u8 = "warn" 80 | checked_conversions = "warn" 81 | complexity = { level = "deny", priority = -1 } 82 | correctness = { level = "deny", priority = -1 } 83 | dbg_macro = "warn" 84 | debug_assert_with_mut_call = "warn" 85 | doc_markdown = { level = "allow", priority = 1 } 86 | empty_enum = "warn" 87 | empty_line_after_doc_comments = "allow" 88 | empty_line_after_outer_attr = "allow" 89 | enum_glob_use = "warn" 90 | expl_impl_clone_on_copy = "warn" 91 | explicit_deref_methods = "warn" 92 | explicit_into_iter_loop = "warn" 93 | fallible_impl_from = "warn" 94 | filter_map_next = "warn" 95 | flat_map_option = "warn" 96 | float_cmp_const = "warn" 97 | fn_params_excessive_bools = "warn" 98 | from_iter_instead_of_collect = "warn" 99 | if_let_mutex = "warn" 100 | implicit_clone = "warn" 101 | implicit_return = { level = "allow", priority = 1 } 102 | imprecise_flops = "warn" 103 | inefficient_to_string = "warn" 104 | invalid_upcast_comparisons = "warn" 105 | large_digit_groups = "warn" 106 | large_stack_arrays = "warn" 107 | large_types_passed_by_value = "warn" 108 | let_unit_value = "warn" 109 | linkedlist = "warn" 110 | lossy_float_literal = "warn" 111 | macro_use_imports = "warn" 112 | manual_ok_or = "warn" 113 | map_flatten = "warn" 114 | map_unwrap_or = "warn" 115 | match_same_arms = "warn" 116 | match_wild_err_arm = "warn" 117 | match_wildcard_for_single_variants = "warn" 118 | mem_forget = "warn" 119 | missing_enforced_import_renames = "warn" 120 | missing_errors_doc = { level = "allow", priority = 1 } 121 | missing_panics_doc = { level = "allow", priority = 1 } 122 | module_name_repetitions = { level = "allow", priority = 1 } 123 | mut_mut = "warn" 124 | mutex_integer = "warn" 125 | needless_borrow = "warn" 126 | needless_continue = "warn" 127 | needless_for_each = "warn" 128 | option_option = "warn" 129 | path_buf_push_overwrite = "warn" 130 | pedantic = { level = "deny", priority = -1 } 131 | perf = { level = "deny", priority = -1 } 132 | ptr_as_ptr = "warn" 133 | rc_mutex = "warn" 134 | ref_option_ref = "warn" 135 | rest_pat_in_fully_bound_structs = "warn" 136 | same_functions_in_if_condition = "warn" 137 | semicolon_if_nothing_returned = "warn" 138 | single_match_else = "warn" 139 | string_add = "warn" 140 | string_add_assign = "warn" 141 | string_lit_as_bytes = "warn" 142 | struct_excessive_bools = "allow" 143 | style = { level = "deny", priority = -1 } 144 | suspicious = { level = "deny", priority = -1 } 145 | trait_duplication_in_bounds = "warn" 146 | unnested_or_patterns = "warn" 147 | unused_self = "warn" 148 | useless_transmute = "warn" 149 | verbose_file_reads = "warn" 150 | zero_sized_map_values = "warn" 151 | 152 | [lints.rust] 153 | rust_2018_idioms = { level = "deny", priority = -1 } 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # macos-defaults 2 | 3 | A tool for managing macOS defaults declaratively via YAML files. 4 | 5 | ## Install 6 | 7 | ### Homebrew 8 | 9 | ```shell 10 | brew install dsully/tap/macos-defaults 11 | ``` 12 | 13 | ### Source 14 | 15 | ```shell 16 | cargo install --git https://github.com/dsully/macos-defaults 17 | ``` 18 | 19 | ## Usage 20 | 21 | ### Dump a defaults domain to YAML 22 | 23 | ```shell 24 | # To stdout: 25 | macos-defaults dump -d com.apple.Dock 26 | 27 | # To a file: 28 | macos-defaults dump -d com.apple.Dock dock.yaml 29 | 30 | # Global domain 31 | macos-defaults dump -g 32 | ``` 33 | 34 | ### Apply defaults from a YAML file 35 | 36 | ```shell 37 | # From a single YAML file: 38 | macos-defaults apply dock.yaml 39 | 40 | # From a directory with YAML files & debug logging: 41 | macos-defaults apply -vvv ~/.config/macos-defaults/ 42 | ``` 43 | 44 | ### Generate shell completions 45 | 46 | ```shell 47 | macos-defaults completions [bash|fish|zsh] > ~/.config/fish/completions/macos-defaults.fish 48 | ``` 49 | 50 | See `macos-defaults --help` for more details. 51 | 52 | ## YAML Format 53 | 54 | ```yaml 55 | --- 56 | # This will be printed to stdout. 57 | description: Contacts 58 | 59 | # Use the currentHost hardware UUID to find the correct plist file. 60 | # https://apple.stackexchange.com/questions/353528/what-is-currenthost-for-in-defaults 61 | current_host: false 62 | 63 | # Send a SIGTERM to one or more processes if any defaults were changed. 64 | kill: ["Contacts", "cfprefsd"] 65 | 66 | # A nested map of plist domains to key/value pairs to set. 67 | data: 68 | # Show first name 69 | # 1 = before last name 70 | # 2 = after last name 71 | NSGlobalDomain: 72 | NSPersonNameDefaultDisplayNameOrder: 1 73 | 74 | # Sort by 75 | com.apple.AddressBook: 76 | ABNameSortingFormat: "sortingFirstName sortingLastName" 77 | 78 | # vCard format 79 | # false = 3.0 80 | # true = 2.1 81 | ABUse21vCardFormat: false 82 | 83 | # Enable private me vCard 84 | ABPrivateVCardFieldsEnabled: false 85 | 86 | # Export notes in vCards 87 | ABIncludeNotesInVCard: true 88 | 89 | # Export photos in vCards 90 | ABIncludePhotosInVCard: true 91 | --- 92 | # Multiple yaml docs in single file. 93 | description: Dock 94 | 95 | kill: ["Dock"] 96 | 97 | data: 98 | # Automatically hide and show the Dock 99 | com.apple.dock: 100 | autohide: true 101 | ``` 102 | 103 | You may also use full paths to `.plist` files instead of domain names. This is the only way to set values in /Library/Preferences/. 104 | 105 | ### Overwrite syntax 106 | 107 | By default, the YAML will be merged against existing domains. 108 | 109 | For example, the following config will leave any other keys on `DesktopViewSettings:IconViewSettings` untouched: 110 | ```yaml 111 | data: 112 | com.apple.finder: 113 | DesktopViewSettings: 114 | IconViewSettings: 115 | labelOnBottom: false # item info on right 116 | iconSize: 80.0 117 | ``` 118 | 119 | This can be overridden by adding the key `"!"` to a dict, which will delete any keys which are not specified. For example, the following config will delete all properties on the com.apple.finder domain except for DesktopViewSettings, and likewise, all properties on `IconViewSettings` except those specified. 120 | 121 | ```yaml 122 | data: 123 | com.apple.finder: 124 | "!": {} # overwrite! 125 | DesktopViewSettings: 126 | IconViewSettings: 127 | "!": {} # overwrite! 128 | labelOnBottom: false # item info on right 129 | iconSize: 80.0 130 | ``` 131 | 132 | This feature has the potential to erase important settings, so exercise caution. Running `macos-defaults apply` creates a backup of each modified plist at, for example, `~/Library/Preferences/com.apple.finder.plist.prev`. 133 | 134 | ### Array merge syntax 135 | 136 | If an array contains the element `"..."`, it will be replaced by the contents of the existing array. Arrays are treated like sets, so elements which already exist will not be added. 137 | 138 | For example, the following config: 139 | 140 | ```yaml 141 | data: 142 | org.my.test: 143 | aDict: 144 | }; 145 | anArray: ["foo", "...", "bar"] 146 | ``` 147 | 148 | * Prepend `"foo"` to `aDict:anArray`, if it doesn't already contain `"foo"`. 149 | * Append `"bar"` to `aDict:anArray`, if it doesn't already contain `"bar"`. 150 | 151 | ## Examples 152 | 153 | See my [dotfiles](https://github.com/dsully/dotfiles/tree/main/.data/macos-defaults) repository. 154 | 155 | ## On YAML 156 | 157 | ![Yelling At My Laptop](docs/YAML.jpg?raw=true) 158 | 159 | [YAML](https://yaml.org) is not a format I prefer, but out of common formats it unfortunately had the most properties I wanted. 160 | 161 | * [JSON](https://en.wikipedia.org/wiki/JSON) doesn't have comments and is overly verbose (JSONC/JSON5 is not common) 162 | 163 | * [XML](https://en.wikipedia.org/wiki/XML): No. 164 | 165 | * [INI](https://en.wikipedia.org/wiki/INI_file) is too limited. 166 | 167 | * [TOML](https://toml.io/en/) is overly verbose and is surprisingly not that easy to work with in Rust. Deeply nested maps are poorly handled. 168 | 169 | * [KDL](https://kdl.dev) is nice, but document oriented & needs struct annotations. Derive is implemented in the 3rd party [Knuffle](https://docs.rs/knuffel/latest/knuffel/) crate. 170 | 171 | * [RON](https://github.com/ron-rs/ron) is Rust specific, so editor support isn't there. 172 | 173 | * [KCL](https://kcl-lang.io), [CUE](https://cuelang.org), [HCL](https://github.com/hashicorp/hcl), too high level & not appropriate for the task. 174 | 175 | So YAML it is. 176 | 177 | ## Inspiration 178 | 179 | This tool was heavily inspired by and uses code from [up-rs](https://github.com/gibfahn/up-rs) 180 | -------------------------------------------------------------------------------- /src/cmd/apply.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::io::{BufReader, BufRead}; 3 | use std::ffi::OsStr; 4 | use std::fs::File; 5 | use std::os::unix::ffi::OsStrExt; 6 | 7 | use camino::Utf8PathBuf; 8 | use color_eyre::eyre::{eyre, Result, WrapErr}; 9 | use colored::Colorize; 10 | use log::{debug, error, trace}; 11 | use serde::{Deserialize, Serialize}; 12 | use sysinfo::{Signal, System}; 13 | use yaml_split::DocumentIterator; 14 | 15 | use crate::defaults::{write_defaults_values, MacOSDefaults}; 16 | use crate::errors::DefaultsError as E; 17 | 18 | /* 19 | // NB: Some of this code originated from: https://github.com/gibfahn/up-rs, MIT & Apache 2.0 licensed. 20 | 21 | Update macOS defaults. 22 | 23 | Make it easy for users to provide a list of defaults to update, and run all 24 | the updates at once. Also takes care of restarting any tools to pick up the 25 | config, or notifying the user if they need to log out or reboot. 26 | 27 | Note that manually editing .plist files on macOS (rather than using e.g. the `defaults` binary) 28 | may cause changes not to be picked up until `cfprefsd` is restarted 29 | ([more information](https://eclecticlight.co/2017/07/06/sticky-preferences-why-trashing-or-editing-them-may-not-change-anything/)). 30 | 31 | Work around this by adding `kill: ["cfprefsd"]` to the YAML file. 32 | 33 | ## Specifying preference domains 34 | 35 | For normal preference domains, you can directly specify the domain as a key, so to set `defaults read NSGlobalDomain com.apple.swipescrolldirection` you would use: 36 | 37 | ```yaml 38 | kill: ["cfprefsd"] 39 | data: 40 | NSGlobalDomain: 41 | com.apple.swipescrolldirection: false 42 | ``` 43 | 44 | You can also use a full path to a plist file (the `.plist` file extension is optional, as with the `defaults` command). 45 | 46 | ## Current Host modifications 47 | 48 | To modify defaults for the current host, you will need to add a `current_host: true` key/value pair: 49 | 50 | e.g. to set the preference returned by `defaults -currentHost read -globalDomain com.apple.mouse.tapBehavior` you would have: 51 | 52 | ```yaml 53 | kill: ["cfprefsd"] 54 | current_host: true 55 | data: 56 | NSGlobalDomain: 57 | # Enable Tap to Click for the current user. 58 | com.apple.mouse.tapBehavior: 1 59 | ``` 60 | 61 | ## Root-owned Defaults 62 | 63 | To write to files owned by root, set the `sudo: true` environment variable, and use the full path to the preferences file. 64 | 65 | ```yaml 66 | kill: cfprefsd 67 | sudo: true 68 | data: 69 | # System Preferences -> Users & Groups -> Login Options -> Show Input menu in login window 70 | /Library/Preferences/com.apple.loginwindow: 71 | showInputMenu: true 72 | 73 | # System Preferences -> Software Update -> Automatically keep my mac up to date 74 | /Library/Preferences/com.apple.SoftwareUpdate: 75 | AutomaticDownload: true 76 | ``` 77 | 78 | */ 79 | 80 | // Dummy struct before YAML deserialization attempt. 81 | #[derive(Debug, Default, Serialize, Deserialize)] 82 | struct DefaultsConfig(HashMap>); 83 | 84 | pub fn apply_defaults(path: &Utf8PathBuf) -> Result { 85 | // 86 | let file = File::open(path).map_err(|e| E::FileRead { 87 | path: path.to_owned(), 88 | source: e, 89 | })?; 90 | 91 | let reader = BufReader::new(file); 92 | 93 | trace!("Processing YAML documents from file: {path}"); 94 | 95 | let mut any_changed = false; 96 | 97 | for doc in DocumentIterator::new(reader) { 98 | let doc = doc.map_err(|e| E::YamlSplitError { 99 | path: path.to_owned(), 100 | source: e, 101 | })?; 102 | any_changed |= process_yaml_document(doc.as_bytes(), path)?; 103 | } 104 | 105 | Ok(any_changed) 106 | } 107 | 108 | fn process_yaml_document(doc: impl BufRead, path: &Utf8PathBuf) -> Result { 109 | let config: MacOSDefaults = serde_yaml::from_reader(doc).map_err(|e| E::InvalidYaml { 110 | path: path.to_owned(), 111 | source: e, 112 | })?; 113 | 114 | let maybe_data = config.data.ok_or_else(|| eyre!("Couldn't parse YAML data key in: {path}"))?; 115 | 116 | let defaults: DefaultsConfig = serde_yaml::from_value(maybe_data).map_err(|e| E::DeserializationFailed { source: e })?; 117 | 118 | debug!("Setting defaults"); 119 | 120 | // TODO: Get global CLI verbosity values. 121 | if let Some(description) = config.description { 122 | println!(" {} {}", "▶".green(), description.bold().white()); 123 | } 124 | 125 | let results: Vec<_> = defaults.0 126 | .into_iter() 127 | .map(|(domain, prefs)| write_defaults_values(&domain, prefs, config.current_host)) 128 | .collect(); 129 | 130 | let (passed, errors): (Vec<_>, Vec<_>) = results.into_iter().partition(Result::is_ok); 131 | 132 | let changed = passed.iter().any(|r| matches!(r, Ok(true))); 133 | 134 | if changed { 135 | if let Some(kill) = config.kill { 136 | for process in kill { 137 | println!(" {} Restarting: {}", "✖".blue(), process.white()); 138 | kill_process_by_name(&process); 139 | } 140 | } 141 | } 142 | 143 | if errors.is_empty() { 144 | return Ok(changed); 145 | } 146 | 147 | for error in &errors { 148 | error!("{error:?}"); 149 | } 150 | 151 | let mut errors_iter = errors.into_iter(); 152 | 153 | let first_error = errors_iter.next().ok_or(E::UnexpectedNone)??; 154 | 155 | Err(eyre!("{:?}", errors_iter.collect::>())).wrap_err(first_error) 156 | } 157 | 158 | fn kill_process_by_name(name: &str) { 159 | let mut sys = System::new(); 160 | sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true); 161 | 162 | for process in sys.processes_by_exact_name(OsStr::from_bytes(name.as_bytes())) { 163 | debug!("Process running: {} {}", process.pid(), process.name().to_string_lossy()); 164 | 165 | process.kill_with(Signal::Term); 166 | } 167 | } 168 | 169 | fn is_yaml(path: &Utf8PathBuf) -> bool { 170 | path.extension().map(str::to_ascii_lowercase).is_some_and(|ext| ext == "yml" || ext == "yaml") 171 | } 172 | 173 | pub fn process_path(path: Utf8PathBuf) -> Result> { 174 | match path { 175 | path if path.is_file() => Ok(vec![path]), 176 | path if path.is_dir() => { 177 | let mut files = path 178 | .read_dir_utf8()? 179 | .filter_map(Result::ok) 180 | .map(camino::Utf8DirEntry::into_path) 181 | .filter(is_yaml) 182 | .collect::>(); 183 | 184 | files.sort(); 185 | 186 | if files.is_empty() { 187 | Err(eyre!("No YAML files were found in path {path}.")) 188 | } else { 189 | Ok(files) 190 | } 191 | } 192 | _ => Err(eyre!("Couldn't read YAML from: {path}.")), 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist 2 | # 3 | # Copyright 2022-2024, axodotdev 4 | # SPDX-License-Identifier: MIT or Apache-2.0 5 | # 6 | # CI that: 7 | # 8 | # * checks for a Git Tag that looks like a release 9 | # * builds artifacts with dist (archives, installers, hashes) 10 | # * uploads those artifacts to temporary workflow zip 11 | # * on success, uploads the artifacts to a GitHub Release 12 | # 13 | # Note that the GitHub Release will be created with a generated 14 | # title/body based on your changelogs. 15 | 16 | name: Release 17 | permissions: 18 | "contents": "write" 19 | 20 | # This task will run whenever you push a git tag that looks like a version 21 | # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. 22 | # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where 23 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 24 | # must be a Cargo-style SemVer Version (must have at least major.minor.patch). 25 | # 26 | # If PACKAGE_NAME is specified, then the announcement will be for that 27 | # package (erroring out if it doesn't have the given version or isn't dist-able). 28 | # 29 | # If PACKAGE_NAME isn't specified, then the announcement will be for all 30 | # (dist-able) packages in the workspace with that version (this mode is 31 | # intended for workspaces with only one dist-able package, or with all dist-able 32 | # packages versioned/released in lockstep). 33 | # 34 | # If you push multiple tags at once, separate instances of this workflow will 35 | # spin up, creating an independent announcement for each one. However, GitHub 36 | # will hard limit this to 3 tags per commit, as it will assume more tags is a 37 | # mistake. 38 | # 39 | # If there's a prerelease-style suffix to the version, then the release(s) 40 | # will be marked as a prerelease. 41 | on: 42 | pull_request: 43 | push: 44 | tags: 45 | - '**[0-9]+.[0-9]+.[0-9]+*' 46 | 47 | jobs: 48 | # Run 'dist plan' (or host) to determine what tasks we need to do 49 | plan: 50 | runs-on: "ubuntu-22.04" 51 | outputs: 52 | val: ${{ steps.plan.outputs.manifest }} 53 | tag: ${{ !github.event.pull_request && github.ref_name || '' }} 54 | tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} 55 | publishing: ${{ !github.event.pull_request }} 56 | env: 57 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | steps: 59 | - uses: actions/checkout@v4 60 | with: 61 | persist-credentials: false 62 | submodules: recursive 63 | - name: Install dist 64 | # we specify bash to get pipefail; it guards against the `curl` command 65 | # failing. otherwise `sh` won't catch that `curl` returned non-0 66 | shell: bash 67 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.1/cargo-dist-installer.sh | sh" 68 | - name: Cache dist 69 | uses: actions/upload-artifact@v4 70 | with: 71 | name: cargo-dist-cache 72 | path: ~/.cargo/bin/dist 73 | # sure would be cool if github gave us proper conditionals... 74 | # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible 75 | # functionality based on whether this is a pull_request, and whether it's from a fork. 76 | # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* 77 | # but also really annoying to build CI around when it needs secrets to work right.) 78 | - id: plan 79 | run: | 80 | dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json 81 | echo "dist ran successfully" 82 | cat plan-dist-manifest.json 83 | echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" 84 | - name: "Upload dist-manifest.json" 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: artifacts-plan-dist-manifest 88 | path: plan-dist-manifest.json 89 | 90 | # Build and packages all the platform-specific things 91 | build-local-artifacts: 92 | name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) 93 | # Let the initial task tell us to not run (currently very blunt) 94 | needs: 95 | - plan 96 | if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} 97 | strategy: 98 | fail-fast: false 99 | # Target platforms/runners are computed by dist in create-release. 100 | # Each member of the matrix has the following arguments: 101 | # 102 | # - runner: the github runner 103 | # - dist-args: cli flags to pass to dist 104 | # - install-dist: expression to run to install dist on the runner 105 | # 106 | # Typically there will be: 107 | # - 1 "global" task that builds universal installers 108 | # - N "local" tasks that build each platform's binaries and platform-specific installers 109 | matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} 110 | runs-on: ${{ matrix.runner }} 111 | container: ${{ matrix.container && matrix.container.image || null }} 112 | env: 113 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 114 | BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json 115 | steps: 116 | - name: enable windows longpaths 117 | run: | 118 | git config --global core.longpaths true 119 | - uses: actions/checkout@v4 120 | with: 121 | persist-credentials: false 122 | submodules: recursive 123 | - name: Install Rust non-interactively if not already installed 124 | if: ${{ matrix.container }} 125 | run: | 126 | if ! command -v cargo > /dev/null 2>&1; then 127 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 128 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 129 | fi 130 | - name: Install dist 131 | run: ${{ matrix.install_dist.run }} 132 | # Get the dist-manifest 133 | - name: Fetch local artifacts 134 | uses: actions/download-artifact@v4 135 | with: 136 | pattern: artifacts-* 137 | path: target/distrib/ 138 | merge-multiple: true 139 | - name: Install dependencies 140 | run: | 141 | ${{ matrix.packages_install }} 142 | - name: Build artifacts 143 | run: | 144 | # Actually do builds and make zips and whatnot 145 | dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json 146 | echo "dist ran successfully" 147 | - id: cargo-dist 148 | name: Post-build 149 | # We force bash here just because github makes it really hard to get values up 150 | # to "real" actions without writing to env-vars, and writing to env-vars has 151 | # inconsistent syntax between shell and powershell. 152 | shell: bash 153 | run: | 154 | # Parse out what we just built and upload it to scratch storage 155 | echo "paths<> "$GITHUB_OUTPUT" 156 | dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" 157 | echo "EOF" >> "$GITHUB_OUTPUT" 158 | 159 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 160 | - name: "Upload artifacts" 161 | uses: actions/upload-artifact@v4 162 | with: 163 | name: artifacts-build-local-${{ join(matrix.targets, '_') }} 164 | path: | 165 | ${{ steps.cargo-dist.outputs.paths }} 166 | ${{ env.BUILD_MANIFEST_NAME }} 167 | 168 | # Build and package all the platform-agnostic(ish) things 169 | build-global-artifacts: 170 | needs: 171 | - plan 172 | - build-local-artifacts 173 | runs-on: "ubuntu-22.04" 174 | env: 175 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 176 | BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json 177 | steps: 178 | - uses: actions/checkout@v4 179 | with: 180 | persist-credentials: false 181 | submodules: recursive 182 | - name: Install cached dist 183 | uses: actions/download-artifact@v4 184 | with: 185 | name: cargo-dist-cache 186 | path: ~/.cargo/bin/ 187 | - run: chmod +x ~/.cargo/bin/dist 188 | # Get all the local artifacts for the global tasks to use (for e.g. checksums) 189 | - name: Fetch local artifacts 190 | uses: actions/download-artifact@v4 191 | with: 192 | pattern: artifacts-* 193 | path: target/distrib/ 194 | merge-multiple: true 195 | - id: cargo-dist 196 | shell: bash 197 | run: | 198 | dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json 199 | echo "dist ran successfully" 200 | 201 | # Parse out what we just built and upload it to scratch storage 202 | echo "paths<> "$GITHUB_OUTPUT" 203 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 204 | echo "EOF" >> "$GITHUB_OUTPUT" 205 | 206 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 207 | - name: "Upload artifacts" 208 | uses: actions/upload-artifact@v4 209 | with: 210 | name: artifacts-build-global 211 | path: | 212 | ${{ steps.cargo-dist.outputs.paths }} 213 | ${{ env.BUILD_MANIFEST_NAME }} 214 | # Determines if we should publish/announce 215 | host: 216 | needs: 217 | - plan 218 | - build-local-artifacts 219 | - build-global-artifacts 220 | # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine) 221 | if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} 222 | env: 223 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 224 | runs-on: "ubuntu-22.04" 225 | outputs: 226 | val: ${{ steps.host.outputs.manifest }} 227 | steps: 228 | - uses: actions/checkout@v4 229 | with: 230 | persist-credentials: false 231 | submodules: recursive 232 | - name: Install cached dist 233 | uses: actions/download-artifact@v4 234 | with: 235 | name: cargo-dist-cache 236 | path: ~/.cargo/bin/ 237 | - run: chmod +x ~/.cargo/bin/dist 238 | # Fetch artifacts from scratch-storage 239 | - name: Fetch artifacts 240 | uses: actions/download-artifact@v4 241 | with: 242 | pattern: artifacts-* 243 | path: target/distrib/ 244 | merge-multiple: true 245 | - id: host 246 | shell: bash 247 | run: | 248 | dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json 249 | echo "artifacts uploaded and released successfully" 250 | cat dist-manifest.json 251 | echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" 252 | - name: "Upload dist-manifest.json" 253 | uses: actions/upload-artifact@v4 254 | with: 255 | # Overwrite the previous copy 256 | name: artifacts-dist-manifest 257 | path: dist-manifest.json 258 | # Create a GitHub Release while uploading all files to it 259 | - name: "Download GitHub Artifacts" 260 | uses: actions/download-artifact@v4 261 | with: 262 | pattern: artifacts-* 263 | path: artifacts 264 | merge-multiple: true 265 | - name: Cleanup 266 | run: | 267 | # Remove the granular manifests 268 | rm -f artifacts/*-dist-manifest.json 269 | - name: Create GitHub Release 270 | env: 271 | PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" 272 | ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" 273 | ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" 274 | RELEASE_COMMIT: "${{ github.sha }}" 275 | run: | 276 | # Write and read notes from a file to avoid quoting breaking things 277 | echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt 278 | 279 | gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* 280 | 281 | publish-homebrew-formula: 282 | needs: 283 | - plan 284 | - host 285 | runs-on: "ubuntu-22.04" 286 | env: 287 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 288 | PLAN: ${{ needs.plan.outputs.val }} 289 | GITHUB_USER: "axo bot" 290 | GITHUB_EMAIL: "admin+bot@axo.dev" 291 | if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} 292 | steps: 293 | - uses: actions/checkout@v4 294 | with: 295 | persist-credentials: true 296 | repository: "dsully/homebrew-tap" 297 | token: ${{ secrets.HOMEBREW_TAP_TOKEN }} 298 | # So we have access to the formula 299 | - name: Fetch homebrew formulae 300 | uses: actions/download-artifact@v4 301 | with: 302 | pattern: artifacts-* 303 | path: Formula/ 304 | merge-multiple: true 305 | # This is extra complex because you can make your Formula name not match your app name 306 | # so we need to find releases with a *.rb file, and publish with that filename. 307 | - name: Commit formula files 308 | run: | 309 | git config --global user.name "${GITHUB_USER}" 310 | git config --global user.email "${GITHUB_EMAIL}" 311 | 312 | for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith(".rb")] | any)'); do 313 | filename=$(echo "$release" | jq '.artifacts[] | select(endswith(".rb"))' --raw-output) 314 | name=$(echo "$filename" | sed "s/\.rb$//") 315 | version=$(echo "$release" | jq .app_version --raw-output) 316 | 317 | export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH" 318 | brew update 319 | # We avoid reformatting user-provided data such as the app description and homepage. 320 | brew style --except-cops FormulaAudit/Homepage,FormulaAudit/Desc,FormulaAuditStrict --fix "Formula/${filename}" || true 321 | 322 | git add "Formula/${filename}" 323 | git commit -m "${name} ${version}" 324 | done 325 | git push 326 | 327 | announce: 328 | needs: 329 | - plan 330 | - host 331 | - publish-homebrew-formula 332 | # use "always() && ..." to allow us to wait for all publish jobs while 333 | # still allowing individual publish jobs to skip themselves (for prereleases). 334 | # "host" however must run to completion, no skipping allowed! 335 | if: ${{ always() && needs.host.result == 'success' && (needs.publish-homebrew-formula.result == 'skipped' || needs.publish-homebrew-formula.result == 'success') }} 336 | runs-on: "ubuntu-22.04" 337 | env: 338 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 339 | steps: 340 | - uses: actions/checkout@v4 341 | with: 342 | persist-credentials: false 343 | submodules: recursive 344 | -------------------------------------------------------------------------------- /src/defaults.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions for updating plist files. 2 | // 3 | // NB: Most of this code originated from: https://github.com/gibfahn/up-rs, MIT & Apache 2.0 licensed. 4 | 5 | use std::collections::HashMap; 6 | use std::fs::{self, File}; 7 | use std::io::Read; 8 | use std::mem; 9 | 10 | use camino::{Utf8Path, Utf8PathBuf}; 11 | use color_eyre::eyre::{eyre, Result}; 12 | use duct::cmd; 13 | use log::{debug, info, trace, warn}; 14 | use plist::{Dictionary, Value}; 15 | use serde::{Deserialize, Serialize}; 16 | 17 | use super::errors::DefaultsError as E; 18 | 19 | /// A value in an array that means "insert existing values here" 20 | const ELLIPSIS: &str = "..."; 21 | /// A value in a dictionary or domain that means "delete any keys not specified here". 22 | const BANG: &str = "!"; 23 | 24 | pub const NS_GLOBAL_DOMAIN: &str = "NSGlobalDomain"; 25 | 26 | #[derive(Debug, Serialize, Deserialize)] 27 | #[serde(deny_unknown_fields)] 28 | pub(super) struct MacOSDefaults { 29 | /// Description of the task. 30 | #[serde(skip_serializing_if = "Option::is_none")] 31 | pub description: Option, 32 | 33 | /// List of processes to kill if updates were needed. 34 | #[serde(skip_serializing_if = "Option::is_none")] 35 | pub kill: Option>, 36 | 37 | /// Set to true to prompt for superuser privileges before running. 38 | /// This will allow all subtasks that up executes in this iteration. 39 | #[serde(default = "default_false")] 40 | pub sudo: bool, 41 | 42 | /// Set to true to use the current host / hardware UUID for defaults. 43 | #[serde(default = "default_false")] 44 | pub current_host: bool, 45 | 46 | // This field must be the last one in order for the yaml serializer in the generate functions 47 | // to be able to serialise it properly. 48 | /// Set of data provided to the Run library. 49 | #[serde(skip_serializing_if = "Option::is_none")] 50 | pub data: Option, 51 | } 52 | 53 | /// Used for serde defaults above. 54 | const fn default_false() -> bool { 55 | false 56 | } 57 | 58 | /** 59 | Get the path to the plist file given a domain. 60 | 61 | This function does not handle root-owned preferences, e.g. those at `/Library/Preferences/`. 62 | 63 | ## Preferences Locations 64 | 65 | Working out the rules for preferences was fairly complex, but if you run `defaults domains` then 66 | you can work out which plist files are actually being read on the machine. 67 | 68 | As far as I can tell, the rules are: 69 | 70 | - `NSGlobalDomain` -> `~/Library/Preferences/.GlobalPreferences.plist` 71 | - `~/Library/Containers/{domain}/Data/Library/Preferences/{domain}.plist` if it exists. 72 | - `~/Library/Preferences/{domain}.plist` 73 | 74 | If none of these exist then create `~/Library/Preferences/{domain}.plist`. 75 | 76 | Note that `defaults domains` actually prints out 77 | `~/Library/Containers/{*}/Data/Library/Preferences/{*}.plist` (i.e. any plist file name inside 78 | a container folder), but `defaults read` only actually checks 79 | `~/Library/Containers/{domain}/Data/Library/Preferences/{domain}.plist` (a plist file whose name 80 | matches the container folder. 81 | 82 | ### Useful Resources 83 | 84 | - [macOS Containers and defaults](https://lapcatsoftware.com/articles/containers.html) 85 | - [Preference settings: where to find them in Mojave](https://eclecticlight.co/2019/08/28/preference-settings-where-to-find-them-in-mojave/) 86 | */ 87 | pub(super) fn plist_path(domain: &str, current_host: bool) -> Result { 88 | // User passed an absolute path -> use it directly. 89 | if domain.starts_with('/') { 90 | return Ok(Utf8PathBuf::from(domain)); 91 | } 92 | 93 | let home_dir = dirs::home_dir().ok_or_else(|| eyre!("Expected to be able to calculate the user's home directory."))?; 94 | let home_dir = Utf8PathBuf::try_from(home_dir)?; 95 | 96 | // Global Domain -> hard coded value. 97 | if domain == NS_GLOBAL_DOMAIN { 98 | let mut plist_path = home_dir; 99 | let filename = plist_filename(".GlobalPreferences", current_host)?; 100 | extend_with_prefs_folders(current_host, &mut plist_path, &filename); 101 | return Ok(plist_path); 102 | } 103 | 104 | // If passed com.foo.bar.plist, trim it to com.foo.bar 105 | let domain = domain.trim_end_matches(".plist"); 106 | let filename = plist_filename(domain, current_host)?; 107 | 108 | let mut sandboxed_plist_path = home_dir.clone(); 109 | sandboxed_plist_path.extend(&["Library", "Containers", domain, "Data"]); 110 | extend_with_prefs_folders(current_host, &mut sandboxed_plist_path, &filename); 111 | 112 | if sandboxed_plist_path.exists() { 113 | trace!("Sandboxed plist path exists."); 114 | return Ok(sandboxed_plist_path); 115 | } 116 | 117 | // let library_plist_path = Utf8PathBuf::from(format!("/Library/Preferences/{filename}")); 118 | // 119 | // if library_plist_path.exists() { 120 | // trace!("/Library plist path exists."); 121 | // return Ok(library_plist_path); 122 | // } 123 | 124 | trace!("Sandboxed plist path does not exist."); 125 | let mut plist_path = home_dir; 126 | extend_with_prefs_folders(current_host, &mut plist_path, &filename); 127 | 128 | // We return this even if it doesn't yet exist. 129 | Ok(plist_path) 130 | } 131 | 132 | /// Take a directory path, and add on the directories and files containing the application's 133 | /// preferences. Normally this is `./Library/Preferences/{domain}.plist`, but if `current_host` is 134 | /// `true`, then we need to look in the `ByHost` subfolder. 135 | fn extend_with_prefs_folders(current_host: bool, plist_path: &mut Utf8PathBuf, filename: &str) { 136 | if current_host { 137 | plist_path.extend(&["Library", "Preferences", "ByHost", filename]); 138 | } else { 139 | plist_path.extend(&["Library", "Preferences", filename]); 140 | } 141 | } 142 | 143 | /// Get the expected filename for a plist file. Normally it's just the preference name + `.plist`, 144 | /// but if it's a currentHost setup, then we need to include the current host UUID as well. 145 | fn plist_filename(domain: &str, current_host: bool) -> Result { 146 | if current_host { 147 | return Ok(format!( 148 | "{domain}.{hardware_uuid}.plist", 149 | hardware_uuid = get_hardware_uuid().map_err(|e| E::EyreError { source: e })? 150 | )); 151 | } 152 | 153 | Ok(format!("{domain}.plist")) 154 | } 155 | 156 | /// String representation of a plist Value's type. 157 | pub(super) fn get_plist_value_type(plist: &plist::Value) -> &'static str { 158 | match plist { 159 | p if p.as_array().is_some() => "array", 160 | p if p.as_boolean().is_some() => "boolean", 161 | p if p.as_date().is_some() => "date", 162 | p if p.as_real().is_some() => "real", 163 | p if p.as_signed_integer().is_some() => "signed_integer", 164 | p if p.as_unsigned_integer().is_some() => "unsigned_integer", 165 | p if p.as_string().is_some() => "string", 166 | p if p.as_dictionary().is_some() => "dictionary", 167 | p if p.as_data().is_some() => "data", 168 | _ => "unknown", 169 | } 170 | } 171 | 172 | /// Check whether a plist file is in the binary plist format or the XML plist format. 173 | fn is_binary(file: &Utf8Path) -> Result { 174 | let mut f = File::open(file).map_err(|e| E::FileRead { 175 | path: file.to_path_buf(), 176 | source: e, 177 | })?; 178 | let mut magic = [0; 8]; 179 | 180 | // read exactly 8 bytes 181 | f.read_exact(&mut magic).map_err(|e| E::FileRead { 182 | path: file.to_path_buf(), 183 | source: e, 184 | })?; 185 | 186 | Ok(&magic == b"bplist00") 187 | } 188 | 189 | /// Write a `HashMap` of key-value pairs to a plist file. 190 | pub(super) fn write_defaults_values(domain: &str, mut prefs: HashMap, current_host: bool) -> Result { 191 | let plist_path = plist_path(domain, current_host)?; 192 | 193 | debug!("Plist path: {plist_path}"); 194 | 195 | let plist_path_exists = plist_path.exists(); 196 | 197 | let mut plist_value: plist::Value = if plist_path_exists { 198 | plist::from_file(&plist_path).map_err(|e| E::PlistRead { 199 | path: plist_path.clone(), 200 | source: e, 201 | })? 202 | } else { 203 | plist::Value::Dictionary(Dictionary::new()) 204 | }; 205 | 206 | trace!("Plist: {plist_value:?}"); 207 | 208 | // Whether we changed anything. 209 | let mut values_changed = false; 210 | 211 | // If we have a key "!", wipe out the existing array. 212 | if prefs.contains_key(BANG) { 213 | plist_value = Value::from(Dictionary::new()); 214 | prefs.remove(BANG); 215 | } 216 | 217 | for (key, mut new_value) in prefs { 218 | let old_value = plist_value 219 | .as_dictionary() 220 | .ok_or_else(|| E::NotADictionary { 221 | domain: domain.to_owned(), 222 | key: key.clone(), 223 | plist_type: get_plist_value_type(&plist_value), 224 | })? 225 | .get(&key); 226 | 227 | debug!( 228 | "Working out whether we need to change the default {domain} {key}: {old_value:?} -> \ 229 | {new_value:?}" 230 | ); 231 | 232 | // Performs merge operations 233 | merge_value(&mut new_value, old_value); 234 | 235 | if let Some(old_value) = old_value { 236 | if old_value == &new_value { 237 | trace!("Nothing to do, values already match: {key:?} = {new_value:?}"); 238 | continue; 239 | } 240 | } 241 | 242 | values_changed = true; 243 | 244 | info!("Changing default {domain} {key}: {old_value:?} -> {new_value:?}",); 245 | 246 | let plist_type = get_plist_value_type(&plist_value); 247 | 248 | trace!("Plist type: {plist_type:?}"); 249 | 250 | plist_value 251 | .as_dictionary_mut() 252 | .ok_or_else(|| E::NotADictionary { 253 | domain: domain.to_owned(), 254 | key: key.clone(), 255 | plist_type, 256 | })? 257 | .insert(key, new_value); 258 | } 259 | 260 | if !values_changed { 261 | return Ok(values_changed); 262 | } 263 | 264 | if plist_path_exists { 265 | let backup_path = Utf8PathBuf::from(format!("{plist_path}.prev")); 266 | 267 | trace!("Backing up plist file {plist_path} -> {backup_path}",); 268 | 269 | // TODO: Handle sudo case and not being able to backup. 270 | fs::copy(&plist_path, &backup_path).map_err(|e| E::FileCopy { 271 | from_path: plist_path.clone(), 272 | to_path: backup_path.clone(), 273 | source: e, 274 | })?; 275 | } else { 276 | warn!("Defaults plist doesn't exist, creating it: {plist_path}"); 277 | 278 | let plist_dirpath = plist_path.parent().ok_or(E::UnexpectedNone)?; 279 | 280 | fs::create_dir_all(plist_dirpath).map_err(|e| E::DirCreation { 281 | path: plist_dirpath.to_owned(), 282 | source: e, 283 | })?; 284 | } 285 | 286 | write_plist(plist_path_exists, &plist_path, &plist_value)?; 287 | trace!("Plist updated at {plist_path}"); 288 | 289 | Ok(values_changed) 290 | } 291 | 292 | /// Write a plist file to a path. Will fall back to trying to use sudo if a normal write fails. 293 | fn write_plist(plist_path_exists: bool, plist_path: &Utf8Path, plist_value: &plist::Value) -> Result<(), E> { 294 | // 295 | let should_write_binary = !plist_path_exists || is_binary(plist_path)?; 296 | 297 | let write_result = if should_write_binary { 298 | trace!("Writing binary plist"); 299 | plist::to_file_binary(plist_path, &plist_value) 300 | } else { 301 | trace!("Writing xml plist"); 302 | plist::to_file_xml(plist_path, &plist_value) 303 | }; 304 | 305 | let Err(plist_error) = write_result else { 306 | return Ok(()); 307 | }; 308 | 309 | let io_error = match plist_error.into_io() { 310 | Ok(io_error) => io_error, 311 | Err(plist_error) => { 312 | return Err(E::PlistWrite { 313 | path: plist_path.to_path_buf(), 314 | source: plist_error, 315 | }) 316 | } 317 | }; 318 | 319 | trace!("Tried to write plist file, got IO error {io_error:?}, trying again with sudo"); 320 | 321 | let mut plist_bytes = Vec::new(); 322 | 323 | if should_write_binary { 324 | plist::to_writer_binary(&mut plist_bytes, &plist_value) 325 | } else { 326 | plist::to_writer_xml(&mut plist_bytes, &plist_value) 327 | } 328 | .map_err(|e| E::PlistWrite { 329 | path: Utf8Path::new("/dev/stdout").to_path_buf(), 330 | source: e, 331 | })?; 332 | 333 | cmd!("sudo", "tee", plist_path) 334 | .stdin_bytes(plist_bytes) 335 | .stdout_null() 336 | .run() 337 | .map_err(|e| E::PlistSudoWrite { 338 | path: plist_path.to_path_buf(), 339 | source: e, 340 | }) 341 | .map(|_| ())?; 342 | Ok(()) 343 | } 344 | 345 | /// Combines plist values using the following operations: 346 | /// * Merges dictionaries so new keys apply and old keys are let untouched 347 | /// * Replaces "..." in arrays with a copy of the old array (duplicates removed) 348 | /// 349 | /// This operation is performed recursively on dictionaries. 350 | fn merge_value(new_value: &mut Value, old_value: Option<&Value>) { 351 | deep_merge_dictionaries(new_value, old_value); 352 | replace_ellipsis_array(new_value, old_value); 353 | } 354 | 355 | /// Replace `...` values in an input array. 356 | /// You end up with: [, , ] 357 | /// But any duplicates between old and new values are removed, with the first value taking 358 | /// precedence. 359 | fn replace_ellipsis_array(new_value: &mut Value, old_value: Option<&Value>) { 360 | // 361 | let Value::Array(new_array) = new_value else { 362 | trace!("Value isn't an array, skipping ellipsis replacement..."); 363 | return; 364 | }; 365 | 366 | let ellipsis = plist::Value::from(ELLIPSIS); 367 | 368 | let Some(position) = new_array.iter().position(|x| x == &ellipsis) else { 369 | trace!("New value doesn't contain ellipsis, skipping ellipsis replacement..."); 370 | return; 371 | }; 372 | 373 | let Some(old_array) = old_value.and_then(plist::Value::as_array) else { 374 | trace!("Old value wasn't an array, skipping ellipsis replacement..."); 375 | new_array.remove(position); 376 | return; 377 | }; 378 | 379 | let array_copy: Vec<_> = std::mem::take(new_array); 380 | 381 | trace!("Performing array ellipsis replacement..."); 382 | 383 | for element in array_copy { 384 | if element == ellipsis { 385 | for old_element in old_array { 386 | if new_array.contains(old_element) { 387 | continue; 388 | } 389 | new_array.push(old_element.clone()); 390 | } 391 | } else if !new_array.contains(&element) { 392 | new_array.push(element); 393 | } 394 | } 395 | } 396 | 397 | // Recursively merge dictionaries, unless the new value is empty `{}`. 398 | // If a dictionary 399 | // * is empty `{}` 400 | // * contains a key `{}` 401 | // Then the merge step will be skipped for it and its children. 402 | fn deep_merge_dictionaries(new_value: &mut Value, old_value: Option<&Value>) { 403 | // 404 | let Value::Dictionary(new_dict) = new_value else { 405 | trace!("New value is not a dictionary, Skipping merge..."); 406 | return; 407 | }; 408 | 409 | if new_dict.is_empty() { 410 | trace!("New value is an empty dictionary. Skipping merge..."); 411 | return; 412 | } 413 | 414 | // the "..." key is no longer used, and its merging behavior is performed by default. ignore it, for compatibility with older YAML. 415 | new_dict.remove(ELLIPSIS); 416 | 417 | let Some(old_dict) = old_value.and_then(plist::Value::as_dictionary) else { 418 | trace!("Old value wasn't a dict. Skipping merge..."); 419 | return; 420 | }; 421 | 422 | // for each value, recursively invoke this to merge any child dictionaries. 423 | // also perform array ellipsis replacement. 424 | // this occurs even if "!" is present. 425 | for (key, new_child_value) in &mut *new_dict { 426 | let old_child_value = old_dict.get(key); 427 | merge_value(new_child_value, old_child_value); 428 | } 429 | 430 | if new_dict.contains_key(BANG) { 431 | trace!("Dictionary contains key '!'. Skipping merge..."); 432 | new_dict.remove(BANG); 433 | return; 434 | } 435 | 436 | trace!("Performing deep merge..."); 437 | 438 | for (key, old_value) in old_dict { 439 | if !new_dict.contains_key(key) { 440 | new_dict.insert(key.clone(), old_value.clone()); 441 | } 442 | } 443 | } 444 | 445 | /// Get the hardware UUID of the current Mac. 446 | /// You can get the Hardware UUID from: 447 | /// 448 | fn get_hardware_uuid() -> Result { 449 | let raw_output = cmd!("ioreg", "-d2", "-a", "-c", "IOPlatformExpertDevice").read()?; 450 | let ioreg_output: IoregOutput = plist::from_bytes(raw_output.as_bytes())?; 451 | Ok(ioreg_output 452 | .io_registry_entry_children 453 | .into_iter() 454 | .next() 455 | .ok_or_else(|| eyre!("Failed to get the Hardware UUID for the current Mac."))? 456 | .io_platform_uuid) 457 | } 458 | 459 | /// XML output returned by `ioreg -d2 -a -c IOPlatformExpertDevice` 460 | #[derive(Debug, Clone, Deserialize, Serialize)] 461 | struct IoregOutput { 462 | /// The set of `IORegistry` entries. 463 | #[serde(rename = "IORegistryEntryChildren")] 464 | io_registry_entry_children: Vec, 465 | } 466 | 467 | /// A specific `IORegistry` entry. 468 | #[derive(Debug, Clone, Deserialize, Serialize)] 469 | struct IoRegistryEntryChildren { 470 | /// The platform UUID. 471 | #[serde(rename = "IOPlatformUUID")] 472 | io_platform_uuid: String, 473 | } 474 | 475 | /// Helper to allow serializing plists containing binary data to yaml. 476 | /// Replace binary data attributes to work around . 477 | pub fn replace_data_in_plist(value: &mut Value) -> Result<()> { 478 | let mut stringified_data_value = match value { 479 | Value::Array(arr) => { 480 | for el in arr.iter_mut() { 481 | replace_data_in_plist(el)?; 482 | } 483 | return Ok(()); 484 | } 485 | Value::Dictionary(dict) => { 486 | for (_, v) in dict.iter_mut() { 487 | replace_data_in_plist(v)?; 488 | } 489 | return Ok(()); 490 | } 491 | Value::Data(bytes) => Value::String(hex::encode(bytes)), 492 | _ => { 493 | return Ok(()); 494 | } 495 | }; 496 | mem::swap(value, &mut stringified_data_value); 497 | 498 | Ok(()) 499 | } 500 | 501 | #[cfg(test)] 502 | mod tests { 503 | use log::info; 504 | use testresult::TestResult; 505 | 506 | use crate::defaults::deep_merge_dictionaries; 507 | 508 | use super::{replace_ellipsis_array, NS_GLOBAL_DOMAIN}; 509 | 510 | #[test] 511 | fn plist_path_tests() -> TestResult { 512 | let home_dir = dirs::home_dir().expect("Expected to be able to calculate the user's home directory."); 513 | 514 | { 515 | let domain_path = super::plist_path(NS_GLOBAL_DOMAIN, false)?; 516 | assert_eq!(home_dir.join("Library/Preferences/.GlobalPreferences.plist"), domain_path); 517 | } 518 | 519 | { 520 | let mut expected_plist_path = home_dir.join( 521 | "Library/Containers/com.apple.Safari/Data/Library/Preferences/com.apple.Safari.\ 522 | plist", 523 | ); 524 | if !expected_plist_path.exists() { 525 | expected_plist_path = home_dir.join("Library/Preferences/com.apple.Safari.plist"); 526 | } 527 | let domain_path = super::plist_path("com.apple.Safari", false)?; 528 | assert_eq!(expected_plist_path, domain_path); 529 | } 530 | 531 | // Per-host preference (`current_host` is true). 532 | { 533 | let domain_path = super::plist_path(NS_GLOBAL_DOMAIN, true)?; 534 | let hardware_uuid = super::get_hardware_uuid()?; 535 | assert_eq!( 536 | home_dir.join(format!("Library/Preferences/ByHost/.GlobalPreferences.{hardware_uuid}.plist")), 537 | domain_path 538 | ); 539 | } 540 | 541 | // Per-host sandboxed preference (`current_host` is true and the sandboxed plist exists). 542 | { 543 | let domain_path = super::plist_path("com.apple.Safari", true)?; 544 | let hardware_uuid = super::get_hardware_uuid()?; 545 | assert_eq!( 546 | home_dir.join(format!( 547 | "Library/Containers/com.apple.Safari/Data/Library/Preferences/ByHost/com.\ 548 | apple.Safari.{hardware_uuid}.plist" 549 | )), 550 | domain_path 551 | ); 552 | } 553 | 554 | Ok(()) 555 | } 556 | 557 | #[test] 558 | fn test_get_hardware_uuid() -> TestResult { 559 | use duct::cmd; 560 | 561 | let system_profiler_output = cmd!("system_profiler", "SPHardwareDataType").read()?; 562 | 563 | let expected_value = system_profiler_output 564 | .lines() 565 | .find_map(|line| line.contains("UUID").then(|| line.split_whitespace().last().unwrap_or_default())) 566 | .unwrap_or_default(); 567 | 568 | let actual_value = super::get_hardware_uuid()?; 569 | assert_eq!(expected_value, actual_value); 570 | 571 | Ok(()) 572 | } 573 | 574 | #[test] 575 | fn test_serialize_binary() -> TestResult { 576 | // Modified version of ~/Library/Preferences/com.apple.humanunderstanding.plist 577 | let binary_plist_as_hex = "62706c6973743030d101025f10124861736847656e657261746f722e73616c744f10201111111122222222333333334444444455555555666666667777777788888888080b200000000000000101000000000000000300000000000000000000000000000043"; 578 | let expected_yaml = "HashGenerator.salt: \ 579 | '1111111122222222333333334444444455555555666666667777777788888888'\n"; 580 | 581 | let binary_plist = hex::decode(binary_plist_as_hex)?; 582 | 583 | let mut value: plist::Value = plist::from_bytes(&binary_plist)?; 584 | info!("Value before: {value:?}"); 585 | super::replace_data_in_plist(&mut value)?; 586 | info!("Value after: {value:?}"); 587 | let yaml_string = serde_yaml::to_string(&value)?; 588 | info!("Yaml value: {yaml_string}"); 589 | assert_eq!(expected_yaml, yaml_string); 590 | 591 | Ok(()) 592 | } 593 | 594 | #[test] 595 | fn test_deep_merge_dictionaries() { 596 | use plist::{Dictionary, Value}; 597 | 598 | let old_value = Dictionary::from_iter([ 599 | ("foo", Value::from(10)), // !!! takes precedence 600 | ("fub", 11.into()), // !!! 601 | ("bar", 12.into()), // ! 602 | ("baz", 13.into()), // ! 603 | ]) 604 | .into(); 605 | let mut new_value = Dictionary::from_iter([ 606 | ("bar", Value::from(22)), // !! 607 | ("baz", 23.into()), // !! takes precedence 608 | ]) 609 | .into(); 610 | 611 | deep_merge_dictionaries(&mut new_value, Some(&old_value)); 612 | 613 | let expected = Dictionary::from_iter([ 614 | ("foo", Value::from(10)), // from new 615 | ("fub", 11.into()), 616 | ("bar", 22.into()), // from old 617 | ("baz", 23.into()), 618 | ]) 619 | .into(); 620 | 621 | assert_eq!(new_value, expected); 622 | } 623 | 624 | #[test] 625 | fn test_replace_ellipsis_dict_nested() { 626 | use plist::{Dictionary, Value}; 627 | 628 | let old_value = Dictionary::from_iter([( 629 | "level_1", 630 | Dictionary::from_iter([( 631 | "level_2", 632 | Dictionary::from_iter([ 633 | ("foo", Value::from(10)), // 634 | ("bar", 20.into()), 635 | ("baz", 30.into()), 636 | ]), 637 | )]), 638 | )]) 639 | .into(); 640 | 641 | let mut new_value = Dictionary::from_iter([( 642 | "level_1", 643 | Dictionary::from_iter([( 644 | "level_2", 645 | Dictionary::from_iter([ 646 | ("baz", Value::from(90)), // 647 | ]), 648 | )]), 649 | )]) 650 | .into(); 651 | 652 | deep_merge_dictionaries(&mut new_value, Some(&old_value)); 653 | 654 | let expected = Dictionary::from_iter([( 655 | "level_1", 656 | Dictionary::from_iter([( 657 | "level_2", 658 | Dictionary::from_iter([ 659 | ("foo", Value::from(10)), // 660 | ("bar", 20.into()), 661 | ("baz", 90.into()), 662 | ]), 663 | )]), 664 | )]) 665 | .into(); 666 | 667 | assert_eq!(new_value, expected); 668 | } 669 | 670 | #[test] 671 | fn test_replace_ellipsis_dict_nested_bang() { 672 | use plist::{Dictionary, Value}; 673 | 674 | let old_value = Dictionary::from_iter([( 675 | "level_1", 676 | Dictionary::from_iter([( 677 | "level_2", 678 | Dictionary::from_iter([ 679 | ("foo", Value::from(10)), // 680 | ("bar", 20.into()), 681 | ("baz", 30.into()), 682 | ]), 683 | )]), 684 | )]) 685 | .into(); 686 | 687 | let mut new_value = Dictionary::from_iter([( 688 | "level_1", 689 | Dictionary::from_iter([( 690 | "level_2", 691 | Dictionary::from_iter([ 692 | ("!", Value::from("")), // 693 | ("baz", 90.into()), // 694 | ]), 695 | )]), 696 | )]) 697 | .into(); 698 | 699 | deep_merge_dictionaries(&mut new_value, Some(&old_value)); 700 | 701 | let expected = Dictionary::from_iter([( 702 | "level_1", 703 | Dictionary::from_iter([("level_2", Dictionary::from_iter([("baz", Value::from(90))]))]), 704 | )]) 705 | .into(); 706 | 707 | assert_eq!(new_value, expected); 708 | } 709 | 710 | #[test] 711 | fn test_replace_ellipsis_array() { 712 | let old_value = vec![ 713 | 10.into(), // ! 714 | 20.into(), // ! 715 | 30.into(), // ! 716 | 40.into(), // ! 717 | ] 718 | .into(); 719 | let mut new_value = vec![ 720 | 30.into(), // !!! 721 | 20.into(), // !!! 722 | "...".into(), 723 | 60.into(), // !! 724 | 50.into(), // !! 725 | 40.into(), // !! 726 | ] 727 | .into(); 728 | 729 | replace_ellipsis_array(&mut new_value, Some(&old_value)); 730 | 731 | let expected = vec![ 732 | 30.into(), // from new array before "..." 733 | 20.into(), 734 | 10.into(), // from old array 735 | 40.into(), 736 | 60.into(), // from new array after "..." 737 | 50.into(), 738 | ] 739 | .into(); 740 | 741 | assert_eq!(new_value, expected); 742 | } 743 | } 744 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.25.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.4" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "android_system_properties" 31 | version = "0.1.5" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 34 | dependencies = [ 35 | "libc", 36 | ] 37 | 38 | [[package]] 39 | name = "anstream" 40 | version = "0.6.21" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" 43 | dependencies = [ 44 | "anstyle", 45 | "anstyle-parse", 46 | "anstyle-query", 47 | "anstyle-wincon", 48 | "colorchoice", 49 | "is_terminal_polyfill", 50 | "utf8parse", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle" 55 | version = "1.0.13" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 58 | 59 | [[package]] 60 | name = "anstyle-parse" 61 | version = "0.2.7" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 64 | dependencies = [ 65 | "utf8parse", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-query" 70 | version = "1.1.4" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 73 | dependencies = [ 74 | "windows-sys 0.60.2", 75 | ] 76 | 77 | [[package]] 78 | name = "anstyle-wincon" 79 | version = "3.0.10" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 82 | dependencies = [ 83 | "anstyle", 84 | "once_cell_polyfill", 85 | "windows-sys 0.60.2", 86 | ] 87 | 88 | [[package]] 89 | name = "backtrace" 90 | version = "0.3.76" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" 93 | dependencies = [ 94 | "addr2line", 95 | "cfg-if", 96 | "libc", 97 | "miniz_oxide", 98 | "object", 99 | "rustc-demangle", 100 | "windows-link 0.2.1", 101 | ] 102 | 103 | [[package]] 104 | name = "base64" 105 | version = "0.22.1" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 108 | 109 | [[package]] 110 | name = "bitflags" 111 | version = "2.10.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 114 | 115 | [[package]] 116 | name = "bumpalo" 117 | version = "3.19.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 120 | 121 | [[package]] 122 | name = "camino" 123 | version = "1.2.1" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" 126 | 127 | [[package]] 128 | name = "cc" 129 | version = "1.2.45" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" 132 | dependencies = [ 133 | "find-msvc-tools", 134 | "shlex", 135 | ] 136 | 137 | [[package]] 138 | name = "cfg-if" 139 | version = "1.0.4" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 142 | 143 | [[package]] 144 | name = "clap" 145 | version = "4.5.51" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" 148 | dependencies = [ 149 | "clap_builder", 150 | "clap_derive", 151 | ] 152 | 153 | [[package]] 154 | name = "clap-verbosity-flag" 155 | version = "3.0.4" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "9d92b1fab272fe943881b77cc6e920d6543e5b1bfadbd5ed81c7c5a755742394" 158 | dependencies = [ 159 | "clap", 160 | "log", 161 | ] 162 | 163 | [[package]] 164 | name = "clap_builder" 165 | version = "4.5.51" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" 168 | dependencies = [ 169 | "anstream", 170 | "anstyle", 171 | "clap_lex", 172 | "strsim", 173 | "terminal_size", 174 | ] 175 | 176 | [[package]] 177 | name = "clap_complete" 178 | version = "4.5.60" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "8e602857739c5a4291dfa33b5a298aeac9006185229a700e5810a3ef7272d971" 181 | dependencies = [ 182 | "clap", 183 | ] 184 | 185 | [[package]] 186 | name = "clap_derive" 187 | version = "4.5.49" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" 190 | dependencies = [ 191 | "heck", 192 | "proc-macro2", 193 | "quote", 194 | "syn", 195 | ] 196 | 197 | [[package]] 198 | name = "clap_lex" 199 | version = "0.7.6" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" 202 | 203 | [[package]] 204 | name = "color-eyre" 205 | version = "0.6.5" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" 208 | dependencies = [ 209 | "backtrace", 210 | "color-spantrace", 211 | "eyre", 212 | "indenter", 213 | "once_cell", 214 | "owo-colors", 215 | "tracing-error", 216 | ] 217 | 218 | [[package]] 219 | name = "color-spantrace" 220 | version = "0.3.0" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" 223 | dependencies = [ 224 | "once_cell", 225 | "owo-colors", 226 | "tracing-core", 227 | "tracing-error", 228 | ] 229 | 230 | [[package]] 231 | name = "colorchoice" 232 | version = "1.0.4" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 235 | 236 | [[package]] 237 | name = "colored" 238 | version = "3.0.0" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" 241 | dependencies = [ 242 | "windows-sys 0.59.0", 243 | ] 244 | 245 | [[package]] 246 | name = "const_format" 247 | version = "0.2.35" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" 250 | dependencies = [ 251 | "const_format_proc_macros", 252 | ] 253 | 254 | [[package]] 255 | name = "const_format_proc_macros" 256 | version = "0.2.34" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" 259 | dependencies = [ 260 | "proc-macro2", 261 | "quote", 262 | "unicode-xid", 263 | ] 264 | 265 | [[package]] 266 | name = "core-foundation-sys" 267 | version = "0.8.7" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 270 | 271 | [[package]] 272 | name = "deranged" 273 | version = "0.5.5" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" 276 | dependencies = [ 277 | "powerfmt", 278 | ] 279 | 280 | [[package]] 281 | name = "dirs" 282 | version = "6.0.0" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" 285 | dependencies = [ 286 | "dirs-sys", 287 | ] 288 | 289 | [[package]] 290 | name = "dirs-sys" 291 | version = "0.5.0" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 294 | dependencies = [ 295 | "libc", 296 | "option-ext", 297 | "redox_users", 298 | "windows-sys 0.61.2", 299 | ] 300 | 301 | [[package]] 302 | name = "duct" 303 | version = "1.1.0" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "d7478638a31d1f1f3d6c9f5e57c76b906a04ac4879d6fd0fb6245bc88f73fd0b" 306 | dependencies = [ 307 | "libc", 308 | "os_pipe", 309 | "shared_child", 310 | "shared_thread", 311 | ] 312 | 313 | [[package]] 314 | name = "either" 315 | version = "1.15.0" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 318 | 319 | [[package]] 320 | name = "env_filter" 321 | version = "0.1.4" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" 324 | dependencies = [ 325 | "log", 326 | "regex", 327 | ] 328 | 329 | [[package]] 330 | name = "env_logger" 331 | version = "0.11.8" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 334 | dependencies = [ 335 | "anstream", 336 | "anstyle", 337 | "env_filter", 338 | "jiff", 339 | "log", 340 | ] 341 | 342 | [[package]] 343 | name = "equivalent" 344 | version = "1.0.2" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 347 | 348 | [[package]] 349 | name = "errno" 350 | version = "0.3.14" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 353 | dependencies = [ 354 | "libc", 355 | "windows-sys 0.61.2", 356 | ] 357 | 358 | [[package]] 359 | name = "eyre" 360 | version = "0.6.12" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" 363 | dependencies = [ 364 | "indenter", 365 | "once_cell", 366 | ] 367 | 368 | [[package]] 369 | name = "find-msvc-tools" 370 | version = "0.1.4" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" 373 | 374 | [[package]] 375 | name = "getrandom" 376 | version = "0.2.16" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 379 | dependencies = [ 380 | "cfg-if", 381 | "libc", 382 | "wasi", 383 | ] 384 | 385 | [[package]] 386 | name = "gimli" 387 | version = "0.32.3" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" 390 | 391 | [[package]] 392 | name = "hashbrown" 393 | version = "0.16.0" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" 396 | 397 | [[package]] 398 | name = "heck" 399 | version = "0.5.0" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 402 | 403 | [[package]] 404 | name = "hex" 405 | version = "0.4.3" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 408 | 409 | [[package]] 410 | name = "iana-time-zone" 411 | version = "0.1.64" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" 414 | dependencies = [ 415 | "android_system_properties", 416 | "core-foundation-sys", 417 | "iana-time-zone-haiku", 418 | "js-sys", 419 | "log", 420 | "wasm-bindgen", 421 | "windows-core", 422 | ] 423 | 424 | [[package]] 425 | name = "iana-time-zone-haiku" 426 | version = "0.1.2" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 429 | dependencies = [ 430 | "cc", 431 | ] 432 | 433 | [[package]] 434 | name = "indenter" 435 | version = "0.3.4" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" 438 | 439 | [[package]] 440 | name = "indexmap" 441 | version = "2.12.0" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" 444 | dependencies = [ 445 | "equivalent", 446 | "hashbrown", 447 | ] 448 | 449 | [[package]] 450 | name = "is_debug" 451 | version = "1.1.0" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "1fe266d2e243c931d8190177f20bf7f24eed45e96f39e87dc49a27b32d12d407" 454 | 455 | [[package]] 456 | name = "is_terminal_polyfill" 457 | version = "1.70.2" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 460 | 461 | [[package]] 462 | name = "itertools" 463 | version = "0.14.0" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 466 | dependencies = [ 467 | "either", 468 | ] 469 | 470 | [[package]] 471 | name = "itoa" 472 | version = "1.0.15" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 475 | 476 | [[package]] 477 | name = "jiff" 478 | version = "0.2.16" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" 481 | dependencies = [ 482 | "jiff-static", 483 | "log", 484 | "portable-atomic", 485 | "portable-atomic-util", 486 | "serde_core", 487 | ] 488 | 489 | [[package]] 490 | name = "jiff-static" 491 | version = "0.2.16" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" 494 | dependencies = [ 495 | "proc-macro2", 496 | "quote", 497 | "syn", 498 | ] 499 | 500 | [[package]] 501 | name = "js-sys" 502 | version = "0.3.82" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" 505 | dependencies = [ 506 | "once_cell", 507 | "wasm-bindgen", 508 | ] 509 | 510 | [[package]] 511 | name = "lazy_static" 512 | version = "1.5.0" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 515 | 516 | [[package]] 517 | name = "libc" 518 | version = "0.2.177" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 521 | 522 | [[package]] 523 | name = "libredox" 524 | version = "0.1.10" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" 527 | dependencies = [ 528 | "bitflags", 529 | "libc", 530 | ] 531 | 532 | [[package]] 533 | name = "linked-hash-map" 534 | version = "0.5.6" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 537 | 538 | [[package]] 539 | name = "linux-raw-sys" 540 | version = "0.11.0" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 543 | 544 | [[package]] 545 | name = "log" 546 | version = "0.4.28" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 549 | 550 | [[package]] 551 | name = "macos-defaults" 552 | version = "0.3.0" 553 | dependencies = [ 554 | "camino", 555 | "clap", 556 | "clap-verbosity-flag", 557 | "clap_complete", 558 | "color-eyre", 559 | "colored", 560 | "dirs", 561 | "duct", 562 | "env_logger", 563 | "hex", 564 | "itertools", 565 | "log", 566 | "plist", 567 | "serde", 568 | "serde_yaml", 569 | "shadow-rs", 570 | "sysinfo", 571 | "testresult", 572 | "thiserror 2.0.17", 573 | "yaml-rust", 574 | "yaml-split", 575 | ] 576 | 577 | [[package]] 578 | name = "memchr" 579 | version = "2.7.6" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 582 | 583 | [[package]] 584 | name = "miniz_oxide" 585 | version = "0.8.9" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 588 | dependencies = [ 589 | "adler2", 590 | ] 591 | 592 | [[package]] 593 | name = "ntapi" 594 | version = "0.4.1" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" 597 | dependencies = [ 598 | "winapi", 599 | ] 600 | 601 | [[package]] 602 | name = "num-conv" 603 | version = "0.1.0" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 606 | 607 | [[package]] 608 | name = "num_threads" 609 | version = "0.1.7" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 612 | dependencies = [ 613 | "libc", 614 | ] 615 | 616 | [[package]] 617 | name = "objc2-core-foundation" 618 | version = "0.3.2" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" 621 | dependencies = [ 622 | "bitflags", 623 | ] 624 | 625 | [[package]] 626 | name = "objc2-io-kit" 627 | version = "0.3.2" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" 630 | dependencies = [ 631 | "libc", 632 | "objc2-core-foundation", 633 | ] 634 | 635 | [[package]] 636 | name = "object" 637 | version = "0.37.3" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" 640 | dependencies = [ 641 | "memchr", 642 | ] 643 | 644 | [[package]] 645 | name = "once_cell" 646 | version = "1.21.3" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 649 | 650 | [[package]] 651 | name = "once_cell_polyfill" 652 | version = "1.70.2" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 655 | 656 | [[package]] 657 | name = "option-ext" 658 | version = "0.2.0" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 661 | 662 | [[package]] 663 | name = "os_pipe" 664 | version = "1.2.3" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" 667 | dependencies = [ 668 | "libc", 669 | "windows-sys 0.61.2", 670 | ] 671 | 672 | [[package]] 673 | name = "owo-colors" 674 | version = "4.2.3" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" 677 | 678 | [[package]] 679 | name = "pin-project-lite" 680 | version = "0.2.16" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 683 | 684 | [[package]] 685 | name = "plist" 686 | version = "1.8.0" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" 689 | dependencies = [ 690 | "base64", 691 | "indexmap", 692 | "quick-xml", 693 | "serde", 694 | "time", 695 | ] 696 | 697 | [[package]] 698 | name = "portable-atomic" 699 | version = "1.11.1" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 702 | 703 | [[package]] 704 | name = "portable-atomic-util" 705 | version = "0.2.4" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 708 | dependencies = [ 709 | "portable-atomic", 710 | ] 711 | 712 | [[package]] 713 | name = "powerfmt" 714 | version = "0.2.0" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 717 | 718 | [[package]] 719 | name = "proc-macro2" 720 | version = "1.0.103" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 723 | dependencies = [ 724 | "unicode-ident", 725 | ] 726 | 727 | [[package]] 728 | name = "quick-xml" 729 | version = "0.38.3" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" 732 | dependencies = [ 733 | "memchr", 734 | ] 735 | 736 | [[package]] 737 | name = "quote" 738 | version = "1.0.42" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 741 | dependencies = [ 742 | "proc-macro2", 743 | ] 744 | 745 | [[package]] 746 | name = "redox_users" 747 | version = "0.5.2" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" 750 | dependencies = [ 751 | "getrandom", 752 | "libredox", 753 | "thiserror 2.0.17", 754 | ] 755 | 756 | [[package]] 757 | name = "regex" 758 | version = "1.12.2" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 761 | dependencies = [ 762 | "aho-corasick", 763 | "memchr", 764 | "regex-automata", 765 | "regex-syntax", 766 | ] 767 | 768 | [[package]] 769 | name = "regex-automata" 770 | version = "0.4.13" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 773 | dependencies = [ 774 | "aho-corasick", 775 | "memchr", 776 | "regex-syntax", 777 | ] 778 | 779 | [[package]] 780 | name = "regex-syntax" 781 | version = "0.8.8" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 784 | 785 | [[package]] 786 | name = "rustc-demangle" 787 | version = "0.1.26" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" 790 | 791 | [[package]] 792 | name = "rustix" 793 | version = "1.1.2" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 796 | dependencies = [ 797 | "bitflags", 798 | "errno", 799 | "libc", 800 | "linux-raw-sys", 801 | "windows-sys 0.61.2", 802 | ] 803 | 804 | [[package]] 805 | name = "rustversion" 806 | version = "1.0.22" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 809 | 810 | [[package]] 811 | name = "ryu" 812 | version = "1.0.20" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 815 | 816 | [[package]] 817 | name = "serde" 818 | version = "1.0.228" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 821 | dependencies = [ 822 | "serde_core", 823 | "serde_derive", 824 | ] 825 | 826 | [[package]] 827 | name = "serde_core" 828 | version = "1.0.228" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 831 | dependencies = [ 832 | "serde_derive", 833 | ] 834 | 835 | [[package]] 836 | name = "serde_derive" 837 | version = "1.0.228" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 840 | dependencies = [ 841 | "proc-macro2", 842 | "quote", 843 | "syn", 844 | ] 845 | 846 | [[package]] 847 | name = "serde_yaml" 848 | version = "0.9.34+deprecated" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" 851 | dependencies = [ 852 | "indexmap", 853 | "itoa", 854 | "ryu", 855 | "serde", 856 | "unsafe-libyaml", 857 | ] 858 | 859 | [[package]] 860 | name = "shadow-rs" 861 | version = "1.4.0" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "72d18183cef626bce22836103349c7050d73db799be0171386b80947d157ae32" 864 | dependencies = [ 865 | "const_format", 866 | "is_debug", 867 | "time", 868 | "tzdb", 869 | ] 870 | 871 | [[package]] 872 | name = "sharded-slab" 873 | version = "0.1.7" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 876 | dependencies = [ 877 | "lazy_static", 878 | ] 879 | 880 | [[package]] 881 | name = "shared_child" 882 | version = "1.1.1" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" 885 | dependencies = [ 886 | "libc", 887 | "sigchld", 888 | "windows-sys 0.60.2", 889 | ] 890 | 891 | [[package]] 892 | name = "shared_thread" 893 | version = "0.2.0" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "52b86057fcb5423f5018e331ac04623e32d6b5ce85e33300f92c79a1973928b0" 896 | 897 | [[package]] 898 | name = "shlex" 899 | version = "1.3.0" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 902 | 903 | [[package]] 904 | name = "sigchld" 905 | version = "0.2.4" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" 908 | dependencies = [ 909 | "libc", 910 | "os_pipe", 911 | "signal-hook", 912 | ] 913 | 914 | [[package]] 915 | name = "signal-hook" 916 | version = "0.3.18" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" 919 | dependencies = [ 920 | "libc", 921 | "signal-hook-registry", 922 | ] 923 | 924 | [[package]] 925 | name = "signal-hook-registry" 926 | version = "1.4.6" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" 929 | dependencies = [ 930 | "libc", 931 | ] 932 | 933 | [[package]] 934 | name = "strsim" 935 | version = "0.11.1" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 938 | 939 | [[package]] 940 | name = "syn" 941 | version = "2.0.109" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" 944 | dependencies = [ 945 | "proc-macro2", 946 | "quote", 947 | "unicode-ident", 948 | ] 949 | 950 | [[package]] 951 | name = "sysinfo" 952 | version = "0.37.2" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" 955 | dependencies = [ 956 | "libc", 957 | "memchr", 958 | "ntapi", 959 | "objc2-core-foundation", 960 | "objc2-io-kit", 961 | "windows", 962 | ] 963 | 964 | [[package]] 965 | name = "terminal_size" 966 | version = "0.4.3" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" 969 | dependencies = [ 970 | "rustix", 971 | "windows-sys 0.60.2", 972 | ] 973 | 974 | [[package]] 975 | name = "testresult" 976 | version = "0.4.1" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "614b328ff036a4ef882c61570f72918f7e9c5bee1da33f8e7f91e01daee7e56c" 979 | 980 | [[package]] 981 | name = "thiserror" 982 | version = "1.0.69" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 985 | dependencies = [ 986 | "thiserror-impl 1.0.69", 987 | ] 988 | 989 | [[package]] 990 | name = "thiserror" 991 | version = "2.0.17" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 994 | dependencies = [ 995 | "thiserror-impl 2.0.17", 996 | ] 997 | 998 | [[package]] 999 | name = "thiserror-impl" 1000 | version = "1.0.69" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1003 | dependencies = [ 1004 | "proc-macro2", 1005 | "quote", 1006 | "syn", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "thiserror-impl" 1011 | version = "2.0.17" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 1014 | dependencies = [ 1015 | "proc-macro2", 1016 | "quote", 1017 | "syn", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "thread_local" 1022 | version = "1.1.9" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 1025 | dependencies = [ 1026 | "cfg-if", 1027 | ] 1028 | 1029 | [[package]] 1030 | name = "time" 1031 | version = "0.3.44" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" 1034 | dependencies = [ 1035 | "deranged", 1036 | "itoa", 1037 | "libc", 1038 | "num-conv", 1039 | "num_threads", 1040 | "powerfmt", 1041 | "serde", 1042 | "time-core", 1043 | "time-macros", 1044 | ] 1045 | 1046 | [[package]] 1047 | name = "time-core" 1048 | version = "0.1.6" 1049 | source = "registry+https://github.com/rust-lang/crates.io-index" 1050 | checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" 1051 | 1052 | [[package]] 1053 | name = "time-macros" 1054 | version = "0.2.24" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" 1057 | dependencies = [ 1058 | "num-conv", 1059 | "time-core", 1060 | ] 1061 | 1062 | [[package]] 1063 | name = "tracing" 1064 | version = "0.1.41" 1065 | source = "registry+https://github.com/rust-lang/crates.io-index" 1066 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1067 | dependencies = [ 1068 | "pin-project-lite", 1069 | "tracing-core", 1070 | ] 1071 | 1072 | [[package]] 1073 | name = "tracing-core" 1074 | version = "0.1.34" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 1077 | dependencies = [ 1078 | "once_cell", 1079 | "valuable", 1080 | ] 1081 | 1082 | [[package]] 1083 | name = "tracing-error" 1084 | version = "0.2.1" 1085 | source = "registry+https://github.com/rust-lang/crates.io-index" 1086 | checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" 1087 | dependencies = [ 1088 | "tracing", 1089 | "tracing-subscriber", 1090 | ] 1091 | 1092 | [[package]] 1093 | name = "tracing-subscriber" 1094 | version = "0.3.20" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" 1097 | dependencies = [ 1098 | "sharded-slab", 1099 | "thread_local", 1100 | "tracing-core", 1101 | ] 1102 | 1103 | [[package]] 1104 | name = "tz-rs" 1105 | version = "0.7.1" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "14eff19b8dc1ace5bf7e4d920b2628ae3837f422ff42210cb1567cbf68b5accf" 1108 | 1109 | [[package]] 1110 | name = "tzdb" 1111 | version = "0.7.2" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "0be2ea5956f295449f47c0b825c5e109022ff1a6a53bb4f77682a87c2341fbf5" 1114 | dependencies = [ 1115 | "iana-time-zone", 1116 | "tz-rs", 1117 | "tzdb_data", 1118 | ] 1119 | 1120 | [[package]] 1121 | name = "tzdb_data" 1122 | version = "0.2.2" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "9c4c81d75033770e40fbd3643ce7472a1a9fd301f90b7139038228daf8af03ec" 1125 | dependencies = [ 1126 | "tz-rs", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "unicode-ident" 1131 | version = "1.0.22" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 1134 | 1135 | [[package]] 1136 | name = "unicode-xid" 1137 | version = "0.2.6" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 1140 | 1141 | [[package]] 1142 | name = "unsafe-libyaml" 1143 | version = "0.2.11" 1144 | source = "registry+https://github.com/rust-lang/crates.io-index" 1145 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" 1146 | 1147 | [[package]] 1148 | name = "utf8parse" 1149 | version = "0.2.2" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1152 | 1153 | [[package]] 1154 | name = "valuable" 1155 | version = "0.1.1" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 1158 | 1159 | [[package]] 1160 | name = "wasi" 1161 | version = "0.11.1+wasi-snapshot-preview1" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1164 | 1165 | [[package]] 1166 | name = "wasm-bindgen" 1167 | version = "0.2.105" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" 1170 | dependencies = [ 1171 | "cfg-if", 1172 | "once_cell", 1173 | "rustversion", 1174 | "wasm-bindgen-macro", 1175 | "wasm-bindgen-shared", 1176 | ] 1177 | 1178 | [[package]] 1179 | name = "wasm-bindgen-macro" 1180 | version = "0.2.105" 1181 | source = "registry+https://github.com/rust-lang/crates.io-index" 1182 | checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" 1183 | dependencies = [ 1184 | "quote", 1185 | "wasm-bindgen-macro-support", 1186 | ] 1187 | 1188 | [[package]] 1189 | name = "wasm-bindgen-macro-support" 1190 | version = "0.2.105" 1191 | source = "registry+https://github.com/rust-lang/crates.io-index" 1192 | checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" 1193 | dependencies = [ 1194 | "bumpalo", 1195 | "proc-macro2", 1196 | "quote", 1197 | "syn", 1198 | "wasm-bindgen-shared", 1199 | ] 1200 | 1201 | [[package]] 1202 | name = "wasm-bindgen-shared" 1203 | version = "0.2.105" 1204 | source = "registry+https://github.com/rust-lang/crates.io-index" 1205 | checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" 1206 | dependencies = [ 1207 | "unicode-ident", 1208 | ] 1209 | 1210 | [[package]] 1211 | name = "winapi" 1212 | version = "0.3.9" 1213 | source = "registry+https://github.com/rust-lang/crates.io-index" 1214 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1215 | dependencies = [ 1216 | "winapi-i686-pc-windows-gnu", 1217 | "winapi-x86_64-pc-windows-gnu", 1218 | ] 1219 | 1220 | [[package]] 1221 | name = "winapi-i686-pc-windows-gnu" 1222 | version = "0.4.0" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1225 | 1226 | [[package]] 1227 | name = "winapi-x86_64-pc-windows-gnu" 1228 | version = "0.4.0" 1229 | source = "registry+https://github.com/rust-lang/crates.io-index" 1230 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1231 | 1232 | [[package]] 1233 | name = "windows" 1234 | version = "0.61.3" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" 1237 | dependencies = [ 1238 | "windows-collections", 1239 | "windows-core", 1240 | "windows-future", 1241 | "windows-link 0.1.3", 1242 | "windows-numerics", 1243 | ] 1244 | 1245 | [[package]] 1246 | name = "windows-collections" 1247 | version = "0.2.0" 1248 | source = "registry+https://github.com/rust-lang/crates.io-index" 1249 | checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" 1250 | dependencies = [ 1251 | "windows-core", 1252 | ] 1253 | 1254 | [[package]] 1255 | name = "windows-core" 1256 | version = "0.61.2" 1257 | source = "registry+https://github.com/rust-lang/crates.io-index" 1258 | checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 1259 | dependencies = [ 1260 | "windows-implement", 1261 | "windows-interface", 1262 | "windows-link 0.1.3", 1263 | "windows-result", 1264 | "windows-strings", 1265 | ] 1266 | 1267 | [[package]] 1268 | name = "windows-future" 1269 | version = "0.2.1" 1270 | source = "registry+https://github.com/rust-lang/crates.io-index" 1271 | checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" 1272 | dependencies = [ 1273 | "windows-core", 1274 | "windows-link 0.1.3", 1275 | "windows-threading", 1276 | ] 1277 | 1278 | [[package]] 1279 | name = "windows-implement" 1280 | version = "0.60.2" 1281 | source = "registry+https://github.com/rust-lang/crates.io-index" 1282 | checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" 1283 | dependencies = [ 1284 | "proc-macro2", 1285 | "quote", 1286 | "syn", 1287 | ] 1288 | 1289 | [[package]] 1290 | name = "windows-interface" 1291 | version = "0.59.3" 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" 1293 | checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" 1294 | dependencies = [ 1295 | "proc-macro2", 1296 | "quote", 1297 | "syn", 1298 | ] 1299 | 1300 | [[package]] 1301 | name = "windows-link" 1302 | version = "0.1.3" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 1305 | 1306 | [[package]] 1307 | name = "windows-link" 1308 | version = "0.2.1" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 1311 | 1312 | [[package]] 1313 | name = "windows-numerics" 1314 | version = "0.2.0" 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" 1316 | checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" 1317 | dependencies = [ 1318 | "windows-core", 1319 | "windows-link 0.1.3", 1320 | ] 1321 | 1322 | [[package]] 1323 | name = "windows-result" 1324 | version = "0.3.4" 1325 | source = "registry+https://github.com/rust-lang/crates.io-index" 1326 | checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 1327 | dependencies = [ 1328 | "windows-link 0.1.3", 1329 | ] 1330 | 1331 | [[package]] 1332 | name = "windows-strings" 1333 | version = "0.4.2" 1334 | source = "registry+https://github.com/rust-lang/crates.io-index" 1335 | checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 1336 | dependencies = [ 1337 | "windows-link 0.1.3", 1338 | ] 1339 | 1340 | [[package]] 1341 | name = "windows-sys" 1342 | version = "0.59.0" 1343 | source = "registry+https://github.com/rust-lang/crates.io-index" 1344 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1345 | dependencies = [ 1346 | "windows-targets 0.52.6", 1347 | ] 1348 | 1349 | [[package]] 1350 | name = "windows-sys" 1351 | version = "0.60.2" 1352 | source = "registry+https://github.com/rust-lang/crates.io-index" 1353 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 1354 | dependencies = [ 1355 | "windows-targets 0.53.5", 1356 | ] 1357 | 1358 | [[package]] 1359 | name = "windows-sys" 1360 | version = "0.61.2" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 1363 | dependencies = [ 1364 | "windows-link 0.2.1", 1365 | ] 1366 | 1367 | [[package]] 1368 | name = "windows-targets" 1369 | version = "0.52.6" 1370 | source = "registry+https://github.com/rust-lang/crates.io-index" 1371 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1372 | dependencies = [ 1373 | "windows_aarch64_gnullvm 0.52.6", 1374 | "windows_aarch64_msvc 0.52.6", 1375 | "windows_i686_gnu 0.52.6", 1376 | "windows_i686_gnullvm 0.52.6", 1377 | "windows_i686_msvc 0.52.6", 1378 | "windows_x86_64_gnu 0.52.6", 1379 | "windows_x86_64_gnullvm 0.52.6", 1380 | "windows_x86_64_msvc 0.52.6", 1381 | ] 1382 | 1383 | [[package]] 1384 | name = "windows-targets" 1385 | version = "0.53.5" 1386 | source = "registry+https://github.com/rust-lang/crates.io-index" 1387 | checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 1388 | dependencies = [ 1389 | "windows-link 0.2.1", 1390 | "windows_aarch64_gnullvm 0.53.1", 1391 | "windows_aarch64_msvc 0.53.1", 1392 | "windows_i686_gnu 0.53.1", 1393 | "windows_i686_gnullvm 0.53.1", 1394 | "windows_i686_msvc 0.53.1", 1395 | "windows_x86_64_gnu 0.53.1", 1396 | "windows_x86_64_gnullvm 0.53.1", 1397 | "windows_x86_64_msvc 0.53.1", 1398 | ] 1399 | 1400 | [[package]] 1401 | name = "windows-threading" 1402 | version = "0.1.0" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" 1405 | dependencies = [ 1406 | "windows-link 0.1.3", 1407 | ] 1408 | 1409 | [[package]] 1410 | name = "windows_aarch64_gnullvm" 1411 | version = "0.52.6" 1412 | source = "registry+https://github.com/rust-lang/crates.io-index" 1413 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1414 | 1415 | [[package]] 1416 | name = "windows_aarch64_gnullvm" 1417 | version = "0.53.1" 1418 | source = "registry+https://github.com/rust-lang/crates.io-index" 1419 | checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 1420 | 1421 | [[package]] 1422 | name = "windows_aarch64_msvc" 1423 | version = "0.52.6" 1424 | source = "registry+https://github.com/rust-lang/crates.io-index" 1425 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1426 | 1427 | [[package]] 1428 | name = "windows_aarch64_msvc" 1429 | version = "0.53.1" 1430 | source = "registry+https://github.com/rust-lang/crates.io-index" 1431 | checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 1432 | 1433 | [[package]] 1434 | name = "windows_i686_gnu" 1435 | version = "0.52.6" 1436 | source = "registry+https://github.com/rust-lang/crates.io-index" 1437 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1438 | 1439 | [[package]] 1440 | name = "windows_i686_gnu" 1441 | version = "0.53.1" 1442 | source = "registry+https://github.com/rust-lang/crates.io-index" 1443 | checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 1444 | 1445 | [[package]] 1446 | name = "windows_i686_gnullvm" 1447 | version = "0.52.6" 1448 | source = "registry+https://github.com/rust-lang/crates.io-index" 1449 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1450 | 1451 | [[package]] 1452 | name = "windows_i686_gnullvm" 1453 | version = "0.53.1" 1454 | source = "registry+https://github.com/rust-lang/crates.io-index" 1455 | checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 1456 | 1457 | [[package]] 1458 | name = "windows_i686_msvc" 1459 | version = "0.52.6" 1460 | source = "registry+https://github.com/rust-lang/crates.io-index" 1461 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1462 | 1463 | [[package]] 1464 | name = "windows_i686_msvc" 1465 | version = "0.53.1" 1466 | source = "registry+https://github.com/rust-lang/crates.io-index" 1467 | checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 1468 | 1469 | [[package]] 1470 | name = "windows_x86_64_gnu" 1471 | version = "0.52.6" 1472 | source = "registry+https://github.com/rust-lang/crates.io-index" 1473 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1474 | 1475 | [[package]] 1476 | name = "windows_x86_64_gnu" 1477 | version = "0.53.1" 1478 | source = "registry+https://github.com/rust-lang/crates.io-index" 1479 | checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 1480 | 1481 | [[package]] 1482 | name = "windows_x86_64_gnullvm" 1483 | version = "0.52.6" 1484 | source = "registry+https://github.com/rust-lang/crates.io-index" 1485 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1486 | 1487 | [[package]] 1488 | name = "windows_x86_64_gnullvm" 1489 | version = "0.53.1" 1490 | source = "registry+https://github.com/rust-lang/crates.io-index" 1491 | checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 1492 | 1493 | [[package]] 1494 | name = "windows_x86_64_msvc" 1495 | version = "0.52.6" 1496 | source = "registry+https://github.com/rust-lang/crates.io-index" 1497 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1498 | 1499 | [[package]] 1500 | name = "windows_x86_64_msvc" 1501 | version = "0.53.1" 1502 | source = "registry+https://github.com/rust-lang/crates.io-index" 1503 | checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 1504 | 1505 | [[package]] 1506 | name = "yaml-rust" 1507 | version = "0.4.5" 1508 | source = "registry+https://github.com/rust-lang/crates.io-index" 1509 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 1510 | dependencies = [ 1511 | "linked-hash-map", 1512 | ] 1513 | 1514 | [[package]] 1515 | name = "yaml-split" 1516 | version = "0.4.0" 1517 | source = "registry+https://github.com/rust-lang/crates.io-index" 1518 | checksum = "9dab2bfe3b9aa09e8424e0e5139526c6a3857c4bd334d66b0453a357dd80fc58" 1519 | dependencies = [ 1520 | "thiserror 1.0.69", 1521 | ] 1522 | --------------------------------------------------------------------------------