conan-io/conan

View on GitHub
conans/client/subsystems.py

Summary

Maintainability
F
3 days
Test Coverage
"""
Potential scenarios:

- Running from a Windows native "cmd"
  - Targeting Windows native (os.subsystem = None)
    - No need of bash (no conf at all)
    - Need to build in bash (tools.microsoft.bash:subsystem=xxx,
                             tools.microsoft.bash:path=<path>,
                             conanfile.win_bash)
    - Need to run (tests) in bash (tools.microsoft.bash:subsystem=xxx,
                                   tools.microsoft.bash:path=<path>,
                                   conanfile.win_bash_run)
  - Targeting Subsystem (os.subsystem = msys2/cygwin)
    - Always builds and runs in bash (tools.microsoft.bash:path)

- Running from a subsytem terminal (tools.microsoft.bash:subsystem=xxx,
                                    tools.microsoft.bash:path=None) NO ERROR mode for not specifying it? =CURRENT?
  - Targeting Windows native (os.subsystem = None)
  - Targeting Subsystem (os.subsystem = msys2/cygwin)

"""
import os
import platform
import re
import subprocess

from conans.errors import ConanException

WINDOWS = "windows"
MSYS2 = 'msys2'
MSYS = 'msys'
CYGWIN = 'cygwin'
WSL = 'wsl'  # Windows Subsystem for Linux
SFU = 'sfu'  # Windows Services for UNIX


def command_env_wrapper(conanfile, command, envfiles, envfiles_folder, scope="build"):
    from conan.tools.env.environment import environment_wrap_command
    if getattr(conanfile, "conf", None) is None:
        # TODO: No conf, no profile defined!! This happens at ``export()`` time
        #  Is it possible to run a self.run() in export() in bash?
        #  Is it necessary? Shouldn't be
        return command

    active = conanfile.conf.get("tools.microsoft.bash:active", check_type=bool)
    subsystem = conanfile.conf.get("tools.microsoft.bash:subsystem")

    if platform.system() == "Windows" and (
            (conanfile.win_bash and scope == "build") or
            (conanfile.win_bash_run and scope == "run")):
        if subsystem is None:
            raise ConanException("win_bash/win_bash_run defined but no "
                                 "tools.microsoft.bash:subsystem")
        if active:
            wrapped_cmd = environment_wrap_command(envfiles, envfiles_folder, command)
        else:
            wrapped_cmd = _windows_bash_wrapper(conanfile, command, envfiles, envfiles_folder)
    else:
        wrapped_cmd = environment_wrap_command(envfiles, envfiles_folder, command)
    return wrapped_cmd


def _windows_bash_wrapper(conanfile, command, env, envfiles_folder):
    from conan.tools.env import Environment
    from conan.tools.env.environment import environment_wrap_command
    """ Will wrap a unix command inside a bash terminal It requires to have MSYS2, CYGWIN, or WSL"""

    subsystem = conanfile.conf.get("tools.microsoft.bash:subsystem")
    shell_path = conanfile.conf.get("tools.microsoft.bash:path")
    if not shell_path:
        raise ConanException("The config 'tools.microsoft.bash:path' is "
                             "needed to run commands in a Windows subsystem")
    env = env or []
    if subsystem == MSYS2:
        # Configure MSYS2 to inherith the PATH
        msys2_mode_env = Environment()
        _msystem = {"x86": "MINGW32"}.get(conanfile.settings.get_safe("arch"), "MINGW64")
        # https://www.msys2.org/wiki/Launchers/ dictates that the shell should be launched with
        # - MSYSTEM defined
        # - CHERE_INVOKING is necessary to keep the CWD and not change automatically to the user home
        msys2_mode_env.define("MSYSTEM", _msystem)
        msys2_mode_env.define("MSYS2_PATH_TYPE", "inherit")
        # So --login do not change automatically to the user home
        msys2_mode_env.define("CHERE_INVOKING", "1")
        path = os.path.join(conanfile.generators_folder, "msys2_mode.bat")
        # Make sure we save pure .bat files, without sh stuff
        wb, conanfile.win_bash = conanfile.win_bash, None
        msys2_mode_env.vars(conanfile, "build").save_bat(path)
        conanfile.win_bash = wb
        env.append(path)

    wrapped_shell = '"%s"' % shell_path if " " in shell_path else shell_path
    wrapped_shell = environment_wrap_command(env, envfiles_folder, wrapped_shell,
                                             accepted_extensions=("bat", "ps1"))

    # Wrapping the inside_command enable to prioritize our environment, otherwise /usr/bin go
    # first and there could be commands that we want to skip
    wrapped_user_cmd = environment_wrap_command(env, envfiles_folder, command,
                                                accepted_extensions=("sh", ))
    wrapped_user_cmd = _escape_windows_cmd(wrapped_user_cmd)

    # according to https://www.msys2.org/wiki/Launchers/, it is necessary to use --login shell
    # running without it is discouraged
    final_command = '{} --login -c {}'.format(wrapped_shell, wrapped_user_cmd)
    return final_command


def _escape_windows_cmd(command):
    """ To use in a regular windows cmd.exe
        1. Adds escapes so the argument can be unpacked by CommandLineToArgvW()
        2. Adds escapes for cmd.exe so the argument survives cmd.exe's substitutions.

        Useful to escape commands to be executed in a windows bash (msys2, cygwin etc)
    """
    quoted_arg = subprocess.list2cmdline([command])
    return "".join(["^%s" % arg if arg in r'()%!^"<>&|' else arg for arg in quoted_arg])


def deduce_subsystem(conanfile, scope):
    """ used by:
    - EnvVars: to decide if using :  ; as path separator, translate paths to subsystem
               and decide to generate a .bat or .sh
    - Autotools: to define the full abs path to the "configure" script
    - GnuDeps: to map all the paths from dependencies
    - Aggregation of envfiles: to map each aggregated path to the subsystem
    - unix_path: util for recipes
    """
    if scope.startswith("build"):
        if hasattr(conanfile, "settings_build"):
            the_os = conanfile.settings_build.get_safe("os")
        else:
            the_os = platform.system()  # FIXME: Temporary fallback until 2.0
    else:
        the_os = conanfile.settings.get_safe("os")

    if not str(the_os).startswith("Windows"):
        return None

    subsystem = conanfile.conf.get("tools.microsoft.bash:subsystem")
    if not subsystem:
        if conanfile.win_bash:
            raise ConanException("win_bash=True but tools.microsoft.bash:subsystem "
                                 "configuration not defined")
        if conanfile.win_bash_run:
            raise ConanException("win_bash_run=True but tools.microsoft.bash:subsystem "
                                 "configuration not defined")
        return WINDOWS
    active = conanfile.conf.get("tools.microsoft.bash:active", check_type=bool)
    if active:
        return subsystem

    if scope.startswith("build"):  # "run" scope do not follow win_bash
        if conanfile.win_bash:
            return subsystem
    elif scope.startswith("run"):
        if conanfile.win_bash_run:
            return subsystem

    return WINDOWS


def subsystem_path(subsystem, path):
    """"Used to translate windows paths to MSYS unix paths like
    c/users/path/to/file. Not working in a regular console or MinGW!
    """
    if subsystem is None or subsystem == WINDOWS:
        return path

    if os.path.exists(path):
        # if the path doesn't exist (and abs) we cannot guess the casing
        path = get_cased_path(path)

    if path.startswith('\\\\?\\'):
        path = path[4:]
    path = path.replace(":/", ":\\")
    append_prefix = re.match(r'[a-z]:\\', path, re.IGNORECASE)
    pattern = re.compile(r'([a-z]):\\', re.IGNORECASE)
    path = pattern.sub('/\\1/', path).replace('\\', '/')

    if append_prefix:
        if subsystem in (MSYS, MSYS2):
            return path.lower()
        elif subsystem == CYGWIN:
            return '/cygdrive' + path.lower()
        elif subsystem == WSL:
            return '/mnt' + path[0:2].lower() + path[2:]
        elif subsystem == SFU:
            path = path.lower()
            return '/dev/fs' + path[0] + path[1:].capitalize()
    else:
        return path if subsystem == WSL else path.lower()
    return None


def get_cased_path(name):
    if platform.system() != "Windows":
        return name
    if not os.path.isabs(name):
        name = os.path.abspath(name)

    result = []
    current = name
    while True:
        parent, child = os.path.split(current)
        if parent == current:
            break

        child_cased = child
        if os.path.exists(parent):
            children = os.listdir(parent)
            for c in children:
                if c.upper() == child.upper():
                    child_cased = c
                    break
        result.append(child_cased)
        current = parent
    drive, _ = os.path.splitdrive(current)
    result.append(drive)
    return os.sep.join(reversed(result))