//! Create a standalone native executable for a given Wasm file.

use super::ObjectFormat;
use crate::store::CompilerOptions;
use anyhow::{Context, Result};
use clap::Parser;
use std::env;
use std::fmt::Write as _;
use std::fs;
use std::fs::File;
use std::io::prelude::*;
use std::io::BufWriter;
use std::path::{Path, PathBuf};
use std::process::Command;
use wasmer::*;
use wasmer_object::{emit_serialized, get_object_for_target};
#[cfg(feature = "webc_runner")]
use webc::{ParseOptions, WebCMmap};

/// The `prefixer` returns the a String to prefix each of the
/// functions in the static object generated by the
/// so we can assure no collisions.
#[cfg(feature = "static-artifact-create")]
pub type PrefixerFn = Box<dyn Fn(&[u8]) -> String + Send>;

const WASMER_MAIN_C_SOURCE: &str = include_str!("wasmer_create_exe_main.c");
#[cfg(feature = "static-artifact-create")]
const WASMER_STATIC_MAIN_C_SOURCE: &str = include_str!("wasmer_static_create_exe_main.c");

#[derive(Debug, Clone)]
pub(crate) struct CrossCompile {
    /// Cross-compilation library path.
    pub(crate) library_path: Option<PathBuf>,

    /// Cross-compilation tarball library path.
    pub(crate) tarball: Option<PathBuf>,

    /// Specify `zig` binary path
    pub(crate) zig_binary_path: Option<PathBuf>,
}

#[derive(Debug)]
pub(crate) struct CrossCompileSetup {
    pub(crate) target: Triple,
    pub(crate) zig_binary_path: PathBuf,
    pub(crate) library: PathBuf,
}

#[derive(Debug, Parser)]
/// The options for the `wasmer create-exe` subcommand
pub struct CreateExe {
    /// Input file
    #[clap(name = "FILE", parse(from_os_str))]
    path: PathBuf,

    /// Output file
    #[clap(name = "OUTPUT PATH", short = 'o', parse(from_os_str))]
    output: PathBuf,

    /// Compilation Target triple
    ///
    /// Accepted target triple values must follow the
    /// ['target_lexicon'](https://crates.io/crates/target-lexicon) crate format.
    ///
    /// The recommended targets we try to support are:
    ///
    /// - "x86_64-linux-gnu"
    /// - "aarch64-linux-gnu"
    /// - "x86_64-apple-darwin"
    /// - "arm64-apple-darwin"
    #[clap(long = "target")]
    target_triple: Option<Triple>,

    // Cross-compile with `zig`
    /// Cross-compilation library path.
    #[clap(long = "library-path")]
    library_path: Option<PathBuf>,

    /// Cross-compilation tarball library path.
    #[clap(long = "tarball")]
    tarball: Option<PathBuf>,

    /// Specify `zig` binary path
    #[clap(long = "zig-binary-path")]
    zig_binary_path: Option<PathBuf>,

    /// When compiling .webc files in separate steps
    /// (create-obj, then create-exe on the resulting dir)
    /// the create-exe step needs the path to the .webc file again
    #[clap(long = "webc-volume-path")]
    webc_volume_path: Option<PathBuf>,

    /// Object format options
    ///
    /// This flag accepts two options: `symbols` or `serialized`.
    /// - (default) `symbols` creates an
    /// executable where all functions and metadata of the module are regular object symbols
    /// - `serialized` creates an executable where the module is zero-copy serialized as raw data
    #[clap(name = "OBJECT_FORMAT", long = "object-format", verbatim_doc_comment)]
    object_format: Option<ObjectFormat>,

    /// Header file for object input
    ///
    /// If given, the input `PATH` is assumed to be an object created with `wasmer create-obj` and
    /// this is its accompanying header file.
    #[clap(name = "HEADER", long = "header", verbatim_doc_comment)]
    header: Option<PathBuf>,

    #[clap(short = 'm')]
    cpu_features: Vec<CpuFeature>,

    /// Additional libraries to link against.
    /// This is useful for fixing linker errors that may occur on some systems.
    #[clap(short = 'l')]
    libraries: Vec<String>,

    #[clap(flatten)]
    compiler: CompilerOptions,
}

impl CreateExe {
    /// Runs logic for the `compile` subcommand
    pub fn execute(&self) -> Result<()> {
        /* Making library_path, tarball zig_binary_path flags require that target_triple flag
         * is set cannot be encoded with structopt, so we have to perform cli flag validation
         * manually here */
        let cross_compile: Option<CrossCompile> = if self.target_triple.is_none()
            && (self.library_path.is_some()
                || self.tarball.is_some()
                || self.zig_binary_path.is_some())
        {
            return Err(anyhow!(
                "To cross-compile an executable, you must specify a target triple with --target"
            ));
        } else if self.target_triple.is_some() {
            Some(CrossCompile {
                library_path: self.library_path.clone(),
                zig_binary_path: self.zig_binary_path.clone(),
                tarball: self.tarball.clone(),
            })
        } else {
            None
        };

        let target = self
            .target_triple
            .as_ref()
            .map(|target_triple| {
                let mut features = self
                    .cpu_features
                    .clone()
                    .into_iter()
                    .fold(CpuFeature::set(), |a, b| a | b);
                // Cranelift requires SSE2, so we have this "hack" for now to facilitate
                // usage
                if target_triple.architecture == Architecture::X86_64 {
                    features |= CpuFeature::SSE2;
                }
                Target::new(target_triple.clone(), features)
            })
            .unwrap_or_default();

        let starting_cd = env::current_dir()?;
        let wasm_module_path = starting_cd.join(&self.path);
        let output_path = starting_cd.join(&self.output);
        let object_format = self.object_format.unwrap_or(ObjectFormat::Symbols);

        let cross_compilation = Self::get_cross_compile_setup(
            cross_compile,
            self.target_triple.clone(),
            object_format,
            &starting_cd,
        )?;

        #[cfg(feature = "webc_runner")]
        {
            let working_dir = tempdir::TempDir::new("testpirita")?;
            let working_dir = working_dir.path().to_path_buf();

            if let Ok(pirita) = WebCMmap::parse(wasm_module_path.clone(), &ParseOptions::default())
            {
                return self.create_exe_pirita(
                    &pirita,
                    target,
                    cross_compilation,
                    &working_dir,
                    output_path,
                    object_format,
                );
            }
        }

        let (store, compiler_type) = self.compiler.get_store_for_target(target.clone())?;

        // Object is likely a file created from create-obj
        #[cfg(feature = "webc_runner")]
        if self.path.is_dir() {
            let pirita_volume_path = self.webc_volume_path.clone()
                .ok_or_else(|| anyhow::anyhow!("If compiling using a directory (created by create-obj), you need to also specify the webc path again using --webc-volume-path because the volumes.o is not part of the directory"))?;

            let file = WebCMmap::parse(pirita_volume_path.clone(), &ParseOptions::default())
                .map_err(|e| {
                    anyhow::anyhow!(
                        "could not parse {} as webc: {e}",
                        pirita_volume_path.display()
                    )
                })?;

            let volume_bytes = file.get_volumes_as_fileblock();

            return self.link_exe_from_dir(
                volume_bytes.as_slice(),
                &store,
                &target,
                cross_compilation,
                &self.path,
                output_path,
                object_format,
            );
        }

        println!("Compiler: {}", compiler_type.to_string());
        println!("Target: {}", target.triple());
        println!("Format: {:?}", object_format);

        #[cfg(not(windows))]
        let wasm_object_path = PathBuf::from("wasm.o");
        #[cfg(windows)]
        let wasm_object_path = PathBuf::from("wasm.obj");

        if let Some(header_path) = self.header.as_ref() {
            /* In this case, since a header file is given, the input file is expected to be an
             * object created with `create-obj` subcommand */
            let header_path = starting_cd.join(&header_path);
            std::fs::copy(&header_path, Path::new("static_defs.h"))
                .context("Could not access given header file")?;
            if let Some(setup) = cross_compilation.as_ref() {
                self.compile_zig(
                    output_path,
                    &[wasm_module_path],
                    &[std::path::Path::new("static_defs.h").into()],
                    setup,
                    &[],
                    None,
                    None,
                )?;
            } else {
                self.link(
                    output_path,
                    wasm_module_path,
                    std::path::Path::new("static_defs.h").into(),
                    &[],
                    None,
                    None,
                )?;
            }
        } else {
            match object_format {
                ObjectFormat::Serialized => {
                    let module = Module::from_file(&store, &wasm_module_path)
                        .context("failed to compile Wasm")?;
                    let bytes = module.serialize()?;
                    let mut obj = get_object_for_target(target.triple())?;
                    emit_serialized(&mut obj, &bytes, target.triple(), "WASMER_MODULE")?;
                    let mut writer = BufWriter::new(File::create(&wasm_object_path)?);
                    obj.write_stream(&mut writer)
                        .map_err(|err| anyhow::anyhow!(err.to_string()))?;
                    writer.flush()?;
                    drop(writer);

                    let cli_given_triple = self.target_triple.clone();
                    self.compile_c(wasm_object_path, cli_given_triple, output_path)?;
                }
                #[cfg(not(feature = "static-artifact-create"))]
                ObjectFormat::Symbols => {
                    return Err(anyhow!("This version of wasmer-cli hasn't been compiled with static artifact support. You need to enable the `static-artifact-create` feature during compilation."));
                }
                #[cfg(feature = "static-artifact-create")]
                ObjectFormat::Symbols => {
                    let engine = store.engine();
                    let engine_inner = engine.inner();
                    let compiler = engine_inner.compiler()?;
                    let features = engine_inner.features();
                    let tunables = store.tunables();
                    let data: Vec<u8> = fs::read(wasm_module_path)?;
                    let prefixer: Option<PrefixerFn> = None;
                    let (module_info, obj, metadata_length, symbol_registry) =
                        Artifact::generate_object(
                            compiler, &data, prefixer, &target, tunables, features,
                        )?;

                    let header_file_src = crate::c_gen::staticlib_header::generate_header_file(
                        &module_info,
                        &*symbol_registry,
                        metadata_length,
                    );
                    // Write object file with functions
                    let object_file_path: std::path::PathBuf =
                        std::path::Path::new("functions.o").into();
                    let mut writer = BufWriter::new(File::create(&object_file_path)?);
                    obj.write_stream(&mut writer)
                        .map_err(|err| anyhow::anyhow!(err.to_string()))?;
                    writer.flush()?;
                    // Write down header file that includes pointer arrays and the deserialize function
                    let mut writer = BufWriter::new(File::create("static_defs.h")?);
                    writer.write_all(header_file_src.as_bytes())?;
                    writer.flush()?;
                    if let Some(setup) = cross_compilation.as_ref() {
                        self.compile_zig(
                            output_path,
                            &[object_file_path],
                            &[std::path::Path::new("static_defs.h").into()],
                            setup,
                            &[],
                            None,
                            None,
                        )?;
                    } else {
                        self.link(
                            output_path,
                            object_file_path,
                            std::path::Path::new("static_defs.h").into(),
                            &[],
                            None,
                            None,
                        )?;
                    }
                }
            }
        }

        if cross_compilation.is_some() {
            eprintln!(
                "✔ Cross-compiled executable for `{}` target compiled successfully to `{}`.",
                target.triple(),
                self.output.display(),
            );
        } else {
            eprintln!(
                "✔ Native executable compiled successfully to `{}`.",
                self.output.display(),
            );
        }

        Ok(())
    }

    pub(crate) fn get_cross_compile_setup(
        cross_compile: Option<CrossCompile>,
        target_triple: Option<Triple>,
        object_format: ObjectFormat,
        starting_cd: &Path,
    ) -> Result<Option<CrossCompileSetup>, anyhow::Error> {
        if let Some(mut cross_subc) = cross_compile.or_else(|| {
            if target_triple.is_some() {
                Some(CrossCompile {
                    library_path: None,
                    tarball: None,
                    zig_binary_path: None,
                })
            } else {
                None
            }
        }) {
            if let ObjectFormat::Serialized = object_format {
                return Err(anyhow!(
                    "Cross-compilation with serialized object format is not implemented."
                ));
            }

            let target = if let Some(target_triple) = target_triple {
                target_triple
            } else {
                return Err(anyhow!(
                        "To cross-compile an executable, you must specify a target triple with --target"
                ));
            };
            if let Some(tarball_path) = cross_subc.tarball.as_mut() {
                if tarball_path.is_relative() {
                    *tarball_path = starting_cd.join(&tarball_path);
                    if !tarball_path.exists() {
                        return Err(anyhow!(
                            "Tarball path `{}` does not exist.",
                            tarball_path.display()
                        ));
                    } else if tarball_path.is_dir() {
                        return Err(anyhow!(
                            "Tarball path `{}` is a directory.",
                            tarball_path.display()
                        ));
                    }
                }
            }
            let zig_binary_path =
                find_zig_binary(cross_subc.zig_binary_path.as_ref().and_then(|p| {
                    if p.is_absolute() {
                        p.canonicalize().ok()
                    } else {
                        starting_cd.join(p).canonicalize().ok()
                    }
                }))?;
            let library = if let Some(v) = cross_subc.library_path.clone() {
                v.canonicalize().unwrap_or(v)
            } else {
                let (filename, tarball_dir) =
                    if let Some(local_tarball) = cross_subc.tarball.as_ref() {
                        Self::find_filename(local_tarball, &target)
                    } else {
                        // check if the tarball for the target already exists locally
                        let local_tarball = std::fs::read_dir(get_libwasmer_cache_path()?)?
                            .filter_map(|e| e.ok())
                            .filter_map(|e| {
                                let path = format!("{}", e.path().display());
                                if path.ends_with(".tar.gz") {
                                    Some(e.path())
                                } else {
                                    None
                                }
                            })
                            .filter_map(|p| Self::filter_tarballs(&p, &target))
                            .next();

                        if let Some(local_tarball) = local_tarball.as_ref() {
                            Self::find_filename(local_tarball, &target)
                        } else {
                            let release = http_fetch::get_latest_release()?;
                            let tarball = http_fetch::download_release(release, target.clone())?;
                            Self::find_filename(&tarball, &target)
                        }
                    }?;

                tarball_dir.join(&filename)
            };
            let ccs = CrossCompileSetup {
                target,
                zig_binary_path,
                library,
            };
            Ok(Some(ccs))
        } else {
            Ok(None)
        }
    }

    fn find_filename(
        local_tarball: &Path,
        target: &Triple,
    ) -> Result<(String, PathBuf), anyhow::Error> {
        let target_file_path = local_tarball
            .parent()
            .and_then(|parent| Some(parent.join(local_tarball.file_stem()?)))
            .unwrap_or_else(|| local_tarball.to_path_buf());

        let target_file_path = target_file_path
            .parent()
            .and_then(|parent| Some(parent.join(target_file_path.file_stem()?)))
            .unwrap_or_else(|| target_file_path.clone());

        std::fs::create_dir_all(&target_file_path)
            .map_err(|e| anyhow::anyhow!("{e}"))
            .context(anyhow::anyhow!("{}", target_file_path.display()))?;
        let files = untar(local_tarball.to_path_buf(), target_file_path.clone())?;
        let tarball_dir = target_file_path.canonicalize().unwrap_or(target_file_path);

        let file = files
        .iter()
        .find(|f| f.ends_with("libwasmer.a")).cloned()
        .ok_or_else(|| {
            anyhow!("Could not find libwasmer.a for {} target in the provided tarball path (files = {files:#?})", target)
        })?;

        Ok((file, tarball_dir))
    }

    fn filter_tarballs(p: &Path, target: &Triple) -> Option<PathBuf> {
        if let Architecture::Aarch64(_) = target.architecture {
            if !p.file_name()?.to_str()?.contains("aarch64") {
                return None;
            }
        }

        if let Architecture::X86_64 = target.architecture {
            if !p.file_name()?.to_str()?.contains("x86_64") {
                return None;
            }
        }

        if let OperatingSystem::Windows = target.operating_system {
            if !p.file_name()?.to_str()?.contains("windows") {
                return None;
            }
        }

        if let OperatingSystem::Darwin = target.operating_system {
            if !(p.file_name()?.to_str()?.contains("apple")
                || p.file_name()?.to_str()?.contains("darwin"))
            {
                return None;
            }
        }

        if let OperatingSystem::Linux = target.operating_system {
            if !p.file_name()?.to_str()?.contains("linux") {
                return None;
            }
        }

        Some(p.to_path_buf())
    }

    fn compile_c(
        &self,
        wasm_object_path: PathBuf,
        target_triple: Option<wasmer::Triple>,
        output_path: PathBuf,
    ) -> anyhow::Result<()> {
        let tempdir = tempdir::TempDir::new("compile-c")?;
        let tempdir_path = tempdir.path();

        // write C src to disk
        let c_src_path = tempdir_path.join("wasmer_main.c");
        #[cfg(not(windows))]
        let c_src_obj = tempdir_path.join("wasmer_main.o");
        #[cfg(windows)]
        let c_src_obj = tempdir_path.join("wasmer_main.obj");

        std::fs::write(
            &c_src_path,
            WASMER_MAIN_C_SOURCE
                .replace("// WASI_DEFINES", "#define WASI")
                .as_bytes(),
        )?;

        run_c_compile(&c_src_path, &c_src_obj, target_triple.clone())
            .context("Failed to compile C source code")?;

        LinkCode {
            object_paths: vec![c_src_obj, wasm_object_path],
            output_path,
            additional_libraries: self.libraries.clone(),
            target: target_triple,
            ..Default::default()
        }
        .run()
        .context("Failed to link objects together")?;

        Ok(())
    }

    #[allow(clippy::too_many_arguments)]
    fn compile_zig(
        &self,
        output_path: PathBuf,
        object_paths: &[PathBuf],
        header_code_paths: &[PathBuf],
        setup: &CrossCompileSetup,
        pirita_atoms: &[String],
        pirita_main_atom: Option<&str>,
        pirita_volume_path: Option<PathBuf>,
    ) -> anyhow::Result<()> {
        let tempdir = tempdir::TempDir::new("wasmer-static-compile-zig")?;
        let tempdir_path = tempdir.path();
        let c_src_path = tempdir_path.join("wasmer_main.c");
        let CrossCompileSetup {
            ref target,
            ref zig_binary_path,
            ref library,
        } = setup;
        let mut libwasmer_path = library.to_path_buf();
        println!("Library Path: {}", libwasmer_path.display());
        /* Cross compilation is only possible with zig */
        println!("Using zig binary: {}", zig_binary_path.display());
        let zig_triple = triple_to_zig_triple(target);
        println!("Using zig target triple: {}", &zig_triple);

        let lib_filename = libwasmer_path
            .file_name()
            .unwrap()
            .to_str()
            .unwrap()
            .to_string();
        libwasmer_path.pop();

        if let Some(entrypoint) = pirita_main_atom.as_ref() {
            let c_code = Self::generate_pirita_wasmer_main_c_static(pirita_atoms, entrypoint);
            std::fs::write(&c_src_path, c_code)?;
        } else {
            std::fs::write(&c_src_path, WASMER_STATIC_MAIN_C_SOURCE)?;
        }

        let mut header_code_paths = header_code_paths.to_vec();

        for h in header_code_paths.iter_mut() {
            if !h.is_dir() {
                h.pop();
            }

            if h.display().to_string().is_empty() {
                *h = std::env::current_dir()?;
            }
        }

        /* Compile main function */
        let compilation = {
            let mut include_dir = libwasmer_path.clone();
            include_dir.pop();
            include_dir.push("include");

            let mut cmd = Command::new(zig_binary_path);
            cmd.arg("build-exe");
            cmd.arg("-target");
            cmd.arg(&zig_triple);
            cmd.arg(&format!("-I{}/", include_dir.display()));
            for h in header_code_paths {
                cmd.arg(&format!("-I{}/", h.display()));
            }
            if zig_triple.contains("windows") {
                cmd.arg("-lc++");
            } else {
                cmd.arg("-lc");
            }
            cmd.arg("-lunwind");
            cmd.arg("-OReleaseSafe");
            cmd.arg("-fno-compiler-rt");
            cmd.arg(&format!("-femit-bin={}", output_path.display()));

            for o in object_paths {
                cmd.arg(o);
            }
            cmd.arg(&c_src_path);
            cmd.arg(libwasmer_path.join(&lib_filename));
            if zig_triple.contains("windows") {
                let mut libwasmer_parent = libwasmer_path.clone();
                libwasmer_parent.pop();
                let files_winsdk = std::fs::read_dir(libwasmer_parent.join("winsdk"))
                    .ok()
                    .map(|res| res.filter_map(|r| Some(r.ok()?.path())).collect::<Vec<_>>())
                    .unwrap_or_default();
                for f in files_winsdk {
                    cmd.arg(f);
                }
            }
            if let Some(volume_obj) = pirita_volume_path.as_ref() {
                cmd.arg(volume_obj.clone());
            }

            #[cfg(feature = "debug")]
            log::debug!("{:?}", cmd);
            cmd.output().context("Could not execute `zig`")?
        };
        if !compilation.status.success() {
            return Err(anyhow::anyhow!(String::from_utf8_lossy(
                &compilation.stderr
            )
            .to_string()));
        }
        Ok(())
    }

    // Write the volumes.o file
    #[cfg(feature = "webc_runner")]
    fn write_volume_obj(
        volume_bytes: &[u8],
        target: &Target,
        output_path: &Path,
    ) -> anyhow::Result<PathBuf> {
        #[cfg(not(windows))]
        let volume_object_path = output_path.join("volumes.o");
        #[cfg(windows)]
        let volume_object_path = output_path.join("volumes.obj");

        let mut volumes_object = get_object_for_target(target.triple())?;
        emit_serialized(
            &mut volumes_object,
            volume_bytes,
            target.triple(),
            "VOLUMES",
        )?;

        let mut writer = BufWriter::new(File::create(&volume_object_path)?);
        volumes_object
            .write_stream(&mut writer)
            .map_err(|err| anyhow::anyhow!(err.to_string()))?;
        writer.flush()?;
        drop(writer);

        Ok(volume_object_path)
    }

    #[cfg(feature = "webc_runner")]
    pub(crate) fn create_objs_pirita(
        store: &Store,
        file: &WebCMmap,
        target: &Target,
        output_path: &Path,
        object_format: ObjectFormat,
    ) -> anyhow::Result<()> {
        if !output_path.is_dir() {
            return Err(anyhow::anyhow!(
                "Expected {} to be an output directory, not a file",
                output_path.display()
            ));
        }

        std::fs::create_dir_all(output_path)?;
        std::fs::create_dir_all(output_path.join("atoms"))?;

        let atom_to_run = file
            .manifest
            .entrypoint
            .as_ref()
            .and_then(|s| file.get_atom_name_for_command("wasi", s).ok());
        if let Some(atom_to_run) = atom_to_run.as_ref() {
            std::fs::write(output_path.join("entrypoint"), atom_to_run)?;
        }

        for (atom_name, atom_bytes) in file.get_all_atoms() {
            std::fs::create_dir_all(output_path.join("atoms"))?;

            #[cfg(not(windows))]
            let object_path = output_path.join("atoms").join(&format!("{atom_name}.o"));
            #[cfg(windows)]
            let object_path = output_path.join("atoms").join(&format!("{atom_name}.obj"));

            std::fs::create_dir_all(output_path.join("atoms").join(&atom_name))?;

            let header_path = output_path
                .join("atoms")
                .join(&atom_name)
                .join("static_defs.h");

            match object_format {
                ObjectFormat::Serialized => {
                    let module = Module::new(&store, &atom_bytes)
                        .context(format!("Failed to compile atom {atom_name:?} to wasm"))?;
                    let bytes = module.serialize()?;
                    let mut obj = get_object_for_target(target.triple())?;
                    let atom_name_uppercase = atom_name.to_uppercase();
                    emit_serialized(&mut obj, &bytes, target.triple(), &atom_name_uppercase)?;

                    let mut writer = BufWriter::new(File::create(&object_path)?);
                    obj.write_stream(&mut writer)
                        .map_err(|err| anyhow::anyhow!(err.to_string()))?;
                    writer.flush()?;
                    drop(writer);
                }
                #[cfg(feature = "static-artifact-create")]
                ObjectFormat::Symbols => {
                    let engine = store.engine();
                    let engine_inner = engine.inner();
                    let compiler = engine_inner.compiler()?;
                    let features = engine_inner.features();
                    let tunables = store.tunables();
                    let prefixer: Option<PrefixerFn> = None;
                    let (module_info, obj, metadata_length, symbol_registry) =
                        Artifact::generate_object(
                            compiler, atom_bytes, prefixer, target, tunables, features,
                        )?;

                    let header_file_src = crate::c_gen::staticlib_header::generate_header_file(
                        &module_info,
                        &*symbol_registry,
                        metadata_length,
                    );

                    let mut writer = BufWriter::new(File::create(&object_path)?);
                    obj.write_stream(&mut writer)
                        .map_err(|err| anyhow::anyhow!(err.to_string()))?;
                    writer.flush()?;

                    let mut writer = BufWriter::new(File::create(&header_path)?);
                    writer.write_all(header_file_src.as_bytes())?;
                    writer.flush()?;
                }
                #[cfg(not(feature = "static-artifact-create"))]
                ObjectFormat::Symbols => {
                    return Err(anyhow!("Objects cannot be compiled in format \"symbols\" without static-artifact-create feature"));
                }
            }
        }

        Ok(())
    }

    #[cfg(feature = "webc_runner")]
    #[allow(clippy::too_many_arguments)]
    fn link_exe_from_dir(
        &self,
        volume_bytes: &[u8],
        _store: &Store,
        target: &Target,
        cross_compilation: Option<CrossCompileSetup>,
        working_dir: &Path,
        output_path: PathBuf,
        object_format: ObjectFormat,
    ) -> anyhow::Result<()> {
        let tempdir = tempdir::TempDir::new("link-exe-from-dir")?;
        let tempdir_path = tempdir.path();

        let entrypoint = std::fs::read_to_string(working_dir.join("entrypoint"))
            .map_err(|_| anyhow::anyhow!("file has no entrypoint to run"))?;

        if !working_dir.join("atoms").exists() {
            return Err(anyhow::anyhow!("file has no atoms to compile"));
        }

        let mut atom_names = Vec::new();
        for obj in std::fs::read_dir(working_dir.join("atoms"))? {
            let path = obj?.path();
            if !path.is_dir() {
                if let Some(s) = path.file_stem() {
                    atom_names.push(
                        s.to_str()
                            .ok_or_else(|| anyhow::anyhow!("wrong atom name"))?
                            .to_string(),
                    );
                }
            }
        }

        match object_format {
            ObjectFormat::Serialized => {
                let mut link_objects: Vec<PathBuf> = Vec::new();

                let volume_object_path =
                    Self::write_volume_obj(volume_bytes, target, tempdir_path)?;

                #[cfg(not(windows))]
                let c_src_obj = working_dir.join("wasmer_main.o");
                #[cfg(windows)]
                let c_src_obj = working_dir.join("wasmer_main.obj");

                for obj in std::fs::read_dir(working_dir.join("atoms"))? {
                    let path = obj?.path();
                    if !path.is_dir() {
                        link_objects.push(path.to_path_buf());
                    }
                }

                let c_code = Self::generate_pirita_wasmer_main_c(&atom_names, &entrypoint);

                let c_src_path = working_dir.join("wasmer_main.c");

                std::fs::write(&c_src_path, c_code.as_bytes())
                    .context("Failed to open C source code file")?;

                // TODO: this branch is never hit because object format serialized +
                // cross compilation doesn't work
                if let Some(setup) = cross_compilation {
                    // zig treats .o files the same as .c files
                    link_objects.push(c_src_path);

                    self.compile_zig(
                        output_path,
                        &link_objects,
                        &[],
                        &setup,
                        &atom_names,
                        Some(&entrypoint),
                        Some(volume_object_path),
                    )?;
                } else {
                    // compile with cc instead of zig
                    run_c_compile(c_src_path.as_path(), &c_src_obj, self.target_triple.clone())
                        .context("Failed to compile C source code")?;

                    link_objects.push(c_src_obj);
                    link_objects.push(volume_object_path);

                    LinkCode {
                        object_paths: link_objects,
                        output_path,
                        additional_libraries: self.libraries.clone(),
                        target: self.target_triple.clone(),
                        ..Default::default()
                    }
                    .run()
                    .context("Failed to link objects together")?;
                }
            }
            ObjectFormat::Symbols => {
                let object_file_path = working_dir.join("atoms").join(&format!("{entrypoint}.o"));
                let static_defs_file_path = working_dir
                    .join("atoms")
                    .join(&entrypoint)
                    .join("static_defs.h");
                let volumes_obj_path = Self::write_volume_obj(volume_bytes, target, tempdir_path)?;

                if let Some(setup) = cross_compilation.as_ref() {
                    self.compile_zig(
                        output_path,
                        &[object_file_path],
                        &[static_defs_file_path],
                        setup,
                        &atom_names,
                        Some(&entrypoint),
                        Some(volumes_obj_path),
                    )?;
                } else {
                    self.link(
                        output_path,
                        object_file_path,
                        static_defs_file_path,
                        &atom_names,
                        Some(&entrypoint),
                        Some(volumes_obj_path),
                    )?;
                }
            }
        }

        Ok(())
    }

    fn normalize_atom_name(s: &str) -> String {
        s.chars()
            .filter_map(|c| {
                if char::is_alphabetic(c) {
                    Some(c)
                } else if c == '-' {
                    Some('_')
                } else {
                    None
                }
            })
            .collect()
    }

    fn generate_pirita_wasmer_main_c_static(atom_names: &[String], atom_to_run: &str) -> String {
        let mut c_code_to_instantiate = String::new();
        let mut deallocate_module = String::new();

        let atom_to_run = Self::normalize_atom_name(atom_to_run);

        for atom_name in atom_names.iter() {
            let atom_name = Self::normalize_atom_name(atom_name);

            write!(
                c_code_to_instantiate,
                "

            wasm_module_t *atom_{atom_name} = wasmer_object_module_new(store, \"{atom_name}\");

            if (!atom_{atom_name}) {{
                fprintf(stderr, \"Failed to create module from atom \\\"{atom_name}\\\"\\n\");
                print_wasmer_error();
                return -1;
            }}
            "
            )
            .unwrap();
            write!(deallocate_module, "wasm_module_delete(atom_{atom_name});").unwrap();
        }

        write!(
            c_code_to_instantiate,
            "wasm_module_t *module = atom_{atom_to_run};"
        )
        .unwrap();

        WASMER_STATIC_MAIN_C_SOURCE
            .replace("#define WASI", "#define WASI\r\n#define WASI_PIRITA")
            .replace("// INSTANTIATE_MODULES", &c_code_to_instantiate)
            .replace("##atom-name##", &atom_to_run)
            .replace("wasm_module_delete(module);", &deallocate_module)
    }

    #[cfg(feature = "webc_runner")]
    fn generate_pirita_wasmer_main_c(atom_names: &[String], atom_to_run: &str) -> String {
        let mut c_code_to_add = String::new();
        let mut c_code_to_instantiate = String::new();
        let mut deallocate_module = String::new();

        for atom_name in atom_names.iter() {
            let atom_name = Self::normalize_atom_name(atom_name);
            let atom_name_uppercase = atom_name.to_uppercase();

            c_code_to_add.push_str(&format!(
                "
            extern size_t {atom_name_uppercase}_LENGTH asm(\"{atom_name_uppercase}_LENGTH\");
            extern char {atom_name_uppercase}_DATA asm(\"{atom_name_uppercase}_DATA\");
            "
            ));

            c_code_to_instantiate.push_str(&format!("
            wasm_byte_vec_t atom_{atom_name}_byte_vec = {{
                .size = {atom_name_uppercase}_LENGTH,
                .data = &{atom_name_uppercase}_DATA,
            }};
            wasm_module_t *atom_{atom_name} = wasm_module_deserialize(store, &atom_{atom_name}_byte_vec);

            if (!atom_{atom_name}) {{
                fprintf(stderr, \"Failed to create module from atom \\\"{atom_name}\\\"\\n\");
                print_wasmer_error();
                return -1;
            }}
            "));
            deallocate_module.push_str(&format!("wasm_module_delete(atom_{atom_name});"));
        }

        c_code_to_instantiate.push_str(&format!("wasm_module_t *module = atom_{atom_to_run};"));

        WASMER_MAIN_C_SOURCE
            .replace("#define WASI", "#define WASI\r\n#define WASI_PIRITA")
            .replace("// DECLARE_MODULES", &c_code_to_add)
            .replace("// INSTANTIATE_MODULES", &c_code_to_instantiate)
            .replace("##atom-name##", atom_to_run)
            .replace("wasm_module_delete(module);", &deallocate_module)
    }

    #[cfg(feature = "webc_runner")]
    fn create_exe_pirita(
        &self,
        file: &WebCMmap,
        target: Target,
        cross_compilation: Option<CrossCompileSetup>,
        working_dir: &Path,
        output_path: PathBuf,
        object_format: ObjectFormat,
    ) -> anyhow::Result<()> {
        let _ = std::fs::create_dir_all(&working_dir);
        let (store, _) = self.compiler.get_store_for_target(target.clone())?;

        Self::create_objs_pirita(&store, file, &target, working_dir, object_format)?;

        let volumes_obj = file.get_volumes_as_fileblock();
        self.link_exe_from_dir(
            volumes_obj.as_slice(),
            &store,
            &target,
            cross_compilation,
            working_dir,
            output_path,
            object_format,
        )?;

        Ok(())
    }

    #[cfg(feature = "static-artifact-create")]
    fn link(
        &self,
        output_path: PathBuf,
        object_path: PathBuf,
        mut header_code_path: PathBuf,
        pirita_atoms: &[String],
        pirita_main_atom: Option<&str>,
        pirita_volume_path: Option<PathBuf>,
    ) -> anyhow::Result<()> {
        let tempdir = tempdir::TempDir::new("wasmer-static-compile")?;
        let tempdir_path = tempdir.path();

        let mut object_paths = vec![object_path, "main_obj.obj".into()];
        if let Some(volume_obj) = pirita_volume_path.as_ref() {
            object_paths.push(volume_obj.to_path_buf());
        }

        let linkcode = LinkCode {
            object_paths,
            output_path,
            ..Default::default()
        };
        let c_src_path = tempdir_path.join("wasmer_main.c");
        let mut libwasmer_path = get_libwasmer_path()?
            .canonicalize()
            .context("Failed to find libwasmer")?;

        let lib_filename = libwasmer_path
            .file_name()
            .unwrap()
            .to_str()
            .unwrap()
            .to_string();
        libwasmer_path.pop();

        if let Some(entrypoint) = pirita_main_atom.as_ref() {
            let c_code = Self::generate_pirita_wasmer_main_c_static(pirita_atoms, entrypoint);
            std::fs::write(&c_src_path, c_code)?;
        } else {
            std::fs::write(&c_src_path, WASMER_STATIC_MAIN_C_SOURCE)?;
        }

        if !header_code_path.is_dir() {
            header_code_path.pop();
        }

        if header_code_path.display().to_string().is_empty() {
            header_code_path = std::env::current_dir()?;
        }

        let wasmer_include_dir = get_wasmer_include_directory()?;
        let wasmer_h_path = wasmer_include_dir.join("wasmer.h");
        if !wasmer_h_path.exists() {
            return Err(anyhow::anyhow!(
                "Could not find wasmer.h in {}",
                wasmer_include_dir.display()
            ));
        }
        let wasm_h_path = wasmer_include_dir.join("wasm.h");
        if !wasm_h_path.exists() {
            return Err(anyhow::anyhow!(
                "Could not find wasm.h in {}",
                wasmer_include_dir.display()
            ));
        }
        std::fs::copy(wasmer_h_path, header_code_path.join("wasmer.h"))?;
        std::fs::copy(wasm_h_path, header_code_path.join("wasm.h"))?;

        /* Compile main function */
        let compilation = {
            Command::new("cc")
                .arg("-c")
                .arg(&c_src_path)
                .arg(if linkcode.optimization_flag.is_empty() {
                    "-O2"
                } else {
                    linkcode.optimization_flag.as_str()
                })
                .arg(&format!("-L{}", libwasmer_path.display()))
                .arg(&format!("-l:{}", lib_filename))
                //.arg("-lwasmer")
                // Add libraries required per platform.
                // We need userenv, sockets (Ws2_32), advapi32 for some system calls and bcrypt for random numbers.
                //#[cfg(windows)]
                //    .arg("-luserenv")
                //    .arg("-lWs2_32")
                //    .arg("-ladvapi32")
                //    .arg("-lbcrypt")
                // On unix we need dlopen-related symbols, libmath for a few things, and pthreads.
                //#[cfg(not(windows))]
                .arg("-ldl")
                .arg("-lm")
                .arg("-pthread")
                .arg(&format!("-I{}", header_code_path.display()))
                .arg("-v")
                .arg("-o")
                .arg("main_obj.obj")
                .output()?
        };
        if !compilation.status.success() {
            return Err(anyhow::anyhow!(String::from_utf8_lossy(
                &compilation.stderr
            )
            .to_string()));
        }
        linkcode.run().context("Failed to link objects together")?;
        Ok(())
    }
}

#[test]
fn test_normalize_atom_name() {
    assert_eq!(
        CreateExe::normalize_atom_name("atom-name-with-dash"),
        "atom_name_with_dash".to_string()
    );
}

fn triple_to_zig_triple(target_triple: &Triple) -> String {
    let arch = match target_triple.architecture {
        wasmer_types::Architecture::X86_64 => "x86_64".into(),
        wasmer_types::Architecture::Aarch64(wasmer_types::Aarch64Architecture::Aarch64) => {
            "aarch64".into()
        }
        v => v.to_string(),
    };
    let os = match target_triple.operating_system {
        wasmer_types::OperatingSystem::Linux => "linux".into(),
        wasmer_types::OperatingSystem::Darwin => "macos".into(),
        wasmer_types::OperatingSystem::Windows => "windows".into(),
        v => v.to_string(),
    };
    let env = match target_triple.environment {
        wasmer_types::Environment::Musl => "musl",
        wasmer_types::Environment::Gnu => "gnu",
        wasmer_types::Environment::Msvc => "msvc",
        _ => "none",
    };
    format!("{}-{}-{}", arch, os, env)
}

fn get_wasmer_dir() -> anyhow::Result<PathBuf> {
    Ok(PathBuf::from(
        env::var("WASMER_DIR")
            .or_else(|e| {
                option_env!("WASMER_INSTALL_PREFIX")
                    .map(str::to_string)
                    .ok_or(e)
            })
            .context("Trying to read env var `WASMER_DIR`")?,
    ))
}

fn get_wasmer_include_directory() -> anyhow::Result<PathBuf> {
    let mut path = get_wasmer_dir()?;
    if path.clone().join("wasmer.h").exists() {
        return Ok(path);
    }
    path.push("include");
    if !path.clone().join("wasmer.h").exists() {
        println!(
            "wasmer.h does not exist in {}, will probably default to the system path",
            path.canonicalize().unwrap().display()
        );
    }
    Ok(path)
}

/// path to the static libwasmer
fn get_libwasmer_path() -> anyhow::Result<PathBuf> {
    let path = get_wasmer_dir()?;

    // TODO: prefer headless Wasmer if/when it's a separate library.
    #[cfg(not(windows))]
    let libwasmer_static_name = "libwasmer.a";
    #[cfg(windows)]
    let libwasmer_static_name = "libwasmer.lib";

    if path.exists() && path.join(libwasmer_static_name).exists() {
        Ok(path.join(libwasmer_static_name))
    } else {
        Ok(path.join("lib").join(libwasmer_static_name))
    }
}

/// path to library tarball cache dir
fn get_libwasmer_cache_path() -> anyhow::Result<PathBuf> {
    let mut path = get_wasmer_dir()?;
    path.push("cache");
    let _ = std::fs::create_dir(&path);
    Ok(path)
}

/// Compile the C code.
fn run_c_compile(
    path_to_c_src: &Path,
    output_name: &Path,
    target: Option<Triple>,
) -> anyhow::Result<()> {
    #[cfg(not(windows))]
    let c_compiler = "cc";
    // We must use a C++ compiler on Windows because wasm.h uses `static_assert`
    // which isn't available in `clang` on Windows.
    #[cfg(windows)]
    let c_compiler = "clang++";

    let mut command = Command::new(c_compiler);
    let command = command
        .arg("-Wall")
        .arg("-O2")
        .arg("-c")
        .arg(path_to_c_src)
        .arg("-I")
        .arg(get_wasmer_include_directory()?);

    let command = if let Some(target) = target {
        command.arg("-target").arg(format!("{}", target))
    } else {
        command
    };

    let output = command.arg("-o").arg(output_name).output()?;
    eprintln!(
        "run_c_compile: stdout: {}\n\nstderr: {}",
        std::str::from_utf8(&output.stdout)
            .expect("stdout is not utf8! need to handle arbitrary bytes"),
        std::str::from_utf8(&output.stderr)
            .expect("stderr is not utf8! need to handle arbitrary bytes")
    );

    if !output.status.success() {
        bail!(
            "C code compile failed with: stdout: {}\n\nstderr: {}",
            std::str::from_utf8(&output.stdout)
                .expect("stdout is not utf8! need to handle arbitrary bytes"),
            std::str::from_utf8(&output.stderr)
                .expect("stderr is not utf8! need to handle arbitrary bytes")
        );
    }
    Ok(())
}

/// Data used to run a linking command for generated artifacts.
#[derive(Debug)]
struct LinkCode {
    /// Path to the linker used to run the linking command.
    linker_path: PathBuf,
    /// String used as an optimization flag.
    optimization_flag: String,
    /// Paths of objects to link.
    object_paths: Vec<PathBuf>,
    /// Additional libraries to link against.
    additional_libraries: Vec<String>,
    /// Path to the output target.
    output_path: PathBuf,
    /// Path to the dir containing the static libwasmer library.
    libwasmer_path: PathBuf,
    /// The target to link the executable for.
    target: Option<Triple>,
}

impl Default for LinkCode {
    fn default() -> Self {
        #[cfg(not(windows))]
        let linker = "cc";
        #[cfg(windows)]
        let linker = "clang";
        Self {
            linker_path: PathBuf::from(linker),
            optimization_flag: String::from("-O2"),
            object_paths: vec![],
            additional_libraries: vec![],
            output_path: PathBuf::from("a.out"),
            libwasmer_path: get_libwasmer_path().unwrap(),
            target: None,
        }
    }
}

impl LinkCode {
    fn run(&self) -> anyhow::Result<()> {
        let libwasmer_path = self
            .libwasmer_path
            .canonicalize()
            .context("Failed to find libwasmer")?;
        println!(
            "Using path `{}` as libwasmer path.",
            libwasmer_path.display()
        );
        let mut command = Command::new(&self.linker_path);
        let command = command
            .arg("-Wall")
            .arg(&self.optimization_flag)
            .args(
                self.object_paths
                    .iter()
                    .map(|path| path.canonicalize().unwrap()),
            )
            .arg(&libwasmer_path);
        let command = if let Some(target) = &self.target {
            command.arg("-target").arg(format!("{}", target))
        } else {
            command
        };
        // Add libraries required per platform.
        // We need userenv, sockets (Ws2_32), advapi32 for some system calls and bcrypt for random numbers.
        #[cfg(windows)]
        let command = command
            .arg("-luserenv")
            .arg("-lWs2_32")
            .arg("-ladvapi32")
            .arg("-lbcrypt");
        // On unix we need dlopen-related symbols, libmath for a few things, and pthreads.
        #[cfg(not(windows))]
        let command = command.arg("-ldl").arg("-lm").arg("-pthread");
        let link_against_extra_libs = self
            .additional_libraries
            .iter()
            .map(|lib| format!("-l{}", lib));
        let command = command.args(link_against_extra_libs);
        let command = command.arg("-o").arg(&self.output_path);
        let output = command.output()?;

        if !output.status.success() {
            bail!(
                "linking failed with: stdout: {}\n\nstderr: {}",
                std::str::from_utf8(&output.stdout)
                    .expect("stdout is not utf8! need to handle arbitrary bytes"),
                std::str::from_utf8(&output.stderr)
                    .expect("stderr is not utf8! need to handle arbitrary bytes")
            );
        }
        Ok(())
    }
}

mod http_fetch {
    use anyhow::{anyhow, Context, Result};
    use http_req::{request::Request, response::StatusCode, uri::Uri};
    use std::convert::TryFrom;
    use target_lexicon::OperatingSystem;

    pub fn get_latest_release() -> Result<serde_json::Value> {
        let mut writer = Vec::new();
        let uri = Uri::try_from("https://api.github.com/repos/wasmerio/wasmer/releases").unwrap();

        // Increases rate-limiting in GitHub CI
        let auth = std::env::var("GITHUB_TOKEN");
        let mut response = Request::new(&uri);

        if let Ok(token) = auth {
            response.header("Authorization", &format!("Bearer {token}"));
        }

        let response = response
            .header("User-Agent", "wasmerio")
            .header("Accept", "application/vnd.github.v3+json")
            .timeout(Some(std::time::Duration::new(30, 0)))
            .send(&mut writer)
            .map_err(anyhow::Error::new)
            .context("Could not lookup wasmer repository on Github.")?;

        if response.status_code() != StatusCode::new(200) {
            #[cfg(feature = "debug")]
            log::warn!(
                "Warning: Github API replied with non-200 status code: {}. Response: {}",
                response.status_code(),
                String::from_utf8_lossy(&writer),
            );
        }

        let v: std::result::Result<serde_json::Value, _> = serde_json::from_reader(&*writer);
        let mut response = v.map_err(anyhow::Error::new)?;

        if let Some(releases) = response.as_array_mut() {
            releases.retain(|r| {
                r["tag_name"].is_string() && !r["tag_name"].as_str().unwrap().is_empty()
            });
            releases.sort_by_cached_key(|r| r["tag_name"].as_str().unwrap_or_default().to_string());
            if let Some(latest) = releases.pop() {
                return Ok(latest);
            }
        }

        Err(anyhow!(
            "Could not get expected Github API response.\n\nReason: response format is not recognized:\n{response:#?}",
        ))
    }

    pub fn download_release(
        mut release: serde_json::Value,
        target_triple: wasmer::Triple,
    ) -> Result<std::path::PathBuf> {
        let check_arch = |name: &str| -> bool {
            match target_triple.architecture {
                wasmer_types::Architecture::X86_64 => {
                    name.contains("x86_64") || name.contains("amd64")
                }
                wasmer_types::Architecture::Aarch64(wasmer_types::Aarch64Architecture::Aarch64) => {
                    name.contains("arm64") || name.contains("aarch64")
                }
                _ => false,
            }
        };

        let check_vendor = |name: &str| -> bool {
            match target_triple.vendor {
                wasmer_types::Vendor::Apple => {
                    name.contains("apple") || name.contains("macos") || name.contains("darwin")
                }
                wasmer_types::Vendor::Pc => name.contains("windows"),
                _ => true,
            }
        };
        let check_os = |name: &str| -> bool {
            match target_triple.operating_system {
                wasmer_types::OperatingSystem::Darwin => {
                    name.contains("apple") || name.contains("darwin") || name.contains("macos")
                }
                wasmer_types::OperatingSystem::Windows => name.contains("windows"),
                wasmer_types::OperatingSystem::Linux => name.contains("linux"),
                _ => false,
            }
        };
        let check_env = |name: &str| -> bool {
            match target_triple.environment {
                wasmer_types::Environment::Musl => name.contains("musl"),
                _ => !name.contains("musl"),
            }
        };

        // Test if file has been already downloaded
        if let Ok(mut cache_path) = super::get_libwasmer_cache_path() {
            let paths = std::fs::read_dir(&cache_path).and_then(|r| {
                r.map(|res| res.map(|e| e.path()))
                    .collect::<Result<Vec<_>, std::io::Error>>()
            });

            if let Ok(mut entries) = paths {
                entries.retain(|p| p.to_str().map(|p| p.ends_with(".tar.gz")).unwrap_or(false));

                // create-exe on Windows is special: we use the windows-gnu64.tar.gz (GNU ABI)
                // to link, not the windows-amd64.tar.gz (MSVC ABI)
                if target_triple.operating_system == OperatingSystem::Windows {
                    entries.retain(|p| {
                        p.to_str()
                            .map(|p| p.contains("windows") && p.contains("gnu64"))
                            .unwrap_or(false)
                    });
                } else {
                    entries.retain(|p| p.to_str().map(|p| check_arch(p)).unwrap_or(true));
                    entries.retain(|p| p.to_str().map(|p| check_vendor(p)).unwrap_or(true));
                    entries.retain(|p| p.to_str().map(|p| check_os(p)).unwrap_or(true));
                    entries.retain(|p| p.to_str().map(|p| check_env(p)).unwrap_or(true));
                }

                if !entries.is_empty() {
                    cache_path.push(&entries[0]);
                    if cache_path.exists() {
                        eprintln!(
                            "Using cached tarball to cache path `{}`.",
                            cache_path.display()
                        );
                        return Ok(cache_path);
                    }
                }
            }
        }

        let assets = match release["assets"].as_array_mut() {
            Some(s) => s,
            None => {
                return Err(anyhow!(
                    "GitHub API: no [assets] array in JSON response for latest releases"
                ));
            }
        };

        // create-exe on Windows is special: we use the windows-gnu64.tar.gz (GNU ABI)
        // to link, not the windows-amd64.tar.gz (MSVC ABI)
        if target_triple.operating_system == OperatingSystem::Windows {
            assets.retain(|a| {
                if let Some(name) = a["name"].as_str() {
                    name.contains("windows") && name.contains("gnu64")
                } else {
                    false
                }
            });
        } else {
            assets.retain(|a| {
                if let Some(name) = a["name"].as_str() {
                    check_arch(name)
                } else {
                    false
                }
            });
            assets.retain(|a| {
                if let Some(name) = a["name"].as_str() {
                    check_vendor(name)
                } else {
                    false
                }
            });

            assets.retain(|a| {
                if let Some(name) = a["name"].as_str() {
                    check_os(name)
                } else {
                    false
                }
            });

            assets.retain(|a| {
                if let Some(name) = a["name"].as_str() {
                    check_env(name)
                } else {
                    false
                }
            });
        }

        if assets.len() != 1 {
            return Err(anyhow!(
                "GitHub API: more that one release selected for target {target_triple}: {assets:?}"
            ));
        }

        let browser_download_url = if let Some(url) = assets[0]["browser_download_url"].as_str() {
            url.to_string()
        } else {
            return Err(anyhow!(
                "Could not get download url from Github API response."
            ));
        };

        let filename = browser_download_url
            .split('/')
            .last()
            .unwrap_or("output")
            .to_string();

        let download_tempdir = tempdir::TempDir::new("wasmer-download")?;
        let download_path = download_tempdir.path().join(&filename);

        let mut file = std::fs::File::create(&download_path)?;
        #[cfg(feature = "debug")]
        log::debug!(
            "Downloading {} to {}",
            browser_download_url,
            download_path.display()
        );

        let mut response = reqwest::blocking::Client::builder()
            .redirect(reqwest::redirect::Policy::limited(10))
            .timeout(std::time::Duration::from_secs(10))
            .build()
            .map_err(anyhow::Error::new)
            .context("Could not lookup wasmer artifact on Github.")?
            .get(browser_download_url.as_str())
            .send()
            .map_err(anyhow::Error::new)
            .context("Could not lookup wasmer artifact on Github.")?;

        response
            .copy_to(&mut file)
            .map_err(|e| anyhow::anyhow!("{e}"))?;

        match super::get_libwasmer_cache_path() {
            Ok(mut cache_path) => {
                cache_path.push(&filename);
                if let Err(err) = std::fs::copy(&download_path, &cache_path) {
                    eprintln!(
                        "Could not store tarball to cache path `{}`: {}",
                        cache_path.display(),
                        err
                    );
                    Err(anyhow!(
                        "Could not copy from {} to {}",
                        download_path.display(),
                        cache_path.display()
                    ))
                } else {
                    eprintln!("Cached tarball to cache path `{}`.", cache_path.display());
                    Ok(cache_path)
                }
            }
            Err(err) => {
                eprintln!(
                    "Could not determine cache path for downloaded binaries.: {}",
                    err
                );
                Err(anyhow!("Could not determine libwasmer cache path"))
            }
        }
    }
}

fn untar(tarball: std::path::PathBuf, target: std::path::PathBuf) -> Result<Vec<String>> {
    use walkdir::WalkDir;

    wasmer_registry::try_unpack_targz(&tarball, &target, false)?;

    Ok(WalkDir::new(&target)
        .into_iter()
        .filter_map(|e| e.ok())
        .map(|entry| format!("{}", entry.path().display()))
        .collect())
}

fn get_zig_exe_str() -> &'static str {
    #[cfg(target_os = "windows")]
    {
        "zig.exe"
    }
    #[cfg(not(target_os = "windows"))]
    {
        "zig"
    }
}

fn find_zig_binary(path: Option<PathBuf>) -> Result<PathBuf> {
    use std::env::split_paths;
    use std::ffi::OsStr;
    #[cfg(unix)]
    use std::os::unix::ffi::OsStrExt;
    let path_var = std::env::var("PATH").unwrap_or_default();
    #[cfg(unix)]
    let system_path_var = std::process::Command::new("getconf")
        .args(&["PATH"])
        .output()
        .map(|output| output.stdout)
        .unwrap_or_default();
    let retval = if let Some(p) = path {
        if p.exists() {
            p
        } else {
            return Err(anyhow!("Could not find `zig` binary in {}.", p.display()));
        }
    } else {
        let mut retval = None;
        for mut p in split_paths(&path_var).chain(split_paths(
            #[cfg(unix)]
            {
                &OsStr::from_bytes(&system_path_var[..])
            },
            #[cfg(not(unix))]
            {
                OsStr::new("")
            },
        )) {
            p.push(get_zig_exe_str());
            if p.exists() {
                retval = Some(p);
                break;
            }
        }
        retval.ok_or_else(|| anyhow!("Could not find `zig` binary in PATH."))?
    };

    let version = std::process::Command::new(&retval)
        .arg("version")
        .output()
        .with_context(|| {
            format!(
                "Could not execute `zig` binary at path `{}`",
                retval.display()
            )
        })?
        .stdout;
    let version_slice = if let Some(pos) = version
        .iter()
        .position(|c| !(c.is_ascii_digit() || (*c == b'.')))
    {
        &version[..pos]
    } else {
        &version[..]
    };

    if version_slice < b"0.10.0".as_ref() {
        Err(anyhow!("`zig` binary in PATH (`{}`) is not a new enough version (`{}`): please use version `0.10.0` or newer.", retval.display(), String::from_utf8_lossy(version_slice)))
    } else {
        Ok(retval)
    }
}
