libpng_src/
lib.rs

1//! Helper Cargo package for compiling [libpng](https://github.com/pnggroup/libpng) into a static C library.
2//!
3//! Meant to be used as build dependency for dufferent `-sys` or `-vendored` packages.
4//! Does not provide directly usable `libpng` functionality or bindings.
5//!
6//! Expected to work for:
7//! - Linux: `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu` (no cross-compilation supported yet)
8//! - Windows: `x86_64-pc-windows-msvc`, `aarch644-pc-windows-msvc` (no cross-compilation supported yet)
9//! - macOS: `x86_64-apple-darwin`, `aarch64-apple-darwin`
10//! - iOS, including simulators (cross-compilation from macOS host): `x86_64-apple-ios`, `aarch64-apple-ios`, `aarch64-apple-ios-sim`
11
12use std::{
13    env::consts::{ARCH as HOST_ARCH, OS as HOST_OS},
14    error::Error,
15    ffi::OsString,
16    fs::{self, copy, create_dir, create_dir_all, remove_dir_all},
17    path::{Path, PathBuf},
18    process::Command,
19    vec::Vec,
20};
21
22/// Version of the `libpng` library
23pub const LIBPNG_VERSION: &str = "1.6.43";
24
25/// Represents result of complete building.
26pub struct Artifacts {
27    /// Artifacts root directory, see [build_all_artifacts](build_all_artifacts) for explanantion.
28    pub root_dir: PathBuf,
29    /// C headers directory, see [build_all_artifacts](build_all_artifacts) for explanantion.
30    pub include_dir: PathBuf,
31    /// Library search directory, see [build_all_artifacts](build_all_artifacts) for explanantion.
32    pub lib_dir: PathBuf,
33    /// Library name for linker.
34    pub link_name: String,
35}
36
37/// Returns the path to the source directory without any modifications.
38///
39/// Use it to generate bindings to the `libpng` if needed.
40/// The directory does not contain 'pnglibconf.h', generated at build time.
41pub fn source_path() -> PathBuf {
42    Path::new(env!("CARGO_MANIFEST_DIR")).join("libpng")
43}
44
45/// Builds all artifacts and aggregates library and include headers in a directory.
46/// Would create working directory if missing.
47/// Would remove previous content of 'build/' and 'libpng/' subdirectories if not empty (see below).
48///
49/// # Example
50/// ```ignore
51/// // 'build.rs' of an another crate
52/// use std::{env::var, path::PathBuf};
53///
54/// use libpng_src::build_artifact;
55///
56/// fn main() {
57///     let target = var("TARGET").unwrap();
58///     let out_dir = var("OUT_DIR").map(PathBuf::from).unwrap();
59///
60///     let artifact_info = build_artifact(&target, &out_dir)
61///         .unwrap();
62///
63///     println!("cargo:rustc-link-search=native={}", artifact_info.lib_dir.to_string_lossy());
64///     println!("cargo:rustc-link-lib=static={}", artifact_info.link_name);
65/// }
66/// ```
67///
68/// # Example with bindgen
69/// ```ignore
70/// use std::{env::var, path::PathBuf};
71/// // 'build.rs' of an another crate
72///
73/// use bindgen;
74///
75/// use libpng_src::build_artifact;
76///
77/// fn main() {
78///     let target = var("TARGET").unwrap();
79///     let out_dir = var("OUT_DIR").map(PathBuf::from).unwrap();
80///
81///     let artifact_info = build_artifact(&target, &out_dir)
82///         .unwrap();
83///
84///     println!("cargo:rustc-link-search=native={}", artifact_info.lib_dir.to_string_lossy());
85///     println!("cargo:rustc-link-lib=static={}", artifact_info.link_name);
86///
87///     let main_header_path = artifact_info.include_dir.join("png.h");
88///
89///     bindgen::builder()
90///         .header(main_header_path.to_string_lossy())
91///         .allowlist_file(main_header_path.to_string_lossy())
92///         .generate()
93///         .unwrap()
94///         .write_to_file(out_dir.join("bindings.rs"))
95///         .unwrap()
96/// }
97/// ```
98///
99/// # File structure
100/// ```text
101/// working_directory/
102///     |->build/  ... Temporary build directory - do not use directly.
103///     └->libpng/ ... Artifact root directory.
104///         |->include/ ... C include headers - generate FFI bindings.
105///         └->lib/ ... Static library - add to link search path.
106/// ```
107pub fn build_artifact(target_str: &str, working_dir: &Path) -> Result<Artifacts, Box<dyn Error>> {
108    let build_dir = working_dir.join("build");
109
110    let library_path = compile_lib(target_str, &build_dir)?;
111    let library_filename = library_path
112        .file_name()
113        .map(|os| os.to_string_lossy())
114        .map(String::from)
115        .unwrap();
116
117    let root_dir = working_dir.join("libpng");
118
119    if root_dir.exists() {
120        remove_dir_all(&root_dir)?;
121    }
122
123    create_dir_all(&root_dir)?;
124
125    let include_dir = root_dir.join("include");
126
127    create_dir(&include_dir)?;
128    copy(source_path().join("png.h"), include_dir.join("png.h"))?;
129    copy(
130        source_path().join("pngconf.h"),
131        include_dir.join("pngconf.h"),
132    )?;
133    copy(
134        build_dir.join("pnglibconf.h"),
135        include_dir.join("pnglibconf.h"),
136    )?;
137
138    let lib_dir = root_dir.join("lib");
139
140    create_dir_all(&lib_dir)?;
141    copy(library_path, lib_dir.join(&library_filename))?;
142    // Cleanup
143    remove_dir_all(build_dir).map_or_else(
144        |_| println!("'libpng-src' cannot clean build directoey"),
145        |f| f,
146    );
147
148    Ok(Artifacts {
149        root_dir,
150        include_dir,
151        lib_dir,
152        link_name: link_name(library_filename),
153    })
154}
155
156/// Statically compiles `libpng` library and returns the path to the compiled artifact.
157/// Should be used when include headers are not needed.
158/// Would create working directory if missing, would remove its previous content if not empty.
159/// # Usage Example
160/// ```ignore
161/// /// 'build.rs' of a consumer crate
162/// use std::{env::var, fs::copy, path::PathBuf};
163///
164/// use libpng_src;
165///
166/// fn main() {
167///     let target = var("TARGET").unwrap();
168///     let out_dir = var("OUT_DIR").map(PathBuf::from).unwrap();
169///     
170///     let lib_path = libpng_src::compile_lib(&target, &out_dir).unwrap();
171///
172///     println!("cargo:rustc-link-search=native={}", lib_path.parent().unwrap().to_string_lossy());
173///     #[cfg(not(target_os = "windows"))]
174///     println!("cargo:rustc-link-lib=static=png16");
175///     #[cfg(target_os = "windows")]
176///     println!("cargo:rustc-link-lib=static=png16_static");
177/// }
178/// ```
179pub fn compile_lib(target_str: &str, working_dir: &Path) -> Result<PathBuf, Box<dyn Error>> {
180    if !allowed_targets_for_host().contains(&target_str) {
181        return Err(format!(
182            "Unsupported target: {target_str}, for host OS: {HOST_OS}, arch: {HOST_ARCH}"
183        )
184        .into());
185    }
186
187    if working_dir.exists() {
188        fs::remove_dir_all(working_dir)?;
189    }
190    fs::create_dir_all(working_dir)?;
191
192    let source_path = source_path();
193
194    let mut cmake_args = cmake_options(target_str)?;
195    cmake_args.push(source_path.into_os_string());
196
197    execute("cmake", &cmake_args, working_dir)?;
198    execute(
199        "cmake",
200        &["--build", ".", "--config", "Release"].map(OsString::from),
201        working_dir,
202    )?;
203
204    artifact_path(working_dir)
205}
206
207fn allowed_targets_for_host() -> Vec<&'static str> {
208    match (HOST_OS, HOST_ARCH) {
209        ("macos", _) => vec![
210            "aarch64-apple-darwin",
211            "x86_64-apple-darwin",
212            "aarch64-apple-ios",
213            "aarch64-apple-ios-sim",
214            "x86_64-apple-ios",
215        ],
216        ("linux", "x86_64") => vec!["x86_64-unknown-linux-gnu"],
217        ("linux", "aarch64") => vec!["aarch64-unknown-linux-gnu"],
218        ("windows", "x86_64") => vec!["x86_64-pc-windows-msvc"],
219        ("windows", "aarch64") => vec!["aarch64-pc-windows-msvc"],
220        _ => vec![],
221    }
222}
223
224fn cmake_options(target_str: &str) -> Result<Vec<OsString>, Box<dyn Error>> {
225    let mut options = common_cmake_options();
226
227    let mut specific_options = match HOST_OS {
228        "macos" => macos_specific_cmake_options(target_str),
229        "windows" => windows_specific_cmake_options(),
230        "linux" => Ok(vec![]),
231        _ => Err(format!("Unsupported host OS: {}", HOST_OS).into()),
232    }?;
233
234    options.append(&mut specific_options);
235
236    Ok(options)
237}
238
239fn common_cmake_options() -> Vec<OsString> {
240    vec![
241        OsString::from("-DPNG_SHARED=OFF"),
242        OsString::from("-DPNG_TESTS=OFF"),
243    ]
244}
245
246fn macos_specific_cmake_options(target_str: &str) -> Result<Vec<OsString>, Box<dyn Error>> {
247    let macos_minimum_vers_str = "-DCMAKE_OSX_DEPLOYMENT_TARGET=12.0";
248    let arm_arch_str = "-DCMAKE_OSX_ARCHITECTURES=arm64";
249    let x86_64_arch_str = "-DCMAKE_OSX_ARCHITECTURES=x86_64";
250    let ios_minimum_vers_str = "-DCMAKE_OSX_DEPLOYMENT_TARGET=15.0";
251    let ios_sysname_str = "-DCMAKE_SYSTEM_NAME=iOS";
252    let ios_sim_sysroot_str = "-DCMAKE_OSX_SYSROOT=iphonesimulator";
253    let no_framework_str = "-DPNG_FRAMEWORK=OFF";
254
255    match target_str {
256        "aarch64-apple-darwin" => Ok(vec![macos_minimum_vers_str, arm_arch_str]),
257        "x86_64-apple-darwin" => Ok(vec![macos_minimum_vers_str, x86_64_arch_str]),
258        "aarch64-apple-ios" => Ok(vec![ios_minimum_vers_str, ios_sysname_str, arm_arch_str]),
259        "aarch64-apple-ios-sim" => Ok(vec![
260            ios_minimum_vers_str,
261            ios_sysname_str,
262            arm_arch_str,
263            ios_sim_sysroot_str,
264        ]),
265        "x86_64-apple-ios" => Ok(vec![
266            ios_minimum_vers_str,
267            ios_sysname_str,
268            x86_64_arch_str,
269            ios_sim_sysroot_str,
270        ]),
271        _ => Err(format!(
272            "Unsupported target: {}, for host OS: {}",
273            target_str, HOST_OS
274        )
275        .into()),
276    }
277    .map(|mut str_vec| {
278        // Don't assemble the framework as it has no sense for Rust
279        str_vec.push(no_framework_str);
280        str_vec
281    })
282    .map(|str_vec| str_vec.into_iter().map(OsString::from).collect())
283}
284
285fn windows_specific_cmake_options() -> Result<Vec<OsString>, Box<dyn Error>> {
286    let zlib_include_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("win-zlib-include");
287    let zlib_lib_path = zlib_include_path.join("zlib.lib");
288
289    let mut include_param = OsString::from("-DZLIB_INCLUDE_DIR=");
290    include_param.push(zlib_include_path);
291
292    let mut lib_param = OsString::from("-DZLIB_LIBRARY=");
293    lib_param.push(zlib_lib_path);
294
295    Ok(vec![include_param, lib_param])
296}
297
298fn execute(command: &str, args: &[OsString], cwd: &Path) -> Result<(), Box<dyn Error>> {
299    let output = Command::new(command).current_dir(cwd).args(args).output()?;
300
301    if !output.status.success() {
302        let message = format!(
303            "Command '{}' failed with status code {}\nError: {}",
304            command,
305            output.status.code().unwrap_or(-1),
306            String::from_utf8_lossy(&output.stderr)
307        );
308        return Err(message.into());
309    }
310
311    let args_vec: Vec<&str> = args
312        .iter()
313        .map(|a| a.to_str().unwrap_or("!error!"))
314        .collect();
315
316    println!("Executed '{} {}' successfully", command, args_vec.join(" "));
317    println!("{}", String::from_utf8_lossy(&output.stdout));
318
319    Ok(())
320}
321
322fn artifact_path(working_dir: &Path) -> Result<PathBuf, Box<dyn Error>> {
323    let filename = match HOST_OS {
324        "windows" => "Release\\libpng16_static.lib",
325        _ => "libpng16.a",
326    };
327
328    let artifact_path = working_dir.join(filename);
329
330    if !artifact_path.exists() {
331        return Err(format!("Artifact not found at path: {}", artifact_path.display()).into());
332    }
333
334    Ok(artifact_path)
335}
336
337fn link_name(file_name: String) -> String {
338    let file_name = file_name.split('.').next().unwrap();
339
340    #[cfg(not(target_os = "windows"))]
341    let file_name = file_name.trim_start_matches("lib");
342
343    file_name.to_string()
344}
345
346#[cfg(test)]
347mod tests;