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