fix: Skip inaccessible CONDA_ENVS_DIRS (#3887)

Co-authored-by: Julien Jerphanion <git@jjerphan.xyz>
This commit is contained in:
Burt Holzman 2025-06-11 17:51:31 -05:00 committed by GitHub
parent 5bc7f9f5da
commit 445bf87ecc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 63 additions and 43 deletions

View File

@ -440,7 +440,8 @@ namespace mamba
for (const auto& dir : dirs)
{
const auto candidate = dir / name;
if (fs::exists(candidate) && fs::is_directory(candidate))
std::error_code _ec;
if (fs::exists(candidate, _ec) && fs::is_directory(candidate))
{
return candidate;
}
@ -990,14 +991,19 @@ namespace mamba
dirs.insert(dirs.begin(), paths.rbegin(), paths.rend());
}
std::error_code ec;
// Check that the values exist as directories
for (auto& d : dirs)
{
d = fs::weakly_canonical(util::expand_home(d.string())).string();
if (fs::exists(d) && !fs::is_directory(d))
auto canonical_dir = fs::weakly_canonical(util::expand_home(d.string()), ec).string();
if (!ec)
{
LOG_ERROR << "Env dir specified is not a directory: " << d.string();
throw std::runtime_error("Aborting.");
d = std::move(canonical_dir);
if (fs::exists(d) && !fs::is_directory(d))
{
LOG_ERROR << "Env dir specified is not a directory: " << d.string();
throw std::runtime_error("Aborting.");
}
}
}

View File

@ -103,13 +103,14 @@ namespace mamba::path
bool is_writable(const fs::u8path& path) noexcept
{
const bool path_exists = fs::exists(path);
std::error_code ec;
const bool path_exists = fs::exists(path, ec);
const auto& path_to_write_in = path_exists ? path : path.parent_path();
static constexpr auto writable_flags = fs::perms::owner_write | fs::perms::group_write
| fs::perms::others_write;
std::error_code ec;
const auto status = fs::status(path_to_write_in, ec);
const bool should_be_writable = !ec && status.type() != fs::file_type::not_found

View File

@ -741,8 +741,52 @@ def test_create_conda_envs_dirs_and_path(tmp_root_prefix, monkeypatch):
)
def remove_perms(path, unreadable=False):
if platform.system() == "Windows":
# Make path less accessible on Windows - this requires:
# Removing inherited and granted permissions, removing ownership,
# and optionally granting group Everyone RX access.
perms = [
"/inheritance:r", # remove inherited permissions
"/remove", # remove ownership from all groups
r"BUILTIN\Administrators",
"/remove",
r"NT AUTHORITY\SYSTEM",
"/remove",
"OWNER RIGHTS",
"/remove:g",
"Everyone",
]
if not unreadable:
perms.extend(["/grant:r", "Everyone:(RX)"]) # grant Read-Execute to everyone
subprocess.run(
["icacls", path] + perms,
check=True,
)
else:
# Make path less accessible on Linux
os.chmod(path, 0o000 if unreadable else 0o555)
def restore_perms(path):
if platform.system() == "Windows":
# Revert ownership change on Windows so that we can clean up
subprocess.run(["icacls", path, "/grant", r"BUILTIN\Administrators:F"], check=True)
subprocess.run(["icacls", path, "/reset"], check=True)
else:
# Revert permission change on Linux so that we can clean up
os.chmod(path, 0o755)
@pytest.mark.parametrize("unreadable", (True, False))
@pytest.mark.parametrize("noperm_dir", ("root_dir", "envs_dir"))
@pytest.mark.parametrize("conda_envs_x", ("CONDA_ENVS_DIRS", "CONDA_ENVS_PATH"))
def test_create_envs_dirs(tmp_root_prefix: Path, tmp_path: Path, conda_envs_x, monkeypatch):
def test_create_envs_dirs(
tmp_root_prefix: Path, tmp_path: Path, unreadable, noperm_dir, conda_envs_x, monkeypatch
):
"""Create an environment when the first env dir is not writable."""
noperm_root_dir = Path(tmp_path / "noperm")
@ -750,46 +794,15 @@ def test_create_envs_dirs(tmp_root_prefix: Path, tmp_path: Path, conda_envs_x, m
monkeypatch.setenv(conda_envs_x, f"{noperm_envs_dir}{os.pathsep}{tmp_path}")
env_name = "myenv"
os.makedirs(noperm_root_dir, exist_ok=True)
target_dir = noperm_root_dir if noperm_dir == "root_dir" else noperm_envs_dir
if platform.system() == "Windows":
# Make first env_dir read-only on Windows - this requires:
# Removing inherited and granted permissions, removing ownership,
# and granting group Everyone RX access.
subprocess.run(
[
"icacls",
noperm_root_dir,
"/inheritance:r", # remove inherited permissions
"/grant:r", # remove explicitly granted permissions
"Everyone:(RX)", # grant Read-Execute to everyone
"/remove", # remove ownership from all groups
r"BUILTIN\Administrators",
"/remove",
r"NT AUTHORITY\SYSTEM",
"/remove",
"OWNER RIGHTS",
],
check=True,
)
else:
# Make first env_dir read-only on Linux
os.chmod(noperm_root_dir, 0o555)
os.makedirs(target_dir, exist_ok=True)
remove_perms(target_dir, unreadable)
try:
helpers.create("-n", env_name, "--offline", "--no-rc", no_dry_run=True)
finally:
if platform.system() == "Windows":
# Revert ownership change on Windows so that we can clean up
subprocess.run(
["icacls", noperm_root_dir, "/grant", r"BUILTIN\Administrators:F"], check=True
)
subprocess.run(["icacls", noperm_root_dir, "/reset"], check=True)
else:
# Revert permission change on Linux so that we can clean up
os.chmod(noperm_root_dir, 0o755)
restore_perms(target_dir)
assert (tmp_path / env_name / "conda-meta" / "history").exists()