ambuild2/frontend/cpp.py
author David Anderson <dvander@alliedmods.net>
Sat Feb 08 20:40:05 2014 -0800 (2014-02-08)
changeset 238 2e3728b08095
parent 237 a3ab09e0556b
child 239 b7f3bcd06c99
permissions -rw-r--r--
Add /FS for /Zi builds on Visual Studio 2013.
     1 # vim: set ts=8 sts=2 sw=2 tw=99 et:
     2 #
     3 # This file is part of AMBuild.
     4 # 
     5 # AMBuild is free software: you can redistribute it and/or modify
     6 # it under the terms of the GNU General Public License as published by
     7 # the Free Software Foundation, either version 3 of the License, or
     8 # (at your option) any later version.
     9 # 
    10 # AMBuild is distributed in the hope that it will be useful,
    11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
    12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    13 # GNU General Public License for more details.
    14 # 
    15 # You should have received a copy of the GNU General Public License
    16 # along with AMBuild. If not, see <http://www.gnu.org/licenses/>.
    17 from __future__ import print_function
    18 import subprocess
    19 import re, os, copy
    20 from ambuild2 import util
    21 
    22 class Vendor(object):
    23   def __init__(self, name, version, behavior, command, objSuffix):
    24     self.name = name
    25     self.version = version
    26     self.behavior = behavior
    27     self.command = command
    28     self.objSuffix = objSuffix
    29     self.debuginfo_argv = []
    30 
    31 class MSVC(Vendor):
    32   def __init__(self, command, version):
    33     super(MSVC, self).__init__('msvc', version, 'msvc', command, '.obj')
    34     self.definePrefix = '/D'
    35     self.debuginfo_argv = ['/Zi']
    36     if self.version >= 1800:
    37       self.debuginfo_argv += ['/FS']
    38 
    39   @staticmethod
    40   def IncludePath(outputPath, includePath):
    41     # Hack - try and get a relative path because CL, with either 
    42     # /Zi or /ZI, combined with subprocess, apparently tries and
    43     # looks for paths like c:\bleh\"c:\bleh" <-- wtf
    44     # .. this according to Process Monitor
    45     outputPath = os.path.normcase(outputPath)
    46     includePath = os.path.normcase(includePath)
    47     outputDrive = os.path.splitdrive(outputPath)[0]
    48     includeDrive = os.path.splitdrive(includePath)[0]
    49     if outputDrive == includeDrive:
    50       return os.path.relpath(includePath, outputPath)
    51     return includePath
    52 
    53   def formatInclude(self, outputPath, includePath):
    54     return ['/I', self.IncludePath(outputPath, includePath)]
    55 
    56   def preprocessArgs(self, sourceFile):
    57     return ['/showIncludes', '/nologo', '/P', '/c', sourceFile]
    58 
    59   def objectArgs(self, sourceFile, objFile):
    60     return ['/showIncludes', '/nologo', '/c', sourceFile, '/Fo' + objFile]
    61 
    62 class CompatGCC(Vendor):
    63   def __init__(self, name, command, version):
    64     super(CompatGCC, self).__init__(name, version, 'gcc', command, '.o')
    65     parts = version.split('.')
    66     self.majorVersion = int(parts[0])
    67     self.minorVersion = int(parts[1])
    68     self.definePrefix = '-D'
    69 
    70   def formatInclude(self, outputPath, includePath):
    71     return ['-I', os.path.normpath(includePath)]
    72 
    73   def objectArgs(self, sourceFile, objFile):
    74     return ['-H', '-c', sourceFile, '-o', objFile]
    75 
    76 class GCC(CompatGCC):
    77   def __init__(self, command, version):
    78     super(GCC, self).__init__('gcc', command, version)
    79     self.debuginfo_argv = ['-g3', '-ggdb3']
    80 
    81 class Clang(CompatGCC):
    82   def __init__(self, command, version):
    83     super(Clang, self).__init__('clang', command, version)
    84     self.debuginfo_argv = ['-g3']
    85 
    86 class SunPro(Vendor):
    87   def __init__(self, command, version):
    88     super(SunPro, self).__init__('sun', version, 'sun', command, '.o')
    89     self.definePrefix = '-D'
    90     self.debuginfo_argv = ['-g3']
    91 
    92   def formatInclude(self, outputPath, includePath):
    93     return ['-I', os.path.normpath(includePath)]
    94 
    95   def objectArgs(self, sourceFile, objFile):
    96     return ['-H', '-c', sourceFile, '-o', objFile]
    97 
    98 def TryVerifyCompiler(cx, env, mode, cmd):
    99   if util.IsWindows():
   100     cc = VerifyCompiler(cx, env, mode, cmd, 'msvc')
   101     if cc:
   102       return cc
   103   return VerifyCompiler(cx, env, mode, cmd, 'gcc')
   104 
   105 CompilerSearch = {
   106   'CC': {
   107     'mac': ['cc', 'clang', 'gcc', 'icc'],
   108     'windows': ['cl'],
   109     'default': ['cc', 'gcc', 'clang', 'icc']
   110   },
   111   'CXX': {
   112     'mac': ['c++', 'clang++', 'g++', 'icc'],
   113     'windows': ['cl'],
   114     'default': ['c++', 'g++', 'clang++', 'icc']
   115   }
   116 }
   117 
   118 def DetectCompiler(cx, env, var):
   119   if var in env:
   120     trys = [env[var]]
   121   else:
   122     if util.Platform() in CompilerSearch[var]:
   123       trys = CompilerSearch[var][util.Platform()]
   124     else:
   125       trys = CompilerSearch[var]['default']
   126   for i in trys:
   127     cc = TryVerifyCompiler(cx, env, var, i)
   128     if cc:
   129       return cc
   130   raise Exception('Unable to find a suitable ' + var + ' compiler')
   131 
   132 def VerifyCompiler(cx, env, mode, cmd, vendor):
   133   args = cmd.split(' ')
   134   if 'CXXFLAGS' in env:
   135     args.extend(env['CXXFLAGS'])
   136   if 'CFLAGS' in env:
   137     args.extend(env['CFLAGS'])
   138   if mode == 'CXX' and 'CXXFLAGS' in env:
   139     args.extend(env['CXXFLAGS'])
   140   if mode == 'CXX':
   141     filename = 'test.cpp'
   142   else:
   143     filename = 'test.c'
   144   file = open(filename, 'w')
   145   file.write("""
   146 #include <stdio.h>
   147 #include <stdlib.h>
   148 
   149 int main()
   150 {
   151 #if defined __ICC
   152   printf("icc %d\\n", __ICC);
   153 #elif defined __clang__
   154 # if defined(__clang_major__) && defined(__clang_minor__)
   155   printf("clang %d.%d\\n", __clang_major__, __clang_minor__);
   156 # else
   157   printf("clang 1.%d\\n", __GNUC_MINOR__);
   158 # endif
   159 #elif defined __GNUC__
   160   printf("gcc %d.%d\\n", __GNUC__, __GNUC_MINOR__);
   161 #elif defined _MSC_VER
   162   printf("msvc %d\\n", _MSC_VER);
   163 #elif defined __TenDRA__
   164   printf("tendra 0\\n");
   165 #elif defined __SUNPRO_C
   166   printf("sun %x\\n", __SUNPRO_C);
   167 #elif defined __SUNPRO_CC
   168   printf("sun %x\\n", __SUNPRO_CC);
   169 #else
   170 #error "Unrecognized compiler!"
   171 #endif
   172 #if defined __cplusplus
   173   printf("CXX\\n");
   174 #else
   175   printf("CC\\n");
   176 #endif
   177   exit(0);
   178 }
   179 """)
   180   file.close()
   181   if mode == 'CC':
   182     executable = 'test' + util.ExecutableSuffix()
   183   elif mode == 'CXX':
   184     executable = 'testp' + util.ExecutableSuffix()
   185 
   186   # Make sure the exe is gone.
   187   if os.path.exists(executable):
   188     os.unlink(executable)
   189 
   190   # Until we can better detect vendors, don't do this.
   191   # if vendor == 'gcc' and mode == 'CXX':
   192   #   args.extend(['-fno-exceptions', '-fno-rtti'])
   193   args.extend([filename, '-o', executable])
   194   util.con_out(
   195     util.ConsoleHeader,
   196     'Checking {0} compiler (vendor test {1})... '.format(mode, vendor),
   197     util.ConsoleBlue,
   198     '{0}'.format(args),
   199     util.ConsoleNormal
   200   )
   201   p = util.CreateProcess(args)
   202   if p == None:
   203     print('not found')
   204     return False
   205   if util.WaitForProcess(p) != 0:
   206     print('failed with return code {0}'.format(p.returncode))
   207     return False
   208 
   209   exe = util.MakePath('.', executable)
   210   p = util.CreateProcess([executable], executable = exe)
   211   if p == None:
   212     print('failed to create executable')
   213     return False
   214   if util.WaitForProcess(p) != 0:
   215     print('executable failed with return code {0}'.format(p.returncode))
   216     return False
   217   lines = p.stdoutText.splitlines()
   218   if len(lines) != 2:
   219     print('invalid executable output')
   220     return False
   221   if lines[1] != mode:
   222     print('requested {0} compiler, found {1}'.format(mode, lines[1]))
   223     return False
   224 
   225   vendor, version = lines[0].split(' ')
   226   if vendor == 'gcc':
   227     v = GCC(cmd, version)
   228   elif vendor == 'clang':
   229     v = Clang(cmd, version)
   230   elif vendor == 'msvc':
   231     v = MSVC(cmd, version)
   232   elif vendor == 'sun':
   233     v = SunPro(cmd, version)
   234   else:
   235     print('Unknown vendor {0}'.format(vendor))
   236     return False
   237 
   238   util.con_out(
   239     util.ConsoleHeader,
   240     'found {0} version {1}'.format(vendor, version),
   241     util.ConsoleNormal
   242   )
   243   return v
   244 
   245 class Dep(object):
   246   def __init__(self, text, node):
   247     self.text = text
   248     self.node = node
   249 
   250 class Compiler(object):
   251   attrs = [
   252     'includes',         # C and C++ include paths
   253     'cxxincludes',      # C++-only include paths
   254     'cflags',           # C and C++ compiler flags
   255     'cxxflags',         # C++-only compiler flags
   256     'defines',          # C and C++ #defines
   257     'cxxdefines',       # C++-only #defines
   258 
   259     'rcdefines',        # Resource Compiler (RC) defines
   260 
   261     # Link flags. If any members are not strings, they will be interpreted as
   262     # Dep entries created from BinaryBuilder.
   263     'linkflags',
   264 
   265     # An array of objects to link, after all link flags have been specified.
   266     # Entries may either be strings containing a path, or Dep entries created
   267     # from BinaryBuilder.
   268     'postlink',
   269 
   270     # An array of nodes which should be weak dependencies on each source
   271     # compilation command.
   272     'sourcedeps',
   273   ]
   274 
   275   def __init__(self, cc, cxx):
   276     self.cc = cc
   277     self.cxx = cxx
   278     self.debuginfo = True
   279     for attr in Compiler.attrs:
   280       setattr(self, attr, [])
   281 
   282   def clone(self):
   283     cc = Compiler(self.cc, self.cxx)
   284     cc.cc = self.cc
   285     cc.cxx = self.cxx
   286     cc.debuginfo = self.debuginfo
   287     for attr in Compiler.attrs:
   288       setattr(cc, attr, copy.copy(getattr(self, attr)))
   289     return cc
   290 
   291   def Program(self, name):
   292     return Program(self, name)
   293 
   294   def Library(self, name):
   295     return Library(self, name)
   296 
   297   def StaticLibrary(self, name):
   298     return StaticLibrary(self, name)
   299 
   300   @staticmethod
   301   def Dep(text, node=None):
   302     return Dep(text, node)
   303 
   304 # Environment representing a C/C++ compiler invocation. Encapsulates most
   305 # arguments.
   306 class CCommandEnv(object):
   307   def __init__(self, outputPath, config, compiler):
   308     args = compiler.command.split(' ')
   309     args += config.cflags
   310     if config.debuginfo:
   311       args += compiler.debuginfo_argv
   312     if compiler == config.cxx:
   313       args += config.cxxflags
   314     args += [compiler.definePrefix + define for define in config.defines]
   315     if compiler == config.cxx:
   316       args += [compiler.definePrefix + define for define in config.cxxdefines]
   317     for include in config.includes:
   318       args += compiler.formatInclude(outputPath, include)
   319     if compiler == config.cxx:
   320       for include in config.cxxincludes:
   321         args += compiler.formatInclude(outputPath, include)
   322     self.argv = args
   323     self.compiler = compiler
   324 
   325 def NameForObjectFile(file):
   326   return re.sub('[^a-zA-Z0-9_]+', '_', os.path.splitext(file)[0]);
   327 
   328 class ObjectFile(object):
   329   def __init__(self, sourceFile, outputFile, argv, sharedOutputs):
   330     self.sourceFile = sourceFile
   331     self.outputFile = outputFile
   332     self.argv = argv
   333     self.sharedOutputs = sharedOutputs
   334 
   335 class RCFile(object):
   336   def __init__(self, sourceFile, preprocFile, outputFile, cl_argv, rc_argv):
   337     self.sourceFile = sourceFile
   338     self.preprocFile = preprocFile
   339     self.outputFile = outputFile
   340     self.cl_argv = cl_argv 
   341     self.rc_argv = rc_argv
   342 
   343 class BinaryBuilder(object):
   344   def __init__(self, compiler, name):
   345     super(BinaryBuilder, self).__init__()
   346     self.compiler = compiler.clone()
   347     self.sources = []
   348     self.name_ = name
   349     self.used_cxx_ = False
   350     self.linker_ = None
   351 
   352   def generate(self, generator, cx):
   353     return generator.addCxxTasks(cx, self)
   354 
   355   # Make an item that can be passed into linkflags/postlink but has an attached
   356   # dependency.
   357   def Dep(self, text, node=None): 
   358     return Dep(text, node)
   359 
   360   # The folder we'll be in, relative to our build context.
   361   @property
   362   def localFolder(self):
   363     return self.name_
   364 
   365   # Exposed only for frontends.
   366   @property
   367   def linker(self):
   368     return self.linker_
   369 
   370   # Compute the build folder.
   371   def getBuildFolder(self, builder):
   372     return os.path.join(builder.buildFolder, self.localFolder)
   373 
   374   def linkFlag(self, cx, item):
   375     if type(item) is Dep:
   376       # If the dep is a file dependency (no node attached), and has a relative
   377       # path, make it absolute so the linker knows where to look.
   378       if item.node is None and not os.path.isabs(item.text):
   379         return os.path.join(cx.currentSourcePath, item.text)
   380       return item.text
   381 
   382     if hasattr(item, 'path'):
   383       if os.path.isabs(item.path):
   384         return item.path
   385 
   386       local_path = os.path.join(cx.buildFolder, self.localFolder)
   387       return os.path.relpath(item.path, local_path)
   388 
   389     return item
   390 
   391   def linkFlags(self, cx):
   392     argv = [self.linkFlag(cx, item) for item in self.compiler.linkflags]
   393     argv += [self.linkFlag(cx, item) for item in self.compiler.postlink]
   394     return argv
   395 
   396   def finish(self, cx):
   397     # Because we want to compute relative include folders for MSVC (see its
   398     # vendor object), we need to compute an absolute path to the build folder.
   399     self.outputFolder = self.getBuildFolder(cx)
   400     self.outputPath = os.path.join(cx.buildPath, self.outputFolder)
   401     self.default_c_env = CCommandEnv(self.outputPath, self.compiler, self.compiler.cc)
   402     self.default_cxx_env = CCommandEnv(self.outputPath, self.compiler, self.compiler.cxx)
   403 
   404     shared_cc_outputs = []
   405     if self.compiler.debuginfo and self.compiler.cc.behavior == 'msvc':
   406       cl_version = int(self.compiler.cc.version) - 600
   407       shared_pdb = 'vc{0}.pdb'.format(int(cl_version / 10))
   408       shared_cc_outputs += [shared_pdb]
   409 
   410     self.objects = []
   411     self.resources = []
   412     for item in self.sources:
   413       if os.path.isabs(item):
   414         sourceFile = item
   415       else:
   416         sourceFile = os.path.join(cx.currentSourcePath, item)
   417       sourceFile = os.path.normpath(sourceFile)
   418 
   419       filename, extension = os.path.splitext(item)
   420       encname = NameForObjectFile(filename)
   421 
   422       if extension == '.rc':
   423         cenv = self.default_c_env
   424         objectFile = encname + '.res'
   425       else:
   426         if extension == '.c':
   427           cenv = self.default_c_env
   428         else:
   429           cenv = self.default_cxx_env
   430           self.used_cxx_ = True
   431         objectFile = encname + cenv.compiler.objSuffix
   432 
   433       if extension == '.rc':
   434         # This is only relevant on Windows.
   435         vendor = cenv.compiler
   436         defines = self.compiler.defines + self.compiler.cxxdefines + self.compiler.rcdefines
   437         cl_argv = vendor.command.split(' ')
   438         cl_argv += [vendor.definePrefix + define for define in defines]
   439         for include in (self.compiler.includes + self.compiler.cxxincludes):
   440           cl_argv += vendor.formatInclude(objectFile, include)
   441         cl_argv += vendor.preprocessArgs(sourceFile)
   442 
   443         rc_argv = ['rc', '/nologo']
   444         for define in defines:
   445           rc_argv.extend(['/d', define])
   446         for include in (self.compiler.includes + self.compiler.cxxincludes):
   447           rc_argv.extend(['/i', MSVC.IncludePath(objectFile, include)])
   448         rc_argv.append('/fo' + objectFile)
   449         rc_argv.append(sourceFile)
   450 
   451         self.resources.append(RCFile(sourceFile, encname + '.i', objectFile, cl_argv, rc_argv))
   452       else:
   453         argv = cenv.argv + cenv.compiler.objectArgs(sourceFile, objectFile)
   454         obj = ObjectFile(sourceFile, objectFile, argv, shared_cc_outputs)
   455         self.objects.append(obj)
   456 
   457     if not self.linker_:
   458       if self.used_cxx_:
   459         self.linker_ = self.compiler.cxx
   460       else:
   461         self.linker_ = self.compiler.cc
   462 
   463     files = [out.outputFile for out in self.objects + self.resources]
   464     self.argv = self.generateBinary(cx, files)
   465     self.linker_outputs = [self.outputFile]
   466     self.debug_entry = None
   467 
   468     if self.linker_.behavior == 'msvc':
   469       if isinstance(self, Library):
   470         # In theory, .dlls should have exports, so MSVC will generate these
   471         # files. If this turns out not to be true, we may have to get fancier.
   472         self.linker_outputs += [self.name_ + '.lib']
   473         self.linker_outputs += [self.name_ + '.exp']
   474 
   475     if self.compiler.debuginfo:
   476       self.perform_symbol_steps(cx)
   477 
   478   def perform_symbol_steps(self, cx):
   479     if isinstance(self.linker_, MSVC):
   480       # Note, pdb is last since we read the pdb as outputs[-1].
   481       self.linker_outputs += [self.name_ + '.pdb']
   482     elif cx.target_platform is 'mac':
   483       bundle_folder = os.path.join(self.localFolder, self.outputFile + '.dSYM')
   484       bundle_entry = cx.AddFolder(bundle_folder)
   485       bundle_layout = [
   486         'Contents',
   487         'Contents/Resources',
   488         'Contents/Resources/DWARF',
   489       ]
   490       for folder in bundle_layout:
   491         cx.AddFolder(os.path.join(bundle_folder, folder))
   492       self.linker_outputs += [
   493         self.outputFile + '.dSYM/Contents/Info.plist',
   494         self.outputFile + '.dSYM/Contents/Resources/DWARF/' + self.outputFile
   495       ]
   496       self.debug_entry = bundle_entry
   497       self.argv = ['ambuild_dsymutil_wrapper.sh', self.outputFile] + self.argv
   498     elif cx.target_platform is 'linux':
   499       self.linker_outputs += [
   500         self.outputFile + '.dbg'
   501       ]
   502       self.argv = ['ambuild_objcopy_wrapper.sh', self.outputFile] + self.argv
   503 
   504   def link(self, context, folder, inputs):
   505     # The existence of .ilk files on Windows does not seem reliable, so we
   506     # treat it as "shared" which does not participate in the DAG (yet).
   507     shared_outputs = []
   508     if self.linker_.behavior == 'msvc':
   509       if not isinstance(self, StaticLibrary) and '/INCREMENTAL:NO' not in self.argv:
   510         shared_outputs += [self.name_ + '.ilk']
   511 
   512     ignore, outputs = context.AddCommand(
   513       inputs = inputs,
   514       argv = self.argv,
   515       outputs = self.linker_outputs,
   516       folder = folder,
   517       shared_outputs = shared_outputs
   518     )
   519     if not self.debug_entry and self.compiler.debuginfo:
   520       self.debug_entry = outputs[-1]
   521     return outputs[0], self.debug_entry
   522 
   523 class Program(BinaryBuilder):
   524   def __init__(self, compiler, name):
   525     super(Program, self).__init__(compiler, name)
   526     self.outputFile = name + util.ExecutableSuffix()
   527 
   528   def generateBinary(self, cx, files):
   529     argv = self.linker_.command.split(' ')
   530     argv += files
   531 
   532     if isinstance(self.linker_, MSVC):
   533       argv.append('/link')
   534       argv.extend(self.linkFlags(cx))
   535       argv.append('/nologo')
   536       argv += [
   537         '/OUT:' + self.outputFile,
   538         '/nologo',
   539       ]
   540       if self.compiler.debuginfo:
   541         argv += ['/DEBUG', '/PDB:"' + self.name_ + '.pdb"']
   542     else:
   543       argv.extend(self.linkFlags(cx))
   544       argv.extend(['-o', self.outputFile])
   545 
   546     return argv
   547 
   548 class Library(BinaryBuilder):
   549   def __init__(self, compiler, name):
   550     super(Library, self).__init__(compiler, name)
   551     self.outputFile = name + util.SharedLibSuffix()
   552 
   553   def generateBinary(self, cx, files):
   554     argv = self.linker_.command.split(' ')
   555     argv += files
   556 
   557     if isinstance(self.linker_, MSVC):
   558       argv.append('/link')
   559       argv.extend(self.linkFlags(cx))
   560       argv += [
   561         '/OUT:' + self.outputFile,
   562         '/DEBUG',
   563         '/nologo',
   564         '/DLL',
   565       ]
   566       if self.compiler.debuginfo:
   567         argv += ['/DEBUG', '/PDB:"' + self.name_ + '.pdb"']
   568     elif isinstance(self.linker_, CompatGCC):
   569       argv.extend(self.linkFlags(cx))
   570       if util.IsMac():
   571         argv.append('-dynamiclib')
   572       else:
   573         argv.append('-shared')
   574       argv.extend(['-o', self.outputFile])
   575 
   576     return argv
   577 
   578 class StaticLibrary(BinaryBuilder):
   579   def __init__(self, compiler, name):
   580     super(StaticLibrary, self).__init__(compiler, name)
   581     self.outputFile = util.StaticLibPrefix() + name + util.StaticLibSuffix()
   582 
   583   def generateBinary(self, cx, files):
   584     if isinstance(self.linker_, MSVC):
   585       argv = ['lib.exe', '/OUT:' + self.outputFile]
   586     else:
   587       argv = ['ar', 'rcs', self.outputFile]
   588     argv += files
   589     return argv
   590 
   591   def perform_symbol_steps(self, cx):
   592     pass