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: MesonToolchain: improve cross-compilation workflow on Apple systems #9711

Closed
wants to merge 1 commit into from
Closed
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
84 changes: 77 additions & 7 deletions conan/tools/meson/toolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
from jinja2 import Template

from conan.tools._check_build_profile import check_using_build_profile
from conan.tools.apple.apple import is_apple_os, to_apple_arch, apple_min_version_flag, XCRun
from conan.tools.env import VirtualBuildEnv
from conan.tools.microsoft import VCVars
from conans.client.build.cppstd_flags import cppstd_from_settings
from conan.tools.cross_building import cross_building, get_cross_building_settings
from conans.util.files import save


class MesonToolchain(object):
class MesonBaseToolchain(object):
native_filename = "conan_meson_native.ini"
cross_filename = "conan_meson_cross.ini"

Expand Down Expand Up @@ -46,7 +47,14 @@ class MesonToolchain(object):
c_link_args = {{c_link_args}}
cpp_args = {{cpp_args}} + preprocessor_definitions
cpp_link_args = {{cpp_link_args}}
objc_args = {{objc_args}} + preprocessor_definitions
objc_link_args = {{objc_link_args}}
objcpp_args = {{objcpp_args}} + preprocessor_definitions
objcpp_link_args = {{objcpp_link_args}}
{% if pkg_config_path %}pkg_config_path = {{pkg_config_path}}{% endif %}

[properties]
{% if root %}root = {{root}}{% endif %}
""")

_cross_file_template = _native_file_template + textwrap.dedent("""
Expand Down Expand Up @@ -111,15 +119,30 @@ def from_build_env(name):

# https://mesonbuild.com/Builtin-options.html#compiler-options
self.cpp_std = self._to_meson_cppstd(self._cppstd) if self._cppstd else None
self.c_args = self._to_meson_value(self._env_array('CPPFLAGS') + self._env_array('CFLAGS'))
self.c_link_args = self._to_meson_value(self._env_array('LDFLAGS'))
self.cpp_args = self._to_meson_value(self._env_array('CPPFLAGS') +
self._env_array('CXXFLAGS'))
self.cpp_link_args = self._to_meson_value(self._env_array('LDFLAGS'))
self.c_args = self._env_array('CPPFLAGS') + self._env_array('CFLAGS')
self.c_link_args = self._env_array('LDFLAGS')
self.cpp_args = self._env_array('CPPFLAGS') + self._env_array('CXXFLAGS')
self.cpp_link_args = self._env_array('LDFLAGS')
self.objc_args = self._env_array('CPPFLAGS') + self._env_array('OBJCFLAGS')
self.objc_link_args = self._env_array('LDFLAGS')
self.objcpp_args = self._env_array('CPPFLAGS') + self._env_array('OBJCXXFLAGS')
self.objcpp_link_args = self._env_array('LDFLAGS')
self.pkg_config_path = "'%s'" % self._conanfile.generators_folder
self.root = None

check_using_build_profile(self._conanfile)

super(MesonBaseToolchain, self).__init__(conanfile)

self.c_args = self._to_meson_value(self.c_args)
self.c_link_args = self._to_meson_value(self.c_link_args)
self.cpp_args = self._to_meson_value(self.cpp_args)
self.cpp_link_args = self._to_meson_value(self.cpp_link_args)
self.objc_args = self._to_meson_value(self.objc_args)
self.objc_link_args = self._to_meson_value(self.objc_link_args)
self.objcpp_args = self._to_meson_value(self.objcpp_args)
self.objcpp_link_args = self._to_meson_value(self.objcpp_link_args)

def _get_backend(self, recipe_backend):
# Returns the name of the backend used by Meson
conanfile = self._conanfile
Expand Down Expand Up @@ -238,8 +261,13 @@ def _context(self):
"c_link_args": self.c_link_args,
"cpp_args": self.cpp_args,
"cpp_link_args": self.cpp_link_args,
"objc_args": self.objc_args,
"objc_link_args": self.objc_link_args,
"objcpp_args": self.objcpp_args,
"objcpp_link_args": self.objcpp_link_args,
"pkg_config_path": self.pkg_config_path,
"preprocessor_definitions": self.preprocessor_definitions
"preprocessor_definitions": self.preprocessor_definitions,
"root": self.root
}
return context

Expand Down Expand Up @@ -333,3 +361,45 @@ def generate(self):
else:
self._write_native_file()
VCVars(self._conanfile).generate()


class MesonAppleMixin(object):
def __init__(self, conanfile):
if not cross_building(conanfile):
return
if self._base_compiler != 'apple-clang':
return
if not is_apple_os(conanfile.settings.get_safe("os")):
return

xcrun = XCRun(conanfile.settings)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better move XCRun first to modern approach. It should be less detection based and more configuration based.

Copy link
Contributor Author

@SSE4 SSE4 Oct 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the XCRun is already imported from a new namespace conan.tools.apple or what exactly do you mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

anyway, I think improvements of XCRun, if any, could be done in another, their own PR. this is outside of the scope of this PR IMO.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that generators like this one do not auto-detect anything. XCRun is deducing things by executing some commands right? That should be moved to input configuration, like the new [conf].

This is a general effort we are doing in 2.0, we are removing all the platform, architecture, compiler, env-vars detection in generators and helpers, and that should happen in the input configuration. Please try that approach better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know right now how it should look like and how it should be implemented.
if you have some example, I can take a look, but my point it belongs to XCRun, not to the meson toolchain.
in other words, as I understand, XCRun will have some code to read conf file instead of executing commands.
I don't see any reason why it should be done inside this particular PR - it is just using XCRun, and for meson toolchain it's an implementation detail how exactly it is implemented under the hood.
I also don't understand how is it going to work in CI, e.g. in case of ConanCenter, e.g. who, when and how exactly should populate conf with the input configutation?
really, I am just relying on the existing implementations of conan.tools namespace here, if they are somehow bbad/buggy or don't meet the conan 2.0 requirements, let's create an another issue and improve in another PR.

self.c = self.c or self._to_meson_value(xcrun.cc)
self.cpp = self.cpp or self._to_meson_value(xcrun.cxx)

arch = to_apple_arch(conanfile.settings.get_safe("arch"))
if arch:
self.extend_args(["-arch", arch])

os_version = conanfile.settings.get_safe("os.version")
if os_version:
flag = apple_min_version_flag(conanfile)
self.extend_args([flag])

if xcrun.sdk_path:
self.extend_args(["-isysroot", xcrun.sdk_path])
self.root = self._to_meson_value(xcrun.sdk_path)

def extend_args(self, args):
self.c_args.extend(args)
self.c_link_args.extend(args)
self.cpp_args.extend(args)
self.cpp_link_args.extend(args)
self.objc_args.extend(args)
self.objc_link_args.extend(args)
self.objcpp_args.extend(args)
self.objcpp_link_args.extend(args)


class MesonToolchain(MesonBaseToolchain, MesonAppleMixin):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, no multiple inheritance. And avoid inheritance as much as possible. Go with composition unless there is an extremely compelling case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a well-known mixin pattern?
/cc @jgsogo
suggestion on how to to it better are welcomed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@memsharded do you want me to implement AppleSystemBlock like it was done in CMakeToolchain?
https://github.com/conan-io/conan/blob/develop/conan/tools/cmake/toolchain.py#L280

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please, avoid inheritance and mixins unless it is strictly necessary. Composition scales better and is more flexible for user customization. Going with a "blocks" approach like the CMakeToolchain will be better, please go that way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am looking at how to implement blocks similarly to how it was done in CMake toolchain.
there are several ideas, and I think I'll need some advice.
first off, there are two main things Apple block should define:

  • compiler executables, c (known as CC in autotools world) and cpp (known as CXX)
  • flags for compilers and linkers, like c_args (CFLAGS), cpp_args (CXXFLAGS), link_args (LDFLAGS), etc.

for executables, it should take the very least priority (only use when not explicitly defined), for variables, it should effectively append them to any existing.

then, unlike CMake, meson machine files are simple .ini files with very limited syntax. they are not turing-complete, and cannot contain conditions, loop, and other flow control instructions. there are very few operators supported, like + and / (https://mesonbuild.com/Machine-files.html#constants), but that's it.

on the other hand, there is an interesting feature that meson supports multiple cross-files overriding each other: https://mesonbuild.com/Machine-files.html#loading-multiple-machine-files

so, we may use something like --cross-file conan_meson_cross_apple.ini --cross-file conan_meson_cross_main.ini.

therefore, there are likely 3 major approaches on how to implement that:

  1. use multiple cross-files overriding each other
  2. use a single cross-file with several blocks
  3. use a single cross-file with one block, but compose several contexts within the toolchain
  1. is easy to implement, but it might be kinda misleading: too many files to deal with for the consumer, e.g. too long command lines, and easy to mess up with an order
  2. might be tricky with a limited .ini syntax. e.g. I can't define the same value twice within the same machine file, I will get an error:
configparser.DuplicateOptionError
option 'buildtype' in section 'built-in options' already exists

and I can't check if it's already defined above, without using an external check in the code.

  1. is also easy to implement, it will be just a matter of creating several context dictionaries and merging them according to our specific rules (executables override, flags append). but it's not really a blocks approach.

looking for the advice, thanks!

def __init__(self, conanfile):
super(MesonToolchain, self).__init__(conanfile)
22 changes: 2 additions & 20 deletions conans/test/functional/toolchains/meson/test_ios.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pytest
from parameterized import parameterized

from conans.client.tools.apple import XCRun, apple_deployment_target_flag, to_apple_arch
from conans.client.tools.apple import XCRun, to_apple_arch
from conans.test.assets.sources import gen_function_cpp, gen_function_h
from conans.test.utils.tools import TestClient

Expand Down Expand Up @@ -61,32 +61,14 @@ def settings(self):
("compiler.version", "12.0"),
("compiler.libcxx", "libc++")]

def env(self):
cc = self.xcrun.cc
cxx = self.xcrun.cxx

deployment_flag = apple_deployment_target_flag(self.os, self.os_version)
sysroot_flag = " -isysroot " + self.xcrun.sdk_path
arch_flag = " -arch " + to_apple_arch(self.arch)
flags = deployment_flag + sysroot_flag + arch_flag

return {'CC': cc,
'CXX': cxx,
'CFLAGS': flags,
'CXXFLAGS': flags,
'LDFLAGS': flags}

def profile(self):
template = textwrap.dedent("""
include(default)
[settings]
{settings}
[buildenv]
{env}
""")
settings = '\n'.join(["%s = %s" % (s[0], s[1]) for s in self.settings()])
env = '\n'.join(["%s=%s" % (k, v) for k, v in self.env().items()])
return template.format(settings=settings, env=env)
return template.format(settings=settings)

@parameterized.expand([('armv8', 'iOS', '10.0', 'iphoneos'),
('armv7', 'iOS', '10.0', 'iphoneos'),
Expand Down
103 changes: 103 additions & 0 deletions conans/test/functional/toolchains/meson/test_m1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import os
import platform
import sys
import textwrap
import unittest

import pytest

from conans.client.tools.apple import XCRun
from conans.test.assets.sources import gen_function_cpp, gen_function_h
from conans.test.utils.tools import TestClient


@pytest.mark.tool_meson
@pytest.mark.skipif(sys.version_info.major == 2, reason="Meson not supported in Py2")
@pytest.mark.skipif(platform.system() != "Darwin", reason="requires Xcode")
class M1MesonTestCase(unittest.TestCase):

_conanfile_py = textwrap.dedent("""
from conans import ConanFile, tools
from conan.tools.meson import Meson, MesonToolchain


class App(ConanFile):
settings = "os", "arch", "compiler", "build_type"
options = {"shared": [True, False], "fPIC": [True, False]}
default_options = {"shared": False, "fPIC": True}

def config_options(self):
if self.settings.os == "Windows":
del self.options.fPIC

def generate(self):
tc = MesonToolchain(self)
tc.generate()

def build(self):
meson = Meson(self)
meson.configure()
meson.build()
""")

_meson_build = textwrap.dedent("""
project('tutorial', 'cpp')
add_global_arguments('-DSTRING_DEFINITION="' + get_option('STRING_DEFINITION') + '"',
language : 'cpp')
hello = library('hello', 'hello.cpp')
executable('demo', 'main.cpp', link_with: hello)
""")

_meson_options_txt = textwrap.dedent("""
option('STRING_DEFINITION', type : 'string', description : 'a string option')
""")

def settings(self):
return [("os", "Macos"),
("arch", "armv8"),
("compiler", "apple-clang"),
("compiler.version", "12.0"),
("compiler.libcxx", "libc++")]

def profile(self):
template = textwrap.dedent("""
include(default)
[settings]
{settings}
""")
settings = '\n'.join(["%s = %s" % (s[0], s[1]) for s in self.settings()])
return template.format(settings=settings)

def test_meson_toolchain(self):
self.xcrun = XCRun(None)

hello_h = gen_function_h(name="hello")
hello_cpp = gen_function_cpp(name="hello", preprocessor=["STRING_DEFINITION"])
app = gen_function_cpp(name="main", includes=["hello"], calls=["hello"])

self.t = TestClient()

self.t.save({"conanfile.py": self._conanfile_py,
"meson.build": self._meson_build,
"meson_options.txt": self._meson_options_txt,
"hello.h": hello_h,
"hello.cpp": hello_cpp,
"main.cpp": app,
"profile_host": self.profile()})

self.t.run("install . --profile:build=default --profile:host=profile_host")

self.t.run("build .")

libhello = os.path.join(self.t.current_folder, "build", "libhello.a")
self.assertTrue(os.path.isfile(libhello))
demo = os.path.join(self.t.current_folder, "build", "demo")
self.assertTrue(os.path.isfile(demo))

lipo = self.xcrun.find('lipo')

self.t.run_command('"%s" -info "%s"' % (lipo, libhello))
self.assertIn("architecture: arm64", self.t.out)

self.t.run_command('"%s" -info "%s"' % (lipo, demo))
self.assertIn("architecture: arm64", self.t.out)