cppmm_build/
lib.rs

1use regex::Regex;
2use std::path::Path;
3
4/// Build a packaged dependency that is stored in directory `name` under
5/// `thirdparty` in the project tree, e.g. `thirdparty/zlib`.
6///
7pub fn build_thirdparty(
8    name: &str,
9    target_dir: &Path,
10    profile: &str,
11    definitions: &[(&str, &str)],
12) -> String {
13    // We need to create a dedicated subdirectory for the build or cmake will
14    // wipe it every time, forcing a rebuild
15    let out_dir = target_dir.join(&format!("build-{}", name));
16    match std::fs::create_dir(&out_dir) {
17        Ok(_) => (),
18        Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => (),
19        Err(e) => panic!(
20            "Could not create build directory '{}': {}",
21            out_dir.display(),
22            e
23        ),
24    }
25
26    let mut config = cmake::Config::new(&format!("thirdparty/{}", name));
27    config.profile(profile);
28    config.define("CMAKE_INSTALL_PREFIX", target_dir.to_str().unwrap());
29    config.define("CMAKE_PREFIX_PATH", target_dir.join("lib").join("cmake"));
30    config.out_dir(&out_dir);
31
32    for def in definitions {
33        config.define(def.0, def.1);
34    }
35
36    config
37        .build()
38        .to_str()
39        .expect(&format!("Unable to convert {} dst to str", name))
40        .to_string()
41}
42
43/// Path information for a linked library.
44///
45/// For a `path` '/home/libs/libmylib.so`, `basename` will be `mylib` and `libname`
46/// will be `libmylib.so`
47///
48#[derive(Debug)]
49pub struct DylibPathInfo {
50    pub path: String,
51    pub basename: String,
52    pub libname: String,
53}
54
55#[derive(Debug)]
56pub enum LinkArg {
57    LinkDir(String),
58    LinkLib(String),
59    Path(DylibPathInfo),
60}
61
62#[cfg(not(target_os = "windows"))]
63fn is_dylib_path(s: &str, re: &Regex) -> Option<LinkArg> {
64    if let Ok(_) = std::env::var("CPPMM_DEBUG_BUILD") {
65        println!("cargo:warning=- {}", s);
66    }
67    
68    if let Some(pos @ 0) = s.find("-l") {
69        return Some(LinkArg::LinkLib(s[2..].to_string()))
70    } else if let Some(pos @ 0) = s.find("-L") {
71        if let Ok(_) = std::env::var("CPPMM_DEBUG_BUILD") {
72            println!("cargo:warning=    is a link dir {}", s);
73        }
74        return Some(LinkArg::LinkDir(s[2..].to_string()))
75    } else if let Some(m) = re.captures_iter(s).next() {
76        if let Some(c0) = m.get(0) {
77            if let Some(c1) = m.get(1) {
78                if let Ok(_) = std::env::var("CPPMM_DEBUG_BUILD") {
79                    println!("cargo:warning=    is a dylib path {}", s);
80                }
81                return Some(LinkArg::Path(DylibPathInfo {
82                    path: s.to_string(),
83                    basename: c0.as_str().to_string(),
84                    libname: c1.as_str().to_string(),
85                }));
86            }
87        }
88    }
89    if let Ok(_) = std::env::var("CPPMM_DEBUG_BUILD") {
90        println!("cargo:warning=    is not a dylib path");
91    }
92
93    None
94}
95
96#[cfg(target_os = "windows")]
97fn is_dll_lib_path(s: &str, re: &Regex) -> Option<LinkArg> {
98    if let Some(m) = re.captures_iter(s).next() {
99        if let Some(c0) = m.get(0) {
100            if let Some(c1) = m.get(1) {
101                return Some(LinkArg::Path(DylibPathInfo {
102                    path: s.to_string(),
103                    basename: c0.as_str().to_string(),
104                    libname: c1.as_str().to_string(),
105                }));
106            }
107        }
108    }
109
110    None
111}
112
113#[cfg(target_os = "windows")]
114fn get_linking_from_vsproj(
115    build_path: &Path,
116    clib_versioned_name: &str,
117    build_type: &str,
118) -> Option<Vec<LinkArg>> {
119    use quick_xml::events::{BytesEnd, BytesStart, Event};
120    use quick_xml::Reader;
121    use std::borrow::Borrow;
122    use std::io::Cursor;
123    use std::iter;
124
125    let proj_path = build_path.join(format!("{}.vcxproj", clib_versioned_name));
126    let proj_xml = std::fs::read_to_string(&proj_path).ok()?;
127
128    let re = Regex::new(r"(?:.*\\(.*))(\.lib)$").unwrap();
129
130    let mut reader = Reader::from_str(&proj_xml);
131    reader.trim_text(true);
132
133    let mut in_item_definition = false;
134    let mut in_link = false;
135    let mut in_deps = false;
136
137    let mut buf = Vec::new();
138
139    loop {
140        match reader.read_event(&mut buf) {
141            Ok(Event::Start(ref e)) => match e.name() {
142                b"ItemDefinitionGroup" => {
143                    for attr in e.attributes() {
144                        if let Ok(attr) = attr {
145                            if attr.key == b"Condition" {
146                                let s =
147                                    std::str::from_utf8(attr.value.borrow())
148                                        .unwrap();
149                                if s.contains(build_type) {
150                                    in_item_definition = true;
151                                }
152                            }
153                        }
154                    }
155                }
156                b"Link" if in_item_definition => {
157                    in_link = true;
158                }
159                b"AdditionalDependencies" if in_item_definition && in_link => {
160                    in_deps = true;
161                }
162                _ => (),
163            },
164            Ok(Event::End(ref e)) => match e.name() {
165                b"ItemDefinitionGroup" => {
166                    in_item_definition = false;
167                }
168                b"Link" => {
169                    in_link = false;
170                }
171                b"AdditionalDependencies" => in_deps = false,
172                _ => (),
173            },
174            Ok(Event::Text(e)) if in_deps => {
175                let mut dlls = Vec::new();
176                for tok in e.unescape_and_decode(&reader).unwrap().split(";") {
177                    if let Some(dll) = is_dll_lib_path(tok, &re) {
178                        dlls.push(dll)
179                    }
180                }
181                return Some(dlls);
182            }
183            Ok(Event::Eof) => break,
184            Err(e) => panic!("Error parsing vsproj xml"),
185            _ => (),
186        }
187    }
188
189    None
190}
191
192#[cfg(target_os = "windows")]
193fn get_linking_from_nmake(
194    build_path: &Path,
195    clib_versioned_name: &str,
196) -> Option<Vec<LinkArg>> {
197    let build_make_path = build_path
198        .join("CMakeFiles")
199        .join(format!("{}-shared.dir", clib_versioned_name))
200        .join("build.make");
201
202    let build_make = std::fs::read_to_string(&build_make_path).ok()?;
203
204    let re = Regex::new(r"(?:.*\\(.*))(\.lib)$").unwrap();
205
206    let mut found_slash_dll = false;
207    let mut libs = Vec::new();
208    // println!("cargo:warning=Found links:");
209    for tok in build_make.split_whitespace() {
210        if tok == "/dll" {
211            found_slash_dll = true;
212        } else if found_slash_dll {
213            if tok == "<<" {
214                break;
215            } else {
216                if let Some(dlp) = is_dll_lib_path(tok, &re) {
217                    libs.push(dlp);
218                }
219            }
220        }
221    }
222
223    Some(libs)
224}
225
226#[cfg(target_os = "windows")]
227/// Parse the generated project files from our C wrapper in order to get its 
228/// set of linker arguments.
229///
230/// On Unices this will parse CMake's auxiliary link.txt file for `.so`s or 
231/// `.dylib`s. On Windows this will parse NMake or VS XML project files.
232///
233pub fn get_linking_from_cmake(
234    build_path: &Path,
235    clib_versioned_name: &str,
236    build_type: &str,
237) -> Vec<LinkArg> {
238    if let Some(libs) =
239        get_linking_from_vsproj(build_path, clib_versioned_name, build_type)
240    {
241        libs
242    } else if let Some(libs) =
243        get_linking_from_nmake(build_path, clib_versioned_name)
244    {
245        libs
246    } else {
247        panic!("Could not open either vsproj or nmake build");
248    }
249}
250
251#[cfg(not(target_os = "windows"))]
252pub fn get_linking_from_cmake(
253    build_path: &Path,
254    clib_versioned_name: &str,
255    _build_type: &str,
256) -> Vec<LinkArg> {
257    let link_txt_path = build_path
258        .join("CMakeFiles")
259        .join(format!("{}.dir", clib_versioned_name))
260        .join("link.txt");
261    let link_txt = std::fs::read_to_string(&link_txt_path).expect(&format!(
262        "Could not read link_txt_path: {}",
263        link_txt_path.display()
264    ));
265
266    if let Ok(_) = std::env::var("CPPMM_DEBUG_BUILD") {
267        println!("cargo:warning=Reading link.txt {}", link_txt);
268    }
269
270    let re = Regex::new(
271        r"lib([^/]+?)(?:\.dylib|\.so|\.so.\d+|\.so.\d+.\d+|\.so.\d+.\d+.\d+)$",
272    )
273    .unwrap();
274
275    // Try and figure out what are libraries we want to copy to target.
276    // Libraries will end with `.so` or `.so.28.1.0` or `.dylib`
277
278    // First, strip off everything up to and including the initial "-o whatever.so"
279    let mut link_txt = link_txt.split(' ');
280    while let Some(s) = link_txt.next() {
281        if s == "-o" {
282            // pop off the output lib as well
283            let _ = link_txt.next();
284            break;
285        }
286    }
287
288    // Now match all the remaining arguments against a regex looking for
289    // shared library paths.
290    link_txt.filter_map(|s| is_dylib_path(s, &re)).collect()
291}
292
293pub struct Dependency {
294    pub name: &'static str,
295    pub definitions: Vec<(&'static str, &'static str)>,
296}
297
298use std::fmt;
299impl fmt::Debug for Dependency {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        write!(f, "{}", self.name)
302    }
303}
304
305/// Build a standard-formatted cppmm c wrapper project and its dependencies.
306///
307/// If the environment variable `CMAKE_PREFIX_PATH` is set, any `dependencies`
308/// will be assumed to be present on the system, available in `CMAKE_PREFIX_PATH`.
309/// If `CMAKE_PREFIX_PATH` is not set, the list of dependencies will be built
310/// from the `thirdparty` directory.
311///
312/// `project_name` controls the name of the generated C library, as well as the 
313/// names of environment variables the user can set to control the build. For
314/// example, setting `project_name` to `openexr` will cause the script to respond
315/// to:
316/// * `CPPMM_OPENEXR_BUILD_LIBRARIES` - Ignore `CMAKE_PREFIX_PATH` and force  
317/// building the dependencies if this is set to "1".
318/// * `CPPMM_OPENEXR_BUILD_TYPE` - Set the build profile used for the C library 
319/// and all dependencies. This defaults to "Release" so you can use this to set 
320/// it to "Debug", for example.
321///
322/// `major_version` and `minor_version` are the crate version numbers and are 
323/// baked into the C library filename.
324///
325pub fn build(project_name: &str, major_version: u32, minor_version: u32, dependencies: &[Dependency]) {
326
327    let env_build_libraries = format!("CPPMM_{}_BUILD_LIBRARIES", project_name.to_ascii_uppercase());
328    let env_build_type = format!("CPPMM_{}_BUILD_TYPE", project_name.to_ascii_uppercase());
329
330    // If the user has set CMAKE_PREFIX_PATH then we don't want to build the
331    // bundled libraries, *unless* they have also set CPPMM_<project_name>_BUILD_LIBRARIES=1
332    let build_libraries = if std::env::var("CMAKE_PREFIX_PATH").is_ok() {
333        if let Ok(obl) = std::env::var(&env_build_libraries) {
334            obl == "1"
335        } else {
336            false
337        }
338    } else {
339        true
340    };
341
342    let out_dir = std::env::var("OUT_DIR").unwrap();
343    let target_dir = Path::new(&out_dir).ancestors().skip(3).next().unwrap();
344
345    let clib_name = format!("{}-c", project_name);
346    let clib_versioned_name =
347        format!("{}-c-{}_{}", project_name, major_version, minor_version);
348    let clib_shared_versioned_name =
349        format!("{}-c-{}_{}-shared", project_name, major_version, minor_version);
350
351    let lib_path = target_dir.join("lib");
352    let bin_path = target_dir.join("bin");
353    let cmake_prefix_path = lib_path.join("cmake");
354
355    // allow user to override build type with environment variables
356    let build_type =
357        if let Ok(build_type) = std::env::var(&env_build_type) {
358            build_type
359        } else {
360            "Release".to_string()
361        };
362
363    let dst = if build_libraries {
364        println!("cargo:warning=Building packaged dependencies {:?}", dependencies);
365        for dep in dependencies {
366            build_thirdparty(dep.name, target_dir, &build_type, &dep.definitions);
367        }
368
369        cmake::Config::new(clib_name)
370            .define("CMAKE_EXPORT_COMPILE_COMMANDS", "ON")
371            .define("CMAKE_PREFIX_PATH", cmake_prefix_path.to_str().unwrap())
372            .profile(&build_type)
373            .build()
374    } else {
375        println!("cargo:warning=Using system dependencies {:?}", dependencies);
376        cmake::Config::new(clib_name)
377            .define("CMAKE_EXPORT_COMPILE_COMMANDS", "ON")
378            .profile(&build_type)
379            .build()
380    };
381
382    let build_path = Path::new(&dst).join("build");
383
384    let link_args = get_linking_from_cmake(
385        &build_path,
386        &clib_shared_versioned_name,
387        &build_type,
388    );
389    println!("cargo:warning=Link libs: {:?}", link_args);
390
391    // Link our wrapper library
392    //
393    // We currently build a dylib on windows just so we can enable Debug
394    // builds. This is because Rust always links against the release msvcrt 
395    // (presumably since the debug one is unusable in a lot of situations), thus
396    // we cannot link statically since setting the C shim to Debug mode will 
397    // cause it to link against the debug msvcrt. This in turn causes all sorts
398    // of bad shit to happen (segfaults mostly). By the way, did you know that 
399    // STL types are different sizes in debug and release builds on Windows?
400    // I didn't until today because I couldn't imagine a world in which something
401    // like that would be allowed to happen.
402    //
403    // In theory, you can override this, but like most things with CMake, the 
404    // correct incantations are buried somewhere in vague mailing list 
405    // threads, and don't actually seem to work (at least not with VS generators, 
406    // which appear to want to force the runtime for you).
407    //
408    // So, the easiest way out here is just to build everything from the C shim
409    // down as a DLL so we can neatly sidestep all this (because the C library 
410    // provides a nice ABI dambreak against the insanity).
411    //
412    // We still build statically on Linux since that way you don't need to install
413    // the DSO along with any Rust binaries you might want to build. Ultimately
414    // installation in a production environment will require a bit more thought,
415    // but suffice to say it's complex. On Windows at least, just copying DLLs
416    // around everywhere seems to be the norm so we assume it's not the end of 
417    // the world.
418    //
419    println!("cargo:rustc-link-search=native={}", dst.display());
420    #[cfg(not(target_os = "windows"))]
421    println!("cargo:rustc-link-lib=static={}", clib_versioned_name);
422    #[cfg(target_os = "windows")]
423    println!("cargo:rustc-link-lib=dylib={}", clib_shared_versioned_name);
424
425    if build_libraries {
426        // Link against the stuff what we built
427        println!("cargo:rustc-link-search=native={}", lib_path.display());
428        // we don't actually want to link against anything in /bin but we 
429        // need to tell rustc where the DLLs are on windows and this is the 
430        // way to do it
431        println!("cargo:rustc-link-search=native={}", bin_path.display());
432    }
433
434    for arg in link_args {
435        // Link against all our dependencies
436        match arg {
437            LinkArg::Path(d) => {
438                let libdir = Path::new(&d.path).parent().unwrap();
439                println!("cargo:rustc-link-search=native={}", libdir.display());
440                println!("cargo:rustc-link-lib=dylib={}", &d.libname);
441            }
442            LinkArg::LinkDir(dir) => {
443                println!("cargo:rustc-link-search=native={}", dir);
444            }
445            LinkArg::LinkLib(lib) => {
446                println!("cargo:rustc-link-lib=dylib={}", lib);
447            }
448        }
449    }
450
451    // On unices we need to link against the stdlib
452    #[cfg(target_os = "linux")]
453    println!("cargo:rustc-link-lib=dylib=stdc++");
454    #[cfg(target_os = "macos")]
455    println!("cargo:rustc-link-lib=dylib=c++");
456
457    // Insert the C++ ABI info
458    //
459    // abigen is a small binary that's autogenerated by cppmm. It simply outputs
460    // the size of all opaquebytes types to a file, `abigen.txt`. Meanwhile, 
461    // cppmm sets up both the C and Rust layer source with placeholder markers
462    // that are replaced by the Python script `insert_abi.py`, below. 
463    //
464    // We do this because certain types (STL mainly) are different sizes between
465    // platforms (and even between build types on Windows!), and generating 
466    // their ABI info at build time here saves us from having to run the entire
467    // binding generation at the crate build level, and thus keeps a libclang 
468    // dependency out of all our end-user crates.
469    //
470    let build_dir = Path::new(&out_dir).join("build");
471    let abigen_bin = build_dir.join("abigen").join("abigen");
472    let abigen_txt = build_dir.join("abigen.txt");
473
474    // Run abigen again if the output doesn't exist.
475    if !abigen_txt.exists() {
476        let _ = std::process::Command::new(abigen_bin)
477            .current_dir(build_dir)
478            .output()
479            .expect("Could not run abigen");
480    }
481
482    let cppmm_abi_out = Path::new(&out_dir).join("cppmm_abi_out").join("cppmmabi.rs");
483
484    // if the generated rust doesn't exist, run the python to generate it
485    if !cppmm_abi_out.exists() {
486        let output = std::process::Command::new("python")
487            .args(&[&format!("{}-c/abigen/insert_abi.py", project_name), 
488                "cppmm_abi_in", 
489                &format!("{}/cppmm_abi_out", out_dir), 
490                &format!("{}/build/abigen.txt", out_dir)])
491            .output()
492            .expect("Could not launch python insert_abi.py");
493
494        if !output.status.success() {{
495            for line in std::str::from_utf8(&output.stderr).unwrap().lines() {{
496                println!("cargo:warning={}", line);
497            }}
498            panic!("python insert_abi failed");
499        }}
500    }
501
502}
503
504#[cfg(test)]
505mod tests {
506    #[test]
507    fn it_works() {
508        assert_eq!(2 + 2, 4);
509    }
510}