| Index: utils/pub/io.dart
 | 
| diff --git a/utils/pub/io.dart b/utils/pub/io.dart
 | 
| index ed5dee68be2a1c265dfd56364385e676b3c56f50..2546827361cc495ebec8637ef722e57494d38f40 100644
 | 
| --- a/utils/pub/io.dart
 | 
| +++ b/utils/pub/io.dart
 | 
| @@ -31,13 +31,14 @@ void printError(value) {
 | 
|    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 = _getPath(part1).replaceAll('\\', '/').split('/');
 | 
| +  final parts = _sanitizePath(part1).split('/');
 | 
|  
 | 
|    for (final part in [part2, part3, part4]) {
 | 
|      if (part == null) continue;
 | 
| @@ -65,7 +66,7 @@ String join(part1, [part2, part3, part4]) {
 | 
|  // 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 = _getPath(file).replaceAll('\\', '/');
 | 
| +  file = _sanitizePath(file);
 | 
|  
 | 
|    int lastSlash = file.lastIndexOf('/', file.length);
 | 
|    if (lastSlash == -1) {
 | 
| @@ -82,7 +83,7 @@ String basename(file) {
 | 
|  // 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 = _getPath(file).replaceAll('\\', '/');
 | 
| +  file = _sanitizePath(file);
 | 
|  
 | 
|    int lastSlash = file.lastIndexOf('/', file.length);
 | 
|    if (lastSlash == -1) {
 | 
| @@ -92,6 +93,11 @@ String dirname(file) {
 | 
|    }
 | 
|  }
 | 
|  
 | 
| +/// 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
 | 
| @@ -228,12 +234,11 @@ Future<Directory> deleteDir(dir) {
 | 
|  /**
 | 
|   * 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 [includeSpecialFiles] is `true`, includes
 | 
| - * hidden `.DS_Store` files (defaults to `false`, other hidden files may be
 | 
| - * omitted later).
 | 
| + * (defaults to `false`). If [includeHiddenFiles] is `true`, includes files
 | 
| + * beginning with `.` (defaults to `false`).
 | 
|   */
 | 
|  Future<List<String>> listDir(dir,
 | 
| -    [bool recursive = false, bool includeSpecialFiles = false]) {
 | 
| +    {bool recursive: false, bool includeHiddenFiles: false}) {
 | 
|    final completer = new Completer<List<String>>();
 | 
|    final contents = <String>[];
 | 
|  
 | 
| @@ -249,9 +254,7 @@ Future<List<String>> listDir(dir,
 | 
|    lister.onError = (error) => completer.completeException(error);
 | 
|    lister.onDir = (file) => contents.add(file);
 | 
|    lister.onFile = (file) {
 | 
| -    if (!includeSpecialFiles) {
 | 
| -      if (basename(file) == '.DS_Store') return;
 | 
| -    }
 | 
| +    if (!includeHiddenFiles && basename(file).startsWith('.')) return;
 | 
|      contents.add(file);
 | 
|    };
 | 
|  
 | 
| @@ -372,6 +375,41 @@ String relativeToPub(String path) {
 | 
|    return scriptDir.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;
 | 
| +  }
 | 
| +
 | 
| +  var completer = new Completer();
 | 
| +  stream.onClosed = () {
 | 
| +    removeCallbacks();
 | 
| +    completer.complete('');
 | 
| +  };
 | 
| +
 | 
| +  stream.onLine = () {
 | 
| +    removeCallbacks();
 | 
| +    completer.complete(stream.readLine());
 | 
| +  };
 | 
| +
 | 
| +  stream.onError = (e) {
 | 
| +    removeCallbacks();
 | 
| +    completer.completeException(e);
 | 
| +  };
 | 
| +
 | 
| +  return completer.future;
 | 
| +}
 | 
| +
 | 
|  // TODO(nweiz): make this configurable
 | 
|  /**
 | 
|   * The amount of time in milliseconds to allow HTTP requests before assuming
 | 
| @@ -432,7 +470,7 @@ Future<InputStream> httpGet(uri) {
 | 
|  Future<String> httpGetString(uri) {
 | 
|    var future = httpGet(uri).chain((stream) => consumeInputStream(stream))
 | 
|        .transform((bytes) => new String.fromCharCodes(bytes));
 | 
| -  return timeout(future, HTTP_TIMEOUT, 'Timed out while fetching URL "$uri".');
 | 
| +  return timeout(future, HTTP_TIMEOUT, 'fetching URL "$uri"');
 | 
|  }
 | 
|  
 | 
|  /**
 | 
| @@ -455,6 +493,8 @@ void pipeInputToInput(InputStream source, ListInputStream sink,
 | 
|   * 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>[]);
 | 
| +
 | 
|    var completer = new Completer<List<int>>();
 | 
|    var buffer = <int>[];
 | 
|    stream.onClosed = () => completer.complete(buffer);
 | 
| @@ -463,22 +503,56 @@ Future<List<int>> consumeInputStream(InputStream stream) {
 | 
|    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('');
 | 
| +
 | 
| +  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);
 | 
| +  return completer.future;
 | 
| +}
 | 
| +
 | 
|  /// Spawns and runs the process located at [executable], passing in [args].
 | 
| -/// Returns a [Future] that will complete the results of the process after it
 | 
| -/// has ended.
 | 
| +/// 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.
 | 
| -///
 | 
| -/// If [pipeStdout] and/or [pipeStderr] are set, all output from the
 | 
| -/// subprocess's output streams are sent to the parent process's output streams.
 | 
| -/// Output from piped streams won't be available in the result object.
 | 
|  Future<PubProcessResult> runProcess(String executable, List<String> args,
 | 
| -    {workingDir, Map<String, String> environment, bool pipeStdout: false,
 | 
| -    bool pipeStderr: false}) {
 | 
| -  int exitCode;
 | 
| +    {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
 | 
| @@ -499,34 +573,25 @@ Future<PubProcessResult> runProcess(String executable, List<String> args,
 | 
|      environment.forEach((key, value) => options.environment[key] = value);
 | 
|    }
 | 
|  
 | 
| -  var future = Process.run(executable, args, options);
 | 
| -  return future.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);
 | 
| -  });
 | 
| +  return fn(executable, args, options);
 | 
|  }
 | 
|  
 | 
|  /**
 | 
|   * 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 [message].
 | 
| + * 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 message) {
 | 
| +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(message));
 | 
| +    completer.completeException(new TimeoutException(
 | 
| +        'Timed out while $description.'));
 | 
|    });
 | 
|    input.handleException((e) {
 | 
|      if (completer.future.isComplete) return false;
 | 
| @@ -704,6 +769,62 @@ Future<bool> _extractTarGzWindows(InputStream stream, String destination) {
 | 
|    }).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(entry).relativeTo(new Path(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);
 | 
| +
 | 
| +    return runProcess(command, args).chain((_) {
 | 
| +      // GZIP it. 7zip doesn't support doing both as a single operation. Send
 | 
| +      // the output to stdout.
 | 
| +      args = ["a", "not used", "-so", tarFile];
 | 
| +      return startProcess(command, args);
 | 
| +    }).transform((process) {
 | 
| +      pipeInputToInput(process.stdout, stream);
 | 
| +      process.stderr.pipe(stderr, close: false);
 | 
| +    });
 | 
| +  });
 | 
| +  return stream;
 | 
| +}
 | 
| +
 | 
|  /**
 | 
|   * Exception thrown when an HTTP operation fails.
 | 
|   */
 | 
| @@ -752,6 +873,16 @@ String _getPath(entry) {
 | 
|    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].
 | 
| 
 |