From f5660eb64a3ebdc405dba4f00d4c96d2df2cf598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Wed, 15 Oct 2025 15:13:36 +0200 Subject: [PATCH 01/27] Second attemp for virtual deactivate functions --------- Co-authored-by: PerseoGI --- conan/internal/api/install/generators.py | 13 +++++++--- conan/tools/env/environment.py | 31 ++++++++++++------------ 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/conan/internal/api/install/generators.py b/conan/internal/api/install/generators.py index 0ee676a8133..0f854f1a6d8 100644 --- a/conan/internal/api/install/generators.py +++ b/conan/internal/api/install/generators.py @@ -202,12 +202,19 @@ def deactivates(filenames): ps1s.append("$PSScriptRoot/"+path) if shs: def sh_content(files): - return ". " + " && . ".join('"{}"'.format(s) for s in files) + content = ". " + " && . ".join('"{}"'.format(s) for s in files) + "\n" + content += f"deactivate_conan{group}() {{\n" + for s in reversed(files): + deactivate_name = os.path.splitext(os.path.basename(s))[0].replace("-", "_") + content += f" deactivate_{deactivate_name} \n" + content += "}\n" + return content filename = "conan{}.sh".format(group) + generated.append(filename) save(os.path.join(conanfile.generators_folder, filename), sh_content(shs)) - save(os.path.join(conanfile.generators_folder, "deactivate_{}".format(filename)), - sh_content(deactivates(shs))) + # save(os.path.join(conanfile.generators_folder, "deactivate_{}".format(filename)), + # sh_content(deactivates(shs))) if bats: def bat_content(files): return "\r\n".join(["@echo off"] + ['call "{}"'.format(b) for b in files]) diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index 09d69f6e675..8c467cdad65 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -500,21 +500,21 @@ def save_ps1(self, file_location, generate_deactivate=True,): def save_sh(self, file_location, generate_deactivate=True): filepath, filename = os.path.split(file_location) - deactivate_file = os.path.join("$script_folder", "deactivate_{}".format(filename)) + deactivate_name = os.path.splitext(os.path.basename(filename))[0].replace("-", "_") + # The variable handling can be improved, now it is very basic deactivate = textwrap.dedent("""\ - echo "echo Restoring environment" > "{deactivate_file}" - for v in {vars} - do - is_defined="true" - value=$(printenv $v) || is_defined="" || true - if [ -n "$value" ] || [ -n "$is_defined" ] - then - echo export "$v='$value'" >> "{deactivate_file}" - else - echo unset $v >> "{deactivate_file}" - fi - done - """.format(deactivate_file=deactivate_file, vars=" ".join(self._values.keys()))) + deactivate_{deactivate_name} () {{ + echo "Restoring environment" + for v in {vars} + do + OLD_VARIABLE="_CONAN_OLD_${{v}}" + export $(eval echo "$v")="$(printenv $(eval echo "${{OLD_VARIABLE}}"))" + unset "${{OLD_VARIABLE}}" + done + unset -f deactivate_{deactivate_name} + }} + """.format(vars=" ".join(self._values.keys()), filename=filename, + deactivate_name=deactivate_name)) capture = textwrap.dedent("""\ {deactivate} """).format(deactivate=deactivate if generate_deactivate else "") @@ -524,8 +524,9 @@ def save_sh(self, file_location, generate_deactivate=True): value = varvalues.get_str("${name}", self._subsystem, pathsep=self._pathsep, root_path=abs_base_path, script_path=new_path) value = value.replace('"', '\\"') + result.append(f'export _CONAN_OLD_{varname}="$(printenv {varname})"') if value: - result.append('export {}="{}"'.format(varname, value)) + result.append(f'export {varname}="{value}"') else: result.append('unset {}'.format(varname)) From 413d40775ce603f0c8a763c947087e8be87902e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Wed, 15 Oct 2025 15:33:59 +0200 Subject: [PATCH 02/27] Fix test, cleanup --- conan/internal/api/install/generators.py | 9 +++++++-- test/unittests/tools/env/test_env_files.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/conan/internal/api/install/generators.py b/conan/internal/api/install/generators.py index 0f854f1a6d8..13453ae8ef4 100644 --- a/conan/internal/api/install/generators.py +++ b/conan/internal/api/install/generators.py @@ -182,6 +182,11 @@ def deactivates(filenames): result.append(os.path.join(folder, "deactivate_{}".format(f))) return result + # To replace the function above once everything works with the new approach + def deactivate_names(filenames): + return [os.path.splitext(os.path.basename(s))[0].replace("-", "_") + for s in reversed(filenames)] + generated = [] for group, env_scripts in conanfile.env_scripts.items(): subsystem = deduce_subsystem(conanfile, group) @@ -204,8 +209,7 @@ def deactivates(filenames): def sh_content(files): content = ". " + " && . ".join('"{}"'.format(s) for s in files) + "\n" content += f"deactivate_conan{group}() {{\n" - for s in reversed(files): - deactivate_name = os.path.splitext(os.path.basename(s))[0].replace("-", "_") + for deactivate_name in deactivate_names(shs): content += f" deactivate_{deactivate_name} \n" content += "}\n" return content @@ -213,6 +217,7 @@ def sh_content(files): generated.append(filename) save(os.path.join(conanfile.generators_folder, filename), sh_content(shs)) + # TODO: Flag to keep these? The code gets a bit harder to maintain # save(os.path.join(conanfile.generators_folder, "deactivate_{}".format(filename)), # sh_content(deactivates(shs))) if bats: diff --git a/test/unittests/tools/env/test_env_files.py b/test/unittests/tools/env/test_env_files.py index 852ae42317a..8e2f1eb9347 100644 --- a/test/unittests/tools/env/test_env_files.py +++ b/test/unittests/tools/env/test_env_files.py @@ -151,7 +151,7 @@ def test_env_files_sh(env, prevenv): save("display.sh", display) os.chmod("display.sh", 0o777) # We include the "set -e" to test it is robust against errors - cmd = 'set -e && . ./test.sh && ./display.sh && . ./deactivate_test.sh && ./display.sh' + cmd = 'set -e && . ./test.sh && ./display.sh && deactivate_test && ./display.sh' check_env_files_output(cmd, prevenv) From fe0b2c84b4ec204279506269b832417fb965efe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Wed, 15 Oct 2025 15:52:49 +0200 Subject: [PATCH 03/27] Fix indent --- conan/internal/api/install/generators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conan/internal/api/install/generators.py b/conan/internal/api/install/generators.py index 13453ae8ef4..f87737b7623 100644 --- a/conan/internal/api/install/generators.py +++ b/conan/internal/api/install/generators.py @@ -211,7 +211,7 @@ def sh_content(files): content += f"deactivate_conan{group}() {{\n" for deactivate_name in deactivate_names(shs): content += f" deactivate_{deactivate_name} \n" - content += "}\n" + content += "}\n" return content filename = "conan{}.sh".format(group) From b7af331e5b66155188d1293d98c7a17808ac6f31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Wed, 15 Oct 2025 15:55:14 +0200 Subject: [PATCH 04/27] Fix tests, remove those having to deal with deactivate locations (new tests will be added later) --- .../editable/test_editable_layout.py | 2 - test/integration/environment/test_env.py | 43 ++----------------- 2 files changed, 3 insertions(+), 42 deletions(-) diff --git a/test/integration/editable/test_editable_layout.py b/test/integration/editable/test_editable_layout.py index 1f9beefd260..dc474647951 100644 --- a/test/integration/editable/test_editable_layout.py +++ b/test/integration/editable/test_editable_layout.py @@ -101,8 +101,6 @@ def test_editable_matching_folder_names(): data = client.load("hello/say-release-data.cmake") hello_say_folder = os.path.join(client.current_folder, "hello-say").replace("\\", "/") assert f'set(say_PACKAGE_FOLDER_RELEASE "{hello_say_folder}")' in data - env = client.load("hello/conanbuildenv-release.sh") - assert "$script_folder/deactivate_conanbuildenv-release.sh" in env.replace("\\", "/") def test_install_editable_build_folder_vars(): diff --git a/test/integration/environment/test_env.py b/test/integration/environment/test_env.py index 92539cacb87..b04560b7bb2 100644 --- a/test/integration/environment/test_env.py +++ b/test/integration/environment/test_env.py @@ -519,7 +519,7 @@ def generate(self): if platform.system() == "Windows": cmd = "conanbuild.bat && display.bat && deactivate_conanbuild.bat && display.bat" else: - cmd = '. ./conanbuild.sh && ./display.sh && . ./deactivate_conanbuild.sh && ./display.sh' + cmd = '. ./conanbuild.sh && ./display.sh && deactivate_conanbuild && ./display.sh' out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() out = out.decode() @@ -564,7 +564,7 @@ def generate(self): if platform.system() == "Windows": cmd = "conanbuild.bat && display.bat && deactivate_conanbuild.bat && display.bat" else: - cmd = '. ./conanbuild.sh && ./display.sh && . ./deactivate_conanbuild.sh && ./display.sh' + cmd = '. ./conanbuild.sh && ./display.sh && deactivate_conanbuild && ./display.sh' out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() out = out.decode() @@ -664,7 +664,7 @@ def test_profile_build_env_spaces(): if platform.system() == "Windows": cmd = "conanbuild.bat && display.bat && deactivate_conanbuild.bat && display.bat" else: - cmd = '. ./conanbuild.sh && ./display.sh && . ./deactivate_conanbuild.sh && ./display.sh' + cmd = '. ./conanbuild.sh && ./display.sh && deactivate_conanbuild && ./display.sh' out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() out = out.decode() @@ -673,32 +673,6 @@ def test_profile_build_env_spaces(): assert "VAR1=!!" in out -def test_deactivate_location(): - conanfile = textwrap.dedent(r""" - from conan import ConanFile - from conan.tools.env import Environment - class Pkg(ConanFile): - def package_info(self): - self.buildenv_info.define("FOO", "BAR") - """) - client = TestClient() - client.save({"pkg.py": conanfile}) - client.run("create pkg.py --name pkg --version 1.0") - client.run("install --requires pkg/1.0@ -g VirtualBuildEnv -of=myfolder -s build_type=Release -s arch=x86_64") - - source_cmd, script_ext = ("myfolder\\", ".bat") if platform.system() == "Windows" else (". ./myfolder/", ".sh") - cmd = "{}conanbuild{}".format(source_cmd, script_ext) - - subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, - cwd=client.current_folder).communicate() - - assert not os.path.exists(os.path.join(client.current_folder, - "deactivate_conanbuildenv-release-x86_64{}".format(script_ext))) - - assert os.path.exists(os.path.join(client.current_folder, "myfolder", - "deactivate_conanbuildenv-release-x86_64{}".format(script_ext))) - - @pytest.mark.skipif(platform.system() == "Windows", reason="Requires sh") def test_skip_virtualbuildenv_run(): # Build require @@ -843,17 +817,6 @@ def test(self): assert "MYLIBVAR=MYLIBVALUE:Release" in c.out -def test_deactivate_relocatable_substitute(): - c = TestClient() - # this cannot be tested in CI, because permissions over root folder - # c.current_folder = "/build" - c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) - c.run("install . -s os=Linux -s:b os=Linux") - conanbuild = c.load("conanbuildenv.sh") - result = os.path.join("$script_folder", "deactivate_conanbuildenv.sh") - assert f'"{result}"' in conanbuild - - class TestDotEnv: def test_dotenv(self): c = TestClient() From 3039ef29de504021113ed23e4d87464d3a953b04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Wed, 15 Oct 2025 16:11:10 +0200 Subject: [PATCH 05/27] Add conf toggle --- conan/internal/api/install/generators.py | 27 ++++++----- conan/internal/model/conf.py | 1 + conan/tools/env/environment.py | 57 +++++++++++++++++------- 3 files changed, 58 insertions(+), 27 deletions(-) diff --git a/conan/internal/api/install/generators.py b/conan/internal/api/install/generators.py index f87737b7623..3b0f0ce0e37 100644 --- a/conan/internal/api/install/generators.py +++ b/conan/internal/api/install/generators.py @@ -182,11 +182,12 @@ def deactivates(filenames): result.append(os.path.join(folder, "deactivate_{}".format(f))) return result - # To replace the function above once everything works with the new approach - def deactivate_names(filenames): + def deactivate_function_names(filenames): return [os.path.splitext(os.path.basename(s))[0].replace("-", "_") for s in reversed(filenames)] + use_deactivate_function = conanfile.conf.get("tools.env:deactivate_function", default=False, + check_type=bool) generated = [] for group, env_scripts in conanfile.env_scripts.items(): subsystem = deduce_subsystem(conanfile, group) @@ -207,19 +208,23 @@ def deactivate_names(filenames): ps1s.append("$PSScriptRoot/"+path) if shs: def sh_content(files): - content = ". " + " && . ".join('"{}"'.format(s) for s in files) + "\n" - content += f"deactivate_conan{group}() {{\n" - for deactivate_name in deactivate_names(shs): - content += f" deactivate_{deactivate_name} \n" - content += "}\n" - return content + aggregated_calls = ". " + " && . ".join('"{}"'.format(s) for s in files) + if use_deactivate_function: + content = aggregated_calls + "\n" + content += f"deactivate_conan{group}() {{\n" + for deactivate_name in deactivate_function_names(shs): + content += f" deactivate_{deactivate_name}\n" + content += "}\n" + return content + else: + return aggregated_calls filename = "conan{}.sh".format(group) generated.append(filename) save(os.path.join(conanfile.generators_folder, filename), sh_content(shs)) - # TODO: Flag to keep these? The code gets a bit harder to maintain - # save(os.path.join(conanfile.generators_folder, "deactivate_{}".format(filename)), - # sh_content(deactivates(shs))) + if not use_deactivate_function: + save(os.path.join(conanfile.generators_folder, "deactivate_{}".format(filename)), + sh_content(deactivates(shs))) if bats: def bat_content(files): return "\r\n".join(["@echo off"] + ['call "{}"'.format(b) for b in files]) diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py index 98c58bb5e35..38ebc93647c 100644 --- a/conan/internal/model/conf.py +++ b/conan/internal/model/conf.py @@ -135,6 +135,7 @@ "tools.apple:enable_visibility": "(boolean) Enable/Disable Visibility Apple Clang flags", "tools.env.virtualenv:powershell": "If specified, it generates PowerShell launchers (.ps1). Use this configuration setting the PowerShell executable you want to use (e.g., 'powershell.exe' or 'pwsh'). Setting it to True or False is deprecated as of Conan 2.11.0.", "tools.env:dotenv": "(Experimental) Generate dotenv environment files", + "tools.env:deactivate_function": "(Experimental) Generate a deactivate function instead of a script to unset the environment variables", # Compilers/Flags configurations "tools.build:compiler_executables": "Defines a Python dict-like with the compilers path to be used. Allowed keys {'c', 'cpp', 'cuda', 'objc', 'objcxx', 'rc', 'fortran', 'asm', 'hip', 'ispc'}", "tools.build:cxxflags": "List of extra CXX flags used by different toolchains like CMakeToolchain, AutotoolsToolchain and MesonToolchain", diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index 8c467cdad65..19a35a5a264 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -345,6 +345,8 @@ def __init__(self, conanfile, values, scope): self._conanfile = conanfile self._scope = scope self._subsystem = deduce_subsystem(conanfile, scope) + self._use_deactivate_function = conanfile.conf.get("tools.env:deactivate_function", + default=False, check_type=bool) @property def _pathsep(self): @@ -500,21 +502,7 @@ def save_ps1(self, file_location, generate_deactivate=True,): def save_sh(self, file_location, generate_deactivate=True): filepath, filename = os.path.split(file_location) - deactivate_name = os.path.splitext(os.path.basename(filename))[0].replace("-", "_") - # The variable handling can be improved, now it is very basic - deactivate = textwrap.dedent("""\ - deactivate_{deactivate_name} () {{ - echo "Restoring environment" - for v in {vars} - do - OLD_VARIABLE="_CONAN_OLD_${{v}}" - export $(eval echo "$v")="$(printenv $(eval echo "${{OLD_VARIABLE}}"))" - unset "${{OLD_VARIABLE}}" - done - unset -f deactivate_{deactivate_name} - }} - """.format(vars=" ".join(self._values.keys()), filename=filename, - deactivate_name=deactivate_name)) + deactivate = _sh_deactivate_contents(self._use_deactivate_function, self._values, filename) capture = textwrap.dedent("""\ {deactivate} """).format(deactivate=deactivate if generate_deactivate else "") @@ -524,7 +512,8 @@ def save_sh(self, file_location, generate_deactivate=True): value = varvalues.get_str("${name}", self._subsystem, pathsep=self._pathsep, root_path=abs_base_path, script_path=new_path) value = value.replace('"', '\\"') - result.append(f'export _CONAN_OLD_{varname}="$(printenv {varname})"') + if self._use_deactivate_function: + result.append(f'export _CONAN_OLD_{varname}="$(printenv {varname})"') if value: result.append(f'export {varname}="{value}"') else: @@ -593,6 +582,42 @@ def save_script(self, filename): register_env_script(self._conanfile, path, self._scope) +def _sh_deactivate_contents(use_deactivate_function, values, filename): + if use_deactivate_function: + # The variable handling can be improved, now it is very basic + deactivate_name = os.path.splitext(os.path.basename(filename))[0].replace("-", "_") + deactivate = textwrap.dedent("""\ + deactivate_{deactivate_name} () {{ + echo "Restoring environment" + for v in {vars} + do + OLD_VARIABLE="_CONAN_OLD_${{v}}" + export $(eval echo "$v")="$(printenv $(eval echo "${{OLD_VARIABLE}}"))" + unset "${{OLD_VARIABLE}}" + done + unset -f deactivate_{deactivate_name} + }} + """.format(vars=" ".join(values.keys()), filename=filename, + deactivate_name=deactivate_name)) + else: + deactivate_file = os.path.join("$script_folder", "deactivate_{}".format(filename)) + deactivate = textwrap.dedent("""\ + echo "echo Restoring environment" > "{deactivate_file}" + for v in {vars} + do + is_defined="true" + value=$(printenv $v) || is_defined="" || true + if [ -n "$value" ] || [ -n "$is_defined" ] + then + echo export "$v='$value'" >> "{deactivate_file}" + else + echo unset $v >> "{deactivate_file}" + fi + done + """.format(deactivate_file=deactivate_file, vars=" ".join(values.keys()))) + return deactivate + + class ProfileEnvironment: def __init__(self): self._environments = OrderedDict() From 8f265486a3c8407136ad7521b401cf36cb8f8cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Wed, 15 Oct 2025 16:19:25 +0200 Subject: [PATCH 06/27] Bring back tests --- conan/tools/env/environment.py | 2 +- .../editable/test_editable_layout.py | 2 + test/integration/environment/test_env.py | 61 ++++++++++++++++--- test/unittests/tools/env/test_env_files.py | 11 +++- 4 files changed, 63 insertions(+), 13 deletions(-) diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index 19a35a5a264..fe65167adc9 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -515,7 +515,7 @@ def save_sh(self, file_location, generate_deactivate=True): if self._use_deactivate_function: result.append(f'export _CONAN_OLD_{varname}="$(printenv {varname})"') if value: - result.append(f'export {varname}="{value}"') + result.append('export {}="{}"'.format(varname, value)) else: result.append('unset {}'.format(varname)) diff --git a/test/integration/editable/test_editable_layout.py b/test/integration/editable/test_editable_layout.py index dc474647951..1f9beefd260 100644 --- a/test/integration/editable/test_editable_layout.py +++ b/test/integration/editable/test_editable_layout.py @@ -101,6 +101,8 @@ def test_editable_matching_folder_names(): data = client.load("hello/say-release-data.cmake") hello_say_folder = os.path.join(client.current_folder, "hello-say").replace("\\", "/") assert f'set(say_PACKAGE_FOLDER_RELEASE "{hello_say_folder}")' in data + env = client.load("hello/conanbuildenv-release.sh") + assert "$script_folder/deactivate_conanbuildenv-release.sh" in env.replace("\\", "/") def test_install_editable_build_folder_vars(): diff --git a/test/integration/environment/test_env.py b/test/integration/environment/test_env.py index b04560b7bb2..772549ee382 100644 --- a/test/integration/environment/test_env.py +++ b/test/integration/environment/test_env.py @@ -486,7 +486,8 @@ class Pkg(ConanFile): assert "LD_LIBRARY_PATH" in conanrunenv -def test_multiple_deactivate(): +@pytest.mark.parametrize("use_function", [True, False]) +def test_multiple_deactivate(use_function): conanfile = textwrap.dedent(r""" from conan import ConanFile from conan.tools.env import Environment @@ -513,13 +514,14 @@ def generate(self): "display.bat": display_bat, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) - client.run("install .") + client.run(f"install . -c=tools.env:deactivate_function={use_function}") for _ in range(2): # Just repeat it, so we can check things keep working if platform.system() == "Windows": cmd = "conanbuild.bat && display.bat && deactivate_conanbuild.bat && display.bat" else: - cmd = '. ./conanbuild.sh && ./display.sh && deactivate_conanbuild && ./display.sh' + deactivate_cmd = "deactivate_conanbuild" if use_function else ". ./deactivate_conanbuild.sh" + cmd = f'. ./conanbuild.sh && ./display.sh && {deactivate_cmd} && ./display.sh' out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() out = out.decode() @@ -530,7 +532,8 @@ def generate(self): assert "VAR2=!!" in out -def test_multiple_deactivate_order(): +@pytest.mark.parametrize("use_function", [True, False]) +def test_multiple_deactivate_order(use_function): """ https://github.com/conan-io/conan/issues/13693 """ @@ -558,13 +561,14 @@ def generate(self): "display.bat": display_bat, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) - client.run("install .") + client.run(f"install . -c=tools.env:deactivate_function={use_function}") for _ in range(2): # Just repeat it, so we can check things keep working if platform.system() == "Windows": cmd = "conanbuild.bat && display.bat && deactivate_conanbuild.bat && display.bat" else: - cmd = '. ./conanbuild.sh && ./display.sh && deactivate_conanbuild && ./display.sh' + deactivate_cmd = "deactivate_conanbuild" if use_function else ". ./deactivate_conanbuild.sh" + cmd = f'. ./conanbuild.sh && ./display.sh && {deactivate_cmd} && ./display.sh' out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() out = out.decode() @@ -645,7 +649,8 @@ class Pkg(ConanFile): assert "MYTOOL {}!!".format(i) in client.out -def test_profile_build_env_spaces(): +@pytest.mark.parametrize("use_function", [True, False]) +def test_profile_build_env_spaces(use_function): display_bat = textwrap.dedent("""\ @echo off echo VAR1=%VAR1%!! @@ -659,12 +664,13 @@ def test_profile_build_env_spaces(): "display.bat": display_bat, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) - client.run("install . -g VirtualBuildEnv -pr=profile") + client.run(f"install . -g VirtualBuildEnv -pr=profile -c=tools.env:deactivate_function={use_function}") if platform.system() == "Windows": cmd = "conanbuild.bat && display.bat && deactivate_conanbuild.bat && display.bat" else: - cmd = '. ./conanbuild.sh && ./display.sh && deactivate_conanbuild && ./display.sh' + deactivate_cmd = "deactivate_conanbuild" if use_function else ". ./deactivate_conanbuild.sh" + cmd = f'. ./conanbuild.sh && ./display.sh && {deactivate_cmd} && ./display.sh' out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() out = out.decode() @@ -673,6 +679,32 @@ def test_profile_build_env_spaces(): assert "VAR1=!!" in out +def test_deactivate_location(): + conanfile = textwrap.dedent(r""" + from conan import ConanFile + from conan.tools.env import Environment + class Pkg(ConanFile): + def package_info(self): + self.buildenv_info.define("FOO", "BAR") + """) + client = TestClient() + client.save({"pkg.py": conanfile}) + client.run("create pkg.py --name pkg --version 1.0") + client.run("install --requires pkg/1.0@ -g VirtualBuildEnv -of=myfolder -s build_type=Release -s arch=x86_64") + + source_cmd, script_ext = ("myfolder\\", ".bat") if platform.system() == "Windows" else (". ./myfolder/", ".sh") + cmd = "{}conanbuild{}".format(source_cmd, script_ext) + + subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, + cwd=client.current_folder).communicate() + + assert not os.path.exists(os.path.join(client.current_folder, + "deactivate_conanbuildenv-release-x86_64{}".format(script_ext))) + + assert os.path.exists(os.path.join(client.current_folder, "myfolder", + "deactivate_conanbuildenv-release-x86_64{}".format(script_ext))) + + @pytest.mark.skipif(platform.system() == "Windows", reason="Requires sh") def test_skip_virtualbuildenv_run(): # Build require @@ -817,6 +849,17 @@ def test(self): assert "MYLIBVAR=MYLIBVALUE:Release" in c.out +def test_deactivate_relocatable_substitute(): + c = TestClient() + # this cannot be tested in CI, because permissions over root folder + # c.current_folder = "/build" + c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) + c.run("install . -s os=Linux -s:b os=Linux") + conanbuild = c.load("conanbuildenv.sh") + result = os.path.join("$script_folder", "deactivate_conanbuildenv.sh") + assert f'"{result}"' in conanbuild + + class TestDotEnv: def test_dotenv(self): c = TestClient() diff --git a/test/unittests/tools/env/test_env_files.py b/test/unittests/tools/env/test_env_files.py index 8e2f1eb9347..63dc357004d 100644 --- a/test/unittests/tools/env/test_env_files.py +++ b/test/unittests/tools/env/test_env_files.py @@ -130,7 +130,8 @@ def test_env_files_ps1(env, prevenv): @pytest.mark.skipif(platform.system() == "Windows", reason="Not in Windows") -def test_env_files_sh(env, prevenv): +@pytest.mark.parametrize("use_function", [True, False]) +def test_env_files_sh(env, prevenv, use_function): display = textwrap.dedent("""\ echo MyVar=$MyVar!! echo MyVar1=$MyVar1!! @@ -146,12 +147,16 @@ def test_env_files_sh(env, prevenv): """) with chdir(temp_folder()): - env = env.vars(ConanFileMock()) + conanfile = ConanFileMock() + if use_function: + conanfile.conf.define("tools.env:deactivate_function", True) + env = env.vars(conanfile) env.save_sh("test.sh") save("display.sh", display) os.chmod("display.sh", 0o777) # We include the "set -e" to test it is robust against errors - cmd = 'set -e && . ./test.sh && ./display.sh && deactivate_test && ./display.sh' + deactivate_cmd = "deactivate_test" if use_function else ". ./deactivate_test.sh" + cmd = f'set -e && . ./test.sh && ./display.sh && {deactivate_cmd} && ./display.sh' check_env_files_output(cmd, prevenv) From 610ace91543bc56252fae544e62d5a01a9bff3be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Wed, 15 Oct 2025 16:24:29 +0200 Subject: [PATCH 07/27] Fix reported name when more than 1 env export the same value --- conan/tools/env/environment.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index fe65167adc9..586bc056ddf 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -513,7 +513,8 @@ def save_sh(self, file_location, generate_deactivate=True): root_path=abs_base_path, script_path=new_path) value = value.replace('"', '\\"') if self._use_deactivate_function: - result.append(f'export _CONAN_OLD_{varname}="$(printenv {varname})"') + deactivate_name = _sh_get_deactivate_name(filename) + result.append(f'export _CONAN_OLD_{deactivate_name}_{varname}="$(printenv {varname})"') if value: result.append('export {}="{}"'.format(varname, value)) else: @@ -582,23 +583,27 @@ def save_script(self, filename): register_env_script(self._conanfile, path, self._scope) +def _sh_get_deactivate_name(filename): + # TODO: Consolidate into one if all the deactivate_name are the same + return os.path.splitext(os.path.basename(filename))[0].replace("-", "_") + + def _sh_deactivate_contents(use_deactivate_function, values, filename): if use_deactivate_function: # The variable handling can be improved, now it is very basic - deactivate_name = os.path.splitext(os.path.basename(filename))[0].replace("-", "_") deactivate = textwrap.dedent("""\ deactivate_{deactivate_name} () {{ echo "Restoring environment" for v in {vars} do - OLD_VARIABLE="_CONAN_OLD_${{v}}" + OLD_VARIABLE="_CONAN_OLD_{deactivate_name}_${{v}}" export $(eval echo "$v")="$(printenv $(eval echo "${{OLD_VARIABLE}}"))" unset "${{OLD_VARIABLE}}" done unset -f deactivate_{deactivate_name} }} """.format(vars=" ".join(values.keys()), filename=filename, - deactivate_name=deactivate_name)) + deactivate_name=_sh_get_deactivate_name(filename))) else: deactivate_file = os.path.join("$script_folder", "deactivate_{}".format(filename)) deactivate = textwrap.dedent("""\ From d4634db70770bf3b19b97ca33a9f6b16fbfb4b53 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Wed, 15 Oct 2025 18:32:04 +0200 Subject: [PATCH 08/27] Make shell scripts more robust and sh-like --- conan/tools/env/environment.py | 37 ++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index 586bc056ddf..c9f9fae406a 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -1,5 +1,6 @@ import os import textwrap +from shlex import quote from collections import OrderedDict from contextlib import contextmanager @@ -513,8 +514,12 @@ def save_sh(self, file_location, generate_deactivate=True): root_path=abs_base_path, script_path=new_path) value = value.replace('"', '\\"') if self._use_deactivate_function: - deactivate_name = _sh_get_deactivate_name(filename) - result.append(f'export _CONAN_OLD_{deactivate_name}_{varname}="$(printenv {varname})"') + # Check environment variable existence before saving value + result.append( + f'if [ -n "${{{varname}+x}}" ]; then ' + f'export {_sh_get_old_prefix(filename)}_{varname}="${{{varname}}}"; ' + f'fi;' + ) if value: result.append('export {}="{}"'.format(varname, value)) else: @@ -583,27 +588,33 @@ def save_script(self, filename): register_env_script(self._conanfile, path, self._scope) -def _sh_get_deactivate_name(filename): - # TODO: Consolidate into one if all the deactivate_name are the same +def _sh_get_deactivate_func_name(filename): return os.path.splitext(os.path.basename(filename))[0].replace("-", "_") +def _sh_get_old_prefix(filename): + return f"_CONAN_OLD_{_sh_get_deactivate_func_name(filename).upper()}" def _sh_deactivate_contents(use_deactivate_function, values, filename): if use_deactivate_function: # The variable handling can be improved, now it is very basic deactivate = textwrap.dedent("""\ - deactivate_{deactivate_name} () {{ + # sh-like function to restore environment + deactivate_{func_name} () {{ echo "Restoring environment" - for v in {vars} - do - OLD_VARIABLE="_CONAN_OLD_{deactivate_name}_${{v}}" - export $(eval echo "$v")="$(printenv $(eval echo "${{OLD_VARIABLE}}"))" - unset "${{OLD_VARIABLE}}" + for v in {vars_list}; do + old_var="{var_prefix}_${{v}}" + # Use eval for indirect expansion (POSIX safe) + eval "old_value=\\${{${{old_var}}}}" + if [ -n "${{old_value+x}}" ]; then + eval "export ${{v}}=\\${{old_value}}" + unset "${{old_var}}" + fi done - unset -f deactivate_{deactivate_name} + unset -f deactivate_{func_name} 2>/dev/null || true }} - """.format(vars=" ".join(values.keys()), filename=filename, - deactivate_name=_sh_get_deactivate_name(filename))) + """.format(vars_list=" ".join(quote(v) for v in values.keys()), + var_prefix=_sh_get_old_prefix(filename), + func_name=_sh_get_deactivate_func_name(filename))) else: deactivate_file = os.path.join("$script_folder", "deactivate_{}".format(filename)) deactivate = textwrap.dedent("""\ From 49d8df377559c887efdb11ff079bbf469a7890bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Thu, 16 Oct 2025 12:49:37 +0200 Subject: [PATCH 09/27] Powershell --- conan/tools/env/environment.py | 64 +++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index c9f9fae406a..7b5763bfd14 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -455,31 +455,7 @@ def save_bat(self, file_location, generate_deactivate=True): def save_ps1(self, file_location, generate_deactivate=True,): _, filename = os.path.split(file_location) - deactivate_file = "deactivate_{}".format(filename) - deactivate = textwrap.dedent("""\ - Push-Location $PSScriptRoot - "echo `"Restoring environment`"" | Out-File -FilePath "{deactivate_file}" - $vars = (Get-ChildItem env:*).name - $updated_vars = @({vars}) - - foreach ($var in $updated_vars) - {{ - if ($var -in $vars) - {{ - $var_value = (Get-ChildItem env:$var).value - Add-Content "{deactivate_file}" "`n`$env:$var = `"$var_value`"" - }} - else - {{ - Add-Content "{deactivate_file}" "`nif (Test-Path env:$var) {{ Remove-Item env:$var }}" - }} - }} - Pop-Location - """).format( - deactivate_file=deactivate_file, - vars=",".join(['"{}"'.format(var) for var in self._values.keys()]) - ) - + deactivate = _ps1_deactivate_contents(self._use_deactivate_function, self._values, filename) capture = textwrap.dedent("""\ {deactivate} """).format(deactivate=deactivate if generate_deactivate else "") @@ -488,6 +464,11 @@ def save_ps1(self, file_location, generate_deactivate=True,): for varname, varvalues in self._values.items(): value = varvalues.get_str("$env:{name}", subsystem=self._subsystem, pathsep=self._pathsep, root_path=abs_base_path, script_path=new_path) + if self._use_deactivate_function: + # Check environment variable existence before saving value + result.append( + f'if ($env:{varname}) {{ $env:CONAN_OLD_{varname} = $env:{varname} }}' + ) if value: value = value.replace('"', '`"') # escape quotes result.append('$env:{}="{}"'.format(varname, value)) @@ -591,9 +572,11 @@ def save_script(self, filename): def _sh_get_deactivate_func_name(filename): return os.path.splitext(os.path.basename(filename))[0].replace("-", "_") + def _sh_get_old_prefix(filename): return f"_CONAN_OLD_{_sh_get_deactivate_func_name(filename).upper()}" + def _sh_deactivate_contents(use_deactivate_function, values, filename): if use_deactivate_function: # The variable handling can be improved, now it is very basic @@ -634,6 +617,37 @@ def _sh_deactivate_contents(use_deactivate_function, values, filename): return deactivate +def _ps1_deactivate_contents(use_deactivate_function, values, filename): + if use_deactivate_function: + deactivate = textwrap.dedent("""""") + else: + deactivate_file = "deactivate_{}".format(filename) + deactivate = textwrap.dedent("""\ + Push-Location $PSScriptRoot + "echo `"Restoring environment`"" | Out-File -FilePath "{deactivate_file}" + $vars = (Get-ChildItem env:*).name + $updated_vars = @({vars}) + + foreach ($var in $updated_vars) + {{ + if ($var -in $vars) + {{ + $var_value = (Get-ChildItem env:$var).value + Add-Content "{deactivate_file}" "`n`$env:$var = `"$var_value`"" + }} + else + {{ + Add-Content "{deactivate_file}" "`nif (Test-Path env:$var) {{ Remove-Item env:$var }}" + }} + }} + Pop-Location + """).format( + deactivate_file=deactivate_file, + vars=",".join(['"{}"'.format(var) for var in values.keys()]) + ) + return deactivate + + class ProfileEnvironment: def __init__(self): self._environments = OrderedDict() From 4a1e65bacee223b86a7c5599b6638735d1285e22 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Thu, 16 Oct 2025 13:40:37 +0200 Subject: [PATCH 10/27] WIP (not tested): added powershell deactivate --- conan/tools/env/environment.py | 127 ++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 56 deletions(-) diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index 7b5763bfd14..44155bd7248 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -467,7 +467,7 @@ def save_ps1(self, file_location, generate_deactivate=True,): if self._use_deactivate_function: # Check environment variable existence before saving value result.append( - f'if ($env:{varname}) {{ $env:CONAN_OLD_{varname} = $env:{varname} }}' + f'if ($env:{varname}) {{ $env:{_old_env_prefix(filename)}_{varname} = $env:{varname} }}' ) if value: value = value.replace('"', '`"') # escape quotes @@ -498,7 +498,7 @@ def save_sh(self, file_location, generate_deactivate=True): # Check environment variable existence before saving value result.append( f'if [ -n "${{{varname}+x}}" ]; then ' - f'export {_sh_get_old_prefix(filename)}_{varname}="${{{varname}}}"; ' + f'export {_old_env_prefix(filename)}_{varname}="${{{varname}}}"; ' f'fi;' ) if value: @@ -569,82 +569,97 @@ def save_script(self, filename): register_env_script(self._conanfile, path, self._scope) -def _sh_get_deactivate_func_name(filename): +def _deactivate_func_name(filename): return os.path.splitext(os.path.basename(filename))[0].replace("-", "_") -def _sh_get_old_prefix(filename): - return f"_CONAN_OLD_{_sh_get_deactivate_func_name(filename).upper()}" +def _old_env_prefix(filename): + return f"_CONAN_OLD_{_deactivate_func_name(filename).upper()}" def _sh_deactivate_contents(use_deactivate_function, values, filename): if use_deactivate_function: # The variable handling can be improved, now it is very basic deactivate = textwrap.dedent("""\ - # sh-like function to restore environment - deactivate_{func_name} () {{ - echo "Restoring environment" - for v in {vars_list}; do - old_var="{var_prefix}_${{v}}" - # Use eval for indirect expansion (POSIX safe) - eval "old_value=\\${{${{old_var}}}}" - if [ -n "${{old_value+x}}" ]; then - eval "export ${{v}}=\\${{old_value}}" - unset "${{old_var}}" - fi - done - unset -f deactivate_{func_name} 2>/dev/null || true - }} + # sh-like function to restore environment + deactivate_{func_name} () {{ + echo "Restoring environment" + for v in {vars_list}; do + old_var="{var_prefix}_${{v}}" + # Use eval for indirect expansion (POSIX safe) + eval "old_value=\\${{${{old_var}}}}" + if [ -n "${{old_value+x}}" ]; then + eval "export ${{v}}=\\${{old_value}}" + unset "${{old_var}}" + fi + done + unset -f deactivate_{func_name} 2>/dev/null || true + }} """.format(vars_list=" ".join(quote(v) for v in values.keys()), - var_prefix=_sh_get_old_prefix(filename), - func_name=_sh_get_deactivate_func_name(filename))) + var_prefix=_old_env_prefix(filename), + func_name=_deactivate_func_name(filename))) else: deactivate_file = os.path.join("$script_folder", "deactivate_{}".format(filename)) deactivate = textwrap.dedent("""\ - echo "echo Restoring environment" > "{deactivate_file}" - for v in {vars} - do - is_defined="true" - value=$(printenv $v) || is_defined="" || true - if [ -n "$value" ] || [ -n "$is_defined" ] - then - echo export "$v='$value'" >> "{deactivate_file}" - else - echo unset $v >> "{deactivate_file}" - fi - done + echo "echo Restoring environment" > "{deactivate_file}" + for v in {vars} + do + is_defined="true" + value=$(printenv $v) || is_defined="" || true + if [ -n "$value" ] || [ -n "$is_defined" ] + then + echo export "$v='$value'" >> "{deactivate_file}" + else + echo unset $v >> "{deactivate_file}" + fi + done """.format(deactivate_file=deactivate_file, vars=" ".join(values.keys()))) return deactivate def _ps1_deactivate_contents(use_deactivate_function, values, filename): if use_deactivate_function: - deactivate = textwrap.dedent("""""") + vars_list=", ".join(quote(v) for v in values.keys()) + var_prefix=_old_env_prefix(filename) + func_name=_deactivate_func_name(filename) + + deactivate = textwrap.dedent(f"""\ + function global:{func_name} {{ + Write-Host "Restoring environment" + $vars = @({vars_list}) + foreach ($v in $vars) {{ + $oldVarName = "{var_prefix}_$v" + $oldValue = Get-Item -Path "Env:$oldVarName" -ErrorAction SilentlyContinue + if ($null -ne $oldValue) {{ + Set-Item -Path "Env:$v" -Value $oldValue.Value + Remove-Item -Path "Env:$oldVarName" -ErrorAction SilentlyContinue + }} + }} + Remove-Item function:global:{func_name} -ErrorAction SilentlyContinue + }} + """) else: deactivate_file = "deactivate_{}".format(filename) deactivate = textwrap.dedent("""\ - Push-Location $PSScriptRoot - "echo `"Restoring environment`"" | Out-File -FilePath "{deactivate_file}" - $vars = (Get-ChildItem env:*).name - $updated_vars = @({vars}) - - foreach ($var in $updated_vars) - {{ - if ($var -in $vars) - {{ - $var_value = (Get-ChildItem env:$var).value - Add-Content "{deactivate_file}" "`n`$env:$var = `"$var_value`"" - }} - else - {{ - Add-Content "{deactivate_file}" "`nif (Test-Path env:$var) {{ Remove-Item env:$var }}" - }} - }} - Pop-Location - """).format( - deactivate_file=deactivate_file, - vars=",".join(['"{}"'.format(var) for var in values.keys()]) - ) + Push-Location $PSScriptRoot + "echo `"Restoring environment`"" | Out-File -FilePath "{deactivate_file}" + $vars = (Get-ChildItem env:*).name + $updated_vars = @({vars}) + + foreach ($var in $updated_vars) + {{ + if ($var -in $vars) + {{ + $var_value = (Get-ChildItem env:$var).value + Add-Content "{deactivate_file}" "`n`$env:$var = `"$var_value`"" + }} + else + {{ + Add-Content "{deactivate_file}" "`nif (Test-Path env:$var) {{ Remove-Item env:$var }}" + }} + }} + Pop-Location + """).format(deactivate_file=deactivate_file, vars=",".join(['"{}"'.format(var) for var in values.keys()])) return deactivate From f005d88800d48ba67947653252484f8635542b23 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Thu, 16 Oct 2025 18:42:17 +0200 Subject: [PATCH 11/27] Avoid unsetting existing empty envs --- conan/tools/env/environment.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index 44155bd7248..47ed34a36cb 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -587,13 +587,18 @@ def _sh_deactivate_contents(use_deactivate_function, values, filename): for v in {vars_list}; do old_var="{var_prefix}_${{v}}" # Use eval for indirect expansion (POSIX safe) - eval "old_value=\\${{${{old_var}}}}" - if [ -n "${{old_value+x}}" ]; then + eval "is_set=\\${{${{old_var}}+x}}" + if [ -n "${{is_set}}" ]; then + eval "old_value=\\${{${{old_var}}}}" + echo "Restoring ${{v}} to ${{old_value}}" eval "export ${{v}}=\\${{old_value}}" - unset "${{old_var}}" + else + echo "Unsetting ${{v}}" + unset "${{v}}" fi + unset "${{old_var}}" done - unset -f deactivate_{func_name} 2>/dev/null || true + unset -f deactivate_{func_name} }} """.format(vars_list=" ".join(quote(v) for v in values.keys()), var_prefix=_old_env_prefix(filename), From ac7fad505a7d1d5bda9cce7ccbe332a35d7f3321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Thu, 16 Oct 2025 19:03:29 +0200 Subject: [PATCH 12/27] Add old control for ps1, enable feature by default to see what tests break --- conan/internal/api/install/generators.py | 16 ++++++-- conan/tools/env/environment.py | 47 +++++++++++++++--------- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/conan/internal/api/install/generators.py b/conan/internal/api/install/generators.py index 3b0f0ce0e37..19c50815cc2 100644 --- a/conan/internal/api/install/generators.py +++ b/conan/internal/api/install/generators.py @@ -235,12 +235,22 @@ def bat_content(files): bat_content(deactivates(bats))) if ps1s: def ps1_content(files): - return "\r\n".join(['& "{}"'.format(b) for b in files]) + aggregated_calls = "\r\n".join(['& "{}"'.format(b) for b in files]) + if True or use_deactivate_function: + content = aggregated_calls + "\n" + content += f"function global:deactivate_conan{group} {{\n" + for deactivate_name in deactivate_function_names(shs): + content += f" deactivate_{deactivate_name}\n" + content += "}\n" + return content + else: + return aggregated_calls filename = "conan{}.ps1".format(group) generated.append(filename) save(os.path.join(conanfile.generators_folder, filename), ps1_content(ps1s)) - save(os.path.join(conanfile.generators_folder, "deactivate_{}".format(filename)), - ps1_content(deactivates(ps1s))) + if not use_deactivate_function: + save(os.path.join(conanfile.generators_folder, "deactivate_{}".format(filename)), + ps1_content(deactivates(ps1s))) if generated: conanfile.output.highlight("Generating aggregated env files") conanfile.output.info(f"Generated aggregated env files: {generated}") diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index 47ed34a36cb..6eb50c56b50 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -4,7 +4,7 @@ from collections import OrderedDict from contextlib import contextmanager -from conan.api.output import ConanOutput +from conan.api.output import ConanOutput, LEVEL_VERBOSE from conan.internal.api.install.generators import relativize_paths from conan.internal.subsystems import deduce_subsystem, WINDOWS, subsystem_path from conan.errors import ConanException @@ -348,6 +348,7 @@ def __init__(self, conanfile, values, scope): self._subsystem = deduce_subsystem(conanfile, scope) self._use_deactivate_function = conanfile.conf.get("tools.env:deactivate_function", default=False, check_type=bool) + self._verbose = conanfile.output.level_allowed(LEVEL_VERBOSE) @property def _pathsep(self): @@ -455,7 +456,8 @@ def save_bat(self, file_location, generate_deactivate=True): def save_ps1(self, file_location, generate_deactivate=True,): _, filename = os.path.split(file_location) - deactivate = _ps1_deactivate_contents(self._use_deactivate_function, self._values, filename) + deactivate = _ps1_deactivate_contents(self._use_deactivate_function, self._values, filename, + self._verbose) capture = textwrap.dedent("""\ {deactivate} """).format(deactivate=deactivate if generate_deactivate else "") @@ -472,6 +474,8 @@ def save_ps1(self, file_location, generate_deactivate=True,): if value: value = value.replace('"', '`"') # escape quotes result.append('$env:{}="{}"'.format(varname, value)) + if self._verbose: + result.append(f'Write-Host "Setting {varname} to: {value}"') else: result.append('if (Test-Path env:{0}) {{ Remove-Item env:{0} }}'.format(varname)) @@ -503,6 +507,8 @@ def save_sh(self, file_location, generate_deactivate=True): ) if value: result.append('export {}="{}"'.format(varname, value)) + if self._verbose: + result.append(f'echo "Setting {varname} to: {value}"') else: result.append('unset {}'.format(varname)) @@ -577,7 +583,7 @@ def _old_env_prefix(filename): return f"_CONAN_OLD_{_deactivate_func_name(filename).upper()}" -def _sh_deactivate_contents(use_deactivate_function, values, filename): +def _sh_deactivate_contents(use_deactivate_function, values, filename, verbose): if use_deactivate_function: # The variable handling can be improved, now it is very basic deactivate = textwrap.dedent("""\ @@ -590,10 +596,11 @@ def _sh_deactivate_contents(use_deactivate_function, values, filename): eval "is_set=\\${{${{old_var}}+x}}" if [ -n "${{is_set}}" ]; then eval "old_value=\\${{${{old_var}}}}" - echo "Restoring ${{v}} to ${{old_value}}" + {verbose_old_value} eval "export ${{v}}=\\${{old_value}}" else echo "Unsetting ${{v}}" + {verbose_unset} unset "${{v}}" fi unset "${{old_var}}" @@ -602,7 +609,9 @@ def _sh_deactivate_contents(use_deactivate_function, values, filename): }} """.format(vars_list=" ".join(quote(v) for v in values.keys()), var_prefix=_old_env_prefix(filename), - func_name=_deactivate_func_name(filename))) + func_name=_deactivate_func_name(filename), + verbose_old_value='echo "Restoring ${{v}} to ${{old_value}}"' if verbose else "", + verbose_unset='echo "Unsetting ${{v}}"' if verbose else "")) else: deactivate_file = os.path.join("$script_folder", "deactivate_{}".format(filename)) deactivate = textwrap.dedent("""\ @@ -622,25 +631,28 @@ def _sh_deactivate_contents(use_deactivate_function, values, filename): return deactivate -def _ps1_deactivate_contents(use_deactivate_function, values, filename): - if use_deactivate_function: - vars_list=", ".join(quote(v) for v in values.keys()) - var_prefix=_old_env_prefix(filename) - func_name=_deactivate_func_name(filename) +def _ps1_deactivate_contents(use_deactivate_function, values, filename, verbose): + if True or use_deactivate_function: + vars_list = ", ".join(f'"{v}"' for v in values.keys()) + var_prefix = _old_env_prefix(filename) + func_name = _deactivate_func_name(filename) deactivate = textwrap.dedent(f"""\ - function global:{func_name} {{ + function global:deactivate_{func_name} {{ Write-Host "Restoring environment" - $vars = @({vars_list}) - foreach ($v in $vars) {{ + foreach ($v in @({vars_list})) {{ $oldVarName = "{var_prefix}_$v" $oldValue = Get-Item -Path "Env:$oldVarName" -ErrorAction SilentlyContinue - if ($null -ne $oldValue) {{ + if (Test-Path env:$oldValue) {{ + {'Write-Host "Unsetting $v"' if verbose else ''} + Remove-Item -Path "Env:$v" -ErrorAction SilentlyContinue + }} else {{ + {'Write-Host "Restoring $v to $($oldValue.Value)"' if verbose else ''} Set-Item -Path "Env:$v" -Value $oldValue.Value - Remove-Item -Path "Env:$oldVarName" -ErrorAction SilentlyContinue }} + Remove-Item -Path "Env:$oldVarName" -ErrorAction SilentlyContinue }} - Remove-Item function:global:{func_name} -ErrorAction SilentlyContinue + Remove-Item -Path function:deactivate_{func_name} -ErrorAction SilentlyContinue }} """) else: @@ -664,7 +676,8 @@ def _ps1_deactivate_contents(use_deactivate_function, values, filename): }} }} Pop-Location - """).format(deactivate_file=deactivate_file, vars=",".join(['"{}"'.format(var) for var in values.keys()])) + """).format(deactivate_file=deactivate_file, + vars=",".join(['"{}"'.format(var) for var in values.keys()])) return deactivate From f662dbe6455e2e5d1425f0ae3f9ba8ac91339a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Thu, 16 Oct 2025 19:42:12 +0200 Subject: [PATCH 13/27] Add missing arg --- conan/tools/env/environment.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index 6eb50c56b50..e1cb36b6d02 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -488,7 +488,8 @@ def save_ps1(self, file_location, generate_deactivate=True,): def save_sh(self, file_location, generate_deactivate=True): filepath, filename = os.path.split(file_location) - deactivate = _sh_deactivate_contents(self._use_deactivate_function, self._values, filename) + deactivate = _sh_deactivate_contents(self._use_deactivate_function, self._values, filename, + self._verbose) capture = textwrap.dedent("""\ {deactivate} """).format(deactivate=deactivate if generate_deactivate else "") From d21ccc56170a62303d3dc0e781f06acc2deb2786 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Fri, 17 Oct 2025 17:15:12 +0200 Subject: [PATCH 14/27] Fixed powershell tests and parametrize --- conan/internal/api/install/generators.py | 4 ++-- conan/tools/env/environment.py | 2 +- test/unittests/tools/env/test_env.py | 7 +++++-- test/unittests/tools/env/test_env_files.py | 11 ++++++++--- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/conan/internal/api/install/generators.py b/conan/internal/api/install/generators.py index 19c50815cc2..abc4826b140 100644 --- a/conan/internal/api/install/generators.py +++ b/conan/internal/api/install/generators.py @@ -236,10 +236,10 @@ def bat_content(files): if ps1s: def ps1_content(files): aggregated_calls = "\r\n".join(['& "{}"'.format(b) for b in files]) - if True or use_deactivate_function: + if use_deactivate_function: content = aggregated_calls + "\n" content += f"function global:deactivate_conan{group} {{\n" - for deactivate_name in deactivate_function_names(shs): + for deactivate_name in deactivate_function_names(ps1s): content += f" deactivate_{deactivate_name}\n" content += "}\n" return content diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index e1cb36b6d02..7b2489691e4 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -633,7 +633,7 @@ def _sh_deactivate_contents(use_deactivate_function, values, filename, verbose): def _ps1_deactivate_contents(use_deactivate_function, values, filename, verbose): - if True or use_deactivate_function: + if use_deactivate_function: vars_list = ", ".join(f'"{v}"' for v in values.keys()) var_prefix = _old_env_prefix(filename) func_name = _deactivate_func_name(filename) diff --git a/test/unittests/tools/env/test_env.py b/test/unittests/tools/env/test_env.py index 53411087eb2..88f00ffe0fa 100644 --- a/test/unittests/tools/env/test_env.py +++ b/test/unittests/tools/env/test_env.py @@ -232,7 +232,8 @@ def test_windows_case_insensitive_bat(envvars): @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") -def test_windows_case_insensitive_ps1(envvars): +@pytest.mark.parametrize("deactivate_function", [True, False],) +def test_windows_case_insensitive_ps1(envvars, deactivate_function): display = textwrap.dedent("""\ echo "MyVar=$env:MyVar!!" echo "MyVar1=$env:MyVar1!!" @@ -245,9 +246,11 @@ def test_windows_case_insensitive_ps1(envvars): prevenv.update(dict(os.environ.copy())) with chdir(temp_folder()): + envvars._use_deactivate_function = deactivate_function envvars.save_ps1("test.ps1") save("display.ps1", display) - cmd = "powershell.exe .\\test.ps1 ; .\\display.ps1 ; .\\deactivate_test.ps1 ; .\\display.ps1" + deactivate_cmd = "deactivate_test" if deactivate_function else ".\\deactivate_test.ps1" + cmd = f"powershell.exe .\\test.ps1 ; .\\display.ps1 ; {deactivate_cmd} ; .\\display.ps1" check_command_output(cmd, prevenv) diff --git a/test/unittests/tools/env/test_env_files.py b/test/unittests/tools/env/test_env_files.py index 63dc357004d..cfc10311ade 100644 --- a/test/unittests/tools/env/test_env_files.py +++ b/test/unittests/tools/env/test_env_files.py @@ -103,7 +103,8 @@ def test_env_files_bat(env, prevenv): @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") -def test_env_files_ps1(env, prevenv): +@pytest.mark.parametrize("use_function", [True, False]) +def test_env_files_ps1(env, prevenv, use_function): prevenv.update(dict(os.environ.copy())) display = textwrap.dedent("""\ @@ -121,11 +122,15 @@ def test_env_files_ps1(env, prevenv): """) with chdir(temp_folder()): - env = env.vars(ConanFileMock()) + conanfile = ConanFileMock() + if use_function: + conanfile.conf.define("tools.env:deactivate_function", True) + env = env.vars(conanfile) env._subsystem = WINDOWS env.save_ps1("test.ps1") save("display.ps1", display) - cmd = "powershell.exe .\\test.ps1 ; .\\display.ps1 ; .\\deactivate_test.ps1 ; .\\display.ps1" + deactivate_cmd = "deactivate_test" if use_function else ".\\deactivate_test.ps1" + cmd = f"powershell.exe .\\test.ps1 ; .\\display.ps1 ; {deactivate_cmd} ; .\\display.ps1" check_env_files_output(cmd, prevenv) From af529049f4b7037efd0035e545d039d786c2f348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Mon, 20 Oct 2025 12:19:02 +0200 Subject: [PATCH 15/27] Cleanup code, add test for missing sh var --- conan/tools/env/environment.py | 22 +++++------- test/integration/environment/test_env.py | 44 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index 7b2489691e4..06c08f201f9 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -586,33 +586,29 @@ def _old_env_prefix(filename): def _sh_deactivate_contents(use_deactivate_function, values, filename, verbose): if use_deactivate_function: - # The variable handling can be improved, now it is very basic - deactivate = textwrap.dedent("""\ + _func_name = _deactivate_func_name(filename) + vars_list = " ".join(quote(v) for v in values.keys()) + deactivate = textwrap.dedent(f"""\ # sh-like function to restore environment - deactivate_{func_name} () {{ + deactivate_{_func_name} () {{ echo "Restoring environment" for v in {vars_list}; do - old_var="{var_prefix}_${{v}}" + old_var="{_old_env_prefix(filename)}_${{v}}" # Use eval for indirect expansion (POSIX safe) eval "is_set=\\${{${{old_var}}+x}}" if [ -n "${{is_set}}" ]; then eval "old_value=\\${{${{old_var}}}}" - {verbose_old_value} + {'echo "Restoring ${{v}} to ${{old_value}}"' if verbose else ''} eval "export ${{v}}=\\${{old_value}}" else - echo "Unsetting ${{v}}" - {verbose_unset} + {'echo "Unsetting ${{v}}"' if verbose else ''} unset "${{v}}" fi unset "${{old_var}}" done - unset -f deactivate_{func_name} + unset -f deactivate_{_func_name} }} - """.format(vars_list=" ".join(quote(v) for v in values.keys()), - var_prefix=_old_env_prefix(filename), - func_name=_deactivate_func_name(filename), - verbose_old_value='echo "Restoring ${{v}} to ${{old_value}}"' if verbose else "", - verbose_unset='echo "Unsetting ${{v}}"' if verbose else "")) + """) else: deactivate_file = os.path.join("$script_folder", "deactivate_{}".format(filename)) deactivate = textwrap.dedent("""\ diff --git a/test/integration/environment/test_env.py b/test/integration/environment/test_env.py index 772549ee382..8c378494645 100644 --- a/test/integration/environment/test_env.py +++ b/test/integration/environment/test_env.py @@ -577,6 +577,50 @@ def generate(self): assert "MYVAR=!!" in out +@pytest.mark.parametrize("use_function", [True, False]) +def test_deactivate_missing_vars_stay_missing(use_function): + """ Tests that these two cases preserve variable status + 1. + export FOO= + ./conanrunenv.sh that changes FOO to something with a value + ./deactivate_conanrunenv.sh + FOO is still empty + + 2. + BAR is not defined + ./conanrunenv.sh that changes BAR to something with a value + ./deactivate_conanrunenv.sh + BAR is still not defined + """ + conanfile = textwrap.dedent(r""" + from conan import ConanFile + from conan.tools.env import Environment + class Pkg(ConanFile): + def generate(self): + e1 = Environment() + e1.define("FOO", "Value1") + e1.define("BAR", "Value2") + e1.vars(self).save_script("mybuild1") + """) + display_sh = textwrap.dedent("""\ + echo FOO=$FOO!! + if [ -n "${BAR+x}" ]; then echo "BAR EXISTS!!"; fi; + """) + client = TestClient(light=True) + client.save({"conanfile.py": conanfile, + "display.sh": display_sh}) + os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) + client.run(f"install . -c=tools.env:deactivate_function={use_function}") + + deactivate_cmd = "deactivate_conanbuild" if use_function else ". ./deactivate_conanbuild.sh" + cmd = f'export FOO=&& . ./conanbuild.sh && {deactivate_cmd} && ./display.sh' + out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + shell=True, cwd=client.current_folder).communicate() + out = out.decode() + assert "FOO=!!" in out + assert "BAR EXISTS!!" not in out + + @pytest.mark.skipif(platform.system() != "Windows", reason="Path problem in Windows only") @pytest.mark.parametrize("num_deps", [3, ]) def test_massive_paths(num_deps): From b884ed8a09bdaebb11183681b0e1b257e4e3a906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Mon, 20 Oct 2025 12:21:34 +0200 Subject: [PATCH 16/27] More cases --- test/integration/environment/test_env.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/test/integration/environment/test_env.py b/test/integration/environment/test_env.py index 8c378494645..3a0bfa262c9 100644 --- a/test/integration/environment/test_env.py +++ b/test/integration/environment/test_env.py @@ -591,6 +591,18 @@ def test_deactivate_missing_vars_stay_missing(use_function): ./conanrunenv.sh that changes BAR to something with a value ./deactivate_conanrunenv.sh BAR is still not defined + + 3. + BAZ is defined to some value + ./conanrunenv.sh that unsets BAZ + ./deactivate_conanrunenv.sh + BAZ is still defined to some value + + 4. + FOOBAR is empty + ./conanrunenv.sh that unsets FOOBAR + ./deactivate_conanrunenv.sh + FOOBAR is still empty """ conanfile = textwrap.dedent(r""" from conan import ConanFile @@ -600,11 +612,15 @@ def generate(self): e1 = Environment() e1.define("FOO", "Value1") e1.define("BAR", "Value2") + e1.unset("BAZ") + e1.unset("FOOBAR") e1.vars(self).save_script("mybuild1") """) display_sh = textwrap.dedent("""\ echo FOO=$FOO!! if [ -n "${BAR+x}" ]; then echo "BAR EXISTS!!"; fi; + echo BAZ=$BAZ!! + echo FOOBAR=$FOOBAR!! """) client = TestClient(light=True) client.save({"conanfile.py": conanfile, @@ -613,12 +629,15 @@ def generate(self): client.run(f"install . -c=tools.env:deactivate_function={use_function}") deactivate_cmd = "deactivate_conanbuild" if use_function else ". ./deactivate_conanbuild.sh" - cmd = f'export FOO=&& . ./conanbuild.sh && {deactivate_cmd} && ./display.sh' + cmd = (f'export FOO=&& export BAZ=Value3 && export FOOBAR=' + f'&& . ./conanbuild.sh && {deactivate_cmd} && ./display.sh') out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() out = out.decode() assert "FOO=!!" in out assert "BAR EXISTS!!" not in out + assert "BAZ=Value3!!" in out + assert "FOOBAR=!!" in out @pytest.mark.skipif(platform.system() != "Windows", reason="Path problem in Windows only") From ceac12be9b216f8b05865f7586d986f44b4bbb28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Mon, 20 Oct 2025 13:22:42 +0200 Subject: [PATCH 17/27] Skip for windows, this is only a shell test --- test/integration/environment/test_env.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/integration/environment/test_env.py b/test/integration/environment/test_env.py index 3a0bfa262c9..129520b5362 100644 --- a/test/integration/environment/test_env.py +++ b/test/integration/environment/test_env.py @@ -577,6 +577,7 @@ def generate(self): assert "MYVAR=!!" in out +@pytest.mark.skipif(platform.system() == "Windows", reason="Shell script test") @pytest.mark.parametrize("use_function", [True, False]) def test_deactivate_missing_vars_stay_missing(use_function): """ Tests that these two cases preserve variable status From 58ab106fef9157b8cb435847e90014524444f191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Mon, 20 Oct 2025 13:43:55 +0200 Subject: [PATCH 18/27] Remove global deactivate function --- conan/internal/api/install/generators.py | 3 +++ conan/tools/microsoft/visual.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/conan/internal/api/install/generators.py b/conan/internal/api/install/generators.py index abc4826b140..31c9e29f23c 100644 --- a/conan/internal/api/install/generators.py +++ b/conan/internal/api/install/generators.py @@ -214,6 +214,7 @@ def sh_content(files): content += f"deactivate_conan{group}() {{\n" for deactivate_name in deactivate_function_names(shs): content += f" deactivate_{deactivate_name}\n" + content += f" unset -f deactivate_conan{group}\n" content += "}\n" return content else: @@ -241,6 +242,8 @@ def ps1_content(files): content += f"function global:deactivate_conan{group} {{\n" for deactivate_name in deactivate_function_names(ps1s): content += f" deactivate_{deactivate_name}\n" + content += (f" Remove-Item -Path function:deactivate_conan{group} " + f"-ErrorAction SilentlyContinue") content += "}\n" return content else: diff --git a/conan/tools/microsoft/visual.py b/conan/tools/microsoft/visual.py index c38d9d13636..bf7907c6f9a 100644 --- a/conan/tools/microsoft/visual.py +++ b/conan/tools/microsoft/visual.py @@ -199,7 +199,8 @@ def generate(self, scope="build"): def _create_deactivate_vcvars_file(conanfile, filename): deactivate_filename = f"deactivate_{filename}" - message = f"[{deactivate_filename}]: vcvars env cannot be deactivated" + message = (f'"[{deactivate_filename}]: *** vcvars env cannot be deactivated ***\n' + 'To reset the environment, please close and reopen the terminal."') is_ps1 = filename.endswith(".ps1") if is_ps1: content = f"Write-Host {message}" From 870540b5ee03bb1980817ad1866c5408b6a74966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Mon, 20 Oct 2025 17:41:50 +0200 Subject: [PATCH 19/27] Simplify --- conan/tools/microsoft/visual.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/conan/tools/microsoft/visual.py b/conan/tools/microsoft/visual.py index bf7907c6f9a..2a7d490f114 100644 --- a/conan/tools/microsoft/visual.py +++ b/conan/tools/microsoft/visual.py @@ -199,8 +199,7 @@ def generate(self, scope="build"): def _create_deactivate_vcvars_file(conanfile, filename): deactivate_filename = f"deactivate_{filename}" - message = (f'"[{deactivate_filename}]: *** vcvars env cannot be deactivated ***\n' - 'To reset the environment, please close and reopen the terminal."') + message = f"[{deactivate_filename}]: *** vcvars env cannot be deactivated ***\n" is_ps1 = filename.endswith(".ps1") if is_ps1: content = f"Write-Host {message}" From 3e8da30f0d8a93ea46754368102ca46cc0f6cddd Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Tue, 21 Oct 2025 18:25:52 +0200 Subject: [PATCH 20/27] Improved UX --- conan/internal/api/install/generators.py | 28 +++--- conan/tools/env/environment.py | 120 +++++++++++------------ 2 files changed, 67 insertions(+), 81 deletions(-) diff --git a/conan/internal/api/install/generators.py b/conan/internal/api/install/generators.py index 31c9e29f23c..1f593c76974 100644 --- a/conan/internal/api/install/generators.py +++ b/conan/internal/api/install/generators.py @@ -208,19 +208,15 @@ def deactivate_function_names(filenames): ps1s.append("$PSScriptRoot/"+path) if shs: def sh_content(files): - aggregated_calls = ". " + " && . ".join('"{}"'.format(s) for s in files) + content = ". " + " && . ".join('"{}"'.format(s) for s in files) if use_deactivate_function: - content = aggregated_calls + "\n" - content += f"deactivate_conan{group}() {{\n" + content += f"\n\ndeactivate_conan{group}() {{\n" for deactivate_name in deactivate_function_names(shs): content += f" deactivate_{deactivate_name}\n" - content += f" unset -f deactivate_conan{group}\n" - content += "}\n" - return content - else: - return aggregated_calls + content += (f" unset -f deactivate_conan{group}\n}}\n" + f'echo "Environment activated. Run \"deactivate_conan{group}\" to restore."\n') + return content filename = "conan{}.sh".format(group) - generated.append(filename) save(os.path.join(conanfile.generators_folder, filename), sh_content(shs)) if not use_deactivate_function: @@ -236,18 +232,16 @@ def bat_content(files): bat_content(deactivates(bats))) if ps1s: def ps1_content(files): - aggregated_calls = "\r\n".join(['& "{}"'.format(b) for b in files]) + content = "\r\n".join(['& "{}"'.format(b) for b in files]) if use_deactivate_function: - content = aggregated_calls + "\n" - content += f"function global:deactivate_conan{group} {{\n" + content += f"\n\nfunction global:deactivate_conan{group} {{\n" for deactivate_name in deactivate_function_names(ps1s): content += f" deactivate_{deactivate_name}\n" content += (f" Remove-Item -Path function:deactivate_conan{group} " - f"-ErrorAction SilentlyContinue") - content += "}\n" - return content - else: - return aggregated_calls + "-ErrorAction SilentlyContinue" + "\n}\n" + f'echo \'Environment activated. Run \"deactivate_conan{group}\" to restore.\'\n') + return content filename = "conan{}.ps1".format(group) generated.append(filename) save(os.path.join(conanfile.generators_folder, filename), ps1_content(ps1s)) diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index 06c08f201f9..930b04663f5 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -454,26 +454,25 @@ def save_bat(self, file_location, generate_deactivate=True): with open(file_location, "w", encoding="utf-8") as f: f.write(content) - def save_ps1(self, file_location, generate_deactivate=True,): + def save_ps1(self, file_location, generate_deactivate=True): _, filename = os.path.split(file_location) - deactivate = _ps1_deactivate_contents(self._use_deactivate_function, self._values, filename, - self._verbose) - capture = textwrap.dedent("""\ - {deactivate} - """).format(deactivate=deactivate if generate_deactivate else "") - result = [capture] + + result = [] + if generate_deactivate: + result.append(_ps1_deactivate_contents(self._use_deactivate_function, self._values, + filename, self._verbose)) abs_base_path, new_path = relativize_paths(self._conanfile, "$PSScriptRoot") for varname, varvalues in self._values.items(): value = varvalues.get_str("$env:{name}", subsystem=self._subsystem, pathsep=self._pathsep, root_path=abs_base_path, script_path=new_path) - if self._use_deactivate_function: + if generate_deactivate and self._use_deactivate_function: # Check environment variable existence before saving value result.append( f'if ($env:{varname}) {{ $env:{_old_env_prefix(filename)}_{varname} = $env:{varname} }}' ) if value: value = value.replace('"', '`"') # escape quotes - result.append('$env:{}="{}"'.format(varname, value)) + result.append(f'$env:{varname}="{value}"') if self._verbose: result.append(f'Write-Host "Setting {varname} to: {value}"') else: @@ -488,18 +487,16 @@ def save_ps1(self, file_location, generate_deactivate=True,): def save_sh(self, file_location, generate_deactivate=True): filepath, filename = os.path.split(file_location) - deactivate = _sh_deactivate_contents(self._use_deactivate_function, self._values, filename, - self._verbose) - capture = textwrap.dedent("""\ - {deactivate} - """).format(deactivate=deactivate if generate_deactivate else "") - result = [capture] + result = [] + if generate_deactivate: + result.append(_sh_deactivate_contents(self._use_deactivate_function, self._values, + filename, self._verbose)) abs_base_path, new_path = relativize_paths(self._conanfile, "$script_folder") for varname, varvalues in self._values.items(): value = varvalues.get_str("${name}", self._subsystem, pathsep=self._pathsep, root_path=abs_base_path, script_path=new_path) value = value.replace('"', '\\"') - if self._use_deactivate_function: + if generate_deactivate and self._use_deactivate_function: # Check environment variable existence before saving value result.append( f'if [ -n "${{{varname}+x}}" ]; then ' @@ -507,11 +504,11 @@ def save_sh(self, file_location, generate_deactivate=True): f'fi;' ) if value: - result.append('export {}="{}"'.format(varname, value)) + result.append(f'export {varname}="{value}"') if self._verbose: result.append(f'echo "Setting {varname} to: {value}"') else: - result.append('unset {}'.format(varname)) + result.append(f'unset {varname}') content = "\n".join(result) content = f'script_folder="{os.path.abspath(filepath)}"\n' + content @@ -585,12 +582,12 @@ def _old_env_prefix(filename): def _sh_deactivate_contents(use_deactivate_function, values, filename, verbose): + vars_list = " ".join(quote(v) for v in values.keys()) if use_deactivate_function: - _func_name = _deactivate_func_name(filename) - vars_list = " ".join(quote(v) for v in values.keys()) - deactivate = textwrap.dedent(f"""\ + func_name = _deactivate_func_name(filename) + return textwrap.dedent(f"""\ # sh-like function to restore environment - deactivate_{_func_name} () {{ + deactivate_{func_name} () {{ echo "Restoring environment" for v in {vars_list}; do old_var="{_old_env_prefix(filename)}_${{v}}" @@ -606,35 +603,32 @@ def _sh_deactivate_contents(use_deactivate_function, values, filename, verbose): fi unset "${{old_var}}" done - unset -f deactivate_{_func_name} + unset -f deactivate_{func_name} }} """) - else: - deactivate_file = os.path.join("$script_folder", "deactivate_{}".format(filename)) - deactivate = textwrap.dedent("""\ - echo "echo Restoring environment" > "{deactivate_file}" - for v in {vars} - do - is_defined="true" - value=$(printenv $v) || is_defined="" || true - if [ -n "$value" ] || [ -n "$is_defined" ] - then - echo export "$v='$value'" >> "{deactivate_file}" - else - echo unset $v >> "{deactivate_file}" - fi - done - """.format(deactivate_file=deactivate_file, vars=" ".join(values.keys()))) - return deactivate + deactivate_file = os.path.join("$script_folder", "deactivate_{}".format(filename)) + return textwrap.dedent(f"""\ + echo "echo Restoring environment" > "{deactivate_file}" + for v in {vars} + do + is_defined="true" + value=$(printenv $v) || is_defined="" || true + if [ -n "$value" ] || [ -n "$is_defined" ] + then + echo export "$v='$value'" >> "{deactivate_file}" + else + echo unset $v >> "{deactivate_file}" + fi + done + """) def _ps1_deactivate_contents(use_deactivate_function, values, filename, verbose): + vars_list = ", ".join(f'"{v}"' for v in values.keys()) if use_deactivate_function: - vars_list = ", ".join(f'"{v}"' for v in values.keys()) var_prefix = _old_env_prefix(filename) func_name = _deactivate_func_name(filename) - - deactivate = textwrap.dedent(f"""\ + return textwrap.dedent(f"""\ function global:deactivate_{func_name} {{ Write-Host "Restoring environment" foreach ($v in @({vars_list})) {{ @@ -652,30 +646,28 @@ def _ps1_deactivate_contents(use_deactivate_function, values, filename, verbose) Remove-Item -Path function:deactivate_{func_name} -ErrorAction SilentlyContinue }} """) - else: - deactivate_file = "deactivate_{}".format(filename) - deactivate = textwrap.dedent("""\ - Push-Location $PSScriptRoot - "echo `"Restoring environment`"" | Out-File -FilePath "{deactivate_file}" - $vars = (Get-ChildItem env:*).name - $updated_vars = @({vars}) - foreach ($var in $updated_vars) + deactivate_file = "deactivate_{}".format(filename) + return textwrap.dedent(f"""\ + Push-Location $PSScriptRoot + "echo `"Restoring environment`"" | Out-File -FilePath "{deactivate_file}" + $vars = (Get-ChildItem env:*).name + $updated_vars = @({vars_list}) + + foreach ($var in $updated_vars) + {{ + if ($var -in $vars) {{ - if ($var -in $vars) - {{ - $var_value = (Get-ChildItem env:$var).value - Add-Content "{deactivate_file}" "`n`$env:$var = `"$var_value`"" - }} - else - {{ - Add-Content "{deactivate_file}" "`nif (Test-Path env:$var) {{ Remove-Item env:$var }}" - }} + $var_value = (Get-ChildItem env:$var).value + Add-Content "{deactivate_file}" "`n`$env:$var = `"$var_value`"" + }} + else + {{ + Add-Content "{deactivate_file}" "`nif (Test-Path env:$var) {{ Remove-Item env:$var }}" }} - Pop-Location - """).format(deactivate_file=deactivate_file, - vars=",".join(['"{}"'.format(var) for var in values.keys()])) - return deactivate + }} + Pop-Location + """) class ProfileEnvironment: From 54156733e61d83d5576471827ad123d4bbaf5f4d Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Tue, 21 Oct 2025 18:32:52 +0200 Subject: [PATCH 21/27] Fix bug --- conan/tools/env/environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index 930b04663f5..fef6b025128 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -609,7 +609,7 @@ def _sh_deactivate_contents(use_deactivate_function, values, filename, verbose): deactivate_file = os.path.join("$script_folder", "deactivate_{}".format(filename)) return textwrap.dedent(f"""\ echo "echo Restoring environment" > "{deactivate_file}" - for v in {vars} + for v in {vars_list} do is_defined="true" value=$(printenv $v) || is_defined="" || true From 26be3c09e96bbec2422c5a5f8562c81e0fce06e7 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Wed, 22 Oct 2025 17:33:38 +0200 Subject: [PATCH 22/27] Remove all extra verbosity for this PR --- conan/internal/api/install/generators.py | 6 ++---- conan/tools/env/environment.py | 8 -------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/conan/internal/api/install/generators.py b/conan/internal/api/install/generators.py index 1f593c76974..2ad8195790e 100644 --- a/conan/internal/api/install/generators.py +++ b/conan/internal/api/install/generators.py @@ -213,8 +213,7 @@ def sh_content(files): content += f"\n\ndeactivate_conan{group}() {{\n" for deactivate_name in deactivate_function_names(shs): content += f" deactivate_{deactivate_name}\n" - content += (f" unset -f deactivate_conan{group}\n}}\n" - f'echo "Environment activated. Run \"deactivate_conan{group}\" to restore."\n') + content += f" unset -f deactivate_conan{group}\n}}\n" return content filename = "conan{}.sh".format(group) generated.append(filename) @@ -239,8 +238,7 @@ def ps1_content(files): content += f" deactivate_{deactivate_name}\n" content += (f" Remove-Item -Path function:deactivate_conan{group} " "-ErrorAction SilentlyContinue" - "\n}\n" - f'echo \'Environment activated. Run \"deactivate_conan{group}\" to restore.\'\n') + "\n}\n") return content filename = "conan{}.ps1".format(group) generated.append(filename) diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index fef6b025128..2c2aea12210 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -473,8 +473,6 @@ def save_ps1(self, file_location, generate_deactivate=True): if value: value = value.replace('"', '`"') # escape quotes result.append(f'$env:{varname}="{value}"') - if self._verbose: - result.append(f'Write-Host "Setting {varname} to: {value}"') else: result.append('if (Test-Path env:{0}) {{ Remove-Item env:{0} }}'.format(varname)) @@ -505,8 +503,6 @@ def save_sh(self, file_location, generate_deactivate=True): ) if value: result.append(f'export {varname}="{value}"') - if self._verbose: - result.append(f'echo "Setting {varname} to: {value}"') else: result.append(f'unset {varname}') @@ -595,10 +591,8 @@ def _sh_deactivate_contents(use_deactivate_function, values, filename, verbose): eval "is_set=\\${{${{old_var}}+x}}" if [ -n "${{is_set}}" ]; then eval "old_value=\\${{${{old_var}}}}" - {'echo "Restoring ${{v}} to ${{old_value}}"' if verbose else ''} eval "export ${{v}}=\\${{old_value}}" else - {'echo "Unsetting ${{v}}"' if verbose else ''} unset "${{v}}" fi unset "${{old_var}}" @@ -635,10 +629,8 @@ def _ps1_deactivate_contents(use_deactivate_function, values, filename, verbose) $oldVarName = "{var_prefix}_$v" $oldValue = Get-Item -Path "Env:$oldVarName" -ErrorAction SilentlyContinue if (Test-Path env:$oldValue) {{ - {'Write-Host "Unsetting $v"' if verbose else ''} Remove-Item -Path "Env:$v" -ErrorAction SilentlyContinue }} else {{ - {'Write-Host "Restoring $v to $($oldValue.Value)"' if verbose else ''} Set-Item -Path "Env:$v" -Value $oldValue.Value }} Remove-Item -Path "Env:$oldVarName" -ErrorAction SilentlyContinue From 2a684a5cb99501d576d59fd1b03201c7e120ed96 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Wed, 22 Oct 2025 18:17:27 +0200 Subject: [PATCH 23/27] Renamed conf to new_deactivate --- conan/internal/api/install/generators.py | 3 +- conan/internal/model/conf.py | 2 +- conan/tools/env/environment.py | 87 ++++++++++++------------ test/integration/environment/test_env.py | 8 +-- 4 files changed, 48 insertions(+), 52 deletions(-) diff --git a/conan/internal/api/install/generators.py b/conan/internal/api/install/generators.py index 2ad8195790e..42f6c78260e 100644 --- a/conan/internal/api/install/generators.py +++ b/conan/internal/api/install/generators.py @@ -186,8 +186,7 @@ def deactivate_function_names(filenames): return [os.path.splitext(os.path.basename(s))[0].replace("-", "_") for s in reversed(filenames)] - use_deactivate_function = conanfile.conf.get("tools.env:deactivate_function", default=False, - check_type=bool) + use_deactivate_function = conanfile.conf.get("tools.env:new_deactivate", default=False, check_type=bool) generated = [] for group, env_scripts in conanfile.env_scripts.items(): subsystem = deduce_subsystem(conanfile, group) diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py index 38ebc93647c..6c286af5743 100644 --- a/conan/internal/model/conf.py +++ b/conan/internal/model/conf.py @@ -135,7 +135,7 @@ "tools.apple:enable_visibility": "(boolean) Enable/Disable Visibility Apple Clang flags", "tools.env.virtualenv:powershell": "If specified, it generates PowerShell launchers (.ps1). Use this configuration setting the PowerShell executable you want to use (e.g., 'powershell.exe' or 'pwsh'). Setting it to True or False is deprecated as of Conan 2.11.0.", "tools.env:dotenv": "(Experimental) Generate dotenv environment files", - "tools.env:deactivate_function": "(Experimental) Generate a deactivate function instead of a script to unset the environment variables", + "tools.env:new_deactivate": "(Experimental) Generate a deactivate function instead of a script to unset the environment variables", # Compilers/Flags configurations "tools.build:compiler_executables": "Defines a Python dict-like with the compilers path to be used. Allowed keys {'c', 'cpp', 'cuda', 'objc', 'objcxx', 'rc', 'fortran', 'asm', 'hip', 'ispc'}", "tools.build:cxxflags": "List of extra CXX flags used by different toolchains like CMakeToolchain, AutotoolsToolchain and MesonToolchain", diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index 2c2aea12210..d13d810d1ff 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -459,8 +459,7 @@ def save_ps1(self, file_location, generate_deactivate=True): result = [] if generate_deactivate: - result.append(_ps1_deactivate_contents(self._use_deactivate_function, self._values, - filename, self._verbose)) + result.append(_ps1_deactivate_contents(self._use_deactivate_function, self._values, filename)) abs_base_path, new_path = relativize_paths(self._conanfile, "$PSScriptRoot") for varname, varvalues in self._values.items(): value = varvalues.get_str("$env:{name}", subsystem=self._subsystem, pathsep=self._pathsep, @@ -487,8 +486,7 @@ def save_sh(self, file_location, generate_deactivate=True): filepath, filename = os.path.split(file_location) result = [] if generate_deactivate: - result.append(_sh_deactivate_contents(self._use_deactivate_function, self._values, - filename, self._verbose)) + result.append(_sh_deactivate_contents(self._use_deactivate_function, self._values, filename)) abs_base_path, new_path = relativize_paths(self._conanfile, "$script_folder") for varname, varvalues in self._values.items(): value = varvalues.get_str("${name}", self._subsystem, pathsep=self._pathsep, @@ -577,47 +575,7 @@ def _old_env_prefix(filename): return f"_CONAN_OLD_{_deactivate_func_name(filename).upper()}" -def _sh_deactivate_contents(use_deactivate_function, values, filename, verbose): - vars_list = " ".join(quote(v) for v in values.keys()) - if use_deactivate_function: - func_name = _deactivate_func_name(filename) - return textwrap.dedent(f"""\ - # sh-like function to restore environment - deactivate_{func_name} () {{ - echo "Restoring environment" - for v in {vars_list}; do - old_var="{_old_env_prefix(filename)}_${{v}}" - # Use eval for indirect expansion (POSIX safe) - eval "is_set=\\${{${{old_var}}+x}}" - if [ -n "${{is_set}}" ]; then - eval "old_value=\\${{${{old_var}}}}" - eval "export ${{v}}=\\${{old_value}}" - else - unset "${{v}}" - fi - unset "${{old_var}}" - done - unset -f deactivate_{func_name} - }} - """) - deactivate_file = os.path.join("$script_folder", "deactivate_{}".format(filename)) - return textwrap.dedent(f"""\ - echo "echo Restoring environment" > "{deactivate_file}" - for v in {vars_list} - do - is_defined="true" - value=$(printenv $v) || is_defined="" || true - if [ -n "$value" ] || [ -n "$is_defined" ] - then - echo export "$v='$value'" >> "{deactivate_file}" - else - echo unset $v >> "{deactivate_file}" - fi - done - """) - - -def _ps1_deactivate_contents(use_deactivate_function, values, filename, verbose): +def _ps1_deactivate_contents(use_deactivate_function, values, filename): vars_list = ", ".join(f'"{v}"' for v in values.keys()) if use_deactivate_function: var_prefix = _old_env_prefix(filename) @@ -661,6 +619,45 @@ def _ps1_deactivate_contents(use_deactivate_function, values, filename, verbose) Pop-Location """) +def _sh_deactivate_contents(use_deactivate_function, values, filename): + vars_list = " ".join(quote(v) for v in values.keys()) + if use_deactivate_function: + func_name = _deactivate_func_name(filename) + return textwrap.dedent(f"""\ + # sh-like function to restore environment + deactivate_{func_name} () {{ + echo "Restoring environment" + for v in {vars_list}; do + old_var="{_old_env_prefix(filename)}_${{v}}" + # Use eval for indirect expansion (POSIX safe) + eval "is_set=\\${{${{old_var}}+x}}" + if [ -n "${{is_set}}" ]; then + eval "old_value=\\${{${{old_var}}}}" + eval "export ${{v}}=\\${{old_value}}" + else + unset "${{v}}" + fi + unset "${{old_var}}" + done + unset -f deactivate_{func_name} + }} + """) + deactivate_file = os.path.join("$script_folder", "deactivate_{}".format(filename)) + return textwrap.dedent(f"""\ + echo "echo Restoring environment" > "{deactivate_file}" + for v in {vars_list} + do + is_defined="true" + value=$(printenv $v) || is_defined="" || true + if [ -n "$value" ] || [ -n "$is_defined" ] + then + echo export "$v='$value'" >> "{deactivate_file}" + else + echo unset $v >> "{deactivate_file}" + fi + done + """) + class ProfileEnvironment: def __init__(self): diff --git a/test/integration/environment/test_env.py b/test/integration/environment/test_env.py index 129520b5362..9846272c272 100644 --- a/test/integration/environment/test_env.py +++ b/test/integration/environment/test_env.py @@ -514,7 +514,7 @@ def generate(self): "display.bat": display_bat, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) - client.run(f"install . -c=tools.env:deactivate_function={use_function}") + client.run(f"install . -c=tools.env:new_deactivate={use_function}") for _ in range(2): # Just repeat it, so we can check things keep working if platform.system() == "Windows": @@ -561,7 +561,7 @@ def generate(self): "display.bat": display_bat, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) - client.run(f"install . -c=tools.env:deactivate_function={use_function}") + client.run(f"install . -c=tools.env:new_deactivate={use_function}") for _ in range(2): # Just repeat it, so we can check things keep working if platform.system() == "Windows": @@ -627,7 +627,7 @@ def generate(self): client.save({"conanfile.py": conanfile, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) - client.run(f"install . -c=tools.env:deactivate_function={use_function}") + client.run(f"install . -c=tools.env:new_deactivate={use_function}") deactivate_cmd = "deactivate_conanbuild" if use_function else ". ./deactivate_conanbuild.sh" cmd = (f'export FOO=&& export BAZ=Value3 && export FOOBAR=' @@ -728,7 +728,7 @@ def test_profile_build_env_spaces(use_function): "display.bat": display_bat, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) - client.run(f"install . -g VirtualBuildEnv -pr=profile -c=tools.env:deactivate_function={use_function}") + client.run(f"install . -g VirtualBuildEnv -pr=profile -c=tools.env:new_deactivate={use_function}") if platform.system() == "Windows": cmd = "conanbuild.bat && display.bat && deactivate_conanbuild.bat && display.bat" From 45c735f551b350dd1ea24707f9847d6cb72b582d Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Wed, 22 Oct 2025 18:19:01 +0200 Subject: [PATCH 24/27] Removed unneded _verbose property --- conan/tools/env/environment.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index d13d810d1ff..4148e30c19e 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -4,7 +4,7 @@ from collections import OrderedDict from contextlib import contextmanager -from conan.api.output import ConanOutput, LEVEL_VERBOSE +from conan.api.output import ConanOutput from conan.internal.api.install.generators import relativize_paths from conan.internal.subsystems import deduce_subsystem, WINDOWS, subsystem_path from conan.errors import ConanException @@ -348,7 +348,6 @@ def __init__(self, conanfile, values, scope): self._subsystem = deduce_subsystem(conanfile, scope) self._use_deactivate_function = conanfile.conf.get("tools.env:deactivate_function", default=False, check_type=bool) - self._verbose = conanfile.output.level_allowed(LEVEL_VERBOSE) @property def _pathsep(self): From 3395893aa07f3417ee8c04aa02188f3fbdb7f225 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Thu, 23 Oct 2025 12:05:01 +0200 Subject: [PATCH 25/27] Update config name and fix tests --- conan/internal/api/install/generators.py | 10 +++---- conan/internal/model/conf.py | 2 +- conan/tools/env/environment.py | 19 ++++++------- test/integration/environment/test_env.py | 32 +++++++++++----------- test/unittests/tools/env/test_env_files.py | 20 +++++++------- 5 files changed, 41 insertions(+), 42 deletions(-) diff --git a/conan/internal/api/install/generators.py b/conan/internal/api/install/generators.py index 42f6c78260e..329aea9df0d 100644 --- a/conan/internal/api/install/generators.py +++ b/conan/internal/api/install/generators.py @@ -186,7 +186,7 @@ def deactivate_function_names(filenames): return [os.path.splitext(os.path.basename(s))[0].replace("-", "_") for s in reversed(filenames)] - use_deactivate_function = conanfile.conf.get("tools.env:new_deactivate", default=False, check_type=bool) + deactivation = conanfile.conf.get("tools.env:deactivate", default=None, check_type=str) generated = [] for group, env_scripts in conanfile.env_scripts.items(): subsystem = deduce_subsystem(conanfile, group) @@ -208,7 +208,7 @@ def deactivate_function_names(filenames): if shs: def sh_content(files): content = ". " + " && . ".join('"{}"'.format(s) for s in files) - if use_deactivate_function: + if deactivation == "function": content += f"\n\ndeactivate_conan{group}() {{\n" for deactivate_name in deactivate_function_names(shs): content += f" deactivate_{deactivate_name}\n" @@ -217,7 +217,7 @@ def sh_content(files): filename = "conan{}.sh".format(group) generated.append(filename) save(os.path.join(conanfile.generators_folder, filename), sh_content(shs)) - if not use_deactivate_function: + if not deactivation: save(os.path.join(conanfile.generators_folder, "deactivate_{}".format(filename)), sh_content(deactivates(shs))) if bats: @@ -231,7 +231,7 @@ def bat_content(files): if ps1s: def ps1_content(files): content = "\r\n".join(['& "{}"'.format(b) for b in files]) - if use_deactivate_function: + if deactivation == "function": content += f"\n\nfunction global:deactivate_conan{group} {{\n" for deactivate_name in deactivate_function_names(ps1s): content += f" deactivate_{deactivate_name}\n" @@ -242,7 +242,7 @@ def ps1_content(files): filename = "conan{}.ps1".format(group) generated.append(filename) save(os.path.join(conanfile.generators_folder, filename), ps1_content(ps1s)) - if not use_deactivate_function: + if not deactivation: save(os.path.join(conanfile.generators_folder, "deactivate_{}".format(filename)), ps1_content(deactivates(ps1s))) if generated: diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py index 6c286af5743..fae57a1714d 100644 --- a/conan/internal/model/conf.py +++ b/conan/internal/model/conf.py @@ -135,7 +135,7 @@ "tools.apple:enable_visibility": "(boolean) Enable/Disable Visibility Apple Clang flags", "tools.env.virtualenv:powershell": "If specified, it generates PowerShell launchers (.ps1). Use this configuration setting the PowerShell executable you want to use (e.g., 'powershell.exe' or 'pwsh'). Setting it to True or False is deprecated as of Conan 2.11.0.", "tools.env:dotenv": "(Experimental) Generate dotenv environment files", - "tools.env:new_deactivate": "(Experimental) Generate a deactivate function instead of a script to unset the environment variables", + "tools.env:deactivate": "(Experimental) If 'function', generate a deactivate function instead of a script to unset the environment variables", # Compilers/Flags configurations "tools.build:compiler_executables": "Defines a Python dict-like with the compilers path to be used. Allowed keys {'c', 'cpp', 'cuda', 'objc', 'objcxx', 'rc', 'fortran', 'asm', 'hip', 'ispc'}", "tools.build:cxxflags": "List of extra CXX flags used by different toolchains like CMakeToolchain, AutotoolsToolchain and MesonToolchain", diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index 4148e30c19e..c5d37e7da0e 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -346,8 +346,7 @@ def __init__(self, conanfile, values, scope): self._conanfile = conanfile self._scope = scope self._subsystem = deduce_subsystem(conanfile, scope) - self._use_deactivate_function = conanfile.conf.get("tools.env:deactivate_function", - default=False, check_type=bool) + self._deactivate_method = conanfile.conf.get("tools.env:deactivate", default=None, check_type=str) @property def _pathsep(self): @@ -458,12 +457,12 @@ def save_ps1(self, file_location, generate_deactivate=True): result = [] if generate_deactivate: - result.append(_ps1_deactivate_contents(self._use_deactivate_function, self._values, filename)) + result.append(_ps1_deactivate_contents(self._deactivate_method, self._values, filename)) abs_base_path, new_path = relativize_paths(self._conanfile, "$PSScriptRoot") for varname, varvalues in self._values.items(): value = varvalues.get_str("$env:{name}", subsystem=self._subsystem, pathsep=self._pathsep, root_path=abs_base_path, script_path=new_path) - if generate_deactivate and self._use_deactivate_function: + if generate_deactivate and self._deactivate_method == "function": # Check environment variable existence before saving value result.append( f'if ($env:{varname}) {{ $env:{_old_env_prefix(filename)}_{varname} = $env:{varname} }}' @@ -485,13 +484,13 @@ def save_sh(self, file_location, generate_deactivate=True): filepath, filename = os.path.split(file_location) result = [] if generate_deactivate: - result.append(_sh_deactivate_contents(self._use_deactivate_function, self._values, filename)) + result.append(_sh_deactivate_contents(self._deactivate_method, self._values, filename)) abs_base_path, new_path = relativize_paths(self._conanfile, "$script_folder") for varname, varvalues in self._values.items(): value = varvalues.get_str("${name}", self._subsystem, pathsep=self._pathsep, root_path=abs_base_path, script_path=new_path) value = value.replace('"', '\\"') - if generate_deactivate and self._use_deactivate_function: + if generate_deactivate and self._deactivate_method == "function": # Check environment variable existence before saving value result.append( f'if [ -n "${{{varname}+x}}" ]; then ' @@ -574,9 +573,9 @@ def _old_env_prefix(filename): return f"_CONAN_OLD_{_deactivate_func_name(filename).upper()}" -def _ps1_deactivate_contents(use_deactivate_function, values, filename): +def _ps1_deactivate_contents(deactivate_method, values, filename): vars_list = ", ".join(f'"{v}"' for v in values.keys()) - if use_deactivate_function: + if deactivate_method == "function": var_prefix = _old_env_prefix(filename) func_name = _deactivate_func_name(filename) return textwrap.dedent(f"""\ @@ -618,9 +617,9 @@ def _ps1_deactivate_contents(use_deactivate_function, values, filename): Pop-Location """) -def _sh_deactivate_contents(use_deactivate_function, values, filename): +def _sh_deactivate_contents(deactivate_method, values, filename): vars_list = " ".join(quote(v) for v in values.keys()) - if use_deactivate_function: + if deactivate_method == "function": func_name = _deactivate_func_name(filename) return textwrap.dedent(f"""\ # sh-like function to restore environment diff --git a/test/integration/environment/test_env.py b/test/integration/environment/test_env.py index 9846272c272..2ce874f4e5c 100644 --- a/test/integration/environment/test_env.py +++ b/test/integration/environment/test_env.py @@ -486,8 +486,8 @@ class Pkg(ConanFile): assert "LD_LIBRARY_PATH" in conanrunenv -@pytest.mark.parametrize("use_function", [True, False]) -def test_multiple_deactivate(use_function): +@pytest.mark.parametrize("deactivate", ["function", None]) +def test_multiple_deactivate(deactivate): conanfile = textwrap.dedent(r""" from conan import ConanFile from conan.tools.env import Environment @@ -514,13 +514,13 @@ def generate(self): "display.bat": display_bat, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) - client.run(f"install . -c=tools.env:new_deactivate={use_function}") + client.run(f"install . {f'-c=tools.env:deactivate={deactivate}' if deactivate else ''} ") for _ in range(2): # Just repeat it, so we can check things keep working if platform.system() == "Windows": cmd = "conanbuild.bat && display.bat && deactivate_conanbuild.bat && display.bat" else: - deactivate_cmd = "deactivate_conanbuild" if use_function else ". ./deactivate_conanbuild.sh" + deactivate_cmd = "deactivate_conanbuild" if deactivate else ". ./deactivate_conanbuild.sh" cmd = f'. ./conanbuild.sh && ./display.sh && {deactivate_cmd} && ./display.sh' out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() @@ -532,8 +532,8 @@ def generate(self): assert "VAR2=!!" in out -@pytest.mark.parametrize("use_function", [True, False]) -def test_multiple_deactivate_order(use_function): +@pytest.mark.parametrize("deactivate", ["function", None]) +def test_multiple_deactivate_order(deactivate): """ https://github.com/conan-io/conan/issues/13693 """ @@ -561,13 +561,13 @@ def generate(self): "display.bat": display_bat, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) - client.run(f"install . -c=tools.env:new_deactivate={use_function}") + client.run(f"install . {f'-c=tools.env:deactivate={deactivate}' if deactivate else ''} ") for _ in range(2): # Just repeat it, so we can check things keep working if platform.system() == "Windows": cmd = "conanbuild.bat && display.bat && deactivate_conanbuild.bat && display.bat" else: - deactivate_cmd = "deactivate_conanbuild" if use_function else ". ./deactivate_conanbuild.sh" + deactivate_cmd = "deactivate_conanbuild" if deactivate else ". ./deactivate_conanbuild.sh" cmd = f'. ./conanbuild.sh && ./display.sh && {deactivate_cmd} && ./display.sh' out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() @@ -578,8 +578,8 @@ def generate(self): @pytest.mark.skipif(platform.system() == "Windows", reason="Shell script test") -@pytest.mark.parametrize("use_function", [True, False]) -def test_deactivate_missing_vars_stay_missing(use_function): +@pytest.mark.parametrize("deactivate", ["function", None]) +def test_deactivate_missing_vars_stay_missing(deactivate): """ Tests that these two cases preserve variable status 1. export FOO= @@ -627,9 +627,9 @@ def generate(self): client.save({"conanfile.py": conanfile, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) - client.run(f"install . -c=tools.env:new_deactivate={use_function}") + client.run(f"install . {f'-c=tools.env:deactivate={deactivate}' if deactivate else ''} ") - deactivate_cmd = "deactivate_conanbuild" if use_function else ". ./deactivate_conanbuild.sh" + deactivate_cmd = "deactivate_conanbuild" if deactivate else ". ./deactivate_conanbuild.sh" cmd = (f'export FOO=&& export BAZ=Value3 && export FOOBAR=' f'&& . ./conanbuild.sh && {deactivate_cmd} && ./display.sh') out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -713,8 +713,8 @@ class Pkg(ConanFile): assert "MYTOOL {}!!".format(i) in client.out -@pytest.mark.parametrize("use_function", [True, False]) -def test_profile_build_env_spaces(use_function): +@pytest.mark.parametrize("deactivate", ["function", None]) +def test_profile_build_env_spaces(deactivate): display_bat = textwrap.dedent("""\ @echo off echo VAR1=%VAR1%!! @@ -728,12 +728,12 @@ def test_profile_build_env_spaces(use_function): "display.bat": display_bat, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) - client.run(f"install . -g VirtualBuildEnv -pr=profile -c=tools.env:new_deactivate={use_function}") + client.run(f"install . -g VirtualBuildEnv -pr=profile {f'-c=tools.env:deactivate={deactivate}' if deactivate else ''} ") if platform.system() == "Windows": cmd = "conanbuild.bat && display.bat && deactivate_conanbuild.bat && display.bat" else: - deactivate_cmd = "deactivate_conanbuild" if use_function else ". ./deactivate_conanbuild.sh" + deactivate_cmd = "deactivate_conanbuild" if deactivate else ". ./deactivate_conanbuild.sh" cmd = f'. ./conanbuild.sh && ./display.sh && {deactivate_cmd} && ./display.sh' out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() diff --git a/test/unittests/tools/env/test_env_files.py b/test/unittests/tools/env/test_env_files.py index cfc10311ade..17da15f19be 100644 --- a/test/unittests/tools/env/test_env_files.py +++ b/test/unittests/tools/env/test_env_files.py @@ -103,8 +103,8 @@ def test_env_files_bat(env, prevenv): @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") -@pytest.mark.parametrize("use_function", [True, False]) -def test_env_files_ps1(env, prevenv, use_function): +@pytest.mark.parametrize("deactivate", ["function", None]) +def test_env_files_ps1(env, prevenv, deactivate): prevenv.update(dict(os.environ.copy())) display = textwrap.dedent("""\ @@ -123,20 +123,20 @@ def test_env_files_ps1(env, prevenv, use_function): with chdir(temp_folder()): conanfile = ConanFileMock() - if use_function: - conanfile.conf.define("tools.env:deactivate_function", True) + if deactivate: + conanfile.conf.define("tools.env:deactivate", deactivate) env = env.vars(conanfile) env._subsystem = WINDOWS env.save_ps1("test.ps1") save("display.ps1", display) - deactivate_cmd = "deactivate_test" if use_function else ".\\deactivate_test.ps1" + deactivate_cmd = "deactivate_test" if deactivate else ".\\deactivate_test.ps1" cmd = f"powershell.exe .\\test.ps1 ; .\\display.ps1 ; {deactivate_cmd} ; .\\display.ps1" check_env_files_output(cmd, prevenv) @pytest.mark.skipif(platform.system() == "Windows", reason="Not in Windows") -@pytest.mark.parametrize("use_function", [True, False]) -def test_env_files_sh(env, prevenv, use_function): +@pytest.mark.parametrize("deactivate", ["function", None]) +def test_env_files_sh(env, prevenv, deactivate): display = textwrap.dedent("""\ echo MyVar=$MyVar!! echo MyVar1=$MyVar1!! @@ -153,14 +153,14 @@ def test_env_files_sh(env, prevenv, use_function): with chdir(temp_folder()): conanfile = ConanFileMock() - if use_function: - conanfile.conf.define("tools.env:deactivate_function", True) + if deactivate: + conanfile.conf.define("tools.env:deactivate", deactivate) env = env.vars(conanfile) env.save_sh("test.sh") save("display.sh", display) os.chmod("display.sh", 0o777) # We include the "set -e" to test it is robust against errors - deactivate_cmd = "deactivate_test" if use_function else ". ./deactivate_test.sh" + deactivate_cmd = "deactivate_test" if deactivate else ". ./deactivate_test.sh" cmd = f'set -e && . ./test.sh && ./display.sh && {deactivate_cmd} && ./display.sh' check_env_files_output(cmd, prevenv) From ac4207c5a3aca4d01764ae8a23e05ce47ebe3267 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Thu, 23 Oct 2025 12:34:24 +0200 Subject: [PATCH 26/27] Fix windows tests --- test/unittests/tools/env/test_env.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/unittests/tools/env/test_env.py b/test/unittests/tools/env/test_env.py index 88f00ffe0fa..710ae55566f 100644 --- a/test/unittests/tools/env/test_env.py +++ b/test/unittests/tools/env/test_env.py @@ -232,8 +232,8 @@ def test_windows_case_insensitive_bat(envvars): @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") -@pytest.mark.parametrize("deactivate_function", [True, False],) -def test_windows_case_insensitive_ps1(envvars, deactivate_function): +@pytest.mark.parametrize("deactivate", ["function", None],) +def test_windows_case_insensitive_ps1(envvars, deactivate): display = textwrap.dedent("""\ echo "MyVar=$env:MyVar!!" echo "MyVar1=$env:MyVar1!!" @@ -246,10 +246,10 @@ def test_windows_case_insensitive_ps1(envvars, deactivate_function): prevenv.update(dict(os.environ.copy())) with chdir(temp_folder()): - envvars._use_deactivate_function = deactivate_function + envvars._deactivate_method = deactivate envvars.save_ps1("test.ps1") save("display.ps1", display) - deactivate_cmd = "deactivate_test" if deactivate_function else ".\\deactivate_test.ps1" + deactivate_cmd = "deactivate_test" if deactivate else ".\\deactivate_test.ps1" cmd = f"powershell.exe .\\test.ps1 ; .\\display.ps1 ; {deactivate_cmd} ; .\\display.ps1" check_command_output(cmd, prevenv) From 976d3830b5cc072adcfe38f6a3b581ff50748b7b Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Thu, 23 Oct 2025 12:52:47 +0200 Subject: [PATCH 27/27] Renamed conf again --- conan/internal/api/install/generators.py | 10 +++---- conan/internal/model/conf.py | 2 +- conan/tools/env/environment.py | 18 ++++++------ test/integration/environment/test_env.py | 32 +++++++++++----------- test/unittests/tools/env/test_env.py | 8 +++--- test/unittests/tools/env/test_env_files.py | 20 +++++++------- 6 files changed, 45 insertions(+), 45 deletions(-) diff --git a/conan/internal/api/install/generators.py b/conan/internal/api/install/generators.py index 329aea9df0d..87865d8f9fd 100644 --- a/conan/internal/api/install/generators.py +++ b/conan/internal/api/install/generators.py @@ -186,7 +186,7 @@ def deactivate_function_names(filenames): return [os.path.splitext(os.path.basename(s))[0].replace("-", "_") for s in reversed(filenames)] - deactivation = conanfile.conf.get("tools.env:deactivate", default=None, check_type=str) + deactivation_mode = conanfile.conf.get("tools.env:deactivation_mode", default=None, check_type=str) generated = [] for group, env_scripts in conanfile.env_scripts.items(): subsystem = deduce_subsystem(conanfile, group) @@ -208,7 +208,7 @@ def deactivate_function_names(filenames): if shs: def sh_content(files): content = ". " + " && . ".join('"{}"'.format(s) for s in files) - if deactivation == "function": + if deactivation_mode == "function": content += f"\n\ndeactivate_conan{group}() {{\n" for deactivate_name in deactivate_function_names(shs): content += f" deactivate_{deactivate_name}\n" @@ -217,7 +217,7 @@ def sh_content(files): filename = "conan{}.sh".format(group) generated.append(filename) save(os.path.join(conanfile.generators_folder, filename), sh_content(shs)) - if not deactivation: + if not deactivation_mode: save(os.path.join(conanfile.generators_folder, "deactivate_{}".format(filename)), sh_content(deactivates(shs))) if bats: @@ -231,7 +231,7 @@ def bat_content(files): if ps1s: def ps1_content(files): content = "\r\n".join(['& "{}"'.format(b) for b in files]) - if deactivation == "function": + if deactivation_mode == "function": content += f"\n\nfunction global:deactivate_conan{group} {{\n" for deactivate_name in deactivate_function_names(ps1s): content += f" deactivate_{deactivate_name}\n" @@ -242,7 +242,7 @@ def ps1_content(files): filename = "conan{}.ps1".format(group) generated.append(filename) save(os.path.join(conanfile.generators_folder, filename), ps1_content(ps1s)) - if not deactivation: + if not deactivation_mode: save(os.path.join(conanfile.generators_folder, "deactivate_{}".format(filename)), ps1_content(deactivates(ps1s))) if generated: diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py index fae57a1714d..fcf11ad2748 100644 --- a/conan/internal/model/conf.py +++ b/conan/internal/model/conf.py @@ -135,7 +135,7 @@ "tools.apple:enable_visibility": "(boolean) Enable/Disable Visibility Apple Clang flags", "tools.env.virtualenv:powershell": "If specified, it generates PowerShell launchers (.ps1). Use this configuration setting the PowerShell executable you want to use (e.g., 'powershell.exe' or 'pwsh'). Setting it to True or False is deprecated as of Conan 2.11.0.", "tools.env:dotenv": "(Experimental) Generate dotenv environment files", - "tools.env:deactivate": "(Experimental) If 'function', generate a deactivate function instead of a script to unset the environment variables", + "tools.env:deactivation_mode": "(Experimental) If 'function', generate a deactivate function instead of a script to unset the environment variables", # Compilers/Flags configurations "tools.build:compiler_executables": "Defines a Python dict-like with the compilers path to be used. Allowed keys {'c', 'cpp', 'cuda', 'objc', 'objcxx', 'rc', 'fortran', 'asm', 'hip', 'ispc'}", "tools.build:cxxflags": "List of extra CXX flags used by different toolchains like CMakeToolchain, AutotoolsToolchain and MesonToolchain", diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index c5d37e7da0e..676950b794d 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -346,7 +346,7 @@ def __init__(self, conanfile, values, scope): self._conanfile = conanfile self._scope = scope self._subsystem = deduce_subsystem(conanfile, scope) - self._deactivate_method = conanfile.conf.get("tools.env:deactivate", default=None, check_type=str) + self._deactivation_mode = conanfile.conf.get("tools.env:deactivation_mode", default=None, check_type=str) @property def _pathsep(self): @@ -457,12 +457,12 @@ def save_ps1(self, file_location, generate_deactivate=True): result = [] if generate_deactivate: - result.append(_ps1_deactivate_contents(self._deactivate_method, self._values, filename)) + result.append(_ps1_deactivate_contents(self._deactivation_mode, self._values, filename)) abs_base_path, new_path = relativize_paths(self._conanfile, "$PSScriptRoot") for varname, varvalues in self._values.items(): value = varvalues.get_str("$env:{name}", subsystem=self._subsystem, pathsep=self._pathsep, root_path=abs_base_path, script_path=new_path) - if generate_deactivate and self._deactivate_method == "function": + if generate_deactivate and self._deactivation_mode == "function": # Check environment variable existence before saving value result.append( f'if ($env:{varname}) {{ $env:{_old_env_prefix(filename)}_{varname} = $env:{varname} }}' @@ -484,13 +484,13 @@ def save_sh(self, file_location, generate_deactivate=True): filepath, filename = os.path.split(file_location) result = [] if generate_deactivate: - result.append(_sh_deactivate_contents(self._deactivate_method, self._values, filename)) + result.append(_sh_deactivate_contents(self._deactivation_mode, self._values, filename)) abs_base_path, new_path = relativize_paths(self._conanfile, "$script_folder") for varname, varvalues in self._values.items(): value = varvalues.get_str("${name}", self._subsystem, pathsep=self._pathsep, root_path=abs_base_path, script_path=new_path) value = value.replace('"', '\\"') - if generate_deactivate and self._deactivate_method == "function": + if generate_deactivate and self._deactivation_mode == "function": # Check environment variable existence before saving value result.append( f'if [ -n "${{{varname}+x}}" ]; then ' @@ -573,9 +573,9 @@ def _old_env_prefix(filename): return f"_CONAN_OLD_{_deactivate_func_name(filename).upper()}" -def _ps1_deactivate_contents(deactivate_method, values, filename): +def _ps1_deactivate_contents(deactivation_mode, values, filename): vars_list = ", ".join(f'"{v}"' for v in values.keys()) - if deactivate_method == "function": + if deactivation_mode == "function": var_prefix = _old_env_prefix(filename) func_name = _deactivate_func_name(filename) return textwrap.dedent(f"""\ @@ -617,9 +617,9 @@ def _ps1_deactivate_contents(deactivate_method, values, filename): Pop-Location """) -def _sh_deactivate_contents(deactivate_method, values, filename): +def _sh_deactivate_contents(deactivation_mode, values, filename): vars_list = " ".join(quote(v) for v in values.keys()) - if deactivate_method == "function": + if deactivation_mode == "function": func_name = _deactivate_func_name(filename) return textwrap.dedent(f"""\ # sh-like function to restore environment diff --git a/test/integration/environment/test_env.py b/test/integration/environment/test_env.py index 2ce874f4e5c..945352597fa 100644 --- a/test/integration/environment/test_env.py +++ b/test/integration/environment/test_env.py @@ -486,8 +486,8 @@ class Pkg(ConanFile): assert "LD_LIBRARY_PATH" in conanrunenv -@pytest.mark.parametrize("deactivate", ["function", None]) -def test_multiple_deactivate(deactivate): +@pytest.mark.parametrize("deactivation_mode", ["function", None]) +def test_multiple_deactivate(deactivation_mode): conanfile = textwrap.dedent(r""" from conan import ConanFile from conan.tools.env import Environment @@ -514,13 +514,13 @@ def generate(self): "display.bat": display_bat, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) - client.run(f"install . {f'-c=tools.env:deactivate={deactivate}' if deactivate else ''} ") + client.run(f"install . {f'-c=tools.env:deactivation_mode={deactivation_mode}' if deactivation_mode else ''} ") for _ in range(2): # Just repeat it, so we can check things keep working if platform.system() == "Windows": cmd = "conanbuild.bat && display.bat && deactivate_conanbuild.bat && display.bat" else: - deactivate_cmd = "deactivate_conanbuild" if deactivate else ". ./deactivate_conanbuild.sh" + deactivate_cmd = "deactivate_conanbuild" if deactivation_mode else ". ./deactivate_conanbuild.sh" cmd = f'. ./conanbuild.sh && ./display.sh && {deactivate_cmd} && ./display.sh' out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() @@ -532,8 +532,8 @@ def generate(self): assert "VAR2=!!" in out -@pytest.mark.parametrize("deactivate", ["function", None]) -def test_multiple_deactivate_order(deactivate): +@pytest.mark.parametrize("deactivation_mode", ["function", None]) +def test_multiple_deactivate_order(deactivation_mode): """ https://github.com/conan-io/conan/issues/13693 """ @@ -561,13 +561,13 @@ def generate(self): "display.bat": display_bat, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) - client.run(f"install . {f'-c=tools.env:deactivate={deactivate}' if deactivate else ''} ") + client.run(f"install . {f'-c=tools.env:deactivation_mode={deactivation_mode}' if deactivation_mode else ''} ") for _ in range(2): # Just repeat it, so we can check things keep working if platform.system() == "Windows": cmd = "conanbuild.bat && display.bat && deactivate_conanbuild.bat && display.bat" else: - deactivate_cmd = "deactivate_conanbuild" if deactivate else ". ./deactivate_conanbuild.sh" + deactivate_cmd = "deactivate_conanbuild" if deactivation_mode else ". ./deactivate_conanbuild.sh" cmd = f'. ./conanbuild.sh && ./display.sh && {deactivate_cmd} && ./display.sh' out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() @@ -578,8 +578,8 @@ def generate(self): @pytest.mark.skipif(platform.system() == "Windows", reason="Shell script test") -@pytest.mark.parametrize("deactivate", ["function", None]) -def test_deactivate_missing_vars_stay_missing(deactivate): +@pytest.mark.parametrize("deactivation_mode", ["function", None]) +def test_deactivate_missing_vars_stay_missing(deactivation_mode): """ Tests that these two cases preserve variable status 1. export FOO= @@ -627,9 +627,9 @@ def generate(self): client.save({"conanfile.py": conanfile, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) - client.run(f"install . {f'-c=tools.env:deactivate={deactivate}' if deactivate else ''} ") + client.run(f"install . {f'-c=tools.env:deactivation_mode={deactivation_mode}' if deactivation_mode else ''} ") - deactivate_cmd = "deactivate_conanbuild" if deactivate else ". ./deactivate_conanbuild.sh" + deactivate_cmd = "deactivate_conanbuild" if deactivation_mode else ". ./deactivate_conanbuild.sh" cmd = (f'export FOO=&& export BAZ=Value3 && export FOOBAR=' f'&& . ./conanbuild.sh && {deactivate_cmd} && ./display.sh') out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -713,8 +713,8 @@ class Pkg(ConanFile): assert "MYTOOL {}!!".format(i) in client.out -@pytest.mark.parametrize("deactivate", ["function", None]) -def test_profile_build_env_spaces(deactivate): +@pytest.mark.parametrize("deactivation_mode", ["function", None]) +def test_profile_build_env_spaces(deactivation_mode): display_bat = textwrap.dedent("""\ @echo off echo VAR1=%VAR1%!! @@ -728,12 +728,12 @@ def test_profile_build_env_spaces(deactivate): "display.bat": display_bat, "display.sh": display_sh}) os.chmod(os.path.join(client.current_folder, "display.sh"), 0o777) - client.run(f"install . -g VirtualBuildEnv -pr=profile {f'-c=tools.env:deactivate={deactivate}' if deactivate else ''} ") + client.run(f"install . -g VirtualBuildEnv -pr=profile {f'-c=tools.env:deactivation_mode={deactivation_mode}' if deactivation_mode else ''} ") if platform.system() == "Windows": cmd = "conanbuild.bat && display.bat && deactivate_conanbuild.bat && display.bat" else: - deactivate_cmd = "deactivate_conanbuild" if deactivate else ". ./deactivate_conanbuild.sh" + deactivate_cmd = "deactivate_conanbuild" if deactivation_mode else ". ./deactivate_conanbuild.sh" cmd = f'. ./conanbuild.sh && ./display.sh && {deactivate_cmd} && ./display.sh' out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=client.current_folder).communicate() diff --git a/test/unittests/tools/env/test_env.py b/test/unittests/tools/env/test_env.py index 710ae55566f..282a5f40831 100644 --- a/test/unittests/tools/env/test_env.py +++ b/test/unittests/tools/env/test_env.py @@ -232,8 +232,8 @@ def test_windows_case_insensitive_bat(envvars): @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") -@pytest.mark.parametrize("deactivate", ["function", None],) -def test_windows_case_insensitive_ps1(envvars, deactivate): +@pytest.mark.parametrize("deactivation_mode", ["function", None],) +def test_windows_case_insensitive_ps1(envvars, deactivation_mode): display = textwrap.dedent("""\ echo "MyVar=$env:MyVar!!" echo "MyVar1=$env:MyVar1!!" @@ -246,10 +246,10 @@ def test_windows_case_insensitive_ps1(envvars, deactivate): prevenv.update(dict(os.environ.copy())) with chdir(temp_folder()): - envvars._deactivate_method = deactivate + envvars._deactivation_mode = deactivation_mode envvars.save_ps1("test.ps1") save("display.ps1", display) - deactivate_cmd = "deactivate_test" if deactivate else ".\\deactivate_test.ps1" + deactivate_cmd = "deactivate_test" if deactivation_mode else ".\\deactivate_test.ps1" cmd = f"powershell.exe .\\test.ps1 ; .\\display.ps1 ; {deactivate_cmd} ; .\\display.ps1" check_command_output(cmd, prevenv) diff --git a/test/unittests/tools/env/test_env_files.py b/test/unittests/tools/env/test_env_files.py index 17da15f19be..9218910f1d2 100644 --- a/test/unittests/tools/env/test_env_files.py +++ b/test/unittests/tools/env/test_env_files.py @@ -103,8 +103,8 @@ def test_env_files_bat(env, prevenv): @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") -@pytest.mark.parametrize("deactivate", ["function", None]) -def test_env_files_ps1(env, prevenv, deactivate): +@pytest.mark.parametrize("deactivation_mode", ["function", None]) +def test_env_files_ps1(env, prevenv, deactivation_mode): prevenv.update(dict(os.environ.copy())) display = textwrap.dedent("""\ @@ -123,20 +123,20 @@ def test_env_files_ps1(env, prevenv, deactivate): with chdir(temp_folder()): conanfile = ConanFileMock() - if deactivate: - conanfile.conf.define("tools.env:deactivate", deactivate) + if deactivation_mode: + conanfile.conf.define("tools.env:deactivation_mode", deactivation_mode) env = env.vars(conanfile) env._subsystem = WINDOWS env.save_ps1("test.ps1") save("display.ps1", display) - deactivate_cmd = "deactivate_test" if deactivate else ".\\deactivate_test.ps1" + deactivate_cmd = "deactivate_test" if deactivation_mode else ".\\deactivate_test.ps1" cmd = f"powershell.exe .\\test.ps1 ; .\\display.ps1 ; {deactivate_cmd} ; .\\display.ps1" check_env_files_output(cmd, prevenv) @pytest.mark.skipif(platform.system() == "Windows", reason="Not in Windows") -@pytest.mark.parametrize("deactivate", ["function", None]) -def test_env_files_sh(env, prevenv, deactivate): +@pytest.mark.parametrize("deactivation_mode", ["function", None]) +def test_env_files_sh(env, prevenv, deactivation_mode): display = textwrap.dedent("""\ echo MyVar=$MyVar!! echo MyVar1=$MyVar1!! @@ -153,14 +153,14 @@ def test_env_files_sh(env, prevenv, deactivate): with chdir(temp_folder()): conanfile = ConanFileMock() - if deactivate: - conanfile.conf.define("tools.env:deactivate", deactivate) + if deactivation_mode: + conanfile.conf.define("tools.env:deactivation_mode", deactivation_mode) env = env.vars(conanfile) env.save_sh("test.sh") save("display.sh", display) os.chmod("display.sh", 0o777) # We include the "set -e" to test it is robust against errors - deactivate_cmd = "deactivate_test" if deactivate else ". ./deactivate_test.sh" + deactivate_cmd = "deactivate_test" if deactivation_mode else ". ./deactivate_test.sh" cmd = f'set -e && . ./test.sh && ./display.sh && {deactivate_cmd} && ./display.sh' check_env_files_output(cmd, prevenv)