// build.rs

use std::env;
use std::ffi;
use std::fs;
use std::fs::read_dir;
use std::fs::File;
use std::io;
use std::path;
use std::path::Path;
use std::process;

use nix::fcntl;

fn emit_rerun_directives_for_contents(dir: &Path) {
    for result in read_dir(dir).unwrap() {
        let file = result.unwrap();
        println!("cargo:rerun-if-changed={}", file.path().display());
    }
}

#[cfg(feature = "bindgen")]
fn generate_bindings(src_dir: path::PathBuf) {
    use std::collections::HashSet;

    #[derive(Debug)]
    struct IgnoreMacros(HashSet<&'static str>);

    impl bindgen::callbacks::ParseCallbacks for IgnoreMacros {
        fn will_parse_macro(&self, name: &str) -> bindgen::callbacks::MacroParsingBehavior {
            if self.0.contains(name) {
                bindgen::callbacks::MacroParsingBehavior::Ignore
            } else {
                bindgen::callbacks::MacroParsingBehavior::Default
            }
        }
    }

    let ignored_macros = IgnoreMacros(
        vec![
            "BTF_KIND_FUNC",
            "BTF_KIND_FUNC_PROTO",
            "BTF_KIND_VAR",
            "BTF_KIND_DATASEC",
            "BTF_KIND_FLOAT",
            "BTF_KIND_DECL_TAG",
            "BTF_KIND_TYPE_TAG",
            "BTF_KIND_ENUM64",
        ]
        .into_iter()
        .collect(),
    );

    #[cfg(feature = "bindgen-source")]
    let out_dir = &src_dir.join("src");
    #[cfg(not(feature = "bindgen-source"))]
    let out_dir =
        &path::PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR should always be set"));

    let out_file = out_dir.join("bindings.rs");
    println!("cargo:rerun-if-changed={}", out_file.display());

    bindgen::Builder::default()
        .rust_target(env!("CARGO_PKG_RUST_VERSION").parse().expect("valid"))
        .disable_header_comment()
        .derive_default(true)
        .explicit_padding(true)
        .default_enum_style(bindgen::EnumVariation::Consts)
        .size_t_is_usize(false)
        .prepend_enum_name(false)
        .layout_tests(false)
        .generate_comments(false)
        .emit_builtins()
        .allowlist_function("bpf_.+")
        .allowlist_function("btf_.+")
        .allowlist_function("libbpf_.+")
        .allowlist_function("perf_.+")
        .allowlist_function("ring__.+")
        .allowlist_function("ring_buffer_.+")
        .allowlist_function("user_ring_buffer_.+")
        .allowlist_function("vdprintf")
        .allowlist_type("bpf_.+")
        .allowlist_type("btf_.+")
        .allowlist_type("xdp_.+")
        .allowlist_type("perf_.+")
        .allowlist_var("BPF_.+")
        .allowlist_var("BTF_.+")
        .allowlist_var("XDP_.+")
        .allowlist_var("PERF_.+")
        .parse_callbacks(Box::new(ignored_macros))
        .header("bindings.h")
        .clang_arg(format!("-I{}", src_dir.join("libbpf/include").display()))
        .clang_arg(format!(
            "-I{}",
            src_dir.join("libbpf/include/uapi").display()
        ))
        .generate()
        .expect("Unable to generate bindings")
        .write_to_file(out_file)
        .expect("Couldn't write bindings");
}

#[cfg(not(feature = "bindgen"))]
fn generate_bindings(_: path::PathBuf) {}

fn pkg_check(pkg: &str) {
    if process::Command::new(pkg)
        .stdout(process::Stdio::null())
        .stderr(process::Stdio::null())
        .status()
        .is_err()
    {
        panic!(
            "{} is required to compile libbpf-sys with the selected set of features",
            pkg
        );
    }
}

fn main() {
    let src_dir = path::PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());

    generate_bindings(src_dir.clone());

    let vendored_libbpf = cfg!(feature = "vendored-libbpf");
    let vendored_libelf = cfg!(feature = "vendored-libelf");
    let vendored_zlib = cfg!(feature = "vendored-zlib");
    println!("Using feature vendored-libbpf={}", vendored_libbpf);
    println!("Using feature vendored-libelf={}", vendored_libelf);
    println!("Using feature vendored-zlib={}", vendored_zlib);

    let static_libbpf = cfg!(feature = "static-libbpf");
    let static_libelf = cfg!(feature = "static-libelf");
    let static_zlib = cfg!(feature = "static-zlib");
    println!("Using feature static-libbpf={}", static_libbpf);
    println!("Using feature static-libelf={}", static_libelf);
    println!("Using feature static-zlib={}", static_zlib);

    if cfg!(feature = "novendor") {
        println!("cargo:warning=the `novendor` feature of `libbpf-sys` is deprecated; build without features instead");
        println!(
            "cargo:rustc-link-lib={}bpf",
            if static_libbpf { "static=" } else { "" }
        );
        return;
    }

    let out_dir = path::PathBuf::from(env::var_os("OUT_DIR").unwrap());

    // check for all necessary compilation tools
    if vendored_libelf {
        pkg_check("autoreconf");
        pkg_check("autopoint");
        pkg_check("flex");
        pkg_check("bison");
        pkg_check("gawk");
    }

    let (compiler, mut cflags) = if vendored_libbpf || vendored_libelf || vendored_zlib {
        pkg_check("make");
        pkg_check(
            std::env::var("PKG_CONFIG")
                .as_deref()
                .unwrap_or("pkg-config"),
        );

        let compiler = cc::Build::new().try_get_compiler().expect(
            "a C compiler is required to compile libbpf-sys using the vendored copy of libbpf",
        );
        let mut cflags = compiler.cflags_env();
        println!("cargo:rerun-if-env-changed=LIBBPF_SYS_EXTRA_CFLAGS");
        if let Some(extra_cflags) = env::var_os("LIBBPF_SYS_EXTRA_CFLAGS") {
            cflags.push(" ");
            cflags.push(extra_cflags);
        }
        (Some(compiler), cflags)
    } else {
        (None, ffi::OsString::new())
    };

    if vendored_zlib {
        make_zlib(compiler.as_ref().unwrap(), &src_dir, &out_dir);
        cflags.push(format!(" -I{}/zlib/", src_dir.display()));
    }

    if vendored_libelf {
        make_elfutils(compiler.as_ref().unwrap(), &src_dir, &out_dir);
        cflags.push(format!(" -I{}/elfutils/libelf/", src_dir.display()));
    }

    if vendored_libbpf {
        make_libbpf(compiler.as_ref().unwrap(), &cflags, &src_dir, &out_dir);
    }

    println!(
        "cargo:rustc-link-search=native={}",
        out_dir.to_string_lossy()
    );
    println!(
        "cargo:rustc-link-lib={}elf",
        if static_libelf { "static=" } else { "" }
    );
    println!(
        "cargo:rustc-link-lib={}z",
        if static_zlib { "static=" } else { "" }
    );
    println!(
        "cargo:rustc-link-lib={}bpf",
        if static_libbpf { "static=" } else { "" }
    );
    println!("cargo:include={}/include", out_dir.to_string_lossy());

    let global_lib_path = "LIBBPF_SYS_LIBRARY_PATH";
    let target_lib_path = format!("{}_{}", global_lib_path, env::var("TARGET").unwrap());
    let target_lib_path_underscored = format!(
        "{}_{}",
        global_lib_path,
        env::var("TARGET").unwrap().replace('-', "_")
    );

    println!("cargo:rerun-if-env-changed={}", global_lib_path);
    println!("cargo:rerun-if-env-changed={}", target_lib_path);
    println!("cargo:rerun-if-env-changed={}", target_lib_path_underscored);

    let lib_paths = vec![
        env::var(target_lib_path),
        env::var(target_lib_path_underscored),
        env::var(global_lib_path),
    ];

    for lib_path in lib_paths {
        if let Ok(lib_path) = lib_path {
            for path in lib_path.split(':') {
                if !path.is_empty() {
                    println!("cargo:rustc-link-search=native={}", path);
                }
            }
        }
    }
}

fn open_lockable(path: &Path) -> io::Result<File> {
    let result = File::options()
        .read(true)
        // Open with write permissions because flock(2) may require them
        // on some platforms.
        .write(true)
        .open(path);
    match result {
        Ok(file) => Ok(file),
        Err(err) if err.kind() == io::ErrorKind::PermissionDenied => {
            // On a read-only file system we may not be able to open
            // with write permissions. So just open for reading and hope
            // for the best.
            File::open(path)
        }
        e @ Err(..) => e,
    }
}

fn make_zlib(compiler: &cc::Tool, src_dir: &path::Path, out_dir: &path::Path) {
    let src_dir = src_dir.join("zlib");
    // lock README such that if two crates are trying to compile
    // this at the same time (eg libbpf-rs libbpf-cargo)
    // they wont trample each other
    let file = open_lockable(&src_dir.join("README")).unwrap();
    let _lock = fcntl::Flock::lock(file, fcntl::FlockArg::LockExclusive).unwrap();

    let status = process::Command::new("./configure")
        .arg("--static")
        .arg("--prefix")
        .arg(".")
        .arg("--libdir")
        .arg(out_dir)
        .env("CC", compiler.path())
        .env("CFLAGS", compiler.cflags_env())
        .current_dir(&src_dir)
        .status()
        .expect("could not execute make");

    assert!(status.success(), "make failed");

    let status = process::Command::new("make")
        .arg("install")
        .arg("-j")
        .arg(format!("{}", num_cpus()))
        .current_dir(&src_dir)
        .status()
        .expect("could not execute make");

    assert!(status.success(), "make failed");

    let status = process::Command::new("make")
        .arg("distclean")
        .current_dir(&src_dir)
        .status()
        .expect("could not execute make");

    assert!(status.success(), "make failed");
    emit_rerun_directives_for_contents(&src_dir);
}

fn make_elfutils(compiler: &cc::Tool, src_dir: &path::Path, out_dir: &path::Path) {
    // lock README such that if two crates are trying to compile
    // this at the same time (eg libbpf-rs libbpf-cargo)
    // they wont trample each other
    let file = open_lockable(&src_dir.join("elfutils/README")).unwrap();
    let _lock = fcntl::Flock::lock(file, fcntl::FlockArg::LockExclusive).unwrap();

    let flags = compiler
        .cflags_env()
        .into_string()
        .expect("failed to get cflags");
    let mut cflags: String = flags
        .split_whitespace()
        .filter_map(|arg| {
            if arg != "-static" {
                // compilation fails with -static flag
                Some(format!(" {arg}"))
            } else {
                None
            }
        })
        .collect();

    #[cfg(target_arch = "aarch64")]
    cflags.push_str(" -Wno-error=stringop-overflow");
    cflags.push_str(&format!(" -I{}/zlib/", src_dir.display()));

    let status = process::Command::new("autoreconf")
        .arg("--install")
        .arg("--force")
        .current_dir(src_dir.join("elfutils"))
        .status()
        .expect("could not execute make");

    assert!(status.success(), "make failed");

    // location of libz.a
    let out_lib = format!("-L{}", out_dir.display());
    let status = process::Command::new("./configure")
        .arg("--enable-maintainer-mode")
        .arg("--disable-debuginfod")
        .arg("--disable-libdebuginfod")
        .arg("--disable-demangler")
        .arg("--without-zstd")
        .arg("--prefix")
        .arg(src_dir.join("elfutils/prefix_dir"))
        .arg("--host")
        .arg({
            let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
            let arch = match arch.as_str() {
                "riscv64gc" => "riscv64",
                "riscv32gc" => "riscv32",
                other => other,
            };
            let vendor = env::var("CARGO_CFG_TARGET_VENDOR").unwrap();
            let env = env::var("CARGO_CFG_TARGET_ENV").unwrap();
            let os = env::var("CARGO_CFG_TARGET_OS").unwrap();
            format!("{arch}-{vendor}-{os}-{env}")
        })
        .arg("--libdir")
        .arg(out_dir)
        .env("CC", compiler.path())
        .env("CXX", compiler.path())
        .env("CFLAGS", &cflags)
        .env("CXXFLAGS", &cflags)
        .env("LDFLAGS", &out_lib)
        .current_dir(src_dir.join("elfutils"))
        .status()
        .expect("could not execute make");

    assert!(status.success(), "make failed");

    // Build in elfutils/lib because building libelf requires it.
    let status = process::Command::new("make")
        .arg("-j")
        .arg(format!("{}", num_cpus()))
        .arg("BUILD_STATIC_ONLY=y")
        .current_dir(src_dir.join("elfutils/lib"))
        .status()
        .expect("could not execute make");

    assert!(status.success(), "make failed");

    // Build libelf only
    let status = process::Command::new("make")
        .arg("install")
        .arg("-j")
        .arg(format!("{}", num_cpus()))
        .arg("BUILD_STATIC_ONLY=y")
        .current_dir(src_dir.join("elfutils/libelf"))
        .status()
        .expect("could not execute make");

    assert!(status.success(), "make failed");

    let status = process::Command::new("make")
        .arg("distclean")
        .current_dir(src_dir.join("elfutils"))
        .status()
        .expect("could not execute make");

    assert!(status.success(), "make failed");
    emit_rerun_directives_for_contents(&src_dir.join("elfutils").join("src"));
}

fn make_libbpf(
    compiler: &cc::Tool,
    cflags: &ffi::OsStr,
    src_dir: &path::Path,
    out_dir: &path::Path,
) {
    let src_dir = src_dir.join("libbpf/src");
    // create obj_dir if it doesn't exist
    let obj_dir = path::PathBuf::from(&out_dir.join("obj").into_os_string());
    let _ = fs::create_dir(&obj_dir);

    let status = process::Command::new("make")
        .arg("install")
        .arg("-j")
        .arg(format!("{}", num_cpus()))
        .env("BUILD_STATIC_ONLY", "y")
        .env("PREFIX", "/")
        .env("LIBDIR", "")
        .env("OBJDIR", &obj_dir)
        .env("DESTDIR", out_dir)
        .env("CC", compiler.path())
        .env("CFLAGS", cflags)
        .current_dir(&src_dir)
        .status()
        .expect("could not execute make");

    assert!(status.success(), "make failed");

    let status = process::Command::new("make")
        .arg("clean")
        .current_dir(&src_dir)
        .status()
        .expect("could not execute make");

    assert!(status.success(), "make failed");
    emit_rerun_directives_for_contents(&src_dir);
}

fn num_cpus() -> usize {
    std::thread::available_parallelism().map_or(1, |count| count.get())
}
