1use 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
22pub const LIBPNG_VERSION: &str = "1.6.43";
24
25pub struct Artifacts {
27 pub root_dir: PathBuf,
29 pub include_dir: PathBuf,
31 pub lib_dir: PathBuf,
33 pub link_name: String,
35}
36
37pub fn source_path() -> PathBuf {
42 Path::new(env!("CARGO_MANIFEST_DIR")).join("libpng")
43}
44
45pub 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 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
156pub 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 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;