cargo_hakari/
publish.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{cargo_cli::CargoCli, helpers::regenerate_lockfile, output::OutputContext};
5use color_eyre::{eyre::WrapErr, Result};
6use guppy::graph::PackageMetadata;
7use hakari::HakariBuilder;
8use log::{error, info};
9use owo_colors::OwoColorize;
10
11pub(crate) fn publish_hakari(
12    package_name: &str,
13    builder: HakariBuilder<'_>,
14    pass_through: &[String],
15    output: OutputContext,
16) -> Result<()> {
17    let hakari_package = builder
18        .hakari_package()
19        .expect("hakari-package must be specified in hakari.toml");
20    let workspace = builder.graph().workspace();
21    let package = workspace.member_by_name(package_name)?;
22
23    // Remove the workspace-hack dependency from the package if it isn't published as open source.
24    let mut remove_dep = if hakari_package.publish().is_never() {
25        TempRemoveDep::new(builder, package, output.clone())?
26    } else {
27        info!(
28            "not removing dependency to {} because it is marked as published (publish != false)",
29            hakari_package.name().style(output.styles.package_name)
30        );
31        TempRemoveDep::none()
32    };
33
34    let mut cargo_cli = CargoCli::new("publish", output.clone());
35    cargo_cli.add_args(pass_through.iter().map(|arg| arg.as_str()));
36    // Also set --allow-dirty because we make some changes to the working directory.
37    // TODO: is there a better way to handle this?
38    if !remove_dep.is_none() {
39        cargo_cli.add_arg("--allow-dirty");
40    }
41
42    let workspace_dir = package
43        .source()
44        .workspace_path()
45        .expect("package is in workspace");
46    let abs_path = workspace.root().join(workspace_dir);
47
48    let all_args = cargo_cli.all_args().join(" ");
49
50    info!(
51        "{} {}\n---",
52        "executing".style(output.styles.command),
53        all_args
54    );
55    let expression = cargo_cli.to_expression().dir(abs_path);
56
57    match expression.run() {
58        Ok(_) => remove_dep.finish(true),
59        Err(err) => {
60            remove_dep.finish(false)?;
61            Err(err).wrap_err_with(|| format!("`{}` failed", all_args))
62        }
63    }
64}
65
66/// RAII guard to ensure packages are re-added after being published.
67#[derive(Debug)]
68struct TempRemoveDep<'g> {
69    inner: Option<TempRemoveDepInner<'g>>,
70}
71
72impl<'g> TempRemoveDep<'g> {
73    fn new(
74        builder: HakariBuilder<'g>,
75        package: PackageMetadata<'g>,
76        output: OutputContext,
77    ) -> Result<Self> {
78        let hakari_package = builder
79            .hakari_package()
80            .expect("hakari-package must be specified in hakari.toml");
81        let package_set = package.to_package_set();
82        let remove_ops = builder
83            .remove_dep_ops(&package_set, false)
84            .expect("hakari-package must be specified in hakari.toml");
85        let inner = if remove_ops.is_empty() {
86            info!(
87                "dependency from {} to {} not present",
88                package.name().style(output.styles.package_name),
89                hakari_package.name().style(output.styles.package_name),
90            );
91            None
92        } else {
93            info!(
94                "removing dependency from {} to {}",
95                package.name().style(output.styles.package_name),
96                hakari_package.name().style(output.styles.package_name),
97            );
98            remove_ops
99                .apply()
100                .wrap_err_with(|| format!("error removing dependency from {}", package.name()))?;
101            Some(TempRemoveDepInner {
102                builder,
103                package,
104                output,
105            })
106        };
107
108        Ok(Self { inner })
109    }
110
111    fn none() -> Self {
112        Self { inner: None }
113    }
114
115    fn is_none(&self) -> bool {
116        self.inner.is_none()
117    }
118
119    fn finish(&mut self, success: bool) -> Result<()> {
120        match self.inner.take() {
121            Some(inner) => inner.finish(success),
122            None => {
123                // No operations need to be performed or `finish` was already called.
124                Ok(())
125            }
126        }
127    }
128}
129
130impl Drop for TempRemoveDep<'_> {
131    fn drop(&mut self) {
132        // Ignore errors in this impl.
133        let _ = self.finish(false);
134    }
135}
136
137#[derive(Debug)]
138struct TempRemoveDepInner<'g> {
139    builder: HakariBuilder<'g>,
140    package: PackageMetadata<'g>,
141    output: OutputContext,
142}
143
144impl TempRemoveDepInner<'_> {
145    fn finish(self, success: bool) -> Result<()> {
146        let package_set = self.package.to_package_set();
147        let add_ops = self
148            .builder
149            .add_dep_ops(&package_set, true)
150            .expect("hakari-package must be specified in hakari.toml");
151
152        if success {
153            info!(
154                "re-adding dependency from {} to {}",
155                self.package.name().style(self.output.styles.package_name),
156                self.builder
157                    .hakari_package()
158                    .unwrap()
159                    .name()
160                    .style(self.output.styles.package_name),
161            );
162        } else {
163            eprintln!("---");
164            error!("execution failed, rolling back changes");
165        }
166
167        add_ops.apply()?;
168        regenerate_lockfile(self.output)?;
169        Ok(())
170    }
171}