Skip to content

Commit

Permalink
Feature: use CMake File API (#9005)
Browse files Browse the repository at this point in the history
* - use CMake File API

Signed-off-by: SSE4 <tomskside@gmail.com>

* Update conan/tools/cmake/file_api.py

* Update conan/tools/cmake/file_api.py

* Update conan/tools/cmake/file_api.py

* Update conans/model/cpp_package.py

* Update conans/model/cpp_package.py

* Update conans/model/cpp_package.py

* Update conans/model/cpp_package.py

* Update conans/model/cpp_package.py

* Update conans/model/cpp_package.py

* Update conans/model/cpp_package.py

* Update conans/model/cpp_package.py

* Update conans/model/cpp_package.py

* review

* Update conans/test/functional/toolchains/cmake/test_file_api.py

* Update conans/test/functional/toolchains/cmake/test_file_api.py

* Update conans/test/functional/toolchains/cmake/test_file_api.py

* Update conan/tools/files/cpp_package.py

* - process only known target types

Signed-off-by: SSE4 <tomskside@gmail.com>

Co-authored-by: memsharded <james@conan.io>
  • Loading branch information
SSE4 and memsharded authored Jul 6, 2021
1 parent c3a2805 commit 07e59c4
Show file tree
Hide file tree
Showing 7 changed files with 410 additions and 4 deletions.
1 change: 1 addition & 0 deletions conan/tools/cmake/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from conan.tools.cmake.toolchain import CMakeToolchain
from conan.tools.cmake.cmake import CMake
from conan.tools.cmake.cmakedeps.cmakedeps import CMakeDeps
from conan.tools.cmake.file_api import CMakeFileAPI
144 changes: 144 additions & 0 deletions conan/tools/cmake/file_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import json
import fnmatch
import os

from conan.tools.files import CppPackage
from conans.util.files import save, load


class CMakeFileAPI(object):
"""
implements https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html
"""
CODEMODELV2 = "codemodel-v2"
SKIP_TARGETS = ["ZERO_CHECK", "ALL_BUILD"]
# https://cmake.org/cmake/help/v3.21/prop_tgt/TYPE.html
SUPPORTED_TARGET_TYPES = ['STATIC_LIBRARY', 'SHARED_LIBRARY', 'MODULE_LIBRARY']

def __init__(self, conanfile):
self._conanfile = conanfile

@property
def api_dir(self):
"""
:return: api directory <build>/.cmake/api/v1/
"""
return os.path.join(self._conanfile.build_folder, ".cmake", "api", "v1")

@property
def query_dir(self):
"""
:return: api query sub-directory <build>/.cmake/api/v1/query
"""
return os.path.join(self.api_dir, "query")

@property
def reply_dir(self):
"""
:return: api reply sub-directory <build>/.cmake/api/v1/reply
"""
return os.path.join(self.api_dir, "reply")

def query(self, query):
"""
prepare the CMake File API query (the actual query will be done during the configure step)
:param query: type of the CMake File API query (e.g. CODEMODELV2)
:return: new query object
"""
if query == self.CODEMODELV2:
# implements codemodel-v2 query
# https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html#codemodel-version-2
os.makedirs(self.query_dir)
save(os.path.join(self.query_dir, "codemodel-v2"), "")
return
raise NotImplementedError()

def reply(self, reply):
"""
obtain the CMake File API reply (which should have been made during the configure step)
:param reply: type of the CMake File API reply (e.g. CODEMODELV2)
:return: new reply object
"""
if reply == self.CODEMODELV2:
return self.CodeModelReplyV2(self)
raise NotImplementedError()

@property
def build_type(self):
"""
:return: active build type (configuration)
"""
return self._conanfile.settings.get_safe("build_type")

class CodeModelReplyV2(object):
"""
implements codemodel-v2 reply
https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html#codemodel-version-2
"""
def __init__(self, api):

def loadjs(filename):
return json.loads(load(filename))

codemodels = os.listdir(api.reply_dir)
codemodels = [c for c in codemodels if fnmatch.fnmatch(c, "codemodel-v2-*.json")]
assert len(codemodels) == 1
self._codemodel = loadjs(os.path.join(api.reply_dir, codemodels[0]))

self._configurations = dict()
for configuration in self._codemodel['configurations']:
if configuration["name"] != api.build_type:
continue

self._components = dict()
for target in configuration["targets"]:
if target['name'] in CMakeFileAPI.SKIP_TARGETS:
continue
self._components[target["name"]] = \
loadjs(os.path.join(api.reply_dir, target['jsonFile']))

@classmethod
def _name_on_disk2lib(cls, name_on_disk):
"""
convert raw library file name into conan-friendly one
:param name_on_disk: raw library file name read from target.json
:return: conan-friendly library name, without suffix and prefix
"""
if name_on_disk.endswith('.lib'):
return name_on_disk[:-4]
elif name_on_disk.endswith('.dll'):
return name_on_disk[:-4]
elif name_on_disk.startswith('lib') and name_on_disk.endswith('.a'):
return name_on_disk[3:-2]
else:
# FIXME: This fails to parse executables
raise Exception("don't know how to convert %s" % name_on_disk)

@classmethod
def _parse_dep_name(cls, name):
"""
:param name: extract dependency name from the id like 'decoder::@5310dfab9e417c587352'
:return: dependency name (part before ::@ token)
"""
return name.split("::@")[0]

def to_conan_package(self):
"""
converts codemodel-v2 into conan_package.json object
:return: ConanPackage instance
"""

conan_package = CppPackage()

for name, target in self._components.items():
if target['type'] not in CMakeFileAPI.SUPPORTED_TARGET_TYPES:
continue
component = conan_package.add_component(name)
# TODO: CMakeDeps has nothing to do here
component.names["CMakeDeps"] = name
component.libs = [self._name_on_disk2lib(target['nameOnDisk'])]
deps = target["dependencies"] if 'dependencies' in target else []
deps = [self._parse_dep_name(d['id']) for d in deps]
component.requires = [d for d in deps if d not in CMakeFileAPI.SKIP_TARGETS]

return conan_package
1 change: 1 addition & 0 deletions conan/tools/files/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from conan.tools.files.files import load, save, mkdir, ftp_download, download, get, rename, \
load_build_json, save_build_json
from conan.tools.files.patches import patch, apply_conandata_patches
from conan.tools.files.cpp_package import CppPackage
73 changes: 73 additions & 0 deletions conan/tools/files/cpp_package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import json
from conans.util.files import load, save


class CppPackage(object):
"""
models conan_package.json, serializable object
"""

DEFAULT_FILENAME = "cpp_package.json"

def __init__(self):
self.components = dict()

@classmethod
def load(cls, filename=DEFAULT_FILENAME):
"""
loads package model into memory from the conan_package.json file
:param filename: path to the cpp_package.json
:return: a new instance of CppPackage
"""
def from_json(o):
if "components" in o:
return {n: CppPackage.Component(c) for n, c in o["components"].items()}
else:
return o
conan_package = CppPackage()
conan_package.components = json.loads(load(filename), object_hook=from_json)
return conan_package

def save(self, filename=DEFAULT_FILENAME):
"""
saves package model from memory into the cpp_package.json file
:param filename: path to the cpp_package.json
:return: None
"""
text = json.dumps(self, default=lambda o: o.__dict__, indent=4)
save(filename, text)

def package_info(self, conanfile):
"""
performs an automatically generated package_info method on conanfile, populating
conanfile.package_info with the information available inside cpp_package.json
:return: None
"""
for cname, component in self.components.items():
conanfile.cpp_info.components[cname].libs = component.libs
conanfile.cpp_info.components[cname].requires = component.requires
for generator, gname in component.names.items():
conanfile.cpp_info.components[cname].set_property("cmake_target_name", gname, generator)

def add_component(self, name):
"""
appens a new CppPackage.Configuration into the internal dictionary
:param name: name of the given configuration (e.g. Debug)
:return: a new CppPackage.Configuration instance (empty)
"""
self.components[name] = CppPackage.Component()
return self.components[name]

class Component(object):
"""
represents a single component (aka target) within package configuration
"""
def __init__(self, values=None):
if values:
self.names = values["names"]
self.requires = values["requires"]
self.libs = values["libs"]
else:
self.names = dict()
self.requires = []
self.libs = []
48 changes: 45 additions & 3 deletions conans/test/assets/cmake.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

def gen_cmakelists(language="CXX", verify=True, project="project", libname="mylibrary",
libsources=None, appname="myapp", appsources=None, cmake_version="3.15",
install=False, find_package=None, libtype=""):
install=False, find_package=None, libtype="", deps=None, public_header=None):
"""
language: C, C++, C/C++
project: the project name
Expand All @@ -21,17 +21,31 @@ def gen_cmakelists(language="CXX", verify=True, project="project", libname="myli
cmake_minimum_required(VERSION {{cmake_version}})
project({{project}} {{language}})
{% if find_package is mapping %}
{% for s, c in find_package.items() %}
find_package({{s}} COMPONENTS {{c}} )
{% endfor %}
{% else %}
{% for s in find_package %}
find_package({{s}})
{% endfor %}
{% endif %}
{% if libsources %}
add_library({{libname}} {{libtype}} {% for s in libsources %} {{s}} {% endfor %})
{% endif %}
{% if libsources and find_package %}
{% if find_package is mapping %}
target_link_libraries({{libname}} {% for s, c in find_package.items() %} {{s}}::{{c}} {% endfor %})
{% else %}
target_link_libraries({{libname}} {% for s in find_package %} {{s}}::{{s}} {% endfor %})
{% endif %}
{% endif %}
{% if libsources and deps %}
target_link_libraries({{libname}} {% for s in deps %} {{s}} {% endfor %})
{% endif %}
{% if appsources %}
add_executable({{appname}} {% for s in appsources %} {{s}} {% endfor %})
Expand All @@ -42,11 +56,37 @@ def gen_cmakelists(language="CXX", verify=True, project="project", libname="myli
{% endif %}
{% if appsources and not libsources and find_package %}
{% if find_package is mapping %}
target_link_libraries({{appname}} {% for s, c in find_package.items() %} {{s}}::{{c}} {% endfor %})
{% else %}
target_link_libraries({{appname}} {% for s in find_package %} {{s}}::{{s}} {% endfor %})
{% endif %}
{% endif %}
{% if appsources and deps %}
target_link_libraries({{appname}} {% for s in deps %} {{s}} {% endfor %})
{% endif %}
{% if libsources and public_header %}
set_target_properties({{libname}} PROPERTIES PUBLIC_HEADER "{{public_header}}")
{% endif %}
{% if install %}
install(TARGETS {{appname}} {{libname}} DESTINATION ".")
{% if appsources %}
install(TARGETS {{appname}} DESTINATION ".")
{% endif %}
{% if libsources %}
install(TARGETS {{libname}} DESTINATION "."
{% if public_header %}
PUBLIC_HEADER DESTINATION include
{% endif %}
RUNTIME DESTINATION bin
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
FRAMEWORK DESTINATION Frameworks
BUNDLE DESTINATION bin
)
{% endif %}
{% endif %}
""")

Expand All @@ -61,5 +101,7 @@ def gen_cmakelists(language="CXX", verify=True, project="project", libname="myli
"cmake_version": cmake_version,
"install": install,
"find_package": find_package or [],
"libtype": libtype
"libtype": libtype,
"public_header": public_header,
"deps": deps,
})
Loading

0 comments on commit 07e59c4

Please sign in to comment.