1use std::io::{self, Write};
2use std::path::{Path, PathBuf};
3use std::str::FromStr;
4
5use fs_err as fs;
6use fs_err::File;
7use thiserror::Error;
8use tracing::{debug, warn};
9
10use uv_cache::Cache;
11use uv_dirs::user_executable_directory;
12use uv_fs::{LockedFile, Simplified};
13use uv_install_wheel::read_record_file;
14use uv_installer::SitePackages;
15use uv_normalize::{InvalidNameError, PackageName};
16use uv_pep440::Version;
17use uv_preview::Preview;
18use uv_python::{Interpreter, PythonEnvironment};
19use uv_state::{StateBucket, StateStore};
20use uv_static::EnvVars;
21use uv_virtualenv::remove_virtualenv;
22
23pub use receipt::ToolReceipt;
24pub use tool::{Tool, ToolEntrypoint};
25
26mod receipt;
27mod tool;
28
29#[derive(Debug, Clone)]
31pub struct ToolEnvironment {
32 environment: PythonEnvironment,
33 name: PackageName,
34}
35
36impl ToolEnvironment {
37 pub fn new(environment: PythonEnvironment, name: PackageName) -> Self {
38 Self { environment, name }
39 }
40
41 pub fn version(&self) -> Result<Version, Error> {
43 let site_packages = SitePackages::from_environment(&self.environment).map_err(|err| {
44 Error::EnvironmentRead(self.environment.root().to_path_buf(), err.to_string())
45 })?;
46 let packages = site_packages.get_packages(&self.name);
47 let package = packages
48 .first()
49 .ok_or_else(|| Error::MissingToolPackage(self.name.clone()))?;
50 Ok(package.version().clone())
51 }
52
53 pub fn into_environment(self) -> PythonEnvironment {
55 self.environment
56 }
57
58 pub fn environment(&self) -> &PythonEnvironment {
60 &self.environment
61 }
62}
63
64#[derive(Error, Debug)]
65pub enum Error {
66 #[error(transparent)]
67 Io(#[from] io::Error),
68 #[error("Failed to update `uv-receipt.toml` at {0}")]
69 ReceiptWrite(PathBuf, #[source] Box<toml_edit::ser::Error>),
70 #[error("Failed to read `uv-receipt.toml` at {0}")]
71 ReceiptRead(PathBuf, #[source] Box<toml::de::Error>),
72 #[error(transparent)]
73 VirtualEnvError(#[from] uv_virtualenv::Error),
74 #[error("Failed to read package entry points {0}")]
75 EntrypointRead(#[from] uv_install_wheel::Error),
76 #[error("Failed to find a directory to install executables into")]
77 NoExecutableDirectory,
78 #[error(transparent)]
79 ToolName(#[from] InvalidNameError),
80 #[error(transparent)]
81 EnvironmentError(#[from] uv_python::Error),
82 #[error("Failed to find a receipt for tool `{0}` at {1}")]
83 MissingToolReceipt(String, PathBuf),
84 #[error("Failed to read tool environment packages at `{0}`: {1}")]
85 EnvironmentRead(PathBuf, String),
86 #[error("Failed find package `{0}` in tool environment")]
87 MissingToolPackage(PackageName),
88 #[error("Tool `{0}` environment not found at `{1}`")]
89 ToolEnvironmentNotFound(PackageName, PathBuf),
90}
91
92#[derive(Debug, Clone)]
94pub struct InstalledTools {
95 root: PathBuf,
97}
98
99impl InstalledTools {
100 fn from_path(root: impl Into<PathBuf>) -> Self {
102 Self { root: root.into() }
103 }
104
105 pub fn from_settings() -> Result<Self, Error> {
113 if let Some(tool_dir) = std::env::var_os(EnvVars::UV_TOOL_DIR).filter(|s| !s.is_empty()) {
114 Ok(Self::from_path(std::path::absolute(tool_dir)?))
115 } else {
116 Ok(Self::from_path(
117 StateStore::from_settings(None)?.bucket(StateBucket::Tools),
118 ))
119 }
120 }
121
122 pub fn tool_dir(&self, name: &PackageName) -> PathBuf {
124 self.root.join(name.to_string())
125 }
126
127 #[allow(clippy::type_complexity)]
134 pub fn tools(&self) -> Result<Vec<(PackageName, Result<Tool, Error>)>, Error> {
135 let mut tools = Vec::new();
136 for directory in uv_fs::directories(self.root())? {
137 let Some(name) = directory
138 .file_name()
139 .and_then(|file_name| file_name.to_str())
140 else {
141 continue;
142 };
143 let name = PackageName::from_str(name)?;
144 let path = directory.join("uv-receipt.toml");
145 let contents = match fs_err::read_to_string(&path) {
146 Ok(contents) => contents,
147 Err(err) if err.kind() == io::ErrorKind::NotFound => {
148 let err = Error::MissingToolReceipt(name.to_string(), path);
149 tools.push((name, Err(err)));
150 continue;
151 }
152 Err(err) => return Err(err.into()),
153 };
154 match ToolReceipt::from_string(contents) {
155 Ok(tool_receipt) => tools.push((name, Ok(tool_receipt.tool))),
156 Err(err) => {
157 let err = Error::ReceiptRead(path, Box::new(err));
158 tools.push((name, Err(err)));
159 }
160 }
161 }
162 Ok(tools)
163 }
164
165 pub fn get_tool_receipt(&self, name: &PackageName) -> Result<Option<Tool>, Error> {
172 let path = self.tool_dir(name).join("uv-receipt.toml");
173 match ToolReceipt::from_path(&path) {
174 Ok(tool_receipt) => Ok(Some(tool_receipt.tool)),
175 Err(Error::Io(err)) if err.kind() == io::ErrorKind::NotFound => Ok(None),
176 Err(err) => Err(err),
177 }
178 }
179
180 pub async fn lock(&self) -> Result<LockedFile, Error> {
182 Ok(LockedFile::acquire(self.root.join(".lock"), self.root.user_display()).await?)
183 }
184
185 pub fn add_tool_receipt(&self, name: &PackageName, tool: Tool) -> Result<(), Error> {
191 let tool_receipt = ToolReceipt::from(tool);
192 let path = self.tool_dir(name).join("uv-receipt.toml");
193
194 debug!(
195 "Adding metadata entry for tool `{name}` at {}",
196 path.user_display()
197 );
198
199 let doc = tool_receipt
200 .to_toml()
201 .map_err(|err| Error::ReceiptWrite(path.clone(), Box::new(err)))?;
202
203 fs_err::write(&path, doc)?;
205
206 Ok(())
207 }
208
209 pub fn remove_environment(&self, name: &PackageName) -> Result<(), Error> {
219 let environment_path = self.tool_dir(name);
220
221 debug!(
222 "Deleting environment for tool `{name}` at {}",
223 environment_path.user_display()
224 );
225
226 remove_virtualenv(environment_path.as_path())?;
227
228 Ok(())
229 }
230
231 pub fn get_environment(
238 &self,
239 name: &PackageName,
240 cache: &Cache,
241 ) -> Result<Option<ToolEnvironment>, Error> {
242 let environment_path = self.tool_dir(name);
243
244 match PythonEnvironment::from_root(&environment_path, cache) {
245 Ok(venv) => {
246 debug!(
247 "Found existing environment for tool `{name}`: {}",
248 environment_path.user_display()
249 );
250 Ok(Some(ToolEnvironment::new(venv, name.clone())))
251 }
252 Err(uv_python::Error::MissingEnvironment(_)) => Ok(None),
253 Err(uv_python::Error::Query(uv_python::InterpreterError::NotFound(
254 interpreter_path,
255 ))) => {
256 warn!(
257 "Ignoring existing virtual environment with missing Python interpreter: {}",
258 interpreter_path.user_display()
259 );
260
261 Ok(None)
262 }
263 Err(uv_python::Error::Query(uv_python::InterpreterError::BrokenSymlink(
264 broken_symlink,
265 ))) => {
266 let target_path = fs_err::read_link(&broken_symlink.path)?;
267 warn!(
268 "Ignoring existing virtual environment linked to non-existent Python interpreter: {} -> {}",
269 broken_symlink.path.user_display(),
270 target_path.user_display()
271 );
272
273 Ok(None)
274 }
275 Err(err) => Err(err.into()),
276 }
277 }
278
279 pub fn create_environment(
283 &self,
284 name: &PackageName,
285 interpreter: Interpreter,
286 preview: Preview,
287 ) -> Result<PythonEnvironment, Error> {
288 let environment_path = self.tool_dir(name);
289
290 match fs_err::remove_dir_all(&environment_path) {
292 Ok(()) => {
293 debug!(
294 "Removed existing environment for tool `{name}`: {}",
295 environment_path.user_display()
296 );
297 }
298 Err(err) if err.kind() == io::ErrorKind::NotFound => (),
299 Err(err) => return Err(err.into()),
300 }
301
302 debug!(
303 "Creating environment for tool `{name}`: {}",
304 environment_path.user_display()
305 );
306
307 let venv = uv_virtualenv::create_venv(
309 &environment_path,
310 interpreter,
311 uv_virtualenv::Prompt::None,
312 false,
313 uv_virtualenv::OnExisting::Remove(uv_virtualenv::RemovalReason::ManagedEnvironment),
314 false,
315 false,
316 false,
317 preview,
318 )?;
319
320 Ok(venv)
321 }
322
323 pub fn temp() -> Result<Self, Error> {
325 Ok(Self::from_path(
326 StateStore::temp()?.bucket(StateBucket::Tools),
327 ))
328 }
329
330 pub fn init(self) -> Result<Self, Error> {
334 let root = &self.root;
335
336 fs::create_dir_all(root)?;
338
339 match fs::OpenOptions::new()
341 .write(true)
342 .create_new(true)
343 .open(root.join(".gitignore"))
344 {
345 Ok(mut file) => file.write_all(b"*")?,
346 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
347 Err(err) => return Err(err.into()),
348 }
349
350 Ok(self)
351 }
352
353 pub fn root(&self) -> &Path {
355 &self.root
356 }
357}
358
359#[derive(Debug, Clone)]
361pub struct InstalledTool {
362 path: PathBuf,
364}
365
366impl InstalledTool {
367 pub fn new(path: PathBuf) -> Result<Self, Error> {
368 Ok(Self { path })
369 }
370
371 pub fn path(&self) -> &Path {
372 &self.path
373 }
374}
375
376impl std::fmt::Display for InstalledTool {
377 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378 write!(
379 f,
380 "{}",
381 self.path
382 .file_name()
383 .unwrap_or(self.path.as_os_str())
384 .to_string_lossy()
385 )
386 }
387}
388
389pub fn tool_executable_dir() -> Result<PathBuf, Error> {
391 user_executable_directory(Some(EnvVars::UV_TOOL_BIN_DIR)).ok_or(Error::NoExecutableDirectory)
392}
393
394fn find_dist_info<'a>(
396 site_packages: &'a SitePackages,
397 package_name: &PackageName,
398 package_version: &Version,
399) -> Result<&'a Path, Error> {
400 site_packages
401 .get_packages(package_name)
402 .iter()
403 .find(|package| package.version() == package_version)
404 .map(|dist| dist.install_path())
405 .ok_or_else(|| Error::MissingToolPackage(package_name.clone()))
406}
407
408pub fn entrypoint_paths(
415 site_packages: &SitePackages,
416 package_name: &PackageName,
417 package_version: &Version,
418) -> Result<Vec<(String, PathBuf)>, Error> {
419 let dist_info_path = find_dist_info(site_packages, package_name, package_version)?;
421 debug!(
422 "Looking at `.dist-info` at: {}",
423 dist_info_path.user_display()
424 );
425
426 let record = read_record_file(&mut File::open(dist_info_path.join("RECORD"))?)?;
428
429 let layout = site_packages.interpreter().layout();
431 let script_relative = pathdiff::diff_paths(&layout.scheme.scripts, &layout.scheme.purelib)
432 .ok_or_else(|| {
433 io::Error::other(format!(
434 "Could not find relative path for: {}",
435 layout.scheme.scripts.simplified_display()
436 ))
437 })?;
438
439 let mut entrypoints = vec![];
441 for entry in record {
442 let relative_path = PathBuf::from(&entry.path);
443 let Ok(path_in_scripts) = relative_path.strip_prefix(&script_relative) else {
444 continue;
445 };
446
447 let absolute_path = layout.scheme.scripts.join(path_in_scripts);
448 let script_name = relative_path
449 .file_name()
450 .and_then(|filename| filename.to_str())
451 .map(ToString::to_string)
452 .unwrap_or(entry.path);
453 entrypoints.push((script_name, absolute_path));
454 }
455
456 Ok(entrypoints)
457}