| 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 |