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