Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(79)

Side by Side Diff: pylib/gyp/generator/ninja.py

Issue 10228016: ninja windows: fix expansion of some VS macros (Closed) Base URL: https://gyp.googlecode.com/svn/trunk
Patch Set: Created 8 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
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 import copy 5 import copy
6 import gyp 6 import gyp
7 import gyp.common 7 import gyp.common
8 import gyp.msvs_emulation 8 import gyp.msvs_emulation
9 import gyp.MSVSVersion 9 import gyp.MSVSVersion
10 import gyp.system_test 10 import gyp.system_test
(...skipping 182 matching lines...) Expand 10 before | Expand all | Expand 10 after
193 self.ninja = ninja_syntax.Writer(output_file) 193 self.ninja = ninja_syntax.Writer(output_file)
194 self.flavor = flavor 194 self.flavor = flavor
195 self.abs_build_dir = abs_build_dir 195 self.abs_build_dir = abs_build_dir
196 self.obj_ext = '.obj' if flavor == 'win' else '.o' 196 self.obj_ext = '.obj' if flavor == 'win' else '.o'
197 197
198 # Relative path from build output dir to base dir. 198 # Relative path from build output dir to base dir.
199 self.build_to_base = os.path.join(InvertRelativePath(build_dir), base_dir) 199 self.build_to_base = os.path.join(InvertRelativePath(build_dir), base_dir)
200 # Relative path from base dir to build dir. 200 # Relative path from base dir to build dir.
201 self.base_to_build = os.path.join(InvertRelativePath(base_dir), build_dir) 201 self.base_to_build = os.path.join(InvertRelativePath(base_dir), build_dir)
202 202
203 def _WinCase(self, path):
Nico 2012/04/25 23:18:23 Method name is improvable, also docstring
scottmg 2012/04/26 03:06:46 Turns out it already exists as os.path.normcase!
204 if self.flavor == 'win':
205 path = path.lower()
Nico 2012/04/25 23:18:23 Maybe kernel32.GetLongPathName(kernel32.GetShortPa
206 return path
207
203 def ExpandSpecial(self, path, product_dir=None): 208 def ExpandSpecial(self, path, product_dir=None):
204 """Expand specials like $!PRODUCT_DIR in |path|. 209 """Expand specials like $!PRODUCT_DIR in |path|.
205 210
206 If |product_dir| is None, assumes the cwd is already the product 211 If |product_dir| is None, assumes the cwd is already the product
207 dir. Otherwise, |product_dir| is the relative path to the product 212 dir. Otherwise, |product_dir| is the relative path to the product
208 dir. 213 dir.
209 """ 214 """
210 215
211 PRODUCT_DIR = '$!PRODUCT_DIR' 216 PRODUCT_DIR = '$!PRODUCT_DIR'
212 if PRODUCT_DIR in path: 217 if PRODUCT_DIR in path:
(...skipping 30 matching lines...) Expand all
243 248
244 See the above discourse on path conversions.""" 249 See the above discourse on path conversions."""
245 if env: 250 if env:
246 if self.flavor == 'mac': 251 if self.flavor == 'mac':
247 path = gyp.xcode_emulation.ExpandEnvVars(path, env) 252 path = gyp.xcode_emulation.ExpandEnvVars(path, env)
248 elif self.flavor == 'win': 253 elif self.flavor == 'win':
249 path = gyp.msvs_emulation.ExpandMacros(path, env) 254 path = gyp.msvs_emulation.ExpandMacros(path, env)
250 if path.startswith('$!'): 255 if path.startswith('$!'):
251 expanded = self.ExpandSpecial(path) 256 expanded = self.ExpandSpecial(path)
252 if self.flavor == 'win': 257 if self.flavor == 'win':
253 expanded = os.path.normpath(expanded) 258 expanded = self._WinCase(os.path.normpath(expanded))
254 return expanded 259 return expanded
255 assert '$' not in path, path 260 assert '$' not in path, path
256 return os.path.normpath(os.path.join(self.build_to_base, path)) 261 return self._WinCase(
262 os.path.normpath(os.path.join(self.build_to_base, path)))
257 263
258 def GypPathToUniqueOutput(self, path, qualified=True): 264 def GypPathToUniqueOutput(self, path, qualified=True):
259 """Translate a gyp path to a ninja path for writing output. 265 """Translate a gyp path to a ninja path for writing output.
260 266
261 If qualified is True, qualify the resulting filename with the name 267 If qualified is True, qualify the resulting filename with the name
262 of the target. This is necessary when e.g. compiling the same 268 of the target. This is necessary when e.g. compiling the same
263 path twice for two separate output targets. 269 path twice for two separate output targets.
264 270
265 See the above discourse on path conversions.""" 271 See the above discourse on path conversions."""
266 272
(...skipping 11 matching lines...) Expand all
278 # its path, even if the input is brought via a gyp file with '..'. 284 # its path, even if the input is brought via a gyp file with '..'.
279 # 2) simple files like libraries and stamps have a simple filename. 285 # 2) simple files like libraries and stamps have a simple filename.
280 286
281 obj = 'obj' 287 obj = 'obj'
282 if self.toolset != 'target': 288 if self.toolset != 'target':
283 obj += '.' + self.toolset 289 obj += '.' + self.toolset
284 290
285 path_dir, path_basename = os.path.split(path) 291 path_dir, path_basename = os.path.split(path)
286 if qualified: 292 if qualified:
287 path_basename = self.name + '.' + path_basename 293 path_basename = self.name + '.' + path_basename
288 return os.path.normpath(os.path.join(obj, self.base_dir, path_dir, 294 return self._WinCase(
289 path_basename)) 295 os.path.normpath(os.path.join(obj, self.base_dir, path_dir,
296 path_basename)))
290 297
291 def WriteCollapsedDependencies(self, name, targets): 298 def WriteCollapsedDependencies(self, name, targets):
292 """Given a list of targets, return a path for a single file 299 """Given a list of targets, return a path for a single file
293 representing the result of building all the targets or None. 300 representing the result of building all the targets or None.
294 301
295 Uses a stamp file if necessary.""" 302 Uses a stamp file if necessary."""
296 303
297 assert targets == filter(None, targets), targets 304 assert targets == filter(None, targets), targets
298 if len(targets) == 0: 305 if len(targets) == 0:
299 return None 306 return None
(...skipping 109 matching lines...) Expand 10 before | Expand all | Expand 10 after
409 source, self.config_name) 416 source, self.config_name)
410 outdir = self.GypPathToNinja(outdir) 417 outdir = self.GypPathToNinja(outdir)
411 def fix_path(path, rel=None): 418 def fix_path(path, rel=None):
412 path = os.path.join(outdir, path) 419 path = os.path.join(outdir, path)
413 dirname, basename = os.path.split(source) 420 dirname, basename = os.path.split(source)
414 root, ext = os.path.splitext(basename) 421 root, ext = os.path.splitext(basename)
415 path = self.ExpandRuleVariables( 422 path = self.ExpandRuleVariables(
416 path, root, dirname, source, ext, basename) 423 path, root, dirname, source, ext, basename)
417 if rel: 424 if rel:
418 path = os.path.relpath(path, rel) 425 path = os.path.relpath(path, rel)
419 return path 426 return self._WinCase(path)
420 vars = [(name, fix_path(value, outdir)) for name, value in vars] 427 vars = [(name, fix_path(value, outdir)) for name, value in vars]
421 output = [fix_path(p) for p in output] 428 output = [fix_path(p) for p in output]
422 vars.append(('outdir', outdir)) 429 vars.append(('outdir', outdir))
423 vars.append(('idlflags', flags)) 430 vars.append(('idlflags', flags))
424 input = self.GypPathToNinja(source) 431 input = self.GypPathToNinja(source)
425 self.ninja.build(output, 'idl', input, 432 self.ninja.build(output, 'idl', input,
426 variables=vars, order_only=prebuild) 433 variables=vars, order_only=prebuild)
427 outputs.extend(output) 434 outputs.extend(output)
428 435
429 def WriteWinIdlFiles(self, spec, prebuild): 436 def WriteWinIdlFiles(self, spec, prebuild):
(...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after
490 # First write out a rule for the action. 497 # First write out a rule for the action.
491 name = re.sub(r'[ {}$]', '_', action['action_name']) 498 name = re.sub(r'[ {}$]', '_', action['action_name'])
492 description = self.GenerateDescription('ACTION', 499 description = self.GenerateDescription('ACTION',
493 action.get('message', None), 500 action.get('message', None),
494 name) 501 name)
495 is_cygwin = (self.msvs_settings.IsRuleRunUnderCygwin(action) 502 is_cygwin = (self.msvs_settings.IsRuleRunUnderCygwin(action)
496 if self.flavor == 'win' else False) 503 if self.flavor == 'win' else False)
497 rule_name = self.WriteNewNinjaRule(name, action['action'], description, 504 rule_name = self.WriteNewNinjaRule(name, action['action'], description,
498 is_cygwin, env=env) 505 is_cygwin, env=env)
499 506
500 inputs = [self.GypPathToNinja(i, env) for i in action['inputs']] 507 def reldir(path):
508 qualified = os.path.join(self.build_dir, path)
509 return self._WinCase(
510 os.path.normpath(os.path.relpath(qualified, self.build_dir)))
Nico 2012/04/25 23:18:23 What does this do?
scottmg 2012/04/26 03:06:46 Makes one's head hurt. :/ It was trying to be a b
511
512 inputs = [reldir(self.GypPathToNinja(i, env)) for i in action['inputs']]
501 if int(action.get('process_outputs_as_sources', False)): 513 if int(action.get('process_outputs_as_sources', False)):
502 extra_sources += action['outputs'] 514 extra_sources += action['outputs']
503 if int(action.get('process_outputs_as_mac_bundle_resources', False)): 515 if int(action.get('process_outputs_as_mac_bundle_resources', False)):
504 extra_mac_bundle_resources += action['outputs'] 516 extra_mac_bundle_resources += action['outputs']
505 outputs = [self.GypPathToNinja(o, env) for o in action['outputs']] 517 outputs = [reldir(self.GypPathToNinja(o, env)) for o in action['outputs']]
506 518
507 # Then write out an edge using the rule. 519 # Then write out an edge using the rule.
508 self.ninja.build(outputs, rule_name, inputs, 520 self.ninja.build(outputs, rule_name, inputs,
509 order_only=prebuild) 521 order_only=prebuild)
510 all_outputs += outputs 522 all_outputs += outputs
511 523
512 self.ninja.newline() 524 self.ninja.newline()
513 525
514 return all_outputs 526 return all_outputs
515 527
(...skipping 143 matching lines...) Expand 10 before | Expand all | Expand 10 after
659 cflags_cc = self.xcode_settings.GetCflagsCC(config_name) 671 cflags_cc = self.xcode_settings.GetCflagsCC(config_name)
660 cflags_objc = ['$cflags_c'] + \ 672 cflags_objc = ['$cflags_c'] + \
661 self.xcode_settings.GetCflagsObjC(config_name) 673 self.xcode_settings.GetCflagsObjC(config_name)
662 cflags_objcc = ['$cflags_cc'] + \ 674 cflags_objcc = ['$cflags_cc'] + \
663 self.xcode_settings.GetCflagsObjCC(config_name) 675 self.xcode_settings.GetCflagsObjCC(config_name)
664 elif self.flavor == 'win': 676 elif self.flavor == 'win':
665 cflags = self.msvs_settings.GetCflags(config_name) 677 cflags = self.msvs_settings.GetCflags(config_name)
666 cflags_c = self.msvs_settings.GetCflagsC(config_name) 678 cflags_c = self.msvs_settings.GetCflagsC(config_name)
667 cflags_cc = self.msvs_settings.GetCflagsCC(config_name) 679 cflags_cc = self.msvs_settings.GetCflagsCC(config_name)
668 extra_defines = self.msvs_settings.GetComputedDefines(config_name) 680 extra_defines = self.msvs_settings.GetComputedDefines(config_name)
669 self.WriteVariableList('pdbname', [self.name + '.pdb']) 681 self.WriteVariableList('pdbname', [self.name.lower() + '.pdb'])
670 else: 682 else:
671 cflags = config.get('cflags', []) 683 cflags = config.get('cflags', [])
672 cflags_c = config.get('cflags_c', []) 684 cflags_c = config.get('cflags_c', [])
673 cflags_cc = config.get('cflags_cc', []) 685 cflags_cc = config.get('cflags_cc', [])
674 686
675 defines = config.get('defines', []) + extra_defines 687 defines = config.get('defines', []) + extra_defines
676 self.WriteVariableList('defines', 688 self.WriteVariableList('defines',
677 [QuoteShellArgument(ninja_syntax.escape('-D' + d), self.flavor) 689 [QuoteShellArgument(ninja_syntax.escape('-D' + d), self.flavor)
678 for d in defines]) 690 for d in defines])
679 if self.flavor == 'win': 691 if self.flavor == 'win':
(...skipping 311 matching lines...) Expand 10 before | Expand all | Expand 10 after
991 1003
992 if 'product_name' in spec: 1004 if 'product_name' in spec:
993 # If we were given an explicit name, use that. 1005 # If we were given an explicit name, use that.
994 target = spec['product_name'] 1006 target = spec['product_name']
995 else: 1007 else:
996 # Otherwise, derive a name from the target name. 1008 # Otherwise, derive a name from the target name.
997 target = spec['target_name'] 1009 target = spec['target_name']
998 if prefix == 'lib': 1010 if prefix == 'lib':
999 # Snip out an extra 'lib' from libs if appropriate. 1011 # Snip out an extra 'lib' from libs if appropriate.
1000 target = StripPrefix(target, 'lib') 1012 target = StripPrefix(target, 'lib')
1013 target = self._WinCase(target)
1001 1014
1002 if type in ('static_library', 'loadable_module', 'shared_library', 1015 if type in ('static_library', 'loadable_module', 'shared_library',
1003 'executable'): 1016 'executable'):
1004 return '%s%s%s' % (prefix, target, extension) 1017 return '%s%s%s' % (prefix, target, extension)
1005 elif type == 'none': 1018 elif type == 'none':
1006 return '%s.stamp' % target 1019 return '%s.stamp' % target
1007 else: 1020 else:
1008 raise 'Unhandled output type', type 1021 raise 'Unhandled output type', type
1009 1022
1010 def ComputeOutput(self, spec, type=None): 1023 def ComputeOutput(self, spec, type=None):
1011 """Compute the path for the final output of the spec.""" 1024 """Compute the path for the final output of the spec."""
1012 assert not self.is_mac_bundle or type 1025 assert not self.is_mac_bundle or type
1013 1026
1014 if not type: 1027 if not type:
1015 type = spec['type'] 1028 type = spec['type']
1016 1029
1017 if self.flavor == 'win': 1030 if self.flavor == 'win':
1018 overridden_name = self.msvs_settings.GetOutputName(spec, self.config_name) 1031 overridden_name = self.msvs_settings.GetOutputName(spec, self.config_name)
1019 if overridden_name: 1032 if overridden_name:
1020 return self.ExpandSpecial(overridden_name, self.base_to_build) 1033 return self._WinCase(
1034 self.ExpandSpecial(overridden_name, self.base_to_build))
1021 1035
1022 if self.flavor == 'mac' and type in ( 1036 if self.flavor == 'mac' and type in (
1023 'static_library', 'executable', 'shared_library', 'loadable_module'): 1037 'static_library', 'executable', 'shared_library', 'loadable_module'):
1024 filename = self.xcode_settings.GetExecutablePath() 1038 filename = self.xcode_settings.GetExecutablePath()
1025 else: 1039 else:
1026 filename = self.ComputeOutputFileName(spec, type) 1040 filename = self.ComputeOutputFileName(spec, type)
1027 1041
1028 if 'product_dir' in spec: 1042 if 'product_dir' in spec:
1029 path = os.path.join(spec['product_dir'], filename) 1043 path = os.path.join(spec['product_dir'], filename)
1030 return self.ExpandSpecial(path) 1044 return self.ExpandSpecial(path)
(...skipping 160 matching lines...) Expand 10 before | Expand all | Expand 10 after
1191 out.write('@call "%s"\n' % vsvars_path) 1205 out.write('@call "%s"\n' % vsvars_path)
1192 out.close() 1206 out.close()
1193 1207
1194 1208
1195 def GenerateOutputForConfig(target_list, target_dicts, data, params, 1209 def GenerateOutputForConfig(target_list, target_dicts, data, params,
1196 config_name): 1210 config_name):
1197 options = params['options'] 1211 options = params['options']
1198 flavor = gyp.common.GetFlavor(params) 1212 flavor = gyp.common.GetFlavor(params)
1199 generator_flags = params.get('generator_flags', {}) 1213 generator_flags = params.get('generator_flags', {})
1200 1214
1215 def wincase(path):
Nico 2012/04/25 23:18:23 Make the function at the top global, then you don'
1216 if flavor == 'win':
1217 path = path.lower()
1218 return path
1219
1201 # build_dir: relative path from source root to our output files. 1220 # build_dir: relative path from source root to our output files.
1202 # e.g. "out/Debug" 1221 # e.g. "out/Debug"
1203 build_dir = os.path.join(generator_flags.get('output_dir', 'out'), 1222 build_dir = os.path.join(generator_flags.get('output_dir', 'out'),
1204 config_name) 1223 config_name)
1205 1224
1206 toplevel_build = os.path.join(options.toplevel_dir, build_dir) 1225 toplevel_build = wincase(os.path.join(options.toplevel_dir, build_dir))
1207 1226
1208 master_ninja = ninja_syntax.Writer( 1227 master_ninja = ninja_syntax.Writer(
1209 OpenOutput(os.path.join(toplevel_build, 'build.ninja')), 1228 OpenOutput(os.path.join(toplevel_build, 'build.ninja')),
1210 width=120) 1229 width=120)
1211 1230
1212 # Put build-time support tools in out/{config_name}. 1231 # Put build-time support tools in out/{config_name}.
1213 gyp.common.CopyTool(flavor, toplevel_build) 1232 gyp.common.CopyTool(flavor, toplevel_build)
1214 1233
1215 # Grab make settings for CC/CXX. 1234 # Grab make settings for CC/CXX.
1216 if flavor == 'win': 1235 if flavor == 'win':
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after
1270 depfile='$out.d') 1289 depfile='$out.d')
1271 else: 1290 else:
1272 # TODO(scottmg): Requires fork of ninja for dependency and linking 1291 # TODO(scottmg): Requires fork of ninja for dependency and linking
1273 # support: https://github.com/sgraham/ninja 1292 # support: https://github.com/sgraham/ninja
1274 master_ninja.rule( 1293 master_ninja.rule(
1275 'cc', 1294 'cc',
1276 description='CC $out', 1295 description='CC $out',
1277 command=('cmd /s /c "$cc /nologo /showIncludes ' 1296 command=('cmd /s /c "$cc /nologo /showIncludes '
1278 '@$out.rsp ' 1297 '@$out.rsp '
1279 '$cflags_pch_c /c $in /Fo$out /Fd$pdbname ' 1298 '$cflags_pch_c /c $in /Fo$out /Fd$pdbname '
1280 '| ninja-deplist-helper -q -f cl -o $out.dl"'), 1299 '| ninja-deplist-helper -r . -q -f cl -o $out.dl"'),
1281 deplist='$out.dl', 1300 deplist='$out.dl',
1282 rspfile='$out.rsp', 1301 rspfile='$out.rsp',
1283 rspfile_content='$defines $includes $cflags $cflags_c') 1302 rspfile_content='$defines $includes $cflags $cflags_c')
1284 master_ninja.rule( 1303 master_ninja.rule(
1285 'cxx', 1304 'cxx',
1286 description='CXX $out', 1305 description='CXX $out',
1287 command=('cmd /s /c "$cxx /nologo /showIncludes ' 1306 command=('cmd /s /c "$cxx /nologo /showIncludes '
1288 '@$out.rsp ' 1307 '@$out.rsp '
1289 '$cflags_pch_cc /c $in /Fo$out /Fd$pdbname ' 1308 '$cflags_pch_cc /c $in /Fo$out /Fd$pdbname '
1290 '| ninja-deplist-helper -q -f cl -o $out.dl"'), 1309 '| ninja-deplist-helper -r . -q -f cl -o $out.dl"'),
1291 deplist='$out.dl', 1310 deplist='$out.dl',
1292 rspfile='$out.rsp', 1311 rspfile='$out.rsp',
1293 rspfile_content='$defines $includes $cflags $cflags_cc') 1312 rspfile_content='$defines $includes $cflags $cflags_cc')
1294 master_ninja.rule( 1313 master_ninja.rule(
1295 'idl', 1314 'idl',
1296 description='IDL $in', 1315 description='IDL $in',
1297 command=('python gyp-win-tool midl-wrapper $outdir ' 1316 command=('python gyp-win-tool midl-wrapper $outdir '
1298 '$tlb $h $dlldata $iid $proxy $in ' 1317 '$tlb $h $dlldata $iid $proxy $in '
1299 '$idlflags')) 1318 '$idlflags'))
1300 master_ninja.rule( 1319 master_ninja.rule(
(...skipping 139 matching lines...) Expand 10 before | Expand all | Expand 10 after
1440 spec = target_dicts[qualified_target] 1459 spec = target_dicts[qualified_target]
1441 if flavor == 'mac': 1460 if flavor == 'mac':
1442 gyp.xcode_emulation.MergeGlobalXcodeSettingsToSpec(data[build_file], spec) 1461 gyp.xcode_emulation.MergeGlobalXcodeSettingsToSpec(data[build_file], spec)
1443 1462
1444 build_file = gyp.common.RelativePath(build_file, options.toplevel_dir) 1463 build_file = gyp.common.RelativePath(build_file, options.toplevel_dir)
1445 1464
1446 base_path = os.path.dirname(build_file) 1465 base_path = os.path.dirname(build_file)
1447 obj = 'obj' 1466 obj = 'obj'
1448 if toolset != 'target': 1467 if toolset != 'target':
1449 obj += '.' + toolset 1468 obj += '.' + toolset
1450 output_file = os.path.join(obj, base_path, name + '.ninja') 1469 output_file = wincase(os.path.join(obj, base_path, name + '.ninja'))
1451 1470
1452 abs_build_dir = os.path.abspath(toplevel_build) 1471 abs_build_dir = os.path.abspath(toplevel_build)
1453 writer = NinjaWriter(target_outputs, base_path, build_dir, 1472 writer = NinjaWriter(target_outputs, base_path, build_dir,
1454 OpenOutput(os.path.join(toplevel_build, output_file)), 1473 OpenOutput(os.path.join(toplevel_build, output_file)),
1455 flavor, abs_build_dir=abs_build_dir) 1474 flavor, abs_build_dir=abs_build_dir)
1456 master_ninja.subninja(output_file) 1475 master_ninja.subninja(output_file)
1457 1476
1458 target = writer.WriteSpec(spec, config_name, generator_flags) 1477 target = writer.WriteSpec(spec, config_name, generator_flags)
1459 if target: 1478 if target:
1460 target_outputs[qualified_target] = target 1479 target_outputs[qualified_target] = target
(...skipping 10 matching lines...) Expand all
1471 1490
1472 user_config = params.get('generator_flags', {}).get('config', None) 1491 user_config = params.get('generator_flags', {}).get('config', None)
1473 if user_config: 1492 if user_config:
1474 GenerateOutputForConfig(target_list, target_dicts, data, params, 1493 GenerateOutputForConfig(target_list, target_dicts, data, params,
1475 user_config) 1494 user_config)
1476 else: 1495 else:
1477 config_names = target_dicts[target_list[0]]['configurations'].keys() 1496 config_names = target_dicts[target_list[0]]['configurations'].keys()
1478 for config_name in config_names: 1497 for config_name in config_names:
1479 GenerateOutputForConfig(target_list, target_dicts, data, params, 1498 GenerateOutputForConfig(target_list, target_dicts, data, params,
1480 config_name) 1499 config_name)
OLDNEW
« no previous file with comments | « no previous file | pylib/gyp/msvs_emulation.py » ('j') | test/ninja/normalize-paths-win/gyptest-normalize-paths.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698