Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f5660eb
Second attemp for virtual deactivate functions
AbrilRBS Oct 15, 2025
413d407
Fix test, cleanup
AbrilRBS Oct 15, 2025
fe0b2c8
Fix indent
AbrilRBS Oct 15, 2025
b7af331
Fix tests, remove those having to deal with deactivate locations (new…
AbrilRBS Oct 15, 2025
3039ef2
Add conf toggle
AbrilRBS Oct 15, 2025
8f26548
Bring back tests
AbrilRBS Oct 15, 2025
610ace9
Fix reported name when more than 1 env export the same value
AbrilRBS Oct 15, 2025
d4634db
Make shell scripts more robust and sh-like
perseoGI Oct 15, 2025
49d8df3
Powershell
AbrilRBS Oct 16, 2025
4a1e65b
WIP (not tested): added powershell deactivate
perseoGI Oct 16, 2025
f005d88
Avoid unsetting existing empty envs
perseoGI Oct 16, 2025
ac7fad5
Add old control for ps1, enable feature by default to see what tests …
AbrilRBS Oct 16, 2025
f662dbe
Add missing arg
AbrilRBS Oct 16, 2025
d21ccc5
Fixed powershell tests and parametrize
perseoGI Oct 17, 2025
af52904
Cleanup code, add test for missing sh var
AbrilRBS Oct 20, 2025
b884ed8
More cases
AbrilRBS Oct 20, 2025
ceac12b
Skip for windows, this is only a shell test
AbrilRBS Oct 20, 2025
58ab106
Remove global deactivate function
AbrilRBS Oct 20, 2025
870540b
Simplify
AbrilRBS Oct 20, 2025
3e8da30
Improved UX
perseoGI Oct 21, 2025
5415673
Fix bug
perseoGI Oct 21, 2025
26be3c0
Remove all extra verbosity for this PR
perseoGI Oct 22, 2025
2a684a5
Renamed conf to new_deactivate
perseoGI Oct 22, 2025
45c735f
Removed unneded _verbose property
perseoGI Oct 22, 2025
3395893
Update config name and fix tests
perseoGI Oct 23, 2025
ac4207c
Fix windows tests
perseoGI Oct 23, 2025
976d383
Renamed conf again
perseoGI Oct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions conan/internal/api/install/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ def deactivates(filenames):
result.append(os.path.join(folder, "deactivate_{}".format(f)))
return result

def deactivate_function_names(filenames):
return [os.path.splitext(os.path.basename(s))[0].replace("-", "_")
for s in reversed(filenames)]

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)
Expand All @@ -202,12 +207,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)
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"
content += f" unset -f deactivate_conan{group}\n}}\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)))
if not deactivation_mode:
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])
Expand All @@ -218,12 +230,21 @@ def bat_content(files):
bat_content(deactivates(bats)))
if ps1s:
def ps1_content(files):
return "\r\n".join(['& "{}"'.format(b) for b in files])
content = "\r\n".join(['& "{}"'.format(b) for b in files])
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"
content += (f" Remove-Item -Path function:deactivate_conan{group} "
"-ErrorAction SilentlyContinue"
"\n}\n")
return content
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 deactivation_mode:
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}")
Expand Down
1 change: 1 addition & 0 deletions conan/internal/model/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: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",
Expand Down
167 changes: 116 additions & 51 deletions conan/tools/env/environment.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import textwrap
from shlex import quote
from collections import OrderedDict
from contextlib import contextmanager

Expand Down Expand Up @@ -345,6 +346,7 @@ def __init__(self, conanfile, values, scope):
self._conanfile = conanfile
self._scope = scope
self._subsystem = deduce_subsystem(conanfile, scope)
self._deactivation_mode = conanfile.conf.get("tools.env:deactivation_mode", default=None, check_type=str)

@property
def _pathsep(self):
Expand Down Expand Up @@ -450,44 +452,24 @@ 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_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()])
)

capture = textwrap.dedent("""\
{deactivate}
""").format(deactivate=deactivate if generate_deactivate else "")
result = [capture]
result = []
if generate_deactivate:
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._deactivation_mode == "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}"')
else:
result.append('if (Test-Path env:{0}) {{ Remove-Item env:{0} }}'.format(varname))

Expand All @@ -500,34 +482,25 @@ 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 = 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())))
capture = textwrap.dedent("""\
{deactivate}
""").format(deactivate=deactivate if generate_deactivate else "")
result = [capture]
result = []
if generate_deactivate:
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._deactivation_mode == "function":
# Check environment variable existence before saving value
result.append(
f'if [ -n "${{{varname}+x}}" ]; then '
f'export {_old_env_prefix(filename)}_{varname}="${{{varname}}}"; '
f'fi;'
)
if value:
result.append('export {}="{}"'.format(varname, value))
result.append(f'export {varname}="{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
Expand Down Expand Up @@ -592,6 +565,98 @@ def save_script(self, filename):
register_env_script(self._conanfile, path, self._scope)


def _deactivate_func_name(filename):
return os.path.splitext(os.path.basename(filename))[0].replace("-", "_")


def _old_env_prefix(filename):
return f"_CONAN_OLD_{_deactivate_func_name(filename).upper()}"


def _ps1_deactivate_contents(deactivation_mode, values, filename):
vars_list = ", ".join(f'"{v}"' for v in values.keys())
if deactivation_mode == "function":
var_prefix = _old_env_prefix(filename)
func_name = _deactivate_func_name(filename)
return textwrap.dedent(f"""\
function global:deactivate_{func_name} {{
Write-Host "Restoring environment"
foreach ($v in @({vars_list})) {{
$oldVarName = "{var_prefix}_$v"
$oldValue = Get-Item -Path "Env:$oldVarName" -ErrorAction SilentlyContinue
if (Test-Path env:$oldValue) {{
Remove-Item -Path "Env:$v" -ErrorAction SilentlyContinue
}} else {{
Set-Item -Path "Env:$v" -Value $oldValue.Value
}}
Remove-Item -Path "Env:$oldVarName" -ErrorAction SilentlyContinue
}}
Remove-Item -Path function:deactivate_{func_name} -ErrorAction SilentlyContinue
}}
""")

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)
{{
$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
""")

def _sh_deactivate_contents(deactivation_mode, values, filename):
vars_list = " ".join(quote(v) for v in values.keys())
if deactivation_mode == "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):
self._environments = OrderedDict()
Expand Down
2 changes: 1 addition & 1 deletion conan/tools/microsoft/visual.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +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"
message = f"[{deactivate_filename}]: *** vcvars env cannot be deactivated ***\n"
is_ps1 = filename.endswith(".ps1")
if is_ps1:
content = f"Write-Host {message}"
Expand Down
Loading
Loading