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