Skip to main content

wapm_cli/commands/
run.rs

1use crate::config::Config;
2use crate::constants::DEFAULT_RUNTIME;
3use crate::data::lock::is_lockfile_out_of_date;
4use crate::dataflow;
5use crate::dataflow::find_command_result;
6use crate::dataflow::find_command_result::get_command_from_anywhere;
7use crate::dataflow::manifest_packages::ManifestResult;
8use crate::util::get_runtime_with_args;
9use std::ffi::OsString;
10use std::path::{Path, PathBuf};
11#[cfg(not(target_os = "wasi"))]
12use std::process::Command;
13use structopt::StructOpt;
14use thiserror::Error;
15use wapm_toml::Manifest;
16#[cfg(target_os = "wasi")]
17use wasm_bus_process::prelude::Command;
18
19#[derive(StructOpt, Debug)]
20pub struct RunOpt {
21    /// Command name
22    command: String,
23    /// WASI pre-opened directory
24    #[structopt(long = "dir", multiple = true, group = "wasi")]
25    pre_opened_directories: Vec<String>,
26    /// Application arguments
27    #[structopt(multiple = true, parse(from_os_str))]
28    args: Vec<OsString>,
29}
30
31pub fn run(run_options: RunOpt) -> anyhow::Result<()> {
32    let command_name = run_options.command.as_str();
33    let args = &run_options.args;
34    let current_dir = crate::config::Config::get_current_dir()?;
35
36    // always update the local lockfile if the manifest has changed
37    match is_lockfile_out_of_date(&current_dir) {
38        Ok(false) => {}
39        _ => dataflow::update(vec![], vec![], &current_dir)
40            .map(|_| ())
41            .map_err(|e| RunError::CannotRegenLockfile(command_name.to_string(), e))?,
42    }
43
44    let mut registry_error = Vec::new();
45    let find_command_result = get_command_from_anywhere(command_name);
46    if find_command_result.is_err() {
47        let package_info =
48            find_command_result::PackageInfoFromCommand::get(command_name.to_string());
49        registry_error = match package_info {
50            Err(e) => {
51                vec![format!("Error: {e}"), format!("")]
52            }
53            Ok(o) => vec![
54                format!(
55                    "Command {:?} was not found, but the package \"{}@{}\" has this command.",
56                    o.command, o.namespaced_package_name, o.version
57                ),
58                format!(
59                    "You can install it with `wapm install {}@{}`.",
60                    o.namespaced_package_name, o.version
61                ),
62            ],
63        };
64    }
65
66    let find_command_result::Command {
67        source: source_path_buf,
68        manifest_dir,
69        args: _,
70        module_name,
71        is_global,
72        prehashed_cache_key,
73    } = match find_command_result {
74        Err(find_command_result::Error::CommandNotFound {
75            command,
76            mut local_log,
77            mut global_log,
78            ..
79        }) => {
80            let mut log = vec![format!("Could not find command {:?}:", command)];
81            log.append(&mut local_log);
82            log.append(&mut global_log);
83            log.append(&mut registry_error);
84            return Err(anyhow!("{}", log.join("\r\n")));
85        },
86        Err(find_command_result::Error::ReadingLocalDirectory {
87            mut local_log,
88            ..
89        }) => {
90            local_log.append(&mut registry_error);
91            return Err(anyhow!("{}", local_log.join("\r\n")));
92        },
93        Err(find_command_result::Error::CommandNotFoundInLocalDirectoryAndErrorReadingGlobalDirectory {
94            local_log: mut log,
95            mut global_log,
96            ..
97        }) => {
98            log.append(&mut global_log);
99            log.append(&mut registry_error);
100            return Err(anyhow!("{}", log.join("\r\n")));
101        },
102        otherwise => otherwise?,
103    };
104
105    let run_dir = if is_global {
106        Config::get_globals_directory().unwrap()
107    } else {
108        current_dir
109    };
110
111    let manifest_dir = run_dir.join(manifest_dir);
112
113    do_run(
114        run_dir,
115        source_path_buf,
116        manifest_dir,
117        command_name,
118        &module_name,
119        &run_options.pre_opened_directories,
120        args,
121        prehashed_cache_key,
122    )
123}
124
125#[allow(clippy::too_many_arguments)]
126pub(crate) fn do_run(
127    run_dir: PathBuf,
128    source_path_buf: PathBuf,
129    manifest_dir: PathBuf,
130    command_name: &str,
131    module_name: &str,
132    pre_opened_directories: &[String],
133    args: &[OsString],
134    prehashed_cache_key: Option<String>,
135) -> anyhow::Result<()> {
136    debug!(
137        "Running module located at {:?}",
138        &run_dir.join(&source_path_buf)
139    );
140
141    run_dir.join(&source_path_buf).metadata().map_err(|_| {
142        RunError::SourceForCommandNotFound(
143            command_name.to_string(),
144            module_name.to_string(),
145            source_path_buf.to_string_lossy().to_string(),
146        )
147    })?;
148
149    let wasmer_extra_flags: Option<Vec<OsString>> =
150        match ManifestResult::find_in_directory(&manifest_dir) {
151            ManifestResult::Manifest(manifest) => manifest
152                .package
153                .wasmer_extra_flags
154                .map(|extra_flags| extra_flags.split_whitespace().map(OsString::from).collect()),
155            _ => None,
156        };
157
158    let mut wasi_preopened_dir_flags: Vec<OsString> = pre_opened_directories
159        .iter()
160        .map(|entry| OsString::from(format!("--dir={}", entry)))
161        .collect();
162
163    let mut disable_command_rename = false;
164
165    if let ManifestResult::Manifest(Manifest { package, fs, .. }) =
166        ManifestResult::find_in_directory(&manifest_dir)
167    {
168        disable_command_rename = package.disable_command_rename;
169
170        if let Some(ref fs) = fs {
171            // todo: normalize (rm `:` and newline, etc) these paths if we haven't yet
172            for (guest_path, host_path) in fs.iter() {
173                wasi_preopened_dir_flags.push(OsString::from(format!(
174                    "--mapdir={}:{}",
175                    guest_path,
176                    manifest_dir.join(host_path).to_string_lossy(),
177                )));
178            }
179        }
180    }
181
182    let (runtime, runtime_args) = get_runtime_with_args();
183
184    let mut cmd;
185    if cfg!(target_os = "wasi") {
186        debug!("Running wapm process: {:?}", source_path_buf);
187        cmd = Command::new(source_path_buf.to_string_lossy().as_ref());
188        cmd.args(args);
189        #[cfg(target_os = "wasi")]
190        for preopen in wasi_preopened_dir_flags
191            .iter()
192            .map(|a| a.to_string_lossy())
193            .filter_map(|a| a.split_once("=").map(|a| a.1.replace("\"", "")))
194        {
195            cmd.pre_open(preopen.to_string());
196        }
197    } else {
198        // avoid `wasmer-js`, allow other wasmers
199        let using_default_runtime = Path::new(&runtime)
200            .file_name()
201            .map(|file_name| file_name.to_string_lossy().ends_with(DEFAULT_RUNTIME))
202            .unwrap_or(false);
203        let command_override_name = if !using_default_runtime || disable_command_rename {
204            None
205        } else {
206            Some(command_name.to_string())
207        };
208        let command_vec = create_run_command(
209            args,
210            wasmer_extra_flags,
211            wasi_preopened_dir_flags,
212            &run_dir,
213            source_path_buf,
214            command_override_name,
215            prehashed_cache_key,
216        )?;
217        debug!("Running command with args: {:?}", command_vec);
218        cmd = Command::new(&runtime);
219        cmd.args(&runtime_args);
220        cmd.args(&command_vec);
221    };
222
223    let mut child = cmd
224        .spawn()
225        .map_err(|e| -> RunError { RunError::ProcessFailed(runtime, format!("{:?}", e)) })?;
226
227    child.wait()?;
228    Ok(())
229}
230
231fn create_run_command<P: AsRef<Path>, P2: AsRef<Path>>(
232    args: &[OsString],
233    wasmer_extra_flags: Option<Vec<OsString>>,
234    wasi_preopened_dir_flags: Vec<OsString>,
235    directory: P,
236    wasm_file_path: P2,
237    override_command_name: Option<String>,
238    prehashed_cache_key: Option<String>,
239) -> anyhow::Result<Vec<OsString>> {
240    let mut path = PathBuf::new();
241    path.push(directory);
242    path.push(wasm_file_path);
243    let path_string = path.into_os_string();
244    let command_vec = vec![path_string];
245    let override_command_name = override_command_name
246        .map(|cn| vec![OsString::from(format!("--command-name={}", cn))])
247        .unwrap_or_default();
248    let prehashed_cache_key_flag = prehashed_cache_key
249        .map(|pck| vec![OsString::from(format!("--cache-key=\"{}\"", pck))])
250        .unwrap_or_default();
251
252    // NOTE:
253    // for optional types, use an empty vec here:
254    // an empty OsString may pass empty args to the child program which can cause issues
255    Ok([
256        command_vec.as_slice(),
257        &override_command_name,
258        &wasi_preopened_dir_flags,
259        wasmer_extra_flags.as_deref().unwrap_or_default(),
260        &prehashed_cache_key_flag,
261        &[OsString::from("--")],
262        args,
263    ]
264    .concat())
265}
266
267#[cfg(test)]
268mod test {
269    use crate::commands::run::create_run_command;
270    use crate::data::manifest::PACKAGES_DIR_NAME;
271    use crate::util::create_temp_dir;
272    use std::ffi::OsString;
273    use std::fs;
274    use std::path::PathBuf;
275
276    #[test]
277    fn create_run_command_vec() {
278        let args: Vec<OsString> = vec![OsString::from("arg1"), OsString::from("arg2")];
279        let tmp_dir = create_temp_dir().unwrap();
280        let dir: &std::path::Path = tmp_dir.as_ref();
281        let wapm_module_dir = dir.join(
282            [PACKAGES_DIR_NAME, "_", "foo@1.0.2"]
283                .iter()
284                .collect::<PathBuf>(),
285        );
286        fs::create_dir_all(&wapm_module_dir).unwrap();
287        let expected_dir = wapm_module_dir.join("foo_entry.wasm");
288        let expected_command = vec![
289            expected_dir.into_os_string(),
290            OsString::from("--"),
291            OsString::from("arg1"),
292            OsString::from("arg2"),
293        ];
294        let wasm_relative_path: PathBuf = ["wapm_packages", "_", "foo@1.0.2", "foo_entry.wasm"]
295            .iter()
296            .collect();
297        let actual_command =
298            create_run_command(&args, None, vec![], dir, wasm_relative_path, None, None).unwrap();
299        assert_eq!(expected_command, actual_command);
300    }
301}
302
303#[derive(Debug, Error)]
304enum RunError {
305    #[error("Failed to run command \"{0}\". {1}")]
306    CannotRegenLockfile(String, dataflow::Error),
307    #[error(
308        "The command \"{0}\" for module \"{1}\" is defined but the source at \"{2}\" does not exist.",
309    )]
310    SourceForCommandNotFound(String, String, String),
311    #[error("Failed to run {0}: {1}")]
312    ProcessFailed(String, String),
313}