wasefire_cli_tools/
action.rs

1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt::Display;
16use std::path::{Path, PathBuf};
17use std::pin::Pin;
18use std::time::Duration;
19
20use anyhow::{Context, Result, anyhow, bail, ensure};
21use clap::{ValueEnum, ValueHint};
22use rusb::GlobalContext;
23use tokio::fs::File;
24use tokio::io::{AsyncBufRead, AsyncBufReadExt as _, AsyncWrite, AsyncWriteExt, BufReader};
25use tokio::process::Command;
26use wasefire_common::platform::Side;
27use wasefire_protocol::{self as service, Connection, ConnectionExt as _, applet};
28use wasefire_wire::{self as wire, Yoke};
29
30use crate::cargo::metadata;
31use crate::error::root_cause_is;
32use crate::{cmd, fs};
33
34mod protocol;
35pub mod usb_serial;
36
37/// Options to connect to a platform.
38#[derive(Clone, clap::Args)]
39pub struct ConnectionOptions {
40    /// How to connect to the platform.
41    ///
42    /// Possible values are:
43    /// - usb (there must be exactly one connected platform on USB)
44    /// - usb:SERIAL (the serial must be in hexadecimal)
45    /// - usb:BUS:DEV
46    /// - unix[:PATH] (defaults to /tmp/wasefire)
47    /// - tcp[:HOST:PORT] (defaults to 127.0.0.1:3457)
48    #[arg(long, default_value = "usb", env = "WASEFIRE_PROTOCOL", verbatim_doc_comment)]
49    protocol: protocol::Protocol,
50
51    /// Timeout to send or receive with the USB protocol.
52    #[arg(long, default_value = "0s")]
53    timeout: humantime::Duration,
54}
55
56impl ConnectionOptions {
57    /// Establishes a connection.
58    pub async fn connect(&self) -> Result<Box<dyn Connection>> {
59        self.protocol.connect(*self.timeout).await
60    }
61
62    /// Returns whether these options identify a device even after reboot.
63    pub fn reboot_stable(&self) -> bool {
64        match &self.protocol {
65            protocol::Protocol::Usb(x) => match x {
66                protocol::ProtocolUsb::Auto => true,
67                protocol::ProtocolUsb::Serial(_) => true,
68                protocol::ProtocolUsb::BusDev { .. } => false,
69            },
70            protocol::Protocol::Unix(_) => true,
71            protocol::Protocol::Tcp(_) => true,
72        }
73    }
74}
75
76/// Returns the API version of a platform.
77#[derive(clap::Args)]
78pub struct PlatformApiVersion {}
79
80impl PlatformApiVersion {
81    pub async fn run(self, connection: &mut dyn Connection) -> Result<u32> {
82        let PlatformApiVersion {} = self;
83        connection.call::<service::ApiVersion>(()).await.map(|x| *x.get())
84    }
85}
86
87/// Installs an applet on a platform.
88#[derive(clap::Args)]
89pub struct AppletInstall {
90    /// Path to the applet to install.
91    #[arg(value_hint = ValueHint::FilePath)]
92    pub applet: PathBuf,
93
94    #[clap(flatten)]
95    pub transfer: Transfer,
96
97    #[command(subcommand)]
98    pub wait: Option<AppletInstallWait>,
99}
100
101#[derive(clap::Subcommand)]
102pub enum AppletInstallWait {
103    /// Waits until the applet exits.
104    #[group(id = "AppletInstallWait::Wait")]
105    Wait {
106        #[command(flatten)]
107        action: AppletExitStatus,
108    },
109}
110
111impl AppletInstall {
112    pub async fn run(self, connection: &mut dyn Connection) -> Result<()> {
113        let AppletInstall { applet, transfer, wait } = self;
114        transfer
115            .run::<service::AppletInstall>(
116                connection,
117                Some(applet),
118                "Installed",
119                None::<fn(_) -> _>,
120            )
121            .await?;
122        match wait {
123            Some(AppletInstallWait::Wait { mut action }) => {
124                action.wait.ensure_wait();
125                action.run(connection).await
126            }
127            None => Ok(()),
128        }
129    }
130}
131
132/// Uninstalls an applet from a platform.
133#[derive(clap::Args)]
134pub struct AppletUninstall {}
135
136impl AppletUninstall {
137    pub async fn run(self, connection: &mut dyn Connection) -> Result<()> {
138        let AppletUninstall {} = self;
139        let transfer = Transfer { dry_run: false };
140        transfer.run::<service::AppletInstall>(connection, None, "Erased", None::<fn(_) -> _>).await
141    }
142}
143
144/// Prints the exit status of an applet from a platform.
145#[derive(clap::Parser)]
146#[non_exhaustive]
147pub struct AppletExitStatus {
148    #[clap(flatten)]
149    pub wait: Wait,
150
151    /// Also exits with the applet exit code.
152    #[arg(long)]
153    exit_code: bool,
154}
155
156impl AppletExitStatus {
157    fn print(status: Option<applet::ExitStatus>) {
158        match status {
159            Some(status) => println!("{status}."),
160            None => println!("The applet is still running."),
161        }
162    }
163
164    fn code(status: Option<applet::ExitStatus>) -> i32 {
165        match status {
166            Some(applet::ExitStatus::Exit) => 0,
167            Some(applet::ExitStatus::Abort) => 1,
168            Some(applet::ExitStatus::Trap) => 2,
169            Some(applet::ExitStatus::Kill) => 62,
170            None => 63,
171        }
172    }
173
174    pub fn ensure_exit(&mut self) {
175        self.exit_code = true;
176    }
177
178    pub async fn run(self, connection: &mut dyn Connection) -> Result<()> {
179        let AppletExitStatus { wait, exit_code } = self;
180        let status = wait
181            .run::<service::AppletExitStatus, applet::ExitStatus>(connection, applet::AppletId)
182            .await?
183            .map(|x| *x.get());
184        Self::print(status);
185        if exit_code {
186            std::process::exit(Self::code(status))
187        }
188        Ok(())
189    }
190}
191
192/// Reboots an applet installed on a platform.
193#[derive(clap::Args)]
194pub struct AppletReboot {}
195
196impl AppletReboot {
197    pub async fn run(self, connection: &mut dyn Connection) -> Result<()> {
198        let AppletReboot {} = self;
199        connection.call::<service::AppletReboot>(applet::AppletId).await.map(|x| *x.get())
200    }
201}
202
203/// Parameters for an applet or platform RPC.
204#[derive(clap::Args)]
205struct Rpc {
206    /// Reads the request from this file instead of standard input.
207    #[arg(long, value_hint = ValueHint::FilePath)]
208    input: Option<PathBuf>,
209
210    /// Writes the response to this file instead of standard output.
211    #[arg(long, value_hint = ValueHint::AnyPath)]
212    output: Option<PathBuf>,
213
214    /// Loops reading requests as lines and concatenating responses.
215    #[arg(long)]
216    repl: bool,
217}
218
219enum RpcState {
220    One { input: Option<PathBuf>, output: Option<PathBuf>, read: bool },
221    Loop { input: Pin<Box<dyn AsyncBufRead>>, output: Pin<Box<dyn AsyncWrite>> },
222}
223
224impl Rpc {
225    async fn start(self) -> Result<RpcState> {
226        let Rpc { input, output, repl } = self;
227        if !repl {
228            return Ok(RpcState::One { input, output, read: false });
229        }
230        let input: Pin<Box<dyn AsyncBufRead>> = match input {
231            None => Box::pin(BufReader::new(tokio::io::stdin())),
232            Some(path) => Box::pin(BufReader::new(File::open(path).await?)),
233        };
234        let output: Pin<Box<dyn AsyncWrite>> = match output {
235            None => Box::pin(tokio::io::stdout()),
236            Some(path) => Box::pin(File::create(path).await?),
237        };
238        Ok(RpcState::Loop { input, output })
239    }
240}
241
242impl RpcState {
243    async fn read(&mut self) -> Result<Option<Vec<u8>>> {
244        match self {
245            RpcState::One { read: x @ false, .. } => *x = true,
246            RpcState::One { .. } => return Ok(None),
247            RpcState::Loop { .. } => (),
248        }
249        Ok(Some(match self {
250            RpcState::One { input: None, .. } => fs::read_stdin().await?,
251            RpcState::One { input: Some(path), .. } => fs::read(path).await?,
252            RpcState::Loop { input, .. } => {
253                let mut line = String::new();
254                if input.read_line(&mut line).await? == 0 {
255                    return Ok(None);
256                }
257                line.into_bytes()
258            }
259        }))
260    }
261
262    async fn write(&mut self, response: &[u8]) -> Result<()> {
263        match self {
264            RpcState::One { output: None, .. } => fs::write_stdout(response).await,
265            RpcState::One { output: Some(path), .. } => fs::write(path, response).await,
266            RpcState::Loop { output, .. } => {
267                output.write_all(response).await?;
268                output.flush().await?;
269                Ok(())
270            }
271        }
272    }
273}
274
275/// Calls an RPC to an applet on a platform.
276#[derive(clap::Args)]
277pub struct AppletRpc {
278    /// Applet identifier in the platform.
279    applet: Option<String>,
280
281    #[clap(flatten)]
282    rpc: Rpc,
283
284    #[clap(flatten)]
285    wait: Wait,
286}
287
288impl AppletRpc {
289    pub async fn run(self, connection: &mut dyn Connection) -> Result<()> {
290        let AppletRpc { applet, rpc, mut wait } = self;
291        let applet_id = match applet {
292            Some(_) => bail!("applet identifiers are not supported yet"),
293            None => applet::AppletId,
294        };
295        wait.ensure_wait();
296        let mut rpc = rpc.start().await?;
297        while let Some(request) = rpc.read().await? {
298            let request = applet::Request { applet_id, request: &request };
299            connection.call::<service::AppletRequest>(request).await?.get();
300            match wait.run::<service::AppletResponse, &[u8]>(connection, applet_id).await? {
301                None => bail!("did not receive a response"),
302                Some(response) => rpc.write(response.get()).await?,
303            }
304        }
305        Ok(())
306    }
307}
308
309/// Options to repeatedly call a command with an optional response.
310#[derive(clap::Parser)]
311pub struct Wait {
312    /// Waits until there is a response.
313    ///
314    /// This is equivalent to --period=100ms.
315    #[arg(long)]
316    wait: bool,
317
318    /// Retries every so often until there is a response.
319    ///
320    /// The command doesn't return `None` in that case.
321    #[arg(long, conflicts_with = "wait")]
322    period: Option<humantime::Duration>,
323}
324
325impl Wait {
326    pub fn ensure_wait(&mut self) {
327        if self.wait || self.period.is_some() {
328            return;
329        }
330        self.wait = true;
331    }
332
333    pub fn set_period(&mut self, period: Duration) {
334        self.wait = false;
335        self.period = Some(period.into());
336    }
337
338    pub async fn run<S, T: wire::Wire<'static>>(
339        &self, connection: &mut dyn Connection, request: S::Request<'_>,
340    ) -> Result<Option<Yoke<T::Type<'static>>>>
341    where S: for<'a> service::Service<Response<'a> = Option<T::Type<'a>>> {
342        let Wait { wait, period } = self;
343        let period = match (wait, period) {
344            (true, None) => Some(Duration::from_millis(100)),
345            (true, Some(_)) => unreachable!(),
346            (false, None) => None,
347            (false, Some(x)) => Some(**x),
348        };
349        let request = S::request(request);
350        loop {
351            match connection.call_ref::<S>(&request).await?.try_map(|x| x.ok_or(())) {
352                Ok(x) => break Ok(Some(x)),
353                Err(()) => match period {
354                    Some(period) => tokio::time::sleep(period).await,
355                    None => break Ok(None),
356                },
357            }
358        }
359    }
360}
361
362/// Clears the store for the platform and all applets.
363#[derive(clap::Args)]
364pub struct PlatformClearStore {
365    /// Clears all entries with a key greater or equal to this value.
366    #[arg(default_value_t = 0)]
367    min_key: usize,
368}
369
370impl PlatformClearStore {
371    pub async fn run(self, connection: &mut dyn Connection) -> Result<()> {
372        let PlatformClearStore { min_key } = self;
373        connection.call::<service::PlatformClearStore>(min_key).await.map(|x| *x.get())
374    }
375}
376
377/// Returns information about a platform.
378#[derive(clap::Args)]
379pub struct PlatformInfo {}
380
381impl PlatformInfo {
382    pub async fn print(self, connection: &mut dyn Connection) -> Result<()> {
383        Ok(print!("{}", self.run(connection).await?.get()))
384    }
385
386    pub async fn run(
387        self, connection: &mut dyn Connection,
388    ) -> Result<Yoke<service::platform::Info<'static>>> {
389        let PlatformInfo {} = self;
390        connection.call::<service::PlatformInfo>(()).await
391    }
392}
393
394/// Lists the platforms connected on USB.
395#[derive(clap::Args)]
396pub struct PlatformList {
397    /// Timeout to send or receive on the platform protocol.
398    #[arg(long, default_value = "1s")]
399    timeout: humantime::Duration,
400}
401
402impl PlatformList {
403    pub async fn run(self) -> Result<()> {
404        let PlatformList { timeout } = self;
405        let context = GlobalContext::default();
406        let candidates = wasefire_protocol_usb::list(&context)?;
407        println!("There are {} connected platforms on USB:", candidates.len());
408        for candidate in candidates {
409            let mut connection = candidate.connect(*timeout)?;
410            let serial = protocol::ProtocolUsb::Serial(protocol::serial(&mut connection).await?);
411            let bus = connection.device().bus_number();
412            let dev = connection.device().address();
413            let busdev = protocol::ProtocolUsb::BusDev { bus, dev };
414            println!("- {serial} or {busdev}");
415        }
416        Ok(())
417    }
418}
419
420/// Updates a platform.
421#[derive(clap::Args)]
422pub struct PlatformUpdate {
423    /// Path to the A side of the new platform.
424    ///
425    /// If only this file is provided, it is used without checking the running side. In particular,
426    /// it can be the B side of the new platform.
427    #[arg(value_hint = ValueHint::FilePath)]
428    pub platform_a: PathBuf,
429
430    /// Path to the B side of the new platform.
431    ///
432    /// If this file is not provided, [`Self::platform_a`] is used regardless of the running side.
433    #[arg(value_hint = ValueHint::FilePath)]
434    pub platform_b: Option<PathBuf>,
435
436    #[clap(flatten)]
437    pub transfer: Transfer,
438}
439
440impl PlatformUpdate {
441    pub async fn run(self, connection: &mut dyn Connection) -> Result<()> {
442        let PlatformUpdate { platform_a, platform_b, transfer } = self;
443        let platform = match platform_b {
444            Some(platform_b) => match (PlatformInfo {}).run(connection).await?.get().running_side {
445                Side::A => platform_b,
446                Side::B => platform_a,
447            },
448            None => platform_a,
449        };
450        transfer
451            .run::<service::PlatformUpdate>(
452                connection,
453                Some(platform),
454                "Updated",
455                Some(|_| bail!("device responded to a transfer finish")),
456            )
457            .await
458    }
459}
460
461/// Parameters for a transfer from the host to the device.
462#[derive(Clone, clap::Args)]
463pub struct Transfer {
464    /// Whether the transfer is a dry-run.
465    #[arg(long)]
466    dry_run: bool,
467}
468
469impl Transfer {
470    async fn run<S>(
471        self, connection: &mut dyn Connection, payload: Option<PathBuf>, message: &'static str,
472        finish: Option<impl FnOnce(Yoke<S::Response<'static>>) -> Result<!>>,
473    ) -> Result<()>
474    where
475        S: for<'a> service::Service<
476                Request<'a> = service::transfer::Request<'a>,
477                Response<'a> = service::transfer::Response,
478            >,
479    {
480        use wasefire_protocol::transfer::{Request, Response};
481        let Transfer { dry_run } = self;
482        let payload = match payload {
483            None => Vec::new(),
484            Some(x) => fs::read(x).await?,
485        };
486        let Response::Start { chunk_size, num_pages } =
487            *connection.call::<S>(Request::Start { dry_run }).await?.get()
488        else {
489            bail!("received unexpected response");
490        };
491        let multi_progress = indicatif::MultiProgress::new();
492        let style = indicatif::ProgressStyle::with_template(
493            "{msg:9} {elapsed:>3} {spinner} [{wide_bar}] {bytes:>10} / {total_bytes:<10}",
494        )?
495        .tick_chars("-\\|/ ")
496        .progress_chars("##-");
497        let mut progress = None;
498        if 0 < num_pages {
499            let progress = progress.insert(
500                multi_progress.add(indicatif::ProgressBar::new((num_pages * chunk_size) as u64)),
501            );
502            progress.set_style(style.clone());
503            progress.set_message("Erasing");
504            for _ in 0 .. num_pages {
505                connection.call::<S>(Request::Erase).await?.get();
506                progress.inc(chunk_size as u64);
507            }
508        }
509        if !payload.is_empty() {
510            if let Some(progress) = progress.take() {
511                progress.finish_with_message("Erased");
512            }
513            let progress = progress
514                .insert(multi_progress.add(indicatif::ProgressBar::new(payload.len() as u64)));
515            progress.set_style(style.clone());
516            progress.set_message("Writing");
517            for chunk in payload.chunks(chunk_size) {
518                connection.call::<S>(Request::Write { chunk }).await?.get();
519                progress.inc(chunk.len() as u64);
520            }
521        }
522        let progress = progress.unwrap_or_else(|| indicatif::ProgressBar::new(0).with_style(style));
523        progress.set_message("Finishing");
524        match (dry_run, finish) {
525            (false, Some(finish)) => final_call::<S>(connection, Request::Finish, finish).await?,
526            _ => drop(connection.call::<S>(Request::Finish).await?.get()),
527        }
528        progress.finish_with_message(message);
529        Ok(())
530    }
531}
532
533async fn final_call<S: service::Service>(
534    connection: &mut dyn Connection, request: S::Request<'_>,
535    proof: impl FnOnce(Yoke<S::Response<'static>>) -> Result<!>,
536) -> Result<()> {
537    connection.send(&S::request(request)).await?;
538    match connection.receive::<S>().await {
539        Ok(x) => proof(x)?,
540        Err(e) => {
541            if root_cause_is::<rusb::Error>(&e, |x| {
542                use rusb::Error::*;
543                matches!(x, NoDevice | Pipe | Io)
544            }) {
545                return Ok(());
546            }
547            if root_cause_is::<std::io::Error>(&e, |x| {
548                use std::io::ErrorKind::*;
549                matches!(x.kind(), NotConnected | BrokenPipe | UnexpectedEof)
550            }) {
551                return Ok(());
552            }
553            Err(e)
554        }
555    }
556}
557
558/// Reboots a platform.
559#[derive(clap::Args)]
560pub struct PlatformReboot {}
561
562impl PlatformReboot {
563    pub async fn run(self, connection: &mut dyn Connection) -> Result<()> {
564        let PlatformReboot {} = self;
565        final_call::<service::PlatformReboot>(connection, (), |x| match *x.get() {}).await
566    }
567}
568
569/// Locks a platform.
570#[derive(clap::Args)]
571pub struct PlatformLock {}
572
573impl PlatformLock {
574    pub async fn run(self, connection: &mut dyn Connection) -> Result<()> {
575        let PlatformLock {} = self;
576        connection.call::<service::PlatformLock>(()).await.map(|x| *x.get())
577    }
578}
579
580/// Calls a vendor RPC on a platform.
581#[derive(clap::Args)]
582pub struct PlatformRpc {
583    #[clap(flatten)]
584    rpc: Rpc,
585}
586
587impl PlatformRpc {
588    pub async fn run(self, connection: &mut dyn Connection) -> Result<()> {
589        let PlatformRpc { rpc } = self;
590        let mut rpc = rpc.start().await?;
591        while let Some(request) = rpc.read().await? {
592            let response = connection.call::<service::PlatformVendor>(&request).await?;
593            rpc.write(response.get()).await?;
594        }
595        Ok(())
596    }
597}
598
599/// Creates a new Rust applet project.
600#[derive(clap::Args)]
601pub struct RustAppletNew {
602    /// Where to create the applet project.
603    #[arg(value_hint = ValueHint::AnyPath)]
604    path: PathBuf,
605
606    /// Name of the applet project (defaults to the directory name).
607    #[arg(long)]
608    name: Option<String>,
609}
610
611impl RustAppletNew {
612    pub async fn run(self) -> Result<()> {
613        let RustAppletNew { path, name } = self;
614        let mut cargo = Command::new("cargo");
615        cargo.args(["new", "--lib"]).arg(&path);
616        if let Some(name) = name {
617            cargo.arg(format!("--name={name}"));
618        }
619        cmd::execute(&mut cargo).await?;
620        cmd::execute(Command::new("cargo").args(["add", "wasefire"]).current_dir(&path)).await?;
621        let mut cargo = Command::new("cargo");
622        cargo.args(["add", "wasefire-stub", "--optional"]);
623        cmd::execute(cargo.current_dir(&path)).await?;
624        let mut sed = Command::new("sed");
625        sed.arg("-i");
626        sed.arg("s#^wasefire-stub\\( = .\"dep:wasefire-stub\"\\)#test\\1, \"wasefire/test\"#");
627        sed.arg("Cargo.toml");
628        cmd::execute(sed.current_dir(&path)).await?;
629        tokio::fs::remove_file(path.join("src/lib.rs")).await?;
630        fs::write(path.join("src/lib.rs"), include_str!("data/lib.rs")).await?;
631        Ok(())
632    }
633}
634
635/// Builds a Rust applet from its project.
636#[derive(clap::Parser)]
637pub struct RustAppletBuild {
638    /// Builds for production, disabling debugging facilities.
639    #[arg(long)]
640    pub prod: bool,
641
642    /// Builds a native applet, e.g. --native=thumbv7em-none-eabi.
643    #[arg(long, value_name = "TARGET")]
644    pub native: Option<String>,
645
646    /// Builds a pulley applet.
647    #[arg(long, conflicts_with = "native")]
648    pub pulley: bool,
649
650    /// Root directory of the crate.
651    #[arg(long, value_name = "DIRECTORY", default_value = ".")]
652    #[arg(value_hint = ValueHint::DirPath)]
653    pub crate_dir: PathBuf,
654
655    /// Copies the final artifacts to this directory.
656    #[arg(long, value_name = "DIRECTORY", default_value = "wasefire")]
657    #[arg(value_hint = ValueHint::DirPath)]
658    pub output_dir: PathBuf,
659
660    /// Cargo profile.
661    #[arg(long, default_value = "release")]
662    pub profile: String,
663
664    /// Optimization level.
665    #[clap(long, short = 'O')]
666    pub opt_level: Option<OptLevel>,
667
668    /// Stack size (ignored for native applets).
669    #[clap(long, default_value = "16384")]
670    pub stack_size: usize,
671
672    /// Extra arguments to cargo, e.g. --features=foo.
673    #[clap(last = true)]
674    pub cargo: Vec<String>,
675}
676
677impl RustAppletBuild {
678    pub async fn run(self) -> Result<()> {
679        let metadata = metadata(&self.crate_dir).await?;
680        let package = &metadata.packages[0];
681        let target_dir =
682            fs::try_relative(std::env::current_dir()?, &metadata.target_directory).await?;
683        let name = package.name.replace('-', "_");
684        let mut cargo = Command::new("cargo");
685        let mut rustflags = Vec::new();
686        cargo.args(["rustc", "--lib"]);
687        // We deliberately don't use the provided profile for those configs because they don't
688        // depend on user-provided options (as opposed to opt-level).
689        cargo.arg("--config=profile.release.codegen-units=1");
690        cargo.arg("--config=profile.release.lto=true");
691        cargo.arg("--config=profile.release.panic=\"abort\"");
692        match &self.native {
693            None => {
694                rustflags.push(format!("-C link-arg=-zstack-size={}", self.stack_size));
695                rustflags.push("-C target-feature=+bulk-memory".to_string());
696                cargo.args(["--crate-type=cdylib", "--target=wasm32-unknown-unknown"]);
697                wasefire_feature(package, "wasm", &mut cargo)?;
698            }
699            Some(target) => {
700                cargo.args(["--crate-type=staticlib", &format!("--target={target}")]);
701                wasefire_feature(package, "native", &mut cargo)?;
702                if target == "riscv32imc-unknown-none-elf" {
703                    wasefire_feature(package, "unsafe-assume-single-core", &mut cargo)?;
704                }
705            }
706        }
707        let profile = &self.profile;
708        cargo.arg(format!("--profile={profile}"));
709        if let Some(level) = self.opt_level {
710            cargo.arg(format!("--config=profile.{profile}.opt-level={level}"));
711        }
712        cargo.args(&self.cargo);
713        if self.prod {
714            cargo.arg("-Zbuild-std=core,alloc");
715            // TODO(https://github.com/rust-lang/rust/issues/122105): Remove when fixed.
716            rustflags.push("--allow=unused-crate-dependencies".to_string());
717            let mut features = "-Zbuild-std-features=panic_immediate_abort".to_string();
718            if self.opt_level.is_some_and(OptLevel::optimize_for_size) {
719                features.push_str(",optimize_for_size");
720            }
721            cargo.arg(features);
722        } else {
723            cargo.env("WASEFIRE_DEBUG", "");
724        }
725        cargo.env("RUSTFLAGS", rustflags.join(" "));
726        cargo.current_dir(&self.crate_dir);
727        cmd::execute(&mut cargo).await?;
728        if let Some(target) = &self.native {
729            let src = target_dir.join(format!("{target}/{profile}/lib{name}.a"));
730            let dst = self.output_dir.join("libapplet.a");
731            if fs::has_changed(&src, &dst).await? {
732                fs::copy(src, dst).await?;
733            }
734            return Ok(());
735        }
736        let src = target_dir.join(format!("wasm32-unknown-unknown/{profile}/{name}.wasm"));
737        let opt = self.output_dir.join("applet-opt.wasm");
738        optimize_wasm(src, self.opt_level, &opt).await?;
739        if self.pulley {
740            let dst = self.output_dir.join("applet.pulley");
741            compile_pulley(opt, dst).await?;
742        } else {
743            let dst = self.output_dir.join("applet.wasm");
744            compute_sidetable(opt, dst).await?;
745        }
746        Ok(())
747    }
748}
749
750/// Runs the unit-tests of a Rust applet project.
751#[derive(clap::Args)]
752pub struct RustAppletTest {
753    /// Root directory of the crate.
754    #[arg(long, value_name = "DIRECTORY", default_value = ".")]
755    #[arg(value_hint = ValueHint::DirPath)]
756    crate_dir: PathBuf,
757
758    /// Extra arguments to cargo, e.g. --features=foo.
759    #[clap(last = true)]
760    cargo: Vec<String>,
761}
762
763impl RustAppletTest {
764    pub async fn run(self) -> Result<()> {
765        let metadata = metadata(&self.crate_dir).await?;
766        let package = &metadata.packages[0];
767        ensure!(package.features.contains_key("test"), "missing test feature");
768        let mut cargo = Command::new("cargo");
769        cargo.args(["test", "--features=test"]);
770        cargo.args(&self.cargo);
771        cargo.current_dir(&self.crate_dir);
772        cmd::replace(cargo)
773    }
774}
775
776/// Builds and installs a Rust applet from its project.
777#[derive(clap::Parser)]
778pub struct RustAppletInstall {
779    #[clap(flatten)]
780    build: RustAppletBuild,
781
782    #[clap(flatten)]
783    transfer: Transfer,
784
785    #[command(subcommand)]
786    wait: Option<AppletInstallWait>,
787}
788
789impl RustAppletInstall {
790    pub async fn run(self, connection: &mut dyn Connection) -> Result<()> {
791        let RustAppletInstall { build, transfer, wait } = self;
792        let output = build.output_dir.clone();
793        build.run().await?;
794        let install = AppletInstall { applet: output.join("applet.wasm"), transfer, wait };
795        install.run(connection).await
796    }
797}
798
799#[derive(Copy, Clone, ValueEnum)]
800pub enum OptLevel {
801    #[value(name = "0")]
802    O0,
803    #[value(name = "1")]
804    O1,
805    #[value(name = "2")]
806    O2,
807    #[value(name = "3")]
808    O3,
809    #[value(name = "s")]
810    Os,
811    #[value(name = "z")]
812    Oz,
813}
814
815impl OptLevel {
816    /// Returns whether the opt-level optimizes for size.
817    pub fn optimize_for_size(self) -> bool {
818        matches!(self, OptLevel::Os | OptLevel::Oz)
819    }
820}
821
822impl Display for OptLevel {
823    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
824        let value = self.to_possible_value().unwrap();
825        let name = value.get_name();
826        if f.alternate() || !matches!(self, OptLevel::Os | OptLevel::Oz) {
827            write!(f, "{name}")
828        } else {
829            write!(f, "{name:?}")
830        }
831    }
832}
833
834/// Strips and optimizes a wasm module.
835pub async fn optimize_wasm(
836    src: impl AsRef<Path>, opt_level: Option<OptLevel>, dst: impl AsRef<Path>,
837) -> Result<()> {
838    if !fs::has_changed(src.as_ref(), dst.as_ref()).await? {
839        return Ok(());
840    }
841    let result: Result<()> = try {
842        fs::copy(src, dst.as_ref()).await?;
843        cmd::execute(Command::new("wasm-strip").arg(dst.as_ref())).await?;
844        let mut opt = Command::new("wasm-opt");
845        opt.args(["--enable-bulk-memory", "--enable-sign-ext", "--enable-mutable-globals"]);
846        match opt_level {
847            Some(level) => drop(opt.arg(format!("-O{level:#}"))),
848            None => drop(opt.arg("-O")),
849        }
850        opt.arg(dst.as_ref());
851        opt.arg("-o");
852        opt.arg(dst.as_ref());
853        cmd::execute(&mut opt).await?;
854    };
855    if result.is_err() {
856        let _ = fs::remove_file(dst).await;
857    }
858    result.context("optimizing wasm")
859}
860
861/// Compiles a WASM applet into a Pulley applet.
862pub async fn compile_pulley(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
863    if !fs::has_changed(src.as_ref(), dst.as_ref()).await? {
864        return Ok(());
865    }
866    let result: Result<()> = try {
867        let wasm = fs::read(src).await?;
868        let mut config = wasmtime::Config::new();
869        config.target("pulley32")?;
870        // TODO(https://github.com/bytecodealliance/wasmtime/issues/10286): Also strip symbol table.
871        config.generate_address_map(false);
872        config.memory_init_cow(false);
873        config.memory_reservation(0);
874        config.wasm_relaxed_simd(false);
875        config.wasm_simd(false);
876        let engine = wasmtime::Engine::new(&config)?;
877        fs::write(dst.as_ref(), &engine.precompile_module(&wasm)?).await?;
878    };
879    if result.is_err() {
880        let _ = fs::remove_file(dst).await;
881    }
882    result.context("compiling to pulley")
883}
884
885/// Computes the side-table and inserts it as the first section.
886pub async fn compute_sidetable(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
887    if !fs::has_changed(src.as_ref(), dst.as_ref()).await? {
888        return Ok(());
889    }
890    let result: Result<()> = try {
891        let wasm = fs::read(src).await?;
892        let wasm = wasefire_interpreter::prepare(&wasm)
893            .map_err(|_| anyhow!("failed to compute side-table"))?;
894        fs::write(&dst, &wasm).await?;
895    };
896    if result.is_err() {
897        let _ = fs::remove_file(dst).await;
898    }
899    result
900}
901
902fn wasefire_feature(
903    package: &cargo_metadata::Package, feature: &str, cargo: &mut Command,
904) -> Result<()> {
905    if package.features.contains_key(feature) {
906        cargo.arg(format!("--features={feature}"));
907    } else {
908        ensure!(
909            package.dependencies.iter().any(|x| x.name == "wasefire"),
910            "wasefire must be a direct dependency"
911        );
912        cargo.arg(format!("--features=wasefire/{feature}"));
913    }
914    Ok(())
915}