OLD | NEW |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 /// Helper functionality to make working with IO easier. | 5 /// Helper functionality to make working with IO easier. |
6 library io; | 6 library io; |
7 | 7 |
| 8 import 'dart:async'; |
8 import 'dart:io'; | 9 import 'dart:io'; |
9 import 'dart:isolate'; | 10 import 'dart:isolate'; |
10 import 'dart:json'; | 11 import 'dart:json'; |
11 import 'dart:uri'; | 12 import 'dart:uri'; |
12 | 13 |
13 import '../../pkg/path/lib/path.dart' as path; | 14 import '../../pkg/path/lib/path.dart' as path; |
14 import 'log.dart' as log; | 15 import 'log.dart' as log; |
15 import 'utils.dart'; | 16 import 'utils.dart'; |
16 | 17 |
17 bool _isGitInstalledCache; | 18 bool _isGitInstalledCache; |
18 | 19 |
19 /// The cached Git command. | 20 /// The cached Git command. |
20 String _gitCommandCache; | 21 String _gitCommandCache; |
21 | 22 |
22 final NEWLINE_PATTERN = new RegExp("\r\n?|\n\r?"); | 23 final NEWLINE_PATTERN = new RegExp("\r\n?|\n\r?"); |
23 | 24 |
24 /// Joins a number of path string parts into a single path. Handles | 25 /// Joins a number of path string parts into a single path. Handles |
25 /// platform-specific path separators. Parts can be [String], [Directory], or | 26 /// platform-specific path separators. Parts can be [String], [Directory], or |
26 /// [File] objects. | 27 /// [File] objects. |
27 String join(part1, [part2, part3, part4, part5, part6, part7, part8]) { | 28 String join(part1, [part2, part3, part4, part5, part6, part7, part8]) { |
28 var parts = [part1, part2, part3, part4, part5, part6, part7, part8] | 29 var parts = [part1, part2, part3, part4, part5, part6, part7, part8] |
29 .map((part) => part == null ? null : _getPath(part)); | 30 .mappedBy((part) => part == null ? null : _getPath(part)).toList(); |
30 | 31 |
31 return path.join(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], | 32 return path.join(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], |
32 parts[6], parts[7]); | 33 parts[6], parts[7]); |
33 } | 34 } |
34 | 35 |
35 /// Gets the basename, the file name without any leading directory path, for | 36 /// Gets the basename, the file name without any leading directory path, for |
36 /// [file], which can either be a [String], [File], or [Directory]. | 37 /// [file], which can either be a [String], [File], or [Directory]. |
37 String basename(file) => path.basename(_getPath(file)); | 38 String basename(file) => path.basename(_getPath(file)); |
38 | 39 |
39 /// Gets the the leading directory path for [file], which can either be a | 40 /// Gets the the leading directory path for [file], which can either be a |
(...skipping 11 matching lines...) Expand all Loading... |
51 } | 52 } |
52 | 53 |
53 /// Returns the path to [target] from [base]. | 54 /// Returns the path to [target] from [base]. |
54 String relativeTo(target, base) => path.relative(target, from: base); | 55 String relativeTo(target, base) => path.relative(target, from: base); |
55 | 56 |
56 /// Asynchronously determines if [path], which can be a [String] file path, a | 57 /// Asynchronously determines if [path], which can be a [String] file path, a |
57 /// [File], or a [Directory] exists on the file system. Returns a [Future] that | 58 /// [File], or a [Directory] exists on the file system. Returns a [Future] that |
58 /// completes with the result. | 59 /// completes with the result. |
59 Future<bool> exists(path) { | 60 Future<bool> exists(path) { |
60 path = _getPath(path); | 61 path = _getPath(path); |
61 return Futures.wait([fileExists(path), dirExists(path)]).transform((results) { | 62 return Futures.wait([fileExists(path), dirExists(path)]).then((results) { |
62 return results[0] || results[1]; | 63 return results[0] || results[1]; |
63 }); | 64 }); |
64 } | 65 } |
65 | 66 |
66 /// Asynchronously determines if [file], which can be a [String] file path or a | 67 /// Asynchronously determines if [file], which can be a [String] file path or a |
67 /// [File], exists on the file system. Returns a [Future] that completes with | 68 /// [File], exists on the file system. Returns a [Future] that completes with |
68 /// the result. | 69 /// the result. |
69 Future<bool> fileExists(file) { | 70 Future<bool> fileExists(file) { |
70 var path = _getPath(file); | 71 var path = _getPath(file); |
71 return log.ioAsync("Seeing if file $path exists.", | 72 return log.ioAsync("Seeing if file $path exists.", |
(...skipping 24 matching lines...) Expand all Loading... |
96 Future<File> writeTextFile(file, String contents, {dontLogContents: false}) { | 97 Future<File> writeTextFile(file, String contents, {dontLogContents: false}) { |
97 var path = _getPath(file); | 98 var path = _getPath(file); |
98 file = new File(path); | 99 file = new File(path); |
99 | 100 |
100 // Sanity check: don't spew a huge file. | 101 // Sanity check: don't spew a huge file. |
101 log.io("Writing ${contents.length} characters to text file $path."); | 102 log.io("Writing ${contents.length} characters to text file $path."); |
102 if (!dontLogContents && contents.length < 1024 * 1024) { | 103 if (!dontLogContents && contents.length < 1024 * 1024) { |
103 log.fine("Contents:\n$contents"); | 104 log.fine("Contents:\n$contents"); |
104 } | 105 } |
105 | 106 |
106 return file.open(FileMode.WRITE).chain((opened) { | 107 return file.open(FileMode.WRITE).then((opened) { |
107 return opened.writeString(contents).chain((ignore) { | 108 return opened.writeString(contents).then((ignore) { |
108 return opened.close().transform((_) { | 109 return opened.close().then((_) { |
109 log.fine("Wrote text file $path."); | 110 log.fine("Wrote text file $path."); |
110 return file; | 111 return file; |
111 }); | 112 }); |
112 }); | 113 }); |
113 }); | 114 }); |
114 } | 115 } |
115 | 116 |
116 /// Asynchronously deletes [file], which can be a [String] or a [File]. Returns | 117 /// Asynchronously deletes [file], which can be a [String] or a [File]. Returns |
117 /// a [Future] that completes when the deletion is done. | 118 /// a [Future] that completes when the deletion is done. |
118 Future<File> deleteFile(file) { | 119 Future<File> deleteFile(file) { |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
170 } | 171 } |
171 | 172 |
172 /// Ensures that [path] and all its parent directories exist. If they don't | 173 /// Ensures that [path] and all its parent directories exist. If they don't |
173 /// exist, creates them. Returns a [Future] that completes once all the | 174 /// exist, creates them. Returns a [Future] that completes once all the |
174 /// directories are created. | 175 /// directories are created. |
175 Future<Directory> ensureDir(path) { | 176 Future<Directory> ensureDir(path) { |
176 path = _getPath(path); | 177 path = _getPath(path); |
177 log.fine("Ensuring directory $path exists."); | 178 log.fine("Ensuring directory $path exists."); |
178 if (path == '.') return new Future.immediate(new Directory('.')); | 179 if (path == '.') return new Future.immediate(new Directory('.')); |
179 | 180 |
180 return dirExists(path).chain((exists) { | 181 return dirExists(path).then((exists) { |
181 if (exists) { | 182 if (exists) { |
182 log.fine("Directory $path already exists."); | 183 log.fine("Directory $path already exists."); |
183 return new Future.immediate(new Directory(path)); | 184 return new Future.immediate(new Directory(path)); |
184 } | 185 } |
185 | 186 |
186 return ensureDir(dirname(path)).chain((_) { | 187 return ensureDir(dirname(path)).then((_) { |
187 var completer = new Completer<Directory>(); | 188 return createDir(path) |
188 var future = createDir(path); | 189 .catchError((error) { |
189 future.handleException((error) { | 190 if (error is! DirectoryIOException) return false; |
190 if (error is! DirectoryIOException) return false; | 191 // Error 17 means the directory already exists (or 183 on Windows). |
191 // Error 17 means the directory already exists (or 183 on Windows). | 192 if (error.osError.errorCode != 17 && |
192 if (error.osError.errorCode != 17 && | 193 error.osError.errorCode != 183) { |
193 error.osError.errorCode != 183) { | |
194 log.fine("Got 'already exists' error when creating directory."); | 194 log.fine("Got 'already exists' error when creating directory."); |
195 return false; | 195 return false; |
196 } | 196 } |
197 | 197 |
198 completer.complete(_getDirectory(path)); | 198 return _getDirectory(path); |
199 return true; | 199 }); |
200 }); | |
201 future.then(completer.complete); | |
202 return completer.future; | |
203 }); | 200 }); |
204 }); | 201 }); |
205 } | 202 } |
206 | 203 |
207 /// Creates a temp directory whose name will be based on [dir] with a unique | 204 /// Creates a temp directory whose name will be based on [dir] with a unique |
208 /// suffix appended to it. If [dir] is not provided, a temp directory will be | 205 /// suffix appended to it. If [dir] is not provided, a temp directory will be |
209 /// created in a platform-dependent temporary location. Returns a [Future] that | 206 /// created in a platform-dependent temporary location. Returns a [Future] that |
210 /// completes when the directory is created. | 207 /// completes when the directory is created. |
211 Future<Directory> createTempDir([dir = '']) { | 208 Future<Directory> createTempDir([dir = '']) { |
212 dir = _getDirectory(dir); | 209 dir = _getDirectory(dir); |
(...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
303 return log.ioAsync("Seeing if directory ${dir.path} exists.", | 300 return log.ioAsync("Seeing if directory ${dir.path} exists.", |
304 dir.exists(), | 301 dir.exists(), |
305 (exists) => "Directory ${dir.path} " | 302 (exists) => "Directory ${dir.path} " |
306 "${exists ? 'exists' : 'does not exist'}."); | 303 "${exists ? 'exists' : 'does not exist'}."); |
307 } | 304 } |
308 | 305 |
309 /// "Cleans" [dir]. If that directory already exists, it will be deleted. Then a | 306 /// "Cleans" [dir]. If that directory already exists, it will be deleted. Then a |
310 /// new empty directory will be created. Returns a [Future] that completes when | 307 /// new empty directory will be created. Returns a [Future] that completes when |
311 /// the new clean directory is created. | 308 /// the new clean directory is created. |
312 Future<Directory> cleanDir(dir) { | 309 Future<Directory> cleanDir(dir) { |
313 return dirExists(dir).chain((exists) { | 310 return dirExists(dir).then((exists) { |
314 if (exists) { | 311 if (exists) { |
315 // Delete it first. | 312 // Delete it first. |
316 return deleteDir(dir).chain((_) => createDir(dir)); | 313 return deleteDir(dir).then((_) => createDir(dir)); |
317 } else { | 314 } else { |
318 // Just create it. | 315 // Just create it. |
319 return createDir(dir); | 316 return createDir(dir); |
320 } | 317 } |
321 }); | 318 }); |
322 } | 319 } |
323 | 320 |
324 /// Renames (i.e. moves) the directory [from] to [to]. Returns a [Future] with | 321 /// Renames (i.e. moves) the directory [from] to [to]. Returns a [Future] with |
325 /// the destination directory. | 322 /// the destination directory. |
326 Future<Directory> renameDir(from, String to) { | 323 Future<Directory> renameDir(from, String to) { |
327 from = _getDirectory(from); | 324 from = _getDirectory(from); |
328 log.io("Renaming directory ${from.path} to $to."); | 325 log.io("Renaming directory ${from.path} to $to."); |
329 | 326 |
330 return _attemptRetryable(() => from.rename(to)).transform((dir) { | 327 return _attemptRetryable(() => from.rename(to)).then((dir) { |
331 log.fine("Renamed directory ${from.path} to $to."); | 328 log.fine("Renamed directory ${from.path} to $to."); |
332 return dir; | 329 return dir; |
333 }); | 330 }); |
334 } | 331 } |
335 | 332 |
336 /// On Windows, we sometimes get failures where the directory is still in use | 333 /// On Windows, we sometimes get failures where the directory is still in use |
337 /// when we try to do something with it. This is usually because the OS hasn't | 334 /// when we try to do something with it. This is usually because the OS hasn't |
338 /// noticed yet that a process using that directory has closed. To be a bit | 335 /// noticed yet that a process using that directory has closed. To be a bit |
339 /// more resilient, we wait and retry a few times. | 336 /// more resilient, we wait and retry a few times. |
340 /// | 337 /// |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
378 if (Platform.operatingSystem == 'windows') { | 375 if (Platform.operatingSystem == 'windows') { |
379 // Call mklink on Windows to create an NTFS junction point. Only works on | 376 // Call mklink on Windows to create an NTFS junction point. Only works on |
380 // Vista or later. (Junction points are available earlier, but the "mklink" | 377 // Vista or later. (Junction points are available earlier, but the "mklink" |
381 // command is not.) I'm using a junction point (/j) here instead of a soft | 378 // command is not.) I'm using a junction point (/j) here instead of a soft |
382 // link (/d) because the latter requires some privilege shenanigans that | 379 // link (/d) because the latter requires some privilege shenanigans that |
383 // I'm not sure how to specify from the command line. | 380 // I'm not sure how to specify from the command line. |
384 command = 'mklink'; | 381 command = 'mklink'; |
385 args = ['/j', to, from]; | 382 args = ['/j', to, from]; |
386 } | 383 } |
387 | 384 |
388 return runProcess(command, args).transform((result) { | 385 return runProcess(command, args).then((result) { |
389 // TODO(rnystrom): Check exit code and output? | 386 // TODO(rnystrom): Check exit code and output? |
390 return new File(to); | 387 return new File(to); |
391 }); | 388 }); |
392 } | 389 } |
393 | 390 |
394 /// Creates a new symlink that creates an alias from the `lib` directory of | 391 /// Creates a new symlink that creates an alias from the `lib` directory of |
395 /// package [from] to [to], both of which can be a [String], [File], or | 392 /// package [from] to [to], both of which can be a [String], [File], or |
396 /// [Directory]. Returns a [Future] which completes to the symlink file (i.e. | 393 /// [Directory]. Returns a [Future] which completes to the symlink file (i.e. |
397 /// [to]). If [from] does not have a `lib` directory, this shows a warning if | 394 /// [to]). If [from] does not have a `lib` directory, this shows a warning if |
398 /// appropriate and then does nothing. | 395 /// appropriate and then does nothing. |
399 Future<File> createPackageSymlink(String name, from, to, | 396 Future<File> createPackageSymlink(String name, from, to, |
400 {bool isSelfLink: false}) { | 397 {bool isSelfLink: false}) { |
401 // See if the package has a "lib" directory. | 398 // See if the package has a "lib" directory. |
402 from = join(from, 'lib'); | 399 from = join(from, 'lib'); |
403 return dirExists(from).chain((exists) { | 400 return dirExists(from).then((exists) { |
404 log.fine("Creating ${isSelfLink ? "self" : ""}link for package '$name'."); | 401 log.fine("Creating ${isSelfLink ? "self" : ""}link for package '$name'."); |
405 if (exists) return createSymlink(from, to); | 402 if (exists) return createSymlink(from, to); |
406 | 403 |
407 // It's OK for the self link (i.e. the root package) to not have a lib | 404 // It's OK for the self link (i.e. the root package) to not have a lib |
408 // directory since it may just be a leaf application that only has | 405 // directory since it may just be a leaf application that only has |
409 // code in bin or web. | 406 // code in bin or web. |
410 if (!isSelfLink) { | 407 if (!isSelfLink) { |
411 log.warning('Warning: Package "$name" does not have a "lib" directory so ' | 408 log.warning('Warning: Package "$name" does not have a "lib" directory so ' |
412 'you will not be able to import any libraries from it.'); | 409 'you will not be able to import any libraries from it.'); |
413 } | 410 } |
(...skipping 161 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
575 /// Spawns and runs the process located at [executable], passing in [args]. | 572 /// Spawns and runs the process located at [executable], passing in [args]. |
576 /// Returns a [Future] that will complete with the results of the process after | 573 /// Returns a [Future] that will complete with the results of the process after |
577 /// it has ended. | 574 /// it has ended. |
578 /// | 575 /// |
579 /// The spawned process will inherit its parent's environment variables. If | 576 /// The spawned process will inherit its parent's environment variables. If |
580 /// [environment] is provided, that will be used to augment (not replace) the | 577 /// [environment] is provided, that will be used to augment (not replace) the |
581 /// the inherited variables. | 578 /// the inherited variables. |
582 Future<PubProcessResult> runProcess(String executable, List<String> args, | 579 Future<PubProcessResult> runProcess(String executable, List<String> args, |
583 {workingDir, Map<String, String> environment}) { | 580 {workingDir, Map<String, String> environment}) { |
584 return _doProcess(Process.run, executable, args, workingDir, environment) | 581 return _doProcess(Process.run, executable, args, workingDir, environment) |
585 .transform((result) { | 582 .then((result) { |
586 // TODO(rnystrom): Remove this and change to returning one string. | 583 // TODO(rnystrom): Remove this and change to returning one string. |
587 List<String> toLines(String output) { | 584 List<String> toLines(String output) { |
588 var lines = output.split(NEWLINE_PATTERN); | 585 var lines = output.split(NEWLINE_PATTERN); |
589 if (!lines.isEmpty && lines.last == "") lines.removeLast(); | 586 if (!lines.isEmpty && lines.last == "") lines.removeLast(); |
590 return lines; | 587 return lines; |
591 } | 588 } |
592 | 589 |
593 var pubResult = new PubProcessResult(toLines(result.stdout), | 590 var pubResult = new PubProcessResult(toLines(result.stdout), |
594 toLines(result.stderr), | 591 toLines(result.stderr), |
595 result.exitCode); | 592 result.exitCode); |
596 | 593 |
597 log.processResult(executable, pubResult); | 594 log.processResult(executable, pubResult); |
598 return pubResult; | 595 return pubResult; |
599 }); | 596 }); |
600 } | 597 } |
601 | 598 |
602 /// Spawns the process located at [executable], passing in [args]. Returns a | 599 /// Spawns the process located at [executable], passing in [args]. Returns a |
603 /// [Future] that will complete with the [Process] once it's been started. | 600 /// [Future] that will complete with the [Process] once it's been started. |
604 /// | 601 /// |
605 /// The spawned process will inherit its parent's environment variables. If | 602 /// The spawned process will inherit its parent's environment variables. If |
606 /// [environment] is provided, that will be used to augment (not replace) the | 603 /// [environment] is provided, that will be used to augment (not replace) the |
607 /// the inherited variables. | 604 /// the inherited variables. |
608 Future<Process> startProcess(String executable, List<String> args, | 605 Future<Process> startProcess(String executable, List<String> args, |
609 {workingDir, Map<String, String> environment}) => | 606 {workingDir, Map<String, String> environment}) => |
610 _doProcess(Process.start, executable, args, workingDir, environment) | 607 _doProcess(Process.start, executable, args, workingDir, environment) |
611 .transform((process) => new _WrappedProcess(process)); | 608 .then((process) => new _WrappedProcess(process)); |
612 | 609 |
613 /// A wrapper around [Process] that buffers the stdout and stderr to avoid | 610 /// A wrapper around [Process] that buffers the stdout and stderr to avoid |
614 /// running into issue 7218. | 611 /// running into issue 7218. |
615 class _WrappedProcess implements Process { | 612 class _WrappedProcess implements Process { |
616 final Process _process; | 613 final Process _process; |
617 final InputStream stderr; | 614 final InputStream stderr; |
618 final InputStream stdout; | 615 final InputStream stdout; |
619 | 616 |
620 OutputStream get stdin => _process.stdin; | 617 OutputStream get stdin => _process.stdin; |
621 | 618 |
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
664 | 661 |
665 /// Wraps [input] to provide a timeout. If [input] completes before | 662 /// Wraps [input] to provide a timeout. If [input] completes before |
666 /// [milliseconds] have passed, then the return value completes in the same way. | 663 /// [milliseconds] have passed, then the return value completes in the same way. |
667 /// However, if [milliseconds] pass before [input] has completed, it completes | 664 /// However, if [milliseconds] pass before [input] has completed, it completes |
668 /// with a [TimeoutException] with [description] (which should be a fragment | 665 /// with a [TimeoutException] with [description] (which should be a fragment |
669 /// describing the action that timed out). | 666 /// describing the action that timed out). |
670 /// | 667 /// |
671 /// Note that timing out will not cancel the asynchronous operation behind | 668 /// Note that timing out will not cancel the asynchronous operation behind |
672 /// [input]. | 669 /// [input]. |
673 Future timeout(Future input, int milliseconds, String description) { | 670 Future timeout(Future input, int milliseconds, String description) { |
| 671 bool completed = false; |
674 var completer = new Completer(); | 672 var completer = new Completer(); |
675 var timer = new Timer(milliseconds, (_) { | 673 var timer = new Timer(milliseconds, (_) { |
676 if (completer.future.isComplete) return; | 674 completer = true; |
677 completer.completeException(new TimeoutException( | 675 completer.completeError(new TimeoutException( |
678 'Timed out while $description.')); | 676 'Timed out while $description.')); |
679 }); | 677 }); |
680 input.handleException((e) { | 678 input |
681 if (completer.future.isComplete) return false; | 679 .then((value) { |
682 timer.cancel(); | 680 if (completed) return; |
683 completer.completeException(e, input.stackTrace); | 681 timer.cancel(); |
684 return true; | 682 completer.complete(value); |
685 }); | 683 }) |
686 input.then((value) { | 684 .catchError((e) { |
687 if (completer.future.isComplete) return; | 685 if (completed) return; |
688 timer.cancel(); | 686 timer.cancel(); |
689 completer.complete(value); | 687 completer.completeError(e.error, e.stackTrace); |
690 }); | 688 }); |
691 return completer.future; | 689 return completer.future; |
692 } | 690 } |
693 | 691 |
694 /// Creates a temporary directory and passes its path to [fn]. Once the [Future] | 692 /// Creates a temporary directory and passes its path to [fn]. Once the [Future] |
695 /// returned by [fn] completes, the temporary directory and all its contents | 693 /// returned by [fn] completes, the temporary directory and all its contents |
696 /// will be deleted. | 694 /// will be deleted. |
697 Future withTempDir(Future fn(String path)) { | 695 Future withTempDir(Future fn(String path)) { |
698 var tempDir; | 696 var tempDir; |
699 var future = createTempDir().chain((dir) { | 697 var future = createTempDir().then((dir) { |
700 tempDir = dir; | 698 tempDir = dir; |
701 return fn(tempDir.path); | 699 return fn(tempDir.path); |
702 }); | 700 }); |
703 future.onComplete((_) { | 701 future.catchError((_) {}).then(_) { |
704 log.fine('Cleaning up temp directory ${tempDir.path}.'); | 702 log.fine('Cleaning up temp directory ${tempDir.path}.'); |
705 deleteDir(tempDir); | 703 deleteDir(tempDir); |
706 }); | 704 }); |
707 return future; | 705 return future; |
708 } | 706 } |
709 | 707 |
710 /// Tests whether or not the git command-line app is available for use. | 708 /// Tests whether or not the git command-line app is available for use. |
711 Future<bool> get isGitInstalled { | 709 Future<bool> get isGitInstalled { |
712 if (_isGitInstalledCache != null) { | 710 if (_isGitInstalledCache != null) { |
713 // TODO(rnystrom): The sleep is to pump the message queue. Can use | 711 // TODO(rnystrom): The sleep is to pump the message queue. Can use |
714 // Future.immediate() when #3356 is fixed. | 712 // Future.immediate() when #3356 is fixed. |
715 return sleep(0).transform((_) => _isGitInstalledCache); | 713 return sleep(0).then((_) => _isGitInstalledCache); |
716 } | 714 } |
717 | 715 |
718 return _gitCommand.transform((git) => git != null); | 716 return _gitCommand.then((git) => git != null); |
719 } | 717 } |
720 | 718 |
721 /// Run a git process with [args] from [workingDir]. | 719 /// Run a git process with [args] from [workingDir]. |
722 Future<PubProcessResult> runGit(List<String> args, | 720 Future<PubProcessResult> runGit(List<String> args, |
723 {String workingDir, Map<String, String> environment}) { | 721 {String workingDir, Map<String, String> environment}) { |
724 return _gitCommand.chain((git) => runProcess(git, args, | 722 return _gitCommand.then((git) => runProcess(git, args, |
725 workingDir: workingDir, environment: environment)); | 723 workingDir: workingDir, environment: environment)); |
726 } | 724 } |
727 | 725 |
728 /// Returns the name of the git command-line app, or null if Git could not be | 726 /// Returns the name of the git command-line app, or null if Git could not be |
729 /// found on the user's PATH. | 727 /// found on the user's PATH. |
730 Future<String> get _gitCommand { | 728 Future<String> get _gitCommand { |
731 // TODO(nweiz): Just use Future.immediate once issue 3356 is fixed. | 729 // TODO(nweiz): Just use Future.immediate once issue 3356 is fixed. |
732 if (_gitCommandCache != null) { | 730 if (_gitCommandCache != null) { |
733 return sleep(0).transform((_) => _gitCommandCache); | 731 return sleep(0).then((_) => _gitCommandCache); |
734 } | 732 } |
735 | 733 |
736 return _tryGitCommand("git").chain((success) { | 734 return _tryGitCommand("git").then((success) { |
737 if (success) return new Future.immediate("git"); | 735 if (success) return new Future.immediate("git"); |
738 | 736 |
739 // Git is sometimes installed on Windows as `git.cmd` | 737 // Git is sometimes installed on Windows as `git.cmd` |
740 return _tryGitCommand("git.cmd").transform((success) { | 738 return _tryGitCommand("git.cmd").then((success) { |
741 if (success) return "git.cmd"; | 739 if (success) return "git.cmd"; |
742 return null; | 740 return null; |
743 }); | 741 }); |
744 }).transform((command) { | 742 }).then((command) { |
745 _gitCommandCache = command; | 743 _gitCommandCache = command; |
746 return command; | 744 return command; |
747 }); | 745 }); |
748 } | 746 } |
749 | 747 |
750 /// Checks whether [command] is the Git command for this computer. | 748 /// Checks whether [command] is the Git command for this computer. |
751 Future<bool> _tryGitCommand(String command) { | 749 Future<bool> _tryGitCommand(String command) { |
752 var completer = new Completer<bool>(); | 750 var completer = new Completer<bool>(); |
753 | 751 |
754 // If "git --version" prints something familiar, git is working. | 752 // If "git --version" prints something familiar, git is working. |
755 var future = runProcess(command, ["--version"]); | 753 var future = runProcess(command, ["--version"]); |
756 | 754 |
757 future.then((results) { | 755 future.then((results) { |
758 var regex = new RegExp("^git version"); | 756 var regex = new RegExp("^git version"); |
759 completer.complete(results.stdout.length == 1 && | 757 completer.complete(results.stdout.length == 1 && |
760 regex.hasMatch(results.stdout[0])); | 758 regex.hasMatch(results.stdout[0])); |
761 }); | 759 }).catchError((err) { |
762 | |
763 future.handleException((err) { | |
764 // If the process failed, they probably don't have it. | 760 // If the process failed, they probably don't have it. |
765 completer.complete(false); | 761 completer.complete(false); |
766 return true; | |
767 }); | 762 }); |
768 | 763 |
769 return completer.future; | 764 return completer.future; |
770 } | 765 } |
771 | 766 |
772 /// Extracts a `.tar.gz` file from [stream] to [destination], which can be a | 767 /// Extracts a `.tar.gz` file from [stream] to [destination], which can be a |
773 /// directory or a path. Returns whether or not the extraction was successful. | 768 /// directory or a path. Returns whether or not the extraction was successful. |
774 Future<bool> extractTarGz(InputStream stream, destination) { | 769 Future<bool> extractTarGz(InputStream stream, destination) { |
775 destination = _getPath(destination); | 770 destination = _getPath(destination); |
776 | 771 |
777 log.fine("Extracting .tar.gz stream to $destination."); | 772 log.fine("Extracting .tar.gz stream to $destination."); |
778 | 773 |
779 if (Platform.operatingSystem == "windows") { | 774 if (Platform.operatingSystem == "windows") { |
780 return _extractTarGzWindows(stream, destination); | 775 return _extractTarGzWindows(stream, destination); |
781 } | 776 } |
782 | 777 |
783 var completer = new Completer<int>(); | 778 var completer = new Completer<int>(); |
784 var processFuture = startProcess("tar", | 779 var processFuture = startProcess("tar", |
785 ["--extract", "--gunzip", "--directory", destination]); | 780 ["--extract", "--gunzip", "--directory", destination]); |
786 processFuture.then((process) { | 781 processFuture.then((process) { |
787 process.onExit = completer.complete; | 782 process.onExit = completer.complete; |
788 stream.pipe(process.stdin); | 783 stream.pipe(process.stdin); |
789 process.stdout.pipe(stdout, close: false); | 784 process.stdout.pipe(stdout, close: false); |
790 process.stderr.pipe(stderr, close: false); | 785 process.stderr.pipe(stderr, close: false); |
791 }); | 786 }).catchError((e) { |
792 processFuture.handleException((error) { | 787 completer.completeError(e.error, e.stackTrace); |
793 completer.completeException(error, processFuture.stackTrace); | |
794 return true; | |
795 }); | 788 }); |
796 | 789 |
797 return completer.future.transform((exitCode) { | 790 return completer.future.then((exitCode) { |
798 log.fine("Extracted .tar.gz stream to $destination. Exit code $exitCode."); | 791 log.fine("Extracted .tar.gz stream to $destination. Exit code $exitCode."); |
799 // TODO(rnystrom): Does anything check this result value? If not, it should | 792 // TODO(rnystrom): Does anything check this result value? If not, it should |
800 // throw on a bad exit code. | 793 // throw on a bad exit code. |
801 return exitCode == 0; | 794 return exitCode == 0; |
802 }); | 795 }); |
803 } | 796 } |
804 | 797 |
805 Future<bool> _extractTarGzWindows(InputStream stream, String destination) { | 798 Future<bool> _extractTarGzWindows(InputStream stream, String destination) { |
806 // TODO(rnystrom): In the repo's history, there is an older implementation of | 799 // TODO(rnystrom): In the repo's history, there is an older implementation of |
807 // this that does everything in memory by piping streams directly together | 800 // this that does everything in memory by piping streams directly together |
808 // instead of writing out temp files. The code is simpler, but unfortunately, | 801 // instead of writing out temp files. The code is simpler, but unfortunately, |
809 // 7zip seems to periodically fail when we invoke it from Dart and tell it to | 802 // 7zip seems to periodically fail when we invoke it from Dart and tell it to |
810 // read from stdin instead of a file. Consider resurrecting that version if | 803 // read from stdin instead of a file. Consider resurrecting that version if |
811 // we can figure out why it fails. | 804 // we can figure out why it fails. |
812 | 805 |
813 // Note: This line of code gets munged by create_sdk.py to be the correct | 806 // Note: This line of code gets munged by create_sdk.py to be the correct |
814 // relative path to 7zip in the SDK. | 807 // relative path to 7zip in the SDK. |
815 var pathTo7zip = '../../third_party/7zip/7za.exe'; | 808 var pathTo7zip = '../../third_party/7zip/7za.exe'; |
816 var command = relativeToPub(pathTo7zip); | 809 var command = relativeToPub(pathTo7zip); |
817 | 810 |
818 var tempDir; | 811 var tempDir; |
819 | 812 |
820 // TODO(rnystrom): Use withTempDir(). | 813 // TODO(rnystrom): Use withTempDir(). |
821 return createTempDir().chain((temp) { | 814 return createTempDir().then((temp) { |
822 // Write the archive to a temp file. | 815 // Write the archive to a temp file. |
823 tempDir = temp; | 816 tempDir = temp; |
824 return createFileFromStream(stream, join(tempDir, 'data.tar.gz')); | 817 return createFileFromStream(stream, join(tempDir, 'data.tar.gz')); |
825 }).chain((_) { | 818 }).then((_) { |
826 // 7zip can't unarchive from gzip -> tar -> destination all in one step | 819 // 7zip can't unarchive from gzip -> tar -> destination all in one step |
827 // first we un-gzip it to a tar file. | 820 // first we un-gzip it to a tar file. |
828 // Note: Setting the working directory instead of passing in a full file | 821 // Note: Setting the working directory instead of passing in a full file |
829 // path because 7zip says "A full path is not allowed here." | 822 // path because 7zip says "A full path is not allowed here." |
830 return runProcess(command, ['e', 'data.tar.gz'], workingDir: tempDir); | 823 return runProcess(command, ['e', 'data.tar.gz'], workingDir: tempDir); |
831 }).chain((result) { | 824 }).then((result) { |
832 if (result.exitCode != 0) { | 825 if (result.exitCode != 0) { |
833 throw 'Could not un-gzip (exit code ${result.exitCode}). Error:\n' | 826 throw 'Could not un-gzip (exit code ${result.exitCode}). Error:\n' |
834 '${Strings.join(result.stdout, "\n")}\n' | 827 '${Strings.join(result.stdout, "\n")}\n' |
835 '${Strings.join(result.stderr, "\n")}'; | 828 '${Strings.join(result.stderr, "\n")}'; |
836 } | 829 } |
837 // Find the tar file we just created since we don't know its name. | 830 // Find the tar file we just created since we don't know its name. |
838 return listDir(tempDir); | 831 return listDir(tempDir); |
839 }).chain((files) { | 832 }).then((files) { |
840 var tarFile; | 833 var tarFile; |
841 for (var file in files) { | 834 for (var file in files) { |
842 if (path.extension(file) == '.tar') { | 835 if (path.extension(file) == '.tar') { |
843 tarFile = file; | 836 tarFile = file; |
844 break; | 837 break; |
845 } | 838 } |
846 } | 839 } |
847 | 840 |
848 if (tarFile == null) throw 'The gzip file did not contain a tar file.'; | 841 if (tarFile == null) throw 'The gzip file did not contain a tar file.'; |
849 | 842 |
850 // Untar the archive into the destination directory. | 843 // Untar the archive into the destination directory. |
851 return runProcess(command, ['x', tarFile], workingDir: destination); | 844 return runProcess(command, ['x', tarFile], workingDir: destination); |
852 }).chain((result) { | 845 }).then((result) { |
853 if (result.exitCode != 0) { | 846 if (result.exitCode != 0) { |
854 throw 'Could not un-tar (exit code ${result.exitCode}). Error:\n' | 847 throw 'Could not un-tar (exit code ${result.exitCode}). Error:\n' |
855 '${Strings.join(result.stdout, "\n")}\n' | 848 '${Strings.join(result.stdout, "\n")}\n' |
856 '${Strings.join(result.stderr, "\n")}'; | 849 '${Strings.join(result.stderr, "\n")}'; |
857 } | 850 } |
858 | 851 |
859 log.fine('Clean up 7zip temp directory ${tempDir.path}.'); | 852 log.fine('Clean up 7zip temp directory ${tempDir.path}.'); |
860 // TODO(rnystrom): Should also delete this if anything fails. | 853 // TODO(rnystrom): Should also delete this if anything fails. |
861 return deleteDir(tempDir); | 854 return deleteDir(tempDir); |
862 }).transform((_) => true); | 855 }).then((_) => true); |
863 } | 856 } |
864 | 857 |
865 /// Create a .tar.gz archive from a list of entries. Each entry can be a | 858 /// Create a .tar.gz archive from a list of entries. Each entry can be a |
866 /// [String], [Directory], or [File] object. The root of the archive is | 859 /// [String], [Directory], or [File] object. The root of the archive is |
867 /// considered to be [baseDir], which defaults to the current working directory. | 860 /// considered to be [baseDir], which defaults to the current working directory. |
868 /// Returns an [InputStream] that will emit the contents of the archive. | 861 /// Returns an [InputStream] that will emit the contents of the archive. |
869 InputStream createTarGz(List contents, {baseDir}) { | 862 InputStream createTarGz(List contents, {baseDir}) { |
870 var buffer = new StringBuffer(); | 863 var buffer = new StringBuffer(); |
871 buffer.add('Creating .tag.gz stream containing:\n'); | 864 buffer.add('Creating .tag.gz stream containing:\n'); |
872 contents.forEach((file) => buffer.add('$file\n')); | 865 contents.forEach((file) => buffer.add('$file\n')); |
873 log.fine(buffer.toString()); | 866 log.fine(buffer.toString()); |
874 | 867 |
875 // TODO(nweiz): Propagate errors to the returned stream (including non-zero | 868 // TODO(nweiz): Propagate errors to the returned stream (including non-zero |
876 // exit codes). See issue 3657. | 869 // exit codes). See issue 3657. |
877 var stream = new ListInputStream(); | 870 var stream = new ListInputStream(); |
878 | 871 |
879 if (baseDir == null) baseDir = path.current; | 872 if (baseDir == null) baseDir = path.current; |
880 baseDir = getFullPath(baseDir); | 873 baseDir = getFullPath(baseDir); |
881 contents = contents.map((entry) { | 874 contents = contents.mappedBy((entry) { |
882 entry = getFullPath(entry); | 875 entry = getFullPath(entry); |
883 if (!isBeneath(entry, baseDir)) { | 876 if (!isBeneath(entry, baseDir)) { |
884 throw 'Entry $entry is not inside $baseDir.'; | 877 throw 'Entry $entry is not inside $baseDir.'; |
885 } | 878 } |
886 return relativeTo(entry, baseDir); | 879 return relativeTo(entry, baseDir); |
887 }); | 880 }).toList(); |
888 | 881 |
889 if (Platform.operatingSystem != "windows") { | 882 if (Platform.operatingSystem != "windows") { |
890 var args = ["--create", "--gzip", "--directory", baseDir]; | 883 var args = ["--create", "--gzip", "--directory", baseDir]; |
891 args.addAll(contents.map(_getPath)); | 884 args.addAll(contents.mappedBy(_getPath)); |
892 // TODO(nweiz): It's possible that enough command-line arguments will make | 885 // TODO(nweiz): It's possible that enough command-line arguments will make |
893 // the process choke, so at some point we should save the arguments to a | 886 // the process choke, so at some point we should save the arguments to a |
894 // file and pass them in via --files-from for tar and -i@filename for 7zip. | 887 // file and pass them in via --files-from for tar and -i@filename for 7zip. |
895 startProcess("tar", args).then((process) { | 888 startProcess("tar", args).then((process) { |
896 pipeInputToInput(process.stdout, stream); | 889 pipeInputToInput(process.stdout, stream); |
897 | 890 |
898 // Drain and discard 7zip's stderr. 7zip writes its normal output to | 891 // Drain and discard 7zip's stderr. 7zip writes its normal output to |
899 // stderr. We don't want to show that since it's meaningless. | 892 // stderr. We don't want to show that since it's meaningless. |
900 // TODO(rnystrom): Should log this and display it if an actual error | 893 // TODO(rnystrom): Should log this and display it if an actual error |
901 // occurs. | 894 // occurs. |
902 consumeInputStream(process.stderr); | 895 consumeInputStream(process.stderr); |
903 }); | 896 }); |
904 return stream; | 897 return stream; |
905 } | 898 } |
906 | 899 |
907 withTempDir((tempDir) { | 900 withTempDir((tempDir) { |
908 // Create the tar file. | 901 // Create the tar file. |
909 var tarFile = join(tempDir, "intermediate.tar"); | 902 var tarFile = join(tempDir, "intermediate.tar"); |
910 var args = ["a", "-w$baseDir", tarFile]; | 903 var args = ["a", "-w$baseDir", tarFile]; |
911 args.addAll(contents.map((entry) => '-i!"$entry"')); | 904 args.addAll(contents.mappedBy((entry) => '-i!"$entry"')); |
912 | 905 |
913 // Note: This line of code gets munged by create_sdk.py to be the correct | 906 // Note: This line of code gets munged by create_sdk.py to be the correct |
914 // relative path to 7zip in the SDK. | 907 // relative path to 7zip in the SDK. |
915 var pathTo7zip = '../../third_party/7zip/7za.exe'; | 908 var pathTo7zip = '../../third_party/7zip/7za.exe'; |
916 var command = relativeToPub(pathTo7zip); | 909 var command = relativeToPub(pathTo7zip); |
917 | 910 |
918 // We're passing 'baseDir' both as '-w' and setting it as the working | 911 // We're passing 'baseDir' both as '-w' and setting it as the working |
919 // directory explicitly here intentionally. The former ensures that the | 912 // directory explicitly here intentionally. The former ensures that the |
920 // files added to the archive have the correct relative path in the archive. | 913 // files added to the archive have the correct relative path in the archive. |
921 // The latter enables relative paths in the "-i" args to be resolved. | 914 // The latter enables relative paths in the "-i" args to be resolved. |
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
971 Directory _getDirectory(entry) { | 964 Directory _getDirectory(entry) { |
972 if (entry is Directory) return entry; | 965 if (entry is Directory) return entry; |
973 return new Directory(entry); | 966 return new Directory(entry); |
974 } | 967 } |
975 | 968 |
976 /// Gets a [Uri] for [uri], which can either already be one, or be a [String]. | 969 /// Gets a [Uri] for [uri], which can either already be one, or be a [String]. |
977 Uri _getUri(uri) { | 970 Uri _getUri(uri) { |
978 if (uri is Uri) return uri; | 971 if (uri is Uri) return uri; |
979 return new Uri.fromString(uri); | 972 return new Uri.fromString(uri); |
980 } | 973 } |
OLD | NEW |