1import os
2import platform
3
4
5from conans.client import tools
6from conans.client.build import defs_to_string, join_arguments
7from conans.client.build.autotools_environment import AutoToolsBuildEnvironment
8from conans.client.build.cppstd_flags import cppstd_from_settings
9from conans.client.tools.env import environment_append, _environment_add
10from conans.client.tools.oss import args_to_string
11from conans.errors import ConanException
12from conans.model.build_info import DEFAULT_BIN, DEFAULT_INCLUDE, DEFAULT_LIB
13from conans.model.version import Version
14from conans.util.conan_v2_mode import conan_v2_error
15from conans.util.env_reader import get_env
16from conans.util.files import decode_text, get_abs_path, mkdir
17from conans.util.runners import version_runner
18
19
20class Meson(object):
21
22    def __init__(self, conanfile, backend=None, build_type=None, append_vcvars=False):
23        """
24        :param conanfile: Conanfile instance (or settings for retro compatibility)
25        :param backend: Generator name to use or none to autodetect.
26               Possible values: ninja,vs,vs2010,vs2015,vs2017,xcode
27        :param build_type: Overrides default build type comming from settings
28        """
29        self._conanfile = conanfile
30        self._settings = conanfile.settings
31        self._append_vcvars = append_vcvars
32
33        self._os = self._ss("os")
34
35        self._compiler = self._ss("compiler")
36        conan_v2_error("compiler setting should be defined.", not self._compiler)
37
38        self._compiler_version = self._ss("compiler.version")
39
40        self._build_type = self._ss("build_type")
41
42        self.backend = backend or "ninja"  # Other backends are poorly supported, not default other.
43
44        self.options = dict()
45        if self._conanfile.package_folder:
46            self.options['prefix'] = self._conanfile.package_folder
47        self.options['libdir'] = DEFAULT_LIB
48        self.options['bindir'] = DEFAULT_BIN
49        self.options['sbindir'] = DEFAULT_BIN
50        self.options['libexecdir'] = DEFAULT_BIN
51        self.options['includedir'] = DEFAULT_INCLUDE
52
53        # C++ standard
54        cppstd = cppstd_from_settings(self._conanfile.settings)
55        cppstd_conan2meson = {
56            '98': 'c++03', 'gnu98': 'gnu++03',
57            '11': 'c++11', 'gnu11': 'gnu++11',
58            '14': 'c++14', 'gnu14': 'gnu++14',
59            '17': 'c++17', 'gnu17': 'gnu++17',
60            '20': 'c++1z', 'gnu20': 'gnu++1z'
61        }
62
63        if cppstd:
64            self.options['cpp_std'] = cppstd_conan2meson[cppstd]
65
66        # shared
67        shared = self._so("shared")
68        self.options['default_library'] = "shared" if shared is None or shared else "static"
69
70        # fpic
71        if self._os and "Windows" not in self._os:
72            fpic = self._so("fPIC")
73            if fpic is not None:
74                shared = self._so("shared")
75                self.options['b_staticpic'] = "true" if (fpic or shared) else "false"
76
77        self.build_dir = None
78        if build_type and build_type != self._build_type:
79            # Call the setter to warn and update the definitions if needed
80            self.build_type = build_type
81
82    def _ss(self, setname):
83        """safe setting"""
84        return self._conanfile.settings.get_safe(setname)
85
86    def _so(self, setname):
87        """safe option"""
88        return self._conanfile.options.get_safe(setname)
89
90    @property
91    def build_type(self):
92        return self._build_type
93
94    @build_type.setter
95    def build_type(self, build_type):
96        settings_build_type = self._settings.get_safe("build_type")
97        if build_type != settings_build_type:
98            self._conanfile.output.warn(
99                'Set build type "%s" is different than the settings build_type "%s"'
100                % (build_type, settings_build_type))
101        self._build_type = build_type
102
103    @property
104    def build_folder(self):
105        return self.build_dir
106
107    @build_folder.setter
108    def build_folder(self, value):
109        self.build_dir = value
110
111    def _get_dirs(self, source_folder, build_folder, source_dir, build_dir, cache_build_folder):
112        if (source_folder or build_folder) and (source_dir or build_dir):
113            raise ConanException("Use 'build_folder'/'source_folder'")
114
115        if source_dir or build_dir:  # OLD MODE
116            build_ret = build_dir or self.build_dir or self._conanfile.build_folder
117            source_ret = source_dir or self._conanfile.source_folder
118        else:
119            build_ret = get_abs_path(build_folder, self._conanfile.build_folder)
120            source_ret = get_abs_path(source_folder, self._conanfile.source_folder)
121
122        if self._conanfile.in_local_cache and cache_build_folder:
123            build_ret = get_abs_path(cache_build_folder, self._conanfile.build_folder)
124
125        return source_ret, build_ret
126
127    @property
128    def flags(self):
129        return defs_to_string(self.options)
130
131    def configure(self, args=None, defs=None, source_dir=None, build_dir=None,
132                  pkg_config_paths=None, cache_build_folder=None,
133                  build_folder=None, source_folder=None):
134        if not self._conanfile.should_configure:
135            return
136        args = args or []
137        defs = defs or {}
138
139        # overwrite default values with user's inputs
140        self.options.update(defs)
141
142        source_dir, self.build_dir = self._get_dirs(source_folder, build_folder,
143                                                    source_dir, build_dir,
144                                                    cache_build_folder)
145
146        if pkg_config_paths:
147            pc_paths = os.pathsep.join(get_abs_path(f, self._conanfile.install_folder)
148                                       for f in pkg_config_paths)
149        else:
150            pc_paths = self._conanfile.install_folder
151
152        mkdir(self.build_dir)
153
154        bt = {"RelWithDebInfo": "debugoptimized",
155              "MinSizeRel": "release",
156              "Debug": "debug",
157              "Release": "release"}.get(str(self.build_type), "")
158
159        build_type = "--buildtype=%s" % bt
160        arg_list = join_arguments([
161            "--backend=%s" % self.backend,
162            self.flags,
163            args_to_string(args),
164            build_type
165        ])
166        command = 'meson "%s" "%s" %s' % (source_dir, self.build_dir, arg_list)
167        with environment_append({"PKG_CONFIG_PATH": pc_paths}):
168            self._run(command)
169
170    @property
171    def _vcvars_needed(self):
172        return (self._compiler == "Visual Studio" and self.backend == "ninja" and
173                platform.system() == "Windows")
174
175    def _run(self, command):
176        def _build():
177            env_build = AutoToolsBuildEnvironment(self._conanfile)
178            with environment_append(env_build.vars):
179                self._conanfile.run(command)
180
181        if self._vcvars_needed:
182            vcvars_dict = tools.vcvars_dict(self._settings, output=self._conanfile.output)
183            with _environment_add(vcvars_dict, post=self._append_vcvars):
184                _build()
185        else:
186            _build()
187
188    def _run_ninja_targets(self, args=None, build_dir=None, targets=None):
189        if self.backend != "ninja":
190            raise ConanException("Build only supported with 'ninja' backend")
191
192        args = args or []
193        build_dir = build_dir or self.build_dir or self._conanfile.build_folder
194
195        arg_list = join_arguments([
196            '-C "%s"' % build_dir,
197            args_to_string(args),
198            args_to_string(targets)
199        ])
200        self._run("ninja %s" % arg_list)
201
202    def _run_meson_command(self, subcommand=None, args=None, build_dir=None):
203        args = args or []
204        build_dir = build_dir or self.build_dir or self._conanfile.build_folder
205
206        arg_list = join_arguments([
207            subcommand,
208            '-C "%s"' % build_dir,
209            args_to_string(args)
210        ])
211        self._run("meson %s" % arg_list)
212
213    def build(self, args=None, build_dir=None, targets=None):
214        if not self._conanfile.should_build:
215            return
216        conan_v2_error("build_type setting should be defined.", not self._build_type)
217        self._run_ninja_targets(args=args, build_dir=build_dir, targets=targets)
218
219    def install(self, args=None, build_dir=None):
220        if not self._conanfile.should_install:
221            return
222        mkdir(self._conanfile.package_folder)
223        if not self.options.get('prefix'):
224            raise ConanException("'prefix' not defined for 'meson.install()'\n"
225                                 "Make sure 'package_folder' is defined")
226        self._run_ninja_targets(args=args, build_dir=build_dir, targets=["install"])
227
228    def test(self, args=None, build_dir=None, targets=None):
229        if not self._conanfile.should_test or not get_env("CONAN_RUN_TESTS", True) or \
230           self._conanfile.conf["tools.build:skip_test"]:
231            return
232        if not targets:
233            targets = ["test"]
234        self._run_ninja_targets(args=args, build_dir=build_dir, targets=targets)
235
236    def meson_install(self, args=None, build_dir=None):
237        if not self._conanfile.should_install:
238            return
239        self._run_meson_command(subcommand='install', args=args, build_dir=build_dir)
240
241    def meson_test(self, args=None, build_dir=None):
242        if not self._conanfile.should_test or not get_env("CONAN_RUN_TESTS", True) or \
243           self._conanfile.conf["tools.build:skip_test"]:
244            return
245        self._run_meson_command(subcommand='test', args=args, build_dir=build_dir)
246
247    @staticmethod
248    def get_version():
249        try:
250            out = version_runner(["meson", "--version"])
251            version_line = decode_text(out).split('\n', 1)[0]
252            version_str = version_line.rsplit(' ', 1)[-1]
253            return Version(version_str)
254        except Exception as e:
255            raise ConanException("Error retrieving Meson version: '{}'".format(e))
256