diff --git a/docs/changelog/3342.feature.rst b/docs/changelog/3342.feature.rst new file mode 100644 index 000000000..bdfb49165 --- /dev/null +++ b/docs/changelog/3342.feature.rst @@ -0,0 +1,7 @@ +Tox now creates ``CACHEDIR.TAG`` files in work directories it creates, +so that tools like ``tar`` can exclude them from e.g. backups where +ephemeral directories are not desired. + +Tag files are not created in directories Tox does not itself create. + +- by :user:`akx` diff --git a/src/tox/tox_env/api.py b/src/tox/tox_env/api.py index 57f699fba..f1490cbea 100644 --- a/src/tox/tox_env/api.py +++ b/src/tox/tox_env/api.py @@ -17,7 +17,7 @@ from tox.execute.request import ExecuteRequest from tox.tox_env.errors import Fail, Recreate, Skip from tox.tox_env.info import Info -from tox.util.path import ensure_empty_dir +from tox.util.path import ensure_cachedir_dir, ensure_empty_dir if TYPE_CHECKING: from tox.config.cli.parser import Parsed @@ -315,16 +315,27 @@ def _setup_with_env(self) -> None: # noqa: B027 # empty abstract base class def _done_with_setup(self) -> None: # noqa: B027 # empty abstract base class """Called when setup is done.""" + def _maybe_ensure_workdir(self) -> None: + if not self.work_dir.is_dir(): + # Populate the workdir with a CACHEDIR.TAG file only if we would + # be creating it now. If it already exists, do not touch it. + ensure_cachedir_dir(self.work_dir) + def _handle_env_tmp_dir(self) -> None: """Ensure exists and empty.""" env_tmp_dir = self.env_tmp_dir if env_tmp_dir.exists() and next(env_tmp_dir.iterdir(), None) is not None: LOGGER.debug("clear env temp folder %s", env_tmp_dir) ensure_empty_dir(env_tmp_dir) - env_tmp_dir.mkdir(parents=True, exist_ok=True) + if env_tmp_dir.parent == self.work_dir: + self._maybe_ensure_workdir() + ensure_cachedir_dir(env_tmp_dir) def _handle_core_tmp_dir(self) -> None: - self.temp_dir.mkdir(parents=True, exist_ok=True) + temp_dir = self.temp_dir + if temp_dir.parent == self.work_dir: + self._maybe_ensure_workdir() + ensure_cachedir_dir(temp_dir) def _clean(self, transitive: bool = False) -> None: # noqa: ARG002, FBT001, FBT002 if self._run_state["clean"]: # pragma: no branch @@ -333,6 +344,7 @@ def _clean(self, transitive: bool = False) -> None: # noqa: ARG002, FBT001, FBT if env_dir.exists(): LOGGER.warning("remove tox env folder %s", env_dir) ensure_empty_dir(env_dir, except_filename="file.lock") + ensure_cachedir_dir(env_dir) self._log_id = 0 # we deleted logs, so start over counter self.cache.reset() self._run_state.update({"setup": False, "clean": True}) diff --git a/src/tox/tox_env/package.py b/src/tox/tox_env/package.py index 7c5a23a4f..4b4d1f8f4 100644 --- a/src/tox/tox_env/package.py +++ b/src/tox/tox_env/package.py @@ -10,6 +10,8 @@ from filelock import FileLock +from tox.util.path import ensure_cachedir_dir + from .api import ToxEnv, ToxEnvCreateArgs if TYPE_CHECKING: @@ -67,9 +69,12 @@ def __getattribute__(self, name: str) -> Any: def register_config(self) -> None: super().register_config() - file_lock_path: Path = self.env_dir / "file.lock" + env_dir = self.env_dir + if env_dir.parent == self.work_dir: + self._maybe_ensure_workdir() + ensure_cachedir_dir(env_dir) + file_lock_path: Path = env_dir / "file.lock" self._file_lock = FileLock(file_lock_path) - file_lock_path.parent.mkdir(parents=True, exist_ok=True) self.core.add_config( keys=["package_root", "setupdir"], of_type=Path, diff --git a/src/tox/tox_env/python/api.py b/src/tox/tox_env/python/api.py index 3b3124a7a..274e01a5e 100644 --- a/src/tox/tox_env/python/api.py +++ b/src/tox/tox_env/python/api.py @@ -13,6 +13,7 @@ from tox.tox_env.api import ToxEnv, ToxEnvCreateArgs from tox.tox_env.errors import Fail, Recreate, Skip +from tox.util.path import ensure_cachedir_dir if TYPE_CHECKING: from tox.config.main import Config @@ -236,6 +237,7 @@ def ensure_python_env(self) -> None: with self.cache.compare(conf, Python.__name__) as (eq, old): if old is None: # does not exist -> create self.create_python_env() + ensure_cachedir_dir(self.env_dir) elif eq is False: # pragma: no branch # exists but changed -> recreate raise Recreate(self._diff_msg(conf, old)) diff --git a/src/tox/util/path.py b/src/tox/util/path.py index 9eea178f9..8b36d0b05 100644 --- a/src/tox/util/path.py +++ b/src/tox/util/path.py @@ -6,6 +6,12 @@ if TYPE_CHECKING: from pathlib import Path +CACHEDIR_TAG_CONTENT = b"""Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by the Tox automation project (https://tox.wiki/). +# For information about cache directory tags, see: +# http://www.brynosaurus.com/cachedir/ +""" + def ensure_empty_dir(path: Path, except_filename: str | None = None) -> None: if path.exists(): @@ -24,6 +30,18 @@ def ensure_empty_dir(path: Path, except_filename: str | None = None) -> None: path.mkdir(parents=True) +def ensure_cachedir_dir(path: Path) -> None: + """ + Ensure that the given path is a directory, exists and + contains a `CACHEDIR.TAG` file. + """ + path.mkdir(parents=True, exist_ok=True) + cachetag = path / "CACHEDIR.TAG" + if not cachetag.is_file(): + cachetag.write_bytes(CACHEDIR_TAG_CONTENT) + + __all__ = [ + "ensure_cachedir_dir", "ensure_empty_dir", ]