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):
    from conan.tools.env.environment import environment_wrap_command
    if platform.system() == "Windows" and conanfile.win_bash:  # New, Conan 2.0
        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 subsystem or not shell_path:
        raise ConanException("The config 'tools.microsoft.bash:subsystem' and "
                             "'tools.microsoft.bash:path' are "
                             "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")
        msys2_mode_env.define("MSYSTEM", _msystem)
        msys2_mode_env.define("MSYS2_PATH_TYPE", "inherit")
        path = os.path.join(conanfile.generators_folder, "msys2_mode.bat")
        msys2_mode_env.vars(conanfile, "build").save_bat(path)
        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)

    final_command = '{} -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")
            subsystem = conanfile.settings_build.get_safe("os.subsystem")
        else:
            the_os = platform.system()  # FIXME: Temporary fallback until 2.0
            subsystem = None
    else:
        the_os = conanfile.settings.get_safe("os")
        subsystem = conanfile.settings.get_safe("os.subsystem")

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

    if subsystem is None and not scope.startswith("build"):  # "run" scope do not follow win_bash
        return WINDOWS

    if subsystem is None:  # Not defined by settings, so native windows
        if not conanfile.win_bash:
            return WINDOWS

        subsystem = conanfile.conf.get("tools.microsoft.bash:subsystem")
        if not subsystem:
            raise ConanException("The config 'tools.microsoft.bash:subsystem' is "
                                 "needed to run commands in a Windows subsystem")
    return subsystem


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))