Skip to main content

cargo_release/ops/
cargo.rs

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/// Expresses what features flags should be used
12#[derive(Clone, Debug)]
13pub enum Features {
14    /// None - don't use special features
15    None,
16    /// Only use selected features
17    Selective(Vec<String>),
18    /// Use all features via `all-features`
19    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    // Not worth passing around allow_dirty to here since we are just getting a file list.
33    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            // For both http and git indices, this _might_ be an error that goes away in
135            // a future call, but at least printing out something should give the user
136            // an indication something is amiss
137            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    // "Look-before-you-leap" in case the user has permission to publish but not set owners.
201    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    // HACK: No programmatic CLI access and don't want to link against `cargo` (yet), so parsing
220    // text output
221    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(&current).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                // HACK: Can't error as the user might not have permission to set owners and we can't
253                // tell what the error was without parsing it
254                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                // Ignore dev dependencies. This breaks dev dependency cycles and allows for
487                // correct publishing order when a workspace package depends on the root package.
488
489                // It would be more correct to ignore only dev dependencies without a version
490                // field specified. However, cargo_metadata exposes only the resolved version of
491                // a package, and not what semver range (if any) is requested in Cargo.toml.
492
493                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}