Version 0.3.7.6
Merge revisions 18686 and 18715 to trunk

git-svn-id: http://dart.googlecode.com/svn/trunk@18717 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/pkg/args/test/usage_test.dart b/pkg/args/test/usage_test.dart
index b29bf95..6068f32 100644
--- a/pkg/args/test/usage_test.dart
+++ b/pkg/args/test/usage_test.dart
@@ -211,5 +211,5 @@
     }
   }
 
-  return Strings.join(lines, '\n');
+  return lines.join('\n');
 }
diff --git a/pkg/http/lib/src/utils.dart b/pkg/http/lib/src/utils.dart
index a70e937..739a567 100644
--- a/pkg/http/lib/src/utils.dart
+++ b/pkg/http/lib/src/utils.dart
@@ -38,7 +38,7 @@
   var pairs = <List<String>>[];
   map.forEach((key, value) =>
       pairs.add([encodeUriComponent(key), encodeUriComponent(value)]));
-  return Strings.join(pairs.map((pair) => "${pair[0]}=${pair[1]}"), "&");
+  return pairs.map((pair) => "${pair[0]}=${pair[1]}").join("&");
 }
 
 /// Adds all key/value pairs from [source] to [destination], overwriting any
diff --git a/pkg/oauth2/lib/src/authorization_code_grant.dart b/pkg/oauth2/lib/src/authorization_code_grant.dart
index b361f21..b199579 100644
--- a/pkg/oauth2/lib/src/authorization_code_grant.dart
+++ b/pkg/oauth2/lib/src/authorization_code_grant.dart
@@ -139,7 +139,7 @@
     };
 
     if (state != null) parameters['state'] = state;
-    if (!scopes.isEmpty) parameters['scope'] = Strings.join(scopes, ' ');
+    if (!scopes.isEmpty) parameters['scope'] = scopes.join(' ');
 
     return addQueryParameters(this.authorizationEndpoint, parameters);
   }
diff --git a/pkg/oauth2/lib/src/credentials.dart b/pkg/oauth2/lib/src/credentials.dart
index 17cd410..26112c6 100644
--- a/pkg/oauth2/lib/src/credentials.dart
+++ b/pkg/oauth2/lib/src/credentials.dart
@@ -171,7 +171,7 @@
         "client_id": identifier,
         "client_secret": secret
       };
-      if (!scopes.isEmpty) fields["scope"] = Strings.join(scopes, ' ');
+      if (!scopes.isEmpty) fields["scope"] = scopes.join(' ');
 
       return httpClient.post(tokenEndpoint, fields: fields);
     }).then((response) {
diff --git a/pkg/oauth2/lib/src/utils.dart b/pkg/oauth2/lib/src/utils.dart
index 83fe2b7..f69ac8b 100644
--- a/pkg/oauth2/lib/src/utils.dart
+++ b/pkg/oauth2/lib/src/utils.dart
@@ -39,10 +39,10 @@
     value = (value == null || value.isEmpty) ? null : encodeUriComponent(value);
     pairs.add([key, value]);
   });
-  return Strings.join(pairs.map((pair) {
+  return pairs.map((pair) {
     if (pair[1] == null) return pair[0];
     return "${pair[0]}=${pair[1]}";
-  }), "&");
+  }).join("&");
 }
 
 /// Add all key/value pairs from [source] to [destination], overwriting any
diff --git a/pkg/scheduled_test/test/metatest.dart b/pkg/scheduled_test/test/metatest.dart
index ff85d51..2a83015 100644
--- a/pkg/scheduled_test/test/metatest.dart
+++ b/pkg/scheduled_test/test/metatest.dart
@@ -170,7 +170,7 @@
   // TODO(nweiz): Use this simpler code once issue 2980 is fixed.
   // return str.replaceAll(new RegExp("^", multiLine: true), "  ");
 
-  return Strings.join(str.split("\n").map((line) => "  $line"), "\n");
+  return str.split("\n").map((line) => "  $line").join("\n");
 }
 
 /// Ensure that the metatest configuration is loaded.
diff --git a/pkg/unittest/lib/src/config.dart b/pkg/unittest/lib/src/config.dart
index 73de212..2cd2334 100644
--- a/pkg/unittest/lib/src/config.dart
+++ b/pkg/unittest/lib/src/config.dart
@@ -146,7 +146,7 @@
     // TODO(nweiz): Use this simpler code once issue 2980 is fixed.
     // return str.replaceAll(new RegExp("^", multiLine: true), "  ");
 
-    return Strings.join(str.split("\n").map((line) => "  $line"), "\n");
+    return str.split("\n").map((line) => "  $line").join("\n");
   }
 
   /** Handle errors that happen outside the tests. */
diff --git a/pkg/yaml/lib/model.dart b/pkg/yaml/lib/model.dart
index 7a609b9..9f1474e 100644
--- a/pkg/yaml/lib/model.dart
+++ b/pkg/yaml/lib/model.dart
@@ -90,8 +90,7 @@
     return true;
   }
 
-  String toString() =>
-      '$tag [${Strings.join(content.map((e) => '$e'), ', ')}]';
+  String toString() => '$tag [${content.map((e) => '$e').join(', ')}]';
 
   int get hashCode => super.hashCode ^ _hashCode(content);
 
@@ -179,7 +178,7 @@
           }
         }
       });
-      return '"${Strings.join(escapedValue, '')}"';
+      return '"${escapedValue.join()}"';
     }
 
     throw new YamlException("unknown scalar value: $value");
@@ -193,7 +192,7 @@
     assert(length >= str.length);
     var prefix = [];
     prefix.insertRange(0, length - str.length, '0');
-    return '${Strings.join(prefix, '')}$str';
+    return '${prefix.join()}$str';
   }
 
   int get hashCode => super.hashCode ^ content.hashCode;
diff --git a/pkg/yaml/test/yaml_test.dart b/pkg/yaml/test/yaml_test.dart
index b939b67..901bd3d 100644
--- a/pkg/yaml/test/yaml_test.dart
+++ b/pkg/yaml/test/yaml_test.dart
@@ -1350,7 +1350,7 @@
       // TODO(nweiz): enable this when we throw an error for long keys
       // var dotList = [];
       // dotList.insertRange(0, 1024, ' ');
-      // var dots = Strings.join(dotList, '');
+      // var dots = dotList.join();
       // Expect.throws(() => loadYaml('[ "foo...$dots...bar": invalid ]'));
     });
   });
diff --git a/runtime/lib/string_buffer_patch.dart b/runtime/lib/string_buffer_patch.dart
index ac700bf..8fadafa 100644
--- a/runtime/lib/string_buffer_patch.dart
+++ b/runtime/lib/string_buffer_patch.dart
@@ -43,7 +43,7 @@
   /* patch */ String toString() {
     if (_buffer.length == 0) return "";
     if (_buffer.length == 1) return _buffer[0];
-    String result = Strings.concatAll(_buffer);
+    String result = _StringBase.concatAll(_buffer);
     _buffer.clear();
     _buffer.add(result);
     // Since we track the length at each add operation, there is no
diff --git a/runtime/lib/string_patch.dart b/runtime/lib/string_patch.dart
index 7157ff9..6a9cf54 100644
--- a/runtime/lib/string_patch.dart
+++ b/runtime/lib/string_patch.dart
@@ -7,13 +7,3 @@
     return _StringBase.createFromCharCodes(charCodes);
   }
 }
-
-patch class Strings {
-  /* patch */ static String join(Iterable<String> strings, String separator) {
-    return _StringBase.join(strings, separator);
-  }
-
-  /* patch */ static String concatAll(List<String> strings) {
-    return _StringBase.concatAll(strings);
-  }
-}
diff --git a/sdk/lib/_internal/compiler/implementation/lib/core_patch.dart b/sdk/lib/_internal/compiler/implementation/lib/core_patch.dart
index b542b7a..b0f09ccf 100644
--- a/sdk/lib/_internal/compiler/implementation/lib/core_patch.dart
+++ b/sdk/lib/_internal/compiler/implementation/lib/core_patch.dart
@@ -202,33 +202,6 @@
   }
 }
 
-// Patch for String implementation.
-patch class Strings {
-  patch static String join(Iterable<String> strings, String separator) {
-    checkNull(strings);
-    if (separator is !String) throw new ArgumentError(separator);
-    return stringJoinUnchecked(_toJsStringArray(strings), separator);
-  }
-
-  patch static String concatAll(Iterable<String> strings) {
-    return stringJoinUnchecked(_toJsStringArray(strings), "");
-  }
-
-  static List _toJsStringArray(Iterable<String> strings) {
-    checkNull(strings);
-    var array;
-    if (!isJsArray(strings)) {
-      strings = new List.from(strings);
-    }
-    final length = strings.length;
-    for (int i = 0; i < length; i++) {
-      final string = strings[i];
-      if (string is !String) throw new ArgumentError(string);
-    }
-    return strings;
-  }
-}
-
 patch class RegExp {
   patch factory RegExp(String pattern,
                        {bool multiLine: false,
diff --git a/sdk/lib/_internal/dartdoc/lib/dartdoc.dart b/sdk/lib/_internal/dartdoc/lib/dartdoc.dart
index 59ab485..e0305ab 100644
--- a/sdk/lib/_internal/dartdoc/lib/dartdoc.dart
+++ b/sdk/lib/_internal/dartdoc/lib/dartdoc.dart
@@ -691,7 +691,7 @@
         for (final typeVariable in type.originalDeclaration.typeVariables) {
           typeVariables.add(typeVariable.displayName);
         }
-        typeInfo[ARGS] = Strings.join(typeVariables, ', ');
+        typeInfo[ARGS] = typeVariables.join(', ');
       }
       types.add(typeInfo);
     }
@@ -1766,15 +1766,14 @@
       if (typeParams.isEmpty) {
         return type.simpleName;
       }
-      final params = Strings.join(typeParams, ', ');
+      final params = typeParams.join(', ');
       return '${type.simpleName}&lt;$params&gt;';
     }
 
     // See if it's an instantiation of a generic type.
     final typeArgs = type.typeArguments;
     if (typeArgs.length > 0) {
-      final args =
-          Strings.join(typeArgs.map((arg) => typeName(arg)), ', ');
+      final args = typeArgs.map((arg) => typeName(arg)).join(', ');
       return '${type.originalDeclaration.simpleName}&lt;$args&gt;';
     }
 
@@ -1793,7 +1792,7 @@
       lines[i] = unindent(lines[i], column);
     }
 
-    final code = Strings.join(lines, '\n');
+    final code = lines.join('\n');
     return code;
   }
 
diff --git a/sdk/lib/_internal/dartdoc/lib/src/dartdoc/utils.dart b/sdk/lib/_internal/dartdoc/lib/src/dartdoc/utils.dart
index 79a9aa6..59e14b5 100644
--- a/sdk/lib/_internal/dartdoc/lib/src/dartdoc/utils.dart
+++ b/sdk/lib/_internal/dartdoc/lib/src/dartdoc/utils.dart
@@ -68,7 +68,7 @@
 String joinWithCommas(List<String> items, [String conjunction = 'and']) {
   if (items.length == 1) return items[0];
   if (items.length == 2) return "${items[0]} $conjunction ${items[1]}";
-  return '${Strings.join(items.getRange(0, items.length - 1), ', ')}'
+  return '${items.getRange(0, items.length - 1).join(', ')}'
     ', $conjunction ${items[items.length - 1]}';
 }
 
diff --git a/sdk/lib/_internal/dartdoc/lib/src/markdown/block_parser.dart b/sdk/lib/_internal/dartdoc/lib/src/markdown/block_parser.dart
index 67109a4..a5be1a7 100644
--- a/sdk/lib/_internal/dartdoc/lib/src/markdown/block_parser.dart
+++ b/sdk/lib/_internal/dartdoc/lib/src/markdown/block_parser.dart
@@ -234,7 +234,7 @@
     childLines.add('');
 
     // Escape the code.
-    final escaped = classifySource(Strings.join(childLines, '\n'));
+    final escaped = classifySource(childLines.join('\n'));
 
     return new Element.text('pre', escaped);
   }
@@ -275,7 +275,7 @@
       parser.advance();
     }
 
-    return new Text(Strings.join(childLines, '\n'));
+    return new Text(childLines.join('\n'));
   }
 }
 
@@ -457,8 +457,7 @@
       parser.advance();
     }
 
-    final contents = parser.document.parseInline(
-        Strings.join(childLines, '\n'));
+    final contents = parser.document.parseInline(childLines.join('\n'));
     return new Element('p', contents);
   }
 }
diff --git a/sdk/lib/_internal/dartdoc/test/markdown_test.dart b/sdk/lib/_internal/dartdoc/test/markdown_test.dart
index b786f94..bd23384 100644
--- a/sdk/lib/_internal/dartdoc/test/markdown_test.dart
+++ b/sdk/lib/_internal/dartdoc/test/markdown_test.dart
@@ -825,7 +825,7 @@
     }
   }
 
-  return Strings.join(lines, '\n');
+  return lines.join('\n');
 }
 
 validate(String description, String markdown, String html,
diff --git a/sdk/lib/core/core.dart b/sdk/lib/core/core.dart
index ca88387..016d6c0 100644
--- a/sdk/lib/core/core.dart
+++ b/sdk/lib/core/core.dart
@@ -36,5 +36,4 @@
 part "string.dart";
 part "string_buffer.dart";
 part "string_sink.dart";
-part "strings.dart";
 part "type.dart";
diff --git a/sdk/lib/core/corelib_sources.gypi b/sdk/lib/core/corelib_sources.gypi
index 529d725..f85e349 100644
--- a/sdk/lib/core/corelib_sources.gypi
+++ b/sdk/lib/core/corelib_sources.gypi
@@ -31,7 +31,6 @@
     'set.dart',
     'stopwatch.dart',
     'string.dart',
-    'strings.dart',
     'string_buffer.dart',
     'string_sink.dart',
     'type.dart',
diff --git a/sdk/lib/core/strings.dart b/sdk/lib/core/strings.dart
deleted file mode 100644
index c8a2afc..0000000
--- a/sdk/lib/core/strings.dart
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (c) 2011, 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.
-
-part of dart.core;
-
-@deprecated
-abstract class Strings {
-  /**
-   * Joins all the given strings to create a new string.
-   *
-   * *Deprecated* Use `strings.join(separator)` instead.
-   */
-  @deprecated
-  external static String join(Iterable<String> strings, String separator);
-
-  /**
-   * Concatenates all the given strings to create a new string.
-   *
-   * *Deprecated* Use `strings.join()` instead.
-   */
-  @deprecated
-  external static String concatAll(Iterable<String> strings);
-}
diff --git a/sdk/lib/html/dart2js/html_dart2js.dart b/sdk/lib/html/dart2js/html_dart2js.dart
index 3420aff..78346e6 100644
--- a/sdk/lib/html/dart2js/html_dart2js.dart
+++ b/sdk/lib/html/dart2js/html_dart2js.dart
@@ -29753,7 +29753,7 @@
 abstract class CssClassSet implements Set<String> {
 
   String toString() {
-    return Strings.join(new List.from(readClasses()), ' ');
+    return readClasses().join(' ');
   }
 
   /**
diff --git a/sdk/lib/html/dartium/html_dartium.dart b/sdk/lib/html/dartium/html_dartium.dart
index 8a3b951..017bfb3 100644
--- a/sdk/lib/html/dartium/html_dartium.dart
+++ b/sdk/lib/html/dartium/html_dartium.dart
@@ -32238,7 +32238,7 @@
 abstract class CssClassSet implements Set<String> {
 
   String toString() {
-    return Strings.join(new List.from(readClasses()), ' ');
+    return readClasses().join(' ');
   }
 
   /**
diff --git a/sdk/lib/io/path_impl.dart b/sdk/lib/io/path_impl.dart
index 2ea2fea..a42a290 100644
--- a/sdk/lib/io/path_impl.dart
+++ b/sdk/lib/io/path_impl.dart
@@ -103,7 +103,7 @@
     if (hasTrailingSeparator) {
         segments.add('');
     }
-    return new Path(Strings.join(segments, '/'));
+    return new Path(segments.join('/'));
   }
 
 
@@ -210,8 +210,7 @@
         segmentsToJoin.add('');
       }
     }
-    return new _Path._internal(Strings.join(segmentsToJoin, '/'),
-                               isWindowsShare);
+    return new _Path._internal(segmentsToJoin.join('/'), isWindowsShare);
   }
 
   String toNativePath() {
diff --git a/sdk/lib/io/process.dart b/sdk/lib/io/process.dart
index 34e0140..e146c61 100644
--- a/sdk/lib/io/process.dart
+++ b/sdk/lib/io/process.dart
@@ -242,7 +242,7 @@
                           int this.errorCode = 0]);
   String toString() {
     var msg = (message == null) ? 'OS error code: $errorCode' : message;
-    var args = Strings.join(arguments, ' ');
+    var args = arguments.join(' ');
     return "ProcessException: $msg\n  Command: $executable $args";
   }
 
diff --git a/sdk/lib/svg/dart2js/svg_dart2js.dart b/sdk/lib/svg/dart2js/svg_dart2js.dart
index 7543c2a..4b26804 100644
--- a/sdk/lib/svg/dart2js/svg_dart2js.dart
+++ b/sdk/lib/svg/dart2js/svg_dart2js.dart
@@ -5771,8 +5771,7 @@
   }
 
   void writeClasses(Set s) {
-    List list = new List.from(s);
-    _element.attributes['class'] = Strings.join(list, ' ');
+    _element.attributes['class'] = s.join(' ');
   }
 }
 
diff --git a/sdk/lib/svg/dartium/svg_dartium.dart b/sdk/lib/svg/dartium/svg_dartium.dart
index c0d430b..1c03b3a 100644
--- a/sdk/lib/svg/dartium/svg_dartium.dart
+++ b/sdk/lib/svg/dartium/svg_dartium.dart
@@ -6520,8 +6520,7 @@
   }
 
   void writeClasses(Set s) {
-    List list = new List.from(s);
-    _element.attributes['class'] = Strings.join(list, ' ');
+    _element.attributes['class'] = s.join(' ');
   }
 }
 
diff --git a/sdk/lib/uri/helpers.dart b/sdk/lib/uri/helpers.dart
index d17810f..2a16a47 100644
--- a/sdk/lib/uri/helpers.dart
+++ b/sdk/lib/uri/helpers.dart
@@ -25,5 +25,5 @@
     }
   }
   if (appendSlash) output.add("");
-  return Strings.join(output, "/");
+  return output.join("/");
 }
diff --git a/tests/co19/co19-compiler.status b/tests/co19/co19-compiler.status
index 191148a..423a814 100644
--- a/tests/co19/co19-compiler.status
+++ b/tests/co19/co19-compiler.status
@@ -491,5 +491,16 @@
 LibTest/core/Set/Set.from_A01_t01: Fail # Moved collection classes from core to collection. co19 issue 371.
 
 
+LibTest/core/RegExp/Pattern_semantics/firstMatch_CharacterClassEscape_A03_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/RegExp/Pattern_semantics/firstMatch_CharacterClassEscape_A04_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/Strings/join_A01_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/Strings/join_A02_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/Strings/join_A03_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/Strings/join_A04_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/Strings/concatAll_A01_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/Strings/concatAll_A02_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/Strings/concatAll_A03_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/Strings/concatAll_A04_t01: Fail # Strings class has been removed. co19 issue 380
+
 [ $runtime == drt && ($compiler == none || $compiler == frog) ]
 *: Skip
diff --git a/tests/co19/co19-dart2dart.status b/tests/co19/co19-dart2dart.status
index 1f87d90..8d256ea 100644
--- a/tests/co19/co19-dart2dart.status
+++ b/tests/co19/co19-dart2dart.status
@@ -654,6 +654,11 @@
 LibTest/core/Set/Set.from_A01_t01: Fail # Moved collection classes from core to collection. co19 issue 371.
 Language/14_Types/4_Interface_Types_A08_t06: Fail # Moved collection classes from core to collection. co19 issue 371.
 
+LibTest/core/Strings/join_A01_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/Strings/join_A04_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/Strings/concatAll_A01_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/Strings/concatAll_A04_t01: Fail # Strings class has been removed. co19 issue 380
+
 # Doesn't expect null to be allowed in Set or Map keys (issue 377).
 LibTest/core/Map/containsKey_A01_t02: Fail
 LibTest/core/Map/operator_subscript_A01_t02: Fail
diff --git a/tests/co19/co19-dart2js.status b/tests/co19/co19-dart2js.status
index 1ba4426..41e3a33 100644
--- a/tests/co19/co19-dart2js.status
+++ b/tests/co19/co19-dart2js.status
@@ -575,6 +575,11 @@
 
 LibTest/core/Date/operator_equality_A01_t01: Fail # DateTime.== now looks at timezone, co19 issue 379.
 
+LibTest/core/Strings/join_A01_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/Strings/join_A04_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/Strings/concatAll_A01_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/Strings/concatAll_A04_t01: Fail # Strings class has been removed. co19 issue 380
+
 # Issues with co19 test suite in checked mode.
 [ $compiler == dart2js && $checked ]
 LibTest/isolate/SendPort/call_A01_t01: Fail # Future is in async library. co19 issue 367
diff --git a/tests/co19/co19-runtime.status b/tests/co19/co19-runtime.status
index 6a312f5..0c4e1d2 100644
--- a/tests/co19/co19-runtime.status
+++ b/tests/co19/co19-runtime.status
@@ -504,6 +504,11 @@
 LibTest/core/Queue/removeLast_A02_t01: Fail # Moved collection classes from core to collection. co19 issue 371.
 LibTest/core/Set/Set.from_A01_t01: Fail # Moved collection classes from core to collection. co19 issue 371.
 
+LibTest/core/Strings/join_A01_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/Strings/join_A04_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/Strings/concatAll_A01_t01: Fail # Strings class has been removed. co19 issue 380
+LibTest/core/Strings/concatAll_A04_t01: Fail # Strings class has been removed. co19 issue 380
+
 # Doesn't expect null to be allowed in Set or Map keys (issue 377).
 LibTest/core/Map/containsKey_A01_t02: Fail
 LibTest/core/Map/operator_subscript_A01_t02: Fail
diff --git a/tests/language/named_parameters_with_dollars_test.dart b/tests/language/named_parameters_with_dollars_test.dart
index 6bd7af3..e4f0f27 100644
--- a/tests/language/named_parameters_with_dollars_test.dart
+++ b/tests/language/named_parameters_with_dollars_test.dart
@@ -28,7 +28,7 @@
       fragments.add(format(item));
     }
     fragments.add(']');
-    return Strings.concatAll(fragments);
+    return fragments.join();
   }
   return thing.toString();
 }
diff --git a/tests/language/string_join_test.dart b/tests/language/string_join_test.dart
index d332331..c98b79fa 100644
--- a/tests/language/string_join_test.dart
+++ b/tests/language/string_join_test.dart
@@ -8,7 +8,8 @@
     List<String> ga = new List<String>();
     ga.add("a");
     ga.add("b");
-    Expect.equals("ab", Strings.join(ga, ""));
+    Expect.equals("ab", ga.join());
+    Expect.equals("ab", ga.join(""));
   }
 }
 
diff --git a/tests/language/string_test.dart b/tests/language/string_test.dart
index 9e84396..52cdf71 100644
--- a/tests/language/string_test.dart
+++ b/tests/language/string_test.dart
@@ -24,7 +24,7 @@
     List<String> a = new List<String>.fixedLength(2);
     a[0] = "Hello";
     a[1] = "World";
-    String s = Strings.join(a, "*^*");
+    String s = a.join("*^*");
     Expect.equals("Hello*^*World", s);
   }
 
diff --git a/tests/standalone/io/secure_socket_bad_certificate_test.dart b/tests/standalone/io/secure_socket_bad_certificate_test.dart
index e3a2616..d53dd93 100644
--- a/tests/standalone/io/secure_socket_bad_certificate_test.dart
+++ b/tests/standalone/io/secure_socket_bad_certificate_test.dart
@@ -63,7 +63,7 @@
   };
   secure.onClosed = () {
     Expect.isTrue(acceptCertificate);
-    String fullPage = Strings.concatAll(chunks);
+    String fullPage = chunks.join();
     Expect.isTrue(fullPage.contains('</body></html>'));
     completer.complete(null);
   };
diff --git a/tests/standalone/io/secure_socket_test.dart b/tests/standalone/io/secure_socket_test.dart
index e1fe2a8..f010444 100644
--- a/tests/standalone/io/secure_socket_test.dart
+++ b/tests/standalone/io/secure_socket_test.dart
@@ -53,7 +53,7 @@
   };
   secure.onData = useRead;
   secure.onClosed = () {
-    String fullPage = Strings.concatAll(chunks);
+    String fullPage = chunks.join();
     Expect.isTrue(fullPage.contains('</body></html>'));
     keepAlive.close();
   };
diff --git a/tests/standalone/io/secure_stream_test.dart b/tests/standalone/io/secure_stream_test.dart
index 63c0fce..11acb7b 100644
--- a/tests/standalone/io/secure_stream_test.dart
+++ b/tests/standalone/io/secure_stream_test.dart
@@ -29,7 +29,7 @@
     chunks.add(new String.fromCharCodes(input.read()));
   };
   input.onClosed = () {
-    String fullPage = Strings.concatAll(chunks);
+    String fullPage = chunks.join();
     Expect.isTrue(fullPage.contains('</body></html>'));
     keepAlive.close();
   };
diff --git a/tests/utils/test_utils.dart b/tests/utils/test_utils.dart
index 40a2e55..a786fa2 100644
--- a/tests/utils/test_utils.dart
+++ b/tests/utils/test_utils.dart
@@ -30,7 +30,7 @@
     }
   }
 
-  return Strings.join(lines, '\n');
+  return lines.join('\n');
 }
 
 /**
@@ -49,5 +49,5 @@
     lines[i] = "        ${lines[i]}";
   }
 
-  return Strings.join(lines, "\n");
+  return lines.join("\n");
 }
diff --git a/tools/VERSION b/tools/VERSION
index 54f29d1..f532fc3 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -1,4 +1,4 @@
 MAJOR 0
 MINOR 3
 BUILD 7
-PATCH 5
+PATCH 6
diff --git a/tools/ddbg.dart b/tools/ddbg.dart
index de21364..7d95f79 100644
--- a/tools/ddbg.dart
+++ b/tools/ddbg.dart
@@ -556,7 +556,7 @@
     arguments = <String>['--debug', '--verbose_debug']..addAll(arguments);
     Process.start(options.executable, arguments).then((Process process) {
       process.onExit = (int exitCode) {
-        print('${Strings.join(arguments, " ")} exited with $exitCode');
+        print('${arguments.join(" ")} exited with $exitCode');
       };
       process.stdin.close();
       // Redirecting both stdout and stderr of the child process to
diff --git a/tools/dom/scripts/idlrenderer.dart b/tools/dom/scripts/idlrenderer.dart
index 9bfcd4f..9b7bd32 100644
--- a/tools/dom/scripts/idlrenderer.dart
+++ b/tools/dom/scripts/idlrenderer.dart
@@ -111,7 +111,7 @@
             else
               formattedArgs.add('$argName=$argValue');
           }
-          w('@$name(${Strings.join(formattedArgs,',')})');
+          w('@$name(${formattedArgs.join(',')})');
         }
         w(' ');
       }
@@ -176,5 +176,5 @@
   };
 
   w(idl_node);
-  return Strings.concatAll(output);
+  return output.join();
 }
diff --git a/tools/dom/src/CssClassSet.dart b/tools/dom/src/CssClassSet.dart
index 72fdc7e..51dd3b5 100644
--- a/tools/dom/src/CssClassSet.dart
+++ b/tools/dom/src/CssClassSet.dart
@@ -7,7 +7,7 @@
 abstract class CssClassSet implements Set<String> {
 
   String toString() {
-    return Strings.join(new List.from(readClasses()), ' ');
+    return readClasses().join(' ');
   }
 
   /**
diff --git a/tools/dom/templates/html/impl/impl_SVGElement.darttemplate b/tools/dom/templates/html/impl/impl_SVGElement.darttemplate
index c3ee680..61cf7f6 100644
--- a/tools/dom/templates/html/impl/impl_SVGElement.darttemplate
+++ b/tools/dom/templates/html/impl/impl_SVGElement.darttemplate
@@ -26,8 +26,7 @@
   }
 
   void writeClasses(Set s) {
-    List list = new List.from(s);
-    _element.attributes['class'] = Strings.join(list, ' ');
+    _element.attributes['class'] = s.join(' ');
   }
 }
 
diff --git a/tools/test-runtime.dart b/tools/test-runtime.dart
index 036c861..867d39e 100755
--- a/tools/test-runtime.dart
+++ b/tools/test-runtime.dart
@@ -67,9 +67,9 @@
       List settings = ['compiler', 'runtime', 'mode', 'arch']
           .mappedBy((name) => conf[name]).toList();
       if (conf['checked']) settings.add('checked');
-      output_words.add(Strings.join(settings, '_'));
+      output_words.add(settings.join('_'));
     }
-    print(Strings.join(output_words, ' '));
+    print(output_words.join(' '));
   }
 
   var testSuites = new List<TestSuite>();
diff --git a/tools/test.dart b/tools/test.dart
index 12cff10..1f88fd7 100755
--- a/tools/test.dart
+++ b/tools/test.dart
@@ -104,9 +104,9 @@
       List settings = ['compiler', 'runtime', 'mode', 'arch']
           .mappedBy((name) => conf[name]).toList();
       if (conf['checked']) settings.add('checked');
-      output_words.add(Strings.join(settings, '_'));
+      output_words.add(settings.join('_'));
     }
-    print(Strings.join(output_words, ' '));
+    print(output_words.join(' '));
   }
 
   // Start global http servers that serve the entire dart repo.
diff --git a/tools/testing/dart/multitest.dart b/tools/testing/dart/multitest.dart
index 85a9d1b..585502c 100644
--- a/tools/testing/dart/multitest.dart
+++ b/tools/testing/dart/multitest.dart
@@ -122,8 +122,7 @@
 
   // Copy all the tests into the output map tests, as multiline strings.
   for (String key in testsAsLines.keys) {
-    tests[key] =
-        Strings.join(testsAsLines[key], line_separator).concat(line_separator);
+    tests[key] = testsAsLines[key].join(line_separator).concat(line_separator);
   }
 }
 
diff --git a/tools/testing/dart/test_runner.dart b/tools/testing/dart/test_runner.dart
index 356b664..9833dd4 100644
--- a/tools/testing/dart/test_runner.dart
+++ b/tools/testing/dart/test_runner.dart
@@ -107,7 +107,7 @@
       // TODO(efortuna): Remove this when fixed (Issue 1306).
       executable = executable.replaceAll('/', '\\');
     }
-    commandLine = "$executable ${Strings.join(arguments, ' ')}";
+    commandLine = "$executable ${arguments.join(' ')}";
   }
 
   String toString() => commandLine;
@@ -196,7 +196,7 @@
     if (needDartFlags) {
       env = new Map.from(io.Platform.environment);
       if (needDartFlags) {
-        env['DART_FLAGS'] = Strings.join(dartFlags, " ");
+        env['DART_FLAGS'] = dartFlags.join(" ");
       }
     }
 
@@ -305,7 +305,7 @@
         newCommands.add(newCommand);
         // If there are extra spaces inside the prefix or suffix, this fails.
         String expected =
-            '$prefix ${c.executable} $suffix ${Strings.join(c.arguments, ' ')}';
+            '$prefix ${c.executable} $suffix ${c.arguments.join(' ')}';
         Expect.stringEquals(expected.trim(), newCommand.commandLine);
       }
       commands = newCommands;
@@ -1152,7 +1152,7 @@
   }
 
   String _createArgumentsLine(List<String> arguments) {
-    return Strings.join(arguments, ' ').concat('\n');
+    return arguments.join(' ').concat('\n');
   }
 
   void _reportResult() {
@@ -1287,7 +1287,7 @@
       callback();
     }).catchError((e) {
       print("Process error:");
-      print("  Command: $_executable ${Strings.join(_batchArguments, ' ')}");
+      print("  Command: $_executable ${_batchArguments.join(' ')}");
       print("  Error: $e");
       // If there is an error starting a batch process, chances are that
       // it will always fail. So rather than re-trying a 1000+ times, we
@@ -1464,7 +1464,7 @@
         };
       }).catchError((e) {
         print("Error starting process:");
-        print("  Command: $cmd ${Strings.join(arg, ' ')}");
+        print("  Command: $cmd ${arg.join(' ')}");
         print("  Error: $e");
         // TODO(ahe): How to report this as a test failure?
         exit(1);
@@ -1580,10 +1580,10 @@
       TestCase test = _tests.removeFirst();
       if (_listTests) {
         var fields = [test.displayName,
-                      Strings.join(new List.from(test.expectedOutcomes), ','),
+                      test.expectedOutcomes.join(','),
                       test.isNegative.toString()];
         fields.addAll(test.commands.last.arguments);
-        print(Strings.join(fields, '\t'));
+        print(fields.join('\t'));
         return;
       }
       if (test.usesWebDriver && _needsSelenium && !_isSeleniumAvailable || (test
diff --git a/tools/testing/dart/test_suite.dart b/tools/testing/dart/test_suite.dart
index 894fb5a..f8b59d8 100644
--- a/tools/testing/dart/test_suite.dart
+++ b/tools/testing/dart/test_suite.dart
@@ -850,9 +850,9 @@
       // replaceAll(RegExp, String) is implemented.
       String optionsName = '';
       if (getVmOptions(optionsFromFile).length > 1) {
-          optionsName = Strings.join(vmOptions, '-').replaceAll('-','')
-                                                    .replaceAll('=','')
-                                                    .replaceAll('/','');
+          optionsName = vmOptions.join('-').replaceAll('-','')
+                                           .replaceAll('=','')
+                                           .replaceAll('/','');
       }
       final String tempDir = createOutputDirectory(info.filePath, optionsName);
 
@@ -1633,7 +1633,7 @@
   }
 
   void computeClassPath() {
-    classPath = Strings.join(
+    classPath =
         ['$buildDir/analyzer/util/analyzer/dart_analyzer.jar',
          '$buildDir/analyzer/dart_analyzer_tests.jar',
          // Third party libraries.
@@ -1644,8 +1644,8 @@
          '$dartDir/third_party/hamcrest/v1_3/hamcrest-generator-1.3.0RC2.jar',
          '$dartDir/third_party/hamcrest/v1_3/hamcrest-integration-1.3.0RC2.jar',
          '$dartDir/third_party/hamcrest/v1_3/hamcrest-library-1.3.0RC2.jar',
-         '$dartDir/third_party/junit/v4_8_2/junit.jar'],
-        Platform.operatingSystem == 'windows'? ';': ':');  // Path separator.
+         '$dartDir/third_party/junit/v4_8_2/junit.jar']
+        .join(Platform.operatingSystem == 'windows'? ';': ':');  // Path separator.
   }
 }
 
diff --git a/utils/apidoc/html_diff.dart b/utils/apidoc/html_diff.dart
index d6a5067..3c8324d 100644
--- a/utils/apidoc/html_diff.dart
+++ b/utils/apidoc/html_diff.dart
@@ -197,7 +197,7 @@
             for (var t in domTypes) {
               options.add('$t.$name');
             }
-            Strings.join(options, ' or ');
+            options.join(' or ');
             warn('no member $options');
           }
         }
diff --git a/utils/lib/file_system.dart b/utils/lib/file_system.dart
index 7b8da62..306be7a 100644
--- a/utils/lib/file_system.dart
+++ b/utils/lib/file_system.dart
@@ -47,7 +47,7 @@
       pieces.add(piece);
     }
   }
-  return Strings.join(pieces, '/');
+  return pieces.join('/');
 }
 
 /** Returns the directory name for the [path]. */
diff --git a/utils/peg/pegparser.dart b/utils/peg/pegparser.dart
index 1333682..9072e2d 100644
--- a/utils/peg/pegparser.dart
+++ b/utils/peg/pegparser.dart
@@ -275,7 +275,7 @@
                   a.startsWith("'") == b.startsWith("'")
                       ? a.compareTo(b)
                       : a.startsWith("'") ? +1 : -1);
-      var expected = Strings.join(tokens, ' or ');
+      var expected = tokens.join(' or ');
       var found = state.max_pos == state._end ? 'end of file'
           : "'${state._text[state.max_pos]}'";
       message = 'Expected $expected but found $found';
diff --git a/utils/pub/entrypoint.dart b/utils/pub/entrypoint.dart
index 7de7c2c..b2dc9b4 100644
--- a/utils/pub/entrypoint.dart
+++ b/utils/pub/entrypoint.dart
@@ -209,7 +209,7 @@
   LockFile loadLockFile() {
     var lockFilePath = path.join(root.dir, 'pubspec.lock');
     if (!fileExists(lockFilePath)) return new LockFile.empty();
-    return new LockFile.parse(readTextFile(lockFilePath), cache.sources);
+    return new LockFile.load(lockFilePath, cache.sources);
   }
 
   /// Saves a list of concrete package versions to the `pubspec.lock` file.
@@ -294,9 +294,9 @@
   /// Creates a symlink to the `packages` directory in [dir] if none exists.
   Future _linkSecondaryPackageDir(String dir) {
     return defer(() {
-      var to = path.join(dir, 'packages');
-      if (entryExists(to)) return;
-      return createSymlink(packagesDir, to);
+      var symlink = path.join(dir, 'packages');
+      if (entryExists(symlink)) return;
+      return createSymlink(packagesDir, symlink);
     });
   }
 }
diff --git a/utils/pub/git_source.dart b/utils/pub/git_source.dart
index fdb0ed4..2b54c72 100644
--- a/utils/pub/git_source.dart
+++ b/utils/pub/git_source.dart
@@ -10,6 +10,7 @@
 
 import 'git.dart' as git;
 import 'io.dart';
+import 'log.dart' as log;
 import 'package.dart';
 import 'source.dart';
 import 'source_registry.dart';
@@ -67,24 +68,33 @@
       return path.join(systemCacheRoot, revisionCacheName);
     });
   }
+
   /// Ensures [description] is a Git URL.
-  void validateDescription(description, {bool fromLockFile: false}) {
+  dynamic parseDescription(String containingPath, description,
+                           {bool fromLockFile: false}) {
+    // TODO(rnystrom): Handle git URLs that are relative file paths (#8570).
+    // TODO(rnystrom): Now that this function can modify the description, it
+    // may as well canonicalize it to a map so that other code in the source
+    // can assume that.
     // A single string is assumed to be a Git URL.
-    if (description is String) return;
+    if (description is String) return description;
     if (description is! Map || !description.containsKey('url')) {
       throw new FormatException("The description must be a Git URL or a map "
           "with a 'url' key.");
     }
-    description = new Map.from(description);
-    description.remove('url');
-    description.remove('ref');
-    if (fromLockFile) description.remove('resolved-ref');
 
-    if (!description.isEmpty) {
-      var plural = description.length > 1;
-      var keys = description.keys.join(', ');
+    var parsed = new Map.from(description);
+    parsed.remove('url');
+    parsed.remove('ref');
+    if (fromLockFile) parsed.remove('resolved-ref');
+
+    if (!parsed.isEmpty) {
+      var plural = parsed.length > 1;
+      var keys = parsed.keys.join(', ');
       throw new FormatException("Invalid key${plural ? 's' : ''}: $keys.");
     }
+
+    return description;
   }
 
   /// Two Git descriptions are equal if both their URLs and their refs are
diff --git a/utils/pub/hosted_source.dart b/utils/pub/hosted_source.dart
index dda00a4..6fdc8e9 100644
--- a/utils/pub/hosted_source.dart
+++ b/utils/pub/hosted_source.dart
@@ -56,7 +56,7 @@
       "${id.version}.yaml";
 
     return httpClient.read(fullUrl).then((yaml) {
-      return new Pubspec.parse(yaml, systemCache.sources);
+      return new Pubspec.parse(null, yaml, systemCache.sources);
     }).catchError((ex) {
       _throwFriendlyError(ex, id, parsed.last);
     });
@@ -114,8 +114,10 @@
   /// There are two valid formats. A plain string refers to a package with the
   /// given name from the default host, while a map with keys "name" and "url"
   /// refers to a package with the given name from the host at the given URL.
-  void validateDescription(description, {bool fromLockFile: false}) {
+  dynamic parseDescription(String containingPath, description,
+                           {bool fromLockFile: false}) {
     _parseDescription(description);
+    return description;
   }
 
   /// When an error occurs trying to read something about [package] from [url],
diff --git a/utils/pub/io.dart b/utils/pub/io.dart
index 82614ca..a031da2 100644
--- a/utils/pub/io.dart
+++ b/utils/pub/io.dart
@@ -264,15 +264,32 @@
   return makeAttempt(null);
 }
 
-/// Creates a new symlink that creates an alias from [from] to [to]. Returns a
-/// [Future] which completes to the symlink file (i.e. [to]).
+/// Creates a new symlink at path [symlink] that points to [target]. Returns a
+/// [Future] which completes to the path to the symlink file.
+///
+/// If [relative] is true, creates a symlink with a relative path from the
+/// symlink to the target. Otherwise, uses the [target] path unmodified.
 ///
 /// Note that on Windows, only directories may be symlinked to.
-Future<String> createSymlink(String from, String to) {
-  log.fine("Creating symlink ($to is a symlink to $from)");
+Future<String> createSymlink(String target, String symlink,
+    {bool relative: false}) {
+  if (relative) {
+    // Relative junction points are not supported on Windows. Instead, just
+    // make sure we have a clean absolute path because it will interpret a
+    // relative path to be relative to the cwd, not the symlink, and will be
+    // confused by forward slashes.
+    if (Platform.operatingSystem == 'windows') {
+      target = path.normalize(path.absolute(target));
+    } else {
+      target = path.normalize(
+          path.relative(target, from: path.dirname(symlink)));
+    }
+  }
+
+  log.fine("Creating $symlink pointing to $target");
 
   var command = 'ln';
-  var args = ['-s', from, to];
+  var args = ['-s', target, symlink];
 
   if (Platform.operatingSystem == 'windows') {
     // Call mklink on Windows to create an NTFS junction point. Only works on
@@ -281,24 +298,29 @@
     // 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];
+    args = ['/j', symlink, target];
   }
 
   // TODO(rnystrom): Check exit code and output?
-  return runProcess(command, args).then((result) => to);
+  return runProcess(command, args).then((result) => symlink);
 }
 
-/// Creates a new symlink that creates an alias from the `lib` directory of
-/// package [from] to [to]. 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<String> createPackageSymlink(String name, String from, String to,
-    {bool isSelfLink: false}) {
+/// Creates a new symlink that creates an alias at [symlink] that points to the
+/// `lib` directory of package [target]. Returns a [Future] which completes to
+/// the path to the symlink file. If [target] does not have a `lib` directory,
+/// this shows a warning if appropriate and then does nothing.
+///
+/// If [relative] is true, creates a symlink with a relative path from the
+/// symlink to the target. Otherwise, uses the [target] path unmodified.
+Future<String> createPackageSymlink(String name, String target, String symlink,
+    {bool isSelfLink: false, bool relative: false}) {
   return defer(() {
     // See if the package has a "lib" directory.
-    from = path.join(from, 'lib');
+    target = path.join(target, 'lib');
     log.fine("Creating ${isSelfLink ? "self" : ""}link for package '$name'.");
-    if (dirExists(from)) return createSymlink(from, to);
+    if (dirExists(target)) {
+      return createSymlink(target, symlink, relative: relative);
+    }
 
     // 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
@@ -308,7 +330,7 @@
                   'you will not be able to import any libraries from it.');
     }
 
-    return to;
+    return symlink;
   });
 }
 
diff --git a/utils/pub/lock_file.dart b/utils/pub/lock_file.dart
index 23e7099..594e523 100644
--- a/utils/pub/lock_file.dart
+++ b/utils/pub/lock_file.dart
@@ -5,6 +5,7 @@
 library lock_file;
 
 import 'dart:json' as json;
+import 'io.dart';
 import 'package.dart';
 import 'source_registry.dart';
 import 'utils.dart';
@@ -21,8 +22,19 @@
   LockFile.empty()
     : packages = <String, PackageId>{};
 
-  /// Parses the lockfile whose text is [contents].
+  /// Loads a lockfile from [filePath].
+  factory LockFile.load(String filePath, SourceRegistry sources) {
+    return LockFile._parse(filePath, readTextFile(filePath), sources);
+  }
+
+  /// Parses a lockfile whose text is [contents].
   factory LockFile.parse(String contents, SourceRegistry sources) {
+    return LockFile._parse(null, contents, sources);
+  }
+
+  /// Parses the lockfile whose text is [contents].
+  static LockFile _parse(String filePath, String contents,
+      SourceRegistry sources) {
     var packages = <String, PackageId>{};
 
     if (contents.trim() == '') return new LockFile.empty();
@@ -54,7 +66,8 @@
           throw new FormatException('Package $name is missing a description.');
         }
         var description = spec['description'];
-        source.validateDescription(description, fromLockFile: true);
+        description = source.parseDescription(filePath, description,
+            fromLockFile: true);
 
         var id = new PackageId(name, source, version, description);
 
diff --git a/utils/pub/path_source.dart b/utils/pub/path_source.dart
index 123a308..69471f8 100644
--- a/utils/pub/path_source.dart
+++ b/utils/pub/path_source.dart
@@ -25,45 +25,77 @@
   Future<Pubspec> describe(PackageId id) {
     return defer(() {
       _validatePath(id.name, id.description);
-      return new Pubspec.load(id.name, id.description, systemCache.sources);
+      return new Pubspec.load(id.name, id.description["path"],
+          systemCache.sources);
     });
   }
 
-  Future<bool> install(PackageId id, String path) {
+  Future<bool> install(PackageId id, String destination) {
     return defer(() {
       try {
         _validatePath(id.name, id.description);
       } on FormatException catch(err) {
         return false;
       }
-      return createPackageSymlink(id.name, id.description, path);
+
+      return createPackageSymlink(id.name, id.description["path"], destination,
+          relative: id.description["relative"]);
     }).then((_) => true);
   }
 
-  void validateDescription(description, {bool fromLockFile: false}) {
+  /// Parses a path dependency. This takes in a path string and returns a map.
+  /// The "path" key will be the original path but resolves relative to the
+  /// containing path. The "relative" key will be `true` if the original path
+  /// was relative.
+  ///
+  /// A path coming from a pubspec is a simple string. From a lock file, it's
+  /// an expanded {"path": ..., "relative": ...} map.
+  dynamic parseDescription(String containingPath, description,
+                           {bool fromLockFile: false}) {
+    if (fromLockFile) {
+      if (description is! Map) {
+        throw new FormatException("The description must be a map.");
+      }
+
+      if (description["path"] is! String) {
+        throw new FormatException("The 'path' field of the description must "
+            "be a string.");
+      }
+
+      if (description["relative"] is! bool) {
+        throw new FormatException("The 'relative' field of the description "
+            "must be a boolean.");
+      }
+
+      return description;
+    }
+
     if (description is! String) {
       throw new FormatException("The description must be a path string.");
     }
+
+    // Resolve the path relative to the containing file path, and remember
+    // whether the original path was relative or absolute.
+    bool isRelative = path.isRelative(description);
+    if (path.isRelative(description)) {
+      // Can't handle relative paths coming from pubspecs that are not on the
+      // local file system.
+      assert(containingPath != null);
+
+      description = path.join(path.dirname(containingPath), description);
+    }
+
+    return {
+      "path": description,
+      "relative": isRelative
+    };
   }
 
-  /// Ensures that [dir] is a valid path. It must be an absolute path that
-  /// points to an existing directory. Throws a [FormatException] if the path
-  /// is invalid.
-  void _validatePath(String name, String dir) {
-    // Relative paths are not (currently) allowed because the user would expect
-    // them to be relative to the pubspec where the dependency appears. That in
-    // turn means that two pubspecs in different locations with the same
-    // relative path dependency could refer to two different packages. That
-    // violates pub's rule that a description should uniquely identify a
-    // package.
-    //
-    // At some point, we may want to loosen this, but it will mean tracking
-    // where a given PackageId appeared.
-    if (!path.isAbsolute(dir)) {
-      throw new FormatException(
-          "Path dependency for package '$name' must be an absolute path. "
-          "Was '$dir'.");
-    }
+  /// Ensures that [description] is a valid path description. It must be a map,
+  /// with a "path" key containing a path that points to an existing directory.
+  /// Throws a [FormatException] if the path is invalid.
+  void _validatePath(String name, description) {
+    var dir = description["path"];
 
     if (fileExists(dir)) {
       throw new FormatException(
@@ -75,4 +107,4 @@
       throw new FormatException("Could not find package '$name' at '$dir'.");
     }
   }
-}
\ No newline at end of file
+}
diff --git a/utils/pub/pubspec.dart b/utils/pub/pubspec.dart
index 779a294..55b775d 100644
--- a/utils/pub/pubspec.dart
+++ b/utils/pub/pubspec.dart
@@ -38,7 +38,8 @@
     if (!fileExists(pubspecPath)) throw new PubspecNotFoundException(name);
 
     try {
-      var pubspec = new Pubspec.parse(readTextFile(pubspecPath), sources);
+      var pubspec = new Pubspec.parse(pubspecPath, readTextFile(pubspecPath),
+          sources);
 
       if (pubspec.name == null) {
         throw new PubspecHasNoNameException(name);
@@ -69,10 +70,14 @@
   bool get isEmpty =>
     name == null && version == Version.none && dependencies.isEmpty;
 
-  // TODO(rnystrom): Make this a static method to match corelib.
-  /// Parses the pubspec whose text is [contents]. If the pubspec doesn't define
-  /// version for itself, it defaults to [Version.none].
-  factory Pubspec.parse(String contents, SourceRegistry sources) {
+  // TODO(rnystrom): Instead of allowing a null argument here, split this up
+  // into load(), parse(), and _parse() like LockFile does.
+  /// Parses the pubspec stored at [filePath] whose text is [contents]. If the
+  /// pubspec doesn't define version for itself, it defaults to [Version.none].
+  /// [filePath] may be `null` if the pubspec is not on the user's local
+  /// file system.
+  factory Pubspec.parse(String filePath, String contents,
+      SourceRegistry sources) {
     var name = null;
     var version = Version.none;
 
@@ -97,7 +102,7 @@
       version = new Version.parse(parsedPubspec['version']);
     }
 
-    var dependencies = _parseDependencies(sources,
+    var dependencies = _parseDependencies(filePath, sources,
         parsedPubspec['dependencies']);
 
     var environmentYaml = parsedPubspec['environment'];
@@ -187,7 +192,8 @@
   }
 }
 
-List<PackageRef> _parseDependencies(SourceRegistry sources, yaml) {
+List<PackageRef> _parseDependencies(String pubspecPath, SourceRegistry sources,
+    yaml) {
   var dependencies = <PackageRef>[];
 
   // Allow an empty dependencies key.
@@ -233,7 +239,8 @@
           'Dependency specification $spec should be a string or a mapping.');
     }
 
-    source.validateDescription(description, fromLockFile: false);
+    description = source.parseDescription(pubspecPath, description,
+        fromLockFile: false);
 
     dependencies.add(new PackageRef(
         name, source, versionConstraint, description));
diff --git a/utils/pub/source.dart b/utils/pub/source.dart
index 1fd9327..0568d95 100644
--- a/utils/pub/source.dart
+++ b/utils/pub/source.dart
@@ -169,13 +169,23 @@
   /// When a [Pubspec] or [LockFile] is parsed, it reads in the description for
   /// each dependency. It is up to the dependency's [Source] to determine how
   /// that should be interpreted. This will be called during parsing to validate
-  /// that the given [description] is well-formed according to this source. It
-  /// should return if the description is valid, or throw a [FormatException] if
-  /// not.
+  /// that the given [description] is well-formed according to this source, and
+  /// to give the source a chance to canonicalize the description.
+  ///
+  /// [containingPath] is the path to the local file (pubspec or lockfile)
+  /// where this description appears. It may be `null` if the description is
+  /// coming from some in-memory source (such as pulling down a pubspec from
+  /// pub.dartlang.org).
+  ///
+  /// It should return if a (possibly modified) valid description, or throw a
+  /// [FormatException] if not valid.
   ///
   /// [fromLockFile] is true when the description comes from a [LockFile], to
   /// allow the source to use lockfile-specific descriptions via [resolveId].
-  void validateDescription(description, {bool fromLockFile: false}) {}
+  dynamic parseDescription(String containingPath, description,
+                           {bool fromLockFile: false}) {
+    return description;
+  }
 
   /// Returns whether or not [description1] describes the same package as
   /// [description2] for this source. This method should be light-weight. It
@@ -199,7 +209,7 @@
   /// the resolved id.
   ///
   /// The returned [PackageId] may have a description field that's invalid
-  /// according to [validateDescription], although it must still be serializable
+  /// according to [parseDescription], although it must still be serializable
   /// to JSON and YAML. It must also be equal to [id] according to
   /// [descriptionsEqual].
   ///
diff --git a/utils/testrunner/pipeline_utils.dart b/utils/testrunner/pipeline_utils.dart
index 4362e3d..94391dc 100644
--- a/utils/testrunner/pipeline_utils.dart
+++ b/utils/testrunner/pipeline_utils.dart
@@ -61,7 +61,7 @@
 Future _processHelper(String command, List<String> args,
     [int timeout = 300, int procId = 0, Function outputMonitor]) {
   var completer = procId == 0 ? new Completer() : null;
-  log.add('Running $command ${Strings.join(args, " ")}');
+  log.add('Running $command ${args.join(" ")}');
   var timer = null;
   var stdoutHandler, stderrHandler;
   var processFuture = Process.start(command, args);
diff --git a/utils/tests/pub/install/path/absolute_symlink_test.dart b/utils/tests/pub/install/path/absolute_symlink_test.dart
new file mode 100644
index 0000000..37aec85
--- /dev/null
+++ b/utils/tests/pub/install/path/absolute_symlink_test.dart
@@ -0,0 +1,45 @@
+// Copyright (c) 2013, 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.
+
+import '../../../../../pkg/path/lib/path.dart' as path;
+
+import '../../../../pub/exit_codes.dart' as exit_codes;
+import '../../test_pub.dart';
+
+main() {
+  initConfig();
+  integration("generates a symlink with an absolute path if the dependency "
+              "path was absolute", () {
+    dir("foo", [
+      libDir("foo"),
+      libPubspec("foo", "0.0.1")
+    ]).scheduleCreate();
+
+    dir(appPath, [
+      pubspec({
+        "name": "myapp",
+        "dependencies": {
+          "foo": {"path": path.join(sandboxDir, "foo")}
+        }
+      })
+    ]).scheduleCreate();
+
+    schedulePub(args: ["install"],
+        output: new RegExp(r"Dependencies installed!$"));
+
+    dir("moved").scheduleCreate();
+
+    // Move the app but not the package. Since the symlink is absolute, it
+    // should still be able to find it.
+    scheduleRename(appPath, path.join("moved", appPath));
+
+    dir("moved", [
+      dir(packagesPath, [
+        dir("foo", [
+          file("foo.dart", 'main() => "foo";')
+        ])
+      ])
+    ]).scheduleValidate();
+  });
+}
\ No newline at end of file
diff --git a/utils/tests/pub/install/path/relative_path_test.dart b/utils/tests/pub/install/path/relative_path_test.dart
index 56fe28a..497283b 100644
--- a/utils/tests/pub/install/path/relative_path_test.dart
+++ b/utils/tests/pub/install/path/relative_path_test.dart
@@ -2,12 +2,19 @@
 // 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.
 
+import '../../../../../pkg/path/lib/path.dart' as path;
+
 import '../../../../pub/exit_codes.dart' as exit_codes;
 import '../../test_pub.dart';
 
 main() {
   initConfig();
-  integration('path dependencies cannot use relative paths', () {
+  integration("can use relative path", () {
+    dir("foo", [
+      libDir("foo"),
+      libPubspec("foo", "0.0.1")
+    ]).scheduleCreate();
+
     dir(appPath, [
       pubspec({
         "name": "myapp",
@@ -17,9 +24,49 @@
       })
     ]).scheduleCreate();
 
-    schedulePub(args: ['install'],
-        error: new RegExp("Path dependency for package 'foo' must be an "
-                          "absolute path. Was '../foo'."),
-        exitCode: exit_codes.DATA);
+    schedulePub(args: ["install"],
+        output: new RegExp(r"Dependencies installed!$"));
+
+    dir(packagesPath, [
+      dir("foo", [
+        file("foo.dart", 'main() => "foo";')
+      ])
+    ]).scheduleValidate();
   });
-}
\ No newline at end of file
+
+  integration("path is relative to containing pubspec", () {
+    dir("relative", [
+      dir("foo", [
+        libDir("foo"),
+        libPubspec("foo", "0.0.1", deps: [
+          {"path": "../bar"}
+        ])
+      ]),
+      dir("bar", [
+        libDir("bar"),
+        libPubspec("bar", "0.0.1")
+      ])
+    ]).scheduleCreate();
+
+    dir(appPath, [
+      pubspec({
+        "name": "myapp",
+        "dependencies": {
+          "foo": {"path": "../relative/foo"}
+        }
+      })
+    ]).scheduleCreate();
+
+    schedulePub(args: ["install"],
+        output: new RegExp(r"Dependencies installed!$"));
+
+    dir(packagesPath, [
+      dir("foo", [
+        file("foo.dart", 'main() => "foo";')
+      ]),
+      dir("bar", [
+        file("bar.dart", 'main() => "bar";')
+      ])
+    ]).scheduleValidate();
+  });
+}
diff --git a/utils/tests/pub/install/path/relative_symlink_test.dart b/utils/tests/pub/install/path/relative_symlink_test.dart
new file mode 100644
index 0000000..9460d7d
--- /dev/null
+++ b/utils/tests/pub/install/path/relative_symlink_test.dart
@@ -0,0 +1,54 @@
+// Copyright (c) 2013, 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.
+
+import 'dart:io';
+
+import '../../../../../pkg/path/lib/path.dart' as path;
+
+import '../../../../pub/exit_codes.dart' as exit_codes;
+import '../../test_pub.dart';
+
+main() {
+  // Pub uses NTFS junction points to create links in the packages directory.
+  // These (unlike the symlinks that are supported in Vista and later) do not
+  // support relative paths. So this test, by design, will not pass on Windows.
+  // So just skip it.
+  if (Platform.operatingSystem == "windows") return;
+
+  initConfig();
+  integration("generates a symlink with a relative path if the dependency "
+              "path was relative", () {
+    dir("foo", [
+      libDir("foo"),
+      libPubspec("foo", "0.0.1")
+    ]).scheduleCreate();
+
+    dir(appPath, [
+      pubspec({
+        "name": "myapp",
+        "dependencies": {
+          "foo": {"path": "../foo"}
+        }
+      })
+    ]).scheduleCreate();
+
+    schedulePub(args: ["install"],
+        output: new RegExp(r"Dependencies installed!$"));
+
+    dir("moved").scheduleCreate();
+
+    // Move the app and package. Since they are still next to each other, it
+    // should still be found.
+    scheduleRename("foo", path.join("moved", "foo"));
+    scheduleRename(appPath, path.join("moved", appPath));
+
+    dir("moved", [
+      dir(packagesPath, [
+        dir("foo", [
+          file("foo.dart", 'main() => "foo";')
+        ])
+      ])
+    ]).scheduleValidate();
+  });
+}
diff --git a/utils/tests/pub/lock_file_test.dart b/utils/tests/pub/lock_file_test.dart
index eeef7bf..cd57bd1 100644
--- a/utils/tests/pub/lock_file_test.dart
+++ b/utils/tests/pub/lock_file_test.dart
@@ -17,8 +17,10 @@
   final String name = 'mock';
   final bool shouldCache = false;
 
-  void validateDescription(String description, {bool fromLockFile: false}) {
+  dynamic parseDescription(String filePath, String description,
+                           {bool fromLockFile: false}) {
     if (!description.endsWith(' desc')) throw new FormatException();
+    return description;
   }
 
   String packageName(String description) {
diff --git a/utils/tests/pub/pubspec_test.dart b/utils/tests/pub/pubspec_test.dart
index f2d0b61..9813cf5 100644
--- a/utils/tests/pub/pubspec_test.dart
+++ b/utils/tests/pub/pubspec_test.dart
@@ -15,8 +15,10 @@
 class MockSource extends Source {
   final String name = "mock";
   final bool shouldCache = false;
-  void validateDescription(description, {bool fromLockFile: false}) {
+  dynamic parseDescription(String filePath, description,
+                           {bool fromLockFile: false}) {
     if (description != 'ok') throw new FormatException('Bad');
+    return description;
   }
   String packageName(description) => 'foo';
 }
@@ -29,12 +31,12 @@
       sources.register(new MockSource());
 
       expectFormatError(String pubspec) {
-        expect(() => new Pubspec.parse(pubspec, sources),
+        expect(() => new Pubspec.parse(null, pubspec, sources),
             throwsFormatException);
       }
 
       test("allows a version constraint for dependencies", () {
-        var pubspec = new Pubspec.parse('''
+        var pubspec = new Pubspec.parse(null, '''
 dependencies:
   foo:
     mock: ok
@@ -49,7 +51,7 @@
       });
 
       test("allows an empty dependencies map", () {
-        var pubspec = new Pubspec.parse('''
+        var pubspec = new Pubspec.parse(null, '''
 dependencies:
 ''', sources);
 
@@ -74,8 +76,8 @@
       });
 
       test("throws if 'homepage' doesn't have an HTTP scheme", () {
-        new Pubspec.parse('homepage: http://ok.com', sources);
-        new Pubspec.parse('homepage: https://also-ok.com', sources);
+        new Pubspec.parse(null, 'homepage: http://ok.com', sources);
+        new Pubspec.parse(null, 'homepage: https://also-ok.com', sources);
 
         expectFormatError('homepage: ftp://badscheme.com');
         expectFormatError('homepage: javascript:alert("!!!")');
@@ -89,8 +91,8 @@
       });
 
       test("throws if 'documentation' doesn't have an HTTP scheme", () {
-        new Pubspec.parse('documentation: http://ok.com', sources);
-        new Pubspec.parse('documentation: https://also-ok.com', sources);
+        new Pubspec.parse(null, 'documentation: http://ok.com', sources);
+        new Pubspec.parse(null, 'documentation: https://also-ok.com', sources);
 
         expectFormatError('documentation: ftp://badscheme.com');
         expectFormatError('documentation: javascript:alert("!!!")');
@@ -99,8 +101,8 @@
       });
 
       test("throws if 'authors' is not a string or a list of strings", () {
-        new Pubspec.parse('authors: ok fine', sources);
-        new Pubspec.parse('authors: [also, ok, fine]', sources);
+        new Pubspec.parse(null, 'authors: ok fine', sources);
+        new Pubspec.parse(null, 'authors: [also, ok, fine]', sources);
 
         expectFormatError('authors: 123');
         expectFormatError('authors: {not: {a: string}}');
@@ -108,7 +110,7 @@
       });
 
       test("throws if 'author' is not a string", () {
-        new Pubspec.parse('author: ok fine', sources);
+        new Pubspec.parse(null, 'author: ok fine', sources);
 
         expectFormatError('author: 123');
         expectFormatError('author: {not: {a: string}}');
@@ -120,7 +122,7 @@
       });
 
       test("allows comment-only files", () {
-        var pubspec = new Pubspec.parse('''
+        var pubspec = new Pubspec.parse(null, '''
 # No external dependencies yet
 # Including for completeness
 # ...and hoping the spec expands to include details about author, version, etc
@@ -132,12 +134,12 @@
 
       group("environment", () {
         test("defaults to any SDK constraint if environment is omitted", () {
-          var pubspec = new Pubspec.parse('', sources);
+          var pubspec = new Pubspec.parse(null, '', sources);
           expect(pubspec.environment.sdkVersion, equals(VersionConstraint.any));
         });
 
         test("allows an empty environment map", () {
-          var pubspec = new Pubspec.parse('''
+          var pubspec = new Pubspec.parse(null, '''
 environment:
 ''', sources);
           expect(pubspec.environment.sdkVersion, equals(VersionConstraint.any));
@@ -150,7 +152,7 @@
         });
 
         test("allows a version constraint for the sdk", () {
-          var pubspec = new Pubspec.parse('''
+          var pubspec = new Pubspec.parse(null, '''
 environment:
   sdk: ">=1.2.3 <2.3.4"
 ''', sources);