generate_make.py revision 5821806d5e7f356e8fa4b058a389a808ea183019
1#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import buildbot_common
7import optparse
8import os
9import sys
10from buildbot_common import ErrorExit
11from make_rules import MakeRules, SetVar, GenerateCleanRules, GenerateNMFRules
12
13SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
14SDK_SRC_DIR = os.path.dirname(SCRIPT_DIR)
15SDK_EXAMPLE_DIR = os.path.join(SDK_SRC_DIR, 'examples')
16SDK_DIR = os.path.dirname(SDK_SRC_DIR)
17SRC_DIR = os.path.dirname(SDK_DIR)
18OUT_DIR = os.path.join(SRC_DIR, 'out')
19PPAPI_DIR = os.path.join(SRC_DIR, 'ppapi')
20
21use_gyp = False
22
23# Add SDK make tools scripts to the python path.
24sys.path.append(os.path.join(SDK_SRC_DIR, 'tools'))
25import getos
26
27def Replace(text, replacements):
28  for key, val in replacements.items():
29    if val is not None:
30      text = text.replace(key, val)
31  return text
32
33
34def WriteReplaced(srcpath, dstpath, replacements):
35  text = open(srcpath, 'rb').read()
36  text = Replace(text, replacements)
37  open(dstpath, 'wb').write(text)
38
39
40def ShouldProcessHTML(desc):
41  return desc['DEST'] in ('examples', 'tests')
42
43
44def GenerateSourceCopyList(desc):
45  sources = []
46  # Add sources for each target
47  for target in desc['TARGETS']:
48    sources.extend(target['SOURCES'])
49
50  # And HTML and data files
51  sources.extend(desc.get('DATA', []))
52
53  if ShouldProcessHTML(desc):
54    sources.append('common.js')
55
56  return sources
57
58
59def GetSourcesDict(sources):
60  source_map = {}
61  for key in ['.c', '.cc']:
62    source_list = [fname for fname in sources if fname.endswith(key)]
63    if source_list:
64      source_map[key] = source_list
65    else:
66      source_map[key] = []
67  return source_map
68
69
70def GetProjectObjects(source_dict):
71  object_list = []
72  for key in ['.c', '.cc']:
73    for src in source_dict[key]:
74      object_list.append(os.path.splitext(src)[0])
75  return object_list
76
77
78def GetPlatforms(plat_list, plat_filter):
79  platforms = []
80  for plat in plat_list:
81    if plat in plat_filter:
82      platforms.append(plat)
83  return platforms
84
85
86def GenerateToolDefaults(tools):
87  defaults = ''
88  for tool in tools:
89    defaults += MakeRules(tool).BuildDefaults()
90  return defaults
91
92
93def GenerateSettings(desc, tools):
94  settings = SetVar('VALID_TOOLCHAINS', tools)
95  settings += 'TOOLCHAIN?=%s\n\n' % tools[0]
96  for target in desc['TARGETS']:
97    project = target['NAME']
98    macro = project.upper()
99
100    c_flags = target.get('CCFLAGS')
101    cc_flags = target.get('CXXFLAGS')
102    ld_flags = target.get('LDFLAGS')
103
104    if c_flags:
105      settings += SetVar(macro + '_CCFLAGS', c_flags)
106    if cc_flags:
107      settings += SetVar(macro + '_CXXFLAGS', cc_flags)
108    if ld_flags:
109      settings += SetVar(macro + '_LDFLAGS', ld_flags)
110  return settings
111
112
113def GenerateRules(desc, tools):
114  rules = '#\n# Per target object lists\n#\n'
115
116  #Determine which projects are in the NMF files.
117  executable = None
118  dlls = []
119  project_list = []
120  glibc_rename = []
121
122  for target in desc['TARGETS']:
123    ptype = target['TYPE'].upper()
124    project = target['NAME']
125    project_list.append(project)
126    srcs = GetSourcesDict(target['SOURCES'])
127    if ptype == 'MAIN':
128      executable = project
129    if ptype == 'SO':
130      dlls.append(project)
131      for arch in ['x86_32', 'x86_64']:
132        glibc_rename.append('-n %s_%s.so,%s.so' % (project, arch, project))
133
134    objects = GetProjectObjects(srcs)
135    rules += SetVar('%s_OBJS' % project.upper(), objects)
136    if glibc_rename:
137      rules += SetVar('GLIBC_REMAP', glibc_rename)
138
139  configs = desc.get('CONFIGS', ['Debug', 'Release'])
140  for tc in tools:
141    makeobj = MakeRules(tc)
142    arches = makeobj.GetArches()
143    rules += makeobj.BuildDirectoryRules(configs)
144    for cfg in configs:
145      makeobj.SetConfig(cfg)
146      for target in desc['TARGETS']:
147        project = target['NAME']
148        ptype = target['TYPE']
149        srcs = GetSourcesDict(target['SOURCES'])
150        defs = target.get('DEFINES', [])
151        incs = target.get('INCLUDES', [])
152        libs = target.get('LIBS', [])
153        makeobj.SetProject(project, ptype, defs=defs, incs=incs, libs=libs)
154        if ptype == 'main':
155          rules += makeobj.GetPepperPlugin()
156        for arch in arches:
157          makeobj.SetArch(arch)
158          for src in srcs.get('.c', []):
159            rules += makeobj.BuildCompileRule('CC', src)
160          for src in srcs.get('.cc', []):
161            rules += makeobj.BuildCompileRule('CXX', src)
162          rules += '\n'
163          rules += makeobj.BuildObjectList()
164          rules += makeobj.BuildLinkRule()
165      if executable:
166        rules += GenerateNMFRules(tc, executable, dlls, cfg, arches)
167
168  rules += GenerateCleanRules(tools, configs)
169  rules += '\nall: $(ALL_TARGETS)\n'
170
171  return '', rules
172
173
174
175def GenerateReplacements(desc, tools):
176  # Generate target settings
177  settings = GenerateSettings(desc, tools)
178  tool_def = GenerateToolDefaults(tools)
179  _, rules = GenerateRules(desc, tools)
180
181  prelaunch = desc.get('LAUNCH', '')
182  prerun = desc.get('PRE', '')
183  postlaunch = desc.get('POST', '')
184
185  target_def = 'all:'
186
187  return {
188      '__PROJECT_SETTINGS__' : settings,
189      '__PROJECT_TARGETS__' : target_def,
190      '__PROJECT_TOOLS__' : tool_def,
191      '__PROJECT_RULES__' : rules,
192      '__PROJECT_PRELAUNCH__' : prelaunch,
193      '__PROJECT_PRERUN__' : prerun,
194      '__PROJECT_POSTLAUNCH__' : postlaunch
195  }
196
197
198# 'KEY' : ( <TYPE>, [Accepted Values], <Required?>)
199DSC_FORMAT = {
200    'TOOLS' : (list, ['newlib', 'glibc', 'pnacl', 'win', 'linux'], True),
201    'CONFIGS' : (list, ['Debug', 'Release'], False),
202    'PREREQ' : (list, '', False),
203    'TARGETS' : (list, {
204        'NAME': (str, '', True),
205        'TYPE': (str, ['main', 'nexe', 'lib', 'so'], True),
206        'SOURCES': (list, '', True),
207        'CCFLAGS': (list, '', False),
208        'CXXFLAGS': (list, '', False),
209        'DEFINES': (list, '', False),
210        'LDFLAGS': (list, '', False),
211        'INCLUDES': (list, '', False),
212        'LIBS' : (list, '', False)
213    }, True),
214    'HEADERS': (list, {
215        'FILES': (list, '', True),
216        'DEST': (str, '', True),
217    }, False),
218    'SEARCH': (list, '', False),
219    'POST': (str, '', False),
220    'PRE': (str, '', False),
221    'DEST': (str, ['examples', 'src', 'testlibs', 'tests'], True),
222    'NAME': (str, '', False),
223    'DATA': (list, '', False),
224    'TITLE': (str, '', False),
225    'DESC': (str, '', False),
226    'INFO': (str, '', False),
227    'EXPERIMENTAL': (bool, [True, False], False)
228}
229
230
231def ErrorMsgFunc(text):
232  sys.stderr.write(text + '\n')
233
234
235def ValidateFormat(src, dsc_format, ErrorMsg=ErrorMsgFunc):
236  failed = False
237
238  # Verify all required keys are there
239  for key in dsc_format:
240    (exp_type, exp_value, required) = dsc_format[key]
241    if required and key not in src:
242      ErrorMsg('Missing required key %s.' % key)
243      failed = True
244
245  # For each provided key, verify it's valid
246  for key in src:
247    # Verify the key is known
248    if key not in dsc_format:
249      ErrorMsg('Unexpected key %s.' % key)
250      failed = True
251      continue
252
253    exp_type, exp_value, required = dsc_format[key]
254    value = src[key]
255
256    # Verify the key is of the expected type
257    if exp_type != type(value):
258      ErrorMsg('Key %s expects %s not %s.' % (
259          key, exp_type.__name__.upper(), type(value).__name__.upper()))
260      failed = True
261      continue
262
263    # Verify the value is non-empty if required
264    if required and not value:
265      ErrorMsg('Expected non-empty value for %s.' % key)
266      failed = True
267      continue
268
269    # If it's a bool, the expected values are always True or False.
270    if exp_type is bool:
271      continue
272
273    # If it's a string and there are expected values, make sure it matches
274    if exp_type is str:
275      if type(exp_value) is list and exp_value:
276        if value not in exp_value:
277          ErrorMsg('Value %s not expected for %s.' % (value, key))
278          failed = True
279      continue
280
281    # if it's a list, then we need to validate the values
282    if exp_type is list:
283      # If we expect a dictionary, then call this recursively
284      if type(exp_value) is dict:
285        for val in value:
286          if not ValidateFormat(val, exp_value, ErrorMsg):
287            failed = True
288        continue
289      # If we expect a list of strings
290      if type(exp_value) is str:
291        for val in value:
292          if type(val) is not str:
293            ErrorMsg('Value %s in %s is not a string.' % (val, key))
294            failed = True
295        continue
296      # if we expect a particular string
297      if type(exp_value) is list:
298        for val in value:
299          if val not in exp_value:
300            ErrorMsg('Value %s not expected in %s.' % (val, key))
301            failed = True
302        continue
303
304    # If we got this far, it's an unexpected type
305    ErrorMsg('Unexpected type %s for key %s.' % (str(type(src[key])), key))
306    continue
307  return not failed
308
309
310def AddMakeBat(pepperdir, makepath):
311  """Create a simple batch file to execute Make.
312
313  Creates a simple batch file named make.bat for the Windows platform at the
314  given path, pointing to the Make executable in the SDK."""
315
316  makepath = os.path.abspath(makepath)
317  if not makepath.startswith(pepperdir):
318    ErrorExit('Make.bat not relative to Pepper directory: ' + makepath)
319
320  makeexe = os.path.abspath(os.path.join(pepperdir, 'tools'))
321  relpath = os.path.relpath(makeexe, makepath)
322
323  fp = open(os.path.join(makepath, 'make.bat'), 'wb')
324  outpath = os.path.join(relpath, 'make.exe')
325
326  # Since make.bat is only used by Windows, for Windows path style
327  outpath = outpath.replace(os.path.sep, '\\')
328  fp.write('@%s %%*\n' % outpath)
329  fp.close()
330
331
332def FindFile(name, srcroot, srcdirs):
333  checks = []
334  for srcdir in srcdirs:
335    srcfile = os.path.join(srcroot, srcdir, name)
336    srcfile = os.path.abspath(srcfile)
337    if os.path.exists(srcfile):
338      return srcfile
339    else:
340      checks.append(srcfile)
341
342  ErrorMsgFunc('%s not found in:\n\t%s' % (name, '\n\t'.join(checks)))
343  return None
344
345
346def IsNexe(desc):
347  for target in desc['TARGETS']:
348    if target['TYPE'] == 'main':
349      return True
350  return False
351
352
353def ProcessHTML(srcroot, dstroot, desc, toolchains):
354  name = desc['NAME']
355  outdir = os.path.join(dstroot, desc['DEST'], name)
356  srcfile = os.path.join(srcroot, 'index.html')
357  tools = GetPlatforms(toolchains, desc['TOOLS'])
358
359  if use_gyp and getos.GetPlatform() != 'win':
360    configs = ['debug', 'release']
361  else:
362    configs = ['Debug', 'Release']
363
364  for tool in tools:
365    for cfg in configs:
366      dstfile = os.path.join(outdir, 'index_%s_%s.html' % (tool, cfg))
367      print 'Writing from %s to %s' % (srcfile, dstfile)
368      if use_gyp:
369        path = "build/%s-%s" % (tool, cfg)
370      else:
371        path = "%s/%s" % (tool, cfg)
372      replace = {
373        '<path>': path,
374        '<NAME>': name,
375        '<TITLE>': desc['TITLE'],
376        '<tc>': tool
377      }
378      WriteReplaced(srcfile, dstfile, replace)
379
380  replace['<tc>'] = tools[0]
381  replace['<config>'] = configs[0]
382
383  srcfile = os.path.join(SDK_SRC_DIR, 'build_tools', 'redirect.html')
384  dstfile = os.path.join(outdir, 'index.html')
385  WriteReplaced(srcfile, dstfile, replace)
386
387
388def LoadProject(filename, toolchains):
389  """Generate a Master Makefile that builds all examples.
390
391  Load a project desciption file, verifying it conforms and checking
392  if it matches the set of requested toolchains.  Return None if the
393  project is filtered out."""
394
395  print '\n\nProcessing %s...' % filename
396  # Default src directory is the directory the description was found in
397  desc = open(filename, 'r').read()
398  desc = eval(desc, {}, {})
399
400  # Verify the format of this file
401  if not ValidateFormat(desc, DSC_FORMAT):
402    ErrorExit('Failed to validate: ' + filename)
403
404  # Check if we are actually interested in this example
405  match = False
406  for toolchain in toolchains:
407    if toolchain in desc['TOOLS']:
408      match = True
409      break
410  if not match:
411    return None
412
413  desc['FILENAME'] = filename
414  return desc
415
416
417def FindAndCopyFiles(src_files, root, search_dirs, dst_dir):
418  buildbot_common.MakeDir(dst_dir)
419  for src_name in src_files:
420    src_file = FindFile(src_name, root, search_dirs)
421    if not src_file:
422      ErrorExit('Failed to find: ' + src_name)
423    dst_file = os.path.join(dst_dir, src_name)
424    if os.path.exists(dst_file):
425      if os.stat(src_file).st_mtime <= os.stat(dst_file).st_mtime:
426        print 'Skipping "%s", destination "%s" is newer.' % (src_file, dst_file)
427        continue
428    buildbot_common.CopyFile(src_file, dst_file)
429
430
431def ProcessProject(srcroot, dstroot, desc, toolchains):
432  name = desc['NAME']
433  out_dir = os.path.join(dstroot, desc['DEST'], name)
434  buildbot_common.MakeDir(out_dir)
435  srcdirs = desc.get('SEARCH', ['.', '..'])
436
437  # Copy sources to example directory
438  sources = GenerateSourceCopyList(desc)
439  FindAndCopyFiles(sources, srcroot, srcdirs, out_dir)
440
441  # Copy public headers to the include directory.
442  for headers_set in desc.get('HEADERS', []):
443    headers = headers_set['FILES']
444    header_out_dir = os.path.join(dstroot, headers_set['DEST'])
445    FindAndCopyFiles(headers, srcroot, srcdirs, header_out_dir)
446
447  make_path = os.path.join(out_dir, 'Makefile')
448
449  if use_gyp:
450    # Process the dsc file to produce gyp input
451    dsc = desc['FILENAME']
452    dsc2gyp = os.path.join(SDK_SRC_DIR, 'build_tools/dsc2gyp.py')
453    gypfile = os.path.join(OUT_DIR, 'tmp', name, name + '.gyp')
454    buildbot_common.Run([sys.executable, dsc2gyp, dsc, '-o', gypfile],
455                        cwd=out_dir)
456
457    # Run gyp on the generated gyp file
458    if sys.platform == 'win32':
459      generator = 'msvs'
460    else:
461      generator = os.path.join(SCRIPT_DIR, "make_simple.py")
462    gyp = os.path.join(SDK_SRC_DIR, '..', '..', 'tools', 'gyp', 'gyp')
463    if sys.platform == 'win32':
464      gyp += '.bat'
465    buildbot_common.Run([gyp, '-Gstandalone', '--format',  generator,
466                        '--toplevel-dir=.', gypfile], cwd=out_dir)
467
468  if sys.platform == 'win32' or not use_gyp:
469    if IsNexe(desc):
470      template = os.path.join(SCRIPT_DIR, 'template.mk')
471    else:
472      template = os.path.join(SCRIPT_DIR, 'library.mk')
473
474    tools = []
475    for tool in desc['TOOLS']:
476      if tool in toolchains:
477        tools.append(tool)
478
479
480    # Add Makefile and make.bat
481    repdict = GenerateReplacements(desc, tools)
482    WriteReplaced(template, make_path, repdict)
483
484  outdir = os.path.dirname(os.path.abspath(make_path))
485  pepperdir = os.path.dirname(os.path.dirname(outdir))
486  AddMakeBat(pepperdir, outdir)
487  return (name, desc['DEST'])
488
489
490def GenerateMasterMakefile(in_path, out_path, projects):
491  """Generate a Master Makefile that builds all examples. """
492  project_names = [project['NAME'] for project in projects]
493
494  # TODO(binji): This is kind of a hack; we use the target's LIBS to determine
495  # dependencies. This project-level dependency is then injected into the
496  # master Makefile.
497  dependencies = []
498  for project in projects:
499    project_deps_set = set()
500    for target in project['TARGETS']:
501      target_libs = target.get('LIBS', [])
502      dependent_libs = set(target_libs) & set(project_names)
503      project_deps_set.update(dependent_libs)
504
505    if project_deps_set:
506      # If project foo depends on projects bar and baz, generate:
507      #   "foo_TARGET: bar_TARGET baz_TARGET"
508      # _TARGET is appended for all targets in the master makefile template.
509      project_deps = ' '.join(p + '_TARGET' for p in project_deps_set)
510      project_deps_string = '%s_TARGET: %s' % (project['NAME'], project_deps)
511      dependencies.append(project_deps_string)
512
513  dependencies_string = '\n'.join(dependencies)
514
515  replace = {
516      '__PROJECT_LIST__' : SetVar('PROJECTS', project_names),
517      '__DEPENDENCIES__': dependencies_string,
518  }
519
520  WriteReplaced(in_path, out_path, replace)
521
522  outdir = os.path.dirname(os.path.abspath(out_path))
523  pepperdir = os.path.dirname(outdir)
524  AddMakeBat(pepperdir, outdir)
525
526
527def main(argv):
528  parser = optparse.OptionParser()
529  parser.add_option('--dstroot', help='Set root for destination.',
530      dest='dstroot', default=os.path.join(OUT_DIR, 'pepper_canary'))
531  parser.add_option('--master', help='Create master Makefile.',
532      action='store_true', dest='master', default=False)
533  parser.add_option('--newlib', help='Create newlib examples.',
534      action='store_true', dest='newlib', default=False)
535  parser.add_option('--glibc', help='Create glibc examples.',
536      action='store_true', dest='glibc', default=False)
537  parser.add_option('--pnacl', help='Create pnacl examples.',
538      action='store_true', dest='pnacl', default=False)
539  parser.add_option('--host', help='Create host examples.',
540      action='store_true', dest='host', default=False)
541  parser.add_option('--experimental', help='Create experimental examples.',
542      action='store_true', dest='experimental', default=False)
543
544  toolchains = []
545  platform = getos.GetPlatform()
546
547  options, args = parser.parse_args(argv)
548  if options.newlib:
549    toolchains.append('newlib')
550  if options.glibc:
551    toolchains.append('glibc')
552  if options.pnacl:
553    toolchains.append('pnacl')
554  if options.host:
555    toolchains.append(platform)
556
557  if not args:
558    ErrorExit('Please specify one or more projects to generate Makefiles for.')
559
560  # By default support newlib and glibc
561  if not toolchains:
562    toolchains = ['newlib', 'glibc']
563    print 'Using default toolchains: ' + ' '.join(toolchains)
564
565  master_projects = {}
566
567  for filename in args:
568    desc = LoadProject(filename, toolchains)
569    if not desc:
570      print 'Skipping %s, not in [%s].' % (filename, ', '.join(toolchains))
571      continue
572
573    if desc.get('EXPERIMENTAL', False) and not options.experimental:
574      print 'Skipping %s, experimental only.' % (filename,)
575      continue
576
577    srcroot = os.path.dirname(os.path.abspath(filename))
578    if not ProcessProject(srcroot, options.dstroot, desc, toolchains):
579      ErrorExit('\n*** Failed to process project: %s ***' % filename)
580
581    # if this is an example update the html
582    if ShouldProcessHTML(desc):
583      ProcessHTML(srcroot, options.dstroot, desc, toolchains)
584
585    # Create a list of projects for each DEST. This will be used to generate a
586    # master makefile.
587    master_projects.setdefault(desc['DEST'], []).append(desc)
588
589  if options.master:
590    if use_gyp:
591      master_in = os.path.join(SDK_EXAMPLE_DIR, 'Makefile_gyp')
592    else:
593      master_in = os.path.join(SDK_EXAMPLE_DIR, 'Makefile')
594    for dest, projects in master_projects.iteritems():
595      master_out = os.path.join(options.dstroot, dest, 'Makefile')
596      GenerateMasterMakefile(master_in, master_out, projects)
597
598  return 0
599
600
601if __name__ == '__main__':
602  sys.exit(main(sys.argv[1:]))
603