diff --git a/conan/internal/api/install/generators.py b/conan/internal/api/install/generators.py index 946133a7d4b..9bc70c396ed 100644 --- a/conan/internal/api/install/generators.py +++ b/conan/internal/api/install/generators.py @@ -30,6 +30,7 @@ "XcodeDeps": "conan.tools.apple", "XcodeToolchain": "conan.tools.apple", "PremakeDeps": "conan.tools.premake", + "PremakeToolchain": "conan.tools.premake", "MakeDeps": "conan.tools.gnu", "SConsDeps": "conan.tools.scons", "QbsDeps": "conan.tools.qbs", diff --git a/conan/tools/premake/__init__.py b/conan/tools/premake/__init__.py index 339d38a6b18..c51356c5995 100644 --- a/conan/tools/premake/__init__.py +++ b/conan/tools/premake/__init__.py @@ -1,2 +1,3 @@ from conan.tools.premake.premake import Premake -from conan.tools.premake.premakedeps import PremakeDeps \ No newline at end of file +from conan.tools.premake.premakedeps import PremakeDeps +from conan.tools.premake.toolchain import PremakeToolchain diff --git a/conan/tools/premake/constants.py b/conan/tools/premake/constants.py new file mode 100644 index 00000000000..d826a0e8dbf --- /dev/null +++ b/conan/tools/premake/constants.py @@ -0,0 +1,34 @@ +# Source: https://premake.github.io/docs/architecture/ +CONAN_TO_PREMAKE_ARCH = { + "x86": "x86", + "x86_64": "x86_64", + + "armv4": "arm", + "armv4i": "arm", + "armv5el": "arm", + "armv5hf": "arm", + "armv6": "arm", + "armv7": "arm", + "armv7hf": "arm", + "armv7s": "arm", + "armv7k": "arm", + + "armv8": "arm64", + "armv8_32": "arm64", + "armv8.3": "arm64", + "arm64ec": "arm64", + + "e2k-v2": "e2k", + "e2k-v3": "e2k", + "e2k-v4": "e2k", + "e2k-v5": "e2k", + "e2k-v6": "e2k", + "e2k-v7": "e2k", + + "riscv64": "riscv64", + + "wasm": "wasm32", + "asm.js": "wasm32", +} + +INDENT_LEVEL = 4 diff --git a/conan/tools/premake/premake.py b/conan/tools/premake/premake.py index 69765dd3c56..bf3c683a4e4 100644 --- a/conan/tools/premake/premake.py +++ b/conan/tools/premake/premake.py @@ -1,40 +1,91 @@ +import textwrap +from pathlib import Path + +from jinja2 import Template + +from conan.errors import ConanException +from conan.tools.files import save +from conan.tools.microsoft.msbuild import MSBuild +from conan.tools.premake.toolchain import PremakeToolchain +from conan.tools.premake.constants import CONAN_TO_PREMAKE_ARCH + # Source: https://learn.microsoft.com/en-us/cpp/overview/compiler-versions?view=msvc-170 PREMAKE_VS_VERSION = { - '190': '2015', - '191': '2017', - '192': '2019', - '193': '2022', - '194': '2022', # still 2022 + "190": "2015", + "191": "2017", + "192": "2019", + "193": "2022", + "194": "2022", # still 2022 } - class Premake: """ Premake cli wrapper """ + filename = "conanfile.premake5.lua" + + # Conan premake file which will preconfigure toolchain and then will call the user's premake file + _premake_file_template = textwrap.dedent( + """\ + #!lua + include("{{luafile}}") + include("{{premake_conan_toolchain}}") + """ + ) + def __init__(self, conanfile): self._conanfile = conanfile - - self.action = None # premake5 action to use (Autogenerated) - self.luafile = 'premake5.lua' # Path to the root (premake5) lua file + # Path to the root (premake5) lua file + self.luafile = (Path(self._conanfile.source_folder) / "premake5.lua").as_posix() # (key value pairs. Will translate to "--{key}={value}") - self.arguments = {} # https://premake.github.io/docs/Command-Line-Arguments/ + self.arguments = {} # https://premake.github.io/docs/Command-Line-Arguments/ + + arch = str(self._conanfile.settings.arch) + if arch not in CONAN_TO_PREMAKE_ARCH: + raise ConanException(f"Premake does not support {arch} architecture.") + self.arguments["arch"] = CONAN_TO_PREMAKE_ARCH[arch] if "msvc" in self._conanfile.settings.compiler: - msvc_version = PREMAKE_VS_VERSION.get(str(self._conanfile.settings.compiler.version)) - self.action = f'vs{msvc_version}' + msvc_version = PREMAKE_VS_VERSION.get( + str(self._conanfile.settings.compiler.version) + ) + self.action = f"vs{msvc_version}" else: - self.action = 'gmake2' + self.action = "gmake" # New generator (old gmakelegacy is deprecated) @staticmethod def _expand_args(args): - return ' '.join([f'--{key}={value}' for key, value in args.items()]) + return " ".join([f"--{key}={value}" for key, value in args.items()]) def configure(self): + premake_conan_toolchain = Path(self._conanfile.generators_folder) / PremakeToolchain.filename + if not premake_conan_toolchain.exists(): + raise ConanException( + f"Premake toolchain file does not exist: {premake_conan_toolchain}\nUse PremakeToolchain to generate it." + ) + content = Template(self._premake_file_template).render( + premake_conan_toolchain=premake_conan_toolchain, luafile=self.luafile + ) + + conan_luafile = Path(self._conanfile.build_folder) / self.filename + save(self._conanfile, conan_luafile, content) + premake_options = dict() - premake_options["file"] = self.luafile + premake_options["file"] = conan_luafile - premake_command = f'premake5 {self._expand_args(premake_options)} {self.action} ' \ - f'{self._expand_args(self.arguments)}' + premake_command = ( + f"premake5 {self._expand_args(premake_options)} {self.action} " + f"{self._expand_args(self.arguments)}" + ) self._conanfile.run(premake_command) + + + def build(self, workspace, targets=None): + if self.action.startswith("vs"): + msbuild = MSBuild(self._conanfile) + msbuild.build(sln=f"{workspace}.sln", targets=targets) + else: + build_type = str(self._conanfile.settings.build_type) + targets = "all" if targets is None else " ".join(targets) + self._conanfile.run(f"make config={build_type.lower()} {targets} -j") diff --git a/conan/tools/premake/premakedeps.py b/conan/tools/premake/premakedeps.py index 3e868eabaed..0df5fd1b621 100644 --- a/conan/tools/premake/premakedeps.py +++ b/conan/tools/premake/premakedeps.py @@ -4,9 +4,10 @@ from conan.internal import check_duplicated_generator from conans.util.files import save +from conan.tools.premake.constants import CONAN_TO_PREMAKE_ARCH # Filename format strings -PREMAKE_VAR_FILE = "conan_{pkgname}_vars{config}.premake5.lua" +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" @@ -145,10 +146,11 @@ def generate(self): save(generator_file, content) def _config_suffix(self): - props = [("Configuration", self.configuration), - ("Platform", self.architecture)] - name = "".join("_%s" % v for _, v in props) - return name.lower() + # props = [("Configuration", self.configuration), + # ("Platform", self.architecture)] + # name = "".join("_%s" % v for _, v in props) + # return name.lower() + return f"{self.configuration}_{CONAN_TO_PREMAKE_ARCH[str(self.architecture)]}".lower() def _output_lua_file(self, filename, content): self.output_files[filename] = "\n".join(["#!lua", *content]) @@ -180,8 +182,7 @@ def content(self): check_duplicated_generator(self, self._conanfile) self.output_files = {} - conf_suffix = str(self._config_suffix()) - conf_name = conf_suffix[1::] + conf_name = self._config_suffix() # Global utility file self._output_lua_file("conanutils.premake5.lua", [PREMAKE_TEMPLATE_UTILS]) @@ -206,15 +207,15 @@ def content(self): dep_aggregate = dep.cpp_info.aggregated_components() # Generate config dependent package variable and setup premake file - var_filename = PREMAKE_VAR_FILE.format(pkgname=dep_name, config=conf_suffix) + var_filename = PREMAKE_VAR_FILE.format(pkgname=dep_name, config=conf_name) self._output_lua_file(var_filename, [ PREMAKE_TEMPLATE_VAR.format(pkgname=dep_name, config=conf_name, deps=_PremakeTemplate(dep_aggregate)) ]) # 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="_(([^_]*)_(.*))") + 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: diff --git a/conan/tools/premake/toolchain.py b/conan/tools/premake/toolchain.py new file mode 100644 index 00000000000..3acf54e1a14 --- /dev/null +++ b/conan/tools/premake/toolchain.py @@ -0,0 +1,224 @@ +import os +import textwrap +from pathlib import Path + +from jinja2 import Environment + +from conan.tools.build.cross_building import cross_building +from conan.tools.files import save +from conan.tools.microsoft.visual import VCVars +from conan.tools.premake.constants import INDENT_LEVEL +from conan.tools.premake.premakedeps import PREMAKE_ROOT_FILE + +_jinja_env = Environment(trim_blocks=True, lstrip_blocks=True) + +def _generate_flags(self, conanfile): + template = _jinja_env.from_string(textwrap.dedent( + """\ + -- Workspace flags + + {% if extra_cflags %} + -- C flags retrieved from CFLAGS environment, conan.conf(tools.build:cflags) and extra_cflags + filter {"files:**.c"} + buildoptions { {{ extra_cflags }} } + filter {} + {% endif %} + {% if extra_cxxflags %} + -- CXX flags retrieved from CXXFLAGS environment, conan.conf(tools.build:cxxflags) and extra_cxxflags + filter {"files:**.cpp", "**.cxx", "**.cc"} + buildoptions { {{ extra_cxxflags }} } + filter {} + {% endif %} + {% if extra_ldflags %} + -- Link flags retrieved from LDFLAGS environment, conan.conf(tools.build:sharedlinkflags), conan.conf(tools.build:exelinkflags) and extra_cxxflags + linkoptions { {{ extra_ldflags }} } + {% endif %} + {% if extra_defines %} + -- Defines retrieved from DEFINES environment, conan.conf(tools.build:defines) and extra_defines + defines { {{ extra_defines }} } + {% endif %} + """)) + + def format_list(items): + return ", ".join(f'"{item}"' for item in items) if items else None + + + build_env = self._conanfile.buildenv.vars(self._conanfile) + def _get_env_list(env): + v = build_env.get(env, []) + return v.strip().split() if not isinstance(v, list) else v + + extra_defines = format_list( + _get_env_list("DEFINES") + + conanfile.conf.get("tools.build:defines", default=[], check_type=list) + + self.extra_defines + ) + extra_c_flags = format_list( + _get_env_list("CFLAGS") + + conanfile.conf.get("tools.build:cflags", default=[], check_type=list) + + self.extra_cflags + ) + extra_cxx_flags = format_list( + _get_env_list("CXXFLAGS") + + conanfile.conf.get("tools.build:cxxflags", default=[], check_type=list) + + self.extra_cxxflags + ) + extra_ld_flags = format_list( + _get_env_list("LDFLAGS") + + conanfile.conf.get("tools.build:sharedlinkflags", default=[], check_type=list) + + conanfile.conf.get("tools.build:exelinkflags", default=[], check_type=list) + + self.extra_ldflags + ) + + return template.render( + extra_defines=extra_defines, + extra_cflags=extra_c_flags, + extra_cxxflags=extra_cxx_flags, + extra_ldflags=extra_ld_flags, + ).strip() + + +class _PremakeProject: + _premake_project_template = _jinja_env.from_string(textwrap.dedent( + """\ + project "{{ name }}" + {% if kind %} + kind "{{ kind }}" + {% endif %} + {% if flags %} + -- Project flags {{ "(global)" if is_global else "(specific)"}} + {{ flags | indent(indent_level, first=True) }} + {% endif %} + """)) + + def __init__(self, name, conanfile) -> None: + self.name = name + self.kind = None + self.extra_cxxflags = [] + self.extra_cflags = [] + self.extra_ldflags = [] + self.extra_defines = [] + self.disable = False + self._conanfile = conanfile + + def _generate(self): + """Generates project block""" + flags_content = _generate_flags(self, self._conanfile) # Generate flags specific to this project + return self._premake_project_template.render( + name=self.name, + kind="None" if self.disable else self.kind, + flags=flags_content, + indent_level=INDENT_LEVEL, + ) + + +class PremakeToolchain: + """ + PremakeToolchain generator + """ + + filename = "conantoolchain.premake5.lua" + # Keep template indented correctly for Lua output + _premake_file_template = textwrap.dedent( + """\ + #!lua + -- Conan auto-generated toolchain file + {% if has_conan_deps %} + -- Include conandeps.premake5.lua with Conan dependency setup + include("conandeps.premake5.lua") + {% endif %} + + -- Base build directory + local locationDir = path.normalize("{{ build_folder }}") + + -- Generate workspace configurations + for wks in premake.global.eachWorkspace() do + workspace(wks.name) + -- Set base location for all workspaces + location(locationDir) + targetdir(path.join(locationDir, "bin")) + objdir(path.join(locationDir, "obj")) + + {% if cppstd %} + cppdialect "{{ cppstd }}" + {% endif %} + {% if cstd %} + cdialect "{{ cstd }}" + {% endif %} + + {% if cross_build_arch %} + -- TODO: this should be fixed by premake: https://github.com/premake/premake-core/issues/2136 + buildoptions "-arch {{ cross_build_arch }}" + linkoptions "-arch {{ cross_build_arch }}" + {% endif %} + + {% if flags %} + {{ flags | indent(indent_level, first=True) }} + {% endif %} + + filter { "system:macosx" } + -- runpathdirs { "@loader_path" } + -- TODO Fix shared libs + linkoptions { "-Wl,-rpath,@loader_path" } + filter {} + + conan_setup() + end + + {% for project in projects.values() %} + + {{ project._generate() }} + {% endfor %} + """) + + + def __init__(self, conanfile): + self._conanfile = conanfile + self._projects = {} + self.extra_cxxflags = [] + self.extra_cflags = [] + self.extra_ldflags = [] + self.extra_defines = [] + + def project(self, project_name): + if project_name not in self._projects: + self._projects[project_name] = _PremakeProject(project_name, self._conanfile) + return self._projects[project_name] + + def generate(self): + premake_conan_deps = Path(self._conanfile.generators_folder) / PREMAKE_ROOT_FILE + cppstd = self._conanfile.settings.get_safe("compiler.cppstd") + if cppstd: + # TODO consider improving using existing function + # See premake possible cppstd values: https://premake.github.io/docs/cppdialect/ + if cppstd.startswith("gnu"): + cppstd = f"gnu++{cppstd[3:]}" + elif cppstd[0].isnumeric(): + cppstd = f"c++{cppstd}" + + # TODO: for some reason, cstd is always None + cstd = self._conanfile.settings.get_safe("compiler.cstd") + cross_build_arch = self._conanfile.settings.arch if cross_building(self._conanfile) else None + + flags_content = _generate_flags(self, self._conanfile) # Generate flags specific to this workspace + + template = _jinja_env.from_string(self._premake_file_template) + content = template.render( + # Pass posix path for better cross-platform compatibility in Lua + build_folder=Path(self._conanfile.build_folder).as_posix(), + has_conan_deps=premake_conan_deps.exists(), + cppstd=cppstd, + cstd=cstd, + cross_build_arch=cross_build_arch, + projects=self._projects, + flags=flags_content, + indent_level=INDENT_LEVEL*2, + ) + save( + self, + os.path.join(self._conanfile.generators_folder, self.filename), + content, + ) + # TODO: improve condition + if "msvc" in self._conanfile.settings.compiler: + VCVars(self._conanfile).generate() diff --git a/test/integration/toolchains/premake/test_premake.py b/test/integration/toolchains/premake/test_premake.py index a7d8bce37ad..52191736b49 100644 --- a/test/integration/toolchains/premake/test_premake.py +++ b/test/integration/toolchains/premake/test_premake.py @@ -1,18 +1,22 @@ import textwrap from conan.test.utils.tools import TestClient +import os def test_premake_args(): c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile - from conan.tools.premake import Premake + from conan.tools.premake import Premake, PremakeToolchain class Pkg(ConanFile): settings = "os", "compiler", "build_type", "arch" def run(self, cmd, *args, **kwargs): self.output.info(f"Running {cmd}!!") + def generate(self): + toolchain = PremakeToolchain(self) + toolchain.generate() def build(self): premake = Premake(self) premake.luafile = "myproject.lua" @@ -21,4 +25,81 @@ def build(self): """) c.save({"conanfile.py": conanfile}) c.run("build . -s compiler=msvc -s compiler.version=193 -s compiler.runtime=dynamic") - assert "conanfile.py: Running premake5 --file=myproject.lua vs2022 --myarg=myvalue!!" in c.out + assert "conanfile.py: Running premake5" in c.out + assert "conanfile.premake5.lua vs2022 --myarg=myvalue!!" in c.out + + +def test_premake_full_compilation(): + client = TestClient(path_with_spaces=False) + client.run("new cmake_lib -d name=dep -d version=1.0 -o dep") + + consumer_source = textwrap.dedent(""" + #include + #include "dep.h" + + int main(void) { + dep(); + std::cout << "Hello World" << std::endl; + return 0; + } + """) + + premake5 = textwrap.dedent(""" + workspace "Project" + language "C++" + configurations { "Debug", "Release" } + + project "app" + kind "ConsoleApp" + files { "**.h", "**.cpp" } + filter "configurations:Debug" + defines { "DEBUG" } + symbols "On" + filter "configurations:Release" + defines { "NDEBUG" } + optimize "On" + """) + + + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.layout import basic_layout + from conan.tools.premake import Premake, PremakeDeps, PremakeToolchain + import os + + class Pkg(ConanFile): + settings = "os", "compiler", "build_type", "arch" + name = "pkg" + version = "1.0" + exports_sources = '*' + + def layout(self): + basic_layout(self, src_folder="src") + + def requirements(self): + self.requires("dep/1.0") + + def generate(self): + deps = PremakeDeps(self) + deps.generate() + tc = PremakeToolchain(self) + tc.generate() + + def build(self): + premake = Premake(self) + premake.configure() + premake.build(workspace="Project") + """) + + client.save({"consumer/conanfile.py": conanfile, + "consumer/src/hello.cpp": consumer_source, + "consumer/src/premake5.lua": premake5, + }) + + client.run("create dep") + client.run("create consumer") + bin_folder = os.path.join(client.created_layout().build(), "build-release", "bin") + exe_path = os.path.join(bin_folder, "app") + assert os.path.exists(exe_path) + client.run_command(exe_path) + assert "dep/1.0: Hello World Release!" in client.out diff --git a/test/integration/toolchains/premake/test_premakedeps.py b/test/integration/toolchains/premake/test_premakedeps.py index 5beafc197dd..9e42410b893 100644 --- a/test/integration/toolchains/premake/test_premakedeps.py +++ b/test/integration/toolchains/premake/test_premakedeps.py @@ -62,3 +62,4 @@ def package_info(self): # Assert package per configuration files assert_vars_file(client, 'debug') assert_vars_file(client, 'release') + diff --git a/test/integration/toolchains/premake/test_premaketoolchain.py b/test/integration/toolchains/premake/test_premaketoolchain.py new file mode 100644 index 00000000000..f8a4d6d592d --- /dev/null +++ b/test/integration/toolchains/premake/test_premaketoolchain.py @@ -0,0 +1,65 @@ +import textwrap + +from conan.test.utils.tools import TestClient +from conan.tools.premake.toolchain import PremakeToolchain + + +def test_extra_flags_via_conf(): + profile = textwrap.dedent( + """ + [settings] + os=Linux + arch=x86_64 + compiler=gcc + compiler.version=9 + compiler.cppstd=17 + compiler.cstd=11 + compiler.libcxx=libstdc++ + build_type=Release + + [buildenv] + CFLAGS=-flag00 -other=val0 + CXXFLAGS=-flag01 -other=val1 + LDFLAGS=-flag02 -other=val2 + + [conf] + tools.build:cxxflags=["-flag1", "-flag2"] + tools.build:cflags=["-flag3", "-flag4"] + tools.build:sharedlinkflags+=["-flag5"] + tools.build:exelinkflags+=["-flag6"] + tools.build:defines=["define1=0"] + """ + ) + t = TestClient() + t.save({"conanfile.txt": "[generators]\nPremakeToolchain", "profile": profile}) + + t.run("install . -pr:a=profile") + content = t.load(PremakeToolchain.filename) + print(content) + assert 'cppdialect "c++17"' in content + # assert 'cdialect "99"' in content # TODO + + assert ( + """ + filter {"files:**.c"} + buildoptions { "-flag00", "-other=val0", "-flag3", "-flag4" } + filter {} + """ + in content + ) + + assert ( + """ + filter {"files:**.cpp", "**.cxx", "**.cc"} + buildoptions { "-flag01", "-other=val1", "-flag1", "-flag2" } + filter {} + """ + in content + ) + + assert 'linkoptions { "-flag02", "-other=val2", "-flag5", "-flag6" }' in content + + # assert "cpp_args = ['-flag0', '-other=val', '-m64', '-flag1', '-flag2', '-Ddefine1=0', '-D_GLIBCXX_USE_CXX11_ABI=0']" in content + # assert "c_args = ['-flag0', '-other=val', '-m64', '-flag3', '-flag4', '-Ddefine1=0']" in content + # assert "c_link_args = ['-flag0', '-other=val', '-m64', '-flag5', '-flag6']" in content + # assert "cpp_link_args = ['-flag0', '-other=val', '-m64', '-flag5', '-flag6']" in content