diff --git a/conan/internal/api/install/generators.py b/conan/internal/api/install/generators.py index 87865d8f9fd..a0670726a7a 100644 --- a/conan/internal/api/install/generators.py +++ b/conan/internal/api/install/generators.py @@ -2,12 +2,14 @@ import os import traceback import importlib +import textwrap from conan.internal.cache.home_paths import HomePaths from conan.internal.subsystems import deduce_subsystem, subsystem_path from conan.internal.errors import conanfile_exception_formatter from conan.errors import ConanException from conan.internal.util.files import save, mkdir, chdir +from .templates import ps_virtualenv_global_template, ps_virtualenv_call_scripts, sh_virtualenv_global_template, sh_virtualenv_call_scripts _generators = {"CMakeToolchain": "conan.tools.cmake", "CMakeDeps": "conan.tools.cmake", @@ -206,20 +208,14 @@ def deactivate_function_names(filenames): # This $PSScriptRoot uses the current script directory ps1s.append("$PSScriptRoot/"+path) if shs: - def sh_content(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 + template = sh_virtualenv_global_template if deactivation_mode == "function" else sh_virtualenv_call_scripts + content = template.render(group=group, files=shs) 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, filename), content) if not deactivation_mode: save(os.path.join(conanfile.generators_folder, "deactivate_{}".format(filename)), - sh_content(deactivates(shs))) + sh_virtualenv_call_scripts.render(files=(deactivates(shs)))) if bats: def bat_content(files): return "\r\n".join(["@echo off"] + ['call "{}"'.format(b) for b in files]) @@ -229,22 +225,14 @@ def bat_content(files): save(os.path.join(conanfile.generators_folder, "deactivate_{}".format(filename)), bat_content(deactivates(bats))) if ps1s: - def ps1_content(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 + template = ps_virtualenv_global_template if deactivation_mode == "function" else ps_virtualenv_call_scripts + content = template.render(group=group, files=ps1s) 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, filename), content) if not deactivation_mode: save(os.path.join(conanfile.generators_folder, "deactivate_{}".format(filename)), - ps1_content(deactivates(ps1s))) + ps_virtualenv_call_scripts.render(files=(deactivates(ps1s)))) if generated: conanfile.output.highlight("Generating aggregated env files") conanfile.output.info(f"Generated aggregated env files: {generated}") diff --git a/conan/internal/api/install/templates.py b/conan/internal/api/install/templates.py new file mode 100644 index 00000000000..078ee80bc9d --- /dev/null +++ b/conan/internal/api/install/templates.py @@ -0,0 +1,253 @@ +from jinja2 import Environment +import os + +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 _deactivate_function_names(filenames): + return [os.path.splitext(os.path.basename(s))[0].replace("-", "_") + for s in reversed(filenames)] + +env = Environment() +env.globals["old_env_prefix"] = _old_env_prefix +env.globals["deactivate_func_name"] = _deactivate_func_name +env.globals["deactivate_func_names"] = _deactivate_function_names +env.globals["os"] = os + +ps_virtualenv_global_template = env.from_string('''<# +.SYNOPSIS + Activates the Conan {{group}} environment for the current shell session. +.DESCRIPTION + Sets environment variables (like PATH) for the Conan {{group}} configuration. + Defines a 'deactivate_conan{{group}}' function to safely restore the original environment. +.PARAMETER Verbose + Print information about the modified variables during activation and restoration. +.EXAMPLE + .\\conan{{group}}.ps1 -Verbose +.EXAMPLE + .\\conan{{group}}.ps1 ; deactivate_conan{{group}} -Verbose +#> +# Requires PowerShell 3.0 or later + +# --- Top-Level Script Argument Handling (Enables -Verbose implicitly) --- +[CmdletBinding()] +param() + +# 1. Execute the environment setup scripts +# Note: We use @PSBoundParameters to forward all built-in and custom parameters +# (including -Verbose) to the inner script. +{% for file in files -%} +& "{{file}}" @PSBoundParameters +{% endfor %} +Write-Verbose 'Environment activated. Run "deactivate_conan{{group}}" to restore.' + + +function global:deactivate_conan{{group}} { + <# + .SYNOPSIS + Restores the environment modified by conan{{group}}.ps1 + .DESCRIPTION + Restores the PATH and other environment variables set by the Conan {{group}} activation script. + .PARAMETER Verbose + Prints information about the restored variables. + .EXAMPLE + deactivate_conan{{group}} -Verbose + #> + # CmdletBinding enables -Verbose for this function implicitly. + [CmdletBinding()] + param() + + # Call deactivation functions + {% for name in deactivate_func_names(files) -%} + & "deactivate_{{name}}" @PSBoundParameters + {% endfor %} + # Cleanup (Remove the function itself) + Remove-Item -Path function:deactivate_conan{{group}} -ErrorAction SilentlyContinue +} + +''') + +ps_virtualenv_call_scripts = env.from_string((""" +{% for file in files -%} +& "{{file}}" +{% endfor %} +""").strip()) + +sh_virtualenv_global_template = env.from_string((''' +if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then + printf "%s [-v|--verbose]\\n" "$0" + printf " Activate Conan {{group}} environment\\n" + printf " -v, --verbose Print information about the modified variables\\n" + return 0 +fi + +conan_verbose=false; [ "$1" = "-v" ] || [ "$1" = "--verbose" ] && conan_verbose=true + +{% for file in files -%} +. "{{file}}" +{% endfor %} + +deactivate_conan{{group}}() { + if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then + printf "deactivate_conan{{group}} [-v|--verbose]\\n" + printf " Restores the environment modified by conan{{group}}.sh\\n" + printf " -v, --verbose Print information about the restored variables\\n" + return 0 + fi + conan_verbose=false; [ "$1" = "-v" ] || [ "$1" = "--verbose" ] && conan_verbose=true + # Call deactivation functions + {% for name in deactivate_func_names(files) -%} + "deactivate_{{name}}" + {% endfor -%} + + # Remove the function itself + unset -f deactivate_conan{{group}} +} + +''').strip()) + +sh_virtualenv_call_scripts = env.from_string((""" +{% for file in files -%} +. "{{file}}" +{% endfor %} +""").strip()) + +ps_virtualenv_function_template = env.from_string((""" +{% if generate_deactivate -%} +{% set func_name = "deactivate_" + deactivate_func_name(filename) -%} +{% set var_prefix = old_env_prefix(filename) -%} +function global:{{func_name}} { + [CmdletBinding()] + param() + 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) { + Write-Verbose "Unsetting $v" + Remove-Item -Path "Env:$v" -ErrorAction SilentlyContinue + } else { + Write-Verbose "Restoring $v to $oldValue.Value" + Set-Item -Path "Env:$v" -Value $oldValue.Value + } + Remove-Item -Path "Env:$oldVarName" -ErrorAction SilentlyContinue + } + Remove-Item -Path function:{{func_name}} -ErrorAction SilentlyContinue +} +{% endif -%} + + +{% for varname, value in values.items() -%} +if ($env:{{varname}}) { $env:{{old_env_prefix(filename)}}_{{varname}} = $env:{{varname}} } +{% if value -%} +Write-Verbose "Exporting {{varname}}={{value}}" +$env:{{varname}}="{{value}}" +{% else -%} +if (Test-Path env:{{varname}}) { Write-Verbose "Unsetting {{varname}}"; Remove-Item env:{{varname}} } +{% endif %} +{% endfor %} +""").strip()) + +ps_virtualenv_script_template = env.from_string((""" +{% set deactivate_file = "deactivate_" + filename -%} +Push-Location $PSScriptRoot +{% if generate_deactivate -%} +"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 +{% endif %} +{% for varname, value in values.items() -%} +{% if value -%} +$env:{{varname}}="{{value}}" +{% else -%} +if (Test-Path env:{{varname}}) { Write-Verbose "Unsetting {{varname}}"; Remove-Item env:{{varname}} } +{% endif -%} +{% endfor -%} +""").strip()) + + +sh_virtualenv_function_template = env.from_string((""" +{% if generate_deactivate -%} +{% set func_name = "deactivate_" + deactivate_func_name(filename) -%} +# sh-like function to restore environment +{{func_name}} () { + echo "Restoring environment" + conan_verbose=${conan_verbose:-false} + 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}}" + "${conan_verbose}" && echo "Restoring ${v} to ${old_value}" + eval "export ${v}=\\${old_value}" + else + "${conan_verbose}" && echo "Unsetting ${v}" + unset "${v}" + fi + unset "${old_var}" + done + unset -f {{func_name}} +} +{% endif -%} + +conan_verbose=${conan_verbose:-false} + +{% for varname, value in values.items() -%} +{% raw %}if [ -n "${{% endraw %}{{ varname }}{% raw %}+x}" ]; then export {% endraw %}{{ old_env_prefix(filename) }}{% raw %}_{% endraw %}{{ varname }}{% raw %}="${{% endraw %}{{ varname }}{% raw %}}";fi{% endraw %} +{% if value -%} +${conan_verbose} && echo "Exporting {{varname}}={{value}}" +export {{varname}}="{{value}}" +{% else -%} +${conan_verbose} && echo "Unsetting {{varname}}" +unset {{varname}} +{% endif %} +{% endfor %} +""").strip()) + +sh_virtualenv_script_template = env.from_string((""" +{% if generate_deactivate -%} +script_folder="{{os.path.abspath(filepath)}}" +{% set deactivate_file = os.path.join("$script_folder", "deactivate_" + filename) -%} +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 +{% endif %} + +{% for varname, value in values.items() -%} +{% if value -%} +export {{varname}}="{{value}}" +{% else -%} +unset {{varname}} +{% endif -%} +{% endfor %} +""").strip()) + diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index 676950b794d..c8ccaf4ada3 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -6,6 +6,7 @@ from conan.api.output import ConanOutput from conan.internal.api.install.generators import relativize_paths +from conan.internal.api.install.templates import ps_virtualenv_function_template, ps_virtualenv_script_template, sh_virtualenv_function_template, sh_virtualenv_script_template from conan.internal.subsystems import deduce_subsystem, WINDOWS, subsystem_path from conan.errors import ConanException from conan.internal.model.recipe_ref import ref_matches @@ -454,56 +455,33 @@ def save_bat(self, file_location, generate_deactivate=True): def save_ps1(self, file_location, generate_deactivate=True): _, filename = os.path.split(file_location) - - 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") + values = {} 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(f'$env:{varname}="{value}"') - else: - result.append('if (Test-Path env:{0}) {{ Remove-Item env:{0} }}'.format(varname)) - - content = "\n".join(result) - # It is very important to save it correctly with utf-16, the Conan util save() is broken - # and powershell uses utf-16 files!!! + values[varname] = varvalues.get_str("$env:{name}", subsystem=self._subsystem, pathsep=self._pathsep, + root_path=abs_base_path, script_path=new_path).replace('"', '`"') # escape quotes + template = ps_virtualenv_function_template if self._deactivation_mode == "function" else ps_virtualenv_script_template + content = template.render(generate_deactivate=generate_deactivate, + values=values, + filename=filename, + vars_list=", ".join(f'"{v}"' for v in values.keys())) os.makedirs(os.path.dirname(os.path.abspath(file_location)), exist_ok=True) with open(file_location, "w", encoding="utf-16") as f: f.write(content) 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._deactivation_mode, self._values, filename)) abs_base_path, new_path = relativize_paths(self._conanfile, "$script_folder") + values = {} 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(f'export {varname}="{value}"') - else: - result.append(f'unset {varname}') - - content = "\n".join(result) - content = f'script_folder="{os.path.abspath(filepath)}"\n' + content + values[varname] = varvalues.get_str("${name}", self._subsystem, pathsep=self._pathsep, + root_path=abs_base_path, script_path=new_path).replace('"', '\\"') + template = sh_virtualenv_function_template if self._deactivation_mode == "function" else sh_virtualenv_script_template + content = template.render(generate_deactivate=generate_deactivate, + values=values, + filename=filename, + filepath=filepath, + vars_list=" ".join(quote(v) for v in values.keys())) save(file_location, content) def save_script(self, filename): @@ -565,98 +543,6 @@ 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()