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