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 |