1use std::env;
2use std::path::{Path, PathBuf};
3
4pub struct SdlBuilder {
5 link_name: String,
6 lib_name: String,
7 #[cfg(feature = "build-from-source")]
8 source_dir: PathBuf,
9 repo_url: Option<String>,
10 include_dirs: Vec<PathBuf>,
11 cmake_options: Vec<(String, String)>,
12 requires_base_sdl: bool,
13}
14
15impl SdlBuilder {
16 pub fn new(
17 link_name: impl Into<String>,
18 lib_name: impl Into<String>,
19 #[allow(unused_variables)] source_dir: impl AsRef<Path>,
20 ) -> Self {
21 Self {
22 link_name: link_name.into(),
23 lib_name: lib_name.into(),
24 #[cfg(feature = "build-from-source")]
25 source_dir: source_dir.as_ref().to_path_buf(),
26 repo_url: None,
27 include_dirs: vec![PathBuf::from("src/generated/include")],
28 cmake_options: vec![],
29 requires_base_sdl: false,
30 }
31 }
32
33 pub fn with_repo_url(mut self, url: impl Into<String>) -> Self {
34 self.repo_url = Some(url.into());
35 self
36 }
37
38 pub fn include_dir(mut self, dir: impl AsRef<Path>) -> Self {
39 self.include_dirs.push(dir.as_ref().to_path_buf());
40 self
41 }
42
43 pub fn with_cmake_option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
44 self.cmake_options.push((key.into(), value.into()));
45 self
46 }
47
48 pub fn requires_base_sdl(mut self, req: bool) -> Self {
49 self.requires_base_sdl = req;
50 self
51 }
52
53 pub fn build(self) {
54 let manifest_dir = PathBuf::from(
55 env::var("CARGO_MANIFEST_DIR")
56 .map_err(|_| "CARGO_MANIFEST_DIR not set")
57 .unwrap(),
58 );
59 let target = env::var("TARGET").map_err(|_| "TARGET not set").unwrap();
60 let safe_target = target.replace("-", "_");
61
62 let inlines_c = manifest_dir
63 .join("src")
64 .join("generated")
65 .join(format!("inlines_{}.c", safe_target));
66 let include_paths: Vec<PathBuf> = self
67 .include_dirs
68 .iter()
69 .map(|d| manifest_dir.join(d))
70 .collect();
71 let include_refs: Vec<&Path> = include_paths.iter().map(|p| p.as_path()).collect();
72
73 if inlines_c.exists() {
75 compile_inlines(&self.lib_name, &include_refs, &inlines_c);
76 }
77
78 let build_from_source = env::var("CARGO_FEATURE_BUILD_FROM_SOURCE").is_ok();
80 let link_static = env::var("CARGO_FEATURE_LINK_STATIC").is_ok();
81 let use_pkg_config = env::var("CARGO_FEATURE_USE_PKG_CONFIG").is_ok();
82 let use_vcpkg = env::var("CARGO_FEATURE_USE_VCPKG").is_ok();
83
84 if build_from_source {
85 #[cfg(feature = "build-from-source")]
86 self.build_cmake(&manifest_dir, link_static);
87 #[cfg(not(feature = "build-from-source"))]
88 panic!("Feature 'build-from-source' is not enabled but required by environment.");
89 } else if use_pkg_config {
90 #[cfg(feature = "use-pkg-config")]
91 pkg_config::Config::new()
92 .statik(link_static)
93 .probe(&self.link_name)
94 .unwrap_or_else(|e| {
95 panic!("Failed to find {} using pkg-config: {}", self.link_name, e)
96 });
97 #[cfg(not(feature = "use-pkg-config"))]
98 panic!("Feature 'use-pkg-config' is not enabled but required by environment.");
99 } else if use_vcpkg {
100 #[cfg(feature = "use-vcpkg")]
101 vcpkg::Config::new()
102 .find_package(&self.link_name)
103 .unwrap_or_else(|e| panic!("Failed to find {} using vcpkg: {}", self.link_name, e));
104 #[cfg(not(feature = "use-vcpkg"))]
105 panic!("Feature 'use-vcpkg' is not enabled but required by environment.");
106 } else {
107 let kind = if link_static { "static=" } else { "" };
109 println!("cargo:rustc-link-lib={}{}", kind, self.link_name);
110 }
111 }
112
113 #[cfg(feature = "build-from-source")]
114 fn build_cmake(&self, manifest_dir: &Path, link_static: bool) {
115 let link_name_upper = self.link_name.to_uppercase().replace("-", "_");
116 let source_override_var = format!("{}_SOURCE_OVERRIDE", link_name_upper);
117
118 let mut source_path = if let Ok(override_path) = env::var(&source_override_var) {
119 let path = PathBuf::from(override_path);
120 path.canonicalize().unwrap_or(path)
121 } else if self.source_dir.is_absolute() {
122 self.source_dir.clone()
123 } else {
124 manifest_dir.join(&self.source_dir)
125 };
126
127 if !source_path.exists() {
128 source_path = PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("source");
129 if !source_path.exists() {
130 checkout_repo(self, &source_path);
131 }
132 }
133
134 let mut cfg = cmake::Config::new(source_path);
135
136 cfg.define("BUILD_SHARED_LIBS", if link_static { "OFF" } else { "ON" });
137
138 for (k, v) in &self.cmake_options {
139 cfg.define(k, v);
140 }
141
142 let link_name_upper = self.link_name.to_uppercase().replace("-", "_");
143 let cmake_override_var = format!("{}_CMAKE_OVERRIDE", link_name_upper);
144 if let Ok(overrides) = env::var(&cmake_override_var) {
145 for part in overrides.split_whitespace() {
146 let part = part.strip_prefix("-D").unwrap_or(part);
147 if let Some((k, v)) = part.split_once('=') {
148 cfg.define(k, v);
149 }
150 }
151 }
152
153 if self.requires_base_sdl
154 && let Ok(sdl_cmake_dir) = env::var("DEP_SDL3_CMAKE_DIR")
155 {
156 cfg.define("SDL3_DIR", sdl_cmake_dir);
157 } let dst = cfg.build();
160
161 println!(
162 "cargo:rustc-link-search=native={}",
163 dst.join("lib").display()
164 );
165 println!(
166 "cargo:rustc-link-search=native={}",
167 dst.join("lib64").display()
168 );
169
170 let kind = if link_static { "static=" } else { "" };
171
172 if link_static && env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default() == "msvc" {
174 println!("cargo:rustc-link-lib={}{}-static", kind, self.link_name);
175 } else {
176 println!("cargo:rustc-link-lib={}{}", kind, self.link_name);
177 }
178
179 let cmake_dir = find_cmake_dir(&dst, &self.link_name);
181 println!("cargo:cmake_dir={}", cmake_dir.display()); }
183}
184
185#[cfg(feature = "build-from-source")]
186fn checkout_repo(builder: &SdlBuilder, dest: &Path) {
187 let link_name_upper = builder.link_name.to_uppercase();
188 let repo_override_var = format!("{}_REPOSITORY_OVERRIDE", link_name_upper);
189 let branch_override_var = format!("{}_BRANCH_OVERRIDE", link_name_upper);
190
191 let url = env::var(&repo_override_var)
192 .ok()
193 .or_else(|| builder.repo_url.clone())
194 .expect("No repository URL provided and no override found");
195
196 let repo = git2::Repository::clone(&url, dest).expect("Failed to clone repository");
197 let version = env::var("CARGO_PKG_VERSION").unwrap_or_default();
198
199 let default_commit = version.split('-').next_back().unwrap_or("main");
202
203 let commit = env::var(&branch_override_var).unwrap_or_else(|_| default_commit.to_string());
204
205 println!("cargo:info=Checking out commit/branch: {}", commit);
206
207 let (object, reference) = repo.revparse_ext(&commit).unwrap_or_else(|_| {
208 if env::var(&branch_override_var).is_ok() {
209 panic!("Specified branch/commit override '{}' not found", commit);
210 }
211 repo.revparse_ext("main")
213 .or_else(|_| repo.revparse_ext("master"))
214 .expect("Target commit, 'main', or 'master' branch not found")
215 });
216 repo.checkout_tree(&object, None)
217 .expect("Failed to checkout tree");
218
219 match reference {
220 Some(gref) => repo.set_head(gref.name().unwrap()),
221 None => repo.set_head_detached(object.id()),
222 }
223 .expect("Failed to set HEAD");
224}
225
226#[cfg(feature = "build-from-source")]
228fn find_cmake_dir(dst: &Path, link_name: &str) -> PathBuf {
229 let paths = [
230 dst.join("lib").join("cmake").join(link_name),
231 dst.join("lib64").join("cmake").join(link_name),
232 dst.join("cmake"),
233 ];
234 for p in paths {
235 if p.exists() {
236 return p;
237 }
238 }
239 dst.to_path_buf()
240}
241
242pub fn compile_inlines(lib_name: &str, include_dirs: &[&Path], inlines_c: &Path) {
244 println!("cargo:rerun-if-changed={}", inlines_c.display());
245
246 let mut build = cc::Build::new();
247 build.file(inlines_c);
248
249 for dir in include_dirs {
250 println!("cargo:rerun-if-changed={}", dir.display());
251 build.include(dir);
252 }
253
254 build
255 .warnings(false)
256 .compile(&format!("{}_inlines", lib_name));
257}