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