diff --git a/src/ape/utils/__init__.py b/src/ape/utils/__init__.py index 8959b4c500..fbfb0c7ab1 100644 --- a/src/ape/utils/__init__.py +++ b/src/ape/utils/__init__.py @@ -72,6 +72,7 @@ def __getattr__(name: str): "get_relative_path", "in_tempdir", "path_match", + "remove_readonly", "run_in_tempdir", "use_temp_sys_path", ): @@ -171,6 +172,7 @@ def __getattr__(name: str): "parse_gas_table", "path_match", "raises_not_implemented", + "remove_readonly", "returns_array", "request_with_retry", "RPCHeaders", diff --git a/src/ape/utils/os.py b/src/ape/utils/os.py index 6a3a4213df..2746ed69a1 100644 --- a/src/ape/utils/os.py +++ b/src/ape/utils/os.py @@ -1,6 +1,7 @@ import json import os import re +import stat import sys import tarfile import zipfile @@ -372,6 +373,14 @@ def extract_archive(archive_file: Path, destination: Optional[Path] = None): raise ValueError(f"Unsupported zip format: '{archive_file.suffix}'.") +def remove_readonly(func, path, excinfo): + """ + Error handler for shutil.rmtree that handles removing read-only files. + """ + os.chmod(path, stat.S_IWRITE) + func(path) + + class CacheDirectory: """ A directory for caching data where each data item is named diff --git a/src/ape_pm/dependency.py b/src/ape_pm/dependency.py index 6e0828c90f..388bfbc990 100644 --- a/src/ape_pm/dependency.py +++ b/src/ape_pm/dependency.py @@ -16,7 +16,7 @@ from ape.managers.project import _version_to_options from ape.utils._github import _GithubClient, github_client from ape.utils.basemodel import ManagerAccessMixin -from ape.utils.os import clean_path, extract_archive, get_package_path, in_tempdir +from ape.utils.os import clean_path, extract_archive, get_package_path, in_tempdir, remove_readonly def _fetch_local(src: Path, destination: Path, config_override: Optional[dict] = None): @@ -204,8 +204,11 @@ def __repr__(self) -> str: def fetch(self, destination: Path): destination.parent.mkdir(exist_ok=True, parents=True) if ref := self.ref: + # NOTE: destination path should not exist at this point, + # so delete it in case it's left over from a failure. + shutil.rmtree(destination, onerror=remove_readonly) + # Fetch using git-clone approach (by git-reference). - # NOTE: destination path does not exist at this point. self._fetch_ref(ref, destination) else: # Fetch using Version API from GitHub. @@ -222,7 +225,7 @@ def fetch(self, destination: Path): # NOTE: When using ref-from-a-version, ensure # it didn't create the destination along the way; # else, the ref is cloned in the wrong spot. - shutil.rmtree(destination, ignore_errors=True) + shutil.rmtree(destination, onerror=remove_readonly) try: self._fetch_ref(version, destination) except Exception: