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