| // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| /** |
| * Helper functionality to make working with IO easier. |
| */ |
| library io; |
| |
| import 'dart:io'; |
| import 'dart:isolate'; |
| import 'dart:uri'; |
| |
| // TODO(nweiz): Make this import better. |
| import '../../pkg/http/lib/http.dart' as http; |
| import 'utils.dart'; |
| import 'curl_client.dart'; |
| |
| bool _isGitInstalledCache; |
| |
| /// The cached Git command. |
| String _gitCommandCache; |
| |
| /** Gets the current working directory. */ |
| String get currentWorkingDir => new File('.').fullPathSync(); |
| |
| final NEWLINE_PATTERN = new RegExp("\r\n?|\n\r?"); |
| |
| /** |
| * Prints the given string to `stderr` on its own line. |
| */ |
| void printError(value) { |
| stderr.writeString(value.toString()); |
| stderr.writeString('\n'); |
| } |
| |
| |
| /** |
| * Joins a number of path string parts into a single path. Handles |
| * platform-specific path separators. Parts can be [String], [Directory], or |
| * [File] objects. |
| */ |
| String join(part1, [part2, part3, part4]) { |
| final parts = _sanitizePath(part1).split('/'); |
| |
| for (final part in [part2, part3, part4]) { |
| if (part == null) continue; |
| |
| for (final piece in _getPath(part).split('/')) { |
| if (piece == '..' && parts.length > 0 && |
| parts.last != '.' && parts.last != '..') { |
| parts.removeLast(); |
| } else if (piece != '') { |
| if (parts.length > 0 && parts.last == '.') { |
| parts.removeLast(); |
| } |
| parts.add(piece); |
| } |
| } |
| } |
| |
| return Strings.join(parts, Platform.pathSeparator); |
| } |
| |
| /** |
| * Gets the basename, the file name without any leading directory path, for |
| * [file], which can either be a [String], [File], or [Directory]. |
| */ |
| // TODO(rnystrom): Copied from file_system (so that we don't have to add |
| // file_system to the SDK). Should unify. |
| String basename(file) { |
| file = _sanitizePath(file); |
| |
| int lastSlash = file.lastIndexOf('/', file.length); |
| if (lastSlash == -1) { |
| return file; |
| } else { |
| return file.substring(lastSlash + 1); |
| } |
| } |
| |
| /** |
| * Gets the the leading directory path for [file], which can either be a |
| * [String], [File], or [Directory]. |
| */ |
| // TODO(nweiz): Copied from file_system (so that we don't have to add |
| // file_system to the SDK). Should unify. |
| String dirname(file) { |
| file = _sanitizePath(file); |
| |
| int lastSlash = file.lastIndexOf('/', file.length); |
| if (lastSlash == -1) { |
| return '.'; |
| } else { |
| return file.substring(0, lastSlash); |
| } |
| } |
| |
| /// Returns whether or not [entry] is nested somewhere within [dir]. This just |
| /// performs a path comparison; it doesn't look at the actual filesystem. |
| bool isBeneath(entry, dir) => |
| _sanitizePath(entry).startsWith('${_sanitizePath(dir)}/'); |
| |
| /** |
| * Asynchronously determines if [path], which can be a [String] file path, a |
| * [File], or a [Directory] exists on the file system. Returns a [Future] that |
| * completes with the result. |
| */ |
| Future<bool> exists(path) { |
| path = _getPath(path); |
| return Futures.wait([fileExists(path), dirExists(path)]).transform((results) { |
| return results[0] || results[1]; |
| }); |
| } |
| |
| /** |
| * Asynchronously determines if [file], which can be a [String] file path or a |
| * [File], exists on the file system. Returns a [Future] that completes with |
| * the result. |
| */ |
| Future<bool> fileExists(file) { |
| return new File(_getPath(file)).exists(); |
| } |
| |
| /** |
| * Reads the contents of the text file [file], which can either be a [String] or |
| * a [File]. |
| */ |
| Future<String> readTextFile(file) { |
| return new File(_getPath(file)).readAsString(Encoding.UTF_8); |
| } |
| |
| /** |
| * Creates [file] (which can either be a [String] or a [File]), and writes |
| * [contents] to it. Completes when the file is written and closed. |
| */ |
| Future<File> writeTextFile(file, String contents) { |
| file = new File(_getPath(file)); |
| return file.open(FileMode.WRITE).chain((opened) { |
| return opened.writeString(contents).chain((ignore) { |
| return opened.close().transform((ignore) => file); |
| }); |
| }); |
| } |
| |
| /** |
| * Asynchronously deletes [file], which can be a [String] or a [File]. Returns a |
| * [Future] that completes when the deletion is done. |
| */ |
| Future<File> deleteFile(file) { |
| return new File(_getPath(file)).delete(); |
| } |
| |
| /// Writes [stream] to a new file at [path], which may be a [String] or a |
| /// [File]. Will replace any file already at that path. Completes when the file |
| /// is done being written. |
| Future<File> createFileFromStream(InputStream stream, path) { |
| path = _getPath(path); |
| |
| var completer = new Completer<File>(); |
| var file = new File(path); |
| var outputStream = file.openOutputStream(); |
| stream.pipe(outputStream); |
| |
| outputStream.onClosed = () { |
| completer.complete(file); |
| }; |
| |
| // TODO(nweiz): remove this when issue 4061 is fixed. |
| var stackTrace; |
| try { |
| throw ""; |
| } catch (_, localStackTrace) { |
| stackTrace = localStackTrace; |
| } |
| |
| completeError(error) { |
| if (!completer.isComplete) completer.completeException(error, stackTrace); |
| } |
| |
| stream.onError = completeError; |
| outputStream.onError = completeError; |
| |
| return completer.future; |
| } |
| |
| /** |
| * Creates a directory [dir]. Returns a [Future] that completes when the |
| * directory is created. |
| */ |
| Future<Directory> createDir(dir) { |
| dir = _getDirectory(dir); |
| return dir.create(); |
| } |
| |
| /** |
| * Ensures that [path] and all its parent directories exist. If they don't |
| * exist, creates them. Returns a [Future] that completes once all the |
| * directories are created. |
| */ |
| Future<Directory> ensureDir(path) { |
| path = _getPath(path); |
| if (path == '.') return new Future.immediate(new Directory('.')); |
| |
| return dirExists(path).chain((exists) { |
| if (exists) return new Future.immediate(new Directory(path)); |
| return ensureDir(dirname(path)).chain((_) { |
| var completer = new Completer<Directory>(); |
| var future = createDir(path); |
| future.handleException((error) { |
| if (error is! DirectoryIOException) return false; |
| // Error 17 means the directory already exists (or 183 on Windows). |
| if (error.osError.errorCode != 17 && |
| error.osError.errorCode != 183) return false; |
| |
| completer.complete(_getDirectory(path)); |
| return true; |
| }); |
| future.then(completer.complete); |
| return completer.future; |
| }); |
| }); |
| } |
| |
| /** |
| * Creates a temp directory whose name will be based on [dir] with a unique |
| * suffix appended to it. If [dir] is not provided, a temp directory will be |
| * created in a platform-dependent temporary location. Returns a [Future] that |
| * completes when the directory is created. |
| */ |
| Future<Directory> createTempDir([dir = '']) { |
| dir = _getDirectory(dir); |
| return dir.createTemp(); |
| } |
| |
| /** |
| * Asynchronously recursively deletes [dir], which can be a [String] or a |
| * [Directory]. Returns a [Future] that completes when the deletion is done. |
| */ |
| Future<Directory> deleteDir(dir) { |
| dir = _getDirectory(dir); |
| return dir.delete(recursive: true); |
| } |
| |
| /** |
| * Asynchronously lists the contents of [dir], which can be a [String] directory |
| * path or a [Directory]. If [recursive] is `true`, lists subdirectory contents |
| * (defaults to `false`). If [includeHiddenFiles] is `true`, includes files |
| * beginning with `.` (defaults to `false`). |
| */ |
| Future<List<String>> listDir(dir, |
| {bool recursive: false, bool includeHiddenFiles: false}) { |
| final completer = new Completer<List<String>>(); |
| final contents = <String>[]; |
| |
| dir = _getDirectory(dir); |
| var lister = dir.list(recursive: recursive); |
| |
| lister.onDone = (done) { |
| // TODO(rnystrom): May need to sort here if it turns out onDir and onFile |
| // aren't guaranteed to be called in a certain order. So far, they seem to. |
| if (done) completer.complete(contents); |
| }; |
| |
| // TODO(nweiz): remove this when issue 4061 is fixed. |
| var stackTrace; |
| try { |
| throw ""; |
| } catch (_, localStackTrace) { |
| stackTrace = localStackTrace; |
| } |
| |
| lister.onError = (error) => completer.completeException(error, stackTrace); |
| lister.onDir = (file) => contents.add(file); |
| lister.onFile = (file) { |
| if (!includeHiddenFiles && basename(file).startsWith('.')) return; |
| contents.add(file); |
| }; |
| |
| return completer.future; |
| } |
| |
| /** |
| * Asynchronously determines if [dir], which can be a [String] directory path |
| * or a [Directory], exists on the file system. Returns a [Future] that |
| * completes with the result. |
| */ |
| Future<bool> dirExists(dir) { |
| dir = _getDirectory(dir); |
| return dir.exists(); |
| } |
| |
| /** |
| * "Cleans" [dir]. If that directory already exists, it will be deleted. Then a |
| * new empty directory will be created. Returns a [Future] that completes when |
| * the new clean directory is created. |
| */ |
| Future<Directory> cleanDir(dir) { |
| return dirExists(dir).chain((exists) { |
| if (exists) { |
| // Delete it first. |
| return deleteDir(dir).chain((_) => createDir(dir)); |
| } else { |
| // Just create it. |
| return createDir(dir); |
| } |
| }); |
| } |
| |
| /// Renames (i.e. moves) the directory [from] to [to]. Returns a [Future] with |
| /// the destination directory. |
| Future<Directory> renameDir(from, String to) =>_getDirectory(from).rename(to); |
| |
| /** |
| * Creates a new symlink that creates an alias from [from] to [to], both of |
| * which can be a [String], [File], or [Directory]. Returns a [Future] which |
| * completes to the symlink file (i.e. [to]). |
| */ |
| Future<File> createSymlink(from, to) { |
| from = _getPath(from); |
| to = _getPath(to); |
| |
| var command = 'ln'; |
| var args = ['-s', from, to]; |
| |
| if (Platform.operatingSystem == 'windows') { |
| // Call mklink on Windows to create an NTFS junction point. Only works on |
| // Vista or later. (Junction points are available earlier, but the "mklink" |
| // command is not.) I'm using a junction point (/j) here instead of a soft |
| // link (/d) because the latter requires some privilege shenanigans that |
| // I'm not sure how to specify from the command line. |
| command = 'mklink'; |
| args = ['/j', to, from]; |
| } |
| |
| return runProcess(command, args).transform((result) { |
| // TODO(rnystrom): Check exit code and output? |
| return new File(to); |
| }); |
| } |
| |
| /** |
| * Creates a new symlink that creates an alias from the `lib` directory of |
| * package [from] to [to], both of which can be a [String], [File], or |
| * [Directory]. Returns a [Future] which completes to the symlink file (i.e. |
| * [to]). If [from] does not have a `lib` directory, this shows a warning if |
| * appropriate and then does nothing. |
| */ |
| Future<File> createPackageSymlink(String name, from, to, |
| {bool isSelfLink: false}) { |
| // See if the package has a "lib" directory. |
| from = join(from, 'lib'); |
| return dirExists(from).chain((exists) { |
| if (exists) return createSymlink(from, to); |
| |
| // It's OK for the self link (i.e. the root package) to not have a lib |
| // directory since it may just be a leaf application that only has |
| // code in bin or web. |
| if (!isSelfLink) { |
| printError( |
| 'Warning: Package "$name" does not have a "lib" directory so you ' |
| 'will not be able to import any libraries from it.'); |
| } |
| |
| return new Future.immediate(to); |
| }); |
| } |
| |
| /// Given [entry] which may be a [String], [File], or [Directory] relative to |
| /// the current working directory, returns its full canonicalized path. |
| String getFullPath(entry) { |
| var path = _getPath(entry); |
| |
| // Don't do anything if it's already absolute. |
| if (Platform.operatingSystem == 'windows') { |
| // An absolute path on Windows is either UNC (two leading backslashes), |
| // or a drive letter followed by a colon and a slash. |
| var ABSOLUTE = new RegExp(r'^(\\\\|[a-zA-Z]:[/\\])'); |
| if (ABSOLUTE.hasMatch(path)) return path; |
| } else { |
| if (path.startsWith('/')) return path; |
| } |
| |
| // Using Path.join here instead of File().fullPathSync() because the former |
| // does not require an actual file to exist at that path. |
| return new Path.fromNative(currentWorkingDir).join(new Path(path)) |
| .toNativePath(); |
| } |
| |
| /// Resolves [path] relative to the location of pub.dart. |
| String relativeToPub(String path) { |
| var scriptPath = new File(new Options().script).fullPathSync(); |
| |
| // Walk up until we hit the "util(s)" directory. This lets us figure out where |
| // we are if this function is called from pub.dart, or one of the tests, |
| // which also live under "utils", or from the SDK where pub is in "util". |
| var utilDir = new Path.fromNative(scriptPath).directoryPath; |
| while (utilDir.filename != 'utils' && utilDir.filename != 'util') { |
| if (utilDir.filename == '') throw 'Could not find path to pub.'; |
| utilDir = utilDir.directoryPath; |
| } |
| |
| return utilDir.append('pub').append(path).canonicalize().toNativePath(); |
| } |
| |
| /// A StringInputStream reading from stdin. |
| final _stringStdin = new StringInputStream(stdin); |
| |
| /// Returns a single line read from a [StringInputStream]. By default, reads |
| /// from stdin. |
| /// |
| /// A [StringInputStream] passed to this should have no callbacks registered. |
| Future<String> readLine([StringInputStream stream]) { |
| if (stream == null) stream = _stringStdin; |
| if (stream.closed) return new Future.immediate(''); |
| void removeCallbacks() { |
| stream.onClosed = null; |
| stream.onLine = null; |
| stream.onError = null; |
| } |
| |
| // TODO(nweiz): remove this when issue 4061 is fixed. |
| var stackTrace; |
| try { |
| throw ""; |
| } catch (_, localStackTrace) { |
| stackTrace = localStackTrace; |
| } |
| |
| var completer = new Completer(); |
| stream.onClosed = () { |
| removeCallbacks(); |
| completer.complete(''); |
| }; |
| |
| stream.onLine = () { |
| removeCallbacks(); |
| completer.complete(stream.readLine()); |
| }; |
| |
| stream.onError = (e) { |
| removeCallbacks(); |
| completer.completeException(e, stackTrace); |
| }; |
| |
| return completer.future; |
| } |
| |
| // TODO(nweiz): make this configurable |
| /** |
| * The amount of time in milliseconds to allow HTTP requests before assuming |
| * they've failed. |
| */ |
| final HTTP_TIMEOUT = 30 * 1000; |
| |
| /// An HTTP client that transforms 40* errors and socket exceptions into more |
| /// user-friendly error messages. |
| class PubHttpClient extends http.BaseClient { |
| final http.Client _inner; |
| |
| PubHttpClient([http.Client inner]) |
| : _inner = inner == null ? new http.Client() : inner; |
| |
| Future<http.StreamedResponse> send(http.BaseRequest request) { |
| // TODO(nweiz): remove this when issue 4061 is fixed. |
| var stackTrace; |
| try { |
| throw null; |
| } catch (_, localStackTrace) { |
| stackTrace = localStackTrace; |
| } |
| |
| // TODO(nweiz): Ideally the timeout would extend to reading from the |
| // response input stream, but until issue 3657 is fixed that's not feasible. |
| return timeout(_inner.send(request).chain((streamedResponse) { |
| if (streamedResponse.statusCode < 400) { |
| return new Future.immediate(streamedResponse); |
| } |
| |
| return http.Response.fromStream(streamedResponse).transform((response) { |
| throw new PubHttpException(response); |
| }); |
| }).transformException((e) { |
| if (e is SocketIOException && |
| e.osError != null && |
| (e.osError.errorCode == 8 || |
| e.osError.errorCode == -2 || |
| e.osError.errorCode == -5 || |
| e.osError.errorCode == 11004)) { |
| throw 'Could not resolve URL "${request.url.origin}".'; |
| } |
| throw e; |
| }), HTTP_TIMEOUT, 'fetching URL "${request.url}"'); |
| } |
| } |
| |
| /// The HTTP client to use for all HTTP requests. |
| final httpClient = new PubHttpClient(); |
| |
| final curlClient = new PubHttpClient(new CurlClient()); |
| |
| /** |
| * Takes all input from [source] and writes it to [sink]. |
| * |
| * Returns a future that completes when [source] is closed. |
| */ |
| Future pipeInputToInput(InputStream source, ListInputStream sink) { |
| var completer = new Completer(); |
| source.onClosed = () { |
| sink.markEndOfStream(); |
| completer.complete(null); |
| }; |
| source.onData = () => sink.write(source.read()); |
| // TODO(nweiz): propagate this error to the sink. See issue 3657. |
| source.onError = (e) { throw e; }; |
| return completer.future; |
| } |
| |
| /** |
| * Buffers all input from an InputStream and returns it as a future. |
| */ |
| Future<List<int>> consumeInputStream(InputStream stream) { |
| if (stream.closed) return new Future.immediate(<int>[]); |
| |
| // TODO(nweiz): remove this when issue 4061 is fixed. |
| var stackTrace; |
| try { |
| throw ""; |
| } catch (_, localStackTrace) { |
| stackTrace = localStackTrace; |
| } |
| |
| var completer = new Completer<List<int>>(); |
| var buffer = <int>[]; |
| stream.onClosed = () => completer.complete(buffer); |
| stream.onData = () => buffer.addAll(stream.read()); |
| stream.onError = (e) => completer.completeException(e, stackTrace); |
| return completer.future; |
| } |
| |
| /// Buffers all input from a StringInputStream and returns it as a future. |
| Future<String> consumeStringInputStream(StringInputStream stream) { |
| if (stream.closed) return new Future.immediate(''); |
| |
| // TODO(nweiz): remove this when issue 4061 is fixed. |
| var stackTrace; |
| try { |
| throw ""; |
| } catch (_, localStackTrace) { |
| stackTrace = localStackTrace; |
| } |
| |
| var completer = new Completer<String>(); |
| var buffer = new StringBuffer(); |
| stream.onClosed = () => completer.complete(buffer.toString()); |
| stream.onData = () => buffer.add(stream.read()); |
| stream.onError = (e) => completer.completeException(e, stackTrace); |
| return completer.future; |
| } |
| |
| /// Spawns and runs the process located at [executable], passing in [args]. |
| /// Returns a [Future] that will complete with the results of the process after |
| /// it has ended. |
| /// |
| /// The spawned process will inherit its parent's environment variables. If |
| /// [environment] is provided, that will be used to augment (not replace) the |
| /// the inherited variables. |
| Future<PubProcessResult> runProcess(String executable, List<String> args, |
| {workingDir, Map<String, String> environment}) { |
| return _doProcess(Process.run, executable, args, workingDir, environment) |
| .transform((result) { |
| // TODO(rnystrom): Remove this and change to returning one string. |
| List<String> toLines(String output) { |
| var lines = output.split(NEWLINE_PATTERN); |
| if (!lines.isEmpty && lines.last == "") lines.removeLast(); |
| return lines; |
| } |
| return new PubProcessResult(toLines(result.stdout), |
| toLines(result.stderr), |
| result.exitCode); |
| }); |
| } |
| |
| /// Spawns the process located at [executable], passing in [args]. Returns a |
| /// [Future] that will complete with the [Process] once it's been started. |
| /// |
| /// The spawned process will inherit its parent's environment variables. If |
| /// [environment] is provided, that will be used to augment (not replace) the |
| /// the inherited variables. |
| Future<Process> startProcess(String executable, List<String> args, |
| {workingDir, Map<String, String> environment}) => |
| _doProcess(Process.start, executable, args, workingDir, environment); |
| |
| /// Calls [fn] with appropriately modified arguments. [fn] should have the same |
| /// signature as [Process.start], except that the returned [Future] may have a |
| /// type other than [Process]. |
| Future _doProcess(Function fn, String executable, List<String> args, workingDir, |
| Map<String, String> environment) { |
| // TODO(rnystrom): Should dart:io just handle this? |
| // Spawning a process on Windows will not look for the executable in the |
| // system path. So, if executable looks like it needs that (i.e. it doesn't |
| // have any path separators in it), then spawn it through a shell. |
| if ((Platform.operatingSystem == "windows") && |
| (executable.indexOf('\\') == -1)) { |
| args = flatten(["/c", executable, args]); |
| executable = "cmd"; |
| } |
| |
| final options = new ProcessOptions(); |
| if (workingDir != null) { |
| options.workingDirectory = _getDirectory(workingDir).path; |
| } |
| |
| if (environment != null) { |
| options.environment = new Map.from(Platform.environment); |
| environment.forEach((key, value) => options.environment[key] = value); |
| } |
| |
| return fn(executable, args, options); |
| } |
| |
| /// Closes [response] while ignoring the body of [request]. Returns a Future |
| /// that completes once the response is closed. |
| /// |
| /// Due to issue 6984, it's necessary to drain the request body before closing |
| /// the response. |
| Future closeHttpResponse(HttpRequest request, HttpResponse response) { |
| // TODO(nweiz): remove this when issue 4061 is fixed. |
| var stackTrace; |
| try { |
| throw ""; |
| } catch (_, localStackTrace) { |
| stackTrace = localStackTrace; |
| } |
| |
| var completer = new Completer(); |
| request.inputStream.onError = (e) => |
| completer.completeException(e, stackTrace); |
| request.inputStream.onData = request.inputStream.read; |
| request.inputStream.onClosed = () { |
| response.outputStream.close(); |
| completer.complete(null); |
| }; |
| return completer.future; |
| } |
| |
| /** |
| * Wraps [input] to provide a timeout. If [input] completes before |
| * [milliseconds] have passed, then the return value completes in the same way. |
| * However, if [milliseconds] pass before [input] has completed, it completes |
| * with a [TimeoutException] with [description] (which should be a fragment |
| * describing the action that timed out). |
| * |
| * Note that timing out will not cancel the asynchronous operation behind |
| * [input]. |
| */ |
| Future timeout(Future input, int milliseconds, String description) { |
| var completer = new Completer(); |
| var timer = new Timer(milliseconds, (_) { |
| if (completer.future.isComplete) return; |
| completer.completeException(new TimeoutException( |
| 'Timed out while $description.')); |
| }); |
| input.handleException((e) { |
| if (completer.future.isComplete) return false; |
| timer.cancel(); |
| completer.completeException(e, input.stackTrace); |
| return true; |
| }); |
| input.then((value) { |
| if (completer.future.isComplete) return; |
| timer.cancel(); |
| completer.complete(value); |
| }); |
| return completer.future; |
| } |
| |
| /// Creates a temporary directory and passes its path to [fn]. Once the [Future] |
| /// returned by [fn] completes, the temporary directory and all its contents |
| /// will be deleted. |
| Future withTempDir(Future fn(String path)) { |
| var tempDir; |
| var future = createTempDir().chain((dir) { |
| tempDir = dir; |
| return fn(tempDir.path); |
| }); |
| future.onComplete((_) => tempDir.delete(recursive: true)); |
| return future; |
| } |
| |
| /// Tests whether or not the git command-line app is available for use. |
| Future<bool> get isGitInstalled { |
| if (_isGitInstalledCache != null) { |
| // TODO(rnystrom): The sleep is to pump the message queue. Can use |
| // Future.immediate() when #3356 is fixed. |
| return sleep(0).transform((_) => _isGitInstalledCache); |
| } |
| |
| return _gitCommand.transform((git) => git != null); |
| } |
| |
| /// Run a git process with [args] from [workingDir]. |
| Future<PubProcessResult> runGit(List<String> args, |
| {String workingDir, Map<String, String> environment}) { |
| return _gitCommand.chain((git) => runProcess(git, args, |
| workingDir: workingDir, environment: environment)); |
| } |
| |
| /// Returns the name of the git command-line app, or null if Git could not be |
| /// found on the user's PATH. |
| Future<String> get _gitCommand { |
| // TODO(nweiz): Just use Future.immediate once issue 3356 is fixed. |
| if (_gitCommandCache != null) { |
| return sleep(0).transform((_) => _gitCommandCache); |
| } |
| |
| return _tryGitCommand("git").chain((success) { |
| if (success) return new Future.immediate("git"); |
| |
| // Git is sometimes installed on Windows as `git.cmd` |
| return _tryGitCommand("git.cmd").transform((success) { |
| if (success) return "git.cmd"; |
| return null; |
| }); |
| }).transform((command) { |
| _gitCommandCache = command; |
| return command; |
| }); |
| } |
| |
| /// Checks whether [command] is the Git command for this computer. |
| Future<bool> _tryGitCommand(String command) { |
| var completer = new Completer<bool>(); |
| |
| // If "git --version" prints something familiar, git is working. |
| var future = runProcess(command, ["--version"]); |
| |
| future.then((results) { |
| var regex = new RegExp("^git version"); |
| completer.complete(results.stdout.length == 1 && |
| regex.hasMatch(results.stdout[0])); |
| }); |
| |
| future.handleException((err) { |
| // If the process failed, they probably don't have it. |
| completer.complete(false); |
| return true; |
| }); |
| |
| return completer.future; |
| } |
| |
| /** |
| * Extracts a `.tar.gz` file from [stream] to [destination], which can be a |
| * directory or a path. Returns whether or not the extraction was successful. |
| */ |
| Future<bool> extractTarGz(InputStream stream, destination) { |
| destination = _getPath(destination); |
| |
| if (Platform.operatingSystem == "windows") { |
| return _extractTarGzWindows(stream, destination); |
| } |
| |
| var completer = new Completer<int>(); |
| var processFuture = Process.start("tar", |
| ["--extract", "--gunzip", "--directory", destination]); |
| processFuture.then((process) { |
| process.onExit = completer.complete; |
| stream.pipe(process.stdin); |
| process.stdout.pipe(stdout, close: false); |
| process.stderr.pipe(stderr, close: false); |
| }); |
| processFuture.handleException((error) { |
| completer.completeException(error, processFuture.stackTrace); |
| return true; |
| }); |
| |
| return completer.future.transform((exitCode) => exitCode == 0); |
| } |
| |
| Future<bool> _extractTarGzWindows(InputStream stream, String destination) { |
| // TODO(rnystrom): In the repo's history, there is an older implementation of |
| // this that does everything in memory by piping streams directly together |
| // instead of writing out temp files. The code is simpler, but unfortunately, |
| // 7zip seems to periodically fail when we invoke it from Dart and tell it to |
| // read from stdin instead of a file. Consider resurrecting that version if |
| // we can figure out why it fails. |
| |
| // Note: This line of code gets munged by create_sdk.py to be the correct |
| // relative path to 7zip in the SDK. |
| var pathTo7zip = '../../third_party/7zip/7za.exe'; |
| var command = relativeToPub(pathTo7zip); |
| |
| var tempDir; |
| |
| // TODO(rnystrom): Use withTempDir(). |
| return createTempDir().chain((temp) { |
| // Write the archive to a temp file. |
| tempDir = temp; |
| return createFileFromStream(stream, join(tempDir, 'data.tar.gz')); |
| }).chain((_) { |
| // 7zip can't unarchive from gzip -> tar -> destination all in one step |
| // first we un-gzip it to a tar file. |
| // Note: Setting the working directory instead of passing in a full file |
| // path because 7zip says "A full path is not allowed here." |
| return runProcess(command, ['e', 'data.tar.gz'], workingDir: tempDir); |
| }).chain((result) { |
| if (result.exitCode != 0) { |
| throw 'Could not un-gzip (exit code ${result.exitCode}). Error:\n' |
| '${Strings.join(result.stdout, "\n")}\n' |
| '${Strings.join(result.stderr, "\n")}'; |
| } |
| // Find the tar file we just created since we don't know its name. |
| return listDir(tempDir); |
| }).chain((files) { |
| var tarFile; |
| for (var file in files) { |
| if (new Path(file).extension == 'tar') { |
| tarFile = file; |
| break; |
| } |
| } |
| |
| if (tarFile == null) throw 'The gzip file did not contain a tar file.'; |
| |
| // Untar the archive into the destination directory. |
| return runProcess(command, ['x', tarFile], workingDir: destination); |
| }).chain((result) { |
| if (result.exitCode != 0) { |
| throw 'Could not un-tar (exit code ${result.exitCode}). Error:\n' |
| '${Strings.join(result.stdout, "\n")}\n' |
| '${Strings.join(result.stderr, "\n")}'; |
| } |
| |
| // Clean up the temp directory. |
| // TODO(rnystrom): Should also delete this if anything fails. |
| return deleteDir(tempDir); |
| }).transform((_) => true); |
| } |
| |
| /// Create a .tar.gz archive from a list of entries. Each entry can be a |
| /// [String], [Directory], or [File] object. The root of the archive is |
| /// considered to be [baseDir], which defaults to the current working directory. |
| /// Returns an [InputStream] that will emit the contents of the archive. |
| InputStream createTarGz(List contents, {baseDir}) { |
| // TODO(nweiz): Propagate errors to the returned stream (including non-zero |
| // exit codes). See issue 3657. |
| var stream = new ListInputStream(); |
| |
| if (baseDir == null) baseDir = currentWorkingDir; |
| baseDir = getFullPath(baseDir); |
| contents = contents.map((entry) { |
| entry = getFullPath(entry); |
| if (!isBeneath(entry, baseDir)) { |
| throw 'Entry $entry is not inside $baseDir.'; |
| } |
| return new Path.fromNative(entry).relativeTo(new Path.fromNative(baseDir)) |
| .toNativePath(); |
| }); |
| |
| if (Platform.operatingSystem != "windows") { |
| var args = ["--create", "--gzip", "--directory", baseDir]; |
| args.addAll(contents.map(_getPath)); |
| // TODO(nweiz): It's possible that enough command-line arguments will make |
| // the process choke, so at some point we should save the arguments to a |
| // file and pass them in via --files-from for tar and -i@filename for 7zip. |
| startProcess("tar", args).then((process) { |
| pipeInputToInput(process.stdout, stream); |
| process.stderr.pipe(stderr, close: false); |
| }); |
| return stream; |
| } |
| |
| withTempDir((tempDir) { |
| // Create the tar file. |
| var tarFile = join(tempDir, "intermediate.tar"); |
| var args = ["a", "-w$baseDir", tarFile]; |
| args.addAll(contents.map((entry) => '-i!"$entry"')); |
| |
| // Note: This line of code gets munged by create_sdk.py to be the correct |
| // relative path to 7zip in the SDK. |
| var pathTo7zip = '../../third_party/7zip/7za.exe'; |
| var command = relativeToPub(pathTo7zip); |
| |
| // We're passing 'baseDir' both as '-w' and setting it as the working |
| // directory explicitly here intentionally. The former ensures that the |
| // files added to the archive have the correct relative path in the archive. |
| // The latter enables relative paths in the "-i" args to be resolved. |
| return runProcess(command, args, workingDir: baseDir).chain((_) { |
| // GZIP it. 7zip doesn't support doing both as a single operation. Send |
| // the output to stdout. |
| args = ["a", "unused", "-tgzip", "-so", tarFile]; |
| return startProcess(command, args); |
| }).chain((process) { |
| process.stderr.pipe(stderr, close: false); |
| return pipeInputToInput(process.stdout, stream); |
| }); |
| }); |
| return stream; |
| } |
| |
| /** |
| * Exception thrown when an HTTP operation fails. |
| */ |
| class PubHttpException implements Exception { |
| final http.Response response; |
| |
| const PubHttpException(this.response); |
| |
| String toString() => 'HTTP error ${response.statusCode}: ${response.reason}'; |
| } |
| |
| /** |
| * Exception thrown when an operation times out. |
| */ |
| class TimeoutException implements Exception { |
| final String message; |
| |
| const TimeoutException(this.message); |
| |
| String toString() => message; |
| } |
| |
| /** |
| * Contains the results of invoking a [Process] and waiting for it to complete. |
| */ |
| class PubProcessResult { |
| final List<String> stdout; |
| final List<String> stderr; |
| final int exitCode; |
| |
| const PubProcessResult(this.stdout, this.stderr, this.exitCode); |
| |
| bool get success => exitCode == 0; |
| } |
| |
| /** |
| * Gets the path string for [entry], which can either already be a path string, |
| * or be a [File] or [Directory]. Allows working generically with "file-like" |
| * objects. |
| */ |
| String _getPath(entry) { |
| if (entry is String) return entry; |
| if (entry is File) return entry.name; |
| if (entry is Directory) return entry.path; |
| throw 'Entry $entry is not a supported type.'; |
| } |
| |
| /// Gets the path string for [entry] as in [_getPath], but normalizes |
| /// backslashes to forward slashes on Windows. |
| String _sanitizePath(entry) { |
| entry = _getPath(entry); |
| if (Platform.operatingSystem == 'windows') { |
| entry = entry.replaceAll('\\', '/'); |
| } |
| return entry; |
| } |
| |
| /** |
| * Gets a [Directory] for [entry], which can either already be one, or be a |
| * [String]. |
| */ |
| Directory _getDirectory(entry) { |
| if (entry is Directory) return entry; |
| return new Directory(entry); |
| } |
| |
| /** |
| * Gets a [Uri] for [uri], which can either already be one, or be a [String]. |
| */ |
| Uri _getUri(uri) { |
| if (uri is Uri) return uri; |
| return new Uri.fromString(uri); |
| } |