uv/crates/uv-workspace/src/workspace.rs
konsti 2a17309782
Use colon more consistently in error messages (#3788)
Make the error messages more consistent with the format use elsewhere.
Split out from https://github.com/astral-sh/uv/pull/3705
2024-05-23 14:33:21 +02:00

160 lines
5.4 KiB
Rust

use std::path::{Path, PathBuf};
use tracing::debug;
use uv_fs::Simplified;
use uv_warnings::warn_user;
use crate::{Options, PyProjectToml};
/// Represents a project workspace that contains a set of options and a root path.
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct Workspace {
pub options: Options,
pub root: PathBuf,
}
impl Workspace {
/// Load the user [`Workspace`].
pub fn user() -> Result<Option<Self>, WorkspaceError> {
let Some(dir) = config_dir() else {
return Ok(None);
};
let root = dir.join("uv");
let file = root.join("uv.toml");
debug!("Loading user configuration from: `{}`", file.display());
match read_file(&file) {
Ok(options) => Ok(Some(Self { options, root })),
Err(WorkspaceError::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(err),
}
}
/// Find the [`Workspace`] for the given path.
///
/// The search starts at the given path and goes up the directory tree until a workspace is
/// found.
pub fn find(path: impl AsRef<Path>) -> Result<Option<Self>, WorkspaceError> {
for ancestor in path.as_ref().ancestors() {
match find_in_directory(ancestor) {
Ok(Some(options)) => {
return Ok(Some(Self {
options,
root: ancestor.to_path_buf(),
}))
}
Ok(None) => {
// Continue traversing the directory tree.
}
Err(err @ WorkspaceError::PyprojectToml(..)) => {
// If we see an invalid `pyproject.toml`, warn but continue.
warn_user!("{err}");
}
Err(err) => {
// Otherwise, warn and stop.
return Err(err);
}
}
}
Ok(None)
}
/// Load a [`Workspace`] from a `uv.toml` file.
pub fn from_file(path: impl AsRef<Path>) -> Result<Self, WorkspaceError> {
Ok(Self {
options: read_file(path.as_ref())?,
root: path.as_ref().parent().unwrap().to_path_buf(),
})
}
}
/// Returns the path to the user configuration directory.
///
/// This is similar to the `config_dir()` returned by the `dirs` crate, but it uses the
/// `XDG_CONFIG_HOME` environment variable on both Linux _and_ macOS, rather than the
/// `Application Support` directory on macOS.
fn config_dir() -> Option<PathBuf> {
// On Windows, use, e.g., C:\Users\Alice\AppData\Roaming
#[cfg(windows)]
{
dirs_sys::known_folder_roaming_app_data()
}
// On Linux and macOS, use, e.g., /home/alice/.config.
#[cfg(not(windows))]
{
std::env::var_os("XDG_CONFIG_HOME")
.and_then(dirs_sys::is_absolute_path)
.or_else(|| dirs_sys::home_dir().map(|path| path.join(".config")))
}
}
/// Read a `uv.toml` or `pyproject.toml` file in the given directory.
fn find_in_directory(dir: &Path) -> Result<Option<Options>, WorkspaceError> {
// Read a `uv.toml` file in the current directory.
let path = dir.join("uv.toml");
match fs_err::read_to_string(&path) {
Ok(content) => {
let options: Options = toml::from_str(&content)
.map_err(|err| WorkspaceError::UvToml(path.user_display().to_string(), err))?;
debug!("Found workspace configuration at `{}`", path.display());
return Ok(Some(options));
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
// Read a `pyproject.toml` file in the current directory.
let path = dir.join("pyproject.toml");
match fs_err::read_to_string(&path) {
Ok(content) => {
// Parse, but skip any `pyproject.toml` that doesn't have a `[tool.uv]` section.
let pyproject: PyProjectToml = toml::from_str(&content).map_err(|err| {
WorkspaceError::PyprojectToml(path.user_display().to_string(), err)
})?;
let Some(tool) = pyproject.tool else {
debug!(
"Skipping `pyproject.toml` in `{}` (no `[tool]` section)",
dir.display()
);
return Ok(None);
};
let Some(options) = tool.uv else {
debug!(
"Skipping `pyproject.toml` in `{}` (no `[tool.uv]` section)",
dir.display()
);
return Ok(None);
};
debug!("Found workspace configuration at `{}`", path.display());
return Ok(Some(options));
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
Ok(None)
}
/// Load [`Options`] from a `uv.toml` file.
fn read_file(path: &Path) -> Result<Options, WorkspaceError> {
let content = fs_err::read_to_string(path)?;
let options: Options = toml::from_str(&content)
.map_err(|err| WorkspaceError::UvToml(path.user_display().to_string(), err))?;
Ok(options)
}
#[derive(thiserror::Error, Debug)]
pub enum WorkspaceError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("Failed to parse: `{0}`")]
PyprojectToml(String, #[source] toml::de::Error),
#[error("Failed to parse: `{0}`")]
UvToml(String, #[source] toml::de::Error),
}