1#!/usr/bin/env python 2 3"""Ninja toolchain abstraction""" 4 5import sys 6import os 7import subprocess 8import platform 9import random 10import string 11import json 12import zlib 13import version 14import android 15import xcode 16 17def supported_toolchains(): 18 return ['msvc', 'gcc', 'clang', 'intel'] 19 20def supported_architectures(): 21 return ['x86', 'x86-64', 'ppc', 'ppc64', 'arm6', 'arm7', 'arm64', 'mips', 'mips64', 'generic'] 22 23def get_boolean_flag(val): 24 return (val == True or val == "True" or val == "true" or val == "1" or val == 1) 25 26def make_toolchain(host, target, toolchain): 27 if toolchain is None: 28 if target.is_raspberrypi(): 29 toolchain = 'gcc' 30 elif host.is_windows() and target.is_windows(): 31 toolchain = 'msvc' 32 else: 33 toolchain = 'clang' 34 35 toolchainmodule = __import__(toolchain, globals(), locals()) 36 return toolchainmodule.create(host, target, toolchain) 37 38def make_pathhash(path, targettype): 39 return '-' + hex(zlib.adler32((path + targettype).encode()) & 0xffffffff)[2:-1] 40 41class Toolchain(object): 42 def __init__(self, host, target, toolchain): 43 self.host = host 44 self.target = target 45 self.toolchain = toolchain 46 self.subninja = '' 47 self.buildprefs = '' 48 49 #Set default values 50 self.build_monolithic = False 51 self.build_coverage = False 52 self.support_lua = False 53 self.internal_deps = False 54 self.python = 'python' 55 self.objext = '.o' 56 if target.is_windows(): 57 self.libprefix = '' 58 self.staticlibext = '.lib' 59 self.dynamiclibext = '.dll' 60 self.binprefix = '' 61 self.binext = '.exe' 62 elif target.is_android(): 63 self.libprefix = 'lib' 64 self.staticlibext = '.a' 65 self.dynamiclibext = '.so' 66 self.binprefix = 'lib' 67 self.binext = '.so' 68 else: 69 self.libprefix = 'lib' 70 self.staticlibext = '.a' 71 if target.is_macos() or target.is_ios(): 72 self.dynamiclibext = '.dylib' 73 else: 74 self.dynamiclibext = '.so' 75 self.binprefix = '' 76 self.binext = '' 77 78 #Paths 79 self.buildpath = os.path.join('build', 'ninja', target.platform) 80 self.libpath = os.path.join('lib', target.platform) 81 self.binpath = os.path.join('bin', target.platform) 82 83 #Dependency paths 84 self.depend_includepaths = [] 85 self.depend_libpaths = [] 86 87 #Target helpers 88 self.android = None 89 self.xcode = None 90 91 #Command wrappers 92 if host.is_windows(): 93 self.rmcmd = lambda p: 'cmd /C (IF exist ' + p + ' (del /F /Q ' + p + '))' 94 self.cdcmd = lambda p: 'cmd /C cd ' + p 95 self.mkdircmd = lambda p: 'cmd /C (IF NOT exist ' + p + ' (mkdir ' + p + '))' 96 self.copycmd = lambda p, q: 'cmd /C (IF exist ' + q + ' (del /F /Q ' + q + ')) & copy /Y ' + p + ' ' + q + ' > NUL' 97 else: 98 self.rmcmd = lambda p: 'rm -f ' + p 99 self.cdcmd = lambda p: 'cd ' + p 100 self.mkdircmd = lambda p: 'mkdir -p ' + p 101 self.copycmd = lambda p, q: 'cp -f ' + p + ' ' + q 102 103 #Target functionality 104 if target.is_android(): 105 self.android = android.make_target(self, host, target) 106 if target.is_macos() or target.is_ios(): 107 self.xcode = xcode.make_target(self, host, target) 108 109 #Builders 110 self.builders = {} 111 112 #Paths created 113 self.paths_created = {} 114 115 def initialize_subninja(self, path): 116 self.subninja = path 117 118 def initialize_project(self, project): 119 self.project = project 120 version.generate_version(self.project, self.project) 121 122 def initialize_archs(self, archs): 123 self.archs = list(archs) 124 if self.archs is None or self.archs == []: 125 self.initialize_default_archs() 126 127 def initialize_default_archs(self): 128 if self.target.is_windows(): 129 self.archs = ['x86-64'] 130 elif self.target.is_linux() or self.target.is_bsd(): 131 localarch = subprocess.check_output(['uname', '-m']).decode().strip() 132 if localarch == 'x86_64' or localarch == 'amd64': 133 self.archs = ['x86-64'] 134 elif localarch == 'i686': 135 self.archs = ['x86'] 136 else: 137 self.archs = [localarch] 138 elif self.target.is_macos(): 139 self.archs = ['x86-64'] 140 elif self.target.is_ios(): 141 self.archs = ['arm7', 'arm64'] 142 elif self.target.is_raspberrypi(): 143 self.archs = ['arm6'] 144 elif self.target.is_android(): 145 self.archs = ['arm7', 'arm64', 'x86', 'x86-64'] #'mips', 'mips64' 146 elif self.target.is_tizen(): 147 self.archs = ['x86', 'arm7'] 148 149 def initialize_configs(self, configs): 150 self.configs = list(configs) 151 if self.configs is None or self.configs == []: 152 self.initialize_default_configs() 153 154 def initialize_default_configs(self): 155 self.configs = ['debug', 'release']#, 'profile', 'deploy'] 156 157 def initialize_toolchain(self): 158 if self.android != None: 159 self.android.initialize_toolchain() 160 if self.xcode != None: 161 self.xcode.initialize_toolchain() 162 163 def initialize_depends(self, dependlibs): 164 for lib in dependlibs: 165 includepath = '' 166 libpath = '' 167 testpaths = [ 168 os.path.join('..', lib), 169 os.path.join('..', lib + '_lib') 170 ] 171 for testpath in testpaths: 172 if os.path.isfile(os.path.join(testpath, lib, lib + '.h')): 173 if self.subninja != '': 174 basepath, _ = os.path.split(self.subninja) 175 _, libpath = os.path.split(testpath) 176 testpath = os.path.join(basepath, libpath) 177 includepath = testpath 178 libpath = testpath 179 break 180 if includepath == '': 181 print("Unable to locate dependent lib: " + lib) 182 sys.exit(-1) 183 else: 184 self.depend_includepaths += [includepath] 185 if self.subninja == '': 186 self.depend_libpaths += [libpath] 187 188 def build_toolchain(self): 189 if self.android != None: 190 self.android.build_toolchain() 191 if self.xcode != None: 192 self.xcode.build_toolchain() 193 194 def parse_default_variables(self, variables): 195 if not variables: 196 return 197 if isinstance(variables, dict): 198 iterator = iter(variables.items()) 199 else: 200 iterator = iter(variables) 201 for key, val in iterator: 202 if key == 'monolithic': 203 self.build_monolithic = get_boolean_flag(val) 204 elif key == 'coverage': 205 self.build_coverage = get_boolean_flag(val) 206 elif key == 'support_lua': 207 self.support_lua = get_boolean_flag(val) 208 elif key == 'internal_deps': 209 self.internal_deps = get_boolean_flag(val) 210 if self.xcode != None: 211 self.xcode.parse_default_variables(variables) 212 213 def read_build_prefs(self): 214 self.read_prefs('build.json') 215 self.read_prefs(os.path.join('build', 'ninja', 'build.json')) 216 if self.buildprefs != '': 217 self.read_prefs(self.buildprefs) 218 219 def read_prefs(self, filename): 220 if not os.path.isfile( filename ): 221 return 222 file = open(filename, 'r') 223 prefs = json.load(file) 224 file.close() 225 self.parse_prefs(prefs) 226 227 def parse_prefs(self, prefs): 228 if 'monolithic' in prefs: 229 self.build_monolithic = get_boolean_flag(prefs['monolithic']) 230 if 'coverage' in prefs: 231 self.build_coverage = get_boolean_flag( prefs['coverage'] ) 232 if 'support_lua' in prefs: 233 self.support_lua = get_boolean_flag(prefs['support_lua']) 234 if 'python' in prefs: 235 self.python = prefs['python'] 236 if self.android != None: 237 self.android.parse_prefs(prefs) 238 if self.xcode != None: 239 self.xcode.parse_prefs(prefs) 240 241 def archs(self): 242 return self.archs 243 244 def configs(self): 245 return self.configs 246 247 def project(self): 248 return self.project 249 250 def is_monolithic(self): 251 return self.build_monolithic 252 253 def use_coverage(self): 254 return self.build_coverage 255 256 def write_variables(self, writer): 257 writer.variable('buildpath', self.buildpath) 258 writer.variable('target', self.target.platform) 259 writer.variable('config', '') 260 if self.android != None: 261 self.android.write_variables(writer) 262 if self.xcode != None: 263 self.xcode.write_variables(writer) 264 265 def write_rules(self, writer): 266 writer.pool('serial_pool', 1) 267 writer.rule('copy', command = self.copycmd('$in', '$out'), description = 'COPY $in -> $out') 268 writer.rule('mkdir', command = self.mkdircmd('$out'), description = 'MKDIR $out') 269 if self.android != None: 270 self.android.write_rules(writer) 271 if self.xcode != None: 272 self.xcode.write_rules(writer) 273 274 def cdcmd(self): 275 return self.cdcmd 276 277 def mkdircmd(self): 278 return self.mkdircmd 279 280 def mkdir(self, writer, path, implicit = None, order_only = None): 281 if path in self.paths_created: 282 return self.paths_created[path] 283 if self.subninja != '': 284 return 285 cmd = writer.build(path, 'mkdir', None, implicit = implicit, order_only = order_only) 286 self.paths_created[path] = cmd 287 return cmd 288 289 def copy(self, writer, src, dst, implicit = None, order_only = None): 290 return writer.build(dst, 'copy', src, implicit = implicit, order_only = order_only) 291 292 def builder_multicopy(self, writer, config, archs, targettype, infiles, outpath, variables): 293 output = [] 294 rootdir = self.mkdir(writer, outpath) 295 for file in infiles: 296 path, targetfile = os.path.split(file) 297 archpath = outpath 298 #Find which arch we are copying from and append to target path 299 #unless on generic arch targets, then re-add if not self.target.is_generic(): 300 for arch in archs: 301 remainpath, subdir = os.path.split(path) 302 while remainpath != '': 303 if subdir == arch: 304 archpath = os.path.join(outpath, arch) 305 break 306 remainpath, subdir = os.path.split(remainpath) 307 if remainpath != '': 308 break 309 targetpath = os.path.join(archpath, targetfile) 310 if os.path.normpath(file) != os.path.normpath(targetpath): 311 archdir = self.mkdir(writer, archpath, implicit = rootdir) 312 output += self.copy(writer, file, targetpath, order_only = archdir) 313 return output 314 315 def path_escape(self, path): 316 if self.host.is_windows(): 317 return "\"%s\"" % path.replace("\"", "'") 318 return path 319 320 def paths_forward_slash(self, paths): 321 return [path.replace('\\', '/') for path in paths] 322 323 def prefix_includepath(self, path): 324 if os.path.isabs(path) or self.subninja == '': 325 return path 326 if path == '.': 327 return self.subninja 328 return os.path.join(self.subninja, path) 329 330 def prefix_includepaths(self, includepaths): 331 return [self.prefix_includepath(path) for path in includepaths] 332 333 def list_per_config(self, config_dicts, config): 334 if config_dicts is None: 335 return [] 336 config_list = [] 337 for config_dict in config_dicts: 338 config_list += config_dict[config] 339 return config_list 340 341 def implicit_deps(self, config, variables): 342 if variables == None: 343 return None 344 if 'implicit_deps' in variables: 345 return self.list_per_config(variables['implicit_deps'], config) 346 return None 347 348 def make_implicit_deps(self, outpath, arch, config, dependlibs): 349 deps = {} 350 deps[config] = [] 351 for lib in dependlibs: 352 if self.target.is_macos() or self.target.is_ios(): 353 finalpath = os.path.join(self.libpath, config, self.libprefix + lib + self.staticlibext) 354 else: 355 finalpath = os.path.join(self.libpath, config, arch, self.libprefix + lib + self.staticlibext) 356 deps[config] += [finalpath] 357 return [deps] 358 359 def compile_file(self, writer, config, arch, targettype, infile, outfile, variables): 360 extension = os.path.splitext(infile)[1][1:] 361 if extension in self.builders: 362 return self.builders[extension](writer, config, arch, targettype, infile, outfile, variables) 363 return [] 364 365 def compile_node(self, writer, nodetype, config, arch, infiles, outfile, variables): 366 if nodetype in self.builders: 367 return self.builders[nodetype](writer, config, arch, nodetype, infiles, outfile, variables) 368 return [] 369 370 def build_sources(self, writer, nodetype, multitype, module, sources, binfile, basepath, outpath, configs, includepaths, libpaths, dependlibs, libs, implicit_deps, variables, frameworks): 371 if module != '': 372 decoratedmodule = module + make_pathhash(self.subninja + module + binfile, nodetype) 373 else: 374 decoratedmodule = basepath + make_pathhash(self.subninja + basepath + binfile, nodetype) 375 built = {} 376 if includepaths is None: 377 includepaths = [] 378 if libpaths is None: 379 libpaths = [] 380 sourcevariables = (variables or {}).copy() 381 sourcevariables.update({ 382 'includepaths': self.depend_includepaths + self.prefix_includepaths(list(includepaths))}) 383 if not libs and dependlibs != None: 384 libs = [] 385 if dependlibs != None: 386 libs = (dependlibs or []) + libs 387 nodevariables = (variables or {}).copy() 388 nodevariables.update({ 389 'libs': libs, 390 'implicit_deps': implicit_deps, 391 'libpaths': self.depend_libpaths + list(libpaths), 392 'frameworks': frameworks}) 393 self.module = module 394 self.buildtarget = binfile 395 for config in configs: 396 archnodes = [] 397 built[config] = [] 398 for arch in self.archs: 399 objs = [] 400 buildpath = os.path.join('$buildpath', config, arch) 401 modulepath = os.path.join(buildpath, basepath, decoratedmodule) 402 sourcevariables['modulepath'] = modulepath 403 nodevariables['modulepath'] = modulepath 404 #Make per-arch-and-config list of final implicit deps, including dependent libs 405 if self.internal_deps and dependlibs != None: 406 dep_implicit_deps = [] 407 if implicit_deps: 408 dep_implicit_deps += implicit_deps 409 dep_implicit_deps += self.make_implicit_deps(outpath, arch, config, dependlibs) 410 nodevariables['implicit_deps'] = dep_implicit_deps 411 #Compile all sources 412 for name in sources: 413 if os.path.isabs(name): 414 infile = name 415 outfile = os.path.join(modulepath, os.path.splitext(os.path.basename(name))[0] + make_pathhash(infile, nodetype) + self.objext) 416 else: 417 infile = os.path.join(basepath, module, name) 418 outfile = os.path.join(modulepath, os.path.splitext(name)[0] + make_pathhash(infile, nodetype) + self.objext) 419 if self.subninja != '': 420 infile = os.path.join(self.subninja, infile) 421 objs += self.compile_file(writer, config, arch, nodetype, infile, outfile, sourcevariables) 422 #Build arch node (per-config-and-arch binary) 423 archoutpath = os.path.join(modulepath, binfile) 424 archnodes += self.compile_node(writer, nodetype, config, arch, objs, archoutpath, nodevariables) 425 #Build final config node (per-config binary) 426 built[config] += self.compile_node(writer, multitype, config, self.archs, archnodes, os.path.join(outpath, config), None) 427 writer.newline() 428 return built 429 430 def lib(self, writer, module, sources, libname, basepath, configs, includepaths, variables, outpath = None): 431 built = {} 432 if basepath == None: 433 basepath = '' 434 if configs is None: 435 configs = list(self.configs) 436 if libname is None: 437 libname = module 438 libfile = self.libprefix + libname + self.staticlibext 439 if outpath is None: 440 outpath = self.libpath 441 return self.build_sources(writer, 'lib', 'multilib', module, sources, libfile, basepath, outpath, configs, includepaths, None, None, None, None, variables, None) 442 443 def sharedlib(self, writer, module, sources, libname, basepath, configs, includepaths, libpaths, implicit_deps, dependlibs, libs, frameworks, variables, outpath = None): 444 built = {} 445 if basepath == None: 446 basepath = '' 447 if configs is None: 448 configs = list(self.configs) 449 if libname is None: 450 libname = module 451 libfile = self.libprefix + libname + self.dynamiclibext 452 if outpath is None: 453 outpath = self.binpath 454 return self.build_sources(writer, 'sharedlib', 'multisharedlib', module, sources, libfile, basepath, outpath, configs, includepaths, libpaths, dependlibs, libs, implicit_deps, variables, frameworks) 455 456 def bin(self, writer, module, sources, binname, basepath, configs, includepaths, libpaths, implicit_deps, dependlibs, libs, frameworks, variables, outpath = None): 457 built = {} 458 if basepath == None: 459 basepath = '' 460 if configs is None: 461 configs = list(self.configs) 462 binfile = self.binprefix + binname + self.binext 463 if outpath is None: 464 outpath = self.binpath 465 return self.build_sources(writer, 'bin', 'multibin', module, sources, binfile, basepath, outpath, configs, includepaths, libpaths, dependlibs, libs, implicit_deps, variables, frameworks) 466 467 def app(self, writer, module, sources, binname, basepath, configs, includepaths, libpaths, implicit_deps, dependlibs, libs, frameworks, variables, resources): 468 builtbin = [] 469 # Filter out platforms that do not have app concept 470 if not (self.target.is_macos() or self.target.is_ios() or self.target.is_android() or self.target.is_tizen()): 471 return builtbin 472 if basepath is None: 473 basepath = '' 474 if binname is None: 475 binname = module 476 if configs is None: 477 configs = list(self.configs) 478 for config in configs: 479 archbins = self.bin(writer, module, sources, binname, basepath, [config], includepaths, libpaths, implicit_deps, dependlibs, libs, frameworks, variables, '$buildpath') 480 if self.target.is_macos() or self.target.is_ios(): 481 binpath = os.path.join(self.binpath, config, binname + '.app') 482 builtbin += self.xcode.app(self, writer, module, archbins, self.binpath, binname, basepath, config, None, resources, True) 483 if self.target.is_android(): 484 javasources = [name for name in sources if name.endswith('.java')] 485 builtbin += self.android.apk(self, writer, module, archbins, javasources, self.binpath, binname, basepath, config, None, resources) 486 #elif self.target.is_tizen(): 487 # builtbin += self.tizen.tpk( writer, config, basepath, module, binname = binname, archbins = archbins, resources = resources ) 488 return builtbin 489