semantic_release_cargo/
lib.rs

1// Copyright 2020 Steven Bosnick
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE-2.0 or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9//! Implementation of the semantic release steps to for integrating a cargo-based Rust
10//! project.
11
12#![forbid(unsafe_code)]
13#![deny(warnings, missing_docs)]
14
15use std::{
16    collections::HashMap,
17    env, fmt, fs,
18    io::{BufRead, Cursor},
19    path::{Path, PathBuf},
20    process::Command,
21    result,
22};
23
24use guppy::{
25    graph::{DependencyDirection, PackageGraph, PackageLink, PackageMetadata, PackageSource},
26    MetadataCommand, PackageId,
27};
28use log::{debug, error, info, log, trace, Level};
29use serde::Serialize;
30use toml_edit::{DocumentMut, InlineTable, Item, Table, Value};
31use url::Url;
32
33#[cfg(feature = "napi-rs")]
34use napi_derive::napi;
35
36mod error;
37mod itertools;
38mod logger;
39
40pub use error::{CargoTomlError, Error, Result};
41
42pub use logger::LoggerBuilder;
43
44use crate::itertools::Itertools;
45
46/// Verify that the conditions for a release are satisfied.
47///
48/// The conditions for a release checked by this function are:
49///
50///    1. That the cargo registry token has been defined.
51///    2. That it can construct the graph of all of the dependencies in the
52///       workspace.
53///    3. That the dependencies and build-dependencies of all of crates in the
54///       workspace are suitable for publishing to `crates.io`.
55///
56/// If `manifest_path` is provided then it is expect to give the path to the
57/// `Cargo.toml` file for the root of the workspace. If `manifest_path` is `None`
58/// then `verify_conditions` will look for the root of the workspace in a
59/// `Cargo.toml` file in the current directory. If one of the conditions for a
60/// release are not satisfied then an explanation for that will be written to
61/// `output`.
62///
63/// This implements the `verifyConditions` step for `semantic-release` for a
64/// Cargo-based rust workspace.
65#[cfg(feature = "napi-rs")]
66#[napi]
67pub fn verify_conditions() -> Result<()> {
68    let maybe_manifest_path: Option<&'static str> = None;
69
70    internal_verify_conditions(None, maybe_manifest_path)
71}
72
73/// Verify that the conditions for a release are satisfied.
74///
75/// The conditions for a release checked by this function are:
76///
77///    1. That the cargo registry token has been defined.
78///    2. That it can construct the graph of all of the dependencies in the
79///       workspace.
80///    3. That the dependencies and build-dependencies of all of crates in the
81///       workspace are suitable for publishing to `crates.io`.
82///
83/// If `manifest_path` is provided then it is expect to give the path to the
84/// `Cargo.toml` file for the root of the workspace. If `manifest_path` is `None`
85/// then `verify_conditions` will look for the root of the workspace in a
86/// `Cargo.toml` file in the current directory. If one of the conditions for a
87/// release are not satisfied then an explanation for that will be written to
88/// `output`.
89///
90/// This implements the `verifyConditions` step for `semantic-release` for a
91/// Cargo-based rust workspace.
92#[cfg(not(feature = "napi-rs"))]
93pub fn verify_conditions(manifest_path: Option<impl AsRef<Path>>) -> Result<()> {
94    internal_verify_conditions(None, manifest_path)
95}
96
97/// Verify that the conditions for a release are satisfied.
98///
99/// The conditions for a release checked by this function are:
100///
101///    1. That the cargo registry token has been defined and is non-empty, if
102///       the registry field is not set. Otherwise, that the alternate registry
103///       token has been defined and is non-empty.
104///    2. That it can construct the graph of all of the dependencies in the
105///       workspace.
106///    3. That the dependencies and build-dependencies of all of crates in the
107///       workspace are suitable for publishing to `crates.io`.
108///
109/// If `alternate_registry` is provided then it is expected to point to an
110/// [alternate registry](https://doc.rust-lang.org/cargo/reference/registries.html#using-an-alternate-registry)
111/// defined in a cargo.toml file.
112/// If `manifest_path` is provided then it is expect to give the path to the
113/// `Cargo.toml` file for the root of the workspace. If `manifest_path` is `None`
114/// then `verify_conditions` will look for the root of the workspace in a
115/// `Cargo.toml` file in the current directory. If one of the conditions for a
116/// release are not satisfied then an explanation for that will be written to
117/// `output`.
118///
119/// This implements the `verifyConditions` step for `semantic-release` for a
120/// Cargo-based rust workspace.
121#[cfg(not(feature = "napi-rs"))]
122pub fn verify_conditions_with_alternate(
123    alternate_registry: Option<&str>,
124    manifest_path: Option<impl AsRef<Path>>,
125) -> Result<()> {
126    internal_verify_conditions(alternate_registry, manifest_path)
127}
128
129fn internal_verify_conditions(
130    alternate_registry: Option<&str>,
131    manifest_path: Option<impl AsRef<Path>>,
132) -> Result<()> {
133    let cargo_config = cargo_config2::Config::load()?;
134
135    let registry_token_set = match alternate_registry {
136        Some(alternate_registry_id) => {
137            // The key can be both uppercased or lowercased depending on the
138            // source, uppercase if from environment variables, so we try both.
139            let registry_value = cargo_config
140                .registries
141                .get(alternate_registry_id)
142                .or_else(|| {
143                    let uppercased_registry = alternate_registry_id.to_uppercase();
144                    cargo_config.registries.get(&uppercased_registry)
145                });
146
147            registry_value.and_then(|registry| registry.token.as_ref().map(|_| ()))
148        }
149        None => cargo_config.registry.token.map(|_| ()),
150    };
151
152    debug!("Checking cargo registry token is set");
153    registry_token_set.ok_or_else(|| {
154        let registry_id = alternate_registry.unwrap_or("crates-io");
155
156        Error::verify_error(format!(
157            "Registry token for {} empty or not set.",
158            &registry_id
159        ))
160    })?;
161
162    debug!("Checking that workspace dependencies graph is buildable");
163    let graph = get_package_graph(manifest_path)?;
164
165    debug!("Checking that the workspace does not contain any cycles");
166    if let Some(cycle) = graph.cycles().all_cycles().next() {
167        assert!(cycle.len() >= 2);
168        let crate0 = get_crate_name(&graph, cycle[0]);
169        let crate1 = get_crate_name(&graph, cycle[1]);
170        let workspace_error = Error::WorkspaceCycles {
171            crate1: crate0.to_owned(),
172            crate2: crate1.to_owned(),
173        };
174
175        return Err(workspace_error.into());
176    }
177
178    debug!("Checking that dependencies are suitable for publishing");
179    for (from, links) in graph
180        .workspace()
181        .iter()
182        .flat_map(|package| package.direct_links())
183        .filter(|link| !link_is_publishable(link))
184        .chunk_by(PackageLink::from)
185        .into_iter()
186    {
187        debug!("Checking links for package {}", from.name());
188        let cargo = read_cargo_toml(from.manifest_path().as_std_path())?;
189        for link in links {
190            if link.normal().is_present() {
191                dependency_has_version(&cargo, &link, DependencyType::Normal)?;
192            }
193            if link.build().is_present() {
194                dependency_has_version(&cargo, &link, DependencyType::Build)?;
195            }
196        }
197    }
198
199    Ok(())
200}
201
202/// Prepare the Rust workspace for a release.
203///
204/// Preparing the release updates the version of each crate in the workspace and of
205/// the intra-workspace dependencies. The `version` field in the `packages` table of
206/// each `Cargo.toml` file in the workspace is set to the supplied version. The
207/// `version` field of each dependency, build-dependency and dev-dependency that
208/// is otherwise identified by a workspace-relative path dependencies is also set
209/// to the supplied version (the version filed will be added if it isn't
210/// already present).
211///
212/// This implements the `prepare` step for `semantic-release` for a Cargo-based Rust
213/// workspace.
214#[cfg(feature = "napi-rs")]
215#[napi]
216pub fn prepare(next_release_version: String) -> Result<()> {
217    let manifest_path: Option<&Path> = None;
218    internal_prepare(manifest_path, next_release_version)
219}
220
221/// Prepare the Rust workspace for a release.
222///
223/// Preparing the release updates the version of each crate in the workspace and of
224/// the intra-workspace dependencies. The `version` field in the `packages` table of
225/// each `Cargo.toml` file in the workspace is set to the supplied version. The
226/// `version` field of each dependency, build-dependency and dev-dependency that
227/// is otherwise identified by a workspace-relative path dependencies is also set
228/// to the supplied version (the version filed will be added if it isn't
229/// already present).
230///
231/// This implements the `prepare` step for `semantic-release` for a Cargo-based Rust
232/// workspace.
233#[cfg(not(feature = "napi-rs"))]
234pub fn prepare(manifest_path: Option<&Path>, next_release_version: String) -> Result<()> {
235    internal_prepare(manifest_path, next_release_version)
236}
237
238fn internal_prepare(manifest_path: Option<&Path>, next_release_version: String) -> Result<()> {
239    debug!("Building package graph");
240    let graph = get_package_graph(manifest_path)?;
241
242    let link_map = graph
243        .workspace()
244        .iter()
245        .flat_map(|package| package.direct_links())
246        // check that the link neither only a dev dependency or a dev
247        // dependency with an explicit version
248        .filter(|link| !link.dev_only() || !link.version_req().comparators.is_empty())
249        .filter(|link| link.to().in_workspace())
250        .map(|link| (link.from().id(), link))
251        .into_group_map();
252
253    debug!("Setting version information for packages in the workspace.");
254    for package in graph.workspace().iter() {
255        let path = package.manifest_path();
256        debug!("reading {}", path.as_str());
257        let mut cargo = read_cargo_toml(path.as_std_path())?;
258
259        info!(
260            "Setting the version of {} to {}",
261            package.name(),
262            &next_release_version
263        );
264        set_package_version(&mut cargo, &next_release_version)
265            .map_err(|err| err.into_error(path))?;
266
267        if let Some(links) = link_map.get(package.id()) {
268            for link in links {
269                if link.normal().is_present() {
270                    info!(
271                        "Upgrading dependency of {} to {}@{}",
272                        link.to().name(),
273                        package.name(),
274                        &next_release_version
275                    );
276                    set_dependencies_version(
277                        &mut cargo,
278                        &next_release_version,
279                        DependencyType::Normal,
280                        link.to().name(),
281                    )
282                    .map_err(|err| err.into_error(path))?;
283                }
284                if link.build().is_present() {
285                    info!(
286                        "Upgrading build-dependency of {} to {}@{}",
287                        link.to().name(),
288                        package.name(),
289                        &next_release_version
290                    );
291                    set_dependencies_version(
292                        &mut cargo,
293                        &next_release_version,
294                        DependencyType::Build,
295                        link.to().name(),
296                    )
297                    .map_err(|err| err.into_error(path))?;
298                }
299                if link.dev().is_present() {
300                    info!(
301                        "Upgrading dev-dependency of {} to {}@{}",
302                        link.to().name(),
303                        package.name(),
304                        &next_release_version
305                    );
306                    set_dependencies_version(
307                        &mut cargo,
308                        &next_release_version,
309                        DependencyType::Dev,
310                        link.to().name(),
311                    )
312                    .map_err(|err| err.into_error(path))?;
313                }
314            }
315        }
316
317        debug!("writing {}", path.as_str());
318        write_cargo_toml(path.as_std_path(), cargo)?;
319
320        // Update the lockfile metadata.
321        //
322        // This code currently only updates the version number of the crate's
323        // self-describing metadata.
324        //
325        // Unsupported: updating metadata of in-workspace dependencies. I
326        // didn't take a stab at this yet because I don't have this issue
327        // personal yet, and without a repository in which I can reproduce
328        // this problem I think it's most responsible to keep the code simple
329        // and readable.
330        let lockfile_path = get_cargo_lock(path.as_std_path());
331        if lockfile_path.exists() {
332            debug!("reading {}", lockfile_path.to_string_lossy());
333            let mut lockfile = read_cargo_toml(&lockfile_path)?;
334
335            set_lockfile_self_describing_metadata(
336                &mut lockfile,
337                &next_release_version,
338                package.name(),
339            )?;
340
341            debug!("writing {}", lockfile_path.to_string_lossy());
342            write_cargo_toml(&lockfile_path, lockfile)?;
343        }
344    }
345
346    Ok(())
347}
348
349#[cfg_attr(feature = "napi-rs", napi(object))]
350#[derive(Debug, Default)]
351/// Arguments to be passed to the `publish` function.
352pub struct PublishArgs {
353    /// Whether the `--no-dirty` flag should be passed to `cargo publish`.
354    pub no_dirty: Option<bool>,
355
356    /// A map of packages and features to pass to `cargo publish`.
357    pub features: Option<HashMap<String, Vec<String>>>,
358
359    /// Optionally passes a `--registry` flag `cargo publish`.
360    pub registry: Option<String>,
361}
362
363/// Publish the publishable crates from the workspace.
364///
365/// The publishable crates are the crates in the workspace other than those
366/// whose `package.publish` field is set to `false` or that includes a registry other
367/// than `crates.io`.
368///
369/// This implements the `publish` step for `semantic-release` for a Cargo-based
370/// Rust workspace.
371#[cfg(feature = "napi-rs")]
372#[napi]
373pub fn publish(opts: Option<PublishArgs>) -> Result<()> {
374    let manifest_path: Option<&Path> = None;
375    internal_publish(manifest_path, &opts.unwrap_or_default())
376}
377
378/// Publish the publishable crates from the workspace.
379///
380/// The publishable crates are the crates in the workspace other than those
381/// whose `package.publish` field is set to `false` or that includes a registry other
382/// than `crates.io`.
383///
384/// This implements the `publish` step for `semantic-release` for a Cargo-based
385/// Rust workspace.
386#[cfg(not(feature = "napi-rs"))]
387pub fn publish(manifest_path: Option<&Path>, opts: &PublishArgs) -> Result<()> {
388    internal_publish(manifest_path, opts)
389}
390
391fn internal_publish(manifest_path: Option<&Path>, opts: &PublishArgs) -> Result<()> {
392    debug!("Getting the package graph");
393    let graph = get_package_graph(manifest_path)?;
394    let optional_registry = opts.registry.as_deref();
395
396    let mut count = 0;
397    let mut last_id = None;
398
399    process_publishable_packages(&graph, optional_registry, |pkg| {
400        count += 1;
401        last_id = Some(pkg.id().clone());
402        publish_package(pkg, opts)
403    })?;
404
405    let main_crate = match graph.workspace().member_by_path("") {
406        Ok(pkg) if package_is_publishable(&pkg, optional_registry) => Some(pkg.name()),
407        _ => last_id.map(|id| {
408            graph
409                .metadata(&id)
410                .expect("id of a processed package not found in the package graph")
411                .name()
412        }),
413    };
414
415    if let Some(main_crate) = main_crate {
416        debug!("printing release record with main crate: {}", main_crate);
417        let name = format!(
418            "{} packages ({} packages published)",
419            optional_registry.unwrap_or("crates.io"),
420            count
421        );
422
423        // format the release metadata for writing to json
424        let release_meta_json = if optional_registry.is_none() {
425            serde_json::to_string(&Release::new_crates_io_release(name, main_crate)?)
426        } else {
427            serde_json::to_string(&Release::new::<&str>(name, None, main_crate)?)
428        }
429        .map_err(|err| Error::write_release_error(err, main_crate))?;
430
431        info!("{:?}", release_meta_json);
432    } else {
433        debug!("no release record to print");
434    }
435
436    Ok(())
437}
438
439/// List the packages from the workspace in the order of their dependencies.
440///
441/// The list of packages will be written to `output`. If `manifest_path` is provided
442/// then it is expected to give the path to the `Cargo.toml` file for the root of the
443/// workspace. If `manifest_path` is `None` then `list_packages` will look for the
444/// root of the workspace in a `Cargo.toml` file in the current directory.
445///
446/// This is a debuging aid and does not directly correspond to a semantic release
447/// step.
448pub fn list_packages(manifest_path: Option<impl AsRef<Path>>) -> Result<()> {
449    internal_list_packages(None, manifest_path)
450}
451
452/// List the packages from the workspace in the order of their dependencies as
453/// matched against an argument set.
454///
455/// The list of packages will be written to `output`. If `manifest_path` is provided
456/// then it is expected to give the path to the `Cargo.toml` file for the root of the
457/// workspace. If `manifest_path` is `None` then `list_packages` will look for the
458/// root of the workspace in a `Cargo.toml` file in the current directory.
459///
460/// This is a debuging aid and does not directly correspond to a semantic release
461/// step.
462pub fn list_packages_with_arguments(
463    alternate_registry: Option<&str>,
464    manifest_path: Option<impl AsRef<Path>>,
465) -> Result<()> {
466    internal_list_packages(alternate_registry, manifest_path)
467}
468
469fn internal_list_packages(
470    alternate_registry: Option<&str>,
471    manifest_path: Option<impl AsRef<Path>>,
472) -> Result<()> {
473    info!("Building package graph");
474    let graph = get_package_graph(manifest_path)?;
475
476    process_publishable_packages(&graph, alternate_registry, |pkg| {
477        error!("{}({})", pkg.name(), pkg.version());
478        Ok(())
479    })
480}
481
482fn get_package_graph(manifest_path: Option<impl AsRef<Path>>) -> Result<PackageGraph> {
483    let manifest_path = manifest_path.as_ref().map(|path| path.as_ref());
484
485    let mut command = MetadataCommand::new();
486    if let Some(path) = manifest_path {
487        command.manifest_path(path);
488    }
489
490    debug!("manifest_path: {:?}", manifest_path);
491
492    command.build_graph().map_err(|err| {
493        let path = match manifest_path {
494            Some(path) => path.to_path_buf(),
495            None => env::current_dir()
496                .map(|path| path.join("Cargo.toml"))
497                .unwrap_or_else(|e| {
498                    error!("Unable to get current directory: {}", e);
499                    PathBuf::from("unknown manifest")
500                }),
501        };
502        Error::workspace_error(err, path).into()
503    })
504}
505
506/// Is the source of the target of a dependencies publishable?
507///
508/// The target of a dependencies must be available on `crates.io` for the depending
509/// package to be publishable. Workspace relative path dependencies will be published
510/// before their depended on crates and the dependencies in the depended on crate
511/// will have their `version` adjusted so those dependencies will be on `crates.io`
512/// by the time the depended on crate is published.
513fn target_source_is_publishable(source: PackageSource) -> bool {
514    source.is_workspace() || source.is_crates_io()
515}
516
517/// Will this link prevent the `link.from()` package from being published.
518///
519/// `dev-dependencies` links will not prevent publication. For all other links the
520/// target of the link must be either already on `crates.io` or it must be a
521/// workspace relative path dependency (which will be published first).
522fn link_is_publishable(link: &PackageLink) -> bool {
523    let result = link.dev_only() || target_source_is_publishable(link.to().source());
524    if result {
525        trace!(
526            "Link from {} to {} is publishable.",
527            link.from().name(),
528            link.to().name()
529        );
530    }
531
532    result
533}
534
535/// Is a particular package publishable.
536///
537/// A package is publishable if either publication is unrestricted or it can be
538/// published to one registry.
539fn package_is_publishable(pkg: &PackageMetadata, registry: Option<&str>) -> bool {
540    use guppy::graph::PackagePublish;
541    let registry_target = registry;
542
543    let result = match pkg.publish() {
544        guppy::graph::PackagePublish::Unrestricted => true,
545        guppy::graph::PackagePublish::Registries([registry]) => {
546            let registry_target = registry_target.unwrap_or(PackagePublish::CRATES_IO);
547            registry == registry_target
548        }
549        guppy::graph::PackagePublish::Registries([]) => false,
550        _ => todo!(),
551    };
552
553    if result {
554        trace!("package {} is publishable", pkg.name());
555    }
556
557    result
558}
559
560fn process_publishable_packages<F>(
561    graph: &PackageGraph,
562    alternate_registry: Option<&str>,
563    mut f: F,
564) -> Result<()>
565where
566    F: FnMut(&PackageMetadata) -> Result<()>,
567{
568    info!("iterating the workspace crates in dependency order");
569    for pkg in graph
570        .query_workspace()
571        .resolve_with_fn(|_, link| !link.dev_only())
572        .packages(DependencyDirection::Reverse)
573        .filter(|pkg| pkg.in_workspace() && package_is_publishable(pkg, alternate_registry))
574    {
575        f(&pkg)?;
576    }
577
578    Ok(())
579}
580
581// Panics if id is not from graph
582fn get_crate_name<'a>(graph: &'a PackageGraph, id: &PackageId) -> &'a str {
583    graph
584        .metadata(id)
585        .unwrap_or_else(|_| panic!("id {} was not found in the graph {:?}", id, graph))
586        .name()
587}
588
589fn publish_package(pkg: &PackageMetadata, opts: &PublishArgs) -> Result<()> {
590    info!(
591        "Publishing version {} of {} to {} registry",
592        pkg.version(),
593        pkg.name(),
594        opts.registry.as_deref().unwrap_or("crates.io")
595    );
596
597    let cargo = env::var("CARGO")
598        .map(PathBuf::from)
599        .unwrap_or_else(|_| PathBuf::from("cargo"));
600
601    let mut command = Command::new(cargo);
602    command
603        .args(["publish", "--manifest-path"])
604        .arg(pkg.manifest_path());
605    if !opts.no_dirty.unwrap_or_default() {
606        command.arg("--allow-dirty");
607    }
608    if let Some(features) = opts.features.as_ref().and_then(|f| f.get(pkg.name())) {
609        command.arg("--features");
610        command.args(features);
611    }
612    if let Some(registry) = opts.registry.as_ref() {
613        command.arg("--registry");
614        command.arg(registry);
615    }
616
617    trace!("running: {:?}", command);
618
619    let output = command
620        .output()
621        .map_err(|err| Error::cargo_publish(err, pkg.manifest_path().as_std_path()))?;
622
623    let level = if output.status.success() {
624        Level::Trace
625    } else {
626        Level::Info
627    };
628
629    trace!("cargo publish stdout");
630    trace!("--------------------");
631    log_bytes(Level::Trace, &output.stdout);
632
633    log!(level, "cargo publish stderr");
634    log!(level, "--------------------");
635    log_bytes(level, &output.stderr);
636
637    if output.status.success() {
638        info!(
639            "Published {}@{} to {} registry",
640            pkg.name(),
641            pkg.version(),
642            opts.registry.as_deref().unwrap_or("crates.io")
643        );
644        Ok(())
645    } else {
646        error!(
647            "publishing package {} failed: {}\n{}",
648            pkg.name(),
649            output.status,
650            String::from_utf8_lossy(&output.stderr)
651        );
652        Err(Error::cargo_publish_status(
653            output.status,
654            pkg.manifest_path().as_std_path(),
655            &output.stderr,
656        )
657        .into())
658    }
659}
660
661fn log_bytes(level: Level, bytes: &[u8]) {
662    let mut buffer = Cursor::new(bytes);
663    let mut string = String::new();
664
665    while let Ok(size) = buffer.read_line(&mut string) {
666        if size == 0 {
667            return;
668        }
669        log!(level, "{}", string);
670        string.clear();
671    }
672}
673
674/// Given the path to a cargo manifest, return the path to the associated
675/// lock file. This function does not test the existence of the lockfile.
676fn get_cargo_lock(path: &Path) -> PathBuf {
677    path.parent().unwrap().join("Cargo.lock")
678}
679
680fn read_cargo_toml(path: &Path) -> Result<DocumentMut> {
681    fs::read_to_string(path)
682        .map_err(|err| Error::file_read_error(err, path))?
683        .parse()
684        .map_err(|err| Error::toml_error(err, path).into())
685}
686
687fn write_cargo_toml(path: &Path, cargo: DocumentMut) -> Result<()> {
688    fs::write(path, cargo.to_string()).map_err(|err| Error::file_write_error(err, path).into())
689}
690
691fn get_top_table<'a>(doc: &'a DocumentMut, key: &str) -> Option<&'a Table> {
692    doc.as_table().get(key).and_then(Item::as_table)
693}
694
695fn get_top_table_mut<'a>(doc: &'a mut DocumentMut, key: &str) -> Option<&'a mut Table> {
696    doc.get_key_value_mut(key)
697        .and_then(|(_key, value)| value.as_table_mut())
698}
699
700fn table_add_or_update_value(table: &mut Table, key: &str, value: Value) -> Option<()> {
701    let entry = table.entry(key);
702
703    match entry {
704        toml_edit::Entry::Occupied(mut val) => {
705            val.insert(Item::Value(value));
706            Some(())
707        }
708        toml_edit::Entry::Vacant(val) => {
709            val.insert(Item::Value(value));
710            Some(())
711        }
712    }
713}
714
715fn inline_table_add_or_update_value(table: &mut InlineTable, key: &str, value: Value) {
716    match table.get_mut(key) {
717        Some(ver) => *ver = value,
718        None => {
719            table.get_or_insert(key, value);
720        }
721    }
722}
723
724fn dependency_has_version(
725    doc: &DocumentMut,
726    link: &PackageLink,
727    typ: DependencyType,
728) -> Result<()> {
729    let top_key = typ.key();
730
731    trace!(
732        "Checking for version key for {} in {} section of {}",
733        link.to().name(),
734        top_key,
735        link.from().name()
736    );
737    get_top_table(doc, top_key)
738        .and_then(|deps| deps.get(link.to().name()))
739        .and_then(Item::as_table_like)
740        .and_then(|dep| dep.get("version"))
741        .map(|_| ())
742        .ok_or_else(|| Error::bad_dependency(link, typ).into())
743}
744
745fn set_package_version(doc: &mut DocumentMut, version: &str) -> result::Result<(), CargoTomlError> {
746    let table =
747        get_top_table_mut(doc, "package").ok_or_else(|| CargoTomlError::no_table("package"))?;
748    table_add_or_update_value(table, "version", version.into())
749        .ok_or_else(|| CargoTomlError::no_value("version"))
750}
751
752/// Finds the table key for the package table for a given dependency.
753fn find_matching_dependency_key<'table>(
754    table: &'table mut Table,
755    name: &'table str,
756) -> Option<String> {
757    for (key, dependency_item) in table.iter() {
758        // short-circuit if the item's key matches the expected name.
759        if key == name {
760            return Some(name.to_string());
761        }
762
763        // If there is no `package` key in the table jump back to the top of the
764        // loop. It should be expected that the value of this package key is a
765        // string.
766        let Some(Item::Value(Value::String(package_ident))) = dependency_item.get("package") else {
767            continue;
768        };
769
770        let Some(package_ident) = package_ident.as_repr() else {
771            continue;
772        };
773
774        let maybe_package_ident_str_repr = package_ident
775            .as_raw()
776            .as_str()
777            // If we've guaranteed it's a string repr it will be `'"'`-wrapped.
778            // Stripping these is easier for matching.
779            .map(|repr| repr.trim_matches('"'));
780        if maybe_package_ident_str_repr == Some(name) {
781            return Some(key.to_string());
782        }
783    }
784
785    None
786}
787
788fn set_dependency_version(table: &mut Table, version: &str, name: &str) -> Option<()> {
789    let dependency_key = match find_matching_dependency_key(table, name) {
790        Some(key) => key,
791        None => return Some(()),
792    };
793
794    match table.entry(&dependency_key) {
795        toml_edit::Entry::Occupied(mut req) => {
796            let item = req.get_mut();
797
798            if let Some(item) = item.as_inline_table_mut() {
799                inline_table_add_or_update_value(item, "version", version.into());
800                return Some(());
801            }
802            if let Some(item) = item.as_table_mut() {
803                return table_add_or_update_value(item, "version", version.into());
804            }
805
806            None
807        }
808        toml_edit::Entry::Vacant(_) => Some(()),
809    }
810}
811
812fn set_dependencies_version(
813    doc: &mut DocumentMut,
814    version: &str,
815    typ: DependencyType,
816    name: &str,
817) -> result::Result<(), CargoTomlError> {
818    if let Some(table) = get_top_table_mut(doc, typ.key()) {
819        set_dependency_version(table, version, name)
820            .ok_or_else(|| CargoTomlError::set_version(name, version))?;
821    }
822
823    if let Some(table) = get_top_table_mut(doc, "target") {
824        let targets: Vec<_> = table.iter().map(|(key, _)| key.to_owned()).collect();
825
826        for target in targets {
827            let target_deps = table.entry(&target);
828            match target_deps {
829                toml_edit::Entry::Occupied(mut target_deps) => {
830                    if let Some(target_deps) = target_deps
831                        .get_mut()
832                        .as_table_mut()
833                        .and_then(|inner| inner[typ.key()].as_table_mut())
834                    {
835                        set_dependency_version(target_deps, version, name)
836                            .ok_or_else(|| CargoTomlError::set_version(name, version))?;
837                    }
838                }
839                toml_edit::Entry::Vacant(_) => {}
840            };
841        }
842    };
843
844    Ok(())
845}
846
847fn set_lockfile_self_describing_metadata(
848    doc: &mut DocumentMut,
849    next_release_version: &str,
850    package_name: &str,
851) -> result::Result<(), Error> {
852    let packages_entry = doc.as_table_mut().entry("package");
853
854    match packages_entry {
855        toml_edit::Entry::Occupied(mut entry) => {
856            let tables = entry
857                .get_mut()
858                .as_array_of_tables_mut()
859                .expect("Expected lockfile to contain an array of tables named 'packages'");
860
861            let matching_index = tables.iter().position(|table| {
862                table
863                    .get("name")
864                    .and_then(|item| item.as_str())
865                    .map(|name| name == package_name)
866                    .unwrap_or_default()
867            });
868
869            if let Some(matching_index) = matching_index {
870                let table = tables
871                    .get_mut(matching_index)
872                    .expect("Expected lockfile to contain reference to self");
873                table_add_or_update_value(table, "version", next_release_version.into());
874            } else {
875                return Err(Error::CargoLockfileUpdate {
876                    reason: "Unable to locate self-referential metadata in lockfile".into(),
877                    package_name: package_name.to_owned(),
878                });
879            }
880        }
881        _ => {
882            return Err(Error::CargoLockfileUpdate {
883                reason: "Cargo lockfile does not contain 'packages' array of tables".into(),
884                package_name: package_name.to_owned(),
885            })
886        }
887    };
888
889    Ok(())
890}
891
892/// The type of a dependency for a package.
893#[derive(Debug)]
894pub enum DependencyType {
895    /// A normal dependency (i.e. "dependencies" section of `Cargo.toml`).
896    Normal,
897
898    /// A build dependency (i.e. "build-dependencies" section of `Cargo.toml`).
899    Build,
900
901    /// A dev dependency (i.e. "dev-dependencies" section of `Cargo.toml`).
902    Dev,
903}
904
905impl DependencyType {
906    fn key(&self) -> &str {
907        use DependencyType::*;
908
909        match self {
910            Normal => "dependencies",
911            Build => "build-dependencies",
912            Dev => "dev-dependencies",
913        }
914    }
915}
916
917impl fmt::Display for DependencyType {
918    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
919        use DependencyType::*;
920
921        match self {
922            Normal => write!(f, "Dependency"),
923            Build => write!(f, "Build dependency"),
924            Dev => write!(f, "Dev dependency"),
925        }
926    }
927}
928
929#[derive(Debug, Serialize)]
930struct Release {
931    name: String,
932    url: Option<Url>,
933}
934
935impl Release {
936    fn new<URL: AsRef<str>>(
937        name: impl AsRef<str>,
938        url: Option<URL>,
939        main_crate: impl AsRef<str>,
940    ) -> Result<Self> {
941        let url = if let Some(url) = url {
942            let base = Url::parse(url.as_ref()).map_err(Error::url_parse_error)?;
943            let url = base
944                .join(main_crate.as_ref())
945                .map_err(Error::url_parse_error)?;
946            Some(url)
947        } else {
948            None
949        };
950
951        Ok(Self {
952            name: name.as_ref().to_owned(),
953            url,
954        })
955    }
956
957    fn new_crates_io_release(name: impl AsRef<str>, main_crate: impl AsRef<str>) -> Result<Self> {
958        let base = Url::parse("https://crates.io/crates/").map_err(Error::url_parse_error)?;
959        let url = base
960            .join(main_crate.as_ref())
961            .map_err(Error::url_parse_error)?;
962
963        Ok(Self {
964            name: name.as_ref().to_owned(),
965            url: Some(url),
966        })
967    }
968}