wasmer_cli/commands/
create_exe.rs

1//! Create a standalone native executable for a given Wasm file.
2
3use self::utils::normalize_atom_name;
4use super::CliCommand;
5use crate::{backend::RuntimeOptions, common::normalize_path, config::WasmerEnv};
6use anyhow::{Context, Result, anyhow, bail};
7use clap::Parser;
8use object::ObjectSection;
9use serde::{Deserialize, Serialize};
10use std::{
11    collections::BTreeMap,
12    env,
13    path::{Path, PathBuf},
14    process::{Command, Stdio},
15};
16use tar::Archive;
17use target_lexicon::BinaryFormat;
18use wasmer::{
19    sys::{engine::NativeEngineExt, *},
20    *,
21};
22use wasmer_compiler::{
23    object::{emit_serialized, get_object_for_target},
24    types::symbols::{ModuleMetadataSymbolRegistry, Symbol, SymbolRegistry},
25};
26use wasmer_package::utils::from_disk;
27use wasmer_types::ModuleInfo;
28use webc::{Container, Metadata, PathSegments, Volume as WebcVolume};
29
30const LINK_SYSTEM_LIBRARIES_WINDOWS: &[&str] = &["userenv", "Ws2_32", "advapi32", "bcrypt"];
31
32const LINK_SYSTEM_LIBRARIES_UNIX: &[&str] = &["dl", "m", "pthread"];
33
34#[derive(Debug, Parser)]
35/// The options for the `wasmer create-exe` subcommand
36pub struct CreateExe {
37    #[clap(flatten)]
38    env: WasmerEnv,
39
40    /// Input file
41    #[clap(name = "FILE")]
42    path: PathBuf,
43
44    /// Output file
45    #[clap(name = "OUTPUT PATH", short = 'o')]
46    output: PathBuf,
47
48    /// Optional directory used for debugging: if present, will output the zig command
49    /// for reproducing issues in a debug directory.
50    #[clap(long, name = "DEBUG PATH")]
51    debug_dir: Option<PathBuf>,
52
53    /// Prefix for every input file, e.g. "wat2wasm:sha256abc123" would
54    /// prefix every function in the wat2wasm input object with the "sha256abc123" hash
55    ///
56    /// If only a single value is given without containing a ":", this value is used for
57    /// all input files. If no value is given, the prefix is always equal to
58    /// the sha256 of the input .wasm file
59    #[clap(
60        long,
61        use_value_delimiter = true,
62        value_delimiter = ',',
63        name = "FILE:PREFIX:PATH"
64    )]
65    precompiled_atom: Vec<String>,
66
67    /// Compilation Target triple
68    ///
69    /// Accepted target triple values must follow the
70    /// ['target_lexicon'](https://crates.io/crates/target-lexicon) crate format.
71    ///
72    /// The recommended targets we try to support are:
73    ///
74    /// - "x86_64-linux-gnu"
75    /// - "aarch64-linux-gnu"
76    /// - "arm64-apple-darwin"
77    /// - "x86_64-windows-gnu"
78    #[clap(long = "target")]
79    target_triple: Option<Triple>,
80
81    /// Can specify either a release version (such as "3.0.1") or a URL to a tarball to use
82    /// for linking. By default, create-exe will always pull the latest release tarball from GitHub,
83    /// this flag can be used to override that behaviour.
84    #[clap(long, name = "URL_OR_RELEASE_VERSION")]
85    use_wasmer_release: Option<String>,
86
87    #[clap(long, short = 'm', number_of_values = 1)]
88    cpu_features: Vec<CpuFeature>,
89
90    /// Additional libraries to link against.
91    /// This is useful for fixing linker errors that may occur on some systems.
92    #[clap(long, short = 'l')]
93    libraries: Vec<String>,
94
95    #[clap(flatten)]
96    cross_compile: CrossCompile,
97
98    #[clap(flatten)]
99    compiler: RuntimeOptions,
100}
101
102/// Url or version to download the release from
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub enum UrlOrVersion {
105    /// URL to download
106    Url(url::Url),
107    /// Release version to download
108    Version(semver::Version),
109}
110
111impl UrlOrVersion {
112    fn from_str(s: &str) -> Result<Self, anyhow::Error> {
113        let mut err;
114        let s = s.strip_prefix('v').unwrap_or(s);
115        match url::Url::parse(s) {
116            Ok(o) => return Ok(Self::Url(o)),
117            Err(e) => {
118                err = anyhow::anyhow!("could not parse as URL: {e}");
119            }
120        }
121
122        match semver::Version::parse(s) {
123            Ok(o) => return Ok(Self::Version(o)),
124            Err(e) => {
125                err = anyhow::anyhow!("could not parse as URL or version: {e}").context(err);
126            }
127        }
128
129        Err(err)
130    }
131}
132
133// Cross-compilation options with `zig`
134#[derive(Debug, Clone, Default, Parser)]
135pub(crate) struct CrossCompile {
136    /// Use the system linker instead of zig for linking
137    #[clap(long)]
138    use_system_linker: bool,
139
140    /// Cross-compilation library path (path to libwasmer.a / wasmer.lib)
141    #[clap(long = "library-path")]
142    library_path: Option<PathBuf>,
143
144    /// Cross-compilation tarball library path
145    #[clap(long = "tarball")]
146    tarball: Option<PathBuf>,
147
148    /// Specify `zig` binary path (defaults to `zig` in $PATH if not present)
149    #[clap(long = "zig-binary-path", env)]
150    zig_binary_path: Option<PathBuf>,
151}
152
153#[derive(Debug)]
154pub(crate) struct CrossCompileSetup {
155    pub(crate) target: Triple,
156    pub(crate) zig_binary_path: Option<PathBuf>,
157    pub(crate) library: PathBuf,
158}
159
160/// Given a pirita file, determines whether the file has one
161/// default command as an entrypoint or multiple (need to be specified via --command)
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163pub struct Entrypoint {
164    /// Compiled atom files to link into the final binary
165    pub atoms: Vec<CommandEntrypoint>,
166    /// Volume objects (if any) to link into the final binary
167    pub volumes: Vec<Volume>,
168}
169
170/// Command entrypoint for multiple commands
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172pub struct CommandEntrypoint {
173    /// Command name
174    pub command: String,
175    /// Atom name
176    pub atom: String,
177    /// Path to the object file, relative to the entrypoint.json parent dir
178    pub path: PathBuf,
179    /// Optional path to the static_defs.h header file, relative to the entrypoint.json parent dir
180    pub header: Option<PathBuf>,
181    /// Module info, set when the wasm file is compiled
182    pub module_info: Option<ModuleInfo>,
183}
184
185/// Volume object file (name + path to object file)
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187pub struct Volume {
188    /// Volume name
189    pub name: String,
190    /// Path to volume fileblock object file
191    pub obj_file: PathBuf,
192}
193
194impl CliCommand for CreateExe {
195    type Output = ();
196
197    /// Runs logic for the `compile` subcommand
198    fn run(self) -> Result<Self::Output, anyhow::Error> {
199        let path = normalize_path(&format!("{}", self.path.display()));
200        let target_triple = self.target_triple.clone().unwrap_or_else(Triple::host);
201        let mut cc = self.cross_compile.clone();
202        let target = utils::target_triple_to_target(&target_triple, &self.cpu_features);
203
204        let starting_cd = env::current_dir()?;
205        let input_path = starting_cd.join(path);
206        let output_path = starting_cd.join(&self.output);
207
208        let url_or_version = match self
209            .use_wasmer_release
210            .as_deref()
211            .map(UrlOrVersion::from_str)
212        {
213            Some(Ok(o)) => Some(o),
214            Some(Err(e)) => return Err(e),
215            None => None,
216        };
217
218        let cross_compilation = utils::get_cross_compile_setup(
219            &self.env,
220            &mut cc,
221            &target_triple,
222            &starting_cd,
223            url_or_version,
224        )?;
225
226        if input_path.is_dir() {
227            return Err(anyhow::anyhow!("input path cannot be a directory"));
228        }
229
230        let _backends = self.compiler.get_available_backends()?;
231        let engine = self.compiler.get_engine(&target)?;
232
233        println!("Compiler: {}", engine.deterministic_id());
234        println!("Target: {}", target.triple());
235        println!(
236            "Using path `{}` as libwasmer path.",
237            cross_compilation.library.display()
238        );
239
240        if !cross_compilation.library.exists() {
241            return Err(anyhow::anyhow!("library path does not exist"));
242        }
243
244        let temp = tempfile::tempdir();
245        let tempdir = match self.debug_dir.as_ref() {
246            Some(s) => s.clone(),
247            None => temp?.path().to_path_buf(),
248        };
249        std::fs::create_dir_all(&tempdir)?;
250
251        let atoms = if let Ok(pirita) = from_disk(&input_path) {
252            // pirita file
253            compile_pirita_into_directory(
254                &pirita,
255                &tempdir,
256                &self.compiler,
257                &self.cpu_features,
258                &cross_compilation.target,
259                &self.precompiled_atom,
260                AllowMultiWasm::Allow,
261                self.debug_dir.is_some(),
262            )
263        } else {
264            // wasm file
265            prepare_directory_from_single_wasm_file(
266                &input_path,
267                &tempdir,
268                &self.compiler,
269                &cross_compilation.target,
270                &self.cpu_features,
271                &self.precompiled_atom,
272                self.debug_dir.is_some(),
273            )
274        }?;
275
276        get_module_infos(&engine, &tempdir, &atoms)?;
277        let mut entrypoint = get_entrypoint(&tempdir)?;
278        create_header_files_in_dir(
279            &tempdir,
280            &mut entrypoint,
281            &atoms,
282            &self.precompiled_atom,
283            &target_triple.binary_format,
284        )?;
285        link_exe_from_dir(
286            &self.env,
287            &tempdir,
288            output_path,
289            &cross_compilation,
290            &self.libraries,
291            self.debug_dir.is_some(),
292            &atoms,
293            &self.precompiled_atom,
294        )?;
295
296        if self.target_triple.is_some() {
297            eprintln!(
298                "✔ Cross-compiled executable for `{}` target compiled successfully to `{}`.",
299                target.triple(),
300                self.output.display(),
301            );
302        } else {
303            eprintln!(
304                "✔ Native executable compiled successfully to `{}`.",
305                self.output.display(),
306            );
307        }
308
309        Ok(())
310    }
311}
312
313fn write_entrypoint(directory: &Path, entrypoint: &Entrypoint) -> Result<(), anyhow::Error> {
314    std::fs::write(
315        directory.join("entrypoint.json"),
316        serde_json::to_string_pretty(&entrypoint).unwrap(),
317    )
318    .map_err(|e| {
319        anyhow::anyhow!(
320            "cannot create entrypoint.json dir in {}: {e}",
321            directory.display()
322        )
323    })
324}
325
326fn get_entrypoint(directory: &Path) -> Result<Entrypoint, anyhow::Error> {
327    let entrypoint_json =
328        std::fs::read_to_string(directory.join("entrypoint.json")).map_err(|e| {
329            anyhow::anyhow!(
330                "could not read entrypoint.json in {}: {e}",
331                directory.display()
332            )
333        })?;
334
335    let entrypoint: Entrypoint = serde_json::from_str(&entrypoint_json).map_err(|e| {
336        anyhow::anyhow!(
337            "could not parse entrypoint.json in {}: {e}",
338            directory.display()
339        )
340    })?;
341
342    if entrypoint.atoms.is_empty() {
343        return Err(anyhow::anyhow!("file has no atoms to compile"));
344    }
345
346    Ok(entrypoint)
347}
348
349/// In pirita mode, specifies whether multi-atom
350/// pirita files should be allowed or rejected
351#[derive(Debug, PartialEq, Eq, Clone)]
352pub enum AllowMultiWasm {
353    /// allow
354    Allow,
355    /// reject
356    Reject(Option<String>),
357}
358
359/// Given a pirita file, compiles the .wasm files into the target directory
360#[allow(clippy::too_many_arguments)]
361pub(super) fn compile_pirita_into_directory(
362    pirita: &Container,
363    target_dir: &Path,
364    compiler: &RuntimeOptions,
365    cpu_features: &[CpuFeature],
366    triple: &Triple,
367    prefixes: &[String],
368    allow_multi_wasm: AllowMultiWasm,
369    debug: bool,
370) -> anyhow::Result<Vec<(String, Vec<u8>)>> {
371    let all_atoms = match &allow_multi_wasm {
372        AllowMultiWasm::Allow | AllowMultiWasm::Reject(None) => {
373            pirita.atoms().into_iter().collect::<Vec<_>>()
374        }
375        AllowMultiWasm::Reject(Some(s)) => {
376            let atom = pirita
377                .get_atom(s)
378                .with_context(|| format!("could not find atom \"{s}\""))?;
379            vec![(s.to_string(), atom)]
380        }
381    };
382
383    allow_multi_wasm.validate(&all_atoms)?;
384
385    std::fs::create_dir_all(target_dir)
386        .map_err(|e| anyhow::anyhow!("cannot create / dir in {}: {e}", target_dir.display()))?;
387
388    let target_dir = target_dir.canonicalize()?;
389    let target = &utils::target_triple_to_target(triple, cpu_features);
390
391    std::fs::create_dir_all(target_dir.join("volumes")).map_err(|e| {
392        anyhow::anyhow!(
393            "cannot create /volumes dir in {}: {e}",
394            target_dir.display()
395        )
396    })?;
397
398    let volumes = pirita.volumes();
399    let volume_bytes = volume_file_block(&volumes);
400    let volume_name = "VOLUMES";
401    let volume_path = target_dir.join("volumes").join("volume.o");
402    write_volume_obj(&volume_bytes, volume_name, &volume_path, target)?;
403    let volume_path = volume_path.canonicalize()?;
404    let volume_path = pathdiff::diff_paths(volume_path, &target_dir).unwrap();
405
406    std::fs::create_dir_all(target_dir.join("atoms")).map_err(|e| {
407        anyhow::anyhow!("cannot create /atoms dir in {}: {e}", target_dir.display())
408    })?;
409
410    let mut atoms_from_file = Vec::new();
411    let mut target_paths = Vec::new();
412
413    for (atom_name, atom_bytes) in all_atoms {
414        atoms_from_file.push((utils::normalize_atom_name(&atom_name), atom_bytes.to_vec()));
415        let atom_path = target_dir
416            .join("atoms")
417            .join(format!("{}.o", utils::normalize_atom_name(&atom_name)));
418        let header_path = {
419            std::fs::create_dir_all(target_dir.join("include")).map_err(|e| {
420                anyhow::anyhow!(
421                    "cannot create /include dir in {}: {e}",
422                    target_dir.display()
423                )
424            })?;
425
426            let header_path = target_dir.join("include").join(format!(
427                "static_defs_{}.h",
428                utils::normalize_atom_name(&atom_name)
429            ));
430
431            Some(header_path)
432        };
433        target_paths.push((
434            atom_name.clone(),
435            utils::normalize_atom_name(&atom_name),
436            atom_path,
437            header_path,
438        ));
439    }
440
441    let prefix_map = PrefixMapCompilation::from_input(&atoms_from_file, prefixes, false)
442        .with_context(|| anyhow::anyhow!("compile_pirita_into_directory"))?;
443
444    let module_infos = compile_atoms(
445        &atoms_from_file,
446        &target_dir.join("atoms"),
447        compiler,
448        target,
449        &prefix_map,
450        debug,
451    )?;
452
453    // target_dir
454    let mut atoms = Vec::new();
455    for (command_name, atom_name, a, opt_header_path) in target_paths {
456        let mut atom_path = a;
457        let mut header_path = opt_header_path;
458        if let Ok(a) = atom_path.canonicalize() {
459            let opt_header_path = header_path.and_then(|p| p.canonicalize().ok());
460            atom_path = pathdiff::diff_paths(&a, &target_dir).unwrap_or_else(|| a.clone());
461            header_path = opt_header_path.and_then(|h| pathdiff::diff_paths(h, &target_dir));
462        }
463        atoms.push(CommandEntrypoint {
464            // TODO: improve, "--command pip" should be able to invoke atom "python" with args "-m pip"
465            command: command_name,
466            atom: atom_name.clone(),
467            path: atom_path,
468            header: header_path,
469            module_info: module_infos.get(&atom_name).cloned(),
470        });
471    }
472
473    let entrypoint = Entrypoint {
474        atoms,
475        volumes: vec![Volume {
476            name: volume_name.to_string(),
477            obj_file: volume_path,
478        }],
479    };
480
481    write_entrypoint(&target_dir, &entrypoint)?;
482
483    Ok(atoms_from_file)
484}
485
486/// Serialize a set of volumes so they can be read by the C API.
487///
488/// This is really backwards, but the only way to create a v1 volume "fileblock"
489/// is by first reading every file in a [`webc::compat::Volume`], serializing it
490/// to webc v1's binary form, parsing those bytes into a [`webc::v1::Volume`],
491/// then constructing a dummy [`webc::v1::WebC`] object just so we can make a
492/// copy of its file block.
493fn volume_file_block(volumes: &BTreeMap<String, WebcVolume>) -> Vec<u8> {
494    let serialized_volumes: Vec<(&String, Vec<u8>)> = volumes
495        .iter()
496        .map(|(name, volume)| (name, serialize_volume_to_webc_v1(volume)))
497        .collect();
498
499    let parsed_volumes: indexmap::IndexMap<String, webc::v1::Volume<'_>> = serialized_volumes
500        .iter()
501        .filter_map(|(name, serialized_volume)| {
502            let volume = webc::v1::Volume::parse(serialized_volume).ok()?;
503            Some((name.to_string(), volume))
504        })
505        .collect();
506
507    let webc = webc::v1::WebC {
508        version: 0,
509        checksum: None,
510        signature: None,
511        manifest: webc::metadata::Manifest::default(),
512        atoms: webc::v1::Volume::default(),
513        volumes: parsed_volumes,
514    };
515
516    webc.get_volumes_as_fileblock()
517}
518
519fn serialize_volume_to_webc_v1(volume: &WebcVolume) -> Vec<u8> {
520    fn read_dir(
521        volume: &WebcVolume,
522        path: &mut PathSegments,
523        files: &mut BTreeMap<webc::v1::DirOrFile, Vec<u8>>,
524    ) {
525        for (segment, _, meta) in volume.read_dir(&*path).unwrap_or_default() {
526            path.push(segment);
527
528            match meta {
529                Metadata::Dir { .. } => {
530                    files.insert(
531                        webc::v1::DirOrFile::Dir(path.to_string().into()),
532                        Vec::new(),
533                    );
534                    read_dir(volume, path, files);
535                }
536                Metadata::File { .. } => {
537                    if let Some((contents, _)) = volume.read_file(&*path) {
538                        files.insert(
539                            webc::v1::DirOrFile::File(path.to_string().into()),
540                            contents.to_vec(),
541                        );
542                    }
543                }
544            }
545
546            path.pop();
547        }
548    }
549
550    let mut path = PathSegments::ROOT;
551    let mut files = BTreeMap::new();
552
553    read_dir(volume, &mut path, &mut files);
554
555    webc::v1::Volume::serialize_files(files)
556}
557
558impl AllowMultiWasm {
559    fn validate(
560        &self,
561        all_atoms: &[(String, webc::compat::SharedBytes)],
562    ) -> Result<(), anyhow::Error> {
563        if matches!(self, AllowMultiWasm::Reject(None)) && all_atoms.len() > 1 {
564            let keys = all_atoms
565                .iter()
566                .map(|(name, _)| name.clone())
567                .collect::<Vec<_>>();
568
569            return Err(anyhow::anyhow!(
570                "where <ATOM> is one of: {}",
571                keys.join(", ")
572            ))
573            .context(anyhow::anyhow!(
574                "note: use --atom <ATOM> to specify which atom to compile"
575            ))
576            .context(anyhow::anyhow!(
577                "cannot compile more than one atom at a time"
578            ));
579        }
580
581        Ok(())
582    }
583}
584
585/// Prefix map used during compilation of object files
586#[derive(Debug, Default, PartialEq)]
587pub(crate) struct PrefixMapCompilation {
588    /// Sha256 hashes for the input files
589    input_hashes: BTreeMap<String, String>,
590    /// Manual prefixes for input files (file:prefix)
591    manual_prefixes: BTreeMap<String, String>,
592    /// Cached compilation objects for files on disk
593    #[allow(dead_code)]
594    compilation_objects: BTreeMap<String, Vec<u8>>,
595}
596
597impl PrefixMapCompilation {
598    /// Sets up the prefix map from a collection like "sha123123" or "wasmfile:sha123123" or "wasmfile:/tmp/filepath/:sha123123"
599    fn from_input(
600        atoms: &[(String, Vec<u8>)],
601        prefixes: &[String],
602        only_validate_prefixes: bool,
603    ) -> Result<Self, anyhow::Error> {
604        // No atoms: no prefixes necessary
605        if atoms.is_empty() {
606            return Ok(Self::default());
607        }
608
609        // default case: no prefixes have been specified:
610        // for all atoms, calculate the sha256
611        if prefixes.is_empty() {
612            return Ok(Self {
613                input_hashes: atoms
614                    .iter()
615                    .map(|(name, bytes)| (normalize_atom_name(name), Self::hash_for_bytes(bytes)))
616                    .collect(),
617                manual_prefixes: BTreeMap::new(),
618                compilation_objects: BTreeMap::new(),
619            });
620        }
621
622        // if prefixes are specified, have to match the atom names exactly
623        if prefixes.len() != atoms.len() {
624            println!(
625                "WARNING: invalid mapping of prefix and atoms: expected prefixes for {} atoms, got {} prefixes",
626                atoms.len(),
627                prefixes.len()
628            );
629        }
630
631        let available_atoms = atoms.iter().map(|(k, _)| k.clone()).collect::<Vec<_>>();
632        let mut manual_prefixes = BTreeMap::new();
633        let mut compilation_objects = BTreeMap::new();
634
635        for p in prefixes.iter() {
636            // On windows, we have to account for paths like "C://", which would
637            // prevent a simple .split(":") or a regex
638            let prefix_split = Self::split_prefix(p);
639
640            match prefix_split.as_slice() {
641                // ATOM:PREFIX:PATH
642                [atom, prefix, path] => {
643                    if only_validate_prefixes {
644                        // only insert the prefix in order to not error out of the fs::read(path)
645                        manual_prefixes.insert(normalize_atom_name(atom), prefix.to_string());
646                    } else {
647                        let atom_hash = atoms
648                        .iter()
649                        .find_map(|(name, _)| if normalize_atom_name(name) == normalize_atom_name(atom) { Some(prefix.to_string()) } else { None })
650                        .ok_or_else(|| anyhow::anyhow!("no atom {atom:?} found, for prefix {p:?}, available atoms are {available_atoms:?}"))?;
651
652                        let current_dir = std::env::current_dir().unwrap().canonicalize().unwrap();
653                        let path = current_dir.join(path.replace("./", ""));
654                        let bytes = std::fs::read(&path).map_err(|e| {
655                            anyhow::anyhow!("could not read file for atom {atom:?} (prefix {p}, path {} in dir {}): {e}", path.display(), current_dir.display())
656                        })?;
657
658                        compilation_objects.insert(normalize_atom_name(atom), bytes);
659                        manual_prefixes.insert(normalize_atom_name(atom), atom_hash.to_string());
660                    }
661                }
662                // atom + path, but default SHA256 prefix
663                [atom, path] => {
664                    let atom_hash = atoms
665                    .iter()
666                    .find_map(|(name, bytes)| if normalize_atom_name(name) == normalize_atom_name(atom) { Some(Self::hash_for_bytes(bytes)) } else { None })
667                    .ok_or_else(|| anyhow::anyhow!("no atom {atom:?} found, for prefix {p:?}, available atoms are {available_atoms:?}"))?;
668                    manual_prefixes.insert(normalize_atom_name(atom), atom_hash.to_string());
669
670                    if !only_validate_prefixes {
671                        let current_dir = std::env::current_dir().unwrap().canonicalize().unwrap();
672                        let path = current_dir.join(path.replace("./", ""));
673                        let bytes = std::fs::read(&path).map_err(|e| {
674                            anyhow::anyhow!("could not read file for atom {atom:?} (prefix {p}, path {} in dir {}): {e}", path.display(), current_dir.display())
675                        })?;
676                        compilation_objects.insert(normalize_atom_name(atom), bytes);
677                    }
678                }
679                // only prefix if atoms.len() == 1
680                [prefix] if atoms.len() == 1 => {
681                    manual_prefixes.insert(normalize_atom_name(&atoms[0].0), prefix.to_string());
682                }
683                _ => {
684                    return Err(anyhow::anyhow!(
685                        "invalid --precompiled-atom {p:?} - correct format is ATOM:PREFIX:PATH or ATOM:PATH"
686                    ));
687                }
688            }
689        }
690
691        Ok(Self {
692            input_hashes: BTreeMap::new(),
693            manual_prefixes,
694            compilation_objects,
695        })
696    }
697
698    fn split_prefix(s: &str) -> Vec<String> {
699        let regex =
700            regex::Regex::new(r"^([a-zA-Z0-9\-_]+)(:([a-zA-Z0-9\.\-_]+))?(:(.+*))?").unwrap();
701        let mut captures = regex
702            .captures(s.trim())
703            .map(|c| {
704                c.iter()
705                    .skip(1)
706                    .flatten()
707                    .map(|m| m.as_str().to_owned())
708                    .collect::<Vec<_>>()
709            })
710            .unwrap_or_default();
711        if captures.is_empty() {
712            vec![s.to_string()]
713        } else if captures.len() == 5 {
714            captures.remove(1);
715            captures.remove(2);
716            captures
717        } else if captures.len() == 3 {
718            s.splitn(2, ':').map(|s| s.to_string()).collect()
719        } else {
720            captures
721        }
722    }
723
724    pub(crate) fn hash_for_bytes(bytes: &[u8]) -> String {
725        use sha2::{Digest, Sha256};
726        let mut hasher = Sha256::new();
727        hasher.update(bytes);
728        let result = hasher.finalize();
729
730        hex::encode(&result[..])
731    }
732
733    fn get_prefix_for_atom(&self, atom_name: &str) -> Option<String> {
734        self.manual_prefixes
735            .get(atom_name)
736            .or_else(|| self.input_hashes.get(atom_name))
737            .cloned()
738    }
739
740    #[allow(dead_code)]
741    fn get_compilation_object_for_atom(&self, atom_name: &str) -> Option<&[u8]> {
742        self.compilation_objects
743            .get(atom_name)
744            .map(|s| s.as_slice())
745    }
746}
747
748#[test]
749fn test_prefix_parsing() {
750    let tempdir = tempfile::TempDir::new().unwrap();
751    let path = tempdir.path();
752    std::fs::write(path.join("test.obj"), b"").unwrap();
753    let str1 = format!("ATOM_NAME:PREFIX:{}", path.join("test.obj").display());
754    let prefix =
755        PrefixMapCompilation::from_input(&[("ATOM_NAME".to_string(), b"".to_vec())], &[str1], true);
756    assert_eq!(
757        prefix.unwrap(),
758        PrefixMapCompilation {
759            input_hashes: BTreeMap::new(),
760            manual_prefixes: vec![("ATOM_NAME".to_string(), "PREFIX".to_string())]
761                .into_iter()
762                .collect(),
763            compilation_objects: Vec::new().into_iter().collect(),
764        }
765    );
766}
767
768#[test]
769fn test_split_prefix() {
770    let split = PrefixMapCompilation::split_prefix(
771        "qjs:abc123:C:\\Users\\felix\\AppData\\Local\\Temp\\.tmpoccCjV\\wasm.obj",
772    );
773    assert_eq!(
774        split,
775        vec![
776            "qjs".to_string(),
777            "abc123".to_string(),
778            "C:\\Users\\felix\\AppData\\Local\\Temp\\.tmpoccCjV\\wasm.obj".to_string(),
779        ]
780    );
781    let split = PrefixMapCompilation::split_prefix("qjs:./tmp.obj");
782    assert_eq!(split, vec!["qjs".to_string(), "./tmp.obj".to_string(),]);
783    let split2 = PrefixMapCompilation::split_prefix(
784        "qjs:abc123:/var/folders/65/2zzy98b16xz254jccxjzqb8w0000gn/T/.tmpNdgVaq/wasm.o",
785    );
786    assert_eq!(
787        split2,
788        vec![
789            "qjs".to_string(),
790            "abc123".to_string(),
791            "/var/folders/65/2zzy98b16xz254jccxjzqb8w0000gn/T/.tmpNdgVaq/wasm.o".to_string(),
792        ]
793    );
794    let split3 = PrefixMapCompilation::split_prefix(
795        "qjs:/var/folders/65/2zzy98b16xz254jccxjzqb8w0000gn/T/.tmpNdgVaq/wasm.o",
796    );
797    assert_eq!(
798        split3,
799        vec![
800            "qjs".to_string(),
801            "/var/folders/65/2zzy98b16xz254jccxjzqb8w0000gn/T/.tmpNdgVaq/wasm.o".to_string(),
802        ]
803    );
804}
805
806fn compile_atoms(
807    atoms: &[(String, Vec<u8>)],
808    output_dir: &Path,
809    compiler: &RuntimeOptions,
810    target: &Target,
811    prefixes: &PrefixMapCompilation,
812    debug: bool,
813) -> Result<BTreeMap<String, ModuleInfo>, anyhow::Error> {
814    use std::{
815        fs::File,
816        io::{BufWriter, Write},
817    };
818
819    let mut module_infos = BTreeMap::new();
820    for (a, data) in atoms {
821        let prefix = prefixes
822            .get_prefix_for_atom(&normalize_atom_name(a))
823            .ok_or_else(|| anyhow::anyhow!("no prefix given for atom {a}"))?;
824        let atom_name = utils::normalize_atom_name(a);
825        let output_object_path = output_dir.join(format!("{atom_name}.o"));
826        if let Some(atom) = prefixes.get_compilation_object_for_atom(a) {
827            std::fs::write(&output_object_path, atom)
828                .map_err(|e| anyhow::anyhow!("{}: {e}", output_object_path.display()))?;
829            if debug {
830                println!("Using cached object file for atom {a:?}.");
831            }
832            continue;
833        }
834        let engine = compiler.get_sys_compiler_engine_for_target(target.clone())?;
835        let engine_inner = engine.as_sys().inner();
836        let compiler = engine_inner.compiler()?;
837        let features = engine_inner.features();
838        let tunables = engine.tunables();
839        let (module_info, obj, _, _) = Artifact::generate_object(
840            compiler,
841            data,
842            Some(prefix.as_str()),
843            target,
844            tunables,
845            features,
846        )?;
847        module_infos.insert(atom_name, module_info);
848        // Write object file with functions
849        let mut writer = BufWriter::new(File::create(&output_object_path)?);
850        obj.write_stream(&mut writer)
851            .map_err(|err| anyhow::anyhow!(err.to_string()))?;
852        writer.flush()?;
853    }
854
855    Ok(module_infos)
856}
857
858/// Compile the C code.
859fn run_c_compile(
860    env: &WasmerEnv,
861    path_to_c_src: &Path,
862    output_name: &Path,
863    target: &Triple,
864    debug: bool,
865    include_dirs: &[PathBuf],
866) -> anyhow::Result<()> {
867    #[cfg(not(windows))]
868    let c_compiler = "cc";
869    // We must use a C++ compiler on Windows because wasm.h uses `static_assert`
870    // which isn't available in `clang` on Windows.
871    #[cfg(windows)]
872    let c_compiler = "clang++";
873
874    let mut command = Command::new(c_compiler);
875    let mut command = command
876        .arg("-Wall")
877        .arg("-O2")
878        .arg("-c")
879        .arg(path_to_c_src)
880        .arg("-I")
881        .arg(utils::get_wasmer_include_directory(env)?);
882
883    for i in include_dirs {
884        command = command.arg("-I");
885        command = command.arg(normalize_path(&i.display().to_string()));
886    }
887
888    // On some compiler -target isn't implemented
889    if *target != Triple::host() {
890        command = command.arg("-target").arg(format!("{target}"));
891    }
892
893    let command = command.arg("-o").arg(output_name);
894
895    if debug {
896        println!("{command:#?}");
897    }
898
899    let output = command.output()?;
900    if debug {
901        eprintln!(
902            "run_c_compile: stdout: {}\n\nstderr: {}",
903            String::from_utf8_lossy(&output.stdout),
904            String::from_utf8_lossy(&output.stderr)
905        );
906    }
907
908    if !output.status.success() {
909        bail!(
910            "C code compile failed with: stdout: {}\n\nstderr: {}",
911            String::from_utf8_lossy(&output.stdout),
912            String::from_utf8_lossy(&output.stderr)
913        );
914    }
915
916    Ok(())
917}
918
919fn write_volume_obj(
920    volume_bytes: &[u8],
921    object_name: &str,
922    output_path: &Path,
923    target: &Target,
924) -> anyhow::Result<()> {
925    use std::{
926        fs::File,
927        io::{BufWriter, Write},
928    };
929
930    let mut volumes_object = get_object_for_target(target.triple())?;
931    emit_serialized(
932        &mut volumes_object,
933        volume_bytes,
934        target.triple(),
935        object_name,
936    )?;
937
938    let mut writer = BufWriter::new(File::create(output_path)?);
939    volumes_object
940        .write_stream(&mut writer)
941        .map_err(|err| anyhow::anyhow!(err.to_string()))?;
942    writer.flush()?;
943    drop(writer);
944
945    Ok(())
946}
947
948/// Given a .wasm file, compiles the .wasm file into the target directory and creates the entrypoint.json
949#[allow(clippy::too_many_arguments)]
950pub(super) fn prepare_directory_from_single_wasm_file(
951    wasm_file: &Path,
952    target_dir: &Path,
953    compiler: &RuntimeOptions,
954    triple: &Triple,
955    cpu_features: &[CpuFeature],
956    prefix: &[String],
957    debug: bool,
958) -> anyhow::Result<Vec<(String, Vec<u8>)>, anyhow::Error> {
959    let bytes = std::fs::read(wasm_file)?;
960    let target = &utils::target_triple_to_target(triple, cpu_features);
961
962    std::fs::create_dir_all(target_dir)
963        .map_err(|e| anyhow::anyhow!("cannot create / dir in {}: {e}", target_dir.display()))?;
964
965    let target_dir = target_dir.canonicalize()?;
966
967    std::fs::create_dir_all(target_dir.join("atoms")).map_err(|e| {
968        anyhow::anyhow!("cannot create /atoms dir in {}: {e}", target_dir.display())
969    })?;
970
971    let mut atoms_from_file = Vec::new();
972    let mut target_paths = Vec::new();
973
974    let all_files = vec![(
975        wasm_file
976            .file_stem()
977            .and_then(|f| f.to_str())
978            .unwrap_or("main")
979            .to_string(),
980        bytes,
981    )];
982
983    for (atom_name, atom_bytes) in all_files.iter() {
984        atoms_from_file.push((atom_name.clone(), atom_bytes.to_vec()));
985        let atom_path = target_dir.join("atoms").join(format!("{atom_name}.o"));
986        target_paths.push((atom_name, atom_path));
987    }
988
989    let prefix_map = PrefixMapCompilation::from_input(&atoms_from_file, prefix, false)
990        .with_context(|| anyhow::anyhow!("prepare_directory_from_single_wasm_file"))?;
991
992    let module_infos = compile_atoms(
993        &atoms_from_file,
994        &target_dir.join("atoms"),
995        compiler,
996        target,
997        &prefix_map,
998        debug,
999    )?;
1000
1001    let mut atoms = Vec::new();
1002    for (atom_name, atom_path) in target_paths {
1003        atoms.push(CommandEntrypoint {
1004            // TODO: improve, "--command pip" should be able to invoke atom "python" with args "-m pip"
1005            command: atom_name.clone(),
1006            atom: atom_name.clone(),
1007            path: atom_path,
1008            header: None,
1009            module_info: module_infos.get(atom_name).cloned(),
1010        });
1011    }
1012
1013    let entrypoint = Entrypoint {
1014        atoms,
1015        volumes: Vec::new(),
1016    };
1017
1018    write_entrypoint(&target_dir, &entrypoint)?;
1019
1020    Ok(all_files)
1021}
1022
1023// Given the input file paths, correctly resolves the .wasm files,
1024// reads the module info from the wasm module and writes the ModuleInfo for each file
1025// into the entrypoint.json file
1026fn get_module_infos(
1027    engine: &Engine,
1028    directory: &Path,
1029    atoms: &[(String, Vec<u8>)],
1030) -> Result<BTreeMap<String, ModuleInfo>, anyhow::Error> {
1031    let mut entrypoint =
1032        get_entrypoint(directory).with_context(|| anyhow::anyhow!("get module infos"))?;
1033
1034    let mut module_infos = BTreeMap::new();
1035    for (atom_name, atom_bytes) in atoms {
1036        let module = Module::new(engine, atom_bytes.as_slice())?;
1037        let module_info = module.info();
1038        if let Some(s) = entrypoint
1039            .atoms
1040            .iter_mut()
1041            .find(|a| a.atom.as_str() == atom_name.as_str())
1042        {
1043            s.module_info = Some(module_info.clone());
1044            module_infos.insert(atom_name.clone(), module_info.clone());
1045        }
1046    }
1047
1048    write_entrypoint(directory, &entrypoint)?;
1049
1050    Ok(module_infos)
1051}
1052
1053/// Create the static_defs.h header files in the /include directory
1054pub(crate) fn create_header_files_in_dir(
1055    directory: &Path,
1056    entrypoint: &mut Entrypoint,
1057    atoms: &[(String, Vec<u8>)],
1058    prefixes: &[String],
1059    binary_fmt: &BinaryFormat,
1060) -> anyhow::Result<()> {
1061    use object::{Object, ObjectSymbol};
1062
1063    std::fs::create_dir_all(directory.join("include")).map_err(|e| {
1064        anyhow::anyhow!("cannot create /include dir in {}: {e}", directory.display())
1065    })?;
1066
1067    let prefixes = PrefixMapCompilation::from_input(atoms, prefixes, false)
1068        .with_context(|| anyhow::anyhow!("create_header_files_in_dir"))?;
1069
1070    for atom in entrypoint.atoms.iter_mut() {
1071        let atom_name = &atom.atom;
1072        let prefix = prefixes
1073            .get_prefix_for_atom(atom_name)
1074            .ok_or_else(|| anyhow::anyhow!("cannot get prefix for atom {atom_name}"))?;
1075        let symbol_registry = ModuleMetadataSymbolRegistry {
1076            prefix: prefix.clone(),
1077        };
1078
1079        let object_file_src = directory.join(&atom.path);
1080        let object_file = std::fs::read(&object_file_src)
1081            .map_err(|e| anyhow::anyhow!("could not read {}: {e}", object_file_src.display()))?;
1082        let obj_file = object::File::parse(&*object_file)?;
1083        let mut symbol_name = symbol_registry.symbol_to_name(Symbol::Metadata);
1084        if matches!(binary_fmt, BinaryFormat::Macho) {
1085            symbol_name = format!("_{symbol_name}");
1086        }
1087
1088        let mut metadata_length = obj_file.symbol_by_name(&symbol_name).unwrap().size() as usize;
1089        if metadata_length == 0 {
1090            let metadata_obj = obj_file.symbol_by_name(&symbol_name).unwrap();
1091            let sec = obj_file
1092                .section_by_index(metadata_obj.section().index().unwrap())
1093                .unwrap();
1094            let mut syms_in_data_sec = obj_file
1095                .symbols()
1096                .filter(|v| v.section_index().is_some_and(|i| i == sec.index()))
1097                .collect::<Vec<_>>();
1098
1099            syms_in_data_sec.sort_by_key(|v| v.address());
1100
1101            let metadata_obj_idx = syms_in_data_sec
1102                .iter()
1103                .position(|v| v.name().is_ok_and(|v| v == symbol_name))
1104                .unwrap();
1105
1106            metadata_length = if metadata_obj_idx == syms_in_data_sec.len() - 1 {
1107                (sec.address() + sec.size()) - syms_in_data_sec[metadata_obj_idx].address()
1108            } else {
1109                syms_in_data_sec[metadata_obj_idx + 1].address()
1110                    - syms_in_data_sec[metadata_obj_idx].address()
1111            } as usize;
1112        }
1113
1114        let module_info = atom
1115            .module_info
1116            .as_ref()
1117            .ok_or_else(|| anyhow::anyhow!("no module info for atom {atom_name:?}"))?;
1118
1119        let base_path = Path::new("include").join(format!("static_defs_{prefix}.h"));
1120        let header_file_path = directory.join(&base_path);
1121
1122        let header_file_src = crate::c_gen::staticlib_header::generate_header_file(
1123            &prefix,
1124            module_info,
1125            &symbol_registry,
1126            metadata_length,
1127        );
1128
1129        std::fs::write(&header_file_path, &header_file_src).map_err(|e| {
1130            anyhow::anyhow!(
1131                "could not write static_defs.h for atom {atom_name} in generate-header step: {e}"
1132            )
1133        })?;
1134
1135        atom.header = Some(base_path);
1136    }
1137
1138    write_entrypoint(directory, entrypoint)?;
1139
1140    Ok(())
1141}
1142
1143/// Given a directory, links all the objects from the directory appropriately
1144#[allow(clippy::too_many_arguments)]
1145fn link_exe_from_dir(
1146    env: &WasmerEnv,
1147    directory: &Path,
1148    output_path: PathBuf,
1149    cross_compilation: &CrossCompileSetup,
1150    additional_libraries: &[String],
1151    debug: bool,
1152    atoms: &[(String, Vec<u8>)],
1153    prefixes: &[String],
1154) -> anyhow::Result<()> {
1155    let entrypoint =
1156        get_entrypoint(directory).with_context(|| anyhow::anyhow!("link exe from dir"))?;
1157
1158    let prefixes = PrefixMapCompilation::from_input(atoms, prefixes, false)
1159        .with_context(|| anyhow::anyhow!("link_exe_from_dir"))?;
1160
1161    let wasmer_main_c = generate_wasmer_main_c(&entrypoint, &prefixes).map_err(|e| {
1162        anyhow::anyhow!(
1163            "could not generate wasmer_main.c in dir {}: {e}",
1164            directory.display()
1165        )
1166    })?;
1167
1168    std::fs::write(directory.join("wasmer_main.c"), wasmer_main_c.as_bytes()).map_err(|e| {
1169        anyhow::anyhow!(
1170            "could not write wasmer_main.c in dir {}: {e}",
1171            directory.display()
1172        )
1173    })?;
1174
1175    let library_path = &cross_compilation.library;
1176
1177    let mut object_paths = entrypoint
1178        .atoms
1179        .iter()
1180        .filter_map(|a| directory.join(&a.path).canonicalize().ok())
1181        .collect::<Vec<_>>();
1182
1183    object_paths.extend(
1184        entrypoint
1185            .volumes
1186            .iter()
1187            .filter_map(|v| directory.join(&v.obj_file).canonicalize().ok()),
1188    );
1189
1190    let zig_triple = utils::triple_to_zig_triple(&cross_compilation.target);
1191    let include_dirs = entrypoint
1192        .atoms
1193        .iter()
1194        .filter_map(|a| {
1195            Some(
1196                directory
1197                    .join(a.header.as_deref()?)
1198                    .canonicalize()
1199                    .ok()?
1200                    .parent()?
1201                    .to_path_buf(),
1202            )
1203        })
1204        .collect::<Vec<_>>();
1205
1206    let mut include_dirs = include_dirs;
1207    include_dirs.sort();
1208    include_dirs.dedup();
1209
1210    // On Windows, cross-compilation to Windows itself with zig does not work due
1211    // to libunwind and libstdc++ not compiling, so we fake this special case of cross-compilation
1212    // by falling back to the system compilation + system linker
1213    if cross_compilation.target == Triple::host()
1214        && cross_compilation.target.operating_system == OperatingSystem::Windows
1215    {
1216        run_c_compile(
1217            env,
1218            &directory.join("wasmer_main.c"),
1219            &directory.join("wasmer_main.o"),
1220            &cross_compilation.target,
1221            debug,
1222            &include_dirs,
1223        )
1224        .map_err(|e| {
1225            anyhow::anyhow!(
1226                "could not run c compile of wasmer_main.c in dir {}: {e}",
1227                directory.display()
1228            )
1229        })?;
1230    }
1231
1232    // compilation done, now link
1233    if cross_compilation.zig_binary_path.is_none()
1234        || (cross_compilation.target == Triple::host()
1235            && cross_compilation.target.operating_system == OperatingSystem::Windows)
1236    {
1237        #[cfg(not(windows))]
1238        let linker = "cc";
1239        #[cfg(windows)]
1240        let linker = "clang";
1241        let optimization_flag = "-O2";
1242
1243        let object_path = match directory.join("wasmer_main.o").canonicalize() {
1244            Ok(s) => s,
1245            Err(_) => directory.join("wasmer_main.c"),
1246        };
1247
1248        object_paths.push(object_path);
1249
1250        return link_objects_system_linker(
1251            library_path,
1252            linker,
1253            optimization_flag,
1254            &include_dirs,
1255            &object_paths,
1256            &cross_compilation.target,
1257            additional_libraries,
1258            &output_path,
1259            debug,
1260        );
1261    }
1262
1263    let zig_binary_path = cross_compilation
1264        .zig_binary_path
1265        .as_ref()
1266        .ok_or_else(|| anyhow::anyhow!("could not find zig in $PATH {}", directory.display()))?;
1267
1268    let mut cmd = Command::new(zig_binary_path);
1269    cmd.arg("build-exe");
1270    cmd.arg("--verbose-cc");
1271    cmd.arg("--verbose-link");
1272    cmd.arg("-target");
1273    cmd.arg(&zig_triple);
1274
1275    // On Windows, libstdc++ does not compile with zig due to zig-internal problems,
1276    // so we link with only -lc
1277    #[cfg(not(target_os = "windows"))]
1278    if zig_triple.contains("windows") {
1279        cmd.arg("-lc++");
1280    } else {
1281        cmd.arg("-lc");
1282    }
1283
1284    #[cfg(target_os = "windows")]
1285    cmd.arg("-lc");
1286
1287    for include_dir in include_dirs {
1288        cmd.arg("-I");
1289        cmd.arg(normalize_path(&format!("{}", include_dir.display())));
1290    }
1291
1292    let mut include_path = library_path.clone();
1293    include_path.pop();
1294    include_path.pop();
1295    include_path.push("include");
1296    if !include_path.exists() {
1297        // Can happen when we got the wrong library_path
1298        return Err(anyhow::anyhow!(
1299            "Wasmer include path {} does not exist, maybe library path {} is wrong (expected /lib/libwasmer.a)?",
1300            include_path.display(),
1301            library_path.display()
1302        ));
1303    }
1304    cmd.arg("-I");
1305    cmd.arg(normalize_path(&format!("{}", include_path.display())));
1306
1307    // On Windows, libunwind does not compile, so we have
1308    // to create binaries without libunwind.
1309    #[cfg(not(target_os = "windows"))]
1310    cmd.arg("-lunwind");
1311
1312    #[cfg(target_os = "windows")]
1313    if !zig_triple.contains("windows") {
1314        cmd.arg("-lunwind");
1315    }
1316
1317    cmd.arg("-OReleaseSafe");
1318    cmd.arg("-fno-compiler-rt");
1319    cmd.arg("-fno-lto");
1320    #[cfg(target_os = "windows")]
1321    let out_path = directory.join("wasmer_main.exe");
1322    #[cfg(not(target_os = "windows"))]
1323    let out_path = directory.join("wasmer_main");
1324    cmd.arg(format!("-femit-bin={}", out_path.display()));
1325
1326    cmd.args(
1327        object_paths
1328            .iter()
1329            .map(|o| normalize_path(&format!("{}", o.display())))
1330            .collect::<Vec<_>>(),
1331    );
1332    cmd.arg(normalize_path(&format!("{}", library_path.display())));
1333    cmd.arg(normalize_path(&format!(
1334        "{}",
1335        directory
1336            .join("wasmer_main.c")
1337            .canonicalize()
1338            .expect("could not find wasmer_main.c / wasmer_main.o")
1339            .display()
1340    )));
1341
1342    if zig_triple.contains("windows") {
1343        let mut winsdk_path = library_path.clone();
1344        winsdk_path.pop();
1345        winsdk_path.pop();
1346        winsdk_path.push("winsdk");
1347
1348        let files_winsdk = std::fs::read_dir(winsdk_path)
1349            .ok()
1350            .map(|res| {
1351                res.filter_map(|r| Some(normalize_path(&format!("{}", r.ok()?.path().display()))))
1352                    .collect::<Vec<_>>()
1353            })
1354            .unwrap_or_default();
1355
1356        cmd.args(files_winsdk);
1357    }
1358
1359    if zig_triple.contains("macos") {
1360        // need to link with Security framework when using zig, but zig
1361        // doesn't include it, so we need to bring a copy of the dtb file
1362        // which is basicaly the collection of exported symbol for the libs
1363        let framework = include_bytes!("security_framework.tgz").to_vec();
1364        // extract files
1365        let tar = flate2::read::GzDecoder::new(framework.as_slice());
1366        let mut archive = Archive::new(tar);
1367        // directory is a temp folder
1368        archive.unpack(directory)?;
1369        // add the framework to the link command
1370        cmd.arg("-framework");
1371        cmd.arg("Security");
1372        cmd.arg(format!("-F{}", directory.display()));
1373    }
1374
1375    if debug {
1376        println!("running cmd: {cmd:?}");
1377        cmd.stdout(Stdio::inherit());
1378        cmd.stderr(Stdio::inherit());
1379    }
1380
1381    let compilation = cmd
1382        .output()
1383        .context(anyhow!("Could not execute `zig`: {cmd:?}"))?;
1384
1385    if !compilation.status.success() {
1386        return Err(anyhow::anyhow!(
1387            String::from_utf8_lossy(&compilation.stderr).to_string()
1388        ));
1389    }
1390
1391    // remove file if it exists - if not done, can lead to errors on copy
1392    let output_path_normalized = normalize_path(&format!("{}", output_path.display()));
1393    let _ = std::fs::remove_file(output_path_normalized);
1394    std::fs::copy(
1395        normalize_path(&format!("{}", out_path.display())),
1396        normalize_path(&format!("{}", output_path.display())),
1397    )
1398    .map_err(|e| {
1399        anyhow::anyhow!(
1400            "could not copy from {} to {}: {e}",
1401            normalize_path(&format!("{}", out_path.display())),
1402            normalize_path(&format!("{}", output_path.display()))
1403        )
1404    })?;
1405
1406    Ok(())
1407}
1408
1409/// Link compiled objects using the system linker
1410#[allow(clippy::too_many_arguments)]
1411fn link_objects_system_linker(
1412    libwasmer_path: &Path,
1413    linker_cmd: &str,
1414    optimization_flag: &str,
1415    include_dirs: &[PathBuf],
1416    object_paths: &[PathBuf],
1417    target: &Triple,
1418    additional_libraries: &[String],
1419    output_path: &Path,
1420    debug: bool,
1421) -> Result<(), anyhow::Error> {
1422    let libwasmer_path = libwasmer_path
1423        .canonicalize()
1424        .context("Failed to find libwasmer")?;
1425    let mut command = Command::new(linker_cmd);
1426    let mut command = command
1427        .arg("-Wall")
1428        .arg(optimization_flag)
1429        .args(object_paths.iter().map(|path| path.canonicalize().unwrap()))
1430        .arg(&libwasmer_path);
1431
1432    if *target != Triple::host() {
1433        command = command.arg("-target");
1434        command = command.arg(format!("{target}"));
1435    }
1436
1437    for include_dir in include_dirs {
1438        command = command.arg("-I");
1439        command = command.arg(normalize_path(&format!("{}", include_dir.display())));
1440    }
1441    let mut include_path = libwasmer_path.clone();
1442    include_path.pop();
1443    include_path.pop();
1444    include_path.push("include");
1445    if !include_path.exists() {
1446        // Can happen when we got the wrong library_path
1447        return Err(anyhow::anyhow!(
1448            "Wasmer include path {} does not exist, maybe library path {} is wrong (expected /lib/libwasmer.a)?",
1449            include_path.display(),
1450            libwasmer_path.display()
1451        ));
1452    }
1453    command = command.arg("-I");
1454    command = command.arg(normalize_path(&format!("{}", include_path.display())));
1455
1456    // Add libraries required per platform.
1457    // We need userenv, sockets (Ws2_32), advapi32 for some system calls and bcrypt for random numbers.
1458    let mut additional_libraries = additional_libraries.to_vec();
1459    if target.operating_system == OperatingSystem::Windows {
1460        additional_libraries.extend(LINK_SYSTEM_LIBRARIES_WINDOWS.iter().map(|s| s.to_string()));
1461    } else {
1462        additional_libraries.extend(LINK_SYSTEM_LIBRARIES_UNIX.iter().map(|s| s.to_string()));
1463    }
1464    let link_against_extra_libs = additional_libraries.iter().map(|lib| format!("-l{lib}"));
1465    let command = command.args(link_against_extra_libs);
1466    let command = command.arg("-o").arg(output_path);
1467    if debug {
1468        println!("{command:#?}");
1469    }
1470    let output = command.output()?;
1471
1472    if !output.status.success() {
1473        bail!(
1474            "linking failed with command line:{:#?} stdout: {}\n\nstderr: {}",
1475            command,
1476            String::from_utf8_lossy(&output.stdout),
1477            String::from_utf8_lossy(&output.stderr),
1478        );
1479    }
1480    Ok(())
1481}
1482
1483/// Generate the wasmer_main.c that links all object files together
1484/// (depending on the object format / atoms number)
1485fn generate_wasmer_main_c(
1486    entrypoint: &Entrypoint,
1487    prefixes: &PrefixMapCompilation,
1488) -> Result<String, anyhow::Error> {
1489    use std::fmt::Write;
1490
1491    const WASMER_MAIN_C_SOURCE: &str = include_str!("wasmer_create_exe_main.c");
1492
1493    // always with compile zig + static_defs.h
1494    let atom_names = entrypoint
1495        .atoms
1496        .iter()
1497        .map(|a| &a.command)
1498        .collect::<Vec<_>>();
1499
1500    let c_code_to_add = String::new();
1501    let mut c_code_to_instantiate = String::new();
1502    let mut deallocate_module = String::new();
1503    let mut extra_headers = Vec::new();
1504
1505    for a in atom_names.iter() {
1506        let prefix = prefixes
1507            .get_prefix_for_atom(&utils::normalize_atom_name(a))
1508            .ok_or_else(|| {
1509                let formatted_prefixes = format!("{prefixes:#?}");
1510                anyhow::anyhow!(
1511                    "cannot find prefix for atom {a} when generating wasmer_main.c ({formatted_prefixes})"
1512                )
1513            })?;
1514        let atom_name = prefix.clone();
1515
1516        extra_headers.push(format!("#include \"static_defs_{atom_name}.h\""));
1517
1518        write!(c_code_to_instantiate, "
1519        wasm_module_t *atom_{atom_name} = wasmer_object_module_new_{atom_name}(store, \"{atom_name}\");
1520        if (!atom_{atom_name}) {{
1521            fprintf(stderr, \"Failed to create module from atom \\\"{a}\\\"\\n\");
1522            print_wasmer_error();
1523            return -1;
1524        }}
1525        ")?;
1526
1527        write!(deallocate_module, "wasm_module_delete(atom_{atom_name});")?;
1528    }
1529
1530    let volumes_str = entrypoint
1531        .volumes
1532        .iter()
1533        .map(|v| utils::normalize_atom_name(&v.name).to_uppercase())
1534        .map(|uppercase| {
1535            format!(
1536                "extern size_t {uppercase}_LENGTH asm(\"{uppercase}_LENGTH\");\r\nextern char {uppercase}_DATA asm(\"{uppercase}_DATA\");"
1537            )
1538        })
1539        .collect::<Vec<_>>();
1540
1541    let base_str = WASMER_MAIN_C_SOURCE;
1542    let volumes_str = volumes_str.join("\r\n");
1543    let return_str = base_str
1544        .replace(
1545            "#define WASI",
1546            if !volumes_str.trim().is_empty() {
1547                "#define WASI\r\n#define WASI_PIRITA"
1548            } else {
1549                "#define WASI"
1550            },
1551        )
1552        .replace("// DECLARE_MODULES", &c_code_to_add)
1553        .replace("// DECLARE_VOLUMES", &volumes_str)
1554        .replace(
1555            "// SET_NUMBER_OF_COMMANDS",
1556            &format!("number_of_commands = {};", atom_names.len()),
1557        )
1558        .replace("// EXTRA_HEADERS", &extra_headers.join("\r\n"))
1559        .replace("wasm_module_delete(module);", &deallocate_module);
1560
1561    if atom_names.len() == 1 {
1562        let prefix = prefixes
1563            .get_prefix_for_atom(&utils::normalize_atom_name(atom_names[0]))
1564            .ok_or_else(|| {
1565                let formatted_prefixes = format!("{prefixes:#?}");
1566                anyhow::anyhow!(
1567                    "cannot find prefix for atom {} when generating wasmer_main.c ({formatted_prefixes})",
1568                    &atom_names[0]
1569                )
1570            })?;
1571        write!(c_code_to_instantiate, "module = atom_{prefix};")?;
1572    } else {
1573        for a in atom_names.iter() {
1574            let prefix = prefixes
1575                .get_prefix_for_atom(&utils::normalize_atom_name(a))
1576                .ok_or_else(|| {
1577                    let formatted_prefixes = format!("{prefixes:#?}");
1578                    anyhow::anyhow!(
1579                        "cannot find prefix for atom {a} when generating wasmer_main.c ({formatted_prefixes})"
1580                    )
1581                })?;
1582            writeln!(
1583                c_code_to_instantiate,
1584                "if (strcmp(selected_atom, \"{a}\") == 0) {{ module = atom_{prefix}; }}",
1585            )?;
1586        }
1587    }
1588
1589    write!(
1590        c_code_to_instantiate,
1591        "
1592    if (!module) {{
1593        fprintf(stderr, \"No --command given, available commands are:\\n\");
1594        fprintf(stderr, \"\\n\");
1595        {commands}
1596        fprintf(stderr, \"\\n\");
1597        return -1;
1598    }}
1599    ",
1600        commands = atom_names
1601            .iter()
1602            .map(|a| format!("fprintf(stderr, \"    {a}\\n\");"))
1603            .collect::<Vec<_>>()
1604            .join("\n")
1605    )?;
1606
1607    Ok(return_str.replace("// INSTANTIATE_MODULES", &c_code_to_instantiate))
1608}
1609
1610#[allow(dead_code)]
1611pub(super) mod utils {
1612
1613    use std::{
1614        ffi::OsStr,
1615        path::{Path, PathBuf},
1616    };
1617
1618    use anyhow::{Context, anyhow};
1619    use target_lexicon::{Architecture, Environment, OperatingSystem, Triple};
1620    use wasmer_types::target::{CpuFeature, Target};
1621
1622    use crate::config::WasmerEnv;
1623
1624    use super::{CrossCompile, CrossCompileSetup, UrlOrVersion};
1625
1626    pub(in crate::commands) fn target_triple_to_target(
1627        target_triple: &Triple,
1628        cpu_features: &[CpuFeature],
1629    ) -> Target {
1630        let mut features = cpu_features.iter().fold(CpuFeature::set(), |a, b| a | *b);
1631        // Cranelift requires SSE2, so we have this "hack" for now to facilitate
1632        // usage
1633        if target_triple.architecture == Architecture::X86_64 {
1634            features |= CpuFeature::SSE2;
1635        }
1636        Target::new(target_triple.clone(), features)
1637    }
1638
1639    pub(in crate::commands) fn get_cross_compile_setup(
1640        env: &WasmerEnv,
1641        cross_subc: &mut CrossCompile,
1642        target_triple: &Triple,
1643        starting_cd: &Path,
1644        specific_release: Option<UrlOrVersion>,
1645    ) -> Result<CrossCompileSetup, anyhow::Error> {
1646        let target = target_triple;
1647
1648        if let Some(tarball_path) = cross_subc.tarball.as_mut()
1649            && tarball_path.is_relative()
1650        {
1651            *tarball_path = starting_cd.join(&tarball_path);
1652            if !tarball_path.exists() {
1653                return Err(anyhow!(
1654                    "Tarball path `{}` does not exist.",
1655                    tarball_path.display()
1656                ));
1657            } else if tarball_path.is_dir() {
1658                return Err(anyhow!(
1659                    "Tarball path `{}` is a directory.",
1660                    tarball_path.display()
1661                ));
1662            }
1663        }
1664
1665        let zig_binary_path = if !cross_subc.use_system_linker {
1666            find_zig_binary(cross_subc.zig_binary_path.as_ref().and_then(|p| {
1667                if p.is_absolute() {
1668                    p.canonicalize().ok()
1669                } else {
1670                    starting_cd.join(p).canonicalize().ok()
1671                }
1672            }))
1673            .ok()
1674        } else {
1675            None
1676        };
1677
1678        let library = if let Some(v) = cross_subc.library_path.clone() {
1679            Some(v.canonicalize().unwrap_or(v))
1680        } else if let Some(local_tarball) = cross_subc.tarball.as_ref() {
1681            let (filename, tarball_dir) = find_filename(local_tarball, target)?;
1682            Some(tarball_dir.join(filename))
1683        } else {
1684            let wasmer_cache_dir =
1685                if *target_triple == Triple::host() && std::env::var("WASMER_DIR").is_ok() {
1686                    Some(env.cache_dir().to_path_buf())
1687                } else {
1688                    get_libwasmer_cache_path(env).ok()
1689                };
1690
1691            // check if the tarball for the target already exists locally
1692            let local_tarball = wasmer_cache_dir.as_ref().and_then(|wc| {
1693                let wasmer_cache = std::fs::read_dir(wc).ok()?;
1694                wasmer_cache
1695                    .filter_map(|e| e.ok())
1696                    .filter_map(|e| {
1697                        let path = format!("{}", e.path().display());
1698                        if path.ends_with(".tar.gz") {
1699                            Some(e.path())
1700                        } else {
1701                            None
1702                        }
1703                    })
1704                    .find(|p| crate::commands::utils::filter_tarball(p, target))
1705            });
1706
1707            if let Some(UrlOrVersion::Url(wasmer_release)) = specific_release.as_ref() {
1708                let tarball = super::http_fetch::download_url(env, wasmer_release.as_ref())?;
1709                let (filename, tarball_dir) = find_filename(&tarball, target)?;
1710                Some(tarball_dir.join(filename))
1711            } else if let Some(UrlOrVersion::Version(wasmer_release)) = specific_release.as_ref() {
1712                let release = super::http_fetch::get_release(Some(wasmer_release.clone()))?;
1713                let tarball = super::http_fetch::download_release(env, release, target.clone())?;
1714                let (filename, tarball_dir) = find_filename(&tarball, target)?;
1715                Some(tarball_dir.join(filename))
1716            } else if let Some(local_tarball) = local_tarball.as_ref() {
1717                let (filename, tarball_dir) = find_filename(local_tarball, target)?;
1718                Some(tarball_dir.join(filename))
1719            } else {
1720                let release = super::http_fetch::get_release(None)?;
1721                let tarball = super::http_fetch::download_release(env, release, target.clone())?;
1722                let (filename, tarball_dir) = find_filename(&tarball, target)?;
1723                Some(tarball_dir.join(filename))
1724            }
1725        };
1726
1727        let library = library.ok_or_else(|| anyhow!("libwasmer.a / wasmer.lib not found"))?;
1728
1729        let ccs = CrossCompileSetup {
1730            target: target.clone(),
1731            zig_binary_path,
1732            library,
1733        };
1734        Ok(ccs)
1735    }
1736
1737    pub(super) fn filter_tarball(p: &Path, target: &Triple) -> bool {
1738        filter_tarball_internal(p, target).unwrap_or(false)
1739    }
1740
1741    fn filter_tarball_internal(p: &Path, target: &Triple) -> Option<bool> {
1742        // [todo]: Move this description to a better suited place.
1743        //
1744        // The filename scheme:
1745        // FILENAME := "wasmer-" [ FEATURE ] OS  PLATFORM  .
1746        // FEATURE  := "v8-" .
1747        // OS       := "darwin" | "linux" | "linux-musl" | "windows" .
1748        // PLATFORM := "aarch64" | "amd64" | "gnu64" .
1749        //
1750        // In this function we want to select only those version where features don't appear.
1751
1752        let filename = p.file_name()?.to_str()?;
1753
1754        if !filename.ends_with(".tar.gz") {
1755            return None;
1756        }
1757
1758        if filename.contains("v8") {
1759            return None;
1760        }
1761
1762        if target.environment == Environment::Musl && !filename.contains("musl")
1763            || filename.contains("musl") && target.environment != Environment::Musl
1764        {
1765            return None;
1766        }
1767
1768        if let Architecture::Aarch64(_) = target.architecture
1769            && !(filename.contains("aarch64") || filename.contains("arm64"))
1770        {
1771            return None;
1772        }
1773
1774        if let Architecture::X86_64 = target.architecture {
1775            if target.operating_system == OperatingSystem::Windows {
1776                if !filename.contains("gnu64") {
1777                    return None;
1778                }
1779            } else if !(filename.contains("x86_64") || filename.contains("amd64")) {
1780                return None;
1781            }
1782        }
1783
1784        if let OperatingSystem::Windows = target.operating_system
1785            && !filename.contains("windows")
1786        {
1787            return None;
1788        }
1789
1790        if let OperatingSystem::Darwin(_) = target.operating_system
1791            && !(filename.contains("apple") || filename.contains("darwin"))
1792        {
1793            return None;
1794        }
1795
1796        if let OperatingSystem::Linux = target.operating_system
1797            && !filename.contains("linux")
1798        {
1799            return None;
1800        }
1801
1802        Some(true)
1803    }
1804
1805    pub(super) fn find_filename(
1806        local_tarball: &Path,
1807        target: &Triple,
1808    ) -> Result<(PathBuf, PathBuf), anyhow::Error> {
1809        let target_file_path = local_tarball
1810            .parent()
1811            .and_then(|parent| Some(parent.join(local_tarball.file_stem()?)))
1812            .unwrap_or_else(|| local_tarball.to_path_buf());
1813
1814        let target_file_path = target_file_path
1815            .parent()
1816            .and_then(|parent| Some(parent.join(target_file_path.file_stem()?)))
1817            .unwrap_or_else(|| target_file_path.clone());
1818
1819        std::fs::create_dir_all(&target_file_path)
1820            .map_err(|e| anyhow!("{e}"))
1821            .with_context(|| anyhow!("{}", target_file_path.display()))?;
1822        let files =
1823            super::http_fetch::untar(local_tarball, &target_file_path).with_context(|| {
1824                anyhow!(
1825                    "{} -> {}",
1826                    local_tarball.display(),
1827                    target_file_path.display()
1828                )
1829            })?;
1830        let tarball_dir = target_file_path.canonicalize().unwrap_or(target_file_path);
1831        let file = find_libwasmer_in_files(target, &files)?;
1832        Ok((file, tarball_dir))
1833    }
1834
1835    fn find_libwasmer_in_files(
1836        target: &Triple,
1837        files: &[PathBuf],
1838    ) -> Result<PathBuf, anyhow::Error> {
1839        let target_files = &[
1840            OsStr::new("libwasmer-headless.a"),
1841            OsStr::new("wasmer-headless.lib"),
1842            OsStr::new("libwasmer.a"),
1843            OsStr::new("wasmer.lib"),
1844        ];
1845        target_files
1846        .iter()
1847        .find_map(|q| {
1848            files.iter().find(|f| f.file_name() == Some(*q))
1849        })
1850        .cloned()
1851        .ok_or_else(|| {
1852            anyhow!(
1853                "Could not find libwasmer.a for {target} target in the provided tarball path (files = {files:#?})"
1854            )
1855        })
1856    }
1857
1858    pub(super) fn normalize_atom_name(s: &str) -> String {
1859        s.chars()
1860            .filter_map(|c| {
1861                if char::is_alphabetic(c) {
1862                    Some(c)
1863                } else if c == '-' || c == '_' {
1864                    Some('_')
1865                } else {
1866                    None
1867                }
1868            })
1869            .collect()
1870    }
1871
1872    pub(super) fn triple_to_zig_triple(target_triple: &Triple) -> String {
1873        let arch = match target_triple.architecture {
1874            Architecture::X86_64 => "x86_64".into(),
1875            Architecture::Aarch64(wasmer_types::target::Aarch64Architecture::Aarch64) => {
1876                "aarch64".into()
1877            }
1878            v => v.to_string(),
1879        };
1880        let os = match target_triple.operating_system {
1881            OperatingSystem::Linux => "linux".into(),
1882            OperatingSystem::Darwin(_) => "macos".into(),
1883            OperatingSystem::Windows => "windows".into(),
1884            v => v.to_string(),
1885        };
1886        let env = match target_triple.environment {
1887            Environment::Musl => "musl",
1888            Environment::Gnu => "gnu",
1889            Environment::Msvc => "msvc",
1890            _ => "none",
1891        };
1892        format!("{arch}-{os}-{env}")
1893    }
1894
1895    pub(super) fn get_wasmer_include_directory(env: &WasmerEnv) -> anyhow::Result<PathBuf> {
1896        let mut path = env.dir().to_path_buf();
1897        if path.clone().join("wasmer.h").exists() {
1898            return Ok(path);
1899        }
1900        path.push("include");
1901        if !path.clone().join("wasmer.h").exists() {
1902            if !path.exists() {
1903                return Err(anyhow!("WASMER_DIR path {} does not exist", path.display()));
1904            }
1905            println!(
1906                "wasmer.h does not exist in {}, will probably default to the system path",
1907                path.canonicalize().unwrap().display()
1908            );
1909        }
1910        Ok(path)
1911    }
1912
1913    /// path to the static libwasmer
1914    pub(super) fn get_libwasmer_path(env: &WasmerEnv) -> anyhow::Result<PathBuf> {
1915        let path = env.dir().to_path_buf();
1916
1917        // TODO: prefer headless Wasmer if/when it's a separate library.
1918        #[cfg(not(windows))]
1919        let libwasmer_static_name = "libwasmer.a";
1920        #[cfg(windows)]
1921        let libwasmer_static_name = "libwasmer.lib";
1922
1923        if path.exists() && path.join(libwasmer_static_name).exists() {
1924            Ok(path.join(libwasmer_static_name))
1925        } else {
1926            Ok(path.join("lib").join(libwasmer_static_name))
1927        }
1928    }
1929
1930    /// path to library tarball cache dir
1931    pub(super) fn get_libwasmer_cache_path(env: &WasmerEnv) -> anyhow::Result<PathBuf> {
1932        let mut path = env.dir().to_path_buf();
1933        path.push("cache");
1934        std::fs::create_dir_all(&path)?;
1935        Ok(path)
1936    }
1937
1938    pub(super) fn get_zig_exe_str() -> &'static str {
1939        #[cfg(target_os = "windows")]
1940        {
1941            "zig.exe"
1942        }
1943        #[cfg(not(target_os = "windows"))]
1944        {
1945            "zig"
1946        }
1947    }
1948
1949    pub(super) fn find_zig_binary(path: Option<PathBuf>) -> Result<PathBuf, anyhow::Error> {
1950        use std::env::split_paths;
1951        #[cfg(not(unix))]
1952        use std::ffi::OsStr;
1953        #[cfg(unix)]
1954        use std::ffi::OsString;
1955        #[cfg(unix)]
1956        use std::os::unix::ffi::OsStringExt;
1957        let path_var = std::env::var("PATH").unwrap_or_default();
1958        #[cfg(unix)]
1959        let system_path_var = std::process::Command::new("getconf")
1960            .args(["PATH"])
1961            .output()
1962            .map(|output| OsString::from_vec(output.stdout))
1963            .unwrap_or_default();
1964        let retval = if let Some(p) = path {
1965            if p.exists() {
1966                p
1967            } else {
1968                return Err(anyhow!("Could not find `zig` binary in {}.", p.display()));
1969            }
1970        } else {
1971            let mut retval = None;
1972            #[cfg(unix)]
1973            let combined_paths =
1974                split_paths(&path_var).chain(split_paths(system_path_var.as_os_str()));
1975            #[cfg(not(unix))]
1976            let combined_paths = split_paths(&path_var).chain(split_paths(OsStr::new("")));
1977            for mut p in combined_paths {
1978                p.push(get_zig_exe_str());
1979                if p.exists() {
1980                    retval = Some(p);
1981                    break;
1982                }
1983            }
1984            retval.ok_or_else(|| anyhow!("Could not find `zig` binary in PATH."))?
1985        };
1986
1987        let version = std::process::Command::new(&retval)
1988            .arg("version")
1989            .output()
1990            .with_context(|| {
1991                format!(
1992                    "Could not execute `zig` binary at path `{}`",
1993                    retval.display()
1994                )
1995            })?
1996            .stdout;
1997        let version_slice = if let Some(pos) = version
1998            .iter()
1999            .position(|c| !(c.is_ascii_digit() || (*c == b'.')))
2000        {
2001            &version[..pos]
2002        } else {
2003            &version[..]
2004        };
2005
2006        let version_slice = String::from_utf8_lossy(version_slice);
2007        let version_semver = semver::Version::parse(&version_slice)
2008            .map_err(|e| anyhow!("could not parse zig version: {version_slice}: {e}"))?;
2009
2010        if version_semver < semver::Version::parse("0.10.0").unwrap() {
2011            Err(anyhow!(
2012                "`zig` binary in PATH (`{}`) is not a new enough version (`{version_slice}`): please use version `0.10.0` or newer.",
2013                retval.display()
2014            ))
2015        } else {
2016            Ok(retval)
2017        }
2018    }
2019
2020    #[test]
2021    fn test_filter_tarball() {
2022        use std::str::FromStr;
2023        let test_paths = [
2024            "/test/wasmer-darwin-arm64.tar.gz",
2025            "/test/wasmer-linux-aarch64.tar.gz",
2026            "/test/wasmer-linux-amd64.tar.gz",
2027            "/test/wasmer-linux-musl-amd64.tar.gz",
2028            "/test/wasmer-windows-amd64.tar.gz",
2029            "/test/wasmer-windows-gnu64.tar.gz",
2030            "/test/wasmer-windows.exe",
2031        ];
2032
2033        let paths = test_paths.iter().map(Path::new).collect::<Vec<_>>();
2034        assert_eq!(
2035            paths
2036                .iter()
2037                .filter(|p| crate::commands::utils::filter_tarball(
2038                    p,
2039                    &Triple::from_str("x86_64-windows").unwrap()
2040                ))
2041                .collect::<Vec<_>>(),
2042            vec![&Path::new("/test/wasmer-windows-gnu64.tar.gz")],
2043        );
2044
2045        let paths = test_paths.iter().map(Path::new).collect::<Vec<_>>();
2046        assert_eq!(
2047            paths
2048                .iter()
2049                .filter(|p| crate::commands::utils::filter_tarball(
2050                    p,
2051                    &Triple::from_str("x86_64-windows-gnu").unwrap()
2052                ))
2053                .collect::<Vec<_>>(),
2054            vec![&Path::new("/test/wasmer-windows-gnu64.tar.gz")],
2055        );
2056
2057        let paths = test_paths.iter().map(Path::new).collect::<Vec<_>>();
2058        assert_eq!(
2059            paths
2060                .iter()
2061                .filter(|p| crate::commands::utils::filter_tarball(
2062                    p,
2063                    &Triple::from_str("x86_64-windows-msvc").unwrap()
2064                ))
2065                .collect::<Vec<_>>(),
2066            vec![&Path::new("/test/wasmer-windows-gnu64.tar.gz")],
2067        );
2068
2069        assert_eq!(
2070            paths
2071                .iter()
2072                .filter(|p| crate::commands::utils::filter_tarball(
2073                    p,
2074                    &Triple::from_str("x86_64-unknown-linux-gnu").unwrap()
2075                ))
2076                .collect::<Vec<_>>(),
2077            vec![&Path::new("/test/wasmer-linux-amd64.tar.gz")],
2078        );
2079
2080        assert_eq!(
2081            paths
2082                .iter()
2083                .filter(|p| crate::commands::utils::filter_tarball(
2084                    p,
2085                    &Triple::from_str("x86_64-linux-gnu").unwrap()
2086                ))
2087                .collect::<Vec<_>>(),
2088            vec![&Path::new("/test/wasmer-linux-amd64.tar.gz")],
2089        );
2090
2091        assert_eq!(
2092            paths
2093                .iter()
2094                .filter(|p| crate::commands::utils::filter_tarball(
2095                    p,
2096                    &Triple::from_str("aarch64-linux-gnu").unwrap()
2097                ))
2098                .collect::<Vec<_>>(),
2099            vec![&Path::new("/test/wasmer-linux-aarch64.tar.gz")],
2100        );
2101
2102        assert_eq!(
2103            paths
2104                .iter()
2105                .filter(|p| crate::commands::utils::filter_tarball(
2106                    p,
2107                    &Triple::from_str("x86_64-windows-gnu").unwrap()
2108                ))
2109                .collect::<Vec<_>>(),
2110            vec![&Path::new("/test/wasmer-windows-gnu64.tar.gz")],
2111        );
2112
2113        assert_eq!(
2114            paths
2115                .iter()
2116                .filter(|p| crate::commands::utils::filter_tarball(
2117                    p,
2118                    &Triple::from_str("aarch64-darwin").unwrap()
2119                ))
2120                .collect::<Vec<_>>(),
2121            vec![&Path::new("/test/wasmer-darwin-arm64.tar.gz")],
2122        );
2123    }
2124
2125    #[test]
2126    fn test_normalize_atom_name() {
2127        assert_eq!(
2128            normalize_atom_name("atom-name-with-dash"),
2129            "atom_name_with_dash".to_string()
2130        );
2131    }
2132}
2133
2134mod http_fetch {
2135    use std::path::Path;
2136
2137    use anyhow::{Context, Result, anyhow};
2138
2139    pub(super) fn get_release(
2140        release_version: Option<semver::Version>,
2141    ) -> Result<serde_json::Value> {
2142        let uri = "https://api.github.com/repos/wasmerio/wasmer/releases";
2143
2144        // Increases rate-limiting in GitHub CI
2145        let auth = std::env::var("GITHUB_TOKEN");
2146
2147        let client = reqwest::blocking::Client::new();
2148        let mut req = client.get(uri);
2149        if let Ok(token) = auth {
2150            req = req.header("Authorization", &format!("Bearer {token}"));
2151        }
2152
2153        let response = req
2154            .header("User-Agent", "wasmerio")
2155            .header("Accept", "application/vnd.github.v3+json")
2156            .send()
2157            .map_err(anyhow::Error::new)
2158            .context("Could not lookup wasmer repository on Github.")?;
2159
2160        let status = response.status();
2161
2162        log::info!("GitHub api response status: {status}");
2163
2164        let body = response
2165            .bytes()
2166            .map_err(anyhow::Error::new)
2167            .context("Could not retrieve wasmer release history body")?;
2168
2169        if status != reqwest::StatusCode::OK {
2170            log::warn!(
2171                "Warning: Github API replied with non-200 status code: {}. Response: {}",
2172                status,
2173                String::from_utf8_lossy(&body),
2174            );
2175        }
2176
2177        let mut response = serde_json::from_slice::<serde_json::Value>(&body)?;
2178
2179        if let Some(releases) = response.as_array_mut() {
2180            releases.retain(|r| {
2181                r["tag_name"].is_string() && !r["tag_name"].as_str().unwrap().is_empty()
2182            });
2183            releases.sort_by_cached_key(|r| r["tag_name"].as_str().unwrap_or_default().to_string());
2184            match release_version {
2185                Some(specific_version) => {
2186                    let mut all_versions = Vec::new();
2187                    for r in releases.iter() {
2188                        if r["tag_name"].as_str().unwrap_or_default()
2189                            == specific_version.to_string()
2190                        {
2191                            return Ok(r.clone());
2192                        } else {
2193                            all_versions
2194                                .push(r["tag_name"].as_str().unwrap_or_default().to_string());
2195                        }
2196                    }
2197                    return Err(anyhow::anyhow!(
2198                        "could not find release version {}, available versions are: {}",
2199                        specific_version,
2200                        all_versions.join(", ")
2201                    ));
2202                }
2203                None => {
2204                    if let Some(latest) = releases.pop() {
2205                        return Ok(latest);
2206                    }
2207                }
2208            }
2209        }
2210
2211        Err(anyhow!(
2212            "Could not get expected Github API response.\n\nReason: response format is not recognized:\n{response:#?}",
2213        ))
2214    }
2215
2216    pub(super) fn download_release(
2217        env: &WasmerEnv,
2218        mut release: serde_json::Value,
2219        target_triple: wasmer::sys::Triple,
2220    ) -> Result<std::path::PathBuf> {
2221        // Test if file has been already downloaded
2222        if let Ok(mut cache_path) = super::utils::get_libwasmer_cache_path(env) {
2223            let paths = std::fs::read_dir(&cache_path).and_then(|r| {
2224                r.map(|res| res.map(|e| e.path()))
2225                    .collect::<Result<Vec<_>, std::io::Error>>()
2226            });
2227
2228            if let Ok(mut entries) = paths {
2229                entries.retain(|p| p.to_str().map(|p| p.ends_with(".tar.gz")).unwrap_or(false));
2230                entries.retain(|p| super::utils::filter_tarball(p, &target_triple));
2231                if !entries.is_empty() {
2232                    cache_path.push(&entries[0]);
2233                    if cache_path.exists() {
2234                        eprintln!(
2235                            "Using cached tarball to cache path `{}`.",
2236                            cache_path.display()
2237                        );
2238                        return Ok(cache_path);
2239                    }
2240                }
2241            }
2242        }
2243
2244        let assets = match release["assets"].as_array_mut() {
2245            Some(s) => s,
2246            None => {
2247                return Err(anyhow!(
2248                    "GitHub API: no [assets] array in JSON response for latest releases"
2249                ));
2250            }
2251        };
2252
2253        assets.retain(|a| {
2254            let name = match a["name"].as_str() {
2255                Some(s) => s,
2256                None => return false,
2257            };
2258            super::utils::filter_tarball(Path::new(&name), &target_triple)
2259        });
2260
2261        if assets.len() != 1 {
2262            return Err(anyhow!(
2263                "GitHub API: more than one release selected for target {target_triple}: {assets:#?}"
2264            ));
2265        }
2266
2267        let browser_download_url = if let Some(url) = assets[0]["browser_download_url"].as_str() {
2268            url.to_string()
2269        } else {
2270            return Err(anyhow!(
2271                "Could not get download url from Github API response."
2272            ));
2273        };
2274
2275        download_url(env, &browser_download_url)
2276    }
2277
2278    pub(crate) fn download_url(
2279        env: &WasmerEnv,
2280        browser_download_url: &str,
2281    ) -> Result<std::path::PathBuf, anyhow::Error> {
2282        let filename = browser_download_url
2283            .split('/')
2284            .next_back()
2285            .unwrap_or("output")
2286            .to_string();
2287
2288        let download_tempdir = tempfile::TempDir::new()?;
2289        let download_path = download_tempdir.path().join(&filename);
2290
2291        let mut file = std::fs::File::create(&download_path)?;
2292        log::debug!(
2293            "Downloading {} to {}",
2294            browser_download_url,
2295            download_path.display()
2296        );
2297
2298        let mut response = reqwest::blocking::Client::builder()
2299            .redirect(reqwest::redirect::Policy::limited(10))
2300            .timeout(std::time::Duration::from_secs(10))
2301            .build()
2302            .map_err(anyhow::Error::new)
2303            .context("Could not lookup wasmer artifact on Github.")?
2304            .get(browser_download_url)
2305            .send()
2306            .map_err(anyhow::Error::new)
2307            .context("Could not lookup wasmer artifact on Github.")?;
2308
2309        response
2310            .copy_to(&mut file)
2311            .map_err(|e| anyhow::anyhow!("{e}"))?;
2312
2313        match super::utils::get_libwasmer_cache_path(env) {
2314            Ok(mut cache_path) => {
2315                cache_path.push(&filename);
2316                if let Err(err) = std::fs::copy(&download_path, &cache_path) {
2317                    eprintln!(
2318                        "Could not store tarball to cache path `{}`: {}",
2319                        cache_path.display(),
2320                        err
2321                    );
2322                    Err(anyhow!(
2323                        "Could not copy from {} to {}",
2324                        download_path.display(),
2325                        cache_path.display()
2326                    ))
2327                } else {
2328                    eprintln!("Cached tarball to cache path `{}`.", cache_path.display());
2329                    Ok(cache_path)
2330                }
2331            }
2332            Err(err) => {
2333                eprintln!("Could not determine cache path for downloaded binaries.: {err}");
2334                Err(anyhow!("Could not determine libwasmer cache path"))
2335            }
2336        }
2337    }
2338
2339    use std::path::PathBuf;
2340
2341    use crate::{config::WasmerEnv, utils::unpack::try_unpack_targz};
2342
2343    pub(crate) fn list_dir(target: &Path) -> Vec<PathBuf> {
2344        use walkdir::WalkDir;
2345        WalkDir::new(target)
2346            .into_iter()
2347            .filter_map(|e| e.ok())
2348            .map(|entry| entry.path().to_path_buf())
2349            .collect()
2350    }
2351
2352    pub(super) fn untar(tarball: &Path, target: &Path) -> Result<Vec<PathBuf>> {
2353        let _ = std::fs::remove_dir(target);
2354        try_unpack_targz(tarball, target, false)?;
2355        Ok(list_dir(target))
2356    }
2357}