conan-io/conan

View on GitHub
conans/client/envvars/environment.py

Summary

Maintainability
A
2 hrs
Test Coverage
import os
import platform
import textwrap
from collections import OrderedDict

from jinja2 import Template

from conans.errors import ConanException
from conans.util.files import normalize

sh_activate = textwrap.dedent("""\
    #!/usr/bin/env sh

    {%- for it in modified_vars %}
    export CONAN_OLD_{{it}}="${{it}}"
    {%- endfor %}

    while read -r line; do
        LINE="$(eval echo $line)";
        export "$LINE";
    done < "{{ environment_file }}"

    export CONAN_OLD_PS1="$PS1"
    export PS1="({{venv_name}}) $PS1"
""")

sh_deactivate = textwrap.dedent("""\
    #!/usr/bin/env sh
    export PS1="$CONAN_OLD_PS1"
    unset CONAN_OLD_PS1

    {% for it in modified_vars %}
    export {{it}}="$CONAN_OLD_{{it}}"
    unset CONAN_OLD_{{it}}
    {%- endfor %}
    {%- for it in new_vars %}
    unset {{it}}
    {%- endfor %}
""")

bat_activate = textwrap.dedent("""\
    @echo off

    {%- for it in modified_vars %}
    SET "CONAN_OLD_{{it}}=%{{it}}%"
    {%- endfor %}

    FOR /F "usebackq tokens=1,* delims==" %%i IN ("{{ environment_file }}") DO (
        CALL SET "%%i=%%j"
    )

    SET "CONAN_OLD_PROMPT=%PROMPT%"
    SET "PROMPT=({{venv_name}}) %PROMPT%"
""")

bat_deactivate = textwrap.dedent("""\
    @echo off

    SET "PROMPT=%CONAN_OLD_PROMPT%"
    SET "CONAN_OLD_PROMPT="

    {% for it in modified_vars %}
    SET "{{it}}=%CONAN_OLD_{{it}}%"
    SET "CONAN_OLD_{{it}}="
    {%- endfor %}
    {%- for it in new_vars %}
    SET "{{it}}="
    {%- endfor %}
""")

ps1_activate = textwrap.dedent("""\
    {%- for it in modified_vars %}
    $env:CONAN_OLD_{{venv_name}}_{{it}}=$env:{{it}}
    {%- endfor %}

    foreach ($line in Get-Content "{{ environment_file }}") {
        $var,$value = $line -split '=',2
        $value_expanded = $ExecutionContext.InvokeCommand.ExpandString($value)
        Set-Item env:\\$var -Value "$value_expanded"
    }

    function global:_old_conan_{{venv_name}}_prompt {""}
    $function:_old_conan_{{venv_name}}_prompt = $function:prompt
    function global:prompt {
        write-host "({{venv_name}}) " -nonewline; & $function:_old_conan_{{venv_name}}_prompt
    }
""")

ps1_deactivate = textwrap.dedent("""\
    $function:prompt = $function:_old_conan_{{venv_name}}_prompt
    remove-item function:_old_conan_{{venv_name}}_prompt

    {% for it in modified_vars %}
    $env:{{it}}=$env:CONAN_OLD_{{venv_name}}_{{it}}
    Remove-Item env:CONAN_OLD_{{venv_name}}_{{it}}
    {%- endfor %}
    {%- for it in new_vars %}
    Remove-Item env:{{it}}
    {%- endfor %}
""")


BAT_FLAVOR = "bat"
PS1_FLAVOR = "ps1"
SH_FLAVOR = "sh"


def _variable_placeholder(flavor, name, append_with_spaces):
    """
    :param flavor: flavor of the execution environment
    :param name: variable name
    :return: placeholder for the variable name formatted for a certain execution environment.
    (e.g., cmd, ps1, sh).
    """
    if flavor == BAT_FLAVOR:
        return "%{}%".format(name)
    if flavor == PS1_FLAVOR:
        return "$env:%s" % name
    # flavor == sh
    return "${%s:+ $%s}" % (name, name) if append_with_spaces else "${%s:+:$%s}" % (name,  name)


def _format_values(flavor, variables, append_with_spaces):
    """
    Formats the values for the different supported script language flavors.
    :param flavor: flavor of the execution environment
    :param variables: variables to be formatted
    :return:
    """

    if flavor in [BAT_FLAVOR, PS1_FLAVOR] and platform.system() == "Windows":
        path_sep, quote_elements = ";", False
    elif flavor == PS1_FLAVOR:
        path_sep, quote_elements = ":", False
    else:
        path_sep, quote_elements = ":", True

    for name, value in variables:
        # activate values
        if isinstance(value, list):
            value = list(OrderedDict.fromkeys(value))  # Avoid repeated entries, while keeping order
            append_space = name in append_with_spaces
            placeholder = _variable_placeholder(flavor, name, append_space)
            if append_space:
                # Variables joined with spaces look like: CPPFLAGS="one two three"
                if flavor == SH_FLAVOR:
                    value = " ".join(value) + placeholder
                else:
                    value = " ".join(value + [placeholder])
                value = "\"%s\"" % value if quote_elements else value
            else:
                # Quoted variables joined with pathset may look like:
                # PATH="one path":"two paths"
                # Unquoted variables joined with pathset may look like: PATH=one path;two paths
                value = ["\"%s\"" % v for v in value] if quote_elements else value
                if flavor == SH_FLAVOR:
                    value = path_sep.join(value) + placeholder
                else:
                    value = path_sep.join(value + [placeholder])
        else:
            # single value
            value = "\"%s\"" % value if quote_elements else value
        if platform.system() != "Windows":
            value = value.replace("\\", "\\\\")

        # deactivate values
        existing = name in os.environ
        yield name, value, existing


def _files(env_vars, vars_with_spaces, flavor, activate_tpl, deactivate_tpl, venv_name,
           env_filepath):
    ret = list(_format_values(flavor, env_vars.items(), vars_with_spaces))
    modified_vars = [name for name, _, existing in ret if existing]
    new_vars = [name for name, _, existing in ret if not existing]

    activate_content = activate_tpl.render(environment_file=env_filepath,
                                           modified_vars=modified_vars, new_vars=new_vars,
                                           venv_name=venv_name)
    deactivate_content = deactivate_tpl.render(modified_vars=modified_vars, new_vars=new_vars,
                                               venv_name=venv_name)

    environment_lines = ["{}={}".format(name, value) for name, value, _ in ret]
    # This blank line is important, otherwise the script doens't process last line
    environment_lines.append('')

    if flavor == SH_FLAVOR:
        # replace CRLF->LF guarantee it is always LF, irrespective of current .py file
        activate_content = activate_content.replace("\r\n", "\n")
        deactivate_content = deactivate_content.replace("\r\n", "\n")
        environment = "\n".join(environment_lines)
    else:
        activate_content = normalize(activate_content)
        deactivate_content = normalize(deactivate_content)
        environment = os.linesep.join(environment_lines)

    return activate_content, deactivate_content, environment


def env_files(env_vars, vars_with_spaces, flavor, folder, name, venv_name):
    env_filename = "environment{}.{}.env".format(name, flavor)
    activate_filename = "activate{}.{}".format(name, flavor)
    deactivate_filename = "deactivate{}.{}".format(name, flavor)

    templates = {SH_FLAVOR: (sh_activate, sh_deactivate),
                 BAT_FLAVOR: (bat_activate, bat_deactivate),
                 PS1_FLAVOR: (ps1_activate, ps1_deactivate)}
    try:
        activate, deactivate = templates[flavor]
    except KeyError:
        raise ConanException("Unrecognized flavor: %s" % flavor)
    activate_tpl, deactivate_tpl = Template(activate), Template(deactivate)

    env_filepath = os.path.abspath(os.path.join(folder, env_filename))
    activate, deactivate, envfile = _files(env_vars, vars_with_spaces, flavor, activate_tpl,
                                           deactivate_tpl, venv_name, env_filepath)

    result = {activate_filename: activate,
              deactivate_filename: deactivate,
              env_filename: envfile}
    return result