diff --git a/crates/uv-cache-info/src/cache_info.rs b/crates/uv-cache-info/src/cache_info.rs index e925c22b595dd..08eab711843ec 100644 --- a/crates/uv-cache-info/src/cache_info.rs +++ b/crates/uv-cache-info/src/cache_info.rs @@ -31,8 +31,11 @@ pub struct CacheInfo { /// The Git tags present at the time of the build. tags: Option, /// Environment variables to include in the cache key. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default)] env: BTreeMap>, + /// The timestamp or inode of any directories that should be considered in the cache key. + #[serde(default)] + directories: BTreeMap>, } impl CacheInfo { @@ -59,6 +62,7 @@ impl CacheInfo { let mut commit = None; let mut tags = None; let mut timestamp = None; + let mut directories = BTreeMap::new(); let mut env = BTreeMap::new(); // Read the cache keys. @@ -82,6 +86,9 @@ impl CacheInfo { CacheKey::Path("pyproject.toml".to_string()), CacheKey::Path("setup.py".to_string()), CacheKey::Path("setup.cfg".to_string()), + CacheKey::Directory { + dir: "src".to_string(), + }, ] }); @@ -117,6 +124,51 @@ impl CacheInfo { } timestamp = max(timestamp, Some(Timestamp::from_metadata(&metadata))); } + CacheKey::Directory { dir } => { + // Treat the path as a directory. + let path = directory.join(&dir); + let metadata = match path.metadata() { + Ok(metadata) => metadata, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + directories.insert(dir, None); + continue; + } + Err(err) => { + warn!("Failed to read metadata for directory: {err}"); + continue; + } + }; + if !metadata.is_dir() { + warn!( + "Expected directory for cache key, but found file: `{}`", + path.display() + ); + continue; + } + + if let Ok(created) = metadata.created() { + // Prefer the creation time. + directories.insert( + dir, + Some(DirectoryTimestamp::Timestamp(Timestamp::from(created))), + ); + } else { + // Fall back to the inode. + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + directories + .insert(dir, Some(DirectoryTimestamp::Inode(metadata.ino()))); + } + #[cfg(not(unix))] + { + warn!( + "Failed to read creation time for directory: `{}`", + path.display() + ); + } + } + } CacheKey::Git { git: GitPattern::Bool(true), } => match Commit::from_repository(directory) { @@ -186,11 +238,16 @@ impl CacheInfo { } } + debug!( + "Computed cache info: {timestamp:?}, {commit:?}, {tags:?}, {env:?}, {directories:?}" + ); + Ok(Self { timestamp, commit, tags, env, + directories, }) } @@ -211,6 +268,7 @@ impl CacheInfo { && self.commit.is_none() && self.tags.is_none() && self.env.is_empty() + && self.directories.is_empty() } } @@ -241,6 +299,8 @@ pub enum CacheKey { Path(String), /// Ex) `{ file = "Cargo.lock" }` or `{ file = "**/*.toml" }` File { file: String }, + /// Ex) `{ dir = "src" }` + Directory { dir: String }, /// Ex) `{ git = true }` or `{ git = { commit = true, tags = false } }` Git { git: GitPattern }, /// Ex) `{ env = "UV_CACHE_INFO" }` @@ -267,3 +327,11 @@ pub enum FilePattern { Glob(String), Path(PathBuf), } + +/// A timestamp used to measure changes to a directory. +#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)] +enum DirectoryTimestamp { + Timestamp(Timestamp), + Inode(u64), +} diff --git a/crates/uv-cache-info/src/timestamp.rs b/crates/uv-cache-info/src/timestamp.rs index 262a03fedf498..51dc1e147c47c 100644 --- a/crates/uv-cache-info/src/timestamp.rs +++ b/crates/uv-cache-info/src/timestamp.rs @@ -44,3 +44,9 @@ impl Timestamp { Self(std::time::SystemTime::now()) } } + +impl From for Timestamp { + fn from(system_time: std::time::SystemTime) -> Self { + Self(system_time) + } +} diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 495bf06dc3d22..e3e3ab893eb4a 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -59,10 +59,11 @@ pub struct Options { /// /// Cache keys enable you to specify the files or directories that should trigger a rebuild when /// modified. By default, uv will rebuild a project whenever the `pyproject.toml`, `setup.py`, - /// or `setup.cfg` files in the project directory are modified, i.e.: + /// or `setup.cfg` files in the project directory are modified, or if a `src` directory is + /// added or removed, i.e.: /// /// ```toml - /// cache-keys = [{ file = "pyproject.toml" }, { file = "setup.py" }, { file = "setup.cfg" }] + /// cache-keys = [{ file = "pyproject.toml" }, { file = "setup.py" }, { file = "setup.cfg" }, { dir = "src" }] /// ``` /// /// As an example: if a project uses dynamic metadata to read its dependencies from a diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 61cc1e2ee0929..fd9b6976ddd89 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -10607,7 +10607,7 @@ fn lock_mixed_extras() -> Result<()> { [tool.uv.workspace] members = ["packages/*"] "#})?; - workspace1.child("src/__init__.py").touch()?; + workspace1.child("src/workspace1/__init__.py").touch()?; let leaf1 = workspace1.child("packages").child("leaf1"); leaf1.child("pyproject.toml").write_str(indoc! {r#" @@ -10621,10 +10621,10 @@ fn lock_mixed_extras() -> Result<()> { async = ["iniconfig>=2"] [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" "#})?; - leaf1.child("src/__init__.py").touch()?; + leaf1.child("src/leaf1/__init__.py").touch()?; // Create a second workspace (`workspace2`) with an extra of the same name. let workspace2 = context.temp_dir.child("workspace2"); @@ -10636,8 +10636,8 @@ fn lock_mixed_extras() -> Result<()> { dependencies = ["leaf2"] [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" [tool.uv.sources] leaf2 = { workspace = true } @@ -10645,7 +10645,7 @@ fn lock_mixed_extras() -> Result<()> { [tool.uv.workspace] members = ["packages/*"] "#})?; - workspace2.child("src/__init__.py").touch()?; + workspace2.child("src/workspace2/__init__.py").touch()?; let leaf2 = workspace2.child("packages").child("leaf2"); leaf2.child("pyproject.toml").write_str(indoc! {r#" @@ -10659,10 +10659,10 @@ fn lock_mixed_extras() -> Result<()> { async = ["packaging>=24"] [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" "#})?; - leaf2.child("src/__init__.py").touch()?; + leaf2.child("src/leaf2/__init__.py").touch()?; // Lock the first workspace. uv_snapshot!(context.filters(), context.lock().current_dir(&workspace1), @r###" @@ -10842,7 +10842,7 @@ fn lock_transitive_extra() -> Result<()> { [tool.uv.workspace] members = ["packages/*"] "#})?; - workspace.child("src/__init__.py").touch()?; + workspace.child("src/workspace/__init__.py").touch()?; let leaf = workspace.child("packages").child("leaf"); leaf.child("pyproject.toml").write_str(indoc! {r#" @@ -10856,10 +10856,10 @@ fn lock_transitive_extra() -> Result<()> { async = ["iniconfig>=2"] [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" "#})?; - leaf.child("src/__init__.py").touch()?; + leaf.child("src/leaf/__init__.py").touch()?; // Lock the workspace. uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r###" diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 0e98df628448b..b499512324a2c 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -9988,3 +9988,217 @@ fn unsupported_git_scheme() { "### ); } + +/// Modify a project to use a `src` layout. +#[test] +fn change_layout_src() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("-e .")?; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#, + )?; + + context + .temp_dir + .child("src") + .child("project") + .child("__init__.py") + .touch()?; + + // Installing should build the package. + uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + iniconfig==2.0.0 + + project==0.1.0 (from file://[TEMP_DIR]/) + "### + ); + + // Reinstalling should have no effect. + uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Audited 1 package in [TIME] + "### + ); + + // Replace the `src` layout with a flat layout. + fs_err::remove_dir_all(context.temp_dir.child("src").path())?; + + context + .temp_dir + .child("project") + .child("__init__.py") + .touch()?; + + // Installing should rebuild the package. + uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + ~ project==0.1.0 (from file://[TEMP_DIR]/) + "### + ); + + // Reinstalling should have no effect. + uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Audited 1 package in [TIME] + "### + ); + + Ok(()) +} + +/// Modify a custom directory in the cache keys. +#[test] +fn change_layout_custom_directory() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("-e .")?; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + + [tool.uv] + cache-keys = [{ dir = "build" }] + "#, + )?; + + context + .temp_dir + .child("src") + .child("project") + .child("__init__.py") + .touch()?; + + // Installing should build the package. + uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + iniconfig==2.0.0 + + project==0.1.0 (from file://[TEMP_DIR]/) + "### + ); + + // Reinstalling should have no effect. + uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Audited 1 package in [TIME] + " + ); + + // Create the `build` directory. + fs_err::create_dir(context.temp_dir.child("build"))?; + + // Installing should rebuild the package. + uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + ~ project==0.1.0 (from file://[TEMP_DIR]/) + "### + ); + + // Reinstalling should have no effect. + uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Audited 1 package in [TIME] + " + ); + + // Remove the `build` directory. + fs_err::remove_dir(context.temp_dir.child("build"))?; + + // Installing should rebuild the package. + uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + ~ project==0.1.0 (from file://[TEMP_DIR]/) + "### + ); + + // Reinstalling should have no effect. + uv_snapshot!(context.filters(), context.pip_install().arg("-r").arg("requirements.txt"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Audited 1 package in [TIME] + " + ); + + Ok(()) +} diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index c49e070c09053..2b86218321b09 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -1208,8 +1208,8 @@ fn run_in_workspace() -> Result<()> { dependencies = ["anyio>3"] [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" [tool.uv.workspace] members = ["child1", "child2"] @@ -1236,8 +1236,8 @@ fn run_in_workspace() -> Result<()> { dependencies = ["iniconfig>1"] [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" "#, )?; child1 @@ -1256,8 +1256,8 @@ fn run_in_workspace() -> Result<()> { dependencies = ["typing-extensions>4"] [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" "#, )?; child2 @@ -1383,11 +1383,18 @@ fn run_with_editable() -> Result<()> { dependencies = ["anyio", "sniffio==1.3.1"] [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" "# })?; + context + .temp_dir + .child("src") + .child("foo") + .child("__init__.py") + .touch()?; + let test_script = context.temp_dir.child("main.py"); test_script.write_str(indoc! { r" import sniffio @@ -1449,8 +1456,8 @@ fn run_with_editable() -> Result<()> { dependencies = ["anyio", "sniffio==1.3.1"] [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" [tool.uv.sources] anyio = { path = "./src/anyio_local", editable = true } @@ -2538,8 +2545,8 @@ fn run_isolated_python_version() -> Result<()> { dependencies = ["anyio"] [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" "# })?; @@ -2635,8 +2642,8 @@ fn run_no_project() -> Result<()> { dependencies = ["anyio"] [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" "# })?; @@ -2913,8 +2920,8 @@ fn run_isolated_incompatible_python() -> Result<()> { dependencies = ["iniconfig"] [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" "# })?; diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index d94e9d2b5981e..37ce99c4aa169 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -367,16 +367,14 @@ fn sync_legacy_non_project_dev_dependencies() -> Result<()> { members = ["child"] "#, )?; - - let src = context.temp_dir.child("src").child("albatross"); - src.create_dir_all()?; - - let init = src.child("__init__.py"); - init.touch()?; + context + .temp_dir + .child("src") + .child("albatross") + .child("__init__.py") + .touch()?; let child = context.temp_dir.child("child"); - fs_err::create_dir_all(&child)?; - let pyproject_toml = child.child("pyproject.toml"); pyproject_toml.write_str( r#" @@ -387,16 +385,15 @@ fn sync_legacy_non_project_dev_dependencies() -> Result<()> { dependencies = ["iniconfig>=1"] [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" "#, )?; - - let src = child.child("src").child("albatross"); - src.create_dir_all()?; - - let init = src.child("__init__.py"); - init.touch()?; + child + .child("src") + .child("child") + .child("__init__.py") + .touch()?; // Syncing with `--no-dev` should omit all dependencies except `iniconfig`. uv_snapshot!(context.filters(), context.sync().arg("--no-dev"), @r###" @@ -521,15 +518,14 @@ fn sync_legacy_non_project_group() -> Result<()> { "#, )?; - let src = context.temp_dir.child("src").child("albatross"); - src.create_dir_all()?; - - let init = src.child("__init__.py"); - init.touch()?; + context + .temp_dir + .child("src") + .child("albatross") + .child("__init__.py") + .touch()?; let child = context.temp_dir.child("child"); - fs_err::create_dir_all(&child)?; - let pyproject_toml = child.child("pyproject.toml"); pyproject_toml.write_str( r#" @@ -543,16 +539,15 @@ fn sync_legacy_non_project_group() -> Result<()> { baz = ["typing-extensions"] [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" "#, )?; - - let src = child.child("src").child("albatross"); - src.create_dir_all()?; - - let init = src.child("__init__.py"); - init.touch()?; + child + .child("src") + .child("child") + .child("__init__.py") + .touch()?; uv_snapshot!(context.filters(), context.sync(), @r###" success: true @@ -5911,8 +5906,8 @@ fn sync_all_extras() -> Result<()> { testing = ["packaging>=24"] [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" "#, )?; child @@ -6028,8 +6023,8 @@ fn sync_all_extras_dynamic() -> Result<()> { async = ["anyio>3"] [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" [tool.uv.workspace] members = ["child"] @@ -6058,6 +6053,9 @@ fn sync_all_extras_dynamic() -> Result<()> { [tool.setuptools.dynamic.optional-dependencies] dev = { file = "requirements-dev.txt" } + [tool.uv] + cache-keys = ["pyproject.toml"] + [build-system] requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" @@ -6168,8 +6166,8 @@ fn sync_all_groups() -> Result<()> { testing = ["packaging>=24"] [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" "#, )?; child diff --git a/docs/concepts/cache.md b/docs/concepts/cache.md index b7ad423263a6f..6610ccb55b4b5 100644 --- a/docs/concepts/cache.md +++ b/docs/concepts/cache.md @@ -32,8 +32,9 @@ explicitly on the command-line (e.g., `uv pip install .`). ## Dynamic metadata By default, uv will _only_ rebuild and reinstall local directory dependencies (e.g., editables) if -the `pyproject.toml`, `setup.py`, or `setup.cfg` file in the directory root has changed. This is a -heuristic and, in some cases, may lead to fewer re-installs than desired. +the `pyproject.toml`, `setup.py`, or `setup.cfg` file in the directory root has changed, or if a +`src` directory is added or removed. This is a heuristic and, in some cases, may lead to fewer +re-installs than desired. To incorporate additional information into the cache key for a given package, you can add cache key entries under [`tool.uv.cache-keys`](https://docs.astral.sh/uv/reference/settings/#cache-keys), @@ -68,7 +69,7 @@ the following to the project's `pyproject.toml`: cache-keys = [{ file = "pyproject.toml" }, { file = "requirements.txt" }] ``` -Globs are supported, following the syntax of the +Globs are supported for `file` keys, following the syntax of the [`glob`](https://docs.rs/glob/0.3.1/glob/struct.Pattern.html) crate. For example, to invalidate the cache whenever a `.toml` file in the project directory or any of its subdirectories is modified, use the following: @@ -91,6 +92,17 @@ project's `pyproject.toml` to invalidate the cache whenever the environment vari cache-keys = [{ file = "pyproject.toml" }, { env = "MY_ENV_VAR" }] ``` +Finally, to invalidate a project whenever a specific directory (like `src`) is created or removed, +add the following to the project's `pyproject.toml`: + +```toml title="pyproject.toml" +[tool.uv] +cache-keys = [{ file = "pyproject.toml" }, { dir = "src" }] +``` + +Note that the `dir` key will only track changes to the directory itself, and not arbitrary changes +within the directory. + As an escape hatch, if a project uses `dynamic` metadata that isn't covered by `tool.uv.cache-keys`, you can instruct uv to _always_ rebuild and reinstall it by adding the project to the `tool.uv.reinstall-package` list: diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 80a887b44b7b4..f005354df2693 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -477,10 +477,11 @@ The keys to consider when caching builds for the project. Cache keys enable you to specify the files or directories that should trigger a rebuild when modified. By default, uv will rebuild a project whenever the `pyproject.toml`, `setup.py`, -or `setup.cfg` files in the project directory are modified, i.e.: +or `setup.cfg` files in the project directory are modified, or if a `src` directory is +added or removed, i.e.: ```toml -cache-keys = [{ file = "pyproject.toml" }, { file = "setup.py" }, { file = "setup.cfg" }] +cache-keys = [{ file = "pyproject.toml" }, { file = "setup.py" }, { file = "setup.cfg" }, { dir = "src" }] ``` As an example: if a project uses dynamic metadata to read its dependencies from a diff --git a/uv.schema.json b/uv.schema.json index 4b8a0c24d5e57..d051d05fe6265 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -32,7 +32,7 @@ ] }, "cache-keys": { - "description": "The keys to consider when caching builds for the project.\n\nCache keys enable you to specify the files or directories that should trigger a rebuild when modified. By default, uv will rebuild a project whenever the `pyproject.toml`, `setup.py`, or `setup.cfg` files in the project directory are modified, i.e.:\n\n```toml cache-keys = [{ file = \"pyproject.toml\" }, { file = \"setup.py\" }, { file = \"setup.cfg\" }] ```\n\nAs an example: if a project uses dynamic metadata to read its dependencies from a `requirements.txt` file, you can specify `cache-keys = [{ file = \"requirements.txt\" }, { file = \"pyproject.toml\" }]` to ensure that the project is rebuilt whenever the `requirements.txt` file is modified (in addition to watching the `pyproject.toml`).\n\nGlobs are supported, following the syntax of the [`glob`](https://docs.rs/glob/0.3.1/glob/struct.Pattern.html) crate. For example, to invalidate the cache whenever a `.toml` file in the project directory or any of its subdirectories is modified, you can specify `cache-keys = [{ file = \"**/*.toml\" }]`. Note that the use of globs can be expensive, as uv may need to walk the filesystem to determine whether any files have changed.\n\nCache keys can also include version control information. For example, if a project uses `setuptools_scm` to read its version from a Git commit, you can specify `cache-keys = [{ git = { commit = true }, { file = \"pyproject.toml\" }]` to include the current Git commit hash in the cache key (in addition to the `pyproject.toml`). Git tags are also supported via `cache-keys = [{ git = { commit = true, tags = true } }]`.\n\nCache keys can also include environment variables. For example, if a project relies on `MACOSX_DEPLOYMENT_TARGET` or other environment variables to determine its behavior, you can specify `cache-keys = [{ env = \"MACOSX_DEPLOYMENT_TARGET\" }]` to invalidate the cache whenever the environment variable changes.\n\nCache keys only affect the project defined by the `pyproject.toml` in which they're specified (as opposed to, e.g., affecting all members in a workspace), and all paths and globs are interpreted as relative to the project directory.", + "description": "The keys to consider when caching builds for the project.\n\nCache keys enable you to specify the files or directories that should trigger a rebuild when modified. By default, uv will rebuild a project whenever the `pyproject.toml`, `setup.py`, or `setup.cfg` files in the project directory are modified, or if a `src` directory is added or removed, i.e.:\n\n```toml cache-keys = [{ file = \"pyproject.toml\" }, { file = \"setup.py\" }, { file = \"setup.cfg\" }, { dir = \"src\" }] ```\n\nAs an example: if a project uses dynamic metadata to read its dependencies from a `requirements.txt` file, you can specify `cache-keys = [{ file = \"requirements.txt\" }, { file = \"pyproject.toml\" }]` to ensure that the project is rebuilt whenever the `requirements.txt` file is modified (in addition to watching the `pyproject.toml`).\n\nGlobs are supported, following the syntax of the [`glob`](https://docs.rs/glob/0.3.1/glob/struct.Pattern.html) crate. For example, to invalidate the cache whenever a `.toml` file in the project directory or any of its subdirectories is modified, you can specify `cache-keys = [{ file = \"**/*.toml\" }]`. Note that the use of globs can be expensive, as uv may need to walk the filesystem to determine whether any files have changed.\n\nCache keys can also include version control information. For example, if a project uses `setuptools_scm` to read its version from a Git commit, you can specify `cache-keys = [{ git = { commit = true }, { file = \"pyproject.toml\" }]` to include the current Git commit hash in the cache key (in addition to the `pyproject.toml`). Git tags are also supported via `cache-keys = [{ git = { commit = true, tags = true } }]`.\n\nCache keys can also include environment variables. For example, if a project relies on `MACOSX_DEPLOYMENT_TARGET` or other environment variables to determine its behavior, you can specify `cache-keys = [{ env = \"MACOSX_DEPLOYMENT_TARGET\" }]` to invalidate the cache whenever the environment variable changes.\n\nCache keys only affect the project defined by the `pyproject.toml` in which they're specified (as opposed to, e.g., affecting all members in a workspace), and all paths and globs are interpreted as relative to the project directory.", "type": [ "array", "null" @@ -602,6 +602,19 @@ }, "additionalProperties": false }, + { + "description": "Ex) `{ dir = \"src\" }`", + "type": "object", + "required": [ + "dir" + ], + "properties": { + "dir": { + "type": "string" + } + }, + "additionalProperties": false + }, { "description": "Ex) `{ git = true }` or `{ git = { commit = true, tags = false } }`", "type": "object",