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