1use std::env;
2use std::path::Path;
3
4use bstr::ByteSlice;
5use itertools::Itertools as _;
6
7use crate::config::{self, CertsSource};
8use crate::error::CargoResult;
9use crate::ops::cmd::call;
10
11#[derive(Clone, Debug)]
13pub enum Features {
14 None,
16 Selective(Vec<String>),
18 All,
20}
21
22fn cargo() -> String {
23 env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned())
24}
25
26pub fn package_content(manifest_path: &Path) -> CargoResult<Vec<std::path::PathBuf>> {
27 let mut cmd = std::process::Command::new(cargo());
28 cmd.arg("package");
29 cmd.arg("--manifest-path");
30 cmd.arg(manifest_path);
31 cmd.arg("--list");
32 cmd.arg("--allow-dirty");
34 let output = cmd.output()?;
35
36 let parent = manifest_path.parent().unwrap_or_else(|| Path::new(""));
37
38 if output.status.success() {
39 let paths = ByteSlice::lines(output.stdout.as_slice())
40 .map(|l| parent.join(l.to_path_lossy()))
41 .collect();
42 Ok(paths)
43 } else {
44 let error = String::from_utf8_lossy(&output.stderr);
45 Err(anyhow::format_err!(
46 "failed to get package content for {}: {}",
47 manifest_path.display(),
48 error
49 ))
50 }
51}
52
53pub fn publish(
54 dry_run: bool,
55 verify: bool,
56 manifest_path: &Path,
57 pkgids: &[&str],
58 features: &[&Features],
59 registry: Option<&str>,
60 target: Option<&str>,
61) -> CargoResult<bool> {
62 if pkgids.is_empty() {
63 return Ok(true);
64 }
65
66 let cargo = cargo();
67
68 let mut command: Vec<&str> = vec![
69 &cargo,
70 "publish",
71 "--manifest-path",
72 manifest_path.to_str().unwrap(),
73 ];
74
75 for pkgid in pkgids {
76 command.push("--package");
77 command.push(pkgid);
78 }
79
80 if let Some(registry) = registry {
81 command.push("--registry");
82 command.push(registry);
83 }
84
85 if dry_run {
86 command.push("--dry-run");
87 command.push("--allow-dirty");
88 }
89
90 if !verify {
91 command.push("--no-verify");
92 }
93
94 if let Some(target) = target {
95 command.push("--target");
96 command.push(target);
97 }
98
99 if features.iter().any(|f| matches!(f, Features::None)) {
100 command.push("--no-default-features");
101 }
102 if features.iter().any(|f| matches!(f, Features::All)) {
103 command.push("--all-features");
104 }
105 let selective = features
106 .iter()
107 .filter_map(|f| {
108 if let Features::Selective(f) = f {
109 Some(f)
110 } else {
111 None
112 }
113 })
114 .flatten()
115 .join(",");
116 if !selective.is_empty() {
117 command.push("--features");
118 command.push(&selective);
119 }
120
121 call(command, false)
122}
123
124pub fn is_published(
125 index: &mut crate::ops::index::CratesIoIndex,
126 registry: Option<&str>,
127 name: &str,
128 version: &str,
129 certs_source: CertsSource,
130) -> bool {
131 match index.has_krate_version(registry, name, version, certs_source) {
132 Ok(has_krate_version) => has_krate_version.unwrap_or(false),
133 Err(err) => {
134 log::warn!("failed to read metadata for {name}: {err:#}");
138 false
139 }
140 }
141}
142
143pub fn is_published_req(
144 index: &mut crate::ops::index::CratesIoIndex,
145 registry: Option<&str>,
146 name: &str,
147 version_req: &semver::VersionReq,
148 certs_source: CertsSource,
149) -> bool {
150 match index.has_krate_version_req(registry, name, version_req, certs_source) {
151 Ok(has_krate_version) => has_krate_version.unwrap_or(false),
152 Err(err) => {
153 log::warn!("failed to read metadata for {name}: {err:#}");
154 false
155 }
156 }
157}
158
159pub fn set_workspace_version(
160 manifest_path: &Path,
161 version: &str,
162 dry_run: bool,
163) -> CargoResult<()> {
164 let original_manifest = std::fs::read_to_string(manifest_path)?;
165 let mut manifest: toml_edit::DocumentMut = original_manifest.parse()?;
166 overwrite_toml_value(
167 manifest["workspace"]["package"]
168 .as_table_like_mut()
169 .expect("workspace.package table"),
170 "version",
171 version,
172 );
173 let manifest = manifest.to_string();
174
175 if dry_run {
176 if manifest != original_manifest {
177 let diff = crate::ops::diff::unified_diff(
178 &original_manifest,
179 &manifest,
180 manifest_path,
181 "updated",
182 );
183 log::debug!("change:\n{diff}");
184 }
185 } else {
186 atomic_write(manifest_path, &manifest)?;
187 }
188
189 Ok(())
190}
191
192pub fn ensure_owners(
193 name: &str,
194 logins: &[String],
195 registry: Option<&str>,
196 dry_run: bool,
197) -> CargoResult<()> {
198 let cargo = cargo();
199
200 let mut cmd = std::process::Command::new(&cargo);
202 cmd.arg("owner").arg(name).arg("--color=never");
203 cmd.arg("--list");
204 if let Some(registry) = registry {
205 cmd.arg("--registry");
206 cmd.arg(registry);
207 }
208 let output = cmd.output()?;
209 if !output.status.success() {
210 anyhow::bail!(
211 "failed talking to registry about crate owners: {}",
212 String::from_utf8_lossy(&output.stderr)
213 );
214 }
215 let raw = String::from_utf8(output.stdout)
216 .map_err(|_| anyhow::format_err!("unrecognized response from registry"))?;
217
218 let mut current = std::collections::BTreeSet::new();
219 for line in raw.lines() {
222 if let Some((owner, _)) = line.split_once(' ')
223 && !owner.is_empty()
224 {
225 current.insert(owner);
226 }
227 }
228
229 let expected = logins
230 .iter()
231 .map(|s| s.as_str())
232 .collect::<std::collections::BTreeSet<_>>();
233
234 let missing = expected.difference(¤t).copied().collect::<Vec<_>>();
235 if !missing.is_empty() {
236 let _ = crate::ops::shell::status(
237 "Adding",
238 format!("owners for {}: {}", name, missing.join(", ")),
239 );
240 if !dry_run {
241 let mut cmd = std::process::Command::new(&cargo);
242 cmd.arg("owner").arg(name).arg("--color=never");
243 for missing in missing {
244 cmd.arg("--add").arg(missing);
245 }
246 if let Some(registry) = registry {
247 cmd.arg("--registry");
248 cmd.arg(registry);
249 }
250 let output = cmd.output()?;
251 if !output.status.success() {
252 let _ = crate::ops::shell::warn(format!(
255 "failed to set owners for {}: {}",
256 name,
257 String::from_utf8_lossy(&output.stderr)
258 ));
259 }
260 }
261 }
262
263 let extra = current.difference(&expected).copied().collect::<Vec<_>>();
264 if !extra.is_empty() {
265 log::debug!("extra owners for {}: {}", name, extra.join(", "));
266 }
267
268 Ok(())
269}
270
271pub fn set_package_version(manifest_path: &Path, version: &str, dry_run: bool) -> CargoResult<()> {
272 let original_manifest = std::fs::read_to_string(manifest_path)?;
273 let mut manifest: toml_edit::DocumentMut = original_manifest.parse()?;
274 overwrite_toml_value(
275 manifest["package"]
276 .as_table_like_mut()
277 .expect("package table"),
278 "version",
279 version,
280 );
281 let manifest = manifest.to_string();
282
283 if dry_run {
284 if manifest != original_manifest {
285 let diff = crate::ops::diff::unified_diff(
286 &original_manifest,
287 &manifest,
288 manifest_path,
289 "updated",
290 );
291 log::debug!("change:\n{diff}");
292 }
293 } else {
294 atomic_write(manifest_path, &manifest)?;
295 }
296
297 Ok(())
298}
299
300pub fn upgrade_dependency_req(
301 manifest_name: &str,
302 manifest_path: &Path,
303 root: &Path,
304 name: &str,
305 version: &semver::Version,
306 upgrade: config::DependentVersion,
307 dry_run: bool,
308) -> CargoResult<()> {
309 let manifest_root = manifest_path
310 .parent()
311 .expect("always at least a parent dir");
312 let original_manifest = std::fs::read_to_string(manifest_path)?;
313 let mut manifest: toml_edit::DocumentMut = original_manifest.parse()?;
314
315 for dep_item in find_dependency_tables(manifest.as_table_mut())
316 .flat_map(|t| t.iter_mut().filter_map(|(_, d)| d.as_table_like_mut()))
317 .filter(|d| is_relevant(*d, manifest_root, root))
318 {
319 upgrade_req(manifest_name, dep_item, name, version, upgrade);
320 }
321
322 let manifest = manifest.to_string();
323 if manifest != original_manifest {
324 if dry_run {
325 let diff = crate::ops::diff::unified_diff(
326 &original_manifest,
327 &manifest,
328 manifest_path,
329 "updated",
330 );
331 log::debug!("change:\n{diff}");
332 } else {
333 atomic_write(manifest_path, &manifest)?;
334 }
335 }
336
337 Ok(())
338}
339
340fn find_dependency_tables(
341 root: &mut toml_edit::Table,
342) -> impl Iterator<Item = &mut dyn toml_edit::TableLike> + '_ {
343 const DEP_TABLES: &[&str] = &["dependencies", "dev-dependencies", "build-dependencies"];
344
345 root.iter_mut().flat_map(|(k, v)| {
346 if DEP_TABLES.contains(&k.get()) {
347 v.as_table_like_mut().into_iter().collect::<Vec<_>>()
348 } else if k == "workspace" {
349 v.as_table_like_mut()
350 .unwrap()
351 .iter_mut()
352 .filter_map(|(k, v)| {
353 if k.get() == "dependencies" {
354 v.as_table_like_mut()
355 } else {
356 None
357 }
358 })
359 .collect::<Vec<_>>()
360 } else if k == "target" {
361 v.as_table_like_mut()
362 .unwrap()
363 .iter_mut()
364 .flat_map(|(_, v)| {
365 v.as_table_like_mut().into_iter().flat_map(|v| {
366 v.iter_mut().filter_map(|(k, v)| {
367 if DEP_TABLES.contains(&k.get()) {
368 v.as_table_like_mut()
369 } else {
370 None
371 }
372 })
373 })
374 })
375 .collect::<Vec<_>>()
376 } else {
377 Vec::new()
378 }
379 })
380}
381
382fn is_relevant(d: &dyn toml_edit::TableLike, dep_crate_root: &Path, crate_root: &Path) -> bool {
383 if !d.contains_key("version") {
384 return false;
385 }
386 match d
387 .get("path")
388 .and_then(|i| i.as_str())
389 .and_then(|relpath| dunce::canonicalize(dep_crate_root.join(relpath)).ok())
390 {
391 Some(dep_path) => dep_path == crate_root,
392 None => false,
393 }
394}
395
396fn upgrade_req(
397 manifest_name: &str,
398 dep_item: &mut dyn toml_edit::TableLike,
399 name: &str,
400 version: &semver::Version,
401 upgrade: config::DependentVersion,
402) -> bool {
403 let version_value = if let Some(version_value) = dep_item.get("version") {
404 version_value
405 } else {
406 log::debug!("not updating path-only dependency on {name}");
407 return false;
408 };
409
410 let existing_req_str = if let Some(existing_req) = version_value.as_str() {
411 existing_req
412 } else {
413 log::debug!("unsupported dependency {name}");
414 return false;
415 };
416 let Ok(existing_req) = semver::VersionReq::parse(existing_req_str) else {
417 log::debug!("unsupported dependency req {name}={existing_req_str}");
418 return false;
419 };
420 let new_req = match upgrade {
421 config::DependentVersion::Fix => {
422 if !existing_req.matches(version) {
423 let new_req = crate::ops::version::upgrade_requirement(existing_req_str, version)
424 .ok()
425 .flatten();
426 if let Some(new_req) = new_req {
427 new_req
428 } else {
429 return false;
430 }
431 } else {
432 return false;
433 }
434 }
435 config::DependentVersion::Upgrade => {
436 let new_req = crate::ops::version::upgrade_requirement(existing_req_str, version)
437 .ok()
438 .flatten();
439 if let Some(new_req) = new_req {
440 new_req
441 } else {
442 return false;
443 }
444 }
445 };
446
447 let _ = crate::ops::shell::status(
448 "Updating",
449 format!("{manifest_name}'s dependency from {existing_req_str} to {new_req}"),
450 );
451 overwrite_toml_value(dep_item, "version", new_req);
452 true
453}
454
455fn overwrite_toml_value(
456 table: &mut dyn toml_edit::TableLike,
457 key: &str,
458 value: impl Into<toml_edit::Value>,
459) {
460 let mut value = value.into();
461 let existing = table.entry(key).or_insert_with(Default::default);
462 if let Some(existing_value) = existing.as_value() {
463 *value.decor_mut() = existing_value.decor().clone();
464 }
465 *existing = toml_edit::Item::Value(value);
466}
467
468pub fn update_lock(manifest_path: &Path) -> CargoResult<()> {
469 cargo_metadata::MetadataCommand::new()
470 .manifest_path(manifest_path)
471 .exec()?;
472
473 Ok(())
474}
475
476pub fn sort_workspace(ws_meta: &cargo_metadata::Metadata) -> Vec<&cargo_metadata::PackageId> {
477 let members: std::collections::HashSet<_> = ws_meta.workspace_members.iter().collect();
478 let dep_tree: std::collections::HashMap<_, _> = ws_meta
479 .resolve
480 .as_ref()
481 .expect("cargo-metadata resolved deps")
482 .nodes
483 .iter()
484 .filter_map(|n| {
485 if members.contains(&n.id) {
486 let non_dev_pkgs = n.deps.iter().filter_map(|dep| {
494 let dev_only = dep
495 .dep_kinds
496 .iter()
497 .all(|info| info.kind == cargo_metadata::DependencyKind::Development);
498
499 if dev_only { None } else { Some(&dep.pkg) }
500 });
501
502 Some((&n.id, non_dev_pkgs.collect()))
503 } else {
504 None
505 }
506 })
507 .collect();
508
509 let mut sorted = Vec::new();
510 let mut processed = std::collections::HashSet::new();
511 for pkg_id in ws_meta.workspace_members.iter() {
512 sort_workspace_inner(pkg_id, &dep_tree, &mut processed, &mut sorted);
513 }
514
515 sorted
516}
517
518fn sort_workspace_inner<'m>(
519 pkg_id: &'m cargo_metadata::PackageId,
520 dep_tree: &std::collections::HashMap<
521 &'m cargo_metadata::PackageId,
522 Vec<&'m cargo_metadata::PackageId>,
523 >,
524 processed: &mut std::collections::HashSet<&'m cargo_metadata::PackageId>,
525 sorted: &mut Vec<&'m cargo_metadata::PackageId>,
526) {
527 if !processed.insert(pkg_id) {
528 return;
529 }
530
531 for dep_id in dep_tree[pkg_id]
532 .iter()
533 .filter(|dep_id| dep_tree.contains_key(*dep_id))
534 {
535 sort_workspace_inner(dep_id, dep_tree, processed, sorted);
536 }
537
538 sorted.push(pkg_id);
539}
540
541fn atomic_write(path: &Path, data: &str) -> std::io::Result<()> {
542 let temp_path = path
543 .parent()
544 .unwrap_or_else(|| Path::new("."))
545 .join("Cargo.toml.work");
546 std::fs::write(&temp_path, data)?;
547 std::fs::rename(&temp_path, path)?;
548
549 Ok(())
550}
551
552#[cfg(test)]
553mod test {
554 use super::*;
555
556 #[allow(unused_imports, reason = "prelude false positive")]
557 use assert_fs::prelude::*;
558 use predicates::prelude::*;
559
560 mod set_package_version {
561 use super::*;
562
563 #[test]
564 fn succeeds() {
565 let temp = assert_fs::TempDir::new().unwrap();
566 temp.copy_from("tests/fixtures/simple", &["**"]).unwrap();
567 let manifest_path = temp.child("Cargo.toml");
568
569 let meta = cargo_metadata::MetadataCommand::new()
570 .manifest_path(manifest_path.path())
571 .exec()
572 .unwrap();
573 assert_eq!(meta.packages[0].version.to_string(), "0.1.0");
574
575 set_package_version(manifest_path.path(), "2.0.0", false).unwrap();
576
577 let meta = cargo_metadata::MetadataCommand::new()
578 .manifest_path(manifest_path.path())
579 .exec()
580 .unwrap();
581 assert_eq!(meta.packages[0].version.to_string(), "2.0.0");
582
583 temp.close().unwrap();
584 }
585 }
586
587 mod update_lock {
588 use super::*;
589
590 #[test]
591 fn in_pkg() {
592 let temp = assert_fs::TempDir::new().unwrap();
593 temp.copy_from("tests/fixtures/simple", &["**"]).unwrap();
594 let manifest_path = temp.child("Cargo.toml");
595 let lock_path = temp.child("Cargo.lock");
596
597 set_package_version(manifest_path.path(), "2.0.0", false).unwrap();
598 lock_path.assert(predicate::path::eq_file(Path::new(
599 "tests/fixtures/simple/Cargo.lock",
600 )));
601
602 update_lock(manifest_path.path()).unwrap();
603 lock_path.assert(
604 predicate::path::eq_file(Path::new("tests/fixtures/simple/Cargo.lock")).not(),
605 );
606
607 temp.close().unwrap();
608 }
609
610 #[test]
611 fn in_pure_workspace() {
612 let temp = assert_fs::TempDir::new().unwrap();
613 temp.copy_from("tests/fixtures/pure_ws", &["**"]).unwrap();
614 let manifest_path = temp.child("b/Cargo.toml");
615 let lock_path = temp.child("Cargo.lock");
616
617 set_package_version(manifest_path.path(), "2.0.0", false).unwrap();
618 lock_path.assert(predicate::path::eq_file(Path::new(
619 "tests/fixtures/pure_ws/Cargo.lock",
620 )));
621
622 update_lock(manifest_path.path()).unwrap();
623 lock_path.assert(
624 predicate::path::eq_file(Path::new("tests/fixtures/pure_ws/Cargo.lock")).not(),
625 );
626
627 temp.close().unwrap();
628 }
629
630 #[test]
631 fn in_mixed_workspace() {
632 let temp = assert_fs::TempDir::new().unwrap();
633 temp.copy_from("tests/fixtures/mixed_ws", &["**"]).unwrap();
634 let manifest_path = temp.child("Cargo.toml");
635 let lock_path = temp.child("Cargo.lock");
636
637 set_package_version(manifest_path.path(), "2.0.0", false).unwrap();
638 lock_path.assert(predicate::path::eq_file(Path::new(
639 "tests/fixtures/mixed_ws/Cargo.lock",
640 )));
641
642 update_lock(manifest_path.path()).unwrap();
643 lock_path.assert(
644 predicate::path::eq_file(Path::new("tests/fixtures/mixed_ws/Cargo.lock")).not(),
645 );
646
647 temp.close().unwrap();
648 }
649 }
650
651 mod sort_workspace {
652 use super::*;
653
654 #[test]
655 fn circular_dev_dependency() {
656 let temp = assert_fs::TempDir::new().unwrap();
657 temp.copy_from("tests/fixtures/mixed_ws", &["**"]).unwrap();
658 let manifest_path = temp.child("a/Cargo.toml");
659 manifest_path
660 .write_str(
661 r#"
662 [package]
663 name = "a"
664 version = "0.1.0"
665 authors = []
666
667 [dev-dependencies]
668 b = { path = "../" }
669 "#,
670 )
671 .unwrap();
672 let root_manifest_path = temp.child("Cargo.toml");
673 let meta = cargo_metadata::MetadataCommand::new()
674 .manifest_path(root_manifest_path.path())
675 .exec()
676 .unwrap();
677
678 let sorted = sort_workspace(&meta);
679 let root_package = meta.resolve.as_ref().unwrap().root.as_ref().unwrap();
680 assert_ne!(
681 sorted[0], root_package,
682 "The root package must not be the first one to be published."
683 );
684
685 temp.close().unwrap();
686 }
687 }
688}