1use std::collections::{BTreeMap, HashSet};
2use std::fs::{self, File};
3use std::io::{self, BufRead};
4use std::iter::repeat;
5use std::str;
6use std::time::Duration;
7use std::{cmp, env};
8
9use anyhow::{bail, format_err};
10use crates_io::{NewCrate, NewCrateDependency, Registry};
11use curl::easy::{Easy, InfoType, SslOpt, SslVersion};
12use log::{log, Level};
13use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
14
15use crate::core::dependency::DepKind;
16use crate::core::manifest::ManifestMetadata;
17use crate::core::source::Source;
18use crate::core::{Package, SourceId, Workspace};
19use crate::ops;
20use crate::sources::{RegistrySource, SourceConfigMap, CRATES_IO_REGISTRY};
21use crate::util::config::{self, Config, SslVersionConfig, SslVersionConfigRange};
22use crate::util::errors::{CargoResult, CargoResultExt};
23use crate::util::important_paths::find_root_manifest_for_wd;
24use crate::util::IntoUrl;
25use crate::util::{paths, validate_package_name};
26use crate::version;
27
28pub struct RegistryConfig {
29 pub index: Option<String>,
30 pub token: Option<String>,
31}
32
33pub struct PublishOpts<'cfg> {
34 pub config: &'cfg Config,
35 pub token: Option<String>,
36 pub index: Option<String>,
37 pub verify: bool,
38 pub allow_dirty: bool,
39 pub jobs: Option<u32>,
40 pub target: Option<String>,
41 pub dry_run: bool,
42 pub registry: Option<String>,
43 pub features: Vec<String>,
44 pub all_features: bool,
45 pub no_default_features: bool,
46}
47
48pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
49 let pkg = ws.current()?;
50
51 if let Some(ref allowed_registries) = *pkg.publish() {
52 let reg_name = opts
53 .registry
54 .clone()
55 .unwrap_or_else(|| CRATES_IO_REGISTRY.to_string());
56 if !allowed_registries.contains(®_name) {
57 bail!(
58 "`{}` cannot be published.\n\
59 The registry `{}` is not listed in the `publish` value in Cargo.toml.",
60 pkg.name(),
61 reg_name
62 );
63 }
64 }
65
66 let (mut registry, reg_id) = registry(
67 opts.config,
68 opts.token.clone(),
69 opts.index.clone(),
70 opts.registry.clone(),
71 true,
72 !opts.dry_run,
73 )?;
74 verify_dependencies(pkg, ®istry, reg_id)?;
75
76 let tarball = ops::package(
79 ws,
80 &ops::PackageOpts {
81 config: opts.config,
82 verify: opts.verify,
83 list: false,
84 check_metadata: true,
85 allow_dirty: opts.allow_dirty,
86 target: opts.target.clone(),
87 jobs: opts.jobs,
88 features: opts.features.clone(),
89 all_features: opts.all_features,
90 no_default_features: opts.no_default_features,
91 },
92 )?
93 .unwrap();
94
95 opts.config
97 .shell()
98 .status("Uploading", pkg.package_id().to_string())?;
99 transmit(
100 opts.config,
101 pkg,
102 tarball.file(),
103 &mut registry,
104 reg_id,
105 opts.dry_run,
106 )?;
107
108 Ok(())
109}
110
111fn verify_dependencies(
112 pkg: &Package,
113 registry: &Registry,
114 registry_src: SourceId,
115) -> CargoResult<()> {
116 for dep in pkg.dependencies().iter() {
117 if dep.source_id().is_path() || dep.source_id().is_git() {
118 if !dep.specified_req() {
119 if !dep.is_transitive() {
120 continue;
122 }
123 let which = if dep.source_id().is_path() {
124 "path"
125 } else {
126 "git"
127 };
128 let dep_version_source = dep.registry_id().map_or_else(
129 || "crates.io".to_string(),
130 |registry_id| registry_id.display_registry_name(),
131 );
132 bail!(
133 "all dependencies must have a version specified when publishing.\n\
134 dependency `{}` does not specify a version\n\
135 Note: The published dependency will use the version from {},\n\
136 the `{}` specification will be removed from the dependency declaration.",
137 dep.package_name(),
138 dep_version_source,
139 which,
140 )
141 }
142 } else if dep.source_id() != registry_src {
145 if !dep.source_id().is_registry() {
146 panic!("unexpected source kind for dependency {:?}", dep);
150 }
151 if registry_src.is_default_registry() || registry.host_is_crates_io() {
156 bail!("crates cannot be published to crates.io with dependencies sourced from other\n\
157 registries. `{}` needs to be published to crates.io before publishing this crate.\n\
158 (crate `{}` is pulled from {})",
159 dep.package_name(),
160 dep.package_name(),
161 dep.source_id());
162 }
163 }
164 }
165 Ok(())
166}
167
168fn transmit(
169 config: &Config,
170 pkg: &Package,
171 tarball: &File,
172 registry: &mut Registry,
173 registry_id: SourceId,
174 dry_run: bool,
175) -> CargoResult<()> {
176 let deps = pkg
177 .dependencies()
178 .iter()
179 .filter(|dep| {
180 dep.is_transitive() || dep.specified_req()
182 })
183 .map(|dep| {
184 let dep_registry_id = match dep.registry_id() {
187 Some(id) => id,
188 None => SourceId::crates_io(config)?,
189 };
190 let dep_registry = if dep_registry_id != registry_id {
193 Some(dep_registry_id.url().to_string())
194 } else {
195 None
196 };
197
198 Ok(NewCrateDependency {
199 optional: dep.is_optional(),
200 default_features: dep.uses_default_features(),
201 name: dep.package_name().to_string(),
202 features: dep.features().iter().map(|s| s.to_string()).collect(),
203 version_req: dep.version_req().to_string(),
204 target: dep.platform().map(|s| s.to_string()),
205 kind: match dep.kind() {
206 DepKind::Normal => "normal",
207 DepKind::Build => "build",
208 DepKind::Development => "dev",
209 }
210 .to_string(),
211 registry: dep_registry,
212 explicit_name_in_toml: dep.explicit_name_in_toml().map(|s| s.to_string()),
213 })
214 })
215 .collect::<CargoResult<Vec<NewCrateDependency>>>()?;
216 let manifest = pkg.manifest();
217 let ManifestMetadata {
218 ref authors,
219 ref description,
220 ref homepage,
221 ref documentation,
222 ref keywords,
223 ref readme,
224 ref repository,
225 ref license,
226 ref license_file,
227 ref categories,
228 ref badges,
229 ref links,
230 } = *manifest.metadata();
231 let readme_content = match *readme {
232 Some(ref readme) => Some(paths::read(&pkg.root().join(readme))?),
233 None => None,
234 };
235 if let Some(ref file) = *license_file {
236 if fs::metadata(&pkg.root().join(file)).is_err() {
237 bail!("the license file `{}` does not exist", file)
238 }
239 }
240
241 if dry_run {
243 config.shell().warn("aborting upload due to dry run")?;
244 return Ok(());
245 }
246
247 let summary = pkg.summary();
248 let string_features = summary
249 .features()
250 .iter()
251 .map(|(feat, values)| {
252 (
253 feat.to_string(),
254 values.iter().map(|fv| fv.to_string(summary)).collect(),
255 )
256 })
257 .collect::<BTreeMap<String, Vec<String>>>();
258
259 let publish = registry.publish(
260 &NewCrate {
261 name: pkg.name().to_string(),
262 vers: pkg.version().to_string(),
263 deps,
264 features: string_features,
265 authors: authors.clone(),
266 description: description.clone(),
267 homepage: homepage.clone(),
268 documentation: documentation.clone(),
269 keywords: keywords.clone(),
270 categories: categories.clone(),
271 readme: readme_content,
272 readme_file: readme.clone(),
273 repository: repository.clone(),
274 license: license.clone(),
275 license_file: license_file.clone(),
276 badges: badges.clone(),
277 links: links.clone(),
278 },
279 tarball,
280 );
281
282 match publish {
283 Ok(warnings) => {
284 if !warnings.invalid_categories.is_empty() {
285 let msg = format!(
286 "the following are not valid category slugs and were \
287 ignored: {}. Please see https://crates.io/category_slugs \
288 for the list of all category slugs. \
289 ",
290 warnings.invalid_categories.join(", ")
291 );
292 config.shell().warn(&msg)?;
293 }
294
295 if !warnings.invalid_badges.is_empty() {
296 let msg = format!(
297 "the following are not valid badges and were ignored: {}. \
298 Either the badge type specified is unknown or a required \
299 attribute is missing. Please see \
300 https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata \
301 for valid badge types and their required attributes.",
302 warnings.invalid_badges.join(", ")
303 );
304 config.shell().warn(&msg)?;
305 }
306
307 if !warnings.other.is_empty() {
308 for msg in warnings.other {
309 config.shell().warn(&msg)?;
310 }
311 }
312
313 Ok(())
314 }
315 Err(e) => Err(e),
316 }
317}
318
319pub fn registry_configuration(
320 config: &Config,
321 registry: Option<String>,
322) -> CargoResult<RegistryConfig> {
323 let (index, token) = match registry {
324 Some(registry) => {
325 validate_package_name(®istry, "registry name", "")?;
326 (
327 Some(config.get_registry_index(®istry)?.to_string()),
328 config
329 .get_string(&format!("registries.{}.token", registry))?
330 .map(|p| p.val),
331 )
332 }
333 None => {
334 (
336 config
337 .get_default_registry_index()?
338 .map(|url| url.to_string()),
339 config.get_string("registry.token")?.map(|p| p.val),
340 )
341 }
342 };
343
344 Ok(RegistryConfig { index, token })
345}
346
347fn registry(
348 config: &Config,
349 token: Option<String>,
350 index: Option<String>,
351 registry: Option<String>,
352 force_update: bool,
353 validate_token: bool,
354) -> CargoResult<(Registry, SourceId)> {
355 let RegistryConfig {
357 token: token_config,
358 index: index_config,
359 } = registry_configuration(config, registry.clone())?;
360 let token = token.or(token_config);
361 let sid = get_source_id(config, index_config.or(index), registry)?;
362 if !sid.is_remote_registry() {
363 bail!(
364 "{} does not support API commands.\n\
365 Check for a source-replacement in .cargo/config.",
366 sid
367 );
368 }
369 let api_host = {
370 let _lock = config.acquire_package_cache_lock()?;
371 let mut src = RegistrySource::remote(sid, &HashSet::new(), config);
372 let cfg = src.config();
374 let mut updated_cfg = || {
375 src.update()
376 .chain_err(|| format!("failed to update {}", sid))?;
377 src.config()
378 };
379
380 let cfg = if force_update {
381 updated_cfg()?
382 } else {
383 cfg.or_else(|_| updated_cfg())?
384 };
385
386 cfg.and_then(|cfg| cfg.api)
387 .ok_or_else(|| format_err!("{} does not support API commands", sid))?
388 };
389 let handle = http_handle(config)?;
390 if validate_token && token.is_none() {
391 bail!("no upload token found, please run `cargo login`");
392 };
393 Ok((Registry::new_handle(api_host, token, handle), sid))
394}
395
396pub fn http_handle(config: &Config) -> CargoResult<Easy> {
398 let (mut handle, timeout) = http_handle_and_timeout(config)?;
399 timeout.configure(&mut handle)?;
400 Ok(handle)
401}
402
403pub fn http_handle_and_timeout(config: &Config) -> CargoResult<(Easy, HttpTimeout)> {
404 if config.frozen() {
405 bail!(
406 "attempting to make an HTTP request, but --frozen was \
407 specified"
408 )
409 }
410 if !config.network_allowed() {
411 bail!("can't make HTTP request in the offline mode")
412 }
413
414 let mut handle = Easy::new();
419 let timeout = configure_http_handle(config, &mut handle)?;
420 Ok((handle, timeout))
421}
422
423pub fn needs_custom_http_transport(config: &Config) -> CargoResult<bool> {
424 Ok(http_proxy_exists(config)?
425 || *config.http_config()? != Default::default()
426 || env::var_os("HTTP_TIMEOUT").is_some())
427}
428
429pub fn configure_http_handle(config: &Config, handle: &mut Easy) -> CargoResult<HttpTimeout> {
431 let http = config.http_config()?;
432 if let Some(proxy) = http_proxy(config)? {
433 handle.proxy(&proxy)?;
434 }
435 if let Some(cainfo) = &http.cainfo {
436 let cainfo = cainfo.resolve_path(config);
437 handle.cainfo(&cainfo)?;
438 }
439 if let Some(check) = http.check_revoke {
440 handle.ssl_options(SslOpt::new().no_revoke(!check))?;
441 }
442
443 if let Some(user_agent) = &http.user_agent {
444 handle.useragent(user_agent)?;
445 } else {
446 handle.useragent(&version().to_string())?;
447 }
448
449 fn to_ssl_version(s: &str) -> CargoResult<SslVersion> {
450 let version = match s {
451 "default" => SslVersion::Default,
452 "tlsv1" => SslVersion::Tlsv1,
453 "tlsv1.0" => SslVersion::Tlsv10,
454 "tlsv1.1" => SslVersion::Tlsv11,
455 "tlsv1.2" => SslVersion::Tlsv12,
456 "tlsv1.3" => SslVersion::Tlsv13,
457 _ => bail!(
458 "Invalid ssl version `{}`,\
459 choose from 'default', 'tlsv1', 'tlsv1.0', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3'.",
460 s
461 ),
462 };
463 Ok(version)
464 }
465 if let Some(ssl_version) = &http.ssl_version {
466 match ssl_version {
467 SslVersionConfig::Single(s) => {
468 let version = to_ssl_version(s.as_str())?;
469 handle.ssl_version(version)?;
470 }
471 SslVersionConfig::Range(SslVersionConfigRange { min, max }) => {
472 let min_version = min
473 .as_ref()
474 .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
475 let max_version = max
476 .as_ref()
477 .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
478 handle.ssl_min_max_version(min_version, max_version)?;
479 }
480 }
481 }
482
483 if let Some(true) = http.debug {
484 handle.verbose(true)?;
485 handle.debug_function(|kind, data| {
486 let (prefix, level) = match kind {
487 InfoType::Text => ("*", Level::Debug),
488 InfoType::HeaderIn => ("<", Level::Debug),
489 InfoType::HeaderOut => (">", Level::Debug),
490 InfoType::DataIn => ("{", Level::Trace),
491 InfoType::DataOut => ("}", Level::Trace),
492 InfoType::SslDataIn | InfoType::SslDataOut => return,
493 _ => return,
494 };
495 match str::from_utf8(data) {
496 Ok(s) => {
497 for line in s.lines() {
498 log!(level, "http-debug: {} {}", prefix, line);
499 }
500 }
501 Err(_) => {
502 log!(
503 level,
504 "http-debug: {} ({} bytes of data)",
505 prefix,
506 data.len()
507 );
508 }
509 }
510 })?;
511 }
512
513 HttpTimeout::new(config)
514}
515
516#[must_use]
517pub struct HttpTimeout {
518 pub dur: Duration,
519 pub low_speed_limit: u32,
520}
521
522impl HttpTimeout {
523 pub fn new(config: &Config) -> CargoResult<HttpTimeout> {
524 let config = config.http_config()?;
525 let low_speed_limit = config.low_speed_limit.unwrap_or(10);
526 let seconds = config
527 .timeout
528 .or_else(|| env::var("HTTP_TIMEOUT").ok().and_then(|s| s.parse().ok()))
529 .unwrap_or(30);
530 Ok(HttpTimeout {
531 dur: Duration::new(seconds, 0),
532 low_speed_limit,
533 })
534 }
535
536 pub fn configure(&self, handle: &mut Easy) -> CargoResult<()> {
537 handle.connect_timeout(self.dur)?;
543 handle.low_speed_time(self.dur)?;
544 handle.low_speed_limit(self.low_speed_limit)?;
545 Ok(())
546 }
547}
548
549fn http_proxy(config: &Config) -> CargoResult<Option<String>> {
554 let http = config.http_config()?;
555 if let Some(s) = &http.proxy {
556 return Ok(Some(s.clone()));
557 }
558 if let Ok(cfg) = git2::Config::open_default() {
559 if let Ok(s) = cfg.get_str("http.proxy") {
560 return Ok(Some(s.to_string()));
561 }
562 }
563 Ok(None)
564}
565
566fn http_proxy_exists(config: &Config) -> CargoResult<bool> {
577 if http_proxy(config)?.is_some() {
578 Ok(true)
579 } else {
580 Ok(["http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY"]
581 .iter()
582 .any(|v| env::var(v).is_ok()))
583 }
584}
585
586pub fn registry_login(
587 config: &Config,
588 token: Option<String>,
589 reg: Option<String>,
590) -> CargoResult<()> {
591 let (registry, _) = registry(config, token.clone(), None, reg.clone(), false, false)?;
592
593 let token = match token {
594 Some(token) => token,
595 None => {
596 println!(
597 "please visit {}/me and paste the API Token below",
598 registry.host()
599 );
600 let mut line = String::new();
601 let input = io::stdin();
602 input
603 .lock()
604 .read_line(&mut line)
605 .chain_err(|| "failed to read stdin")
606 .map_err(anyhow::Error::from)?;
607 line.replace("cargo login", "").trim().to_string()
609 }
610 };
611
612 let RegistryConfig {
613 token: old_token, ..
614 } = registry_configuration(config, reg.clone())?;
615
616 if let Some(old_token) = old_token {
617 if old_token == token {
618 config.shell().status("Login", "already logged in")?;
619 return Ok(());
620 }
621 }
622
623 config::save_credentials(config, token, reg.clone())?;
624 config.shell().status(
625 "Login",
626 format!(
627 "token for `{}` saved",
628 reg.as_ref().map_or("crates.io", String::as_str)
629 ),
630 )?;
631 Ok(())
632}
633
634pub struct OwnersOptions {
635 pub krate: Option<String>,
636 pub token: Option<String>,
637 pub index: Option<String>,
638 pub to_add: Option<Vec<String>>,
639 pub to_remove: Option<Vec<String>>,
640 pub list: bool,
641 pub registry: Option<String>,
642}
643
644pub fn modify_owners(config: &Config, opts: &OwnersOptions) -> CargoResult<()> {
645 let name = match opts.krate {
646 Some(ref name) => name.clone(),
647 None => {
648 let manifest_path = find_root_manifest_for_wd(config.cwd())?;
649 let ws = Workspace::new(&manifest_path, config)?;
650 ws.current()?.package_id().name().to_string()
651 }
652 };
653
654 let (mut registry, _) = registry(
655 config,
656 opts.token.clone(),
657 opts.index.clone(),
658 opts.registry.clone(),
659 true,
660 true,
661 )?;
662
663 if let Some(ref v) = opts.to_add {
664 let v = v.iter().map(|s| &s[..]).collect::<Vec<_>>();
665 let msg = registry
666 .add_owners(&name, &v)
667 .map_err(|e| format_err!("failed to invite owners to crate {}: {}", name, e))?;
668
669 config.shell().status("Owner", msg)?;
670 }
671
672 if let Some(ref v) = opts.to_remove {
673 let v = v.iter().map(|s| &s[..]).collect::<Vec<_>>();
674 config
675 .shell()
676 .status("Owner", format!("removing {:?} from crate {}", v, name))?;
677 registry
678 .remove_owners(&name, &v)
679 .chain_err(|| format!("failed to remove owners from crate {}", name))?;
680 }
681
682 if opts.list {
683 let owners = registry
684 .list_owners(&name)
685 .chain_err(|| format!("failed to list owners of crate {}", name))?;
686 for owner in owners.iter() {
687 print!("{}", owner.login);
688 match (owner.name.as_ref(), owner.email.as_ref()) {
689 (Some(name), Some(email)) => println!(" ({} <{}>)", name, email),
690 (Some(s), None) | (None, Some(s)) => println!(" ({})", s),
691 (None, None) => println!(),
692 }
693 }
694 }
695
696 Ok(())
697}
698
699pub fn yank(
700 config: &Config,
701 krate: Option<String>,
702 version: Option<String>,
703 token: Option<String>,
704 index: Option<String>,
705 undo: bool,
706 reg: Option<String>,
707) -> CargoResult<()> {
708 let name = match krate {
709 Some(name) => name,
710 None => {
711 let manifest_path = find_root_manifest_for_wd(config.cwd())?;
712 let ws = Workspace::new(&manifest_path, config)?;
713 ws.current()?.package_id().name().to_string()
714 }
715 };
716 let version = match version {
717 Some(v) => v,
718 None => bail!("a version must be specified to yank"),
719 };
720
721 let (mut registry, _) = registry(config, token, index, reg, true, true)?;
722
723 if undo {
724 config
725 .shell()
726 .status("Unyank", format!("{}:{}", name, version))?;
727 registry
728 .unyank(&name, &version)
729 .chain_err(|| "failed to undo a yank")?;
730 } else {
731 config
732 .shell()
733 .status("Yank", format!("{}:{}", name, version))?;
734 registry
735 .yank(&name, &version)
736 .chain_err(|| "failed to yank")?;
737 }
738
739 Ok(())
740}
741
742fn get_source_id(
743 config: &Config,
744 index: Option<String>,
745 reg: Option<String>,
746) -> CargoResult<SourceId> {
747 match (reg, index) {
748 (Some(r), _) => SourceId::alt_registry(config, &r),
749 (_, Some(i)) => SourceId::for_registry(&i.into_url()?),
750 _ => {
751 let map = SourceConfigMap::new(config)?;
752 let src = map.load(SourceId::crates_io(config)?, &HashSet::new())?;
753 Ok(src.replaced_source_id())
754 }
755 }
756}
757
758pub fn search(
759 query: &str,
760 config: &Config,
761 index: Option<String>,
762 limit: u32,
763 reg: Option<String>,
764) -> CargoResult<()> {
765 fn truncate_with_ellipsis(s: &str, max_width: usize) -> String {
766 let mut chars = s.chars();
770 let mut prefix = (&mut chars).take(max_width - 1).collect::<String>();
771 if chars.next().is_some() {
772 prefix.push('…');
773 }
774 prefix
775 }
776
777 let (mut registry, source_id) = registry(config, None, index, reg, false, false)?;
778 let (crates, total_crates) = registry
779 .search(query, limit)
780 .chain_err(|| "failed to retrieve search results from the registry")?;
781
782 let names = crates
783 .iter()
784 .map(|krate| format!("{} = \"{}\"", krate.name, krate.max_version))
785 .collect::<Vec<String>>();
786
787 let description_margin = names.iter().map(|s| s.len() + 4).max().unwrap_or_default();
788
789 let description_length = cmp::max(80, 128 - description_margin);
790
791 let descriptions = crates.iter().map(|krate| {
792 krate
793 .description
794 .as_ref()
795 .map(|desc| truncate_with_ellipsis(&desc.replace("\n", " "), description_length))
796 });
797
798 for (name, description) in names.into_iter().zip(descriptions) {
799 let line = match description {
800 Some(desc) => {
801 let space = repeat(' ')
802 .take(description_margin - name.len())
803 .collect::<String>();
804 name + &space + "# " + &desc
805 }
806 None => name,
807 };
808 println!("{}", line);
809 }
810
811 let search_max_limit = 100;
812 if total_crates > limit && limit < search_max_limit {
813 println!(
814 "... and {} crates more (use --limit N to see more)",
815 total_crates - limit
816 );
817 } else if total_crates > limit && limit >= search_max_limit {
818 let extra = if source_id.is_default_registry() {
819 format!(
820 " (go to https://crates.io/search?q={} to see more)",
821 percent_encode(query.as_bytes(), NON_ALPHANUMERIC)
822 )
823 } else {
824 String::new()
825 };
826 println!("... and {} crates more{}", total_crates - limit, extra);
827 }
828
829 Ok(())
830}