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 set_workspace_version(
144 manifest_path: &Path,
145 version: &str,
146 dry_run: bool,
147) -> CargoResult<()> {
148 let original_manifest = std::fs::read_to_string(manifest_path)?;
149 let mut manifest: toml_edit::DocumentMut = original_manifest.parse()?;
150 overwrite_toml_value(
151 manifest["workspace"]["package"]
152 .as_table_like_mut()
153 .expect("workspace.package table"),
154 "version",
155 version,
156 );
157 let manifest = manifest.to_string();
158
159 if dry_run {
160 if manifest != original_manifest {
161 let diff = crate::ops::diff::unified_diff(
162 &original_manifest,
163 &manifest,
164 manifest_path,
165 "updated",
166 );
167 log::debug!("change:\n{diff}");
168 }
169 } else {
170 atomic_write(manifest_path, &manifest)?;
171 }
172
173 Ok(())
174}
175
176pub fn ensure_owners(
177 name: &str,
178 logins: &[String],
179 registry: Option<&str>,
180 dry_run: bool,
181) -> CargoResult<()> {
182 let cargo = cargo();
183
184 let mut cmd = std::process::Command::new(&cargo);
186 cmd.arg("owner").arg(name).arg("--color=never");
187 cmd.arg("--list");
188 if let Some(registry) = registry {
189 cmd.arg("--registry");
190 cmd.arg(registry);
191 }
192 let output = cmd.output()?;
193 if !output.status.success() {
194 anyhow::bail!(
195 "failed talking to registry about crate owners: {}",
196 String::from_utf8_lossy(&output.stderr)
197 );
198 }
199 let raw = String::from_utf8(output.stdout)
200 .map_err(|_| anyhow::format_err!("unrecognized response from registry"))?;
201
202 let mut current = std::collections::BTreeSet::new();
203 for line in raw.lines() {
206 if let Some((owner, _)) = line.split_once(' ')
207 && !owner.is_empty()
208 {
209 current.insert(owner);
210 }
211 }
212
213 let expected = logins
214 .iter()
215 .map(|s| s.as_str())
216 .collect::<std::collections::BTreeSet<_>>();
217
218 let missing = expected.difference(¤t).copied().collect::<Vec<_>>();
219 if !missing.is_empty() {
220 let _ = crate::ops::shell::status(
221 "Adding",
222 format!("owners for {}: {}", name, missing.join(", ")),
223 );
224 if !dry_run {
225 let mut cmd = std::process::Command::new(&cargo);
226 cmd.arg("owner").arg(name).arg("--color=never");
227 for missing in missing {
228 cmd.arg("--add").arg(missing);
229 }
230 if let Some(registry) = registry {
231 cmd.arg("--registry");
232 cmd.arg(registry);
233 }
234 let output = cmd.output()?;
235 if !output.status.success() {
236 let _ = crate::ops::shell::warn(format!(
239 "failed to set owners for {}: {}",
240 name,
241 String::from_utf8_lossy(&output.stderr)
242 ));
243 }
244 }
245 }
246
247 let extra = current.difference(&expected).copied().collect::<Vec<_>>();
248 if !extra.is_empty() {
249 log::debug!("extra owners for {}: {}", name, extra.join(", "));
250 }
251
252 Ok(())
253}
254
255pub fn set_package_version(manifest_path: &Path, version: &str, dry_run: bool) -> CargoResult<()> {
256 let original_manifest = std::fs::read_to_string(manifest_path)?;
257 let mut manifest: toml_edit::DocumentMut = original_manifest.parse()?;
258 overwrite_toml_value(
259 manifest["package"]
260 .as_table_like_mut()
261 .expect("package table"),
262 "version",
263 version,
264 );
265 let manifest = manifest.to_string();
266
267 if dry_run {
268 if manifest != original_manifest {
269 let diff = crate::ops::diff::unified_diff(
270 &original_manifest,
271 &manifest,
272 manifest_path,
273 "updated",
274 );
275 log::debug!("change:\n{diff}");
276 }
277 } else {
278 atomic_write(manifest_path, &manifest)?;
279 }
280
281 Ok(())
282}
283
284pub fn upgrade_dependency_req(
285 manifest_name: &str,
286 manifest_path: &Path,
287 root: &Path,
288 name: &str,
289 version: &semver::Version,
290 upgrade: config::DependentVersion,
291 dry_run: bool,
292) -> CargoResult<()> {
293 let manifest_root = manifest_path
294 .parent()
295 .expect("always at least a parent dir");
296 let original_manifest = std::fs::read_to_string(manifest_path)?;
297 let mut manifest: toml_edit::DocumentMut = original_manifest.parse()?;
298
299 for dep_item in find_dependency_tables(manifest.as_table_mut())
300 .flat_map(|t| t.iter_mut().filter_map(|(_, d)| d.as_table_like_mut()))
301 .filter(|d| is_relevant(*d, manifest_root, root))
302 {
303 upgrade_req(manifest_name, dep_item, name, version, upgrade);
304 }
305
306 let manifest = manifest.to_string();
307 if manifest != original_manifest {
308 if dry_run {
309 let diff = crate::ops::diff::unified_diff(
310 &original_manifest,
311 &manifest,
312 manifest_path,
313 "updated",
314 );
315 log::debug!("change:\n{diff}");
316 } else {
317 atomic_write(manifest_path, &manifest)?;
318 }
319 }
320
321 Ok(())
322}
323
324fn find_dependency_tables(
325 root: &mut toml_edit::Table,
326) -> impl Iterator<Item = &mut dyn toml_edit::TableLike> + '_ {
327 const DEP_TABLES: &[&str] = &["dependencies", "dev-dependencies", "build-dependencies"];
328
329 root.iter_mut().flat_map(|(k, v)| {
330 if DEP_TABLES.contains(&k.get()) {
331 v.as_table_like_mut().into_iter().collect::<Vec<_>>()
332 } else if k == "workspace" {
333 v.as_table_like_mut()
334 .unwrap()
335 .iter_mut()
336 .filter_map(|(k, v)| {
337 if k.get() == "dependencies" {
338 v.as_table_like_mut()
339 } else {
340 None
341 }
342 })
343 .collect::<Vec<_>>()
344 } else if k == "target" {
345 v.as_table_like_mut()
346 .unwrap()
347 .iter_mut()
348 .flat_map(|(_, v)| {
349 v.as_table_like_mut().into_iter().flat_map(|v| {
350 v.iter_mut().filter_map(|(k, v)| {
351 if DEP_TABLES.contains(&k.get()) {
352 v.as_table_like_mut()
353 } else {
354 None
355 }
356 })
357 })
358 })
359 .collect::<Vec<_>>()
360 } else {
361 Vec::new()
362 }
363 })
364}
365
366fn is_relevant(d: &dyn toml_edit::TableLike, dep_crate_root: &Path, crate_root: &Path) -> bool {
367 if !d.contains_key("version") {
368 return false;
369 }
370 match d
371 .get("path")
372 .and_then(|i| i.as_str())
373 .and_then(|relpath| dunce::canonicalize(dep_crate_root.join(relpath)).ok())
374 {
375 Some(dep_path) => dep_path == crate_root,
376 None => false,
377 }
378}
379
380fn upgrade_req(
381 manifest_name: &str,
382 dep_item: &mut dyn toml_edit::TableLike,
383 name: &str,
384 version: &semver::Version,
385 upgrade: config::DependentVersion,
386) -> bool {
387 let version_value = if let Some(version_value) = dep_item.get("version") {
388 version_value
389 } else {
390 log::debug!("not updating path-only dependency on {name}");
391 return false;
392 };
393
394 let existing_req_str = if let Some(existing_req) = version_value.as_str() {
395 existing_req
396 } else {
397 log::debug!("unsupported dependency {name}");
398 return false;
399 };
400 let Ok(existing_req) = semver::VersionReq::parse(existing_req_str) else {
401 log::debug!("unsupported dependency req {name}={existing_req_str}");
402 return false;
403 };
404 let new_req = match upgrade {
405 config::DependentVersion::Fix => {
406 if !existing_req.matches(version) {
407 let new_req = crate::ops::version::upgrade_requirement(existing_req_str, version)
408 .ok()
409 .flatten();
410 if let Some(new_req) = new_req {
411 new_req
412 } else {
413 return false;
414 }
415 } else {
416 return false;
417 }
418 }
419 config::DependentVersion::Upgrade => {
420 let new_req = crate::ops::version::upgrade_requirement(existing_req_str, version)
421 .ok()
422 .flatten();
423 if let Some(new_req) = new_req {
424 new_req
425 } else {
426 return false;
427 }
428 }
429 };
430
431 let _ = crate::ops::shell::status(
432 "Updating",
433 format!("{manifest_name}'s dependency from {existing_req_str} to {new_req}"),
434 );
435 overwrite_toml_value(dep_item, "version", new_req);
436 true
437}
438
439fn overwrite_toml_value(
440 table: &mut dyn toml_edit::TableLike,
441 key: &str,
442 value: impl Into<toml_edit::Value>,
443) {
444 let mut value = value.into();
445 let existing = table.entry(key).or_insert_with(Default::default);
446 if let Some(existing_value) = existing.as_value() {
447 *value.decor_mut() = existing_value.decor().clone();
448 }
449 *existing = toml_edit::Item::Value(value);
450}
451
452pub fn update_lock(manifest_path: &Path) -> CargoResult<()> {
453 cargo_metadata::MetadataCommand::new()
454 .manifest_path(manifest_path)
455 .exec()?;
456
457 Ok(())
458}
459
460pub fn sort_workspace(ws_meta: &cargo_metadata::Metadata) -> Vec<&cargo_metadata::PackageId> {
461 let members: std::collections::HashSet<_> = ws_meta.workspace_members.iter().collect();
462 let dep_tree: std::collections::HashMap<_, _> = ws_meta
463 .resolve
464 .as_ref()
465 .expect("cargo-metadata resolved deps")
466 .nodes
467 .iter()
468 .filter_map(|n| {
469 if members.contains(&n.id) {
470 let non_dev_pkgs = n.deps.iter().filter_map(|dep| {
478 let dev_only = dep
479 .dep_kinds
480 .iter()
481 .all(|info| info.kind == cargo_metadata::DependencyKind::Development);
482
483 if dev_only { None } else { Some(&dep.pkg) }
484 });
485
486 Some((&n.id, non_dev_pkgs.collect()))
487 } else {
488 None
489 }
490 })
491 .collect();
492
493 let mut sorted = Vec::new();
494 let mut processed = std::collections::HashSet::new();
495 for pkg_id in ws_meta.workspace_members.iter() {
496 sort_workspace_inner(pkg_id, &dep_tree, &mut processed, &mut sorted);
497 }
498
499 sorted
500}
501
502fn sort_workspace_inner<'m>(
503 pkg_id: &'m cargo_metadata::PackageId,
504 dep_tree: &std::collections::HashMap<
505 &'m cargo_metadata::PackageId,
506 Vec<&'m cargo_metadata::PackageId>,
507 >,
508 processed: &mut std::collections::HashSet<&'m cargo_metadata::PackageId>,
509 sorted: &mut Vec<&'m cargo_metadata::PackageId>,
510) {
511 if !processed.insert(pkg_id) {
512 return;
513 }
514
515 for dep_id in dep_tree[pkg_id]
516 .iter()
517 .filter(|dep_id| dep_tree.contains_key(*dep_id))
518 {
519 sort_workspace_inner(dep_id, dep_tree, processed, sorted);
520 }
521
522 sorted.push(pkg_id);
523}
524
525fn atomic_write(path: &Path, data: &str) -> std::io::Result<()> {
526 let temp_path = path
527 .parent()
528 .unwrap_or_else(|| Path::new("."))
529 .join("Cargo.toml.work");
530 std::fs::write(&temp_path, data)?;
531 std::fs::rename(&temp_path, path)?;
532
533 Ok(())
534}
535
536#[cfg(test)]
537mod test {
538 use super::*;
539
540 #[allow(unused_imports, reason = "prelude false positive")]
541 use assert_fs::prelude::*;
542 use predicates::prelude::*;
543
544 mod set_package_version {
545 use super::*;
546
547 #[test]
548 fn succeeds() {
549 let temp = assert_fs::TempDir::new().unwrap();
550 temp.copy_from("tests/fixtures/simple", &["**"]).unwrap();
551 let manifest_path = temp.child("Cargo.toml");
552
553 let meta = cargo_metadata::MetadataCommand::new()
554 .manifest_path(manifest_path.path())
555 .exec()
556 .unwrap();
557 assert_eq!(meta.packages[0].version.to_string(), "0.1.0");
558
559 set_package_version(manifest_path.path(), "2.0.0", false).unwrap();
560
561 let meta = cargo_metadata::MetadataCommand::new()
562 .manifest_path(manifest_path.path())
563 .exec()
564 .unwrap();
565 assert_eq!(meta.packages[0].version.to_string(), "2.0.0");
566
567 temp.close().unwrap();
568 }
569 }
570
571 mod update_lock {
572 use super::*;
573
574 #[test]
575 fn in_pkg() {
576 let temp = assert_fs::TempDir::new().unwrap();
577 temp.copy_from("tests/fixtures/simple", &["**"]).unwrap();
578 let manifest_path = temp.child("Cargo.toml");
579 let lock_path = temp.child("Cargo.lock");
580
581 set_package_version(manifest_path.path(), "2.0.0", false).unwrap();
582 lock_path.assert(predicate::path::eq_file(Path::new(
583 "tests/fixtures/simple/Cargo.lock",
584 )));
585
586 update_lock(manifest_path.path()).unwrap();
587 lock_path.assert(
588 predicate::path::eq_file(Path::new("tests/fixtures/simple/Cargo.lock")).not(),
589 );
590
591 temp.close().unwrap();
592 }
593
594 #[test]
595 fn in_pure_workspace() {
596 let temp = assert_fs::TempDir::new().unwrap();
597 temp.copy_from("tests/fixtures/pure_ws", &["**"]).unwrap();
598 let manifest_path = temp.child("b/Cargo.toml");
599 let lock_path = temp.child("Cargo.lock");
600
601 set_package_version(manifest_path.path(), "2.0.0", false).unwrap();
602 lock_path.assert(predicate::path::eq_file(Path::new(
603 "tests/fixtures/pure_ws/Cargo.lock",
604 )));
605
606 update_lock(manifest_path.path()).unwrap();
607 lock_path.assert(
608 predicate::path::eq_file(Path::new("tests/fixtures/pure_ws/Cargo.lock")).not(),
609 );
610
611 temp.close().unwrap();
612 }
613
614 #[test]
615 fn in_mixed_workspace() {
616 let temp = assert_fs::TempDir::new().unwrap();
617 temp.copy_from("tests/fixtures/mixed_ws", &["**"]).unwrap();
618 let manifest_path = temp.child("Cargo.toml");
619 let lock_path = temp.child("Cargo.lock");
620
621 set_package_version(manifest_path.path(), "2.0.0", false).unwrap();
622 lock_path.assert(predicate::path::eq_file(Path::new(
623 "tests/fixtures/mixed_ws/Cargo.lock",
624 )));
625
626 update_lock(manifest_path.path()).unwrap();
627 lock_path.assert(
628 predicate::path::eq_file(Path::new("tests/fixtures/mixed_ws/Cargo.lock")).not(),
629 );
630
631 temp.close().unwrap();
632 }
633 }
634
635 mod sort_workspace {
636 use super::*;
637
638 #[test]
639 fn circular_dev_dependency() {
640 let temp = assert_fs::TempDir::new().unwrap();
641 temp.copy_from("tests/fixtures/mixed_ws", &["**"]).unwrap();
642 let manifest_path = temp.child("a/Cargo.toml");
643 manifest_path
644 .write_str(
645 r#"
646 [package]
647 name = "a"
648 version = "0.1.0"
649 authors = []
650
651 [dev-dependencies]
652 b = { path = "../" }
653 "#,
654 )
655 .unwrap();
656 let root_manifest_path = temp.child("Cargo.toml");
657 let meta = cargo_metadata::MetadataCommand::new()
658 .manifest_path(root_manifest_path.path())
659 .exec()
660 .unwrap();
661
662 let sorted = sort_workspace(&meta);
663 let root_package = meta.resolve.as_ref().unwrap().root.as_ref().unwrap();
664 assert_ne!(
665 sorted[0], root_package,
666 "The root package must not be the first one to be published."
667 );
668
669 temp.close().unwrap();
670 }
671 }
672}