OLD | NEW |
1 # Copyright (c) 2012 Google Inc. All rights reserved. | 1 # Copyright (c) 2012 Google Inc. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """ | 5 """ |
6 This module helps emulate Visual Studio 2008 behavior on top of other | 6 This module helps emulate Visual Studio 2008 behavior on top of other |
7 build systems, primarily ninja. | 7 build systems, primarily ninja. |
8 """ | 8 """ |
9 | 9 |
| 10 import gyp.MSVSVersion |
| 11 import os |
10 import re | 12 import re |
| 13 import sys |
11 | 14 |
12 windows_quoter_regex = re.compile(r'(\\*)"') | 15 windows_quoter_regex = re.compile(r'(\\*)"') |
13 | 16 |
14 def QuoteCmdExeArgument(arg): | 17 def QuoteCmdExeArgument(arg): |
15 """Quote a command line argument so that it appears as one argument when | 18 """Quote a command line argument so that it appears as one argument when |
16 processed via cmd.exe and parsed by CommandLineToArgvW (as is typical for | 19 processed via cmd.exe and parsed by CommandLineToArgvW (as is typical for |
17 Windows programs).""" | 20 Windows programs).""" |
18 # See http://goo.gl/cuFbX and http://goo.gl/dhPnp including the comment | 21 # See http://goo.gl/cuFbX and http://goo.gl/dhPnp including the comment |
19 # threads. This is actually the quoting rules for CommandLineToArgvW, not | 22 # threads. This is actually the quoting rules for CommandLineToArgvW, not |
20 # for the shell, because the shell doesn't do anything in Windows. This | 23 # for the shell, because the shell doesn't do anything in Windows. This |
21 # works more or less because most programs (including the compiler, etc.) | 24 # works more or less because most programs (including the compiler, etc.) |
22 # use that function to handle command line arguments. | 25 # use that function to handle command line arguments. |
23 # | 26 tmp = arg |
| 27 |
| 28 # If the string ends in a \, then it will be interpreted as an escaper for |
| 29 # the trailing ", so we need to pre-escape that. |
| 30 if tmp[-1] == '\\': |
| 31 tmp = tmp + '\\' |
| 32 |
24 # For a literal quote, CommandLineToArgvW requires 2n+1 backslashes | 33 # For a literal quote, CommandLineToArgvW requires 2n+1 backslashes |
25 # preceding it, and results in n backslashes + the quote. So we substitute | 34 # preceding it, and results in n backslashes + the quote. So we substitute |
26 # in 2* what we match, +1 more, plus the quote. | 35 # in 2* what we match, +1 more, plus the quote. |
27 tmp = windows_quoter_regex.sub(lambda mo: 2 * mo.group(1) + '\\"', arg) | 36 tmp = windows_quoter_regex.sub(lambda mo: 2 * mo.group(1) + '\\"', tmp) |
28 | 37 |
29 # Now, we need to escape some things that are actually for the shell. | 38 # Now, we need to escape some things that are actually for the shell. |
30 # ^-escape various characters that are otherwise interpreted by the shell. | 39 # ^-escape various characters that are otherwise interpreted by the shell. |
31 tmp = re.sub(r'([&|^])', r'^\1', tmp) | 40 # Note, removed ^ escape, these args go in .rsp files now. Blech. :( |
| 41 #tmp = re.sub(r'([&|^])', r'^\1', tmp) |
32 | 42 |
33 # %'s also need to be doubled otherwise they're interpreted as batch | 43 # %'s also need to be doubled otherwise they're interpreted as batch |
34 # positional arguments. Also make sure to escape the % so that they're | 44 # positional arguments. Also make sure to escape the % so that they're |
35 # passed literally through escaping so they can be singled to just the | 45 # passed literally through escaping so they can be singled to just the |
36 # original %. Otherwise, trying to pass the literal representation that | 46 # original %. Otherwise, trying to pass the literal representation that |
37 # looks like an environment variable to the shell (e.g. %PATH%) would fail. | 47 # looks like an environment variable to the shell (e.g. %PATH%) would fail. |
38 tmp = tmp.replace('%', '^%^%') | 48 # Note, removed ^ escape, these args go in .rsp files now. Blech :( |
| 49 tmp = tmp.replace('%', '%%') |
39 | 50 |
40 # Finally, wrap the whole thing in quotes so that the above quote rule | 51 # Finally, wrap the whole thing in quotes so that the above quote rule |
41 # applies and whitespace isn't a word break. | 52 # applies and whitespace isn't a word break. |
42 return '"' + tmp + '"' | 53 return '"' + tmp + '"' |
43 | 54 |
44 def EncodeCmdExeList(args): | 55 |
45 """Process a list of arguments using QuoteCmdExeArgument.""" | 56 def _GenericRetrieve(root, default, path): |
46 return ' '.join(QuoteCmdExeArgument(arg) for arg in args) | 57 """Given a list of dictionary keys |path| and a tree of dicts |root|, find |
| 58 value at path, or return |default| if any of the path doesn't exist.""" |
| 59 if not root: |
| 60 return default |
| 61 if not path: |
| 62 return root |
| 63 return _GenericRetrieve(root.get(path[0]), default, path[1:]) |
| 64 |
| 65 |
| 66 def _AddPrefix(element, prefix): |
| 67 """Add |prefix| to |element| or each subelement if element is iterable.""" |
| 68 if element is None: |
| 69 return element |
| 70 # Note, not Iterable because we don't want to handle strings like that. |
| 71 if isinstance(element, list) or isinstance(element, tuple): |
| 72 return [prefix + e for e in element] |
| 73 else: |
| 74 return prefix + element |
| 75 |
| 76 |
| 77 def _DoRemapping(element, map): |
| 78 """If |element| then remap it through |map|. If |element| is iterable then |
| 79 each item will be remapped. Any elements not found will be removed.""" |
| 80 if map is not None and element is not None: |
| 81 if isinstance(element, list) or isinstance(element, tuple): |
| 82 element = filter(None, [map.get(elem) for elem in element]) |
| 83 else: |
| 84 element = map.get(element) |
| 85 return element |
| 86 |
| 87 |
| 88 def _AppendOrReturn(append, element): |
| 89 """If |append| is None, simply return |element|. If |append| is not None, |
| 90 then add |element| to it, adding each item in |element| if it's a list or |
| 91 tuple.""" |
| 92 if append is not None and element is not None: |
| 93 if isinstance(element, list) or isinstance(element, tuple): |
| 94 append.extend(element) |
| 95 else: |
| 96 append.append(element) |
| 97 else: |
| 98 return element |
| 99 |
| 100 |
| 101 class MsvsSettings(object): |
| 102 """A class that understands the gyp 'msvs_...' values (especially the |
| 103 msvs_settings field). They largely correpond to the VS2008 IDE DOM. This |
| 104 class helps map those settings to command line options.""" |
| 105 |
| 106 def __init__(self, spec): |
| 107 self.spec = spec |
| 108 |
| 109 supported_fields = [ |
| 110 ('msvs_configuration_attributes', dict), |
| 111 ('msvs_settings', dict), |
| 112 ('msvs_system_include_dirs', list), |
| 113 ('msvs_disabled_warnings', list), |
| 114 ('msvs_cygwin_dirs', list), |
| 115 ] |
| 116 configs = spec['configurations'] |
| 117 for field, default in supported_fields: |
| 118 setattr(self, field, {}) |
| 119 for configname, config in configs.iteritems(): |
| 120 getattr(self, field)[configname] = config.get(field, default()) |
| 121 |
| 122 def ConvertVSMacros(self, s): |
| 123 """Convert from VS macro names to something equivalent.""" |
| 124 if '$' in s: |
| 125 replacements = { |
| 126 '$(VSInstallDir)': os.environ.get('VSInstallDir') + '\\', |
| 127 '$(VCInstallDir)': os.environ.get('VCInstallDir') + '\\', |
| 128 '$(DXSDK_DIR)': os.environ.get('DXSDK_DIR') + '\\', |
| 129 '$(OutDir)\\': '', |
| 130 } |
| 131 for old, new in replacements.iteritems(): |
| 132 s = s.replace(old, new) |
| 133 return s |
| 134 |
| 135 def EncodeCmdExeList(self, args): |
| 136 """Process a list of arguments using QuoteCmdExeArgument.""" |
| 137 expanded = [self.ConvertVSMacros(arg) for arg in args] |
| 138 return ' '.join(QuoteCmdExeArgument(arg) for arg in expanded) |
| 139 |
| 140 def AdjustLibraries(self, libraries): |
| 141 """Strip -l from library if it's specified with that.""" |
| 142 return [lib[2:] if lib.startswith('-l') else lib for lib in libraries] |
| 143 |
| 144 def _GetAndMunge(self, field, path, default, prefix, append, map): |
| 145 """Retrieve a value from |field| at |path| or return |default|. If |
| 146 |append| is specified, and the item is found, it will be appended to that |
| 147 object instead of returned. If |map| is specified, results will be |
| 148 remapped through |map| before being returned or appended.""" |
| 149 result = _GenericRetrieve(field, default, path) |
| 150 result = _DoRemapping(result, map) |
| 151 result = _AddPrefix(result, prefix) |
| 152 return _AppendOrReturn(append, result) |
| 153 |
| 154 class _GetWrapper(object): |
| 155 def __init__(self, parent, field, base_path, append=None): |
| 156 self.parent = parent |
| 157 self.field = field |
| 158 self.base_path = [base_path] |
| 159 self.append = append |
| 160 |
| 161 def __call__(self, name, map=None, prefix='', default=None): |
| 162 return self.parent._GetAndMunge(self.field, self.base_path + [name], |
| 163 default=default, prefix=prefix, append=self.append, map=map) |
| 164 |
| 165 def Setting(self, path, config, |
| 166 default=None, prefix='', append=None, map=None): |
| 167 """_GetAndMunge for msvs_settings.""" |
| 168 return self._GetAndMunge( |
| 169 self.msvs_settings[config], path, default, prefix, append, map) |
| 170 |
| 171 def ConfigAttrib(self, path, config, |
| 172 default=None, prefix='', append=None, map=None): |
| 173 """_GetAndMunge for msvs_configuration_attributes.""" |
| 174 return self._GetAndMunge( |
| 175 self.msvs_configuration_attributes[config], |
| 176 path, default, prefix, append, map) |
| 177 |
| 178 def AdjustIncludeDirs(self, include_dirs, config): |
| 179 """Updates include_dirs to expand VS specific paths, and adds the system |
| 180 include dirs used for platform SDK and similar.""" |
| 181 includes = include_dirs + self.msvs_system_include_dirs[config] |
| 182 return [self.ConvertVSMacros(p) for p in includes] |
| 183 |
| 184 def GetComputedDefines(self, config): |
| 185 """Returns the set of defines that are injected to the defines list based |
| 186 on other VS settings.""" |
| 187 defines = [] |
| 188 if self.ConfigAttrib(['CharacterSet'], config) == '1': |
| 189 defines.extend(('_UNICODE', 'UNICODE')) |
| 190 if self.ConfigAttrib(['CharacterSet'], config) == '2': |
| 191 defines.append('_MBCS') |
| 192 defines.extend(self.Setting(('VCCLCompilerTool', 'PreprocessorDefinitions'), |
| 193 config, default=[])) |
| 194 return defines |
| 195 |
| 196 def GetCflags(self, config): |
| 197 """Returns the flags that need to be added to .c and .cc compilations.""" |
| 198 cflags = [] |
| 199 cflags.extend(['$!/wd' + w for w in self.msvs_disabled_warnings[config]]) |
| 200 cl = self._GetWrapper(self, self.msvs_settings[config], |
| 201 'VCCLCompilerTool', append=cflags) |
| 202 cl('Optimization', map={'0':'d', '2':'s'}, prefix='$!/O') |
| 203 cl('InlineFunctionExpansion', prefix='$!/Ob') |
| 204 cl('OmitFramePointers', map={'false':'-', 'true':''}, prefix='$!/Oy') |
| 205 cl('FavorSizeOrSpeed', map={'1':'s', '2':'t'}, prefix='$!/O') |
| 206 cl('WholeProgramOptimization', map={'true':'$!/GL'}) |
| 207 cl('WarningLevel', prefix='$!/W') |
| 208 cl('WarnAsError', map={'true':'$!/WX'}) |
| 209 cl('DebugInformationFormat', map={'1':'7', '3':'i', '4':'I'}, prefix='$!/Z') |
| 210 cl('RuntimeTypeInfo', map={'true':'$!/GR', 'false':'$!/GR-'}) |
| 211 cl('EnableFunctionLevelLinking', map={'true':'$!/Gy', 'false':'$!/Gy-'}) |
| 212 cl('MinimalRebuild', map={'true':'$!/Gm'}) |
| 213 cl('BufferSecurityCheck', map={'true':'$!/GS', 'false':'$!/GS-'}) |
| 214 cl('BasicRuntimeChecks', map={'1':'s', '2':'u', '3':'1'}, prefix='$!/RTC') |
| 215 cl('RuntimeLibrary', |
| 216 map={'0':'T', '1':'Td', '2':'D', '3':'Dd'}, prefix='$!/M') |
| 217 cl('ExceptionHandling', map={'1':'sc','2':'a'}, prefix='$!/EH') |
| 218 cl('AdditionalOptions', prefix='$!') |
| 219 return cflags |
| 220 |
| 221 def GetCflagsC(self, config): |
| 222 """Returns the flags that need to be added to .c compilations.""" |
| 223 return [] |
| 224 |
| 225 def GetCflagsCC(self, config): |
| 226 """Returns the flags that need to be added to .cc compilations.""" |
| 227 return ['$!/TP'] |
| 228 |
| 229 def _GetDefFileAsLdflags(self, spec, ldflags, gyp_to_build_path): |
| 230 """.def files get implicitly converted to a ModuleDefinitionFile for the |
| 231 linker in the VS generator. Emulate that behaviour here.""" |
| 232 def_file = '' |
| 233 if spec['type'] in ('shared_library', 'loadable_module'): |
| 234 def_files = [s for s in spec.get('sources', []) if s.endswith('.def')] |
| 235 if len(def_files) == 1: |
| 236 ldflags.append('$!/DEF:"%s"' % gyp_to_build_path(def_files[0])) |
| 237 elif len(def_files) > 1: |
| 238 raise Exception("Multiple .def files") |
| 239 |
| 240 def GetLibFlags(self, config, spec): |
| 241 libflags = [] |
| 242 lib = self._GetWrapper(self, self.msvs_settings[config], |
| 243 'VCLibrarianTool', append=libflags) |
| 244 libpaths = self.Setting(('VCLibrarianTool', 'AdditionalLibraryDirectories'), |
| 245 config, default=[]) |
| 246 libpaths = [os.path.normpath(self.ConvertVSMacros(p)) for p in libpaths] |
| 247 libflags.extend(['$!/LIBPATH:"' + p + '"' for p in libpaths]) |
| 248 lib('AdditionalOptions', prefix='$!') |
| 249 return libflags |
| 250 |
| 251 def GetLdflags(self, config, spec, product_dir, gyp_to_build_path): |
| 252 """Returns the flags that need to be added to link and lib commands.""" |
| 253 ldflags = [] |
| 254 ld = self._GetWrapper(self, self.msvs_settings[config], |
| 255 'VCLinkerTool', append=ldflags) |
| 256 self._GetDefFileAsLdflags(spec, ldflags, gyp_to_build_path) |
| 257 ld('GenerateDebugInformation', map={'true':'$!/DEBUG'}) |
| 258 ld('TargetMachine', map={'1':'X86', '17':'X64'}, prefix='$!/MACHINE:') |
| 259 libpaths = self.Setting(('VCLinkerTool', 'AdditionalLibraryDirectories'), |
| 260 config, default=[]) |
| 261 libpaths = [os.path.normpath(self.ConvertVSMacros(p)) for p in libpaths] |
| 262 ldflags.extend(['$!/LIBPATH:"' + p + '"' for p in libpaths]) |
| 263 ld('DelayLoadDLLs', prefix='$!/DELAYLOAD:') |
| 264 ld('AdditionalOptions', prefix='$!') |
| 265 ld('SubSystem', map={'1':'CONSOLE', '2':'WINDOWS'}, prefix='$!/SUBSYSTEM:') |
| 266 ld('LinkIncremental', map={'1':':NO', '2':''}, prefix='$!/INCREMENTAL') |
| 267 ld('FixedBaseAddress', map={'1':':NO', '2':''}, prefix='$!/FIXED') |
| 268 ld('RandomizedBaseAddress', |
| 269 map={'1':':NO', '2':''}, prefix='$!/DYNAMICBASE') |
| 270 ld('DataExecutionPrevention', |
| 271 map={'1':':NO', '2':''}, prefix='$!/NXCOMPAT') |
| 272 ld('OptimizeReferences', map={'1':'NOREF', '2':'REF'}, prefix='$!/OPT:') |
| 273 ld('EnableCOMDATFolding', map={'1':'NOICF', '2':'ICF'}, prefix='$!/OPT:') |
| 274 ld('LinkTimeCodeGeneration', map={'1':'$!/LTCG'}) |
| 275 ld('IgnoreDefaultLibraryNames', prefix='$!/NODEFAULTLIB:') |
| 276 # TODO(scottmg): This should sort of be somewhere else (not really a flag). |
| 277 ld('AdditionalDependencies', prefix='$!') |
| 278 # TODO(scottmg): These too. |
| 279 ldflags.extend(('kernel32.lib', 'user32.lib', 'gdi32.lib', 'winspool.lib', |
| 280 'comdlg32.lib', 'advapi32.lib', 'shell32.lib', 'ole32.lib', |
| 281 'oleaut32.lib', 'uuid.lib', 'odbc32.lib', 'odbccp32.lib', |
| 282 'DelayImp.lib')) |
| 283 return ldflags |
| 284 |
| 285 def GetOutputName(self, config, product_dir): |
| 286 ld = self._GetWrapper(self, self.msvs_settings[config], 'VCLinkerTool') |
| 287 return ld('OutputFile') |
| 288 |
| 289 def GetIdlRule(self, source, config, |
| 290 gyp_to_build_path, gyp_to_unique_output, expand_rule_variables): |
| 291 """Determine the implicit outputs for an idl file. Returns both the inputs |
| 292 and the outputs for the build rule.""" |
| 293 midl = self._GetWrapper(self, self.msvs_settings[config], 'VCMIDLTool') |
| 294 tlb = midl('TypeLibraryName') |
| 295 header = midl('HeaderFileName') |
| 296 dlldata = midl('DLLDataFileName') |
| 297 iid = midl('InterfaceIdentifierFileName') |
| 298 proxy = midl('ProxyFileName') |
| 299 # Note that .tlb is not included in the outputs as it is not always |
| 300 # generated depending on the content of the input idl file. |
| 301 output = [header, dlldata, iid, proxy] |
| 302 input = gyp_to_build_path(source) |
| 303 outdir = gyp_to_build_path(midl('OutputDirectory')) |
| 304 filename = os.path.splitext(os.path.split(source)[1])[0] |
| 305 def fix_path(path, rel=None): |
| 306 path = os.path.join(outdir, path) |
| 307 path = expand_rule_variables(path, '?', '?', '?', '?', filename) |
| 308 if rel: |
| 309 path = os.path.relpath(path, rel) |
| 310 return path |
| 311 output = [fix_path(o) for o in output] |
| 312 # TODO(scottmg): flags: /char signed /env win32, /Oicf |
| 313 variables = [('name', filename), |
| 314 ('tlb', fix_path(tlb, outdir)), |
| 315 ('h', fix_path(header, outdir)), |
| 316 ('dlldata', fix_path(dlldata, outdir)), |
| 317 ('iid', fix_path(iid, outdir)), |
| 318 ('proxy', fix_path(proxy, outdir)), |
| 319 ('outdir', outdir)] |
| 320 return input, output, variables |
| 321 |
| 322 def HandleIdlFiles(self, ninja, spec, sources, config_name, |
| 323 gyp_to_build_path, gyp_to_unique_output, expand_rule_variables): |
| 324 """Visual Studio has implicit .idl build rules for MIDL files, but other |
| 325 projects (e.g., WebKit) use .idl for a different import format with custom |
| 326 build rules. So, only do the implicit rules if they're not overriden by |
| 327 explicit rules.""" |
| 328 native_idl = True |
| 329 for rule in spec.get('rules', []): |
| 330 if rule['extension'] == 'idl' and int(rule.get('msvs_external_rule', 0)): |
| 331 native_idl = False |
| 332 if native_idl: |
| 333 for source in sources: |
| 334 filename, ext = os.path.splitext(source) |
| 335 if ext == '.idl': |
| 336 input, output, vars = self.GetIdlRule( |
| 337 source, |
| 338 config_name, |
| 339 gyp_to_build_path, |
| 340 gyp_to_unique_output, |
| 341 expand_rule_variables) |
| 342 ninja.build(output, 'idl', input, variables=vars) |
| 343 |
| 344 vs_version = None |
| 345 def GetVSVersion(generator_flags): |
| 346 global vs_version |
| 347 if not vs_version: |
| 348 vs_version = gyp.MSVSVersion.SelectVisualStudioVersion( |
| 349 generator_flags.get('msvs_version', 'auto')) |
| 350 return vs_version |
| 351 |
| 352 def _GetBinaryPath(generator_flags, tool): |
| 353 vs = GetVSVersion(generator_flags) |
| 354 return ('"' + |
| 355 os.path.normpath(os.path.join(vs.Path(), "VC/bin", tool)) + |
| 356 '"') |
| 357 |
| 358 def GetCLPath(generator_flags): |
| 359 return _GetBinaryPath(generator_flags, 'cl.exe') |
| 360 |
| 361 def GetLinkPath(generator_flags): |
| 362 return _GetBinaryPath(generator_flags, 'link.exe') |
| 363 |
| 364 def GetMidlPath(generator_flags): |
| 365 return _GetBinaryPath(generator_flags, 'midl.exe') |
| 366 |
| 367 def IsRuleRunUnderCygwin(spec): |
| 368 """Determine if an action should be run under cygwin. If the variable is |
| 369 unset, or set to 1 we use cygwin.""" |
| 370 if not spec: |
| 371 return False |
| 372 return int(spec.get('msvs_cygwin_shell', 1)) != 0 |
| 373 |
| 374 def BuildCygwinBashCommandLine(settings, rule, args, path_to_base, config): |
| 375 """Build a command line that runs args via cygwin bash. We assume that all |
| 376 incoming paths are in Windows normpath'd form, so they need to be converted |
| 377 to posix style for the part of the command line that's passed to bash. We |
| 378 also have to do some Visual Studio macro emulation here because various rules |
| 379 use magic VS names for things. Also note that rules that contain ninja |
| 380 variables cannot be fixed here (for example ${source}), so the outer |
| 381 generator needs to make sure that the paths that are written out are in posix |
| 382 style, if the command line will be used here.""" |
| 383 assert IsRuleRunUnderCygwin(rule) |
| 384 cygwin_dir = os.path.normpath( |
| 385 os.path.join(path_to_base, settings.msvs_cygwin_dirs[config][0])) |
| 386 cd = ('cd %s' % path_to_base).replace('\\', '/') |
| 387 args = [settings.ConvertVSMacros(a).replace('\\', '/') for a in args] |
| 388 args = ["'%s'" % a.replace("'", "\\'") for a in args] |
| 389 bash_cmd = ' '.join(args) |
| 390 cmd = ( |
| 391 'call "%s\\setup_env.bat" && set CYGWIN=nontsec && ' % cygwin_dir + |
| 392 'bash -c "%s ; %s"' % (cd, bash_cmd)) |
| 393 return cmd |
OLD | NEW |