zig-std 0.1.1

Bindings to Zig `std`, with optional Rust `std` features
Documentation
use build_rs::input::{ profile, out_dir };
use build_rs::output::{
	rerun_if_changed,
	rustc_link_lib_kind,
	rustc_link_search_kind
};
use semver::{ Version, VersionReq };
use std::process::Command;

fn main() {
	// don't do any of this when running in docs.rs
	// https://docs.rs/about/builds#detecting-docsrs
	if std::env::var("DOCS_RS").is_ok() { return }

	let version_req = "0.17.0";
	let version_req = VersionReq::parse(version_req).unwrap();

	// todo skip this when cargo check or cargo doc is run
	// waiting on this to actually be possible https://github.com/rust-lang/cargo/issues/4001
	// lmao https://github.com/rust-lang/cargo/pull/10126
	// i guess this will take a while, until then we will suffer
	// having to wait for zig to compile when it's not needed

	// would directly pathbuf.into_string(), except the msrv
	let out_dir = out_dir().into_os_string().into_string().unwrap();

	let mut out_dir_cache = out_dir.clone();
	out_dir_cache.push_str("/cache");

	rerun_if_changed("src");

	let version = Command::new("zig")
		.arg("version")
		.output()
		.unwrap_or_else(|err| panic!("error fetching version of zig: {err}"));

	let version = String::from_utf8(version.stdout).unwrap();
	let version = Version::parse(version.trim_ascii()).unwrap();
	assert!(version_req.matches(&version), "{version} doesn't satisfy {version_req}");

	let status = Command::new("zig")
		.arg("build")
		.arg("lib")
		.arg("install")
		.arg(get_opt_level_arg())
		.arg("--cache-dir")
		.arg(&*out_dir_cache)
		.arg("--prefix")
		.arg(&*out_dir)
		.status()
		.unwrap_or_else(|err| panic!("failed to build zig: {err:?}"));

	if !status.success() {
		panic!("failed to build zig: zig exited with {status}");
	};

	rustc_link_search_kind("native", &*format!("{out_dir}/lib"));
	rustc_link_lib_kind("static:+whole-archive", "zig-std-rs");
}

fn get_opt_level_arg() -> &'static str {
	let debug = "-Doptimize=debug";
	let fast = "-Doptimize=fast";
	let safe = "-Doptimize=safe";
	let small = "-Doptimize=small";

	macro_rules! declare_features {
		{
			$($feature:ident $feature_str:literal $zig_str:ident)*
		} => {
			$(
				let $feature = (cfg!(feature = $feature_str), $zig_str);
			)*
		}
	}

	declare_features! {
		o_debug "o-debug" debug
		o_fast "o-fast" fast
		o_safe "o-safe" safe
		o_small "o-small" small

		o_debug_debug "o-debug-debug" debug
		o_debug_fast "o-debug-fast" fast
		o_debug_safe "o-debug-safe" safe
		o_debug_small "o-debug-small" small

		o_release_debug "o-release-debug" debug
		o_release_fast "o-release-fast" fast
		o_release_safe "o-release-safe" safe
		o_release_small "o-release-small" small
	}

	fn count(array: [(bool, &'static str); 4]) -> usize {
		array
			.into_iter()
			.filter(|(b, _)| *b)
			.count()
	}

	fn find(array: [(bool, &'static str); 4]) -> &'static str {
		let (_, s) = array
			.into_iter()
			.find(|(b, _)| *b)
			.unwrap();

		s
	}

	let opt = [o_debug, o_fast, o_safe, o_small];
	let debug_opt = [o_debug_debug, o_debug_fast, o_debug_safe, o_debug_small];
	let release_opt = [o_release_debug, o_release_fast, o_release_safe, o_release_small];

	let opt_count = count(opt);
	let debug_opt_count = count(debug_opt);
	let release_opt_count = count(release_opt);

	if opt_count == 1 && debug_opt_count == 0 && release_opt_count == 0 {
		return find(opt);
	}

	let profile = profile();
	macro_rules! profile_debug { () => ("debug") }
	macro_rules! profile_release { () => ("release") }

	if opt_count == 0 && debug_opt_count <= 1 && release_opt_count <= 1 {
		match &*profile {
			profile_debug!() if debug_opt_count == 1 => { return find(debug_opt) }
			profile_debug!() if debug_opt_count == 0 => { return debug }

			profile_release!() if release_opt_count == 1 => { return find(release_opt) }
			profile_release!() if release_opt_count == 0 => { return fast }

			_ => {}
		}
	}

	panic!("invalid combination of optimisation features enabled")
}