| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env dart | |
| 2 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | |
| 3 // for details. All rights reserved. Use of this source code is governed by a | |
| 4 // BSD-style license that can be found in the LICENSE file. | |
| 5 | |
| 6 /// Command line tool to merge the SDK libraries and our patch files. | |
| 7 /// This is currently designed as an offline tool, but we could automate it. | |
| 8 | |
| 9 import 'dart:io'; | |
| 10 import 'dart:math' as math; | |
| 11 | |
| 12 import 'package:analyzer/analyzer.dart'; | |
| 13 import 'package:analyzer/src/generated/sdk.dart'; | |
| 14 import 'package:path/path.dart' as path; | |
| 15 | |
| 16 void main(List<String> argv) { | |
| 17 var base = path.fromUri(Platform.script); | |
| 18 var dartDir = path.dirname(path.dirname(path.absolute(base))); | |
| 19 | |
| 20 if (argv.length != 4 || | |
| 21 !argv.isEmpty && argv.first != 'vm' && argv.first != 'ddc') { | |
| 22 var self = path.relative(base); | |
| 23 print('Usage: $self MODE SDK_DIR PATCH_DIR OUTPUT_DIR'); | |
| 24 print('MODE must be one of ddc or vm.'); | |
| 25 | |
| 26 var toolDir = path.relative(path.dirname(base)); | |
| 27 var sdkExample = path.join(toolDir, 'input_sdk'); | |
| 28 var patchExample = path.join(sdkExample, 'patch'); | |
| 29 var outExample = | |
| 30 path.relative(path.normalize(path.join('gen', 'patched_sdk'))); | |
| 31 print('For example:'); | |
| 32 print('\$ $self ddc $sdkExample $patchExample $outExample'); | |
| 33 | |
| 34 var repositoryDir = path.relative(path.dirname(path.dirname(base))); | |
| 35 sdkExample = path.relative(path.join(repositoryDir, 'sdk')); | |
| 36 patchExample = path.relative(path.join(repositoryDir, 'out', 'DebugX64', | |
| 37 'obj', 'gen', 'patch')); | |
| 38 outExample = path.relative(path.join(repositoryDir, 'out', 'DebugX64', | |
| 39 'obj', 'gen', 'patched_sdk')); | |
| 40 print('or:'); | |
| 41 print('\$ $self vm $sdkExample $patchExample $outExample'); | |
| 42 | |
| 43 exit(1); | |
| 44 } | |
| 45 | |
| 46 var mode = argv[0]; | |
| 47 var input = argv[1]; | |
| 48 var sdkLibIn = path.join(input, 'lib'); | |
| 49 var patchIn = argv[2]; | |
| 50 var sdkOut = path.join(argv[3], 'lib'); | |
| 51 | |
| 52 var privateIn = path.join(input, 'private'); | |
| 53 var INTERNAL_PATH = '_internal/compiler/js_lib/'; | |
| 54 | |
| 55 // Copy libraries.dart and version | |
| 56 var libContents = new File(path.join(sdkLibIn, '_internal', | |
| 57 'sdk_library_metadata', 'lib', 'libraries.dart')).readAsStringSync(); | |
| 58 _writeSync( | |
| 59 path.join( | |
| 60 sdkOut, '_internal', 'sdk_library_metadata', 'lib', 'libraries.dart'), | |
| 61 libContents); | |
| 62 if (mode == 'ddc') { | |
| 63 _writeSync(path.join(sdkOut, '..', 'version'), | |
| 64 new File(path.join(sdkLibIn, '..', 'version')).readAsStringSync()); | |
| 65 } | |
| 66 | |
| 67 // Parse libraries.dart | |
| 68 var sdkLibraries = _getSdkLibraries(libContents); | |
| 69 | |
| 70 // Enumerate core libraries and apply patches | |
| 71 for (SdkLibrary library in sdkLibraries) { | |
| 72 // TODO(jmesserly): analyzer does not handle the default case of | |
| 73 // "both platforms" correctly, and treats it as being supported on neither. | |
| 74 // So instead we skip explicitly marked as either VM or dart2js libs. | |
| 75 if (mode == 'ddc' ? libary.isVmLibrary : library.isDart2JsLibrary) { | |
| 76 continue; | |
| 77 } | |
| 78 | |
| 79 var libraryOut = path.join(sdkLibIn, library.path); | |
| 80 var libraryIn; | |
| 81 if (mode == 'vm' && library.path.contains('typed_data.dart')) { | |
| 82 // dart:typed_data is unlike the other libraries in the SDK. The VM does | |
| 83 // not apply a patch to the base SDK implementation of the library. | |
| 84 // Instead, the VM provides a replacement implementation and ignores the | |
| 85 // sources in the SDK. | |
| 86 libraryIn = | |
| 87 path.join(dartDir, 'runtime', 'lib', 'typed_data.dart'); | |
| 88 } else if (mode == 'ddc' && library.path.contains(INTERNAL_PATH)) { | |
| 89 libraryIn = | |
| 90 path.join(privateIn, library.path.replaceAll(INTERNAL_PATH, '')); | |
| 91 } else { | |
| 92 libraryIn = libraryOut; | |
| 93 } | |
| 94 | |
| 95 var libraryFile = new File(libraryIn); | |
| 96 if (libraryFile.existsSync()) { | |
| 97 var outPaths = <String>[libraryOut]; | |
| 98 var libraryContents = libraryFile.readAsStringSync(); | |
| 99 | |
| 100 int inputModifyTime = | |
| 101 libraryFile.lastModifiedSync().millisecondsSinceEpoch; | |
| 102 var partFiles = <File>[]; | |
| 103 for (var part in parseDirectives(libraryContents).directives) { | |
| 104 if (part is PartDirective) { | |
| 105 var partPath = part.uri.stringValue; | |
| 106 outPaths.add(path.join(path.dirname(libraryOut), partPath)); | |
| 107 | |
| 108 var partFile = new File(path.join(path.dirname(libraryIn), partPath)); | |
| 109 partFiles.add(partFile); | |
| 110 inputModifyTime = math.max(inputModifyTime, | |
| 111 partFile.lastModifiedSync().millisecondsSinceEpoch); | |
| 112 } | |
| 113 } | |
| 114 | |
| 115 // See if we can find a patch file. | |
| 116 var patchPath = path.join( | |
| 117 patchIn, path.basenameWithoutExtension(libraryIn) + '_patch.dart'); | |
| 118 | |
| 119 var patchFile = new File(patchPath); | |
| 120 bool patchExists = patchFile.existsSync(); | |
| 121 if (patchExists) { | |
| 122 inputModifyTime = math.max(inputModifyTime, | |
| 123 patchFile.lastModifiedSync().millisecondsSinceEpoch); | |
| 124 } | |
| 125 | |
| 126 // Compute output paths | |
| 127 outPaths = outPaths | |
| 128 .map((p) => path.join(sdkOut, path.relative(p, from: sdkLibIn))) | |
| 129 .toList(); | |
| 130 | |
| 131 // Compare output modify time with input modify time. | |
| 132 bool needsUpdate = false; | |
| 133 for (var outPath in outPaths) { | |
| 134 var outFile = new File(outPath); | |
| 135 if (!outFile.existsSync() || | |
| 136 outFile.lastModifiedSync().millisecondsSinceEpoch < | |
| 137 inputModifyTime) { | |
| 138 needsUpdate = true; | |
| 139 break; | |
| 140 } | |
| 141 } | |
| 142 | |
| 143 if (needsUpdate) { | |
| 144 var contents = <String>[libraryContents]; | |
| 145 contents.addAll(partFiles.map((f) => f.readAsStringSync())); | |
| 146 if (patchExists) { | |
| 147 var patchContents = patchFile.readAsStringSync(); | |
| 148 contents = _patchLibrary( | |
| 149 patchFile.toString(), contents, patchContents); | |
| 150 } | |
| 151 | |
| 152 for (var i = 0; i < outPaths.length; i++) { | |
| 153 if (path.basename(outPaths[i]) == 'internal.dart') { | |
| 154 contents[i] += ''' | |
| 155 | |
| 156 /// Marks a function as an external implementation ("native" in the Dart VM). | |
| 157 /// | |
| 158 /// Provides a backend-specific String that can be used to identify the | |
| 159 /// function's implementation | |
| 160 class ExternalName { | |
| 161 final String name; | |
| 162 const ExternalName(this.name); | |
| 163 } | |
| 164 '''; | |
| 165 } | |
| 166 | |
| 167 _writeSync(outPaths[i], contents[i]); | |
| 168 } | |
| 169 } | |
| 170 } | |
| 171 } | |
| 172 if (mode == 'vm') { | |
| 173 for (var tuple in [['nativewrappers', 'nativewrappers.dart'], | |
| 174 ['_builtin', 'builtin.dart']]) { | |
| 175 var vmLibrary = tuple[0]; | |
| 176 var dartFile = tuple[1]; | |
| 177 | |
| 178 // The "dart:_builtin" library is only available for the DartVM. | |
| 179 var builtinLibraryIn = path.join(dartDir, 'runtime', 'bin', dartFile); | |
| 180 var builtinLibraryOut = path.join(sdkOut, vmLibrary, '${vmLibrary}.dart'); | |
| 181 _writeSync(builtinLibraryOut, new File(builtinLibraryIn).readAsStringSync(
)); | |
| 182 } | |
| 183 | |
| 184 for (var file in ['loader.dart', 'server.dart', 'vmservice_io.dart']) { | |
| 185 var libraryIn = path.join(dartDir, 'runtime', 'bin', 'vmservice', file); | |
| 186 var libraryOut = path.join(sdkOut, 'vmservice_io', file); | |
| 187 _writeSync(libraryOut, new File(libraryIn).readAsStringSync()); | |
| 188 } | |
| 189 } | |
| 190 } | |
| 191 | |
| 192 /// Writes a file, creating the directory if needed. | |
| 193 void _writeSync(String filePath, String contents) { | |
| 194 var outDir = new Directory(path.dirname(filePath)); | |
| 195 if (!outDir.existsSync()) outDir.createSync(recursive: true); | |
| 196 | |
| 197 new File(filePath).writeAsStringSync(contents); | |
| 198 } | |
| 199 | |
| 200 /// Merges dart:* library code with code from *_patch.dart file. | |
| 201 /// | |
| 202 /// Takes a list of the library's parts contents, with the main library contents | |
| 203 /// first in the list, and the contents of the patch file. | |
| 204 /// | |
| 205 /// The result will have `@patch` implementations merged into the correct place | |
| 206 /// (e.g. the class or top-level function declaration) and all other | |
| 207 /// declarations introduced by the patch will be placed into the main library | |
| 208 /// file. | |
| 209 /// | |
| 210 /// This is purely a syntactic transformation. Unlike dart2js patch files, there | |
| 211 /// is no semantic meaning given to the *_patch files, and they do not magically | |
| 212 /// get their own library scope, etc. | |
| 213 /// | |
| 214 /// Editorializing: the dart2js approach requires a Dart front end such as | |
| 215 /// package:analyzer to semantically model a feature beyond what is specified | |
| 216 /// in the Dart language. Since this feature is only for the convenience of | |
| 217 /// writing the dart:* libraries, and not a tool given to Dart developers, it | |
| 218 /// seems like a non-ideal situation. Instead we keep the preprocessing simple. | |
| 219 List<String> _patchLibrary(String name, | |
| 220 List<String> partsContents, | |
| 221 String patchContents) { | |
| 222 var results = <StringEditBuffer>[]; | |
| 223 | |
| 224 // Parse the patch first. We'll need to extract bits of this as we go through | |
| 225 // the other files. | |
| 226 final patchFinder = new PatchFinder.parseAndVisit(name, patchContents); | |
| 227 | |
| 228 // Merge `external` declarations with the corresponding `@patch` code. | |
| 229 for (var partContent in partsContents) { | |
| 230 var partEdits = new StringEditBuffer(partContent); | |
| 231 var partUnit = parseCompilationUnit(partContent); | |
| 232 partUnit.accept(new PatchApplier(partEdits, patchFinder)); | |
| 233 results.add(partEdits); | |
| 234 } | |
| 235 return new List<String>.from(results.map((e) => e.toString())); | |
| 236 } | |
| 237 | |
| 238 /// Merge `@patch` declarations into `external` declarations. | |
| 239 class PatchApplier extends GeneralizingAstVisitor { | |
| 240 final StringEditBuffer edits; | |
| 241 final PatchFinder patch; | |
| 242 | |
| 243 bool _isLibrary = true; // until proven otherwise. | |
| 244 | |
| 245 PatchApplier(this.edits, this.patch); | |
| 246 | |
| 247 @override | |
| 248 visitCompilationUnit(CompilationUnit node) { | |
| 249 super.visitCompilationUnit(node); | |
| 250 if (_isLibrary) _mergeUnpatched(node); | |
| 251 } | |
| 252 | |
| 253 void _merge(AstNode node, int pos) { | |
| 254 var code = patch.contents.substring(node.offset, node.end); | |
| 255 edits.insert(pos, '\n' + code); | |
| 256 } | |
| 257 | |
| 258 /// Merges directives and declarations that are not `@patch` into the library. | |
| 259 void _mergeUnpatched(CompilationUnit unit) { | |
| 260 // Merge imports from the patch | |
| 261 // TODO(jmesserly): remove duplicate imports | |
| 262 | |
| 263 // To patch a library, we must have a library directive | |
| 264 var libDir = unit.directives.first as LibraryDirective; | |
| 265 int importPos = unit.directives | |
| 266 .lastWhere((d) => d is ImportDirective, orElse: () => libDir) | |
| 267 .end; | |
| 268 for (var d in patch.unit.directives.where((d) => d is ImportDirective)) { | |
| 269 _merge(d, importPos); | |
| 270 } | |
| 271 | |
| 272 int partPos = unit.directives.last.end; | |
| 273 for (var d in patch.unit.directives.where((d) => d is PartDirective)) { | |
| 274 _merge(d, partPos); | |
| 275 } | |
| 276 | |
| 277 // Merge declarations from the patch | |
| 278 int declPos = edits.original.length; | |
| 279 for (var d in patch.mergeDeclarations) { | |
| 280 _merge(d, declPos); | |
| 281 } | |
| 282 } | |
| 283 | |
| 284 @override | |
| 285 visitPartOfDirective(PartOfDirective node) { | |
| 286 _isLibrary = false; | |
| 287 } | |
| 288 | |
| 289 @override | |
| 290 visitFunctionDeclaration(FunctionDeclaration node) { | |
| 291 _maybePatch(node); | |
| 292 } | |
| 293 | |
| 294 /// Merge patches and extensions into the class | |
| 295 @override | |
| 296 visitClassDeclaration(ClassDeclaration node) { | |
| 297 node.members.forEach(_maybePatch); | |
| 298 | |
| 299 var mergeMembers = patch.mergeMembers[_qualifiedName(node)]; | |
| 300 if (mergeMembers == null) return; | |
| 301 | |
| 302 // Merge members from the patch | |
| 303 var pos = node.members.last.end; | |
| 304 for (var member in mergeMembers) { | |
| 305 var code = patch.contents.substring(member.offset, member.end); | |
| 306 edits.insert(pos, '\n\n ' + code); | |
| 307 } | |
| 308 } | |
| 309 | |
| 310 void _maybePatch(AstNode node) { | |
| 311 if (node is FieldDeclaration) return; | |
| 312 | |
| 313 var externalKeyword = (node as dynamic).externalKeyword; | |
| 314 if (externalKeyword == null) return; | |
| 315 | |
| 316 var name = _qualifiedName(node); | |
| 317 var patchNode = patch.patches[name]; | |
| 318 if (patchNode == null) { | |
| 319 print('warning: patch not found for $name: $node'); | |
| 320 return; | |
| 321 } | |
| 322 | |
| 323 Annotation patchMeta = patchNode.metadata.lastWhere(_isPatchAnnotation); | |
| 324 int start = patchMeta.endToken.next.offset; | |
| 325 var code = patch.contents.substring(start, patchNode.end); | |
| 326 | |
| 327 // For some node like static fields, the node's offset doesn't include | |
| 328 // the external keyword. Also starting from the keyword lets us preserve | |
| 329 // documentation comments. | |
| 330 edits.replace(externalKeyword.offset, node.end, code); | |
| 331 } | |
| 332 } | |
| 333 | |
| 334 class PatchFinder extends GeneralizingAstVisitor { | |
| 335 final String contents; | |
| 336 final CompilationUnit unit; | |
| 337 | |
| 338 final Map patches = <String, Declaration>{}; | |
| 339 final Map mergeMembers = <String, List<ClassMember>>{}; | |
| 340 final List mergeDeclarations = <CompilationUnitMember>[]; | |
| 341 | |
| 342 PatchFinder.parseAndVisit(String name, String contents) | |
| 343 : contents = contents, | |
| 344 unit = parseCompilationUnit(contents, name: name) { | |
| 345 visitCompilationUnit(unit); | |
| 346 } | |
| 347 | |
| 348 @override | |
| 349 visitCompilationUnitMember(CompilationUnitMember node) { | |
| 350 mergeDeclarations.add(node); | |
| 351 } | |
| 352 | |
| 353 @override | |
| 354 visitClassDeclaration(ClassDeclaration node) { | |
| 355 if (_isPatch(node)) { | |
| 356 var members = <ClassMember>[]; | |
| 357 for (var member in node.members) { | |
| 358 if (_isPatch(member)) { | |
| 359 patches[_qualifiedName(member)] = member; | |
| 360 } else { | |
| 361 members.add(member); | |
| 362 } | |
| 363 } | |
| 364 if (members.isNotEmpty) { | |
| 365 mergeMembers[_qualifiedName(node)] = members; | |
| 366 } | |
| 367 } else { | |
| 368 mergeDeclarations.add(node); | |
| 369 } | |
| 370 } | |
| 371 | |
| 372 @override | |
| 373 visitFunctionDeclaration(FunctionDeclaration node) { | |
| 374 if (_isPatch(node)) { | |
| 375 patches[_qualifiedName(node)] = node; | |
| 376 } else { | |
| 377 mergeDeclarations.add(node); | |
| 378 } | |
| 379 } | |
| 380 | |
| 381 @override | |
| 382 visitFunctionBody(node) {} // skip method bodies | |
| 383 } | |
| 384 | |
| 385 String _qualifiedName(Declaration node) { | |
| 386 var parent = node.parent; | |
| 387 var className = ''; | |
| 388 if (parent is ClassDeclaration) { | |
| 389 className = parent.name.name + '.'; | |
| 390 } | |
| 391 var name = (node as dynamic).name; | |
| 392 name = (name != null ? name.name : ''); | |
| 393 | |
| 394 var accessor = ''; | |
| 395 if (node is MethodDeclaration) { | |
| 396 if (node.isGetter) accessor = 'get:'; | |
| 397 else if (node.isSetter) accessor = 'set:'; | |
| 398 } | |
| 399 return className + accessor + name; | |
| 400 } | |
| 401 | |
| 402 bool _isPatch(AnnotatedNode node) => node.metadata.any(_isPatchAnnotation); | |
| 403 | |
| 404 bool _isPatchAnnotation(Annotation m) => | |
| 405 m.name.name == 'patch' && m.constructorName == null && m.arguments == null; | |
| 406 | |
| 407 /// Editable string buffer. | |
| 408 /// | |
| 409 /// Applies a series of edits (insertions, removals, replacements) using | |
| 410 /// original location information, and composes them into the edited string. | |
| 411 /// | |
| 412 /// For example, starting with a parsed AST with original source locations, | |
| 413 /// this type allows edits to be made without regards to other edits. | |
| 414 class StringEditBuffer { | |
| 415 final String original; | |
| 416 final _edits = <_StringEdit>[]; | |
| 417 | |
| 418 /// Creates a new transaction. | |
| 419 StringEditBuffer(this.original); | |
| 420 | |
| 421 bool get hasEdits => _edits.length > 0; | |
| 422 | |
| 423 /// Edit the original text, replacing text on the range [begin] and | |
| 424 /// exclusive [end] with the [replacement] string. | |
| 425 void replace(int begin, int end, String replacement) { | |
| 426 _edits.add(new _StringEdit(begin, end, replacement)); | |
| 427 } | |
| 428 | |
| 429 /// Insert [string] at [offset]. | |
| 430 /// Equivalent to `replace(offset, offset, string)`. | |
| 431 void insert(int offset, String string) => replace(offset, offset, string); | |
| 432 | |
| 433 /// Remove text from the range [begin] to exclusive [end]. | |
| 434 /// Equivalent to `replace(begin, end, '')`. | |
| 435 void remove(int begin, int end) => replace(begin, end, ''); | |
| 436 | |
| 437 /// Applies all pending [edit]s and returns a new string. | |
| 438 /// | |
| 439 /// This method is non-destructive: it does not discard existing edits or | |
| 440 /// change the [original] string. Further edits can be added and this method | |
| 441 /// can be called again. | |
| 442 /// | |
| 443 /// Throws [UnsupportedError] if the edits were overlapping. If no edits were | |
| 444 /// made, the original string will be returned. | |
| 445 String toString() { | |
| 446 var sb = new StringBuffer(); | |
| 447 if (_edits.length == 0) return original; | |
| 448 | |
| 449 // Sort edits by start location. | |
| 450 _edits.sort(); | |
| 451 | |
| 452 int consumed = 0; | |
| 453 for (var edit in _edits) { | |
| 454 if (consumed > edit.begin) { | |
| 455 sb = new StringBuffer(); | |
| 456 sb.write('overlapping edits. Insert at offset '); | |
| 457 sb.write(edit.begin); | |
| 458 sb.write(' but have consumed '); | |
| 459 sb.write(consumed); | |
| 460 sb.write(' input characters. List of edits:'); | |
| 461 for (var e in _edits) { | |
| 462 sb.write('\n '); | |
| 463 sb.write(e); | |
| 464 } | |
| 465 throw new UnsupportedError(sb.toString()); | |
| 466 } | |
| 467 | |
| 468 // Add characters from the original string between this edit and the last | |
| 469 // one, if any. | |
| 470 var betweenEdits = original.substring(consumed, edit.begin); | |
| 471 sb.write(betweenEdits); | |
| 472 sb.write(edit.replace); | |
| 473 consumed = edit.end; | |
| 474 } | |
| 475 | |
| 476 // Add any text from the end of the original string that was not replaced. | |
| 477 sb.write(original.substring(consumed)); | |
| 478 return sb.toString(); | |
| 479 } | |
| 480 } | |
| 481 | |
| 482 class _StringEdit implements Comparable<_StringEdit> { | |
| 483 final int begin; | |
| 484 final int end; | |
| 485 final String replace; | |
| 486 | |
| 487 _StringEdit(this.begin, this.end, this.replace); | |
| 488 | |
| 489 int get length => end - begin; | |
| 490 | |
| 491 String toString() => '(Edit @ $begin,$end: "$replace")'; | |
| 492 | |
| 493 int compareTo(_StringEdit other) { | |
| 494 int diff = begin - other.begin; | |
| 495 if (diff != 0) return diff; | |
| 496 return end - other.end; | |
| 497 } | |
| 498 } | |
| 499 | |
| 500 List<SdkLibrary> _getSdkLibraries(String contents) { | |
| 501 var libraryBuilder = new SdkLibrariesReader_LibraryBuilder(true); | |
| 502 parseCompilationUnit(contents).accept(libraryBuilder); | |
| 503 return libraryBuilder.librariesMap.sdkLibraries; | |
| 504 } | |
| OLD | NEW |