mirror of
				https://github.com/astral-sh/uv.git
				synced 2025-10-31 12:06:13 +00:00 
			
		
		
		
	 de40f798b9
			
		
	
	
		de40f798b9
		
			
		
	
	
	
	
		
			
			## Summary
The basic strategy:
- When the user does `uv tool run`, we resolve the `from` and `with`
requirements (always).
- After resolving, we generate a hash of the requirements. For now, I'm
just converting to a lockfile and hashing _that_, but that's an
implementation detail.
- Once we have a hash, we _also_ hash the interpreter.
- We then store environments in
`${CACHE_DIR}/${INTERPRETER_HASH}/${RESOLUTION_HASH}`.
Some consequences:
- We cache based on the interpreter, so if you request a different
Python, we'll create a new environment (even if they're compatible).
This has the nice side-effect of ensuring that we don't use environments
for interpreters that were later deleted.
- We cache the `from` and `with` together. In practice, we may want to
cache them separately, then layer them? But this is also an
implementation detail that we could change later.
- Because we use the lockfile as the cache key, we will invalidate the
cache when the format changes. That seems ok, but we could improve it in
the future by generating a stable hash from a lockfile that's
independent of the schema.
Closes https://github.com/astral-sh/uv/issues/4752.
		
	
			
		
			
				
	
	
		
			111 lines
		
	
	
	
		
			3.1 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			111 lines
		
	
	
	
		
			3.1 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use std::{
 | |
|     io::{self, Write},
 | |
|     path::{Path, PathBuf},
 | |
|     sync::Arc,
 | |
| };
 | |
| 
 | |
| use directories::ProjectDirs;
 | |
| use fs_err as fs;
 | |
| use tempfile::{tempdir, TempDir};
 | |
| 
 | |
| /// The main state storage abstraction.
 | |
| ///
 | |
| /// This is appropriate
 | |
| #[derive(Debug, Clone)]
 | |
| pub struct StateStore {
 | |
|     /// The state storage.
 | |
|     root: PathBuf,
 | |
|     /// A temporary state storage.
 | |
|     ///
 | |
|     /// Included to ensure that the temporary store exists for the length of the operation, but
 | |
|     /// is dropped at the end as appropriate.
 | |
|     _temp_dir_drop: Option<Arc<TempDir>>,
 | |
| }
 | |
| 
 | |
| impl StateStore {
 | |
|     /// A persistent state store at `root`.
 | |
|     pub fn from_path(root: impl Into<PathBuf>) -> Result<Self, io::Error> {
 | |
|         Ok(Self {
 | |
|             root: root.into(),
 | |
|             _temp_dir_drop: None,
 | |
|         })
 | |
|     }
 | |
| 
 | |
|     /// Create a temporary state store.
 | |
|     pub fn temp() -> Result<Self, io::Error> {
 | |
|         let temp_dir = tempdir()?;
 | |
|         Ok(Self {
 | |
|             root: temp_dir.path().to_path_buf(),
 | |
|             _temp_dir_drop: Some(Arc::new(temp_dir)),
 | |
|         })
 | |
|     }
 | |
| 
 | |
|     /// Return the root of the state store.
 | |
|     pub fn root(&self) -> &Path {
 | |
|         &self.root
 | |
|     }
 | |
| 
 | |
|     /// Initialize the state store.
 | |
|     pub fn init(self) -> Result<Self, io::Error> {
 | |
|         let root = &self.root;
 | |
| 
 | |
|         // Create the state store directory, if it doesn't exist.
 | |
|         fs::create_dir_all(root)?;
 | |
| 
 | |
|         // Add a .gitignore.
 | |
|         match fs::OpenOptions::new()
 | |
|             .write(true)
 | |
|             .create_new(true)
 | |
|             .open(root.join(".gitignore"))
 | |
|         {
 | |
|             Ok(mut file) => file.write_all(b"*")?,
 | |
|             Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
 | |
|             Err(err) => return Err(err),
 | |
|         }
 | |
| 
 | |
|         Ok(Self {
 | |
|             root: fs::canonicalize(root)?,
 | |
|             ..self
 | |
|         })
 | |
|     }
 | |
| 
 | |
|     /// The folder for a specific cache bucket
 | |
|     pub fn bucket(&self, state_bucket: StateBucket) -> PathBuf {
 | |
|         self.root.join(state_bucket.to_str())
 | |
|     }
 | |
| 
 | |
|     /// Prefer, in order:
 | |
|     /// 1. The specific state directory specified by the user.
 | |
|     /// 2. The system-appropriate user-level data directory.
 | |
|     /// 3. A `.uv` directory in the current working directory.
 | |
|     ///
 | |
|     /// Returns an absolute cache dir.
 | |
|     pub fn from_settings(state_dir: Option<PathBuf>) -> Result<Self, io::Error> {
 | |
|         if let Some(state_dir) = state_dir {
 | |
|             StateStore::from_path(state_dir)
 | |
|         } else if let Some(project_dirs) = ProjectDirs::from("", "", "uv") {
 | |
|             StateStore::from_path(project_dirs.data_dir())
 | |
|         } else {
 | |
|             StateStore::from_path(".uv")
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// The different kinds of data in the state store are stored in different bucket, which in our case
 | |
| /// are subdirectories of the state store root.
 | |
| #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
 | |
| pub enum StateBucket {
 | |
|     /// Managed Python installations
 | |
|     ManagedPython,
 | |
|     /// Installed tools.
 | |
|     Tools,
 | |
| }
 | |
| 
 | |
| impl StateBucket {
 | |
|     fn to_str(self) -> &'static str {
 | |
|         match self {
 | |
|             Self::ManagedPython => "python",
 | |
|             Self::Tools => "tools",
 | |
|         }
 | |
|     }
 | |
| }
 |