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

new tools.cmake.cmake_layout:build_folder #15767

Merged
merged 9 commits into from
Mar 13, 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
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 @@ -908,6 +909,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"))