cargo_component/commands/
publish.rs

1use std::{path::PathBuf, sync::Arc};
2
3use anyhow::{bail, Context, Result};
4use cargo_component_core::command::CommonOptions;
5use clap::Args;
6use wasm_pkg_client::{warg::WargRegistryConfig, Registry};
7
8use crate::{
9    config::{CargoArguments, CargoPackageSpec, Config},
10    is_wasm_target, load_metadata, publish, run_cargo_command, PackageComponentMetadata,
11    PublishOptions,
12};
13
14/// Publish a package to a registry.
15#[derive(Args)]
16#[clap(disable_version_flag = true)]
17pub struct PublishCommand {
18    /// The common command options.
19    #[clap(flatten)]
20    pub common: CommonOptions,
21
22    /// Build for the target triple (defaults to `wasm32-wasip1`)
23    #[clap(long = "target", value_name = "TRIPLE")]
24    pub target: Option<String>,
25
26    /// Require lock file and cache are up to date
27    #[clap(long = "frozen")]
28    pub frozen: bool,
29
30    /// Directory for all generated artifacts
31    #[clap(long = "target-dir", value_name = "DIRECTORY")]
32    pub target_dir: Option<PathBuf>,
33
34    /// Require lock file is up to date
35    #[clap(long = "locked")]
36    pub locked: bool,
37
38    /// Cargo package to publish (see `cargo help pkgid`)
39    #[clap(long = "package", short = 'p', value_name = "SPEC")]
40    pub cargo_package: Option<CargoPackageSpec>,
41
42    /// Path to Cargo.toml
43    #[clap(long = "manifest-path", value_name = "PATH")]
44    pub manifest_path: Option<PathBuf>,
45
46    /// Run without accessing the network
47    #[clap(long = "offline")]
48    pub offline: bool,
49
50    /// Space or comma separated list of features to activate
51    #[clap(long = "features", value_name = "FEATURES")]
52    pub features: Vec<String>,
53
54    /// Activate all available features
55    #[clap(long = "all-features")]
56    pub all_features: bool,
57
58    /// Do not activate the `default` feature
59    #[clap(long = "no-default-features")]
60    pub no_default_features: bool,
61
62    /// Number of parallel jobs, defaults to # of CPUs
63    #[clap(long = "jobs", short = 'j', value_name = "N")]
64    pub jobs: Option<i32>,
65
66    ///  Perform all checks without publishing
67    #[clap(long = "dry-run")]
68    pub dry_run: bool,
69
70    /// The registry to publish to.
71    #[clap(long = "registry", value_name = "REGISTRY")]
72    pub registry: Option<Registry>,
73}
74
75impl PublishCommand {
76    /// Executes the command.
77    pub async fn exec(self) -> Result<()> {
78        log::debug!("executing publish command");
79
80        let mut config =
81            Config::new(self.common.new_terminal(), self.common.config.clone()).await?;
82        let client = config.client(self.common.cache_dir.clone(), false).await?;
83
84        if let Some(target) = &self.target {
85            if !is_wasm_target(target) {
86                bail!("target `{}` is not a WebAssembly target", target);
87            }
88        }
89
90        let metadata = load_metadata(self.manifest_path.as_deref())?;
91        let spec = match &self.cargo_package {
92            Some(spec) => Some(spec.clone()),
93            None => CargoPackageSpec::find_current_package_spec(&metadata),
94        };
95        let packages = [PackageComponentMetadata::new(if let Some(spec) = &spec {
96            metadata
97                .packages
98                .iter()
99                .find(|p| {
100                    p.name == spec.name
101                        && match spec.version.as_ref() {
102                            Some(v) => &p.version == v,
103                            None => true,
104                        }
105                })
106                .with_context(|| {
107                    format!("package ID specification `{spec}` did not match any packages")
108                })?
109        } else {
110            metadata
111                .root_package()
112                .context("no root package found in manifest")?
113        })?];
114
115        let package = packages[0].package;
116        let component_metadata = &packages[0].metadata;
117
118        let name = component_metadata.section.package.as_ref().with_context(|| {
119            format!(
120                "package `{name}` is missing a `package.metadata.component.package` setting in manifest `{path}`",
121                name = package.name,
122                path = package.manifest_path
123            )
124        })?;
125
126        if let Ok(key) = std::env::var("CARGO_COMPONENT_PUBLISH_KEY") {
127            let registry = config.pkg_config.resolve_registry(name).ok_or_else(|| anyhow::anyhow!("Tried to set a signing key, but registry was not set and no default registry was found. Try setting the `--registry` option."))?.to_owned();
128            // NOTE(thomastaylor312): If config doesn't already exist, this will essentially force warg
129            // usage because we'll be creating a config for warg, which means it will default to that
130            // protocol. So for all intents and purposes, setting a publish key forces warg usage.
131            let reg_config = config
132                .pkg_config
133                .get_or_insert_registry_config_mut(&registry);
134            let mut warg_conf = WargRegistryConfig::try_from(&*reg_config).unwrap_or_default();
135            warg_conf.signing_key = Some(Arc::new(
136                key.try_into().context("Failed to parse signing key")?,
137            ));
138            reg_config.set_backend_config("warg", warg_conf)?;
139        }
140
141        let cargo_build_args = CargoArguments {
142            color: self.common.color,
143            verbose: self.common.verbose as usize,
144            help: false,
145            quiet: self.common.quiet,
146            targets: self.target.clone().into_iter().collect(),
147            manifest_path: self.manifest_path.clone(),
148            message_format: None,
149            frozen: self.frozen,
150            locked: self.locked,
151            release: true,
152            offline: self.offline,
153            workspace: false,
154            packages: self.cargo_package.clone().into_iter().collect(),
155        };
156
157        let spawn_args = self.build_args()?;
158        let outputs = run_cargo_command(
159            client.clone(),
160            &config,
161            &metadata,
162            &packages,
163            Some("build"),
164            &cargo_build_args,
165            &spawn_args,
166        )
167        .await?;
168        if outputs.len() != 1 {
169            bail!(
170                "expected one output from `cargo build`, got {len}",
171                len = outputs.len()
172            );
173        }
174
175        let options = PublishOptions {
176            package,
177            name,
178            registry: self.registry.as_ref(),
179            version: &component_metadata.version,
180            path: &outputs[0],
181            dry_run: self.dry_run,
182        };
183
184        publish(&config, client, &options).await
185    }
186
187    fn build_args(&self) -> Result<Vec<String>> {
188        let mut args = Vec::new();
189        args.push("build".to_string());
190        args.push("--release".to_string());
191
192        if self.common.quiet {
193            args.push("-q".to_string());
194        }
195
196        args.extend(
197            std::iter::repeat("-v")
198                .take(self.common.verbose as usize)
199                .map(ToString::to_string),
200        );
201
202        if let Some(color) = self.common.color {
203            args.push("--color".to_string());
204            args.push(color.to_string());
205        }
206
207        if let Some(target) = &self.target {
208            args.push("--target".to_string());
209            args.push(target.clone());
210        }
211
212        if self.frozen {
213            args.push("--frozen".to_string());
214        }
215
216        if let Some(target_dir) = &self.target_dir {
217            args.push("--target-dir".to_string());
218            args.push(
219                target_dir
220                    .as_os_str()
221                    .to_str()
222                    .with_context(|| {
223                        format!(
224                            "target directory `{dir}` is not valid UTF-8",
225                            dir = target_dir.display()
226                        )
227                    })?
228                    .to_string(),
229            );
230        }
231
232        if self.locked {
233            args.push("--locked".to_string());
234        }
235
236        if let Some(spec) = &self.cargo_package {
237            args.push("--package".to_string());
238            args.push(spec.to_string());
239        }
240
241        if let Some(manifest_path) = &self.manifest_path {
242            args.push("--manifest-path".to_string());
243            args.push(
244                manifest_path
245                    .as_os_str()
246                    .to_str()
247                    .with_context(|| {
248                        format!(
249                            "manifest path `{path}` is not valid UTF-8",
250                            path = manifest_path.display()
251                        )
252                    })?
253                    .to_string(),
254            );
255        }
256
257        if self.offline {
258            args.push("--offline".to_string());
259        }
260
261        if !self.features.is_empty() {
262            args.push("--features".to_string());
263            args.push(self.features.join(","));
264        }
265
266        if self.all_features {
267            args.push("--all-features".to_string());
268        }
269
270        if self.no_default_features {
271            args.push("--no-default-features".to_string());
272        }
273
274        if let Some(jobs) = self.jobs {
275            args.push("--jobs".to_string());
276            args.push(jobs.to_string());
277        }
278
279        Ok(args)
280    }
281}