这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
40dd52e
conda/activate.py: support multiple MSYS2 environments
ifitchet Mar 4, 2024
e0239ef
news item
ifitchet Mar 4, 2024
07124f7
update PATH in troubleshooting doc
ifitchet Mar 4, 2024
1ce56f7
rename news file to reflect PR#
ifitchet Mar 4, 2024
5f9e9b2
Make the news item text terse! And link to the PR#
ifitchet Mar 4, 2024
7d13bda
Merge branch 'main' into cleaner-msys2-support
beeankha Apr 16, 2024
b4174e2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 16, 2024
a4440e3
Merge branch 'conda:main' into cleaner-msys2-support
ifitchet Apr 17, 2024
8e919d0
rework the tests in the new style
ifitchet Apr 17, 2024
144404b
Merge remote-tracking branch 'upstream/main' into pr/13649
kenodegard Apr 18, 2024
af540f4
Parameterize test
kenodegard Apr 18, 2024
32cb5e5
Simplify
kenodegard Apr 18, 2024
8e97a45
Update cygpath tests for greater confidence
kenodegard Apr 19, 2024
81493bf
rework the unix_path_to_native() fallback code to actually resemble t…
ifitchet Apr 24, 2024
ade59fd
tests on windows didnt pick up a missing param on non-windows
ifitchet Apr 24, 2024
62d0440
appease ruff
ifitchet Apr 24, 2024
398659d
appease ruff some more (very finicky!)
ifitchet Apr 24, 2024
01395eb
ruff (again -- why didnt I see this last time?)
ifitchet Apr 24, 2024
1e73766
Merge branch 'main' into cleaner-msys2-support
ifitchet May 2, 2024
1436610
Inline constants
kenodegard May 2, 2024
397aa79
Additional fallback test cases
kenodegard May 3, 2024
ada324e
Update test_activate.py
kenodegard May 3, 2024
64cef8d
Update unix_path_to_native to handle all test cases
kenodegard May 3, 2024
853a2de
Typing
kenodegard May 3, 2024
4d98451
Move cygpath fallback into new _Cygpath class
kenodegard May 6, 2024
5652de9
Use ntpath & posixpath instead of os.path
kenodegard May 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 208 additions & 18 deletions conda/activate.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

import abc
import json
import ntpath
import os
import posixpath
import re
import sys
from logging import getLogger
Expand All @@ -28,6 +30,8 @@
join,
)
from pathlib import Path
from shutil import which
from subprocess import run
from textwrap import dedent
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -618,6 +622,38 @@ def _get_starting_path_list(self):
def _get_path_dirs(self, prefix):
if on_win: # pragma: unix no cover
yield prefix.rstrip("\\")

# We need to stat(2) for possible environments because
# tests can't be told where to look!
#
# mingw-w64 is a legacy variant used by m2w64-* packages
#
# We could include clang32 and mingw32 variants
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could but are't because...?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Erm, good question.
I limited it to 64-bit variants as "we" don't built 32-bit variants of any packages any more.
The choice of "we" and 64-bit only is a little parochial and may not cover the wider conda community's needs.

The cost will be two extra (filename munge + stat(2)) for everyone on Windows each time this function is called (conda activate|deactivate|build).

variants = []
for variant in ["ucrt64", "clang64", "mingw64", "clangarm64"]:
path = self.sep.join((prefix, "Library", variant))

# MSYS2 /c/
# cygwin /cygdrive/c/
if re.match("^(/[A-Za-z]/|/cygdrive/[A-Za-z]/).*", prefix):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will it only ever be a windows drive path? or could we also say:

Suggested change
if re.match("^(/[A-Za-z]/|/cygdrive/[A-Za-z]/).*", prefix):
if "/" in prefix:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kenodegard to me that seems risky. Can I create an expecting-to-be-Windows conda environment with / in the name? Probably: md here/there\conda-meta (or whatever the minimum conda env is).
That said, anyone who mixes / and \ in the same tree and is using MSYS2 is probably in for a world of pain for other reasons.
I might think that prefix.startswith("/") would be OK but can you create an environment that doesn't start at the filesystem root? Can I conda activate here\there and what will the value of prefix be, will it always be resolved to the filesystem root?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we need the match, it might be slightly faster to assign the return value of re.compile to a variable and reuse it in the loop.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jezdez We don't need the match, we're just looking to identify MSYS2 or Cygwin pathnames hence the grouping for alternates.
I don't know enough the the detail of Python's re to know if (?:...) is a saving.

path = unix_path_to_native(path, prefix)

if isdir(path):
variants.append(variant)

if len(variants) > 1:
print(
f"WARNING: {prefix}: {variants} MSYS2 envs exist: please check your dependencies",
file=sys.stderr,
)
print(
f"WARNING: conda list -n {self._default_env(prefix)}",
file=sys.stderr,
)

if variants:
yield self.sep.join((prefix, "Library", variants[0], "bin"))

yield self.sep.join((prefix, "Library", "mingw-w64", "bin"))
yield self.sep.join((prefix, "Library", "usr", "bin"))
yield self.sep.join((prefix, "Library", "bin"))
Expand Down Expand Up @@ -830,6 +866,119 @@ def ensure_fs_path_encoding(value):
return value


class _Cygpath:
@classmethod
def nt_to_posix(cls, paths: str) -> str:
return cls.RE_UNIX.sub(cls.translate_unix, paths).replace(
ntpath.pathsep, posixpath.pathsep
)

RE_UNIX = re.compile(
r"""
(?P<drive>[A-Za-z]:)?
(?P<path>[\/\\]+(?:[^:*?\"<>|;]+[\/\\]*)*)
""",
flags=re.VERBOSE,
)

@staticmethod
def translate_unix(match: re.Match) -> str:
return "/" + (
((match.group("drive") or "").lower() + match.group("path"))
.replace("\\", "/")
.replace(":", "") # remove drive letter delimiter
.replace("//", "/")
.rstrip("/")
)

@classmethod
def posix_to_nt(cls, paths: str, prefix: str) -> str:
if posixpath.sep not in paths:
# nothing to translate
return paths

if posixpath.pathsep in paths:
return ntpath.pathsep.join(
cls.posix_to_nt(path, prefix) for path in paths.split(posixpath.pathsep)
)
path = paths

# Reverting a Unix path means unpicking MSYS2/Cygwin
# conventions -- in order!
# 1. drive letter forms:
# /x/here/there - MSYS2
# /cygdrive/x/here/there - Cygwin
# transformed to X:\here\there -- note the uppercase drive letter!
# 2. either:
# a. mount forms:
# //here/there
# transformed to \\here\there
# b. root filesystem forms:
# /here/there
# transformed to {prefix}\Library\here\there
# 3. anything else

# continue performing substitutions until a match is found
path, subs = cls.RE_DRIVE.subn(cls.translation_drive, path)
if not subs:
path, subs = cls.RE_MOUNT.subn(cls.translation_mount, path)
if not subs:
path, _ = cls.RE_ROOT.subn(
lambda match: cls.translation_root(match, prefix), path
)

return re.sub(r"/+", r"\\", path)

RE_DRIVE = re.compile(
r"""
^
(/cygdrive)?
/(?P<drive>[A-Za-z])
(/+(?P<path>.*)?)?
$
""",
flags=re.VERBOSE,
)

@staticmethod
def translation_drive(match: re.Match) -> str:
drive = match.group("drive").upper()
path = match.group("path") or ""
return f"{drive}:\\{path}"

RE_MOUNT = re.compile(
r"""
^
//(
(?P<mount>[^/]+)
(?P<path>/+.*)?
)?
$
""",
flags=re.VERBOSE,
)

@staticmethod
def translation_mount(match: re.Match) -> str:
mount = match.group("mount") or ""
path = match.group("path") or ""
return f"\\\\{mount}{path}"

RE_ROOT = re.compile(
r"""
^
(?P<path>/[^:]*)
$
""",
flags=re.VERBOSE,
)

@staticmethod
def translation_root(match: re.Match, prefix: str) -> str:
path = match.group("path")
return f"{prefix}\\Library{path}"


def native_path_to_unix(
paths: str | Iterable[str] | None,
) -> str | tuple[str, ...] | None:
Expand All @@ -844,8 +993,6 @@ def native_path_to_unix(
return "." if isinstance(paths, str) else ()

# on windows, uses cygpath to convert windows native paths to posix paths
from shutil import which
from subprocess import run

# It is very easy to end up with a bash in one place and a cygpath in another due to e.g.
# using upstream MSYS2 bash, but with a conda env that does not have bash but does have
Expand All @@ -855,33 +1002,22 @@ def native_path_to_unix(

bash = which("bash")
cygpath = str(Path(bash).parent / "cygpath") if bash else "cygpath"
joined = paths if isinstance(paths, str) else os.pathsep.join(paths)
joined = paths if isinstance(paths, str) else ntpath.pathsep.join(paths)

try:
# if present, use cygpath to convert paths since its more reliable
unix_path = run(
[cygpath, "--path", joined],
[cygpath, "--unix", "--path", joined],
text=True,
capture_output=True,
check=True,
).stdout.strip()
except FileNotFoundError:
# fallback logic when cygpath is not available
# i.e. conda without anything else installed
def _translation(match):
return "/" + (
match.group(1)
.replace("\\", "/")
.replace(":", "")
.replace("//", "/")
.rstrip("/")
)
log.warning("cygpath is not available, fallback to manual path conversion")

unix_path = (
re.sub(r"([a-zA-Z]:[\/\\]+(?:[^:*?\"<>|;]+[\/\\]*)*)", _translation, joined)
.replace(";", ":")
.rstrip(";")
)
unix_path = _Cygpath.nt_to_posix(joined)
except Exception as err:
log.error("Unexpected cygpath error (%s)", err)
raise
Expand All @@ -891,7 +1027,61 @@ def _translation(match):
elif not unix_path:
return ()
else:
return tuple(unix_path.split(":"))
return tuple(unix_path.split(posixpath.pathsep))


def unix_path_to_native(
paths: str | Iterable[str] | None, prefix: str
) -> str | tuple[str, ...] | None:
if paths is None:
return None
elif not on_win:
return path_identity(paths)

# short-circuit if we don't get any paths
paths = paths if isinstance(paths, str) else tuple(paths)
if not paths:
return "." if isinstance(paths, str) else ()

# on windows, uses cygpath to convert posix paths to windows native paths

# It is very easy to end up with a bash in one place and a cygpath in another due to e.g.
# using upstream MSYS2 bash, but with a conda env that does not have bash but does have
# cygpath. When this happens, we have two different virtual POSIX machines, rooted at
# different points in the Windows filesystem. We do our path conversions with one and
# expect the results to work with the other. It does not.

bash = which("bash")
cygpath = str(Path(bash).parent / "cygpath") if bash else "cygpath"
joined = paths if isinstance(paths, str) else posixpath.pathsep.join(paths)

try:
# if present, use cygpath to convert paths since its more reliable
win_path = run(
[cygpath, "--windows", "--path", joined],
text=True,
capture_output=True,
check=True,
).stdout.strip()
except FileNotFoundError:
# fallback logic when cygpath is not available
# i.e. conda without anything else installed
log.warning("cygpath is not available, fallback to manual path conversion")

# The conda prefix can be in a drive letter form
prefix = _Cygpath.posix_to_nt(prefix, prefix)

win_path = _Cygpath.posix_to_nt(joined, prefix)
except Exception as err:
log.error("Unexpected cygpath error (%s)", err)
raise

if isinstance(paths, str):
return win_path
elif not win_path:
return ()
else:
return tuple(win_path.split(ntpath.pathsep))


def path_identity(paths: str | Iterable[str] | None) -> str | tuple[str, ...] | None:
Expand Down
1 change: 1 addition & 0 deletions docs/source/user-guide/troubleshooting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ PATH folders goes from left to right. If you choose to put Anaconda's folders on
PATH, there are several of them:

* (install root)
* (install root)/Library/(MSYS2 env)/bin ## dependent on MSYS2 packages
* (install root)/Library/mingw-w64/bin
* (install root)/Library/usr/bin
* (install root)/Library/bin
Expand Down
3 changes: 3 additions & 0 deletions news/13649-msys2-environments
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Enhancements

* MSYS2 packages can now use the upstream installation prefixes. (#13649)
Loading