Skip to main content

sdl_build_helper/
lib.rs

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        // 1. Compile Inlines
74        if inlines_c.exists() {
75            compile_inlines(&self.lib_name, &include_refs, &inlines_c);
76        }
77
78        // 2. Determine Link Strategy
79        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            // Default system link
108            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        } // Otherwise, the user is not building SDL from source, and will let CMake try to find a system installation
158
159        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        // For MSVC static builds, CMake sometimes outputs SDL3-static.lib
173        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        // Export CMAKE_DIR for downstream crates (like SDL_image)
180        let cmake_dir = find_cmake_dir(&dst, &self.link_name);
181        println!("cargo:cmake_dir={}", cmake_dir.display()); // Cargo turns this into DEP_<LIB>_CMAKE_DIR
182    }
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    // Attempt to extract a meaningful commit or tag from the version.
200    // Versions like "0.1.0-abc1234" will use "abc1234".
201    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        // Fallback to main/master if the specific commit is not found and no override was specified
212        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/// Helper to locate the CMake config package output
227#[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
242/// Compile the generated inline-function wrapper C source for the current target.
243pub 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}