]>
Commit | Line | Data |
---|---|---|
ee5e24ff | 1 | use std::env; |
a9fd1c2c | 2 | use std::fs::{self, File}; |
ee5e24ff | 3 | use std::iter::repeat; |
f7d213e7 | 4 | use std::time::Duration; |
9fba127e | 5 | |
923f21c3 | 6 | use curl::easy::{Easy, SslOpt}; |
9fba127e AC |
7 | use git2; |
8 | use registry::{Registry, NewCrate, NewCrateDependency}; | |
9 | ||
134edb20 JE |
10 | use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET}; |
11 | ||
ae2b5980 | 12 | use version; |
9fba127e | 13 | use core::source::Source; |
58ddb28a | 14 | use core::{Package, SourceId, Workspace}; |
3b5994e7 | 15 | use core::dependency::Kind; |
9fba127e AC |
16 | use core::manifest::ManifestMetadata; |
17 | use ops; | |
0d9eac6e | 18 | use sources::{RegistrySource}; |
30cc3784 | 19 | use util::config::{self, Config}; |
8524efb0 | 20 | use util::paths; |
c7de4859 | 21 | use util::ToUrl; |
c7de4859 | 22 | use util::errors::{CargoError, CargoResult, CargoResultExt}; |
09d62a65 | 23 | use util::important_paths::find_root_manifest_for_wd; |
9fba127e AC |
24 | |
25 | pub struct RegistryConfig { | |
26 | pub index: Option<String>, | |
27 | pub token: Option<String>, | |
28 | } | |
29 | ||
088d14ad AC |
30 | pub struct PublishOpts<'cfg> { |
31 | pub config: &'cfg Config, | |
32 | pub token: Option<String>, | |
33 | pub index: Option<String>, | |
34 | pub verify: bool, | |
35 | pub allow_dirty: bool, | |
f8ee283a | 36 | pub jobs: Option<u32>, |
1c593879 | 37 | pub target: Option<&'cfg str>, |
c05a5b4a | 38 | pub dry_run: bool, |
088d14ad AC |
39 | } |
40 | ||
58ddb28a | 41 | pub fn publish(ws: &Workspace, opts: &PublishOpts) -> CargoResult<()> { |
82655b46 | 42 | let pkg = ws.current()?; |
9fba127e | 43 | |
d28f8bb0 JL |
44 | if !pkg.publish() { |
45 | bail!("some crates cannot be published.\n\ | |
46 | `{}` is marked as unpublishable", pkg.name()); | |
47 | } | |
23591fe5 | 48 | if !pkg.manifest().patch().is_empty() { |
61a3c68b AC |
49 | bail!("published crates cannot contain [patch] sections"); |
50 | } | |
d28f8bb0 | 51 | |
82655b46 | 52 | let (mut registry, reg_id) = registry(opts.config, |
61a3c68b AC |
53 | opts.token.clone(), |
54 | opts.index.clone())?; | |
c5611a32 | 55 | verify_dependencies(pkg, ®_id)?; |
9fba127e | 56 | |
5db1316a HW |
57 | // Prepare a tarball, with a non-surpressable warning if metadata |
58 | // is missing since this is being put online. | |
82655b46 | 59 | let tarball = ops::package(ws, &ops::PackageOpts { |
088d14ad AC |
60 | config: opts.config, |
61 | verify: opts.verify, | |
62 | list: false, | |
63 | check_metadata: true, | |
64 | allow_dirty: opts.allow_dirty, | |
1c593879 | 65 | target: opts.target, |
f8ee283a | 66 | jobs: opts.jobs, |
82655b46 | 67 | })?.unwrap(); |
9fba127e AC |
68 | |
69 | // Upload said tarball to the specified destination | |
82655b46 | 70 | opts.config.shell().status("Uploading", pkg.package_id().to_string())?; |
c5611a32 | 71 | transmit(opts.config, pkg, tarball.file(), &mut registry, opts.dry_run)?; |
9fba127e AC |
72 | |
73 | Ok(()) | |
74 | } | |
75 | ||
76 | fn verify_dependencies(pkg: &Package, registry_src: &SourceId) | |
77 | -> CargoResult<()> { | |
7a2facba AC |
78 | for dep in pkg.dependencies().iter() { |
79 | if dep.source_id().is_path() { | |
0d7b5cc4 | 80 | if !dep.specified_req() { |
7ab18e3a AC |
81 | bail!("all path dependencies must have a version specified \ |
82 | when publishing.\ndependency `{}` does not specify \ | |
83 | a version", dep.name()) | |
9fba127e | 84 | } |
7a2facba | 85 | } else if dep.source_id() != registry_src { |
450da94e BM |
86 | bail!("crates cannot be published to crates.io with dependencies sourced from \ |
87 | a repository\neither publish `{}` as its own crate on crates.io and \ | |
88 | specify a crates.io version as a dependency or pull it into this \ | |
89 | repository and specify it with a path and version\n(crate `{}` has \ | |
90 | repository path `{}`)", dep.name(), dep.name(), dep.source_id()); | |
9fba127e AC |
91 | } |
92 | } | |
93 | Ok(()) | |
94 | } | |
95 | ||
c05a5b4a WM |
96 | fn transmit(config: &Config, |
97 | pkg: &Package, | |
98 | tarball: &File, | |
99 | registry: &mut Registry, | |
100 | dry_run: bool) -> CargoResult<()> { | |
7a2facba | 101 | let deps = pkg.dependencies().iter().map(|dep| { |
9fba127e AC |
102 | NewCrateDependency { |
103 | optional: dep.is_optional(), | |
104 | default_features: dep.uses_default_features(), | |
7a2facba AC |
105 | name: dep.name().to_string(), |
106 | features: dep.features().to_vec(), | |
107 | version_req: dep.version_req().to_string(), | |
f5d786e0 | 108 | target: dep.platform().map(|s| s.to_string()), |
7a2facba | 109 | kind: match dep.kind() { |
3b5994e7 AC |
110 | Kind::Normal => "normal", |
111 | Kind::Build => "build", | |
112 | Kind::Development => "dev", | |
113 | }.to_string(), | |
9fba127e AC |
114 | } |
115 | }).collect::<Vec<NewCrateDependency>>(); | |
7a2facba | 116 | let manifest = pkg.manifest(); |
9fba127e AC |
117 | let ManifestMetadata { |
118 | ref authors, ref description, ref homepage, ref documentation, | |
5acb5f56 | 119 | ref keywords, ref readme, ref repository, ref license, ref license_file, |
f5f4c417 | 120 | ref categories, ref badges, |
7a2facba | 121 | } = *manifest.metadata(); |
9fba127e | 122 | let readme = match *readme { |
82655b46 | 123 | Some(ref readme) => Some(paths::read(&pkg.root().join(readme))?), |
9fba127e AC |
124 | None => None, |
125 | }; | |
c5611a32 AB |
126 | if let Some(ref file) = *license_file { |
127 | if fs::metadata(&pkg.root().join(file)).is_err() { | |
128 | bail!("the license file `{}` does not exist", file) | |
5acb5f56 | 129 | } |
5acb5f56 | 130 | } |
c05a5b4a WM |
131 | |
132 | // Do not upload if performing a dry run | |
133 | if dry_run { | |
82655b46 | 134 | config.shell().warn("aborting upload due to dry run")?; |
c05a5b4a WM |
135 | return Ok(()); |
136 | } | |
137 | ||
154cc0aa | 138 | let publish = registry.publish(&NewCrate { |
7a2facba AC |
139 | name: pkg.name().to_string(), |
140 | vers: pkg.version().to_string(), | |
9fba127e | 141 | deps: deps, |
7a2facba | 142 | features: pkg.summary().features().clone(), |
9fba127e AC |
143 | authors: authors.clone(), |
144 | description: description.clone(), | |
145 | homepage: homepage.clone(), | |
146 | documentation: documentation.clone(), | |
147 | keywords: keywords.clone(), | |
0f01d9bd | 148 | categories: categories.clone(), |
9fba127e AC |
149 | readme: readme, |
150 | repository: repository.clone(), | |
151 | license: license.clone(), | |
5acb5f56 | 152 | license_file: license_file.clone(), |
f5f4c417 | 153 | badges: badges.clone(), |
154cc0aa CNG |
154 | }, tarball); |
155 | ||
156 | match publish { | |
f697b8c6 CNG |
157 | Ok(warnings) => { |
158 | if !warnings.invalid_categories.is_empty() { | |
154cc0aa CNG |
159 | let msg = format!("\ |
160 | the following are not valid category slugs and were \ | |
161 | ignored: {}. Please see https://crates.io/category_slugs \ | |
162 | for the list of all category slugs. \ | |
f697b8c6 | 163 | ", warnings.invalid_categories.join(", ")); |
154cc0aa CNG |
164 | config.shell().warn(&msg)?; |
165 | } | |
f5f4c417 JG |
166 | |
167 | if !warnings.invalid_badges.is_empty() { | |
168 | let msg = format!("\ | |
169 | the following are not valid badges and were ignored: {}. \ | |
170 | Either the badge type specified is unknown or a required \ | |
171 | attribute is missing. Please see \ | |
172 | http://doc.crates.io/manifest.html#package-metadata \ | |
173 | for valid badge types and their required attributes.", | |
174 | warnings.invalid_badges.join(", ")); | |
175 | config.shell().warn(&msg)?; | |
176 | } | |
177 | ||
154cc0aa CNG |
178 | Ok(()) |
179 | }, | |
c7de4859 | 180 | Err(e) => Err(e.into()), |
154cc0aa | 181 | } |
9fba127e AC |
182 | } |
183 | ||
aa17b61e ED |
184 | pub fn registry_configuration(config: &Config) -> CargoResult<RegistryConfig> { |
185 | let index = config.get_string("registry.index")?.map(|p| p.val); | |
186 | let token = config.get_string("registry.token")?.map(|p| p.val); | |
187 | Ok(RegistryConfig { index: index, token: token }) | |
9fba127e AC |
188 | } |
189 | ||
5d0cb3f2 | 190 | pub fn registry(config: &Config, |
9fba127e AC |
191 | token: Option<String>, |
192 | index: Option<String>) -> CargoResult<(Registry, SourceId)> { | |
193 | // Parse all configuration options | |
aa17b61e ED |
194 | let RegistryConfig { |
195 | token: token_config, | |
196 | index: _index_config, | |
197 | } = registry_configuration(config)?; | |
198 | let token = token.or(token_config); | |
8214bb95 | 199 | let sid = match index { |
dc7422b6 | 200 | Some(index) => SourceId::for_registry(&index.to_url()?)?, |
82655b46 | 201 | None => SourceId::crates_io(config)?, |
8214bb95 | 202 | }; |
9fba127e | 203 | let api_host = { |
f1e26ed3 | 204 | let mut src = RegistrySource::remote(&sid, config); |
e95044e3 | 205 | src.update().chain_err(|| { |
c7de4859 | 206 | format!("failed to update {}", sid) |
82655b46 SG |
207 | })?; |
208 | (src.config()?).unwrap().api | |
9fba127e | 209 | }; |
82655b46 | 210 | let handle = http_handle(config)?; |
9fba127e AC |
211 | Ok((Registry::new_handle(api_host, token, handle), sid)) |
212 | } | |
213 | ||
214 | /// Create a new HTTP handle with appropriate global configuration for cargo. | |
f7d213e7 | 215 | pub fn http_handle(config: &Config) -> CargoResult<Easy> { |
a504f480 AC |
216 | if !config.network_allowed() { |
217 | bail!("attempting to make an HTTP request, but --frozen was \ | |
218 | specified") | |
219 | } | |
220 | ||
923c2f2d AC |
221 | // The timeout option for libcurl by default times out the entire transfer, |
222 | // but we probably don't want this. Instead we only set timeouts for the | |
223 | // connect phase as well as a "low speed" timeout so if we don't receive | |
224 | // many bytes in a large-ish period of time then we time out. | |
f7d213e7 | 225 | let mut handle = Easy::new(); |
82655b46 SG |
226 | handle.connect_timeout(Duration::new(30, 0))?; |
227 | handle.low_speed_limit(10 /* bytes per second */)?; | |
228 | handle.low_speed_time(Duration::new(30, 0))?; | |
775c900e | 229 | handle.useragent(&version().to_string())?; |
82655b46 SG |
230 | if let Some(proxy) = http_proxy(config)? { |
231 | handle.proxy(&proxy)?; | |
f7d213e7 | 232 | } |
82655b46 SG |
233 | if let Some(cainfo) = config.get_path("http.cainfo")? { |
234 | handle.cainfo(&cainfo.val)?; | |
8e8a2924 | 235 | } |
923f21c3 AC |
236 | if let Some(check) = config.get_bool("http.check-revoke")? { |
237 | handle.ssl_options(SslOpt::new().no_revoke(!check.val))?; | |
238 | } | |
82655b46 SG |
239 | if let Some(timeout) = http_timeout(config)? { |
240 | handle.connect_timeout(Duration::new(timeout as u64, 0))?; | |
241 | handle.low_speed_time(Duration::new(timeout as u64, 0))?; | |
f7d213e7 | 242 | } |
0602940f | 243 | Ok(handle) |
9fba127e AC |
244 | } |
245 | ||
8eb28ad6 | 246 | /// Find an explicit HTTP proxy if one is available. |
9fba127e | 247 | /// |
8eb28ad6 | 248 | /// Favor cargo's `http.proxy`, then git's `http.proxy`. Proxies specified |
249 | /// via environment variables are picked up by libcurl. | |
250 | fn http_proxy(config: &Config) -> CargoResult<Option<String>> { | |
c5611a32 AB |
251 | if let Some(s) = config.get_string("http.proxy")? { |
252 | return Ok(Some(s.val)) | |
9fba127e | 253 | } |
c5611a32 AB |
254 | if let Ok(cfg) = git2::Config::open_default() { |
255 | if let Ok(s) = cfg.get_str("http.proxy") { | |
256 | return Ok(Some(s.to_string())) | |
9fba127e | 257 | } |
9fba127e | 258 | } |
8eb28ad6 | 259 | Ok(None) |
260 | } | |
261 | ||
262 | /// Determine if an http proxy exists. | |
263 | /// | |
264 | /// Checks the following for existence, in order: | |
265 | /// | |
266 | /// * cargo's `http.proxy` | |
267 | /// * git's `http.proxy` | |
23591fe5 LL |
268 | /// * `http_proxy` env var |
269 | /// * `HTTP_PROXY` env var | |
270 | /// * `https_proxy` env var | |
271 | /// * `HTTPS_PROXY` env var | |
8eb28ad6 | 272 | pub fn http_proxy_exists(config: &Config) -> CargoResult<bool> { |
82655b46 | 273 | if http_proxy(config)?.is_some() { |
8eb28ad6 | 274 | Ok(true) |
275 | } else { | |
276 | Ok(["http_proxy", "HTTP_PROXY", | |
277 | "https_proxy", "HTTPS_PROXY"].iter().any(|v| env::var(v).is_ok())) | |
278 | } | |
9fba127e AC |
279 | } |
280 | ||
0602940f | 281 | pub fn http_timeout(config: &Config) -> CargoResult<Option<i64>> { |
c5611a32 AB |
282 | if let Some(s) = config.get_i64("http.timeout")? { |
283 | return Ok(Some(s.val)) | |
0602940f | 284 | } |
1384050e | 285 | Ok(env::var("HTTP_TIMEOUT").ok().and_then(|s| s.parse().ok())) |
0602940f AC |
286 | } |
287 | ||
aa17b61e | 288 | pub fn registry_login(config: &Config, token: String) -> CargoResult<()> { |
23591fe5 | 289 | let RegistryConfig { token: old_token, .. } = registry_configuration(config)?; |
30cc3784 ED |
290 | if let Some(old_token) = old_token { |
291 | if old_token == token { | |
292 | return Ok(()); | |
293 | } | |
9fba127e | 294 | } |
9fba127e | 295 | |
aa17b61e | 296 | config::save_credentials(config, token) |
9fba127e AC |
297 | } |
298 | ||
a3538e25 AC |
299 | pub struct OwnersOptions { |
300 | pub krate: Option<String>, | |
301 | pub token: Option<String>, | |
302 | pub index: Option<String>, | |
303 | pub to_add: Option<Vec<String>>, | |
304 | pub to_remove: Option<Vec<String>>, | |
305 | pub list: bool, | |
306 | } | |
307 | ||
5d0cb3f2 | 308 | pub fn modify_owners(config: &Config, opts: &OwnersOptions) -> CargoResult<()> { |
a3538e25 AC |
309 | let name = match opts.krate { |
310 | Some(ref name) => name.clone(), | |
9fba127e | 311 | None => { |
82655b46 SG |
312 | let manifest_path = find_root_manifest_for_wd(None, config.cwd())?; |
313 | let pkg = Package::for_path(&manifest_path, config)?; | |
7a2facba | 314 | pkg.name().to_string() |
9fba127e AC |
315 | } |
316 | }; | |
317 | ||
82655b46 SG |
318 | let (mut registry, _) = registry(config, opts.token.clone(), |
319 | opts.index.clone())?; | |
9fba127e | 320 | |
c5611a32 AB |
321 | if let Some(ref v) = opts.to_add { |
322 | let v = v.iter().map(|s| &s[..]).collect::<Vec<_>>(); | |
323 | config.shell().status("Owner", format!("adding {:?} to crate {}", | |
324 | v, name))?; | |
325 | registry.add_owners(&name, &v).map_err(|e| { | |
c7de4859 | 326 | CargoError::from(format!("failed to add owners to crate {}: {}", name, e)) |
c5611a32 | 327 | })?; |
9fba127e AC |
328 | } |
329 | ||
c5611a32 AB |
330 | if let Some(ref v) = opts.to_remove { |
331 | let v = v.iter().map(|s| &s[..]).collect::<Vec<_>>(); | |
332 | config.shell().status("Owner", format!("removing {:?} from crate {}", | |
333 | v, name))?; | |
334 | registry.remove_owners(&name, &v).map_err(|e| { | |
c7de4859 | 335 | CargoError::from(format!("failed to remove owners from crate {}: {}", name, e)) |
c5611a32 | 336 | })?; |
9fba127e AC |
337 | } |
338 | ||
a3538e25 | 339 | if opts.list { |
82655b46 | 340 | let owners = registry.list_owners(&name).map_err(|e| { |
c7de4859 | 341 | CargoError::from(format!("failed to list owners of crate {}: {}", name, e)) |
82655b46 | 342 | })?; |
a3538e25 AC |
343 | for owner in owners.iter() { |
344 | print!("{}", owner.login); | |
345 | match (owner.name.as_ref(), owner.email.as_ref()) { | |
346 | (Some(name), Some(email)) => println!(" ({} <{}>)", name, email), | |
347 | (Some(s), None) | | |
348 | (None, Some(s)) => println!(" ({})", s), | |
349 | (None, None) => println!(""), | |
350 | } | |
351 | } | |
352 | } | |
353 | ||
9fba127e AC |
354 | Ok(()) |
355 | } | |
356 | ||
5d0cb3f2 | 357 | pub fn yank(config: &Config, |
9fba127e AC |
358 | krate: Option<String>, |
359 | version: Option<String>, | |
360 | token: Option<String>, | |
361 | index: Option<String>, | |
362 | undo: bool) -> CargoResult<()> { | |
363 | let name = match krate { | |
364 | Some(name) => name, | |
365 | None => { | |
82655b46 SG |
366 | let manifest_path = find_root_manifest_for_wd(None, config.cwd())?; |
367 | let pkg = Package::for_path(&manifest_path, config)?; | |
7a2facba | 368 | pkg.name().to_string() |
9fba127e AC |
369 | } |
370 | }; | |
371 | let version = match version { | |
372 | Some(v) => v, | |
7ab18e3a | 373 | None => bail!("a version must be specified to yank") |
9fba127e AC |
374 | }; |
375 | ||
82655b46 | 376 | let (mut registry, _) = registry(config, token, index)?; |
9fba127e AC |
377 | |
378 | if undo { | |
82655b46 SG |
379 | config.shell().status("Unyank", format!("{}:{}", name, version))?; |
380 | registry.unyank(&name, &version).map_err(|e| { | |
c7de4859 | 381 | CargoError::from(format!("failed to undo a yank: {}", e)) |
82655b46 | 382 | })?; |
9fba127e | 383 | } else { |
82655b46 SG |
384 | config.shell().status("Yank", format!("{}:{}", name, version))?; |
385 | registry.yank(&name, &version).map_err(|e| { | |
c7de4859 | 386 | CargoError::from(format!("failed to yank: {}", e)) |
82655b46 | 387 | })?; |
9fba127e AC |
388 | } |
389 | ||
390 | Ok(()) | |
391 | } | |
0c25226b | 392 | |
53c9374c JE |
393 | pub fn search(query: &str, |
394 | config: &Config, | |
395 | index: Option<String>, | |
396 | limit: u8) -> CargoResult<()> { | |
55321111 | 397 | fn truncate_with_ellipsis(s: &str, max_length: usize) -> String { |
0c25226b JB |
398 | if s.len() < max_length { |
399 | s.to_string() | |
400 | } else { | |
55321111 | 401 | format!("{}…", &s[..max_length - 1]) |
0c25226b JB |
402 | } |
403 | } | |
404 | ||
82655b46 SG |
405 | let (mut registry, _) = registry(config, None, index)?; |
406 | let (crates, total_crates) = registry.search(query, limit).map_err(|e| { | |
c7de4859 | 407 | CargoError::from(format!("failed to retrieve search results from the registry: {}", e)) |
82655b46 | 408 | })?; |
0c25226b JB |
409 | |
410 | let list_items = crates.iter() | |
411 | .map(|krate| ( | |
94aa4ae2 | 412 | format!("{} = \"{}\"", krate.name, krate.max_version), |
0c25226b | 413 | krate.description.as_ref().map(|desc| |
25e537aa | 414 | truncate_with_ellipsis(&desc.replace("\n", " "), 128)) |
0c25226b JB |
415 | )) |
416 | .collect::<Vec<_>>(); | |
417 | let description_margin = list_items.iter() | |
418 | .map(|&(ref left, _)| left.len() + 4) | |
419 | .max() | |
420 | .unwrap_or(0); | |
421 | ||
422 | for (name, description) in list_items.into_iter() { | |
423 | let line = match description { | |
424 | Some(desc) => { | |
31534136 AC |
425 | let space = repeat(' ').take(description_margin - name.len()) |
426 | .collect::<String>(); | |
8af9e95d | 427 | name + &space + "# " + &desc |
0c25226b JB |
428 | } |
429 | None => name | |
430 | }; | |
deda31fd | 431 | println!("{}", line); |
0c25226b JB |
432 | } |
433 | ||
134edb20 | 434 | let search_max_limit = 100; |
23591fe5 | 435 | if total_crates > u32::from(limit) && limit < search_max_limit { |
deda31fd | 436 | println!("... and {} crates more (use --limit N to see more)", |
23591fe5 LL |
437 | total_crates - u32::from(limit)); |
438 | } else if total_crates > u32::from(limit) && limit >= search_max_limit { | |
deda31fd | 439 | println!("... and {} crates more (go to http://crates.io/search?q={} to see more)", |
23591fe5 | 440 | total_crates - u32::from(limit), |
deda31fd | 441 | percent_encode(query.as_bytes(), QUERY_ENCODE_SET)); |
134edb20 JE |
442 | } |
443 | ||
0c25226b JB |
444 | Ok(()) |
445 | } |