Skip to content

Commit

Permalink
Ability to use modified environment in generate() method in CMakePres…
Browse files Browse the repository at this point in the history
…ets (#15470)

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* fix test

* wip

* refactor test

* minor changes
  • Loading branch information
czoido authored Jan 26, 2024
1 parent 5a8cc86 commit ba78f3f
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 107 deletions.
6 changes: 4 additions & 2 deletions conan/tools/cmake/toolchain/toolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ def __init__(self, conanfile, generator=None):
self.find_builddirs = True
self.user_presets_path = "CMakeUserPresets.json"
self.presets_prefix = "conan"
self.presets_build_environment = None
self.presets_run_environment = None

def _context(self):
""" Returns dict, the context for the template
Expand Down Expand Up @@ -232,8 +234,8 @@ def generate(self):
if self._conanfile.conf.get("tools.cmake.cmaketoolchain:presets_environment", default="",
check_type=str, choices=("disabled", "")) != "disabled":

build_env = VirtualBuildEnv(self._conanfile, auto_generate=True).vars()
run_env = VirtualRunEnv(self._conanfile, auto_generate=True).vars()
build_env = self.presets_build_environment.vars(self._conanfile) if self.presets_build_environment else VirtualBuildEnv(self._conanfile, auto_generate=True).vars()
run_env = self.presets_run_environment.vars(self._conanfile) if self.presets_run_environment else VirtualRunEnv(self._conanfile, auto_generate=True).vars()

buildenv = {name: value for name, value in
build_env.items(variable_reference="$penv{{{name}}}")}
Expand Down
20 changes: 12 additions & 8 deletions conan/tools/env/virtualbuildenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class VirtualBuildEnv:
"""

def __init__(self, conanfile, auto_generate=False):
self._buildenv = None
self._conanfile = conanfile
if not auto_generate:
self._conanfile.virtualbuildenv = False
Expand Down Expand Up @@ -42,33 +43,36 @@ def environment(self):
:return: an ``Environment`` object instance containing the obtained variables.
"""
# FIXME: Cache value?
build_env = Environment()

if self._buildenv is None:
self._buildenv = Environment()
else:
return self._buildenv

# Top priority: profile
profile_env = self._conanfile.buildenv
build_env.compose_env(profile_env)
self._buildenv.compose_env(profile_env)

build_requires = self._conanfile.dependencies.build.topological_sort
for require, build_require in reversed(build_requires.items()):
if require.direct: # Only buildenv_info from direct deps is propagated
# higher priority, explicit buildenv_info
if build_require.buildenv_info:
build_env.compose_env(build_require.buildenv_info)
self._buildenv.compose_env(build_require.buildenv_info)
# Lower priority, the runenv of all transitive "requires" of the build requires
if build_require.runenv_info:
build_env.compose_env(build_require.runenv_info)
self._buildenv.compose_env(build_require.runenv_info)
# Then the implicit
os_name = self._conanfile.settings_build.get_safe("os")
build_env.compose_env(runenv_from_cpp_info(build_require, os_name))
self._buildenv.compose_env(runenv_from_cpp_info(build_require, os_name))

# Requires in host context can also bring some direct buildenv_info
host_requires = self._conanfile.dependencies.host.topological_sort
for require in reversed(host_requires.values()):
if require.buildenv_info:
build_env.compose_env(require.buildenv_info)
self._buildenv.compose_env(require.buildenv_info)

return build_env
return self._buildenv

def vars(self, scope="build"):
"""
Expand Down
16 changes: 10 additions & 6 deletions conan/tools/env/virtualrunenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def __init__(self, conanfile, auto_generate=False):
:param conanfile: The current recipe object. Always use ``self``.
"""
self._runenv = None
self._conanfile = conanfile
if not auto_generate:
self._conanfile.virtualrunenv = False
Expand All @@ -60,23 +61,26 @@ def environment(self):
:return: an ``Environment`` object instance containing the obtained variables.
"""
runenv = Environment()

if self._runenv is None:
self._runenv = Environment()
else:
return self._runenv

# Top priority: profile
profile_env = self._conanfile.runenv
runenv.compose_env(profile_env)
# FIXME: Cache value?
self._runenv.compose_env(profile_env)

host_req = self._conanfile.dependencies.host
test_req = self._conanfile.dependencies.test
for require, dep in list(host_req.items()) + list(test_req.items()):
if dep.runenv_info:
runenv.compose_env(dep.runenv_info)
self._runenv.compose_env(dep.runenv_info)
if require.run: # Only if the require is run (shared or application to be run)
_os = self._conanfile.settings.get_safe("os")
runenv.compose_env(runenv_from_cpp_info(dep, _os))
self._runenv.compose_env(runenv_from_cpp_info(dep, _os))

return runenv
return self._runenv

def vars(self, scope="run"):
"""
Expand Down
256 changes: 165 additions & 91 deletions conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -1496,114 +1496,188 @@ def package(self):


@pytest.mark.tool("cmake", "3.23")
def test_add_env_to_presets():
c = TestClient()
class TestEnvironmentInPresets:
@pytest.fixture(scope="class")
def _init_client(self):
c = TestClient()
tool = textwrap.dedent(r"""
import os
from conan import ConanFile
from conan.tools.files import chdir, save
class Tool(ConanFile):
version = "0.1"
settings = "os", "compiler", "arch", "build_type"
def package(self):
with chdir(self, self.package_folder):
save(self, f"bin/{{self.name}}.bat", f"@echo off\necho running: {{self.name}}/{{self.version}}")
save(self, f"bin/{{self.name}}.sh", f"echo running: {{self.name}}/{{self.version}}")
os.chmod(f"bin/{{self.name}}.sh", 0o777)
def package_info(self):
self.buildenv_info.define("MY_BUILD_VAR", "MY_BUILDVAR_VALUE")
{}
""")

tool = textwrap.dedent(r"""
import os
from conan import ConanFile
from conan.tools.files import chdir, save
class Tool(ConanFile):
version = "0.1"
settings = "os", "compiler", "arch", "build_type"
def package(self):
with chdir(self, self.package_folder):
save(self, f"bin/{{self.name}}.bat", f"@echo off\necho running: {{self.name}}/{{self.version}}")
save(self, f"bin/{{self.name}}.sh", f"echo running: {{self.name}}/{{self.version}}")
os.chmod(f"bin/{{self.name}}.sh", 0o777)
def package_info(self):
self.buildenv_info.define("MY_BUILD_VAR", "MY_BUILDVAR_VALUE")
{}
consumer = textwrap.dedent("""
[tool_requires]
mytool/0.1
[test_requires]
mytesttool/0.1
[layout]
cmake_layout
""")

consumer = textwrap.dedent("""
[tool_requires]
mytool/0.1
[test_requires]
mytesttool/0.1
[layout]
cmake_layout
""")

test_env = textwrap.dedent("""
#include <cstdlib>
int main() {
return std::getenv("MY_RUNVAR") ? 0 : 1;
}
""")
test_env = textwrap.dedent("""
#include <cstdlib>
int main() {
return std::getenv("MY_RUNVAR") ? 0 : 1;
}
""")

cmakelists = textwrap.dedent("""
cmake_minimum_required(VERSION 3.15)
project(MyProject)
if(WIN32)
set(MYTOOL_SCRIPT "mytool.bat")
else()
set(MYTOOL_SCRIPT "mytool.sh")
endif()
add_custom_target(run_mytool COMMAND ${MYTOOL_SCRIPT})
# build var should be available at configure
set(MY_BUILD_VAR $ENV{MY_BUILD_VAR})
if (MY_BUILD_VAR)
message("MY_BUILD_VAR:${MY_BUILD_VAR}")
else()
message("MY_BUILD_VAR NOT FOUND")
endif()
# run var should not be available at configure, just when testing
set(MY_RUNVAR $ENV{MY_RUNVAR})
if (MY_RUNVAR)
message("MY_RUNVAR:${MY_RUNVAR}")
else()
message("MY_RUNVAR NOT FOUND")
endif()
enable_testing()
add_executable(test_env test_env.cpp)
add_test(NAME TestRunEnv COMMAND test_env)
""")
cmakelists = textwrap.dedent("""
cmake_minimum_required(VERSION 3.15)
project(MyProject)
if(WIN32)
set(MYTOOL_SCRIPT "mytool.bat")
else()
set(MYTOOL_SCRIPT "mytool.sh")
endif()
add_custom_target(run_mytool COMMAND ${MYTOOL_SCRIPT})
function(check_and_report_variable var_name)
set(var_value $ENV{${var_name}})
if (var_value)
message("${var_name}:${var_value}")
else()
message("${var_name} NOT FOUND")
endif()
endfunction()
check_and_report_variable("MY_BUILD_VAR")
check_and_report_variable("MY_RUNVAR")
check_and_report_variable("MY_ENV_VAR")
enable_testing()
add_executable(test_env test_env.cpp)
add_test(NAME TestRunEnv COMMAND test_env)
""")

c.save({"tool.py": tool.format(""),
"test_tool.py": tool.format('self.runenv_info.define("MY_RUNVAR", "MY_RUNVAR_VALUE")'),
"conanfile.txt": consumer,
"CMakeLists.txt": cmakelists,
"test_env.cpp": test_env})
c.save({"tool.py": tool.format(""),
"test_tool.py": tool.format('self.runenv_info.define("MY_RUNVAR", "MY_RUNVAR_VALUE")'),
"conanfile.txt": consumer,
"CMakeLists.txt": cmakelists,
"test_env.cpp": test_env})

c.run("create tool.py --name=mytool")
c.run("create tool.py --name=mytool")

c.run("create test_tool.py --name=mytesttool")
c.run("create test_tool.py --name=mytesttool -s build_type=Debug")
c.run("create test_tool.py --name=mytesttool")
c.run("create test_tool.py --name=mytesttool -s build_type=Debug")

# do a first conan install with env disabled just to test that the conf works
c.run("install . -g CMakeToolchain -g CMakeDeps -c tools.cmake.cmaketoolchain:presets_environment=disabled")
yield c

presets_path = os.path.join("build", "Release", "generators", "CMakePresets.json") \
if platform.system() != "Windows" else os.path.join("build", "generators", "CMakePresets.json")
presets = json.loads(c.load(presets_path))
def test_add_env_to_presets(self, _init_client):
c = _init_client

assert presets["configurePresets"][0].get("env") is None
# do a first conan install with env disabled just to test that the conf works
c.run("install . -g CMakeToolchain -g CMakeDeps "
"-c tools.cmake.cmaketoolchain:presets_environment=disabled")

c.run("install . -g CMakeToolchain -g CMakeDeps")
c.run("install . -g CMakeToolchain -g CMakeDeps -s:h build_type=Debug")
presets_path = os.path.join("build", "Release", "generators", "CMakePresets.json") \
if platform.system() != "Windows" else os.path.join("build", "generators",
"CMakePresets.json")
presets = json.loads(c.load(presets_path))

# test that the buildenv is correctly injected to configure and build steps
# that the runenv is not injected to configure, but it is when running tests
assert presets["configurePresets"][0].get("env") is None

preset = "conan-default" if platform.system() == "Windows" else "conan-release"
c.run("install . -g CMakeToolchain -g CMakeDeps")
c.run("install . -g CMakeToolchain -g CMakeDeps -s:h build_type=Debug")

c.run_command(f"cmake --preset {preset}")
assert "MY_BUILD_VAR:MY_BUILDVAR_VALUE" in c.out
assert "MY_RUNVAR NOT FOUND" in c.out
c.run_command("cmake --build --preset conan-release --target run_mytool --target test_env")
assert "running: mytool/0.1" in c.out
# test that the buildenv is correctly injected to configure and build steps
# that the runenv is not injected to configure, but it is when running tests

c.run_command("ctest --preset conan-release")
assert "tests passed" in c.out
preset = "conan-default" if platform.system() == "Windows" else "conan-release"

if platform.system() != "Windows":
c.run_command("cmake --preset conan-debug")
c.run_command(f"cmake --preset {preset}")
assert "MY_BUILD_VAR:MY_BUILDVAR_VALUE" in c.out
assert "MY_RUNVAR NOT FOUND" in c.out
c.run_command("cmake --build --preset conan-release --target run_mytool --target test_env")
assert "running: mytool/0.1" in c.out

c.run_command("ctest --preset conan-release")
assert "tests passed" in c.out

if platform.system() != "Windows":
c.run_command("cmake --preset conan-debug")
assert "MY_BUILD_VAR:MY_BUILDVAR_VALUE" in c.out
assert "MY_RUNVAR NOT FOUND" in c.out

c.run_command("cmake --build --preset conan-debug --target run_mytool --target test_env")
assert "running: mytool/0.1" in c.out

c.run_command("ctest --preset conan-debug")
assert "tests passed" in c.out

def test_add_generate_env_to_presets(self, _init_client):
c = _init_client

consumer = textwrap.dedent("""
from conan import ConanFile
from conan.tools.cmake import cmake_layout, CMakeToolchain, CMakeDeps
from conan.tools.env import VirtualBuildEnv, VirtualRunEnv, Environment
class mypkgRecipe(ConanFile):
name = "mypkg"
version = "1.0"
settings = "os", "compiler", "build_type", "arch"
tool_requires = "mytool/0.1"
test_requires = "mytesttool/0.1"
def layout(self):
cmake_layout(self)
def generate(self):
buildenv = VirtualBuildEnv(self)
buildenv.environment().define("MY_BUILD_VAR", "MY_BUILDVAR_VALUE_OVERRIDEN")
buildenv.generate()
runenv = VirtualRunEnv(self)
runenv.environment().define("MY_RUN_VAR", "MY_RUNVAR_SET_IN_GENERATE")
runenv.generate()
env = Environment()
env.define("MY_ENV_VAR", "MY_ENV_VAR_VALUE")
env = env.vars(self)
env.save_script("other_env")
deps = CMakeDeps(self)
deps.generate()
tc = CMakeToolchain(self)
tc.presets_build_environment = buildenv.environment().compose_env(env)
tc.presets_run_environment = runenv.environment()
tc.generate()
""")

test_env = textwrap.dedent("""
#include <string>
int main() {
return std::string(std::getenv("MY_RUNVAR"))=="MY_RUNVAR_SET_IN_GENERATE";
}
""")

c.save({"conanfile.py": consumer,
"test_env.cpp": test_env})

c.run("install conanfile.py")

preset = "conan-default" if platform.system() == "Windows" else "conan-release"

c.run_command(f"cmake --preset {preset}")
assert "MY_BUILD_VAR:MY_BUILDVAR_VALUE_OVERRIDEN" in c.out
assert "MY_ENV_VAR:MY_ENV_VAR_VALUE" in c.out

c.run_command("cmake --build --preset conan-debug --target run_mytool --target test_env")
assert "running: mytool/0.1" in c.out
c.run_command("cmake --build --preset conan-release --target run_mytool --target test_env")
assert "running: mytool/0.1" in c.out

c.run_command("ctest --preset conan-debug")
assert "tests passed" in c.out
c.run_command(f"ctest --preset conan-release")
assert "tests passed" in c.out

0 comments on commit ba78f3f

Please sign in to comment.