Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to use modified environment in generate() method in CMakePresets #15470

Merged
merged 12 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -220,8 +222,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