wac_cli/commands/
plug.rs

1use std::{
2    io::{IsTerminal as _, Write as _},
3    path::PathBuf,
4};
5
6use anyhow::{bail, Context as _, Error, Result};
7use clap::Args;
8use std::str::FromStr;
9use wac_graph::{CompositionGraph, EncodeOptions};
10use wac_types::Package;
11
12#[cfg(feature = "registry")]
13use warg_client::FileSystemClient;
14
15#[cfg(feature = "registry")]
16use warg_protocol::registry::PackageName;
17
18/// The package path or registry package name.
19#[derive(Clone, Debug)]
20pub enum PackageRef {
21    /// The local file path to the component.
22    LocalPath(PathBuf),
23    /// The registry package name.
24    #[cfg(feature = "registry")]
25    RegistryPackage((PackageName, Option<semver::Version>)),
26}
27
28impl FromStr for PackageRef {
29    type Err = Error;
30
31    fn from_str(s: &str) -> Result<Self, Self::Err> {
32        #[cfg(feature = "registry")]
33        return Ok(s
34            .split_once('@')
35            .map(|(name, version)| {
36                match (PackageName::new(name), semver::Version::parse(version)) {
37                    (Ok(name), Ok(ver)) => Ok(Some(Self::RegistryPackage((name, Some(ver))))),
38                    (Ok(_), Err(e)) => bail!("invalid version for package `{s}`: {e}"),
39                    (Err(_), _) => Ok(None),
40                }
41            })
42            .unwrap_or(Ok(None))?
43            .unwrap_or_else(|| match PackageName::new(s) {
44                Ok(name) => Self::RegistryPackage((name, None)),
45                _ => Self::LocalPath(PathBuf::from(s)),
46            }));
47
48        #[cfg(not(feature = "registry"))]
49        Ok(Self::LocalPath(PathBuf::from(s)))
50    }
51}
52
53impl std::fmt::Display for PackageRef {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            Self::LocalPath(path) => write!(f, "{}", path.display()),
57            #[cfg(feature = "registry")]
58            Self::RegistryPackage((name, Some(ver))) => write!(f, "{}@{}", name, ver),
59            #[cfg(feature = "registry")]
60            Self::RegistryPackage((name, None)) => write!(f, "{}", name),
61        }
62    }
63}
64
65/// Plugs the exports of any number of 'plug' components into the imports of a 'socket' component.
66#[derive(Args)]
67#[clap(disable_version_flag = true)]
68pub struct PlugCommand {
69    /// The local path to the plug component or the registry package name.
70    ///
71    /// More than one plug can be supplied.
72    #[clap(long = "plug", value_name = "PLUG_PATH", required = true)]
73    pub plugs: Vec<PackageRef>,
74
75    /// The local path to the socket component or the registry package name.
76    #[clap(value_name = "SOCKET_PATH", required = true)]
77    pub socket: PackageRef,
78
79    /// Whether to emit the WebAssembly text format.
80    #[clap(long, short = 't')]
81    pub wat: bool,
82
83    /// The path to write the output to.
84    ///
85    /// If not specified, the output will be written to stdout.
86    #[clap(long, short = 'o')]
87    pub output: Option<PathBuf>,
88
89    /// The URL of the registry to use.
90    #[cfg(feature = "registry")]
91    #[clap(long, value_name = "URL")]
92    pub registry: Option<String>,
93}
94
95impl PlugCommand {
96    /// Executes the command.
97    pub async fn exec(&self) -> Result<()> {
98        log::debug!("executing plug command");
99        let mut graph = CompositionGraph::new();
100
101        #[cfg(feature = "registry")]
102        let mut client = None;
103
104        let socket_path = match &self.socket {
105            #[cfg(feature = "registry")]
106            PackageRef::RegistryPackage((name, version)) => {
107                if client.is_none() {
108                    client = Some(
109                        FileSystemClient::new_with_default_config(self.registry.as_deref()).await,
110                    );
111                }
112                let client = client.as_ref().unwrap().as_ref().map_err(|_| {
113                    anyhow::anyhow!(
114                        "Warg registry is not configured. Package `{name}` was not found."
115                    )
116                })?;
117
118                if let Some(ver) = version {
119                    let download = client.download_exact(name, ver).await?;
120                    log::debug!(
121                        "Plugging `{name}` version `{ver}` using registry `{registry}`",
122                        registry = client.url()
123                    );
124                    download.path
125                } else {
126                    let download = client
127                        .download(name, &semver::VersionReq::STAR)
128                        .await?
129                        .ok_or_else(|| anyhow::anyhow!("package `{name}` was not found"))?;
130
131                    log::debug!(
132                        "Plugging `{name}` version `{ver}` using registry `{registry}`",
133                        ver = &download.version,
134                        registry = client.url()
135                    );
136                    download.path
137                }
138            }
139            PackageRef::LocalPath(path) => {
140                log::debug!("Plugging `{path}`", path = path.display());
141
142                path.clone()
143            }
144        };
145        let socket = std::fs::read(socket_path).with_context(|| {
146            format!(
147                "failed to read socket component `{socket}`",
148                socket = self.socket
149            )
150        })?;
151
152        let socket = Package::from_bytes("socket", None, socket, graph.types_mut())?;
153        let socket = graph.register_package(socket)?;
154
155        // Collect the plugs by their names
156        let mut plugs_by_name = std::collections::HashMap::<_, Vec<_>>::new();
157        for plug in self.plugs.iter() {
158            let name = match plug {
159                #[cfg(feature = "registry")]
160                PackageRef::RegistryPackage((name, _)) => std::borrow::Cow::Borrowed(name.as_ref()),
161                PackageRef::LocalPath(path) => path
162                    .file_stem()
163                    .map(|fs| fs.to_string_lossy())
164                    .with_context(|| format!("path to plug '{}' was not a file", plug))?,
165            };
166
167            // TODO(rylev): sanitize the name to ensure it's a valid package identifier.
168            plugs_by_name.entry(name).or_default().push(plug);
169        }
170
171        let mut plug_packages = Vec::new();
172
173        // Plug each plug into the socket.
174        for (name, plug_refs) in plugs_by_name {
175            for (i, plug_ref) in plug_refs.iter().enumerate() {
176                let (mut name, path) = match plug_ref {
177                    #[cfg(feature = "registry")]
178                    PackageRef::RegistryPackage((name, version)) => {
179                        if client.is_none() {
180                            client = Some(
181                                FileSystemClient::new_with_default_config(self.registry.as_deref())
182                                    .await,
183                            );
184                        }
185                        let client = client.as_ref().unwrap().as_ref().map_err(|_| {
186                            anyhow::anyhow!(
187                                "Warg registry is not configured. Package `{name}` was not found."
188                            )
189                        })?;
190
191                        let path = if let Some(ver) = version {
192                            let download = client.download_exact(name, ver).await?;
193                            log::debug!(
194                                "    with `{name}` version `{ver}` using registry `{registry}`",
195                                registry = client.url()
196                            );
197                            download.path
198                        } else {
199                            let download = client
200                                .download(name, &semver::VersionReq::STAR)
201                                .await?
202                                .ok_or_else(|| anyhow::anyhow!("package `{name}` was not found"))?;
203
204                            log::debug!(
205                                "    with `{name}` version `{ver}` using registry `{registry}`",
206                                ver = &download.version,
207                                registry = client.url()
208                            );
209                            download.path
210                        };
211
212                        let name = name.as_ref().to_string();
213                        (name, path)
214                    }
215                    PackageRef::LocalPath(path) => {
216                        log::debug!("    with `{path}`", path = path.display());
217                        (format!("plug:{name}"), path.clone())
218                    }
219                };
220                // If there's more than one plug with the same name, append an index to the name.
221                if plug_refs.len() > 1 {
222                    use core::fmt::Write;
223                    write!(&mut name, "{i}").unwrap();
224                }
225
226                let plug_package = Package::from_file(&name, None, &path, graph.types_mut())?;
227                let package_id = graph.register_package(plug_package)?;
228                plug_packages.push(package_id);
229            }
230        }
231
232        wac_graph::plug(&mut graph, plug_packages, socket)?;
233
234        let mut bytes = graph.encode(EncodeOptions::default())?;
235
236        let binary_output_to_terminal =
237            !self.wat && self.output.is_none() && std::io::stdout().is_terminal();
238        if binary_output_to_terminal {
239            bail!("cannot print binary wasm output to a terminal; pass the `-t` flag to print the text format instead");
240        }
241
242        if self.wat {
243            bytes = wasmprinter::print_bytes(&bytes)
244                .context("failed to convert binary wasm output to text")?
245                .into_bytes();
246        }
247        match &self.output {
248            Some(path) => {
249                std::fs::write(path, bytes).context(format!(
250                    "failed to write output file `{path}`",
251                    path = path.display()
252                ))?;
253                log::debug!("\nWrote plugged component: `{path}`", path = path.display());
254            }
255            None => {
256                std::io::stdout()
257                    .write_all(&bytes)
258                    .context("failed to write to stdout")?;
259
260                if self.wat {
261                    println!();
262                }
263            }
264        }
265        Ok(())
266    }
267}