use std::{
env::consts::{ARCH as HOST_ARCH, OS as HOST_OS},
error::Error,
ffi::OsString,
fs::{self, copy, create_dir, create_dir_all, remove_dir_all},
path::{Path, PathBuf},
process::Command,
vec::Vec,
};
pub const LIBPNG_VERSION: &str = "1.6.43";
pub struct Artifacts {
pub root_dir: PathBuf,
pub include_dir: PathBuf,
pub lib_dir: PathBuf,
pub link_name: String,
}
pub fn source_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("libpng")
}
pub fn build_artifact(target_str: &str, working_dir: &Path) -> Result<Artifacts, Box<dyn Error>> {
let build_dir = working_dir.join("build");
let library_path = compile_lib(target_str, &build_dir)?;
let library_filename = library_path
.file_name()
.map(|os| os.to_string_lossy())
.map(String::from)
.unwrap();
let root_dir = working_dir.join("libpng");
if root_dir.exists() {
remove_dir_all(&root_dir)?;
}
create_dir_all(&root_dir)?;
let include_dir = root_dir.join("include");
create_dir(&include_dir)?;
copy(source_path().join("png.h"), include_dir.join("png.h"))?;
copy(
source_path().join("pngconf.h"),
include_dir.join("pngconf.h"),
)?;
copy(
build_dir.join("pnglibconf.h"),
include_dir.join("pnglibconf.h"),
)?;
let lib_dir = root_dir.join("lib");
create_dir_all(&lib_dir)?;
copy(library_path, lib_dir.join(&library_filename))?;
remove_dir_all(build_dir).map_or_else(
|_| println!("'libpng-src' cannot clean build directoey"),
|f| f,
);
Ok(Artifacts {
root_dir,
include_dir,
lib_dir,
link_name: link_name(library_filename),
})
}
pub fn compile_lib(target_str: &str, working_dir: &Path) -> Result<PathBuf, Box<dyn Error>> {
if !allowed_targets_for_host().contains(&target_str) {
return Err(format!(
"Unsupported target: {target_str}, for host OS: {HOST_OS}, arch: {HOST_ARCH}"
)
.into());
}
if working_dir.exists() {
fs::remove_dir_all(working_dir)?;
}
fs::create_dir_all(working_dir)?;
let source_path = source_path();
let mut cmake_args = cmake_options(target_str)?;
cmake_args.push(source_path.into_os_string());
execute("cmake", &cmake_args, working_dir)?;
execute(
"cmake",
&["--build", ".", "--config", "Release"].map(OsString::from),
working_dir,
)?;
artifact_path(working_dir)
}
fn allowed_targets_for_host() -> Vec<&'static str> {
match (HOST_OS, HOST_ARCH) {
("macos", _) => vec![
"aarch64-apple-darwin",
"x86_64-apple-darwin",
"aarch64-apple-ios",
"aarch64-apple-ios-sim",
"x86_64-apple-ios",
],
("linux", "x86_64") => vec!["x86_64-unknown-linux-gnu"],
("linux", "aarch64") => vec!["aarch64-unknown-linux-gnu"],
("windows", "x86_64") => vec!["x86_64-pc-windows-msvc"],
("windows", "aarch64") => vec!["aarch64-pc-windows-msvc"],
_ => vec![],
}
}
fn cmake_options(target_str: &str) -> Result<Vec<OsString>, Box<dyn Error>> {
let mut options = common_cmake_options();
let mut specific_options = match HOST_OS {
"macos" => macos_specific_cmake_options(target_str),
"windows" => windows_specific_cmake_options(),
"linux" => Ok(vec![]),
_ => Err(format!("Unsupported host OS: {}", HOST_OS).into()),
}?;
options.append(&mut specific_options);
Ok(options)
}
fn common_cmake_options() -> Vec<OsString> {
vec![
OsString::from("-DPNG_SHARED=OFF"),
OsString::from("-DPNG_TESTS=OFF"),
]
}
fn macos_specific_cmake_options(target_str: &str) -> Result<Vec<OsString>, Box<dyn Error>> {
let macos_minimum_vers_str = "-DCMAKE_OSX_DEPLOYMENT_TARGET=12.0";
let arm_arch_str = "-DCMAKE_OSX_ARCHITECTURES=arm64";
let x86_64_arch_str = "-DCMAKE_OSX_ARCHITECTURES=x86_64";
let ios_minimum_vers_str = "-DCMAKE_OSX_DEPLOYMENT_TARGET=15.0";
let ios_sysname_str = "-DCMAKE_SYSTEM_NAME=iOS";
let ios_sim_sysroot_str = "-DCMAKE_OSX_SYSROOT=iphonesimulator";
let no_framework_str = "-DPNG_FRAMEWORK=OFF";
match target_str {
"aarch64-apple-darwin" => Ok(vec![macos_minimum_vers_str, arm_arch_str]),
"x86_64-apple-darwin" => Ok(vec![macos_minimum_vers_str, x86_64_arch_str]),
"aarch64-apple-ios" => Ok(vec![ios_minimum_vers_str, ios_sysname_str, arm_arch_str]),
"aarch64-apple-ios-sim" => Ok(vec![
ios_minimum_vers_str,
ios_sysname_str,
arm_arch_str,
ios_sim_sysroot_str,
]),
"x86_64-apple-ios" => Ok(vec![
ios_minimum_vers_str,
ios_sysname_str,
x86_64_arch_str,
ios_sim_sysroot_str,
]),
_ => Err(format!(
"Unsupported target: {}, for host OS: {}",
target_str, HOST_OS
)
.into()),
}
.map(|mut str_vec| {
str_vec.push(no_framework_str);
str_vec
})
.map(|str_vec| str_vec.into_iter().map(OsString::from).collect())
}
fn windows_specific_cmake_options() -> Result<Vec<OsString>, Box<dyn Error>> {
let zlib_include_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("win-zlib-include");
let zlib_lib_path = zlib_include_path.join("zlib.lib");
let mut include_param = OsString::from("-DZLIB_INCLUDE_DIR=");
include_param.push(zlib_include_path);
let mut lib_param = OsString::from("-DZLIB_LIBRARY=");
lib_param.push(zlib_lib_path);
Ok(vec![include_param, lib_param])
}
fn execute(command: &str, args: &[OsString], cwd: &Path) -> Result<(), Box<dyn Error>> {
let output = Command::new(command).current_dir(cwd).args(args).output()?;
if !output.status.success() {
let message = format!(
"Command '{}' failed with status code {}\nError: {}",
command,
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stderr)
);
return Err(message.into());
}
let args_vec: Vec<&str> = args
.iter()
.map(|a| a.to_str().unwrap_or("!error!"))
.collect();
println!("Executed '{} {}' successfully", command, args_vec.join(" "));
println!("{}", String::from_utf8_lossy(&output.stdout));
Ok(())
}
fn artifact_path(working_dir: &Path) -> Result<PathBuf, Box<dyn Error>> {
let filename = match HOST_OS {
"windows" => "Release\\libpng16_static.lib",
_ => "libpng16.a",
};
let artifact_path = working_dir.join(filename);
if !artifact_path.exists() {
return Err(format!("Artifact not found at path: {}", artifact_path.display()).into());
}
Ok(artifact_path)
}
fn link_name(file_name: String) -> String {
let file_name = file_name.split('.').next().unwrap();
#[cfg(not(target_os = "windows"))]
let file_name = file_name.trim_start_matches("lib");
file_name.to_string()
}
#[cfg(test)]
mod tests;