diff --git a/.gitignore b/.gitignore index 2e4f6277c3e..4a27bb1ea69 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ __pycache__/ # Distribution / packaging .Python -env/ build/ develop-eggs/ dist/ diff --git a/conan/tools/env/__init__.py b/conan/tools/env/__init__.py new file mode 100644 index 00000000000..7656f772c0d --- /dev/null +++ b/conan/tools/env/__init__.py @@ -0,0 +1 @@ +from conan.tools.env.environment import Environment diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py new file mode 100644 index 00000000000..5ee2b7ad647 --- /dev/null +++ b/conan/tools/env/environment.py @@ -0,0 +1,290 @@ +import fnmatch +import os +import textwrap +from collections import OrderedDict + +from conans.errors import ConanException +from conans.util.files import save + + +class _EnvVarPlaceHolder: + pass + + +class _Sep(str): + pass + + +class _PathSep: + pass + + +def environment_wrap_command(filename, cmd, cwd=None): + assert filename + filenames = [filename] if not isinstance(filename, list) else filename + bats, shs = [], [] + for f in filenames: + full_path = os.path.join(cwd, f) if cwd else f + if os.path.isfile("{}.bat".format(full_path)): + bats.append("{}.bat".format(f)) + elif os.path.isfile("{}.sh".format(full_path)): + shs.append("{}.sh".format(f)) + if bats and shs: + raise ConanException("Cannot wrap command with different envs, {} - {}".format(bats, shs)) + + if bats: + command = " && ".join(bats) + return "{} && {}".format(command, cmd) + elif shs: + command = " && ".join(". ./{}".format(f) for f in shs) + return "{} && {}".format(command, cmd) + else: + return cmd + + +class Environment: + def __init__(self): + # TODO: Maybe we need to pass conanfile to get the [conf] + # It being ordered allows for Windows case-insensitive composition + self._values = OrderedDict() # {var_name: [] of values, including separators} + + def __bool__(self): + return bool(self._values) + + __nonzero__ = __bool__ + + def __repr__(self): + return repr(self._values) + + def vars(self): + return list(self._values.keys()) + + def value(self, name, placeholder="{name}", pathsep=os.pathsep): + return self._format_value(name, self._values[name], placeholder, pathsep) + + @staticmethod + def _format_value(name, varvalues, placeholder, pathsep): + values = [] + for v in varvalues: + + if v is _EnvVarPlaceHolder: + values.append(placeholder.format(name=name)) + elif v is _PathSep: + values.append(pathsep) + else: + values.append(v) + return "".join(values) + + @staticmethod + def _list_value(value, separator): + if isinstance(value, list): + result = [] + for v in value[:-1]: + result.append(v) + result.append(separator) + result.extend(value[-1:]) + return result + else: + return [value] + + def define(self, name, value, separator=" "): + value = self._list_value(value, _Sep(separator)) + self._values[name] = value + + def define_path(self, name, value): + value = self._list_value(value, _PathSep) + self._values[name] = value + + def unset(self, name): + """ + clears the variable, equivalent to a unset or set XXX= + """ + self._values[name] = [] + + def append(self, name, value, separator=" "): + value = self._list_value(value, _Sep(separator)) + self._values[name] = [_EnvVarPlaceHolder] + [_Sep(separator)] + value + + def append_path(self, name, value): + value = self._list_value(value, _PathSep) + self._values[name] = [_EnvVarPlaceHolder] + [_PathSep] + value + + def prepend(self, name, value, separator=" "): + value = self._list_value(value, _Sep(separator)) + self._values[name] = value + [_Sep(separator)] + [_EnvVarPlaceHolder] + + def prepend_path(self, name, value): + value = self._list_value(value, _PathSep) + self._values[name] = value + [_PathSep] + [_EnvVarPlaceHolder] + + def save_bat(self, filename, generate_deactivate=False, pathsep=os.pathsep): + deactivate = textwrap.dedent("""\ + echo Capturing current environment in deactivate_{filename} + setlocal + echo @echo off > "deactivate_{filename}" + echo echo Restoring environment >> "deactivate_{filename}" + for %%v in ({vars}) do ( + set foundenvvar= + for /f "delims== tokens=1,2" %%a in ('set') do ( + if "%%a" == "%%v" ( + echo set %%a=%%b>> "deactivate_{filename}" + set foundenvvar=1 + ) + ) + if not defined foundenvvar ( + echo set %%v=>> "deactivate_{filename}" + ) + ) + endlocal + + """).format(filename=filename, vars=" ".join(self._values.keys())) + capture = textwrap.dedent("""\ + @echo off + {deactivate} + echo Configuring environment variables + """).format(deactivate=deactivate if generate_deactivate else "") + result = [capture] + for varname, varvalues in self._values.items(): + value = self._format_value(varname, varvalues, "%{name}%", pathsep) + result.append('set {}={}'.format(varname, value)) + + content = "\n".join(result) + save(filename, content) + + def save_ps1(self, filename, generate_deactivate=False, pathsep=os.pathsep): + # FIXME: This is broken and doesnt work + deactivate = "" + capture = textwrap.dedent("""\ + {deactivate} + """).format(deactivate=deactivate if generate_deactivate else "") + result = [capture] + for varname, varvalues in self._values.items(): + value = self._format_value(varname, varvalues, "$env:{name}", pathsep) + result.append('$env:{}={}'.format(varname, value)) + + content = "\n".join(result) + save(filename, content) + + def save_sh(self, filename, generate_deactivate=False, pathsep=os.pathsep): + deactivate = textwrap.dedent("""\ + echo Capturing current environment in deactivate_{filename} + echo echo Restoring variables >> deactivate_{filename} + for v in {vars} + do + value=$(printenv $v) + if [ -n "$value" ] + then + echo export "$v=$value" >> deactivate_{filename} + else + echo unset $v >> deactivate_{filename} + fi + done + echo Configuring environment variables + """.format(filename=filename, vars=" ".join(self._values.keys()))) + capture = textwrap.dedent("""\ + {deactivate} + echo Configuring environment variables + """).format(deactivate=deactivate if generate_deactivate else "") + result = [capture] + for varname, varvalues in self._values.items(): + value = self._format_value(varname, varvalues, "${name}", pathsep) + if value: + result.append('export {}="{}"'.format(varname, value)) + else: + result.append('unset {}'.format(varname)) + + content = "\n".join(result) + save(filename, content) + + def compose(self, other): + """ + :type other: Environment + """ + for k, v in other._values.items(): + existing = self._values.get(k) + if existing is None: + self._values[k] = v + else: + try: + index = v.index(_EnvVarPlaceHolder) + except ValueError: # The other doesn't have placeholder, overwrites + self._values[k] = v + else: + new_value = v[:] # do a copy + new_value[index:index + 1] = existing # replace the placeholder + # Trim front and back separators + val = new_value[0] + if isinstance(val, _Sep) or val is _PathSep: + new_value = new_value[1:] + val = new_value[-1] + if isinstance(val, _Sep) or val is _PathSep: + new_value = new_value[:-1] + self._values[k] = new_value + return self + + +class ProfileEnvironment: + def __init__(self): + self._environments = OrderedDict() + + def __repr__(self): + return repr(self._environments) + + def get_env(self, ref): + """ computes package-specific Environment + it is only called when conanfile.buildenv is called + """ + result = Environment() + for pattern, env in self._environments.items(): + if pattern is None or fnmatch.fnmatch(str(ref), pattern): + result = result.compose(env) + return result + + def compose(self, other): + """ + :type other: ProfileEnvironment + """ + for pattern, environment in other._environments.items(): + existing = self._environments.get(pattern) + if existing is not None: + self._environments[pattern] = existing.compose(environment) + else: + self._environments[pattern] = environment + + @staticmethod + def loads(text): + result = ProfileEnvironment() + for line in text.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + for op, method in (("+=", "append"), ("=+", "prepend"), + ("=!", "unset"), ("=", "define")): + tokens = line.split(op, 1) + if len(tokens) != 2: + continue + pattern_name, value = tokens + pattern_name = pattern_name.split(":", 1) + if len(pattern_name) == 2: + pattern, name = pattern_name + else: + pattern, name = None, pattern_name[0] + + env = Environment() + if method == "unset": + env.unset(name) + else: + if value.startswith("(path)"): + value = value[6:] + method = method + "_path" + getattr(env, method)(name, value) + + existing = result._environments.get(pattern) + if existing is None: + result._environments[pattern] = env + else: + result._environments[pattern] = existing.compose(env) + break + else: + raise ConanException("Bad env defintion: {}".format(line)) + return result diff --git a/conans/client/generators/__init__.py b/conans/client/generators/__init__.py index 5533655b0e1..7ca1ca0d7a4 100644 --- a/conans/client/generators/__init__.py +++ b/conans/client/generators/__init__.py @@ -89,7 +89,7 @@ def _new_generator(self, generator_name, output): if generator_name == "CMakeToolchain": from conan.tools.cmake import CMakeToolchain return CMakeToolchain - if generator_name == "CMakeDeps": + elif generator_name == "CMakeDeps": from conan.tools.cmake import CMakeDeps return CMakeDeps elif generator_name == "MakeToolchain": @@ -196,4 +196,4 @@ def write_toolchain(conanfile, path, output): with conanfile_exception_formatter(str(conanfile), "generate"): conanfile.generate() - # TODO: Lets discuss what to do with the environment + # TODO: Lets discuss what to do with the environment diff --git a/conans/client/loader.py b/conans/client/loader.py index 78e1a6fee01..a88c9baa45b 100644 --- a/conans/client/loader.py +++ b/conans/client/loader.py @@ -193,7 +193,7 @@ def _initialize_conanfile(conanfile, profile): if pkg_settings: tmp_settings.update_values(pkg_settings) - conanfile.initialize(tmp_settings, profile.env_values) + conanfile.initialize(tmp_settings, profile.env_values, profile.buildenv) conanfile.conf = profile.conf.get_conanfile_conf(ref_str) def load_consumer(self, conanfile_path, profile_host, name=None, version=None, user=None, @@ -262,7 +262,7 @@ def load_conanfile_txt(self, conan_txt_path, profile_host, ref=None): def _parse_conan_txt(self, contents, path, display_name, profile): conanfile = ConanFile(self._output, self._runner, display_name) - conanfile.initialize(Settings(), profile.env_values) + conanfile.initialize(Settings(), profile.env_values, profile.buildenv) conanfile.conf = profile.conf.get_conanfile_conf(None) # It is necessary to copy the settings, because the above is only a constraint of # conanfile settings, and a txt doesn't define settings. Necessary for generators, @@ -302,7 +302,7 @@ def load_virtual(self, references, profile_host, scope_options=True, # for the reference (keep compatibility) conanfile = ConanFile(self._output, self._runner, display_name="virtual") conanfile.initialize(profile_host.processed_settings.copy(), - profile_host.env_values) + profile_host.env_values, profile_host.buildenv) conanfile.conf = profile_host.conf.get_conanfile_conf(None) conanfile.settings = profile_host.processed_settings.copy_values() diff --git a/conans/client/profile_loader.py b/conans/client/profile_loader.py index 67cf0313dc6..9ba3a1cd295 100644 --- a/conans/client/profile_loader.py +++ b/conans/client/profile_loader.py @@ -1,6 +1,7 @@ import os from collections import OrderedDict, defaultdict +from conan.tools.env.environment import ProfileEnvironment from conans.errors import ConanException, ConanV2Exception from conans.model.conf import ConfDefinition from conans.model.env_info import EnvValues, unquote @@ -149,7 +150,8 @@ def _load_profile(text, profile_path, default_folder): # Current profile before update with parents (but parent variables already applied) doc = ConfigParser(profile_parser.profile_text, - allowed_fields=["build_requires", "settings", "env", "options", "conf"]) + allowed_fields=["build_requires", "settings", "env", "options", "conf", + "buildenv"]) # Merge the inherited profile with the readed from current profile _apply_inner_profile(doc, inherited_profile) @@ -224,6 +226,10 @@ def get_package_name_value(item): new_prof.loads(doc.conf, profile=True) base_profile.conf.update_conf_definition(new_prof) + if doc.buildenv: + buildenv = ProfileEnvironment.loads(doc.buildenv) + base_profile.buildenv.compose(buildenv) + def profile_from_args(profiles, settings, options, env, cwd, cache): """ Return a Profile object, as the result of merging a potentially existing Profile diff --git a/conans/model/conan_file.py b/conans/model/conan_file.py index a8720376a03..ce00895baab 100644 --- a/conans/model/conan_file.py +++ b/conans/model/conan_file.py @@ -4,6 +4,7 @@ import six from six import string_types +from conan.tools.env import Environment from conans.client import tools from conans.client.output import ScopedOutput from conans.client.tools.env import environment_append, no_op, pythonpath @@ -147,8 +148,21 @@ def __init__(self, output, runner, display_name="", user=None, channel=None, req self._conan_requester = requester self.layout = Layout() + self.buildenv_info = Environment() + self.runenv_info = Environment() + self._conan_buildenv = None # The profile buildenv, will be assigned initialize() - def initialize(self, settings, env): + @property + def buildenv(self): + # Lazy computation of the package buildenv based on the profileone + if not isinstance(self._conan_buildenv, Environment): + # TODO: missing user/channel + ref_str = "{}/{}".format(self.name, self.version) + self._conan_buildenv = self._conan_buildenv.get_env(ref_str) + return self._conan_buildenv + + def initialize(self, settings, env, buildenv=None): + self._conan_buildenv = buildenv if isinstance(self.generators, str): self.generators = [self.generators] # User defined options @@ -300,6 +314,7 @@ def package_info(self): def run(self, command, output=True, cwd=None, win_bash=False, subsystem=None, msys_mingw=True, ignore_errors=False, run_environment=False, with_login=True): + def _run(): if not win_bash: return self._conan_runner(command, output, os.path.abspath(RUN_LOG_NAME), cwd) diff --git a/conans/model/profile.py b/conans/model/profile.py index 2d6ecd8c2dd..30b538b09c3 100644 --- a/conans/model/profile.py +++ b/conans/model/profile.py @@ -1,6 +1,7 @@ import copy from collections import OrderedDict, defaultdict +from conan.tools.env.environment import ProfileEnvironment from conans.client import settings_preprocessor from conans.errors import ConanException from conans.model.conf import ConfDefinition @@ -22,6 +23,7 @@ def __init__(self): self.options = OptionsValues() self.build_requires = OrderedDict() # ref pattern: list of ref self.conf = ConfDefinition() + self.buildenv = ProfileEnvironment() # Cached processed values self.processed_settings = None # Settings with values, and smart completion @@ -114,6 +116,7 @@ def compose(self, other): self.build_requires[pattern] = list(existing.values()) self.conf.update_conf_definition(other.conf) + self.buildenv.compose(other.buildenv) def update_settings(self, new_settings): """Mix the specified settings with the current profile. diff --git a/conans/test/integration/environment/test_buildenv_profile.py b/conans/test/integration/environment/test_buildenv_profile.py new file mode 100644 index 00000000000..4b10ead5d84 --- /dev/null +++ b/conans/test/integration/environment/test_buildenv_profile.py @@ -0,0 +1,53 @@ +import textwrap + +import pytest + +from conans.test.utils.tools import TestClient + + +@pytest.fixture +def client(): + conanfile = textwrap.dedent(""" + from conans import ConanFile + class Pkg(ConanFile): + def generate(self): + for var in (1, 2): + v = self.buildenv.value("MyVar{}".format(var)) + self.output.info("MyVar{}={}!!".format(var, v)) + """) + profile1 = textwrap.dedent(""" + [buildenv] + MyVar1=MyValue1_1 + MyVar2=MyValue2_1 + """) + client = TestClient() + client.save({"conanfile.py": conanfile, + "profile1": profile1}) + return client + + +def test_buildenv_profile_cli(client): + profile2 = textwrap.dedent(""" + [buildenv] + MyVar1=MyValue1_2 + MyVar2+=MyValue2_2 + """) + client.save({"profile2": profile2}) + + client.run("install . -pr=profile1 -pr=profile2") + assert "conanfile.py: MyVar1=MyValue1_2!!" in client.out + assert "conanfile.py: MyVar2=MyValue2_1 MyValue2_2" in client.out + + +def test_buildenv_profile_include(client): + profile2 = textwrap.dedent(""" + include(profile1) + [buildenv] + MyVar1=MyValue1_2 + MyVar2+=MyValue2_2 + """) + client.save({"profile2": profile2}) + + client.run("install . -pr=profile2") + assert "conanfile.py: MyVar1=MyValue1_2!!" in client.out + assert "conanfile.py: MyVar2=MyValue2_1 MyValue2_2" in client.out diff --git a/conans/test/integration/environment/test_env.py b/conans/test/integration/environment/test_env.py new file mode 100644 index 00000000000..98a860381c5 --- /dev/null +++ b/conans/test/integration/environment/test_env.py @@ -0,0 +1,60 @@ +import os +import platform +import textwrap + + +from conan.tools.env.environment import environment_wrap_command +from conans.test.utils.tools import TestClient + + +def test_profile_buildenv(): + client = TestClient() + conanfile = textwrap.dedent("""\ + import os, platform + from conans import ConanFile + class Pkg(ConanFile): + def generate(self): + if platform.system() == "Windows": + self.buildenv.save_bat("pkgenv.bat") + else: + self.buildenv.save_sh("pkgenv.sh") + os.chmod("pkgenv.sh", 0o777) + + """) + # Some scripts in a random system folders, path adding to the profile [env] + + compiler_bat = "@echo off\necho MYCOMPILER!!\necho MYPATH=%PATH%" + compiler_sh = "echo MYCOMPILER!!\necho MYPATH=$PATH" + compiler2_bat = "@echo off\necho MYCOMPILER2!!\necho MYPATH2=%PATH%" + compiler2_sh = "echo MYCOMPILER2!!\necho MYPATH2=$PATH" + + myprofile = textwrap.dedent(""" + [buildenv] + PATH+=(path){} + mypkg*:PATH=! + mypkg*:PATH+=(path){} + """.format(os.path.join(client.current_folder, "compiler"), + os.path.join(client.current_folder, "compiler2"))) + client.save({"conanfile.py": conanfile, + "myprofile": myprofile, + "compiler/mycompiler.bat": compiler_bat, + "compiler/mycompiler.sh": compiler_sh, + "compiler2/mycompiler.bat": compiler2_bat, + "compiler2/mycompiler.sh": compiler2_sh}) + + os.chmod(os.path.join(client.current_folder, "compiler", "mycompiler.sh"), 0o777) + os.chmod(os.path.join(client.current_folder, "compiler2", "mycompiler.sh"), 0o777) + + client.run("install . -pr=myprofile") + # Run the BUILD environment + ext = "bat" if platform.system() == "Windows" else "sh" # TODO: Decide on logic .bat vs .sh + cmd = environment_wrap_command("pkgenv", "mycompiler.{}".format(ext), cwd=client.current_folder) + client.run_command(cmd) + assert "MYCOMPILER!!" in client.out + assert "MYPATH=" in client.out + + # Now with pkg-specific env-var + client.run("install . mypkg/1.0@ -pr=myprofile") + client.run_command(cmd) + assert "MYCOMPILER2!!" in client.out + assert "MYPATH2=" in client.out diff --git a/conans/test/integration/generators/xcode_gcc_vs_test.py b/conans/test/integration/generators/xcode_gcc_vs_test.py index 5c98c5de4da..29c6026a1f4 100644 --- a/conans/test/integration/generators/xcode_gcc_vs_test.py +++ b/conans/test/integration/generators/xcode_gcc_vs_test.py @@ -8,7 +8,6 @@ from conans.paths import (BUILD_INFO, BUILD_INFO_CMAKE, BUILD_INFO_GCC, BUILD_INFO_VISUAL_STUDIO, BUILD_INFO_XCODE, CONANFILE_TXT, CONANINFO) from conans.test.utils.tools import TestClient -from conans.util.files import load class VSXCodeGeneratorsTest(unittest.TestCase): diff --git a/conans/test/unittests/client/graph/deps_graph_test.py b/conans/test/unittests/client/graph/deps_graph_test.py index 79a8eddde1e..af680c02d82 100644 --- a/conans/test/unittests/client/graph/deps_graph_test.py +++ b/conans/test/unittests/client/graph/deps_graph_test.py @@ -1,4 +1,5 @@ import unittest +from mock import Mock from mock import Mock @@ -31,9 +32,9 @@ def test_basic_levels(self): ref3 = ConanFileReference.loads("Hello/3.0@user/stable") deps = DepsGraph() - n1 = Node(ref1, 1, context=CONTEXT_HOST) - n2 = Node(ref2, 2, context=CONTEXT_HOST) - n3 = Node(ref3, 3, context=CONTEXT_HOST) + n1 = Node(ref1, Mock(), context=CONTEXT_HOST) + n2 = Node(ref2, Mock(), context=CONTEXT_HOST) + n3 = Node(ref3, Mock(), context=CONTEXT_HOST) deps.add_node(n1) deps.add_node(n2) deps.add_node(n3) @@ -48,10 +49,10 @@ def test_multi_levels(self): ref32 = ConanFileReference.loads("Hello/32.0@user/stable") deps = DepsGraph() - n1 = Node(ref1, 1, context=CONTEXT_HOST) - n2 = Node(ref2, 2, context=CONTEXT_HOST) - n31 = Node(ref31, 31, context=CONTEXT_HOST) - n32 = Node(ref32, 32, context=CONTEXT_HOST) + n1 = Node(ref1, Mock(), context=CONTEXT_HOST) + n2 = Node(ref2, Mock(), context=CONTEXT_HOST) + n31 = Node(ref31, Mock(), context=CONTEXT_HOST) + n32 = Node(ref32, Mock(), context=CONTEXT_HOST) deps.add_node(n1) deps.add_node(n2) deps.add_node(n32) @@ -70,11 +71,11 @@ def test_multi_levels_2(self): ref32 = ConanFileReference.loads("Hello/32.0@user/stable") deps = DepsGraph() - n1 = Node(ref1, 1, context=CONTEXT_HOST) - n2 = Node(ref2, 2, context=CONTEXT_HOST) - n5 = Node(ref5, 5, context=CONTEXT_HOST) - n31 = Node(ref31, 31, context=CONTEXT_HOST) - n32 = Node(ref32, 32, context=CONTEXT_HOST) + n1 = Node(ref1, Mock(), context=CONTEXT_HOST) + n2 = Node(ref2, Mock(), context=CONTEXT_HOST) + n5 = Node(ref5, Mock(), context=CONTEXT_HOST) + n31 = Node(ref31, Mock(), context=CONTEXT_HOST) + n32 = Node(ref32, Mock(), context=CONTEXT_HOST) deps.add_node(n1) deps.add_node(n5) deps.add_node(n2) @@ -95,11 +96,11 @@ def test_multi_levels_3(self): ref32 = ConanFileReference.loads("Hello/32.0@user/stable") deps = DepsGraph() - n1 = Node(ref1, 1, context=CONTEXT_HOST) - n2 = Node(ref2, 2, context=CONTEXT_HOST) - n5 = Node(ref5, 5, context=CONTEXT_HOST) - n31 = Node(ref31, 31, context=CONTEXT_HOST) - n32 = Node(ref32, 32, context=CONTEXT_HOST) + n1 = Node(ref1, Mock(), context=CONTEXT_HOST) + n2 = Node(ref2, Mock(), context=CONTEXT_HOST) + n5 = Node(ref5, Mock(), context=CONTEXT_HOST) + n31 = Node(ref31, Mock(), context=CONTEXT_HOST) + n32 = Node(ref32, Mock(), context=CONTEXT_HOST) deps.add_node(n1) deps.add_node(n5) deps.add_node(n2) diff --git a/conans/test/unittests/tools/env/__init__.py b/conans/test/unittests/tools/env/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/conans/test/unittests/tools/env/test_env.py b/conans/test/unittests/tools/env/test_env.py new file mode 100644 index 00000000000..81bf6f3856b --- /dev/null +++ b/conans/test/unittests/tools/env/test_env.py @@ -0,0 +1,284 @@ +import os +import platform +import subprocess +import textwrap + +import pytest + +from conan.tools.env import Environment +from conan.tools.env.environment import ProfileEnvironment +from conans.client.tools import chdir +from conans.test.utils.test_files import temp_folder +from conans.util.files import save + + +def test_compose(): + env = Environment() + env.define("MyVar", "MyValue") + env.define("MyVar2", "MyValue2") + env.define("MyVar3", "MyValue3") + env.define("MyVar4", "MyValue4") + env.unset("MyVar5") + + env2 = Environment() + env2.define("MyVar", "MyNewValue") + env2.append("MyVar2", "MyNewValue2") + env2.prepend("MyVar3", "MyNewValue3") + env2.unset("MyVar4") + env2.define("MyVar5", "MyNewValue5") + + env.compose(env2) + assert env.value("MyVar") == "MyNewValue" + assert env.value("MyVar2") == 'MyValue2 MyNewValue2' + assert env.value("MyVar3") == 'MyNewValue3 MyValue3' + assert env.value("MyVar4") == "" + assert env.value("MyVar5") == 'MyNewValue5' + + +@pytest.mark.parametrize("op1, v1, s1, op2, v2, s2, result", + [("define", "Val1", " ", "define", "Val2", " ", "Val2"), + ("define", "Val1", " ", "append", "Val2", " ", "Val1 Val2"), + ("define", "Val1", " ", "prepend", "Val2", " ", "Val2 Val1"), + ("define", "Val1", " ", "unset", "", " ", ""), + ("append", "Val1", " ", "define", "Val2", " ", "Val2"), + ("append", "Val1", " ", "append", "Val2", " ", "MyVar Val1 Val2"), + ("append", "Val1", " ", "prepend", "Val2", " ", "Val2 MyVar Val1"), + ("append", "Val1", " ", "unset", "", " ", ""), + ("prepend", "Val1", " ", "define", "Val2", " ", "Val2"), + ("prepend", "Val1", " ", "append", "Val2", " ", "Val1 MyVar Val2"), + ("prepend", "Val1", " ", "prepend", "Val2", " ", "Val2 Val1 MyVar"), + ("prepend", "Val1", " ", "unset", "", " ", ""), + ("unset", "", " ", "define", "Val2", " ", "Val2"), + ("unset", "", " ", "append", "Val2", " ", "Val2"), + ("unset", "", " ", "prepend", "Val2", " ", "Val2"), + ("unset", "", " ", "unset", "", " ", ""), + # different separators + ("append", "Val1", "+", "append", "Val2", "-", "MyVar+Val1-Val2"), + ("append", "Val1", "+", "prepend", "Val2", "-", "Val2-MyVar+Val1"), + ("unset", "", " ", "append", "Val2", "+", "Val2"), + ("unset", "", " ", "prepend", "Val2", "+", "Val2"), + ]) +def test_compose_combinations(op1, v1, s1, op2, v2, s2, result): + env = Environment() + if op1 != "unset": + getattr(env, op1)("MyVar", v1, s1) + else: + env.unset("MyVar") + env2 = Environment() + if op2 != "unset": + getattr(env2, op2)("MyVar", v2, s2) + else: + env2.unset("MyVar") + env.compose(env2) + assert env.value("MyVar") == result + + +@pytest.mark.parametrize("op1, v1, op2, v2, result", + [("define", "/path1", "define", "/path2", "/path2"), + ("define", "/path1", "append", "/path2", "/path1:/path2"), + ("define", "/path1", "prepend", "/path2", "/path2:/path1"), + ("define", "/path1", "unset", "", ""), + ("append", "/path1", "define", "/path2", "/path2"), + ("append", "/path1", "append", "/path2", "MyVar:/path1:/path2"), + ("append", "/path1", "prepend", "/path2", "/path2:MyVar:/path1"), + ("append", "/path1", "unset", "", ""), + ("prepend", "/path1", "define", "/path2", "/path2"), + ("prepend", "/path1", "append", "/path2", "/path1:MyVar:/path2"), + ("prepend", "/path1", "prepend", "/path2", "/path2:/path1:MyVar"), + ("prepend", "/path1", "unset", "", ""), + ("unset", "", "define", "/path2", "/path2"), + ("unset", "", "append", "/path2", "/path2"), + ("unset", "", "prepend", "/path2", "/path2"), + ("unset", "", "unset", "", ""), + ]) +def test_compose_path_combinations(op1, v1, op2, v2, result): + env = Environment() + if op1 != "unset": + getattr(env, op1+"_path")("MyVar", v1) + else: + env.unset("MyVar") + env2 = Environment() + if op2 != "unset": + getattr(env2, op2+"_path")("MyVar", v2) + else: + env2.unset("MyVar") + env.compose(env2) + assert env.value("MyVar", pathsep=":") == result + + +def test_profile(): + myprofile = textwrap.dedent(""" + # define + MyVar1=MyValue1 + # append + MyVar2+=MyValue2 + # multiple append works + MyVar2+=MyValue2_2 + # prepend + MyVar3=+MyValue3 + # unset + MyVar4=! + # Empty + MyVar5= + + # PATHS + # define + MyPath1=(path)/my/path1 + # append + MyPath2+=(path)/my/path2 + # multiple append works + MyPath2+=(path)/my/path2_2 + # prepend + MyPath3=+(path)/my/path3 + # unset + MyPath4=! + + # PER-PACKAGE + mypkg*:MyVar2=MyValue2 + """) + + profile_env = ProfileEnvironment.loads(myprofile) + env = profile_env.get_env("") + assert env.value("MyVar1") == "MyValue1" + assert env.value("MyVar2", "$MyVar2") == '$MyVar2 MyValue2 MyValue2_2' + assert env.value("MyVar3", "$MyVar3") == 'MyValue3 $MyVar3' + assert env.value("MyVar4") == "" + assert env.value("MyVar5") == '' + + env = profile_env.get_env("mypkg1/1.0") + assert env.value("MyVar1") == "MyValue1" + assert env.value("MyVar2", "$MyVar2") == 'MyValue2' + + +def test_env_files(): + env = Environment() + env.define("MyVar", "MyValue") + env.define("MyVar1", "MyValue1") + env.append("MyVar2", "MyValue2") + env.prepend("MyVar3", "MyValue3") + env.unset("MyVar4") + env.define("MyVar5", "MyValue5 With Space5=More Space5;:More") + env.append("MyVar6", "MyValue6") # Append, but previous not existing + env.define_path("MyPath1", "/Some/Path1/") + env.append_path("MyPath2", ["/Some/Path2/", "/Other/Path2/"]) + env.prepend_path("MyPath3", "/Some/Path3/") + env.unset("MyPath4") + folder = temp_folder() + + prevenv = {"MyVar1": "OldVar1", + "MyVar2": "OldVar2", + "MyVar3": "OldVar3", + "MyVar4": "OldVar4", + "MyPath1": "OldPath1", + "MyPath2": "OldPath2", + "MyPath3": "OldPath3", + "MyPath4": "OldPath4", + } + + display_bat = textwrap.dedent("""\ + @echo off + echo MyVar=%MyVar%!! + echo MyVar1=%MyVar1%!! + echo MyVar2=%MyVar2%!! + echo MyVar3=%MyVar3%!! + echo MyVar4=%MyVar4%!! + echo MyVar5=%MyVar5%!! + echo MyVar6=%MyVar6%!! + echo MyPath1=%MyPath1%!! + echo MyPath2=%MyPath2%!! + echo MyPath3=%MyPath3%!! + echo MyPath4=%MyPath4%!! + """) + + display_sh = textwrap.dedent("""\ + echo MyVar=$MyVar!! + echo MyVar1=$MyVar1!! + echo MyVar2=$MyVar2!! + echo MyVar3=$MyVar3!! + echo MyVar4=$MyVar4!! + echo MyVar5=$MyVar5!! + echo MyVar6=$MyVar6!! + echo MyPath1=$MyPath1!! + echo MyPath2=$MyPath2!! + echo MyPath3=$MyPath3!! + echo MyPath4=$MyPath4!! + """) + + def check(cmd_): + out, _ = subprocess.Popen(cmd_, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + env=prevenv, shell=True).communicate() + out = out.decode() + assert "MyVar=MyValue!!" in out + assert "MyVar1=MyValue1!!" in out + assert "MyVar2=OldVar2 MyValue2!!" in out + assert "MyVar3=MyValue3 OldVar3!!" in out + assert "MyVar4=!!" in out + assert "MyVar5=MyValue5 With Space5=More Space5;:More!!" in out + assert "MyVar6= MyValue6!!" in out # The previous is non existing, append has space + assert "MyPath1=/Some/Path1/!!" in out + assert "MyPath2=OldPath2:/Some/Path2/:/Other/Path2/!!" in out + assert "MyPath3=/Some/Path3/:OldPath3!!" in out + assert "MyPath4=!!" in out + + # This should be output when deactivated + assert "MyVar=!!" in out + assert "MyVar1=OldVar1!!" in out + assert "MyVar2=OldVar2!!" in out + assert "MyVar3=OldVar3!!" in out + assert "MyVar4=OldVar4!!" in out + assert "MyVar5=!!" in out + assert "MyVar6=!!" in out + assert "MyPath1=OldPath1!!" in out + assert "MyPath2=OldPath2!!" in out + assert "MyPath3=OldPath3!!" in out + assert "MyPath4=OldPath4!!" in out + + with chdir(folder): + if platform.system() == "Windows": + env.save_bat("test.bat", pathsep=":", generate_deactivate=True) + save("display.bat", display_bat) + cmd = "test.bat && display.bat && deactivate_test.bat && display.bat" + check(cmd) + # FIXME: Powershell still not working + # env.save_ps1("test.ps1", pathsep=":") + # cmd = 'powershell.exe -ExecutionPolicy ./test.ps1; gci env:' + # shell = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + # (stdout, stderr) = shell.communicate() + # stdout, stderr = decode_text(stdout), decode_text(stderr) + # check(cmd) + else: + env.save_sh("test.sh", generate_deactivate=True) + save("display.sh", display_sh) + os.chmod("display.sh", 0o777) + cmd = '. ./test.sh && ./display.sh && . ./deactivate_test.sh && ./display.sh' + check(cmd) + + +@pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") +def test_windows_case_insensitive(): + # Append and define operation over the same variable in Windows preserve order + env = Environment() + env.define("MyVar", "MyValueA") + env.define("MYVAR", "MyValueB") + env.define("MyVar1", "MyValue1A") + env.append("MYVAR1", "MyValue1B") + folder = temp_folder() + + display_bat = textwrap.dedent("""\ + @echo off + echo MyVar=%MyVar%!! + echo MyVar1=%MyVar1%!! + """) + + with chdir(folder): + env.save_bat("test.bat", generate_deactivate=True) + save("display.bat", display_bat) + cmd = "test.bat && display.bat && deactivate_test.bat && display.bat" + out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + shell=True).communicate() + + out = out.decode() + assert "MyVar=MyValueB!!" in out + assert "MyVar=!!" in out + assert "MyVar1=MyValue1A MyValue1B!!" in out + assert "MyVar1=!!" in out