Skip to content

Commit

Permalink
new tools.cmake.cmake_layout:build_folder (#15767)
Browse files Browse the repository at this point in the history
* new tools.cmake.cmake_layout:build_folder

* fixes

* add preset test

* fix

* review

* new tests

* wip
  • Loading branch information
memsharded authored Mar 13, 2024
1 parent bdefa4d commit 1a99f17
Show file tree
Hide file tree
Showing 5 changed files with 319 additions and 3 deletions.
16 changes: 14 additions & 2 deletions conan/tools/cmake/layout.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import tempfile

from conans.client.graph.graph import RECIPE_CONSUMER, RECIPE_EDITABLE
from conan.errors import ConanException
Expand Down Expand Up @@ -30,6 +31,15 @@ def cmake_layout(conanfile, generator=None, src_folder=".", build_folder="build"
except ConanException:
raise ConanException("'build_type' setting not defined, it is necessary for cmake_layout()")

try: # TODO: Refactor this repeated pattern to deduce "is-consumer"
if conanfile._conan_node.recipe in (RECIPE_CONSUMER, RECIPE_EDITABLE):
folder = "test_folder" if conanfile.tested_reference_str else "build_folder"
build_folder = conanfile.conf.get(f"tools.cmake.cmake_layout:{folder}") or build_folder
if build_folder == "$TMP" and folder == "test_folder":
build_folder = tempfile.mkdtemp()
except AttributeError:
pass

build_folder = build_folder if not subproject else os.path.join(subproject, build_folder)
config_build_folder, user_defined_build = get_build_folder_custom_vars(conanfile)
if config_build_folder:
Expand All @@ -54,7 +64,8 @@ def get_build_folder_custom_vars(conanfile):
conanfile_vars = conanfile.folders.build_folder_vars
build_vars = conanfile.conf.get("tools.cmake.cmake_layout:build_folder_vars", check_type=list)
if conanfile.tested_reference_str:
build_vars = build_vars or conanfile_vars or \
if build_vars is None: # The user can define conf build_folder_vars = [] for no vars
build_vars = conanfile_vars or \
["settings.compiler", "settings.compiler.version", "settings.arch",
"settings.compiler.cppstd", "settings.build_type", "options.shared"]
else:
Expand All @@ -63,7 +74,8 @@ def get_build_folder_custom_vars(conanfile):
except AttributeError:
is_consumer = False
if is_consumer:
build_vars = build_vars or conanfile_vars or []
if build_vars is None:
build_vars = conanfile_vars or []
else:
build_vars = conanfile_vars or []

Expand Down
5 changes: 4 additions & 1 deletion conans/client/generators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,10 @@ def relativize_paths(conanfile, placeholder):
return None, None
abs_base_path = os.path.join(abs_base_path, "") # For the trailing / to dissambiguate matches
generators_folder = conanfile.generators_folder
rel_path = os.path.relpath(abs_base_path, generators_folder)
try:
rel_path = os.path.relpath(abs_base_path, generators_folder)
except ValueError: # In case the unit in Windows is different, path cannot be made relative
return None, None
new_path = placeholder if rel_path == "." else os.path.join(placeholder, rel_path)
new_path = os.path.join(new_path, "") # For the trailing / to dissambiguate matches
return abs_base_path, new_path
2 changes: 2 additions & 0 deletions conans/model/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
"tools.cmake.cmaketoolchain:toolset_cuda": "(Experimental) Path to a CUDA toolset to use, or version if installed at the system level",
"tools.cmake.cmaketoolchain:presets_environment": "String to define wether to add or not the environment section to the CMake presets. Empty by default, will generate the environment section in CMakePresets. Can take values: 'disabled'.",
"tools.cmake.cmake_layout:build_folder_vars": "Settings and Options that will produce a different build folder and different CMake presets names",
"tools.cmake.cmake_layout:build_folder": "(Experimental) Allow configuring the base folder of the build for local builds",
"tools.cmake.cmake_layout:test_folder": "(Experimental) Allow configuring the base folder of the build for test_package",
"tools.cmake:cmake_program": "Path to CMake executable",
"tools.cmake:install_strip": "Add --strip to cmake.install()",
"tools.deployer:symlinks": "Set to False to disable deployers copying symlinks",
Expand Down
28 changes: 28 additions & 0 deletions conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from conan.tools.microsoft.visual import vcvars_command
from conans.test.assets.cmake import gen_cmakelists
from conans.test.assets.genconanfile import GenConanfile
from conans.test.utils.test_files import temp_folder
from conans.test.utils.tools import TestClient, TurboTestClient
from conans.util.files import save, load, rmdir

Expand Down Expand Up @@ -961,6 +962,33 @@ def test_cmake_presets_multiple_settings_multi_config():
assert "MSVC_LANG2017" in client.out


@pytest.mark.tool("cmake", "3.23")
@pytest.mark.skipif(platform.system() != "Windows", reason="Needs windows")
# Test both with a local folder and an absolute folder
@pytest.mark.parametrize("build", ["mybuild", "temp"])
def test_cmake_presets_build_folder(build):
client = TestClient(path_with_spaces=False)
client.run("new cmake_exe -d name=hello -d version=0.1")

build = temp_folder() if build == "temp" else build
settings_layout = f' -c tools.cmake.cmake_layout:build_folder="{build}" '\
'-c tools.cmake.cmake_layout:build_folder_vars=' \
'\'["settings.compiler.runtime", "settings.compiler.cppstd"]\''
# But If we change, for example, the cppstd and the compiler version, the toolchain
# and presets will be different, but it will be appended to the UserPresets.json
settings = "-s compiler=msvc -s compiler.version=191 -s compiler.runtime=static " \
"-s compiler.cppstd=17"
client.run("install . {} {}".format(settings, settings_layout))
assert os.path.exists(os.path.join(client.current_folder, build, "static-17", "generators"))

client.run_command("cmake . --preset conan-static-17")
client.run_command("cmake --build --preset conan-static-17-release")
client.run_command("ctest --preset conan-static-17-release")
client.run_command(f"{build}\\static-17\\Release\\hello")
assert "Hello World Release!" in client.out
assert "MSVC_LANG2017" in client.out


@pytest.mark.tool("cmake")
def test_cmaketoolchain_sysroot():
client = TestClient(path_with_spaces=False)
Expand Down
271 changes: 271 additions & 0 deletions conans/test/integration/layout/test_cmake_build_folder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
import os
import re
import textwrap

import pytest

from conans.test.assets.genconanfile import GenConanfile
from conans.test.utils.test_files import temp_folder
from conans.test.utils.tools import TestClient


@pytest.mark.parametrize("cmd", ["install", "build"])
def test_cmake_layout_build_folder(cmd):
""" testing the tools.cmake.cmake_layout:build_folder config for
both build and install commands
"""
c = TestClient()
abs_build = temp_folder()
conanfile = textwrap.dedent("""
from conan import ConanFile
from conan.tools.cmake import cmake_layout
class HelloTestConan(ConanFile):
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeToolchain"
def layout(self):
cmake_layout(self, generator="Ninja") # Ninja so same in all OSs, single-config
""")

c.save({"conanfile.py": conanfile})
c.run(f'{cmd} . -c tools.cmake.cmake_layout:build_folder="{abs_build}"')
assert os.path.exists(os.path.join(abs_build, "Release", "generators", "conan_toolchain.cmake"))
assert not os.path.exists(os.path.join(c.current_folder, "build"))

# Make sure that a non-existing folder will not fail and will be created
new_folder = os.path.join(temp_folder(), "my build")
c.run(f'{cmd} . -c tools.cmake.cmake_layout:build_folder="{new_folder}"')
assert os.path.exists(os.path.join(new_folder, "Release", "generators", "conan_toolchain.cmake"))
assert not os.path.exists(os.path.join(c.current_folder, "build"))

# Just in case we check that local build folder would be created if no arg is provided
c.run('build . ')
assert os.path.exists(os.path.join(c.current_folder, "build"))


@pytest.mark.parametrize("cmd", ["install", "build"])
def test_cmake_layout_build_folder_relative(cmd):
""" Same as above, but with a relative folder, which is relative to the conanfile
"""
c = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile
from conan.tools.cmake import cmake_layout
class HelloTestConan(ConanFile):
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeToolchain"
def layout(self):
cmake_layout(self, generator="Ninja") # Ninja so same in all OSs, single-config
""")

c.save({"pkg/conanfile.py": conanfile})
c.run(f'{cmd} pkg -c tools.cmake.cmake_layout:build_folder=mybuild')
abs_build = os.path.join(c.current_folder, "pkg", "mybuild")
assert os.path.exists(os.path.join(abs_build, "Release", "generators", "conan_toolchain.cmake"))
assert not os.path.exists(os.path.join(c.current_folder, "pkg", "build"))

# relative path, pointing to a sibling folder
c.run(f'{cmd} pkg -c tools.cmake.cmake_layout:build_folder=../mybuild')
abs_build = os.path.join(c.current_folder, "mybuild")
assert os.path.exists(os.path.join(abs_build, "Release", "generators", "conan_toolchain.cmake"))
assert not os.path.exists(os.path.join(c.current_folder, "build"))

# Just in case we check that local build folder would be created if no arg is provided
c.run('build pkg ')
assert os.path.exists(os.path.join(c.current_folder, "pkg", "build"))


def test_test_cmake_layout_build_folder_test_package():
""" relocate the test_package temporary build folders to elsewhere
"""
c = TestClient()
abs_build = temp_folder()
test_conanfile = textwrap.dedent("""
from conan import ConanFile
from conan.tools.cmake import cmake_layout
class HelloTestConan(ConanFile):
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeToolchain"
def requirements(self):
self.requires(self.tested_reference_str)
def layout(self):
cmake_layout(self, generator="Ninja")
def test(self):
pass
""")

c.save({"conanfile.py": GenConanfile("pkg", "0.1"),
"test_package/conanfile.py": test_conanfile})
c.run(f'create . -c tools.cmake.cmake_layout:test_folder="{abs_build}" '
'-c tools.cmake.cmake_layout:build_folder_vars=[]')
# Even if build_folder_vars=[] the "Release" folder is added always
assert os.path.exists(os.path.join(abs_build, "Release", "generators", "conan_toolchain.cmake"))
assert not os.path.exists(os.path.join(c.current_folder, "test_package", "build"))

c.run(f'create . -c tools.cmake.cmake_layout:build_folder_vars=[]')
assert os.path.exists(os.path.join(c.current_folder, "test_package", "build"))


def test_test_cmake_layout_build_folder_test_package_temp():
""" using always the same test_package build_folder will cause collisions.
We need a mechanism to relocate, still provide unique folders for each build
"""
c = TestClient()
test_conanfile = textwrap.dedent("""
from conan import ConanFile
from conan.tools.cmake import cmake_layout
class HelloTestConan(ConanFile):
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeToolchain"
def requirements(self):
self.requires(self.tested_reference_str)
def layout(self):
cmake_layout(self)
def test(self):
pass
""")

profile = textwrap.dedent("""
include(default)
[conf]
tools.cmake.cmake_layout:test_folder=$TMP
tools.cmake.cmake_layout:build_folder_vars=[]
""")
c.save({"conanfile.py": GenConanfile("pkg", "0.1"),
"test_package/conanfile.py": test_conanfile,
"profile": profile})
c.run(f'create . -pr=profile')
build_folder = re.search(r"Test package build folder: (\S+)", str(c.out)).group(1)
assert os.path.exists(os.path.join(build_folder, "generators", "conan_toolchain.cmake"))
assert not os.path.exists(os.path.join(c.current_folder, "test_package", "build"))

c.run(f'test test_package pkg/0.1 -pr=profile')
build_folder = re.search(r"Test package build folder: (\S+)", str(c.out)).group(1)
assert os.path.exists(os.path.join(build_folder, "generators", "conan_toolchain.cmake"))
assert not os.path.exists(os.path.join(c.current_folder, "test_package", "build"))

c.run(f'create . -c tools.cmake.cmake_layout:build_folder_vars=[]')
assert os.path.exists(os.path.join(c.current_folder, "test_package", "build"))


def test_cmake_layout_build_folder_editable():
""" testing how it works with editables. Layout
code
pkga # editable add .
pkgb # editable add .
app # install -c build_folder="../../mybuild -c build_folder_vars="['self.name']"
pkga-release-data.cmake
pkga_PACKAGE_FOLDER_RELEASE = /abs/path/to/code/pkga
pkga_INCLUDE_DIRS_RELEASE = ${pkga_PACKAGE_FOLDER_RELEASE}/include
pkga_LIB_DIRS_RELEASE = /abs/path/to/mybuild/pkga/Release
mybuild
pkga/Release/
pkgb/Release/
"""
c = TestClient()
base_folder = temp_folder()
c.current_folder = os.path.join(base_folder, "code").replace("\\", "/")
project_build_folder = os.path.join(base_folder, "mybuild").replace("\\", "/")
conanfile = textwrap.dedent("""
from conan import ConanFile
from conan.tools.cmake import cmake_layout
class Pkg(ConanFile):
name = "{name}"
version = "0.1"
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeToolchain"
def layout(self):
cmake_layout(self, generator="Ninja") # Ninja so same in all OSs, single-config
""")

c.save({"pkga/conanfile.py": conanfile.format(name="pkga"),
"pkgb/conanfile.py": conanfile.format(name="pkgb"),
"app/conanfile.py": GenConanfile().with_requires("pkga/0.1", "pkgb/0.1")
.with_settings("build_type")
.with_generator("CMakeDeps")})

c.run("editable add pkga")
c.run("editable add pkgb")
conf = f'-c tools.cmake.cmake_layout:build_folder="../../mybuild" ' \
'-c tools.cmake.cmake_layout:build_folder_vars="[\'self.name\']"'
c.run(f'install app {conf} --build=editable')

data = c.load("app/pkga-release-data.cmake")

# The package is the ``code/pkga`` folder
assert f'pkga_PACKAGE_FOLDER_RELEASE "{c.current_folder}/pkga"' in data
# This is an absolute path, not relative, as it is not inside the package
assert f'set(pkga_LIB_DIRS_RELEASE "{project_build_folder}/pkga/Release' in data
data = c.load("app/pkgb-release-data.cmake")
assert f'set(pkgb_LIB_DIRS_RELEASE "{project_build_folder}/pkgb/Release' in data

assert os.path.exists(os.path.join(project_build_folder, "pkga", "Release", "generators",
"conan_toolchain.cmake"))
assert os.path.exists(os.path.join(project_build_folder, "pkgb", "Release", "generators",
"conan_toolchain.cmake"))


def test_cmake_layout_editable_output_folder():
""" testing how it works with editables, but --output-folder
code
pkga # editable add . --output-folder = ../mybuild
pkgb # editable add . --output-folder = ../mybuild
app # install -c build_folder_vars="['self.name']"
pkga-release-data.cmake
pkga_PACKAGE_FOLDER_RELEASE = /abs/path/to/mybuild
pkga_INCLUDE_DIRS_RELEASE = /abs/path/to/code/pkga/include
pkga_LIB_DIRS_RELEASE = pkga_PACKAGE_FOLDER_RELEASE/build/pkga/Release
mybuild
build
pkga/Release/
pkgb/Release/
"""
c = TestClient()
base_folder = temp_folder()
c.current_folder = os.path.join(base_folder, "code")
project_build_folder = os.path.join(base_folder, "mybuild").replace("\\", "/")
conanfile = textwrap.dedent("""
from conan import ConanFile
from conan.tools.cmake import cmake_layout
class Pkg(ConanFile):
name = "{name}"
version = "0.1"
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeToolchain"
def layout(self):
cmake_layout(self, generator="Ninja") # Ninja so same in all OSs, single-config
""")

c.save({"pkga/conanfile.py": conanfile.format(name="pkga"),
"pkgb/conanfile.py": conanfile.format(name="pkgb"),
"app/conanfile.py": GenConanfile().with_requires("pkga/0.1", "pkgb/0.1")
.with_settings("build_type")
.with_generator("CMakeDeps")})

c.run(f'editable add pkga --output-folder="../mybuild"')
c.run(f'editable add pkgb --output-folder="../mybuild"')
conf = f'-c tools.cmake.cmake_layout:build_folder_vars="[\'self.name\']"'
c.run(f'install app {conf} --build=editable')

data = c.load("app/pkga-release-data.cmake")
assert f'set(pkga_PACKAGE_FOLDER_RELEASE "{project_build_folder}")' in data
# Thse folders are relative to the package
assert 'pkga_LIB_DIRS_RELEASE "${pkga_PACKAGE_FOLDER_RELEASE}/build/pkga/Release' in data
data = c.load("app/pkgb-release-data.cmake")
assert 'pkgb_LIB_DIRS_RELEASE "${pkgb_PACKAGE_FOLDER_RELEASE}/build/pkgb/Release' in data

assert os.path.exists(os.path.join(project_build_folder, "build", "pkga", "Release",
"generators", "conan_toolchain.cmake"))
assert os.path.exists(os.path.join(project_build_folder, "build", "pkgb", "Release",
"generators", "conan_toolchain.cmake"))

0 comments on commit 1a99f17

Please sign in to comment.