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