use anyhow::{ Context, Result, anyhow, }; use std::{ cell::RefCell, collections::HashMap, env, fmt::Debug, fs::{self, File}, io::Write, path::{Path, PathBuf}, process::Command, }; type Toml = toml::value::Value; const UTF8_PATH: &'static str = "path should be valid UTF-8 string."; const PKG_NAME_IS_STR: &'static str = "pkg name should be str."; fn check_os( table: &toml::Table ) -> Result { if let Some( os ) = table .get("os") { let os = os.as_str().context( "os name should be str." )?; Ok( match_os( os )) } else { Ok( true ) } } fn match_os( name: &str ) -> bool { match name { "android" => if cfg!( target_os = "android" ) {true} else {false}, "dragonfly" => if cfg!( target_os = "dragonfly" ) {true} else {false}, "freebsd" => if cfg!( target_os = "freebsd" ) {true} else {false}, "ios" => if cfg!( target_os = "ios" ) {true} else {false}, "linux" => if cfg!( target_os = "linux" ) {true} else {false}, "macos" => if cfg!( target_os = "macos" ) {true} else {false}, "netbsd" => if cfg!( target_os = "netbsd" ) {true} else {false}, "openbsd" => if cfg!( target_os = "openbsd" ) {true} else {false}, "windows" => if cfg!( target_os = "windows" ) {true} else {false}, "unix" => if cfg!( unix ) {true} else {false}, _ => false, } } #[derive( Debug )] pub struct LibInfo { link_paths : RefCell>, include_paths : RefCell>, headers : RefCell>, specs : HashMap, } impl LibInfo { fn new( specs: HashMap ) -> Self { LibInfo { link_paths : RefCell::default(), include_paths : RefCell::default(), headers : RefCell::default(), specs , } } fn probe( &self, pkg_name: &str, scan_incdir: bool ) -> Result<()> { let probed_ex = self .probe_via_pkgconf( pkg_name, scan_incdir ) .or_else( |_| self.probe_via_search( pkg_name, scan_incdir ))?; if scan_incdir { self.include_paths.borrow_mut().push( self.get_includedir( &probed_ex )? ); } if let Some( spec ) = self.specs.get( pkg_name ) { let include_dir = self.get_includedir( &probed_ex )?; if let Some( table ) = spec.as_table() { if !scan_incdir { table .get( "headers" ) .and_then( |headers| headers.as_array() ) .and_then( |headers| headers .iter() .try_for_each( |header| -> Result<()> { if let Some( header ) = header.as_str() { self.headers.borrow_mut().push( Path::new( &include_dir ) .join( header ) .to_str() .context( UTF8_PATH )? .to_owned() ) } Ok(()) }) .ok() ).context( UTF8_PATH )?; if !probed_ex.pkgconf_ok() { if let Some( dependencies ) = table.get( "dependencies" ) { match dependencies { Toml::Array( dependencies ) => for pkg_name in dependencies { self.probe( pkg_name.as_str().context( PKG_NAME_IS_STR )?, false )?; }, Toml::Table( dependencies ) => for (pkg_name, dep) in dependencies { let dep = dep.as_table().context("named dependency should be table.")?; if check_os( dep )? { self.probe( pkg_name, false )?; } }, _ => return Err( anyhow!( "invalid dependencies." )), } } } } if let Some( dependencies ) = table.get( "header-dependencies" ) { match dependencies { Toml::Array( dependencies ) => for pkg_name in dependencies { self.probe( pkg_name.as_str().context( PKG_NAME_IS_STR )?, true )?; }, Toml::Table( dependencies ) => for (pkg_name, dep) in dependencies { let dep = dep.as_table().context("named dependency should be table.")?; if check_os( dep )? { self.probe( pkg_name, true )?; } }, _ => return Err( anyhow!( "invalid header-dependencies." )), } } } } Ok(()) } fn probe_via_pkgconf( &self, pkg_name: &str, scan_incdir: bool ) -> Result { env::set_var( "PKG_CONFIG_ALLOW_SYSTEM_CFLAGS", "1" ); env::set_var( "PKG_CONFIG_ALLOW_SYSTEM_LIBS", "1" ); let mut cfg = pkg_config::Config::new(); cfg.cargo_metadata( true ); let mut pc_file_names = vec![ pkg_name ]; if let Some( spec ) = self.specs.get( pkg_name ) { let table = spec.as_table().expect("clib specs should be a table."); if let Some( pc_alias ) = table.get("pc-alias") { pc_alias .as_array() .expect("pc-alias should be array.") .iter() .for_each( |pc| { pc_file_names.push( pc.as_str().expect( ".pc file name should be str." )); }); } } let mut names = pc_file_names.into_iter(); let (library, pc_name) = loop { if let Some( name ) = names.next() { if let Ok( library ) = cfg.probe( name ) { break (library, name.to_owned() ); } } else { return Err( anyhow!( "failed to locate .pc file" )); } }; if !scan_incdir { library.link_paths .into_iter() .map( |path| path.to_str().expect( UTF8_PATH ).to_owned() ) .for_each( |link_path| self.link_paths.borrow_mut().push( link_path )); library.include_paths .into_iter() .map( |path| path.to_str().expect( UTF8_PATH ).to_owned() ) .for_each( |include_path| self.include_paths.borrow_mut().push( include_path )); } Ok( ProbedEx::PcName( pc_name )) } fn probe_via_search( &self, pkg_name: &str, scan_incdir: bool ) -> Result { if cfg!( unix ) { return Err( anyhow!( "failed in using pkg-config for probe library" )); } if let Some( table ) = self.specs .get( pkg_name ) .unwrap() .as_table() { if let Some( executable_names ) = table.get( "exe" ).and_then( |exe| exe.as_array() ) { for name in executable_names { let name = name.as_str().expect("exe names should be str."); let output = Command::new( if cfg!(unix) { "which" } else { "where" }) .arg( name ).output(); match output { Ok( output ) => { let s = output.stdout.as_slice(); if s.is_empty() { continue; } let cmd_path = Path::new( std::str::from_utf8( s ) .expect( UTF8_PATH ) .trim_end() ); let parent = cmd_path.parent() .expect("executable should not be found in root directory."); assert_eq!( parent.file_name().expect( UTF8_PATH ), "bin" ); let prefix = parent.parent() .expect("bin should not be found in root directory."); let include_base = prefix.join("include"); let guess_include = table .get("includedir") .and_then( |includedirs| includedirs.as_array() ) .and_then( |dirs| Some( dirs.iter().map( |dir| dir.as_str().expect( "include dir should be str." )))) .and_then( |dirs| { for dir in dirs { let dir = include_base.join( dir ); if dir.exists() { return Some( dir.to_str().expect( UTF8_PATH ).to_owned() ); } } Some( include_base.to_str().expect( UTF8_PATH ).to_owned() ) }) .expect("include_path"); if !scan_incdir { self.link_paths.borrow_mut().push( prefix.join("lib").to_str().expect( UTF8_PATH ).to_owned() ); println!( "cargo:rustc-link-search=native={}/lib", prefix.to_str().expect( UTF8_PATH )); emit_cargo_meta_for_libs( &prefix, table.get( "libs" ).expect( "metadata should contain libs" ))?; if let Some( libs ) = table.get( "libs-private" ) { emit_cargo_meta_for_libs( &prefix, libs )?; } } return Ok( ProbedEx::IncDir( guess_include )); }, Err(_) => continue, } } return Err( anyhow!( "executable not found" )); } else { return Err( anyhow!( "failed to locate executable" )); } } else { return Err( anyhow!( "failed to search lib." )); } } fn get_includedir( &self, probe_ex: &ProbedEx ) -> Result { match probe_ex { ProbedEx::PcName( pc_name ) => { let exe = env::var( "PKG_CONFIG" ).unwrap_or_else( |_| "pkg-config".to_owned() ); let mut cmd = Command::new( exe ); cmd.args( &[ &pc_name, "--variable", "includedir" ]); let output = cmd.output()?; let result = Ok( std::str::from_utf8( output.stdout.as_slice() )? .trim_end().to_owned() ); result }, ProbedEx::IncDir( includedir ) => { let path = Path::new( &includedir ); assert!( path.exists() ); Ok( format!( "{}", path.display() )) }, } } } fn emit_cargo_meta_for_libs( prefix: &Path, value: &Toml ) -> Result<()> { let lib_path = prefix.join("lib"); if let Some( table ) = value.as_table() { 'values: for value in table.values() { let lib_names = value.as_array().expect("names of libs should be an array."); for lib_name in lib_names { let lib_name = lib_name.as_str().expect( "lib name should be str." ); if lib_path.join( lib_name ).exists() { println!( "cargo:rustc-link-lib={}", get_link_name( lib_name )); continue 'values; } } return Err( anyhow!( "lib should be found in {:?} directory.", lib_path )); } } else if let Some( lib_names ) = value.as_array() { for lib_name in lib_names { let lib_name = lib_name.as_str().expect("lib name should be str."); if lib_path.join( lib_name ).exists() { println!( "cargo:rustc-link-lib={}", get_link_name( lib_name )); } else { return Err( anyhow!( "failed to locate {}", lib_name )); } } } Ok(()) } fn get_link_name( lib_name: &str ) -> &str { let start = if lib_name.starts_with( "lib" ) { 3 } else { 0 }; match lib_name.rfind('.') { Some( dot ) => &lib_name[ start..dot ], None => &lib_name[ start.. ], } } enum ProbedEx { IncDir( String ), PcName( String ), } impl ProbedEx { fn pkgconf_ok( &self ) -> bool { match self { ProbedEx::IncDir(_) => false, ProbedEx::PcName(_) => true, } } } fn generate_dummy() { let out_path = PathBuf::from( env::var( "OUT_DIR" ).expect( "$OUT_DIR should exist." )); File::create( out_path.join( "bindings.rs" )).expect( "an empty bindings.rs generated." ); } fn main() { let (specs, builds) = inwelling::collect_downstream( inwelling::Opts::default() ) .packages .into_iter() .fold( ( HashMap::::new(), // pkg name -> spec HashMap::::new(), // builds -> the path of downstream's manifest ), |(mut specs, mut builds), package| { package.metadata .as_table() .map( |table| { table .get( "spec" ) .and_then( |spec| spec.as_table() ) .map( |spec| spec.iter() .for_each( |(key,value)| { specs.insert( key.clone(), value.clone() ); })); table .get( "build" ) .and_then( |build| build.as_array() ) .map( |build_list| build_list.iter() .for_each( |pkg| { pkg.as_str().map( |pkg| { builds.insert( pkg.to_owned(), package.manifest.clone() ); }); })); }); (specs, builds) } ); if builds.is_empty() { generate_dummy(); return; } #[cfg( target_os = "freebsd" )] env::set_var( "PKG_CONFIG_ALLOW_CROSS", "1" ); let lib_info_all = LibInfo::new( specs ); let mut downstream_files_for_docs_rs = Vec::::new(); builds.iter().for_each( |(pkg_name, manifest_path)| { if !pkg_name.is_empty() { match lib_info_all.probe( pkg_name, false ) { Ok(_) => (), Err( err ) => { //if cfg!( target_os = "linux" ) && Path::new( "/.dockerenv" ).exists() { // make docs.rs happy println!( "cargo:warning=[clib] fails to probe library {}, error occured: {:?}", pkg_name, err ); if let Some( spec ) = lib_info_all.specs.get( pkg_name ) { if let Some( table ) = spec.as_table() { if let Some( for_docs_rs ) = table.get( "for-docs-rs" ) { if let Some( for_docs_rs ) = for_docs_rs.as_str() { downstream_files_for_docs_rs.push( manifest_path .parent() .expect("the manifest dir") .join( for_docs_rs ) ); } } } } //} else { // panic!( "{:#?}", err ); //} }, } } }); let out_path = PathBuf::from( env::var( "OUT_DIR" ).expect( "$OUT_DIR should exist." )); if !lib_info_all.headers.borrow().is_empty() { let mut builder = bindgen::Builder::default() .generate_comments( false ) ; for header in lib_info_all.headers.borrow().iter() { builder = builder.header( header ); } for path in lib_info_all.include_paths.borrow().iter() { let opt = format!( "-I{}", path ); builder = builder.clang_arg( &opt ); } let bindings = builder.generate().expect( "bindgen builder constructed." ); bindings.write_to_file( out_path.join( "bindings.rs" )).expect( "bindings.rs generated." ); } else if downstream_files_for_docs_rs.is_empty() { generate_dummy(); } else { let mut out_file = File::create( out_path.join( "bindings.rs" ) ) .expect( &format!( "{:?} should be created for add contents for docs.rs.", out_path )); for path in &downstream_files_for_docs_rs { let contents = fs::read_to_string( path ) .expect( &format!( "contents for generating docs on docs.rs should be read from {:?}", path )); writeln!( &mut out_file, "{}", contents ) .expect( &format!( "Some contents for generating docs on docs.rs should be appended to {:?}.", out_path )); } } }