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#[derive(Clone, Debug)]
20pub enum PackageRef {
21 LocalPath(PathBuf),
23 #[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#[derive(Args)]
67#[clap(disable_version_flag = true)]
68pub struct PlugCommand {
69 #[clap(long = "plug", value_name = "PLUG_PATH", required = true)]
73 pub plugs: Vec<PackageRef>,
74
75 #[clap(value_name = "SOCKET_PATH", required = true)]
77 pub socket: PackageRef,
78
79 #[clap(long, short = 't')]
81 pub wat: bool,
82
83 #[clap(long, short = 'o')]
87 pub output: Option<PathBuf>,
88
89 #[cfg(feature = "registry")]
91 #[clap(long, value_name = "URL")]
92 pub registry: Option<String>,
93}
94
95impl PlugCommand {
96 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 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 plugs_by_name.entry(name).or_default().push(plug);
169 }
170
171 let mut plug_packages = Vec::new();
172
173 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 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}