]> git.proxmox.com Git - cargo.git/blob - src/cargo/ops/registry.rs
Print environment note for json format, too.
[cargo.git] / src / cargo / ops / registry.rs
1 use std::collections::{BTreeMap, HashSet};
2 use std::fs::File;
3 use std::io::{self, BufRead};
4 use std::iter::repeat;
5 use std::path::PathBuf;
6 use std::str;
7 use std::time::Duration;
8 use std::{cmp, env};
9
10 use anyhow::{bail, format_err};
11 use cargo_util::paths;
12 use crates_io::{self, NewCrate, NewCrateDependency, Registry};
13 use curl::easy::{Easy, InfoType, SslOpt, SslVersion};
14 use log::{log, Level};
15 use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
16
17 use crate::core::dependency::DepKind;
18 use crate::core::manifest::ManifestMetadata;
19 use crate::core::resolver::CliFeatures;
20 use crate::core::source::Source;
21 use crate::core::{Package, SourceId, Workspace};
22 use crate::ops;
23 use crate::sources::{RegistrySource, SourceConfigMap, CRATES_IO_REGISTRY};
24 use crate::util::config::{self, Config, SslVersionConfig, SslVersionConfigRange};
25 use crate::util::errors::{CargoResult, CargoResultExt};
26 use crate::util::important_paths::find_root_manifest_for_wd;
27 use crate::util::validate_package_name;
28 use crate::util::IntoUrl;
29 use crate::{drop_print, drop_println, version};
30
31 mod auth;
32
33 /// Registry settings loaded from config files.
34 ///
35 /// This is loaded based on the `--registry` flag and the config settings.
36 #[derive(Debug)]
37 pub struct RegistryConfig {
38 /// The index URL. If `None`, use crates.io.
39 pub index: Option<String>,
40 /// The authentication token.
41 pub token: Option<String>,
42 /// Process used for fetching a token.
43 pub credential_process: Option<(PathBuf, Vec<String>)>,
44 }
45
46 pub struct PublishOpts<'cfg> {
47 pub config: &'cfg Config,
48 pub token: Option<String>,
49 pub index: Option<String>,
50 pub verify: bool,
51 pub allow_dirty: bool,
52 pub jobs: Option<u32>,
53 pub targets: Vec<String>,
54 pub dry_run: bool,
55 pub registry: Option<String>,
56 pub cli_features: CliFeatures,
57 }
58
59 pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
60 let pkg = ws.current()?;
61 let mut publish_registry = opts.registry.clone();
62
63 if let Some(ref allowed_registries) = *pkg.publish() {
64 if publish_registry.is_none() && allowed_registries.len() == 1 {
65 // If there is only one allowed registry, push to that one directly,
66 // even though there is no registry specified in the command.
67 let default_registry = &allowed_registries[0];
68 if default_registry != CRATES_IO_REGISTRY {
69 // Don't change the registry for crates.io and don't warn the user.
70 // crates.io will be defaulted even without this.
71 opts.config.shell().note(&format!(
72 "Found `{}` as only allowed registry. Publishing to it automatically.",
73 default_registry
74 ))?;
75 publish_registry = Some(default_registry.clone());
76 }
77 }
78
79 let reg_name = publish_registry
80 .clone()
81 .unwrap_or_else(|| CRATES_IO_REGISTRY.to_string());
82 if !allowed_registries.contains(&reg_name) {
83 bail!(
84 "`{}` cannot be published.\n\
85 The registry `{}` is not listed in the `publish` value in Cargo.toml.",
86 pkg.name(),
87 reg_name
88 );
89 }
90 }
91
92 let (mut registry, _reg_cfg, reg_id) = registry(
93 opts.config,
94 opts.token.clone(),
95 opts.index.clone(),
96 publish_registry,
97 true,
98 !opts.dry_run,
99 )?;
100 verify_dependencies(pkg, &registry, reg_id)?;
101
102 // Prepare a tarball, with a non-suppressible warning if metadata
103 // is missing since this is being put online.
104 let tarball = ops::package(
105 ws,
106 &ops::PackageOpts {
107 config: opts.config,
108 verify: opts.verify,
109 list: false,
110 check_metadata: true,
111 allow_dirty: opts.allow_dirty,
112 targets: opts.targets.clone(),
113 jobs: opts.jobs,
114 cli_features: opts.cli_features.clone(),
115 },
116 )?
117 .unwrap();
118
119 // Upload said tarball to the specified destination
120 opts.config
121 .shell()
122 .status("Uploading", pkg.package_id().to_string())?;
123 transmit(
124 opts.config,
125 pkg,
126 tarball.file(),
127 &mut registry,
128 reg_id,
129 opts.dry_run,
130 )?;
131
132 Ok(())
133 }
134
135 fn verify_dependencies(
136 pkg: &Package,
137 registry: &Registry,
138 registry_src: SourceId,
139 ) -> CargoResult<()> {
140 for dep in pkg.dependencies().iter() {
141 if dep.source_id().is_path() || dep.source_id().is_git() {
142 if !dep.specified_req() {
143 if !dep.is_transitive() {
144 // dev-dependencies will be stripped in TomlManifest::prepare_for_publish
145 continue;
146 }
147 let which = if dep.source_id().is_path() {
148 "path"
149 } else {
150 "git"
151 };
152 let dep_version_source = dep.registry_id().map_or_else(
153 || "crates.io".to_string(),
154 |registry_id| registry_id.display_registry_name(),
155 );
156 bail!(
157 "all dependencies must have a version specified when publishing.\n\
158 dependency `{}` does not specify a version\n\
159 Note: The published dependency will use the version from {},\n\
160 the `{}` specification will be removed from the dependency declaration.",
161 dep.package_name(),
162 dep_version_source,
163 which,
164 )
165 }
166 // TomlManifest::prepare_for_publish will rewrite the dependency
167 // to be just the `version` field.
168 } else if dep.source_id() != registry_src {
169 if !dep.source_id().is_registry() {
170 // Consider making SourceId::kind a public type that we can
171 // exhaustively match on. Using match can help ensure that
172 // every kind is properly handled.
173 panic!("unexpected source kind for dependency {:?}", dep);
174 }
175 // Block requests to send to crates.io with alt-registry deps.
176 // This extra hostname check is mostly to assist with testing,
177 // but also prevents someone using `--index` to specify
178 // something that points to crates.io.
179 if registry_src.is_default_registry() || registry.host_is_crates_io() {
180 bail!("crates cannot be published to crates.io with dependencies sourced from other\n\
181 registries. `{}` needs to be published to crates.io before publishing this crate.\n\
182 (crate `{}` is pulled from {})",
183 dep.package_name(),
184 dep.package_name(),
185 dep.source_id());
186 }
187 }
188 }
189 Ok(())
190 }
191
192 fn transmit(
193 config: &Config,
194 pkg: &Package,
195 tarball: &File,
196 registry: &mut Registry,
197 registry_id: SourceId,
198 dry_run: bool,
199 ) -> CargoResult<()> {
200 let deps = pkg
201 .dependencies()
202 .iter()
203 .filter(|dep| {
204 // Skip dev-dependency without version.
205 dep.is_transitive() || dep.specified_req()
206 })
207 .map(|dep| {
208 // If the dependency is from a different registry, then include the
209 // registry in the dependency.
210 let dep_registry_id = match dep.registry_id() {
211 Some(id) => id,
212 None => SourceId::crates_io(config)?,
213 };
214 // In the index and Web API, None means "from the same registry"
215 // whereas in Cargo.toml, it means "from crates.io".
216 let dep_registry = if dep_registry_id != registry_id {
217 Some(dep_registry_id.url().to_string())
218 } else {
219 None
220 };
221
222 Ok(NewCrateDependency {
223 optional: dep.is_optional(),
224 default_features: dep.uses_default_features(),
225 name: dep.package_name().to_string(),
226 features: dep.features().iter().map(|s| s.to_string()).collect(),
227 version_req: dep.version_req().to_string(),
228 target: dep.platform().map(|s| s.to_string()),
229 kind: match dep.kind() {
230 DepKind::Normal => "normal",
231 DepKind::Build => "build",
232 DepKind::Development => "dev",
233 }
234 .to_string(),
235 registry: dep_registry,
236 explicit_name_in_toml: dep.explicit_name_in_toml().map(|s| s.to_string()),
237 })
238 })
239 .collect::<CargoResult<Vec<NewCrateDependency>>>()?;
240 let manifest = pkg.manifest();
241 let ManifestMetadata {
242 ref authors,
243 ref description,
244 ref homepage,
245 ref documentation,
246 ref keywords,
247 ref readme,
248 ref repository,
249 ref license,
250 ref license_file,
251 ref categories,
252 ref badges,
253 ref links,
254 } = *manifest.metadata();
255 let readme_content = readme
256 .as_ref()
257 .map(|readme| {
258 paths::read(&pkg.root().join(readme))
259 .chain_err(|| format!("failed to read `readme` file for package `{}`", pkg))
260 })
261 .transpose()?;
262 if let Some(ref file) = *license_file {
263 if !pkg.root().join(file).exists() {
264 bail!("the license file `{}` does not exist", file)
265 }
266 }
267
268 // Do not upload if performing a dry run
269 if dry_run {
270 config.shell().warn("aborting upload due to dry run")?;
271 return Ok(());
272 }
273
274 let string_features = match manifest.original().features() {
275 Some(features) => features
276 .iter()
277 .map(|(feat, values)| {
278 (
279 feat.to_string(),
280 values.iter().map(|fv| fv.to_string()).collect(),
281 )
282 })
283 .collect::<BTreeMap<String, Vec<String>>>(),
284 None => BTreeMap::new(),
285 };
286
287 let warnings = registry
288 .publish(
289 &NewCrate {
290 name: pkg.name().to_string(),
291 vers: pkg.version().to_string(),
292 deps,
293 features: string_features,
294 authors: authors.clone(),
295 description: description.clone(),
296 homepage: homepage.clone(),
297 documentation: documentation.clone(),
298 keywords: keywords.clone(),
299 categories: categories.clone(),
300 readme: readme_content,
301 readme_file: readme.clone(),
302 repository: repository.clone(),
303 license: license.clone(),
304 license_file: license_file.clone(),
305 badges: badges.clone(),
306 links: links.clone(),
307 v: None,
308 },
309 tarball,
310 )
311 .chain_err(|| format!("failed to publish to registry at {}", registry.host()))?;
312
313 if !warnings.invalid_categories.is_empty() {
314 let msg = format!(
315 "the following are not valid category slugs and were \
316 ignored: {}. Please see https://crates.io/category_slugs \
317 for the list of all category slugs. \
318 ",
319 warnings.invalid_categories.join(", ")
320 );
321 config.shell().warn(&msg)?;
322 }
323
324 if !warnings.invalid_badges.is_empty() {
325 let msg = format!(
326 "the following are not valid badges and were ignored: {}. \
327 Either the badge type specified is unknown or a required \
328 attribute is missing. Please see \
329 https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata \
330 for valid badge types and their required attributes.",
331 warnings.invalid_badges.join(", ")
332 );
333 config.shell().warn(&msg)?;
334 }
335
336 if !warnings.other.is_empty() {
337 for msg in warnings.other {
338 config.shell().warn(&msg)?;
339 }
340 }
341
342 Ok(())
343 }
344
345 /// Returns the index and token from the config file for the given registry.
346 ///
347 /// `registry` is typically the registry specified on the command-line. If
348 /// `None`, `index` is set to `None` to indicate it should use crates.io.
349 pub fn registry_configuration(
350 config: &Config,
351 registry: Option<&str>,
352 ) -> CargoResult<RegistryConfig> {
353 let err_both = |token_key: &str, proc_key: &str| {
354 Err(format_err!(
355 "both `{TOKEN_KEY}` and `{PROC_KEY}` \
356 were specified in the config\n\
357 Only one of these values may be set, remove one or the other to proceed.",
358 TOKEN_KEY = token_key,
359 PROC_KEY = proc_key,
360 ))
361 };
362 // `registry.default` is handled in command-line parsing.
363 let (index, token, process) = match registry {
364 Some(registry) => {
365 validate_package_name(registry, "registry name", "")?;
366 let index = Some(config.get_registry_index(registry)?.to_string());
367 let token_key = format!("registries.{}.token", registry);
368 let token = config.get_string(&token_key)?.map(|p| p.val);
369 let process = if config.cli_unstable().credential_process {
370 let mut proc_key = format!("registries.{}.credential-process", registry);
371 let mut process = config.get::<Option<config::PathAndArgs>>(&proc_key)?;
372 if process.is_none() && token.is_none() {
373 // This explicitly ignores the global credential-process if
374 // the token is set, as that is "more specific".
375 proc_key = String::from("registry.credential-process");
376 process = config.get::<Option<config::PathAndArgs>>(&proc_key)?;
377 } else if process.is_some() && token.is_some() {
378 return err_both(&token_key, &proc_key);
379 }
380 process
381 } else {
382 None
383 };
384 (index, token, process)
385 }
386 None => {
387 // Use crates.io default.
388 config.check_registry_index_not_set()?;
389 let token = config.get_string("registry.token")?.map(|p| p.val);
390 let process = if config.cli_unstable().credential_process {
391 let process =
392 config.get::<Option<config::PathAndArgs>>("registry.credential-process")?;
393 if token.is_some() && process.is_some() {
394 return err_both("registry.token", "registry.credential-process");
395 }
396 process
397 } else {
398 None
399 };
400 (None, token, process)
401 }
402 };
403
404 let credential_process =
405 process.map(|process| (process.path.resolve_program(config), process.args));
406
407 Ok(RegistryConfig {
408 index,
409 token,
410 credential_process,
411 })
412 }
413
414 /// Returns the `Registry` and `Source` based on command-line and config settings.
415 ///
416 /// * `token`: The token from the command-line. If not set, uses the token
417 /// from the config.
418 /// * `index`: The index URL from the command-line. This is ignored if
419 /// `registry` is set.
420 /// * `registry`: The registry name from the command-line. If neither
421 /// `registry`, or `index` are set, then uses `crates-io`, honoring
422 /// `[source]` replacement if defined.
423 /// * `force_update`: If `true`, forces the index to be updated.
424 /// * `validate_token`: If `true`, the token must be set.
425 fn registry(
426 config: &Config,
427 token: Option<String>,
428 index: Option<String>,
429 registry: Option<String>,
430 force_update: bool,
431 validate_token: bool,
432 ) -> CargoResult<(Registry, RegistryConfig, SourceId)> {
433 if index.is_some() && registry.is_some() {
434 // Otherwise we would silently ignore one or the other.
435 bail!("both `--index` and `--registry` should not be set at the same time");
436 }
437 // Parse all configuration options
438 let reg_cfg = registry_configuration(config, registry.as_deref())?;
439 let opt_index = reg_cfg.index.as_ref().or_else(|| index.as_ref());
440 let sid = get_source_id(config, opt_index, registry.as_ref())?;
441 if !sid.is_remote_registry() {
442 bail!(
443 "{} does not support API commands.\n\
444 Check for a source-replacement in .cargo/config.",
445 sid
446 );
447 }
448 let api_host = {
449 let _lock = config.acquire_package_cache_lock()?;
450 let mut src = RegistrySource::remote(sid, &HashSet::new(), config);
451 // Only update the index if the config is not available or `force` is set.
452 let cfg = src.config();
453 let mut updated_cfg = || {
454 src.update()
455 .chain_err(|| format!("failed to update {}", sid))?;
456 src.config()
457 };
458
459 let cfg = if force_update {
460 updated_cfg()?
461 } else {
462 cfg.or_else(|_| updated_cfg())?
463 };
464
465 cfg.and_then(|cfg| cfg.api)
466 .ok_or_else(|| format_err!("{} does not support API commands", sid))?
467 };
468 let token = if validate_token {
469 if index.is_some() {
470 if token.is_none() {
471 bail!("command-line argument --index requires --token to be specified");
472 }
473 token
474 } else {
475 // Check `is_default_registry` so that the crates.io index can
476 // change config.json's "api" value, and this won't affect most
477 // people. It will affect those using source replacement, but
478 // hopefully that's a relatively small set of users.
479 if token.is_none()
480 && reg_cfg.token.is_some()
481 && registry.is_none()
482 && !sid.is_default_registry()
483 && !crates_io::is_url_crates_io(&api_host)
484 {
485 config.shell().warn(
486 "using `registry.token` config value with source \
487 replacement is deprecated\n\
488 This may become a hard error in the future; \
489 see <https://github.com/rust-lang/cargo/issues/xxx>.\n\
490 Use the --token command-line flag to remove this warning.",
491 )?;
492 reg_cfg.token.clone()
493 } else {
494 let token = auth::auth_token(
495 config,
496 token.as_deref(),
497 reg_cfg.token.as_deref(),
498 reg_cfg.credential_process.as_ref(),
499 registry.as_deref(),
500 &api_host,
501 )?;
502 log::debug!("found token {:?}", token);
503 Some(token)
504 }
505 }
506 } else {
507 None
508 };
509 let handle = http_handle(config)?;
510 Ok((Registry::new_handle(api_host, token, handle), reg_cfg, sid))
511 }
512
513 /// Creates a new HTTP handle with appropriate global configuration for cargo.
514 pub fn http_handle(config: &Config) -> CargoResult<Easy> {
515 let (mut handle, timeout) = http_handle_and_timeout(config)?;
516 timeout.configure(&mut handle)?;
517 Ok(handle)
518 }
519
520 pub fn http_handle_and_timeout(config: &Config) -> CargoResult<(Easy, HttpTimeout)> {
521 if config.frozen() {
522 bail!(
523 "attempting to make an HTTP request, but --frozen was \
524 specified"
525 )
526 }
527 if !config.network_allowed() {
528 bail!("can't make HTTP request in the offline mode")
529 }
530
531 // The timeout option for libcurl by default times out the entire transfer,
532 // but we probably don't want this. Instead we only set timeouts for the
533 // connect phase as well as a "low speed" timeout so if we don't receive
534 // many bytes in a large-ish period of time then we time out.
535 let mut handle = Easy::new();
536 let timeout = configure_http_handle(config, &mut handle)?;
537 Ok((handle, timeout))
538 }
539
540 pub fn needs_custom_http_transport(config: &Config) -> CargoResult<bool> {
541 Ok(http_proxy_exists(config)?
542 || *config.http_config()? != Default::default()
543 || env::var_os("HTTP_TIMEOUT").is_some())
544 }
545
546 /// Configure a libcurl http handle with the defaults options for Cargo
547 pub fn configure_http_handle(config: &Config, handle: &mut Easy) -> CargoResult<HttpTimeout> {
548 let http = config.http_config()?;
549 if let Some(proxy) = http_proxy(config)? {
550 handle.proxy(&proxy)?;
551 }
552 if let Some(cainfo) = &http.cainfo {
553 let cainfo = cainfo.resolve_path(config);
554 handle.cainfo(&cainfo)?;
555 }
556 if let Some(check) = http.check_revoke {
557 handle.ssl_options(SslOpt::new().no_revoke(!check))?;
558 }
559
560 if let Some(user_agent) = &http.user_agent {
561 handle.useragent(user_agent)?;
562 } else {
563 handle.useragent(&version().to_string())?;
564 }
565
566 fn to_ssl_version(s: &str) -> CargoResult<SslVersion> {
567 let version = match s {
568 "default" => SslVersion::Default,
569 "tlsv1" => SslVersion::Tlsv1,
570 "tlsv1.0" => SslVersion::Tlsv10,
571 "tlsv1.1" => SslVersion::Tlsv11,
572 "tlsv1.2" => SslVersion::Tlsv12,
573 "tlsv1.3" => SslVersion::Tlsv13,
574 _ => bail!(
575 "Invalid ssl version `{}`,\
576 choose from 'default', 'tlsv1', 'tlsv1.0', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3'.",
577 s
578 ),
579 };
580 Ok(version)
581 }
582 if let Some(ssl_version) = &http.ssl_version {
583 match ssl_version {
584 SslVersionConfig::Single(s) => {
585 let version = to_ssl_version(s.as_str())?;
586 handle.ssl_version(version)?;
587 }
588 SslVersionConfig::Range(SslVersionConfigRange { min, max }) => {
589 let min_version = min
590 .as_ref()
591 .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
592 let max_version = max
593 .as_ref()
594 .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
595 handle.ssl_min_max_version(min_version, max_version)?;
596 }
597 }
598 }
599
600 if let Some(true) = http.debug {
601 handle.verbose(true)?;
602 log::debug!("{:#?}", curl::Version::get());
603 handle.debug_function(|kind, data| {
604 let (prefix, level) = match kind {
605 InfoType::Text => ("*", Level::Debug),
606 InfoType::HeaderIn => ("<", Level::Debug),
607 InfoType::HeaderOut => (">", Level::Debug),
608 InfoType::DataIn => ("{", Level::Trace),
609 InfoType::DataOut => ("}", Level::Trace),
610 InfoType::SslDataIn | InfoType::SslDataOut => return,
611 _ => return,
612 };
613 match str::from_utf8(data) {
614 Ok(s) => {
615 for mut line in s.lines() {
616 if line.starts_with("Authorization:") {
617 line = "Authorization: [REDACTED]";
618 } else if line[..line.len().min(10)].eq_ignore_ascii_case("set-cookie") {
619 line = "set-cookie: [REDACTED]";
620 }
621 log!(level, "http-debug: {} {}", prefix, line);
622 }
623 }
624 Err(_) => {
625 log!(
626 level,
627 "http-debug: {} ({} bytes of data)",
628 prefix,
629 data.len()
630 );
631 }
632 }
633 })?;
634 }
635
636 HttpTimeout::new(config)
637 }
638
639 #[must_use]
640 pub struct HttpTimeout {
641 pub dur: Duration,
642 pub low_speed_limit: u32,
643 }
644
645 impl HttpTimeout {
646 pub fn new(config: &Config) -> CargoResult<HttpTimeout> {
647 let config = config.http_config()?;
648 let low_speed_limit = config.low_speed_limit.unwrap_or(10);
649 let seconds = config
650 .timeout
651 .or_else(|| env::var("HTTP_TIMEOUT").ok().and_then(|s| s.parse().ok()))
652 .unwrap_or(30);
653 Ok(HttpTimeout {
654 dur: Duration::new(seconds, 0),
655 low_speed_limit,
656 })
657 }
658
659 pub fn configure(&self, handle: &mut Easy) -> CargoResult<()> {
660 // The timeout option for libcurl by default times out the entire
661 // transfer, but we probably don't want this. Instead we only set
662 // timeouts for the connect phase as well as a "low speed" timeout so
663 // if we don't receive many bytes in a large-ish period of time then we
664 // time out.
665 handle.connect_timeout(self.dur)?;
666 handle.low_speed_time(self.dur)?;
667 handle.low_speed_limit(self.low_speed_limit)?;
668 Ok(())
669 }
670 }
671
672 /// Finds an explicit HTTP proxy if one is available.
673 ///
674 /// Favor cargo's `http.proxy`, then git's `http.proxy`. Proxies specified
675 /// via environment variables are picked up by libcurl.
676 fn http_proxy(config: &Config) -> CargoResult<Option<String>> {
677 let http = config.http_config()?;
678 if let Some(s) = &http.proxy {
679 return Ok(Some(s.clone()));
680 }
681 if let Ok(cfg) = git2::Config::open_default() {
682 if let Ok(s) = cfg.get_string("http.proxy") {
683 return Ok(Some(s));
684 }
685 }
686 Ok(None)
687 }
688
689 /// Determine if an http proxy exists.
690 ///
691 /// Checks the following for existence, in order:
692 ///
693 /// * cargo's `http.proxy`
694 /// * git's `http.proxy`
695 /// * `http_proxy` env var
696 /// * `HTTP_PROXY` env var
697 /// * `https_proxy` env var
698 /// * `HTTPS_PROXY` env var
699 fn http_proxy_exists(config: &Config) -> CargoResult<bool> {
700 if http_proxy(config)?.is_some() {
701 Ok(true)
702 } else {
703 Ok(["http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY"]
704 .iter()
705 .any(|v| env::var(v).is_ok()))
706 }
707 }
708
709 pub fn registry_login(
710 config: &Config,
711 token: Option<String>,
712 reg: Option<String>,
713 ) -> CargoResult<()> {
714 let (registry, reg_cfg, _) = registry(config, token.clone(), None, reg.clone(), false, false)?;
715
716 let token = match token {
717 Some(token) => token,
718 None => {
719 drop_println!(
720 config,
721 "please paste the API Token found on {}/me below",
722 registry.host()
723 );
724 let mut line = String::new();
725 let input = io::stdin();
726 input
727 .lock()
728 .read_line(&mut line)
729 .chain_err(|| "failed to read stdin")?;
730 // Automatically remove `cargo login` from an inputted token to
731 // allow direct pastes from `registry.host()`/me.
732 line.replace("cargo login", "").trim().to_string()
733 }
734 };
735
736 if let Some(old_token) = &reg_cfg.token {
737 if old_token == &token {
738 config.shell().status("Login", "already logged in")?;
739 return Ok(());
740 }
741 }
742
743 auth::login(
744 config,
745 token,
746 reg_cfg.credential_process.as_ref(),
747 reg.as_deref(),
748 registry.host(),
749 )?;
750
751 config.shell().status(
752 "Login",
753 format!(
754 "token for `{}` saved",
755 reg.as_ref().map_or("crates.io", String::as_str)
756 ),
757 )?;
758 Ok(())
759 }
760
761 pub fn registry_logout(config: &Config, reg: Option<String>) -> CargoResult<()> {
762 let (registry, reg_cfg, _) = registry(config, None, None, reg.clone(), false, false)?;
763 let reg_name = reg.as_deref().unwrap_or("crates.io");
764 if reg_cfg.credential_process.is_none() && reg_cfg.token.is_none() {
765 config.shell().status(
766 "Logout",
767 format!("not currently logged in to `{}`", reg_name),
768 )?;
769 return Ok(());
770 }
771 auth::logout(
772 config,
773 reg_cfg.credential_process.as_ref(),
774 reg.as_deref(),
775 registry.host(),
776 )?;
777 config.shell().status(
778 "Logout",
779 format!(
780 "token for `{}` has been removed from local storage",
781 reg_name
782 ),
783 )?;
784 Ok(())
785 }
786
787 pub struct OwnersOptions {
788 pub krate: Option<String>,
789 pub token: Option<String>,
790 pub index: Option<String>,
791 pub to_add: Option<Vec<String>>,
792 pub to_remove: Option<Vec<String>>,
793 pub list: bool,
794 pub registry: Option<String>,
795 }
796
797 pub fn modify_owners(config: &Config, opts: &OwnersOptions) -> CargoResult<()> {
798 let name = match opts.krate {
799 Some(ref name) => name.clone(),
800 None => {
801 let manifest_path = find_root_manifest_for_wd(config.cwd())?;
802 let ws = Workspace::new(&manifest_path, config)?;
803 ws.current()?.package_id().name().to_string()
804 }
805 };
806
807 let (mut registry, _, _) = registry(
808 config,
809 opts.token.clone(),
810 opts.index.clone(),
811 opts.registry.clone(),
812 true,
813 true,
814 )?;
815
816 if let Some(ref v) = opts.to_add {
817 let v = v.iter().map(|s| &s[..]).collect::<Vec<_>>();
818 let msg = registry.add_owners(&name, &v).chain_err(|| {
819 format!(
820 "failed to invite owners to crate `{}` on registry at {}",
821 name,
822 registry.host()
823 )
824 })?;
825
826 config.shell().status("Owner", msg)?;
827 }
828
829 if let Some(ref v) = opts.to_remove {
830 let v = v.iter().map(|s| &s[..]).collect::<Vec<_>>();
831 config
832 .shell()
833 .status("Owner", format!("removing {:?} from crate {}", v, name))?;
834 registry.remove_owners(&name, &v).chain_err(|| {
835 format!(
836 "failed to remove owners from crate `{}` on registry at {}",
837 name,
838 registry.host()
839 )
840 })?;
841 }
842
843 if opts.list {
844 let owners = registry.list_owners(&name).chain_err(|| {
845 format!(
846 "failed to list owners of crate `{}` on registry at {}",
847 name,
848 registry.host()
849 )
850 })?;
851 for owner in owners.iter() {
852 drop_print!(config, "{}", owner.login);
853 match (owner.name.as_ref(), owner.email.as_ref()) {
854 (Some(name), Some(email)) => drop_println!(config, " ({} <{}>)", name, email),
855 (Some(s), None) | (None, Some(s)) => drop_println!(config, " ({})", s),
856 (None, None) => drop_println!(config),
857 }
858 }
859 }
860
861 Ok(())
862 }
863
864 pub fn yank(
865 config: &Config,
866 krate: Option<String>,
867 version: Option<String>,
868 token: Option<String>,
869 index: Option<String>,
870 undo: bool,
871 reg: Option<String>,
872 ) -> CargoResult<()> {
873 let name = match krate {
874 Some(name) => name,
875 None => {
876 let manifest_path = find_root_manifest_for_wd(config.cwd())?;
877 let ws = Workspace::new(&manifest_path, config)?;
878 ws.current()?.package_id().name().to_string()
879 }
880 };
881 let version = match version {
882 Some(v) => v,
883 None => bail!("a version must be specified to yank"),
884 };
885
886 let (mut registry, _, _) = registry(config, token, index, reg, true, true)?;
887
888 if undo {
889 config
890 .shell()
891 .status("Unyank", format!("{}:{}", name, version))?;
892 registry.unyank(&name, &version).chain_err(|| {
893 format!(
894 "failed to undo a yank from the registry at {}",
895 registry.host()
896 )
897 })?;
898 } else {
899 config
900 .shell()
901 .status("Yank", format!("{}:{}", name, version))?;
902 registry
903 .yank(&name, &version)
904 .chain_err(|| format!("failed to yank from the registry at {}", registry.host()))?;
905 }
906
907 Ok(())
908 }
909
910 /// Gets the SourceId for an index or registry setting.
911 ///
912 /// The `index` and `reg` values are from the command-line or config settings.
913 /// If both are None, returns the source for crates.io.
914 fn get_source_id(
915 config: &Config,
916 index: Option<&String>,
917 reg: Option<&String>,
918 ) -> CargoResult<SourceId> {
919 match (reg, index) {
920 (Some(r), _) => SourceId::alt_registry(config, r),
921 (_, Some(i)) => SourceId::for_registry(&i.into_url()?),
922 _ => {
923 let map = SourceConfigMap::new(config)?;
924 let src = map.load(SourceId::crates_io(config)?, &HashSet::new())?;
925 Ok(src.replaced_source_id())
926 }
927 }
928 }
929
930 pub fn search(
931 query: &str,
932 config: &Config,
933 index: Option<String>,
934 limit: u32,
935 reg: Option<String>,
936 ) -> CargoResult<()> {
937 fn truncate_with_ellipsis(s: &str, max_width: usize) -> String {
938 // We should truncate at grapheme-boundary and compute character-widths,
939 // yet the dependencies on unicode-segmentation and unicode-width are
940 // not worth it.
941 let mut chars = s.chars();
942 let mut prefix = (&mut chars).take(max_width - 1).collect::<String>();
943 if chars.next().is_some() {
944 prefix.push('…');
945 }
946 prefix
947 }
948
949 let (mut registry, _, source_id) = registry(config, None, index, reg, false, false)?;
950 let (crates, total_crates) = registry.search(query, limit).chain_err(|| {
951 format!(
952 "failed to retrieve search results from the registry at {}",
953 registry.host()
954 )
955 })?;
956
957 let names = crates
958 .iter()
959 .map(|krate| format!("{} = \"{}\"", krate.name, krate.max_version))
960 .collect::<Vec<String>>();
961
962 let description_margin = names.iter().map(|s| s.len() + 4).max().unwrap_or_default();
963
964 let description_length = cmp::max(80, 128 - description_margin);
965
966 let descriptions = crates.iter().map(|krate| {
967 krate
968 .description
969 .as_ref()
970 .map(|desc| truncate_with_ellipsis(&desc.replace("\n", " "), description_length))
971 });
972
973 for (name, description) in names.into_iter().zip(descriptions) {
974 let line = match description {
975 Some(desc) => {
976 let space = repeat(' ')
977 .take(description_margin - name.len())
978 .collect::<String>();
979 name + &space + "# " + &desc
980 }
981 None => name,
982 };
983 drop_println!(config, "{}", line);
984 }
985
986 let search_max_limit = 100;
987 if total_crates > limit && limit < search_max_limit {
988 drop_println!(
989 config,
990 "... and {} crates more (use --limit N to see more)",
991 total_crates - limit
992 );
993 } else if total_crates > limit && limit >= search_max_limit {
994 let extra = if source_id.is_default_registry() {
995 format!(
996 " (go to https://crates.io/search?q={} to see more)",
997 percent_encode(query.as_bytes(), NON_ALPHANUMERIC)
998 )
999 } else {
1000 String::new()
1001 };
1002 drop_println!(
1003 config,
1004 "... and {} crates more{}",
1005 total_crates - limit,
1006 extra
1007 );
1008 }
1009
1010 Ok(())
1011 }