| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 library pubspec; | |
| 6 | |
| 7 import 'package:yaml/yaml.dart'; | |
| 8 import 'package:pathos/path.dart' as path; | |
| 9 | |
| 10 import 'io.dart'; | |
| 11 import 'package.dart'; | |
| 12 import 'source.dart'; | |
| 13 import 'source_registry.dart'; | |
| 14 import 'utils.dart'; | |
| 15 import 'version.dart'; | |
| 16 | |
| 17 /// The parsed and validated contents of a pubspec file. | |
| 18 class Pubspec { | |
| 19 /// This package's name. | |
| 20 final String name; | |
| 21 | |
| 22 /// This package's version. | |
| 23 final Version version; | |
| 24 | |
| 25 /// The packages this package depends on. | |
| 26 final List<PackageRef> dependencies; | |
| 27 | |
| 28 /// The packages this package depends on when it is the root package. | |
| 29 final List<PackageRef> devDependencies; | |
| 30 | |
| 31 /// The environment-related metadata. | |
| 32 final PubspecEnvironment environment; | |
| 33 | |
| 34 /// All pubspec fields. This includes the fields from which other properties | |
| 35 /// are derived. | |
| 36 final Map<String, Object> fields; | |
| 37 | |
| 38 /// Loads the pubspec for a package [name] located in [packageDir]. | |
| 39 factory Pubspec.load(String name, String packageDir, SourceRegistry sources) { | |
| 40 var pubspecPath = path.join(packageDir, 'pubspec.yaml'); | |
| 41 if (!fileExists(pubspecPath)) throw new PubspecNotFoundException(name); | |
| 42 | |
| 43 try { | |
| 44 var pubspec = new Pubspec.parse(pubspecPath, readTextFile(pubspecPath), | |
| 45 sources); | |
| 46 | |
| 47 if (pubspec.name == null) { | |
| 48 throw new PubspecHasNoNameException(name); | |
| 49 } | |
| 50 | |
| 51 if (name != null && pubspec.name != name) { | |
| 52 throw new PubspecNameMismatchException(name, pubspec.name); | |
| 53 } | |
| 54 | |
| 55 return pubspec; | |
| 56 } on FormatException catch (ex) { | |
| 57 fail('Could not parse $pubspecPath:\n${ex.message}'); | |
| 58 } | |
| 59 } | |
| 60 | |
| 61 Pubspec(this.name, this.version, this.dependencies, this.devDependencies, | |
| 62 this.environment, [Map<String, Object> fields]) | |
| 63 : this.fields = fields == null ? {} : fields; | |
| 64 | |
| 65 Pubspec.empty() | |
| 66 : name = null, | |
| 67 version = Version.none, | |
| 68 dependencies = <PackageRef>[], | |
| 69 devDependencies = <PackageRef>[], | |
| 70 environment = new PubspecEnvironment(), | |
| 71 fields = {}; | |
| 72 | |
| 73 /// Whether or not the pubspec has no contents. | |
| 74 bool get isEmpty => | |
| 75 name == null && version == Version.none && dependencies.isEmpty; | |
| 76 | |
| 77 // TODO(rnystrom): Instead of allowing a null argument here, split this up | |
| 78 // into load(), parse(), and _parse() like LockFile does. | |
| 79 /// Parses the pubspec stored at [filePath] whose text is [contents]. If the | |
| 80 /// pubspec doesn't define version for itself, it defaults to [Version.none]. | |
| 81 /// [filePath] may be `null` if the pubspec is not on the user's local | |
| 82 /// file system. | |
| 83 factory Pubspec.parse(String filePath, String contents, | |
| 84 SourceRegistry sources) { | |
| 85 var name = null; | |
| 86 var version = Version.none; | |
| 87 | |
| 88 if (contents.trim() == '') return new Pubspec.empty(); | |
| 89 | |
| 90 var parsedPubspec = loadYaml(contents); | |
| 91 if (parsedPubspec == null) return new Pubspec.empty(); | |
| 92 | |
| 93 if (parsedPubspec is! Map) { | |
| 94 throw new FormatException('The pubspec must be a YAML mapping.'); | |
| 95 } | |
| 96 | |
| 97 if (parsedPubspec.containsKey('name')) { | |
| 98 name = parsedPubspec['name']; | |
| 99 if (name is! String) { | |
| 100 throw new FormatException( | |
| 101 'The pubspec "name" field should be a string, but was "$name".'); | |
| 102 } | |
| 103 } | |
| 104 | |
| 105 if (parsedPubspec.containsKey('version')) { | |
| 106 version = new Version.parse(parsedPubspec['version']); | |
| 107 } | |
| 108 | |
| 109 var dependencies = _parseDependencies(filePath, sources, | |
| 110 parsedPubspec['dependencies']); | |
| 111 | |
| 112 var devDependencies = _parseDependencies(filePath, sources, | |
| 113 parsedPubspec['dev_dependencies']); | |
| 114 | |
| 115 // Make sure the same package doesn't appear as both a regular and dev | |
| 116 // dependency. | |
| 117 var dependencyNames = dependencies.map((dep) => dep.name).toSet(); | |
| 118 var collisions = dependencyNames.intersection( | |
| 119 devDependencies.map((dep) => dep.name).toSet()); | |
| 120 | |
| 121 if (!collisions.isEmpty) { | |
| 122 var packageNames; | |
| 123 if (collisions.length == 1) { | |
| 124 packageNames = 'Package "${collisions.first}"'; | |
| 125 } else { | |
| 126 var names = collisions.toList(); | |
| 127 names.sort(); | |
| 128 var buffer = new StringBuffer(); | |
| 129 buffer.write("Packages "); | |
| 130 for (var i = 0; i < names.length; i++) { | |
| 131 buffer.write('"'); | |
| 132 buffer.write(names[i]); | |
| 133 buffer.write('"'); | |
| 134 if (i == names.length - 2) { | |
| 135 buffer.write(", "); | |
| 136 } else if (i == names.length - 1) { | |
| 137 buffer.write(", and "); | |
| 138 } | |
| 139 } | |
| 140 | |
| 141 packageNames = buffer.toString(); | |
| 142 } | |
| 143 throw new FormatException( | |
| 144 '$packageNames cannot appear in both "dependencies" and ' | |
| 145 '"dev_dependencies".'); | |
| 146 } | |
| 147 | |
| 148 var environmentYaml = parsedPubspec['environment']; | |
| 149 var sdkConstraint = VersionConstraint.any; | |
| 150 if (environmentYaml != null) { | |
| 151 if (environmentYaml is! Map) { | |
| 152 throw new FormatException( | |
| 153 'The pubspec "environment" field should be a map, but was ' | |
| 154 '"$environmentYaml".'); | |
| 155 } | |
| 156 | |
| 157 var sdkYaml = environmentYaml['sdk']; | |
| 158 if (sdkYaml is! String) { | |
| 159 throw new FormatException( | |
| 160 'The "sdk" field of "environment" should be a string, but was ' | |
| 161 '"$sdkYaml".'); | |
| 162 } | |
| 163 | |
| 164 sdkConstraint = new VersionConstraint.parse(sdkYaml); | |
| 165 } | |
| 166 var environment = new PubspecEnvironment(sdkConstraint); | |
| 167 | |
| 168 // Even though the pub app itself doesn't use these fields, we validate | |
| 169 // them here so that users find errors early before they try to upload to | |
| 170 // the server: | |
| 171 // TODO(rnystrom): We should split this validation into separate layers: | |
| 172 // 1. Stuff that is required in any pubspec to perform any command. Things | |
| 173 // like "must have a name". That should go here. | |
| 174 // 2. Stuff that is required to upload a package. Things like "homepage | |
| 175 // must use a valid scheme". That should go elsewhere. pub upload should | |
| 176 // call it, and we should provide a separate command to show the user, | |
| 177 // and also expose it to the editor in some way. | |
| 178 | |
| 179 if (parsedPubspec.containsKey('homepage')) { | |
| 180 _validateFieldUrl(parsedPubspec['homepage'], 'homepage'); | |
| 181 } | |
| 182 if (parsedPubspec.containsKey('documentation')) { | |
| 183 _validateFieldUrl(parsedPubspec['documentation'], 'documentation'); | |
| 184 } | |
| 185 | |
| 186 if (parsedPubspec.containsKey('author') && | |
| 187 parsedPubspec['author'] is! String) { | |
| 188 throw new FormatException( | |
| 189 'The "author" field should be a string, but was ' | |
| 190 '${parsedPubspec["author"]}.'); | |
| 191 } | |
| 192 | |
| 193 if (parsedPubspec.containsKey('authors')) { | |
| 194 var authors = parsedPubspec['authors']; | |
| 195 if (authors is List) { | |
| 196 // All of the elements must be strings. | |
| 197 if (!authors.every((author) => author is String)) { | |
| 198 throw new FormatException('The "authors" field should be a string ' | |
| 199 'or a list of strings, but was "$authors".'); | |
| 200 } | |
| 201 } else if (authors is! String) { | |
| 202 throw new FormatException('The pubspec "authors" field should be a ' | |
| 203 'string or a list of strings, but was "$authors".'); | |
| 204 } | |
| 205 | |
| 206 if (parsedPubspec.containsKey('author')) { | |
| 207 throw new FormatException('A pubspec should not have both an "author" ' | |
| 208 'and an "authors" field.'); | |
| 209 } | |
| 210 } | |
| 211 | |
| 212 return new Pubspec(name, version, dependencies, devDependencies, | |
| 213 environment, parsedPubspec); | |
| 214 } | |
| 215 } | |
| 216 | |
| 217 /** | |
| 218 * Evaluates whether the given [url] for [field] is valid. | |
| 219 * | |
| 220 * Throws [FormatException] on an invalid url. | |
| 221 */ | |
| 222 void _validateFieldUrl(url, String field) { | |
| 223 if (url is! String) { | |
| 224 throw new FormatException( | |
| 225 'The "$field" field should be a string, but was "$url".'); | |
| 226 } | |
| 227 | |
| 228 var goodScheme = new RegExp(r'^https?:'); | |
| 229 if (!goodScheme.hasMatch(url)) { | |
| 230 throw new FormatException( | |
| 231 'The "$field" field should be an "http:" or "https:" URL, but ' | |
| 232 'was "$url".'); | |
| 233 } | |
| 234 } | |
| 235 | |
| 236 List<PackageRef> _parseDependencies(String pubspecPath, SourceRegistry sources, | |
| 237 yaml) { | |
| 238 var dependencies = <PackageRef>[]; | |
| 239 | |
| 240 // Allow an empty dependencies key. | |
| 241 if (yaml == null) return dependencies; | |
| 242 | |
| 243 if (yaml is! Map || yaml.keys.any((e) => e is! String)) { | |
| 244 throw new FormatException( | |
| 245 'The pubspec dependencies should be a map of package names, but ' | |
| 246 'was ${yaml}.'); | |
| 247 } | |
| 248 | |
| 249 yaml.forEach((name, spec) { | |
| 250 var description, source; | |
| 251 var versionConstraint = new VersionRange(); | |
| 252 if (spec == null) { | |
| 253 description = name; | |
| 254 source = sources.defaultSource; | |
| 255 } else if (spec is String) { | |
| 256 description = name; | |
| 257 source = sources.defaultSource; | |
| 258 versionConstraint = new VersionConstraint.parse(spec); | |
| 259 } else if (spec is Map) { | |
| 260 if (spec.containsKey('version')) { | |
| 261 versionConstraint = new VersionConstraint.parse(spec.remove('version')); | |
| 262 } | |
| 263 | |
| 264 var sourceNames = spec.keys.toList(); | |
| 265 if (sourceNames.length > 1) { | |
| 266 throw new FormatException( | |
| 267 'Dependency $name may only have one source: $sourceNames.'); | |
| 268 } | |
| 269 | |
| 270 var sourceName = only(sourceNames); | |
| 271 if (sourceName is! String) { | |
| 272 throw new FormatException( | |
| 273 'Source name $sourceName should be a string.'); | |
| 274 } | |
| 275 | |
| 276 source = sources[sourceName]; | |
| 277 description = spec[sourceName]; | |
| 278 } else { | |
| 279 throw new FormatException( | |
| 280 'Dependency specification $spec should be a string or a mapping.'); | |
| 281 } | |
| 282 | |
| 283 description = source.parseDescription(pubspecPath, description, | |
| 284 fromLockFile: false); | |
| 285 | |
| 286 dependencies.add(new PackageRef( | |
| 287 name, source, versionConstraint, description)); | |
| 288 }); | |
| 289 | |
| 290 return dependencies; | |
| 291 } | |
| 292 | |
| 293 /// The environment-related metadata in the pubspec. Corresponds to the data | |
| 294 /// under the "environment:" key in the pubspec. | |
| 295 class PubspecEnvironment { | |
| 296 /// The version constraint specifying which SDK versions this package works | |
| 297 /// with. | |
| 298 final VersionConstraint sdkVersion; | |
| 299 | |
| 300 PubspecEnvironment([VersionConstraint sdk]) | |
| 301 : sdkVersion = sdk != null ? sdk : VersionConstraint.any; | |
| 302 } | |
| OLD | NEW |