From ba74b9ea93b676e7d0b84c386b87e69017a4c27b Mon Sep 17 00:00:00 2001 From: John Mumm Date: Mon, 10 Mar 2025 12:05:05 +0100 Subject: [PATCH] Move config dir functions to public functions in uv_dirs (#12090) This PR moves functions for finding user- and system-level config directories to public functions in `uv_fs::config`. This will allow them to be used in future work without duplicating code. --- Cargo.lock | 8 +- crates/uv-dirs/Cargo.toml | 6 ++ crates/uv-dirs/src/lib.rs | 187 +++++++++++++++++++++++++++++++++- crates/uv-fs/Cargo.toml | 1 + crates/uv-settings/Cargo.toml | 4 +- crates/uv-settings/src/lib.rs | 184 +-------------------------------- 6 files changed, 200 insertions(+), 190 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a52eabf07..a5f2f4a8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4928,7 +4928,11 @@ dependencies = [ name = "uv-dirs" version = "0.0.1" dependencies = [ + "assert_fs", "etcetera", + "fs-err 3.1.0", + "indoc", + "tracing", "uv-static", ] @@ -5614,11 +5618,8 @@ dependencies = [ name = "uv-settings" version = "0.0.1" dependencies = [ - "assert_fs", "clap", - "etcetera", "fs-err 3.1.0", - "indoc", "schemars", "serde", "textwrap", @@ -5628,6 +5629,7 @@ dependencies = [ "url", "uv-cache-info", "uv-configuration", + "uv-dirs", "uv-distribution-types", "uv-fs", "uv-install-wheel", diff --git a/crates/uv-dirs/Cargo.toml b/crates/uv-dirs/Cargo.toml index 408c101fd..e28d96aa9 100644 --- a/crates/uv-dirs/Cargo.toml +++ b/crates/uv-dirs/Cargo.toml @@ -20,3 +20,9 @@ workspace = true uv-static = { workspace = true } etcetera = { workspace = true } +fs-err = { workspace = true } +tracing = { workspace = true } + +[dev-dependencies] +assert_fs = { version = "1.1.2" } +indoc = { workspace = true } diff --git a/crates/uv-dirs/src/lib.rs b/crates/uv-dirs/src/lib.rs index 464f95eae..23c39b9b0 100644 --- a/crates/uv-dirs/src/lib.rs +++ b/crates/uv-dirs/src/lib.rs @@ -1,4 +1,8 @@ -use std::{ffi::OsString, path::PathBuf}; +use std::{ + env, + ffi::OsString, + path::{Path, PathBuf}, +}; use etcetera::BaseStrategy; @@ -88,3 +92,184 @@ fn parse_path(path: OsString) -> Option { None } } + +/// Returns the path to the user configuration directory. +/// +/// On Windows, use, e.g., C:\Users\Alice\AppData\Roaming +/// On Linux and macOS, use `XDG_CONFIG_HOME` or $HOME/.config, e.g., /home/alice/.config. +pub fn user_config_dir() -> Option { + etcetera::choose_base_strategy() + .map(|dirs| dirs.config_dir()) + .ok() +} + +#[cfg(not(windows))] +fn locate_system_config_xdg(value: Option<&str>) -> Option { + // On Linux and macOS, read the `XDG_CONFIG_DIRS` environment variable. + + use std::path::Path; + let default = "/etc/xdg"; + let config_dirs = value.filter(|s| !s.is_empty()).unwrap_or(default); + + for dir in config_dirs.split(':').take_while(|s| !s.is_empty()) { + let uv_toml_path = Path::new(dir).join("uv").join("uv.toml"); + if uv_toml_path.is_file() { + return Some(uv_toml_path); + } + } + None +} + +#[cfg(windows)] +fn locate_system_config_windows(system_drive: impl AsRef) -> Option { + // On Windows, use `%SYSTEMDRIVE%\ProgramData\uv\uv.toml` (e.g., `C:\ProgramData`). + let candidate = system_drive + .as_ref() + .join("ProgramData") + .join("uv") + .join("uv.toml"); + candidate.as_path().is_file().then_some(candidate) +} + +/// Returns the path to the system configuration file. +/// +/// On Unix-like systems, uses the `XDG_CONFIG_DIRS` environment variable (falling back to +/// `/etc/xdg/uv/uv.toml` if unset or empty) and then `/etc/uv/uv.toml` +/// +/// On Windows, uses `%SYSTEMDRIVE%\ProgramData\uv\uv.toml`. +pub fn system_config_file() -> Option { + #[cfg(windows)] + { + env::var(EnvVars::SYSTEMDRIVE) + .ok() + .and_then(|system_drive| locate_system_config_windows(format!("{system_drive}\\"))) + } + + #[cfg(not(windows))] + { + if let Some(path) = + locate_system_config_xdg(env::var(EnvVars::XDG_CONFIG_DIRS).ok().as_deref()) + { + return Some(path); + } + + // Fallback to `/etc/uv/uv.toml` if `XDG_CONFIG_DIRS` is not set or no valid + // path is found. + let candidate = Path::new("/etc/uv/uv.toml"); + match candidate.try_exists() { + Ok(true) => Some(candidate.to_path_buf()), + Ok(false) => None, + Err(err) => { + tracing::warn!("Failed to query system configuration file: {err}"); + None + } + } + } +} + +#[cfg(test)] +mod test { + #[cfg(windows)] + use crate::locate_system_config_windows; + #[cfg(not(windows))] + use crate::locate_system_config_xdg; + + use assert_fs::fixture::FixtureError; + use assert_fs::prelude::*; + use indoc::indoc; + + #[test] + #[cfg(not(windows))] + fn test_locate_system_config_xdg() -> Result<(), FixtureError> { + // Write a `uv.toml` to a temporary directory. + let context = assert_fs::TempDir::new()?; + context.child("uv").child("uv.toml").write_str(indoc! { + r#" + [pip] + index-url = "https://test.pypi.org/simple" + "#, + })?; + + // None + assert_eq!(locate_system_config_xdg(None), None); + + // Empty string + assert_eq!(locate_system_config_xdg(Some("")), None); + + // Single colon + assert_eq!(locate_system_config_xdg(Some(":")), None); + + // Assert that the `system_config_file` function returns the correct path. + assert_eq!( + locate_system_config_xdg(Some(context.to_str().unwrap())).unwrap(), + context.child("uv").child("uv.toml").path() + ); + + // Write a separate `uv.toml` to a different directory. + let first = context.child("first"); + let first_config = first.child("uv").child("uv.toml"); + first_config.write_str("")?; + + assert_eq!( + locate_system_config_xdg(Some( + format!("{}:{}", first.to_string_lossy(), context.to_string_lossy()).as_str() + )) + .unwrap(), + first_config.path() + ); + + Ok(()) + } + + #[test] + #[cfg(unix)] + fn test_locate_system_config_xdg_unix_permissions() -> Result<(), FixtureError> { + let context = assert_fs::TempDir::new()?; + let config = context.child("uv").child("uv.toml"); + config.write_str("")?; + fs_err::set_permissions( + &context, + std::os::unix::fs::PermissionsExt::from_mode(0o000), + ) + .unwrap(); + + assert_eq!( + locate_system_config_xdg(Some(context.to_str().unwrap())), + None + ); + + Ok(()) + } + + #[test] + #[cfg(windows)] + fn test_windows_config() -> Result<(), FixtureError> { + // Write a `uv.toml` to a temporary directory. + let context = assert_fs::TempDir::new()?; + context + .child("ProgramData") + .child("uv") + .child("uv.toml") + .write_str(indoc! { r#" + [pip] + index-url = "https://test.pypi.org/simple" + "#})?; + + // This is typically only a drive (that is, letter and colon) but we + // allow anything, including a path to the test fixtures... + assert_eq!( + locate_system_config_windows(context.path()).unwrap(), + context + .child("ProgramData") + .child("uv") + .child("uv.toml") + .path() + ); + + // This does not have a `ProgramData` child, so contains no config. + let context = assert_fs::TempDir::new()?; + assert_eq!(locate_system_config_windows(context.path()), None); + + Ok(()) + } +} diff --git a/crates/uv-fs/Cargo.toml b/crates/uv-fs/Cargo.toml index 2860dc8a8..47271be54 100644 --- a/crates/uv-fs/Cargo.toml +++ b/crates/uv-fs/Cargo.toml @@ -16,6 +16,7 @@ doctest = false workspace = true [dependencies] + dunce = { workspace = true } either = { workspace = true } encoding_rs_io = { workspace = true } diff --git a/crates/uv-settings/Cargo.toml b/crates/uv-settings/Cargo.toml index 33729e806..7c91eaa50 100644 --- a/crates/uv-settings/Cargo.toml +++ b/crates/uv-settings/Cargo.toml @@ -18,6 +18,7 @@ workspace = true [dependencies] uv-cache-info = { workspace = true, features = ["schemars"] } uv-configuration = { workspace = true, features = ["schemars", "clap"] } +uv-dirs = { workspace = true } uv-distribution-types = { workspace = true, features = ["schemars"] } uv-fs = { workspace = true } uv-install-wheel = { workspace = true, features = ["schemars", "clap"] } @@ -32,7 +33,6 @@ uv-static = { workspace = true } uv-warnings = { workspace = true } clap = { workspace = true } -etcetera = { workspace = true } fs-err = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true } @@ -46,5 +46,3 @@ url = { workspace = true } ignored = ["uv-options-metadata", "clap"] [dev-dependencies] -assert_fs = { version = "1.1.2" } -indoc = { workspace = true } diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index e4bc10840..d44cf3156 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -1,11 +1,8 @@ -use std::env; use std::ops::Deref; use std::path::{Path, PathBuf}; -use etcetera::BaseStrategy; - +use uv_dirs::{system_config_file, user_config_dir}; use uv_fs::Simplified; -use uv_static::EnvVars; use uv_warnings::warn_user; pub use crate::combine::*; @@ -180,78 +177,6 @@ impl From for FilesystemOptions { } } -/// Returns the path to the user configuration directory. -/// -/// On Windows, use, e.g., C:\Users\Alice\AppData\Roaming -/// On Linux and macOS, use `XDG_CONFIG_HOME` or $HOME/.config, e.g., /home/alice/.config. -fn user_config_dir() -> Option { - etcetera::choose_base_strategy() - .map(|dirs| dirs.config_dir()) - .ok() -} - -#[cfg(not(windows))] -fn locate_system_config_xdg(value: Option<&str>) -> Option { - // On Linux and macOS, read the `XDG_CONFIG_DIRS` environment variable. - let default = "/etc/xdg"; - let config_dirs = value.filter(|s| !s.is_empty()).unwrap_or(default); - - for dir in config_dirs.split(':').take_while(|s| !s.is_empty()) { - let uv_toml_path = Path::new(dir).join("uv").join("uv.toml"); - if uv_toml_path.is_file() { - return Some(uv_toml_path); - } - } - None -} - -#[cfg(windows)] -fn locate_system_config_windows(system_drive: impl AsRef) -> Option { - // On Windows, use `%SYSTEMDRIVE%\ProgramData\uv\uv.toml` (e.g., `C:\ProgramData`). - let candidate = system_drive - .as_ref() - .join("ProgramData") - .join("uv") - .join("uv.toml"); - candidate.as_path().is_file().then_some(candidate) -} - -/// Returns the path to the system configuration file. -/// -/// On Unix-like systems, uses the `XDG_CONFIG_DIRS` environment variable (falling back to -/// `/etc/xdg/uv/uv.toml` if unset or empty) and then `/etc/uv/uv.toml` -/// -/// On Windows, uses `%SYSTEMDRIVE%\ProgramData\uv\uv.toml`. -fn system_config_file() -> Option { - #[cfg(windows)] - { - env::var(EnvVars::SYSTEMDRIVE) - .ok() - .and_then(|system_drive| locate_system_config_windows(format!("{system_drive}\\"))) - } - - #[cfg(not(windows))] - { - if let Some(path) = - locate_system_config_xdg(env::var(EnvVars::XDG_CONFIG_DIRS).ok().as_deref()) - { - return Some(path); - } - - // Fallback to `/etc/uv/uv.toml` if `XDG_CONFIG_DIRS` is not set or no valid - // path is found. - let candidate = Path::new("/etc/uv/uv.toml"); - match candidate.try_exists() { - Ok(true) => Some(candidate.to_path_buf()), - Ok(false) => None, - Err(err) => { - tracing::warn!("Failed to query system configuration file: {err}"); - None - } - } - } -} - /// Load [`Options`] from a `uv.toml` file. fn read_file(path: &Path) -> Result { let content = fs_err::read_to_string(path)?; @@ -314,110 +239,3 @@ pub enum Error { #[error("Failed to parse: `{}`. The `{}` field is not allowed in a `uv.toml` file. `{}` is only applicable in the context of a project, and should be placed in a `pyproject.toml` file instead.", _0.user_display(), _1, _1)] PyprojectOnlyField(PathBuf, &'static str), } - -#[cfg(test)] -mod test { - #[cfg(windows)] - use crate::locate_system_config_windows; - #[cfg(not(windows))] - use crate::locate_system_config_xdg; - - use assert_fs::fixture::FixtureError; - use assert_fs::prelude::*; - use indoc::indoc; - - #[test] - #[cfg(not(windows))] - fn test_locate_system_config_xdg() -> Result<(), FixtureError> { - // Write a `uv.toml` to a temporary directory. - let context = assert_fs::TempDir::new()?; - context.child("uv").child("uv.toml").write_str(indoc! { - r#" - [pip] - index-url = "https://test.pypi.org/simple" - "#, - })?; - - // None - assert_eq!(locate_system_config_xdg(None), None); - - // Empty string - assert_eq!(locate_system_config_xdg(Some("")), None); - - // Single colon - assert_eq!(locate_system_config_xdg(Some(":")), None); - - // Assert that the `system_config_file` function returns the correct path. - assert_eq!( - locate_system_config_xdg(Some(context.to_str().unwrap())).unwrap(), - context.child("uv").child("uv.toml").path() - ); - - // Write a separate `uv.toml` to a different directory. - let first = context.child("first"); - let first_config = first.child("uv").child("uv.toml"); - first_config.write_str("")?; - - assert_eq!( - locate_system_config_xdg(Some( - format!("{}:{}", first.to_string_lossy(), context.to_string_lossy()).as_str() - )) - .unwrap(), - first_config.path() - ); - - Ok(()) - } - - #[test] - #[cfg(unix)] - fn test_locate_system_config_xdg_unix_permissions() -> Result<(), FixtureError> { - let context = assert_fs::TempDir::new()?; - let config = context.child("uv").child("uv.toml"); - config.write_str("")?; - fs_err::set_permissions( - &context, - std::os::unix::fs::PermissionsExt::from_mode(0o000), - ) - .unwrap(); - - assert_eq!( - locate_system_config_xdg(Some(context.to_str().unwrap())), - None - ); - - Ok(()) - } - - #[test] - #[cfg(windows)] - fn test_windows_config() -> Result<(), FixtureError> { - // Write a `uv.toml` to a temporary directory. - let context = assert_fs::TempDir::new()?; - context - .child("ProgramData") - .child("uv") - .child("uv.toml") - .write_str(indoc! { r#" - [pip] - index-url = "https://test.pypi.org/simple" - "#})?; - - // This is typically only a drive (that is, letter and colon) but we - // allow anything, including a path to the test fixtures... - assert_eq!( - locate_system_config_windows(context.path()).unwrap(), - context - .child("ProgramData") - .child("uv") - .child("uv.toml") - .path() - ); - - // This does not have a `ProgramData` child, so contains no config. - let context = assert_fs::TempDir::new()?; - assert_eq!(locate_system_config_windows(context.path()), None); - - Ok(()) - } -}