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

Feature/premakedeps #13390

Merged
merged 8 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
325 changes: 270 additions & 55 deletions conan/tools/premake/premakedeps.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,82 @@
import itertools
import glob
import re

from conan.internal import check_duplicated_generator
from conans.model.build_info import CppInfo
from conans.util.files import save

PREMAKE_FILE = "conandeps.premake.lua"
# Filename format strings
PREMAKE_VAR_FILE = "conan_{pkgname}_vars{config}.premake5.lua"
PREMAKE_CONF_FILE = "conan_{pkgname}{config}.premake5.lua"
PREMAKE_PKG_FILE = "conan_{pkgname}.premake5.lua"
PREMAKE_ROOT_FILE = "conandeps.premake5.lua"

# File template format strings
PREMAKE_TEMPLATE_VAR = """
conan_includedirs_{pkgname}{config} = {{{deps.includedirs}}}
conan_libdirs_{pkgname}{config} = {{{deps.libdirs}}}
conan_bindirs_{pkgname}{config} = {{{deps.bindirs}}}
conan_libs_{pkgname}{config} = {{{deps.libs}}}
conan_system_libs_{pkgname}{config} = {{{deps.system_libs}}}
conan_defines_{pkgname}{config} = {{{deps.defines}}}
conan_cxxflags_{pkgname}{config} = {{{deps.cxxflags}}}
conan_cflags_{pkgname}{config} = {{{deps.cflags}}}
conan_sharedlinkflags_{pkgname}{config} = {{{deps.sharedlinkflags}}}
conan_exelinkflags_{pkgname}{config} = {{{deps.exelinkflags}}}
conan_frameworks_{pkgname}{config} = {{{deps.frameworks}}}
"""
PREMAKE_TEMPLATE_CONF = """
include "{premake_varfile}"
function conan_setup_build_{pkgname}{config}()
{conf_consume_build}
end
function conan_setup_link_{pkgname}{config}()
{conf_consume_link}
end
function conan_setup_{pkgname}{config}()
Ohjurot marked this conversation as resolved.
Show resolved Hide resolved
conan_setup_build_{pkgname}{config}()
conan_setup_link_{pkgname}{config}()
end
"""
PREMAKE_TEMPLATE_CONF_BUILD = """
includedirs{{conan_includedirs_{pkgname}{config}}}
bindirs{{conan_bindirs_{pkgname}{config}}}
defines{{conan_defines_{pkgname}{config}}}
"""
PREMAKE_TEMPLATE_CONF_LINK = """
libdirs{{conan_libdirs_{pkgname}{config}}}
links{{conan_libs_{pkgname}{config}}}
links{{conan_system_libs_{pkgname}{config}}}
links{{conan_frameworks_{pkgname}{config}}}
"""
PREMAKE_TEMPLATE_PKG = """
function conan_setup_build_{pkgname}()
{pkg_filter_expand_build}
end
function conan_setup_link_{pkgname}()
{pkg_filter_expand_link}
end
function conan_setup_{pkgname}()
conan_setup_build_{pkgname}()
conan_setup_link_{pkgname}()
end
"""
PREMAKE_TEMPLATE_ROOT = """
function conan_setup_build()
{root_call_all_build}
end
function conan_setup_link()
{root_call_all_link}
end
function conan_setup()
conan_setup_build()
conan_setup_link()
end
"""


# Helper class that expands cpp_info meta information in lua readable string sequences
class _PremakeTemplate(object):
def __init__(self, deps_cpp_info):
self.includedirs = ",\n".join('"%s"' % p.replace("\\", "/")
Expand Down Expand Up @@ -37,74 +109,217 @@ def __init__(self, deps_cpp_info):


class PremakeDeps(object):
"""
PremakeDeps class generator
conandeps.premake5.lua: unconditional import of all *direct* dependencies only
"""

def __init__(self, conanfile):
"""
:param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``.
"""

self._conanfile = conanfile

# Tab configuration
self.tab = " "

# Return value buffer
self.output_files = {}
# Extract configuration and architecture form conanfile
self.configuration = conanfile.settings.build_type
self.architecture = conanfile.settings.arch

def generate(self):
"""
Generates ``conan_<pkg>_vars_<config>.premake5.lua``, ``conan_<pkg>_<config>.premake5.lua``,
and ``conan_<pkg>.premake5.lua`` files into the ``conanfile.generators_folder``.
"""

check_duplicated_generator(self, self._conanfile)
# Current directory is the generators_folder
generator_files = self.content
for generator_file, content in generator_files.items():
save(generator_file, content)

def _get_cpp_info(self):
ret = CppInfo()
for dep in self._conanfile.dependencies.host.values():
dep_cppinfo = dep.cpp_info.copy()
dep_cppinfo.set_relative_base_folder(dep.package_folder)
# In case we have components, aggregate them, we do not support isolated "targets"
dep_cppinfo.aggregate_components()
ret.merge(dep_cppinfo)
return ret
def _config_suffix(self):
props = [("Configuration", self.configuration),
("Platform", self.architecture)]
name = "".join("_%s" % v for _, v in props)
return name.lower()

def _output_lua_file(self, filename, content):
self.output_files[filename] = "\n".join(["#!lua", *content])

def _indent_string(self, string, indent=1):
return "\n".join([
f"{self.tab * indent}{line}" for line in list(filter(None, string.splitlines()))
])

def _premake_filtered(self, content, configuration, architecture, indent=0):
"""
- Surrounds the lua line(s) contained within ``content`` with a premake "filter" and returns the result.
- A "filter" will affect all premake function calls after it's set. It's used to limit following project
setup function call(s) to a certain scope. Here it is used to limit the calls in content to only apply
if the premake ``configuration`` and ``architecture`` matches the parameters in this function call.
"""
lines = list(itertools.chain.from_iterable([cnt.splitlines() for cnt in content]))
return [
# Set new filter
f'{self.tab * indent}filter {{ "configurations:{configuration}", "architecture:{architecture}" }}',
# Emit content
*[f"{self.tab * indent}{self.tab}{line.strip()}" for line in list(filter(None, lines))],
# Clear active filter
f"{self.tab * indent}filter {{}}",
]

def _premake_filtered_fallback(self, content, configurations, architecture, indent=1):
"""
- Uses filters that serve the same purpose than ``_premake_filtered()``
- This call will create an inverse filter on configurations. It will only apply if non of the
``configurations`` from this function call is present in premake. This is the fallback when a premake
Ohjurot marked this conversation as resolved.
Show resolved Hide resolved
configuration is NOT named like one of the conan build_type(s).
"""
fallback_filter = ", ".join(
[f'"configurations:not {configuration}"' for configuration in configurations]
)
lines = list(itertools.chain.from_iterable([cnt.splitlines() for cnt in content]))
return [
# Set new filter
f'{self.tab * indent}filter {{ {fallback_filter}, "architecture:{architecture}" }}',
# Emit content
*[f"{self.tab * indent}{self.tab}{line.strip()}" for line in list(filter(None, lines))],
# Clear active filter
f"{self.tab * indent}filter {{}}",
]

def _premake_pkg_expand(self, configurations, architectures, profiles,
callback_profile, callback_architecture):
return "\n".join([
*["\n".join(self._premake_filtered(callback_profile(profile), profile[2],
profile[3], indent=1)) for profile in profiles],
*["\n".join(self._premake_filtered_fallback(callback_architecture(architecture),
configurations, architecture, indent=1)) for architecture in architectures]
])

@property
def content(self):
ret = {} # filename -> file content

check_duplicated_generator(self, self._conanfile)

self.output_files = {}
conf_suffix = self._config_suffix()

# Extract all dependencies
host_req = self._conanfile.dependencies.host
test_req = self._conanfile.dependencies.test
build_req = self._conanfile.dependencies.build

# Merge into one list
full_req = list(host_req.items()) \
+ list(test_req.items()) \
+ list(build_req.items())

# Process dependencies and accumulate globally required data
pkg_files = []
dep_names = []
for require, dep in full_req:
dep_name = require.ref.name
dep_names.append(dep_name)

# Convert and aggregate dependency's
dep_cppinfo = dep.cpp_info.copy()
dep_cppinfo.set_relative_base_folder(dep.package_folder)
dep_aggregate = dep_cppinfo.aggregated_components()

# Generate config dependent package variable and setup premake file
var_filename = PREMAKE_VAR_FILE.format(pkgname=dep_name, config=conf_suffix)
conf_filename = PREMAKE_CONF_FILE.format(pkgname=dep_name, config=conf_suffix)
self._output_lua_file(var_filename, [
PREMAKE_TEMPLATE_VAR.format(pkgname=dep_name,
config=conf_suffix, deps=_PremakeTemplate(dep_aggregate))
])
self._output_lua_file(conf_filename, [
PREMAKE_TEMPLATE_CONF.format(
pkgname=dep_name, config=conf_suffix, premake_varfile=var_filename,
conf_consume_build=self._indent_string(
PREMAKE_TEMPLATE_CONF_BUILD.format(pkgname=dep_name, config=conf_suffix)
),
conf_consume_link=self._indent_string(
PREMAKE_TEMPLATE_CONF_LINK.format(pkgname=dep_name, config=conf_suffix)
)
)
])

# Create list of all available profiles by searching on disk
file_pattern = PREMAKE_VAR_FILE.format(pkgname=dep_name, config="_*")
file_regex = PREMAKE_VAR_FILE.format(pkgname=re.escape(dep_name), config="_(([^_]*)_(.*))")
available_files = glob.glob(file_pattern)
# Add filename of current generations var file if not already present
if var_filename not in available_files:
available_files.append(var_filename)
profiles = [
(regex_res[0], regex_res.group(1), regex_res.group(2), regex_res.group(3)) for regex_res in [
re.search(file_regex, file_name) for file_name in available_files
]
]
configurations = [profile[2] for profile in profiles]
architectures = list(dict.fromkeys([profile[3] for profile in profiles]))

# Fallback configuration (when user defined config is unknown -> prefer release or last)
fallback_configuration = "release"
if "release" not in configurations and len(configurations) > 0:
fallback_configuration = configurations[-1]

# Emit package premake file
pkg_files.append(PREMAKE_PKG_FILE.format(pkgname=dep_name))
self._output_lua_file(pkg_files[-1], [
Ohjurot marked this conversation as resolved.
Show resolved Hide resolved
# Includes
*['include "{}"'.format(profile[0]) for profile in profiles],
# Functions
PREMAKE_TEMPLATE_PKG.format(pkgname=dep_name,
pkg_filter_expand_build=self._premake_pkg_expand(
configurations, architectures, profiles,
lambda profile: [
PREMAKE_TEMPLATE_CONF_BUILD.format(
pkgname=dep_name, config=f"_{profile[1]}"
)
],
lambda architecture: [
PREMAKE_TEMPLATE_CONF_BUILD.format(
pkgname=dep_name, config=f"_{fallback_configuration}_{architecture}"
)
]
),
pkg_filter_expand_link=self._premake_pkg_expand(
configurations, architectures, profiles,
lambda profile: [
PREMAKE_TEMPLATE_CONF_LINK.format(
pkgname=dep_name, config=f"_{profile[1]}"
)
],
lambda architecture: [
PREMAKE_TEMPLATE_CONF_LINK.format(
pkgname=dep_name,
config=f"_{fallback_configuration}_{architecture}"
)
]
),
)
])

# Output global premake file
self._output_lua_file(PREMAKE_ROOT_FILE, [
# Includes
*[f'include "{pkg_file}"' for pkg_file in pkg_files],
# Functions
PREMAKE_TEMPLATE_ROOT.format(
root_call_all_build="\n".join(
[f"{self.tab}conan_setup_build_{dep_name}()" for dep_name in dep_names]
),
root_call_all_link="\n".join(
[f"{self.tab}conan_setup_link_{dep_name}()" for dep_name in dep_names]
)
)
])

template = ('conan_includedirs{dep} = {{{deps.includedirs}}}\n'
'conan_libdirs{dep} = {{{deps.libdirs}}}\n'
'conan_bindirs{dep} = {{{deps.bindirs}}}\n'
'conan_libs{dep} = {{{deps.libs}}}\n'
'conan_system_libs{dep} = {{{deps.system_libs}}}\n'
'conan_defines{dep} = {{{deps.defines}}}\n'
'conan_cxxflags{dep} = {{{deps.cxxflags}}}\n'
'conan_cflags{dep} = {{{deps.cflags}}}\n'
'conan_sharedlinkflags{dep} = {{{deps.sharedlinkflags}}}\n'
'conan_exelinkflags{dep} = {{{deps.exelinkflags}}}\n'
'conan_frameworks{dep} = {{{deps.frameworks}}}\n')

sections = ["#!lua"]

deps = _PremakeTemplate(self._get_cpp_info())
all_flags = template.format(dep="", deps=deps)
sections.append(all_flags)
sections.append(
"function conan_basic_setup()\n"
" configurations{conan_build_type}\n"
" architecture(conan_arch)\n"
" includedirs{conan_includedirs}\n"
" libdirs{conan_libdirs}\n"
" links{conan_libs}\n"
" links{conan_system_libs}\n"
" links{conan_frameworks}\n"
" defines{conan_defines}\n"
" bindirs{conan_bindirs}\n"
"end\n")
ret[PREMAKE_FILE] = "\n".join(sections)

template_deps = template + 'conan_sysroot{dep} = "{deps.sysroot}"\n'

# Iterate all the transitive requires
for require, dep in list(host_req.items()) + list(test_req.items()):
# FIXME: Components are not being aggregated
# FIXME: It is not clear the usage of individual dependencies
deps = _PremakeTemplate(dep.cpp_info)
dep_name = dep.ref.name.replace("-", "_")
dep_flags = template_deps.format(dep="_" + dep_name, deps=deps)
ret[dep.ref.name + '.lua'] = "\n".join(["#!lua", dep_flags])

return ret
return self.output_files
Loading