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