Repository: conan-io/conan
Pull request #12230 (2022-10-02 21:58:52)

new profile [runenv] section
Changelog: Feature: Implement a new ``[runenv]`` section in the Profile, to define the runtime environment
Docs: https://github.com/conan-io/docs/pull/XXXX

I hit this limitation while preparing docs for Windows-CLang integration:

- When building for Msys2-Clang, I can add ``[buildenv]...<path/to/clang/bin>`` so the clang compiler is found
- But when I am executing a ``test_package`` ``test()`` or something in the "run" scope, like ``self.run("myexe", env="conanrun")`` it will fail, because it cannot find the ``libc++.dll`` and ``libunwind.dll`` shared libs in the msys2 clang folder.



memsharded: > Is this with a clang-cl.exe or clang++.exe that are not part of a "system" package or similar? Otherwise I'm surprised libc++.dll is not already found in the "default" search path in this environment (although I'm not very familiar with msys2)

Yes, the msys2 clang++.exe can be installed in a custom location, like ``C:/msys2/clang64/bin/clang++.exe``. This ``C:/msys2/clang64/bin`` folder can be indeed added to the system PATH and everything will work. But if this folder is not added, and it is defined in the Conan profile (which makes sense if we are dealing with multiple compilers and versions), then, the ``libc++.dll`` will not be in the path either.

This behavior is hidden because in test we add the ``pytest.mark.tool("msys2")`` to the test, which injects the path to msys2 during the whole test.

Patch:
diff --git a/conan/tools/env/virtualrunenv.py b/conan/tools/env/virtualrunenv.py
index af342e16d..529aadbda 100644
--- a/conan/tools/env/virtualrunenv.py
+++ b/conan/tools/env/virtualrunenv.py
@@ -55,7 +55,10 @@ class VirtualRunEnv:
         very occasional
         """
         runenv = Environment()
-        # FIXME: Missing profile info
+
+        # Top priority: profile
+        profile_env = self._conanfile.runenv
+        runenv.compose_env(profile_env)
         # FIXME: Cache value?
 
         host_req = self._conanfile.dependencies.host
diff --git a/conans/client/loader.py b/conans/client/loader.py
index 804d73b50..73d798e49 100644
--- a/conans/client/loader.py
+++ b/conans/client/loader.py
@@ -207,7 +207,7 @@ class ConanFileLoader(object):
             if pkg_settings:
                 tmp_settings.update_values(pkg_settings)
 
-        conanfile.initialize(tmp_settings, profile.env_values, profile.buildenv)
+        conanfile.initialize(tmp_settings, profile.env_values, profile.buildenv, profile.runenv)
         conanfile.conf = profile.conf.get_conanfile_conf(ref_str)
 
     def load_consumer(self, conanfile_path, profile_host, name=None, version=None, user=None,
@@ -293,7 +293,7 @@ class ConanFileLoader(object):
             pkg_settings = package_settings_values.get("&")
             if pkg_settings:
                 tmp_settings.update_values(pkg_settings)
-        conanfile.initialize(Settings(), profile.env_values, profile.buildenv)
+        conanfile.initialize(Settings(), profile.env_values, profile.buildenv, profile.runenv)
         conanfile.conf = profile.conf.get_conanfile_conf(None)
         # It is necessary to copy the settings, because the above is only a constraint of
         # conanfile settings, and a txt doesn't define settings. Necessary for generators,
@@ -344,7 +344,7 @@ class ConanFileLoader(object):
         # for the reference (keep compatibility)
         conanfile = ConanFile(self._output, self._runner, display_name="virtual")
         conanfile.initialize(profile_host.processed_settings.copy(),
-                             profile_host.env_values, profile_host.buildenv)
+                             profile_host.env_values, profile_host.buildenv, profile_host.runenv)
         conanfile.conf = profile_host.conf.get_conanfile_conf(None)
         conanfile.settings = profile_host.processed_settings.copy_values()
 
diff --git a/conans/client/profile_loader.py b/conans/client/profile_loader.py
index 74c0dca15..b484f68af 100644
--- a/conans/client/profile_loader.py
+++ b/conans/client/profile_loader.py
@@ -162,7 +162,7 @@ def _load_profile(text, profile_path, default_folder):
         # Current profile before update with parents (but parent variables already applied)
         doc = ConfigParser(profile_parser.profile_text,
                            allowed_fields=["build_requires", "tool_requires", "settings", "env",
-                                           "options", "conf", "buildenv"])
+                                           "options", "conf", "buildenv", "runenv"])
 
         # Merge the inherited profile with the readed from current profile
         _apply_inner_profile(doc, inherited_profile)
@@ -245,6 +245,10 @@ def _apply_inner_profile(doc, base_profile):
         buildenv = ProfileEnvironment.loads(doc.buildenv)
         base_profile.buildenv.update_profile_env(buildenv)
 
+    if doc.runenv:
+        runenv = ProfileEnvironment.loads(doc.runenv)
+        base_profile.runenv.update_profile_env(runenv)
+
 
 def profile_from_args(profiles, settings, options, env, conf, cwd, cache, build_profile=False):
     """ Return a Profile object, as the result of merging a potentially existing Profile
diff --git a/conans/model/conan_file.py b/conans/model/conan_file.py
index 69fe57e1a..4e09e5f40 100644
--- a/conans/model/conan_file.py
+++ b/conans/model/conan_file.py
@@ -163,6 +163,7 @@ class ConanFile(object):
         # At the moment only for build_requires, others will be ignored
         self.conf_info = Conf()
         self._conan_buildenv = None  # The profile buildenv, will be assigned initialize()
+        self._conan_runenv = None
         self._conan_node = None  # access to container Node object, to access info, context, deps...
         self._conan_new_cpp_info = None   # Will be calculated lazy in the getter
         self._conan_dependencies = None
@@ -209,8 +210,19 @@ class ConanFile(object):
             self._conan_buildenv = self._conan_buildenv.get_profile_env(ref_str)
         return self._conan_buildenv
 
-    def initialize(self, settings, env, buildenv=None):
+    @property
+    def runenv(self):
+        # Lazy computation of the package runenv based on the profile one
+        from conan.tools.env import Environment
+        if not isinstance(self._conan_runenv, Environment):
+            # TODO: missing user/channel
+            ref_str = "{}/{}".format(self.name, self.version)
+            self._conan_runenv = self._conan_runenv.get_profile_env(ref_str)
+        return self._conan_runenv
+
+    def initialize(self, settings, env, buildenv=None, runenv=None):
         self._conan_buildenv = buildenv
+        self._conan_runenv = runenv
         if isinstance(self.generators, str):
             self.generators = [self.generators]
         # User defined options
diff --git a/conans/model/profile.py b/conans/model/profile.py
index f5112a41e..b27714bd5 100644
--- a/conans/model/profile.py
+++ b/conans/model/profile.py
@@ -24,6 +24,7 @@ class Profile(object):
         self.build_requires = OrderedDict()  # ref pattern: list of ref
         self.conf = ConfDefinition()
         self.buildenv = ProfileEnvironment()
+        self.runenv = ProfileEnvironment()
 
         # Cached processed values
         self.processed_settings = None  # Settings with values, and smart completion
@@ -94,6 +95,10 @@ class Profile(object):
             result.append("[buildenv]")
             result.append(self.buildenv.dumps())
 
+        if self.runenv:
+            result.append("[runenv]")
+            result.append(self.runenv.dumps())
+
         return "\n".join(result).replace("\n\n", "\n")
 
     def compose_profile(self, other):
@@ -121,6 +126,7 @@ class Profile(object):
 
         self.conf.update_conf_definition(other.conf)
         self.buildenv.update_profile_env(other.buildenv)  # Profile composition, last has priority
+        self.runenv.update_profile_env(other.runenv)
 
     def update_settings(self, new_settings):
         """Mix the specified settings with the current profile.


Test patch:
diff --git a/conans/test/integration/environment/test_runenv_profile.py b/conans/test/integration/environment/test_runenv_profile.py
new file mode 100644
index 000000000..d05b8f910
--- /dev/null
+++ b/conans/test/integration/environment/test_runenv_profile.py
@@ -0,0 +1,36 @@
+import textwrap
+
+import pytest
+
+from conans.test.utils.tools import TestClient
+
+
+@pytest.fixture
+def client():
+    conanfile = textwrap.dedent("""
+       from conan import ConanFile
+       class Pkg(ConanFile):
+           generators = "VirtualRunEnv"
+           def generate(self):
+               for var in (1, 2):
+                   v = self.runenv.vars(self).get("MyVar{}".format(var))
+                   self.output.info("MyVar{}={}!!".format(var, v))
+       """)
+    profile1 = textwrap.dedent("""
+      [runenv]
+      MyVar1=MyValue1_1
+      MyVar2=MyValue2_1
+      """)
+    client = TestClient()
+    client.save({"conanfile.py": conanfile,
+                 "profile1": profile1})
+    return client
+
+
+def test_buildenv_profile_cli(client):
+    client.run("install . -pr=profile1")
+    assert "conanfile.py: MyVar1=MyValue1_1!!" in client.out
+    assert "conanfile.py: MyVar2=MyValue2_1!!" in client.out
+    env = client.load("conanrunenv.sh")
+    assert "MyValue1_1" in env
+    assert "MyValue2_1" in env