OLD | NEW |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library pubspec; | 5 library pubspec; |
6 | 6 |
7 import 'package:yaml/yaml.dart'; | 7 import 'package:yaml/yaml.dart'; |
8 import 'package:pathos/path.dart' as path; | 8 import 'package:pathos/path.dart' as path; |
9 | 9 |
10 import 'io.dart'; | 10 import 'io.dart'; |
(...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
67 version = Version.none, | 67 version = Version.none, |
68 dependencies = <PackageDep>[], | 68 dependencies = <PackageDep>[], |
69 devDependencies = <PackageDep>[], | 69 devDependencies = <PackageDep>[], |
70 environment = new PubspecEnvironment(), | 70 environment = new PubspecEnvironment(), |
71 fields = {}; | 71 fields = {}; |
72 | 72 |
73 /// Whether or not the pubspec has no contents. | 73 /// Whether or not the pubspec has no contents. |
74 bool get isEmpty => | 74 bool get isEmpty => |
75 name == null && version == Version.none && dependencies.isEmpty; | 75 name == null && version == Version.none && dependencies.isEmpty; |
76 | 76 |
| 77 /// Returns a Pubspec object for an already-parsed map representing its |
| 78 /// contents. |
| 79 /// |
| 80 /// This will validate that [contents] is a valid pubspec. |
| 81 factory Pubspec.fromMap(Map contents, SourceRegistry sources) => |
| 82 _parseMap(null, contents, sources); |
| 83 |
77 // TODO(rnystrom): Instead of allowing a null argument here, split this up | 84 // TODO(rnystrom): Instead of allowing a null argument here, split this up |
78 // into load(), parse(), and _parse() like LockFile does. | 85 // into load(), parse(), and _parse() like LockFile does. |
79 /// Parses the pubspec stored at [filePath] whose text is [contents]. If the | 86 /// 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]. | 87 /// 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 | 88 /// [filePath] may be `null` if the pubspec is not on the user's local |
82 /// file system. | 89 /// file system. |
83 factory Pubspec.parse(String filePath, String contents, | 90 factory Pubspec.parse(String filePath, String contents, |
84 SourceRegistry sources) { | 91 SourceRegistry sources) { |
85 var name = null; | |
86 var version = Version.none; | |
87 | |
88 if (contents.trim() == '') return new Pubspec.empty(); | 92 if (contents.trim() == '') return new Pubspec.empty(); |
89 | 93 |
90 var parsedPubspec = loadYaml(contents); | 94 var parsedPubspec = loadYaml(contents); |
91 if (parsedPubspec == null) return new Pubspec.empty(); | 95 if (parsedPubspec == null) return new Pubspec.empty(); |
92 | 96 |
93 if (parsedPubspec is! Map) { | 97 if (parsedPubspec is! Map) { |
94 throw new FormatException('The pubspec must be a YAML mapping.'); | 98 throw new FormatException('The pubspec must be a YAML mapping.'); |
95 } | 99 } |
96 | 100 |
97 if (parsedPubspec.containsKey('name')) { | 101 return _parseMap(filePath, parsedPubspec, sources); |
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 } | 102 } |
215 } | 103 } |
216 | 104 |
217 /// Evaluates whether the given [url] for [field] is valid. | 105 /// Evaluates whether the given [url] for [field] is valid. |
218 /// | 106 /// |
219 /// Throws [FormatException] on an invalid url. | 107 /// Throws [FormatException] on an invalid url. |
220 void _validateFieldUrl(url, String field) { | 108 void _validateFieldUrl(url, String field) { |
221 if (url is! String) { | 109 if (url is! String) { |
222 throw new FormatException( | 110 throw new FormatException( |
223 'The "$field" field should be a string, but was "$url".'); | 111 'The "$field" field should be a string, but was "$url".'); |
224 } | 112 } |
225 | 113 |
226 var goodScheme = new RegExp(r'^https?:'); | 114 var goodScheme = new RegExp(r'^https?:'); |
227 if (!goodScheme.hasMatch(url)) { | 115 if (!goodScheme.hasMatch(url)) { |
228 throw new FormatException( | 116 throw new FormatException( |
229 'The "$field" field should be an "http:" or "https:" URL, but ' | 117 'The "$field" field should be an "http:" or "https:" URL, but ' |
230 'was "$url".'); | 118 'was "$url".'); |
231 } | 119 } |
232 } | 120 } |
233 | 121 |
| 122 Pubspec _parseMap(String filePath, Map map, SourceRegistry sources) { |
| 123 var name = null; |
| 124 var version = Version.none; |
| 125 |
| 126 if (map.containsKey('name')) { |
| 127 name = map['name']; |
| 128 if (name is! String) { |
| 129 throw new FormatException( |
| 130 'The pubspec "name" field should be a string, but was "$name".'); |
| 131 } |
| 132 } |
| 133 |
| 134 if (map.containsKey('version')) { |
| 135 version = new Version.parse(map['version']); |
| 136 } |
| 137 |
| 138 var dependencies = _parseDependencies(filePath, sources, |
| 139 map['dependencies']); |
| 140 |
| 141 var devDependencies = _parseDependencies(filePath, sources, |
| 142 map['dev_dependencies']); |
| 143 |
| 144 // Make sure the same package doesn't appear as both a regular and dev |
| 145 // dependency. |
| 146 var dependencyNames = dependencies.map((dep) => dep.name).toSet(); |
| 147 var collisions = dependencyNames.intersection( |
| 148 devDependencies.map((dep) => dep.name).toSet()); |
| 149 |
| 150 if (!collisions.isEmpty) { |
| 151 var packageNames; |
| 152 if (collisions.length == 1) { |
| 153 packageNames = 'Package "${collisions.first}"'; |
| 154 } else { |
| 155 var names = collisions.toList(); |
| 156 names.sort(); |
| 157 var buffer = new StringBuffer(); |
| 158 buffer.write("Packages "); |
| 159 for (var i = 0; i < names.length; i++) { |
| 160 buffer.write('"'); |
| 161 buffer.write(names[i]); |
| 162 buffer.write('"'); |
| 163 if (i == names.length - 2) { |
| 164 buffer.write(", "); |
| 165 } else if (i == names.length - 1) { |
| 166 buffer.write(", and "); |
| 167 } |
| 168 } |
| 169 |
| 170 packageNames = buffer.toString(); |
| 171 } |
| 172 throw new FormatException( |
| 173 '$packageNames cannot appear in both "dependencies" and ' |
| 174 '"dev_dependencies".'); |
| 175 } |
| 176 |
| 177 var environmentYaml = map['environment']; |
| 178 var sdkConstraint = VersionConstraint.any; |
| 179 if (environmentYaml != null) { |
| 180 if (environmentYaml is! Map) { |
| 181 throw new FormatException( |
| 182 'The pubspec "environment" field should be a map, but was ' |
| 183 '"$environmentYaml".'); |
| 184 } |
| 185 |
| 186 var sdkYaml = environmentYaml['sdk']; |
| 187 if (sdkYaml is! String) { |
| 188 throw new FormatException( |
| 189 'The "sdk" field of "environment" should be a string, but was ' |
| 190 '"$sdkYaml".'); |
| 191 } |
| 192 |
| 193 sdkConstraint = new VersionConstraint.parse(sdkYaml); |
| 194 } |
| 195 var environment = new PubspecEnvironment(sdkConstraint); |
| 196 |
| 197 // Even though the pub app itself doesn't use these fields, we validate |
| 198 // them here so that users find errors early before they try to upload to |
| 199 // the server: |
| 200 // TODO(rnystrom): We should split this validation into separate layers: |
| 201 // 1. Stuff that is required in any pubspec to perform any command. Things |
| 202 // like "must have a name". That should go here. |
| 203 // 2. Stuff that is required to upload a package. Things like "homepage |
| 204 // must use a valid scheme". That should go elsewhere. pub upload should |
| 205 // call it, and we should provide a separate command to show the user, |
| 206 // and also expose it to the editor in some way. |
| 207 |
| 208 if (map.containsKey('homepage')) { |
| 209 _validateFieldUrl(map['homepage'], 'homepage'); |
| 210 } |
| 211 if (map.containsKey('documentation')) { |
| 212 _validateFieldUrl(map['documentation'], 'documentation'); |
| 213 } |
| 214 |
| 215 if (map.containsKey('author') && map['author'] is! String) { |
| 216 throw new FormatException( |
| 217 'The "author" field should be a string, but was ' |
| 218 '${map["author"]}.'); |
| 219 } |
| 220 |
| 221 if (map.containsKey('authors')) { |
| 222 var authors = map['authors']; |
| 223 if (authors is List) { |
| 224 // All of the elements must be strings. |
| 225 if (!authors.every((author) => author is String)) { |
| 226 throw new FormatException('The "authors" field should be a string ' |
| 227 'or a list of strings, but was "$authors".'); |
| 228 } |
| 229 } else if (authors is! String) { |
| 230 throw new FormatException('The pubspec "authors" field should be a ' |
| 231 'string or a list of strings, but was "$authors".'); |
| 232 } |
| 233 |
| 234 if (map.containsKey('author')) { |
| 235 throw new FormatException('A pubspec should not have both an "author" ' |
| 236 'and an "authors" field.'); |
| 237 } |
| 238 } |
| 239 |
| 240 return new Pubspec(name, version, dependencies, devDependencies, |
| 241 environment, map); |
| 242 } |
| 243 |
234 List<PackageDep> _parseDependencies(String pubspecPath, SourceRegistry sources, | 244 List<PackageDep> _parseDependencies(String pubspecPath, SourceRegistry sources, |
235 yaml) { | 245 yaml) { |
236 var dependencies = <PackageDep>[]; | 246 var dependencies = <PackageDep>[]; |
237 | 247 |
238 // Allow an empty dependencies key. | 248 // Allow an empty dependencies key. |
239 if (yaml == null) return dependencies; | 249 if (yaml == null) return dependencies; |
240 | 250 |
241 if (yaml is! Map || yaml.keys.any((e) => e is! String)) { | 251 if (yaml is! Map || yaml.keys.any((e) => e is! String)) { |
242 throw new FormatException( | 252 throw new FormatException( |
243 'The pubspec dependencies should be a map of package names, but ' | 253 'The pubspec dependencies should be a map of package names, but ' |
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
296 /// The environment-related metadata in the pubspec. Corresponds to the data | 306 /// The environment-related metadata in the pubspec. Corresponds to the data |
297 /// under the "environment:" key in the pubspec. | 307 /// under the "environment:" key in the pubspec. |
298 class PubspecEnvironment { | 308 class PubspecEnvironment { |
299 /// The version constraint specifying which SDK versions this package works | 309 /// The version constraint specifying which SDK versions this package works |
300 /// with. | 310 /// with. |
301 final VersionConstraint sdkVersion; | 311 final VersionConstraint sdkVersion; |
302 | 312 |
303 PubspecEnvironment([VersionConstraint sdk]) | 313 PubspecEnvironment([VersionConstraint sdk]) |
304 : sdkVersion = sdk != null ? sdk : VersionConstraint.any; | 314 : sdkVersion = sdk != null ? sdk : VersionConstraint.any; |
305 } | 315 } |
OLD | NEW |