use std::path::{Path, PathBuf};
use std::{env, fs};
use downloader::{Download, Downloader};
const MLN_REVISION: &str = "core-9b6325a14e2cf1cc29ab28c1855ad376f1ba4903";
#[derive(PartialEq, Eq, Clone, Copy)]
enum GraphicsRenderingAPI {
Metal,
OpenGL,
Vulkan,
}
impl GraphicsRenderingAPI {
fn from_selected_features() -> Self {
let with_opengl = env::var("CARGO_FEATURE_OPENGL").is_ok();
let with_metal = env::var("CARGO_FEATURE_METAL").is_ok();
let with_vulkan = env::var("CARGO_FEATURE_VULKAN").is_ok();
let is_macos = cfg!(any(target_os = "ios", target_os = "macos"));
match (with_metal, with_vulkan, with_opengl) {
(true, false, false) => Self::Metal,
(false, true, false) => Self::Vulkan,
(false, false, true) => Self::OpenGL,
(false, false, false) => {
if is_macos {
Self::Metal
} else {
Self::Vulkan
}
}
(_, _, _) => {
println!("cargo::warning=Features 'metal', 'opengl', and 'vulkan' are mutually exclusive.");
let default_choice = if with_opengl {
Self::OpenGL
} else if is_macos {
Self::Metal
} else {
Self::Vulkan
};
println!("cargo::warning=Using only '{default_choice}', but this default selection may change in future releases.");
default_choice
}
}
}
}
impl std::fmt::Display for GraphicsRenderingAPI {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Metal => f.write_str("metal"),
Self::OpenGL => f.write_str("opengl"),
Self::Vulkan => f.write_str("vulkan"),
}
}
}
fn download_static(out_dir: &Path, revision: &str) -> (PathBuf, PathBuf) {
let graphics_api = GraphicsRenderingAPI::from_selected_features();
let target = if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
"amalgam-linux-arm64"
} else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
"amalgam-linux-x64"
} else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
"amalgam-macos-arm64"
} else {
panic!(
"unsupported target: only linux and macos are currently supported by maplibre-native"
);
};
let mut tasks = Vec::new();
let lib_filename = format!("libmaplibre-native-core-{target}-{graphics_api}.a");
let library_file = out_dir.join(&lib_filename);
if !library_file.is_file() {
let static_url = format!("https://github.com/maplibre/maplibre-native/releases/download/{revision}/{lib_filename}");
println!("cargo:warning=Downloading precompiled maplibre-native core library from {static_url} into {}", out_dir.display());
tasks.push(Download::new(&static_url));
}
let headers_file = out_dir.join("maplibre-native-headers.tar.gz");
if !headers_file.is_file() {
let headers_url = format!("https://github.com/maplibre/maplibre-native/releases/download/{revision}/maplibre-native-headers.tar.gz");
println!("cargo:warning=Downloading headers for maplibre-native core library from {headers_url} into {}", out_dir.display());
tasks.push(Download::new(&headers_url));
}
fs::create_dir_all(out_dir).expect("Failed to create output directory");
let mut downloader = Downloader::builder()
.download_folder(out_dir)
.parallel_requests(
u16::try_from(tasks.len()).expect("with the number of tasks, this cannot be exceeded"),
)
.build()
.expect("Failed to create downloader");
let downloads = downloader
.download(&tasks)
.expect("Failed to download maplibre-native static lib")
.into_iter();
for download in downloads {
if let Err(err) = download {
panic!("Unexpected error from downloader: {err}");
}
}
(library_file, headers_file)
}
fn extract_headers(headers_from: &Path, headers_to: &Path) {
println!(
"cargo:warning=Extracting headers for maplibre-native core library from {} into {}",
headers_from.display(),
headers_to.display()
);
let headers_file = fs::File::open(headers_from).expect("Failed to open headers file");
let mut tar = flate2::read::GzDecoder::new(headers_file);
if !headers_to.is_dir() {
fs::create_dir_all(headers_to).expect("Failed to create headers directory");
}
let mut archive = tar::Archive::new(&mut tar);
archive.set_overwrite(true);
archive
.unpack(headers_to)
.expect("Failed to extract headers");
}
fn resolve_mln_core(root: &Path) -> (PathBuf, Vec<PathBuf>) {
let out_dir =
PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is not set")).join("maplibre-native");
println!("cargo:rerun-if-env-changed=MLN_CORE_LIBRARY_PATH");
println!("cargo:rerun-if-env-changed=MLN_CORE_LIBRARY_HEADERS_PATH");
let (library_file, headers) =match (env::var_os("MLN_CORE_LIBRARY_PATH"), env::var_os("MLN_CORE_LIBRARY_HEADERS_PATH")) {
(Some(library_path),Some(headers_path)) => (PathBuf::from(library_path), PathBuf::from(headers_path)),
(Some(_), None) => panic!("MLN_CORE_LIBRARY_HEADERS_PATH is not set. To compile from a local library/headers, both MLN_CORE_LIBRARY_PATH and MLN_CORE_LIBRARY_HEADERS_PATH must be set."),
(None, Some(_)) => panic!("MLN_CORE_LIBRARY_PATH is not set. To compile from a local library/headers, both MLN_CORE_LIBRARY_PATH and MLN_CORE_LIBRARY_HEADERS_PATH must be set."),
(None, None) => download_static(&out_dir, MLN_REVISION),
};
assert!(
library_file.is_file(),
"The MLN library at {} must be a file",
library_file.display()
);
assert!(
headers.is_file(),
"The MLN headers at {} must be a zip file containing the headers",
headers.display()
);
let extracted_path = out_dir.join("headers");
extract_headers(&headers, &extracted_path);
let include_dirs = vec![
root.join("include"),
extracted_path
.join("vendor")
.join("maplibre-native-base")
.join("include"),
extracted_path
.join("vendor")
.join("maplibre-native-base")
.join("deps")
.join("geometry.hpp")
.join("include"),
extracted_path
.join("vendor")
.join("maplibre-native-base")
.join("deps")
.join("variant")
.join("include"),
extracted_path.join("include"),
];
(library_file, include_dirs)
}
fn build_bridge(lib_name: &str, include_dirs: &[PathBuf]) {
println!("cargo:rerun-if-changed=src/renderer/bridge.rs");
println!("cargo:rerun-if-changed=include/map_renderer.h");
println!("cargo:rerun-if-changed=include/rust_log_observer.h");
cxx_build::bridge("src/renderer/bridge.rs")
.includes(include_dirs)
.file("src/renderer/bridge.cpp")
.flag_if_supported("-std=c++20")
.compile("maplibre_rust_map_renderer_bindings");
println!("cargo:rustc-link-lib=static={lib_name}");
}
fn build_mln() {
let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let (cpp_root, include_dirs) = resolve_mln_core(&root);
println!(
"cargo:warning=Using precompiled maplibre-native static library from {}",
cpp_root.display()
);
println!(
"cargo:rustc-link-search=native={}",
cpp_root.parent().unwrap().display()
);
if cfg!(target_os = "macos") {
if let Ok(homebrew_prefix) = env::var("HOMEBREW_PREFIX") {
println!("cargo:rustc-link-search=native={homebrew_prefix}/lib");
} else if Path::new("/opt/homebrew").exists() {
println!("cargo:rustc-link-search=native=/opt/homebrew/lib");
} else if Path::new("/usr/local").exists() {
println!("cargo:rustc-link-search=native=/usr/local/lib");
}
println!("cargo:rustc-link-search=native=/usr/lib");
println!("cargo:rustc-link-search=native=/System/Library/Frameworks");
if let Ok(pkgconfig_path) = env::var("PKG_CONFIG_PATH") {
for path in pkgconfig_path.split(':') {
let lib_path = Path::new(path).parent().map(|p| p.join("lib"));
if let Some(lib_path) = lib_path {
if lib_path.exists() {
println!("cargo:rustc-link-search=native={}", lib_path.display());
}
}
}
}
}
let lib_name = cpp_root
.file_name()
.expect("static library base has a file name")
.to_string_lossy()
.to_string()
.replacen("lib", "", 1)
.replace(".a", "");
build_bridge(&lib_name, &include_dirs);
println!("cargo:rustc-link-lib=curl");
println!("cargo:rustc-link-lib=z");
match GraphicsRenderingAPI::from_selected_features() {
GraphicsRenderingAPI::Vulkan => {}
GraphicsRenderingAPI::OpenGL => {
println!("cargo:rustc-link-lib=GL");
println!("cargo:rustc-link-lib=EGL");
}
GraphicsRenderingAPI::Metal => {
println!("cargo:rustc-link-lib=framework=Metal");
println!("cargo:rustc-link-lib=framework=MetalKit");
println!("cargo:rustc-link-lib=framework=QuartzCore");
println!("cargo:rustc-link-lib=framework=Foundation");
println!("cargo:rustc-link-lib=framework=CoreGraphics");
println!("cargo:rustc-link-lib=framework=AppKit");
println!("cargo:rustc-link-lib=framework=CoreLocation");
}
}
}
fn main() {
println!("cargo:rerun-if-env-changed=DOCS_RS");
if env::var("DOCS_RS").is_ok() {
println!("cargo:warning=Skipping build.rs when building for docs.rs");
println!("cargo::rustc-cfg=docsrs");
println!("cargo:rustc-check-cfg=cfg(docsrs)");
} else {
build_mln();
}
}