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 |