-
Notifications
You must be signed in to change notification settings - Fork 2k
support multiple MSYS2 environments #13649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
40dd52e
e0239ef
07124f7
1ce56f7
5f9e9b2
7d13bda
b4174e2
a4440e3
8e919d0
144404b
af540f4
32cb5e5
8e97a45
81493bf
ade59fd
62d0440
398659d
01395eb
1e73766
1436610
397aa79
ada324e
64cef8d
853a2de
4d98451
5652de9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -13,7 +13,9 @@ | |||||
|
|
||||||
| import abc | ||||||
| import json | ||||||
| import ntpath | ||||||
| import os | ||||||
| import posixpath | ||||||
| import re | ||||||
| import sys | ||||||
| from logging import getLogger | ||||||
|
|
@@ -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 | ||||||
|
|
||||||
|
|
@@ -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 | ||||||
| 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): | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||
| 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", | ||||||
skupr-anaconda marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| 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")) | ||||||
|
|
@@ -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: | ||||||
|
|
@@ -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 | ||||||
|
|
@@ -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 | ||||||
|
|
@@ -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( | ||||||
ifitchet marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| 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: | ||||||
|
|
||||||
| 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) |
There was a problem hiding this comment.
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...?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ifitchet?
There was a problem hiding this comment.
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).