build_gecko.rs (13159B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 5 use super::PYTHON; 6 use bindgen::{Builder, CodegenConfig}; 7 use regex::Regex; 8 use std::cmp; 9 use std::collections::HashSet; 10 use std::env; 11 use std::fs::{self, File}; 12 use std::io::{Read, Write}; 13 use std::path::{Path, PathBuf}; 14 use std::process::{exit, Command}; 15 use std::slice; 16 use std::sync::LazyLock; 17 use std::sync::Mutex; 18 use std::time::SystemTime; 19 use toml; 20 use toml::value::Table; 21 22 static OUTDIR_PATH: LazyLock<PathBuf> = 23 LazyLock::new(|| PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("gecko")); 24 25 const STRUCTS_FILE: &'static str = "structs.rs"; 26 27 fn read_config(path: &PathBuf) -> Table { 28 println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); 29 update_last_modified(&path); 30 31 let mut contents = String::new(); 32 File::open(path) 33 .expect("Failed to open config file") 34 .read_to_string(&mut contents) 35 .expect("Failed to read config file"); 36 match toml::from_str::<Table>(&contents) { 37 Ok(result) => result, 38 Err(e) => panic!("Failed to parse config file: {}", e), 39 } 40 } 41 42 static CONFIG: LazyLock<Table> = LazyLock::new(|| { 43 // Load Gecko's binding generator config from the source tree. 44 let path = mozbuild::TOPSRCDIR.join("layout/style/ServoBindings.toml"); 45 read_config(&path) 46 }); 47 static BINDGEN_FLAGS: LazyLock<Vec<String>> = LazyLock::new(|| { 48 mozbuild::config::BINDGEN_SYSTEM_FLAGS 49 .iter() 50 .chain(&mozbuild::config::NSPR_CFLAGS) 51 .chain(&mozbuild::config::MOZ_PIXMAN_CFLAGS) 52 .chain(&mozbuild::config::MOZ_ICU_CFLAGS) 53 .map(|s| s.to_string()) 54 .collect() 55 }); 56 static INCLUDE_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"#include\s*"(.+?)""#).unwrap()); 57 static DISTDIR_PATH: LazyLock<PathBuf> = LazyLock::new(|| mozbuild::TOPOBJDIR.join("dist")); 58 static SEARCH_PATHS: LazyLock<Vec<PathBuf>> = LazyLock::new(|| { 59 vec![ 60 DISTDIR_PATH.join("include"), 61 DISTDIR_PATH.join("include/nspr"), 62 ] 63 }); 64 static ADDED_PATHS: LazyLock<Mutex<HashSet<PathBuf>>> = 65 LazyLock::new(|| Mutex::new(HashSet::new())); 66 static LAST_MODIFIED: LazyLock<Mutex<SystemTime>> = LazyLock::new(|| { 67 Mutex::new( 68 get_modified_time(&env::current_exe().unwrap()) 69 .expect("Failed to get modified time of executable"), 70 ) 71 }); 72 73 fn get_modified_time(file: &Path) -> Option<SystemTime> { 74 file.metadata().and_then(|m| m.modified()).ok() 75 } 76 77 fn update_last_modified(file: &Path) { 78 let modified = get_modified_time(file).expect("Couldn't get file modification time"); 79 let mut last_modified = LAST_MODIFIED.lock().unwrap(); 80 *last_modified = cmp::max(modified, *last_modified); 81 } 82 83 fn search_include(name: &str) -> Option<PathBuf> { 84 for path in SEARCH_PATHS.iter() { 85 let file = path.join(name); 86 if file.is_file() { 87 update_last_modified(&file); 88 return Some(file); 89 } 90 } 91 None 92 } 93 94 fn add_headers_recursively(path: PathBuf, added_paths: &mut HashSet<PathBuf>) { 95 if added_paths.contains(&path) { 96 return; 97 } 98 let mut file = File::open(&path).unwrap(); 99 let mut content = String::new(); 100 file.read_to_string(&mut content).unwrap(); 101 added_paths.insert(path); 102 // Find all includes and add them recursively 103 for cap in INCLUDE_RE.captures_iter(&content) { 104 if let Some(path) = search_include(cap.get(1).unwrap().as_str()) { 105 add_headers_recursively(path, added_paths); 106 } 107 } 108 } 109 110 fn add_include(name: &str) -> String { 111 let mut added_paths = ADDED_PATHS.lock().unwrap(); 112 let file = match search_include(name) { 113 Some(file) => file, 114 None => panic!("Include not found: {}", name), 115 }; 116 let result = String::from(file.to_str().unwrap()); 117 add_headers_recursively(file, &mut *added_paths); 118 result 119 } 120 121 trait BuilderExt { 122 fn get_initial_builder() -> Builder; 123 fn include<T: Into<String>>(self, file: T) -> Builder; 124 } 125 126 impl BuilderExt for Builder { 127 fn get_initial_builder() -> Builder { 128 // Disable rust unions, because we replace some types inside of 129 // them. 130 let mut builder = Builder::default() 131 .size_t_is_usize(true) 132 .disable_untagged_union(); 133 134 let rustfmt_path = env::var_os("RUSTFMT") 135 .filter(|p| !p.is_empty()) 136 .map(PathBuf::from); 137 if let Some(path) = rustfmt_path { 138 builder = builder.with_rustfmt(path); 139 } 140 141 for dir in SEARCH_PATHS.iter() { 142 builder = builder.clang_arg("-I").clang_arg(dir.to_str().unwrap()); 143 } 144 145 builder = builder.include(add_include("mozilla-config.h")); 146 147 if env::var("CARGO_FEATURE_GECKO_DEBUG").is_ok() { 148 builder = builder.clang_arg("-DDEBUG=1").clang_arg("-DJS_DEBUG=1"); 149 } 150 151 for item in &*BINDGEN_FLAGS { 152 builder = builder.clang_arg(item); 153 } 154 155 builder 156 } 157 fn include<T: Into<String>>(self, file: T) -> Builder { 158 self.clang_arg("-include").clang_arg(file) 159 } 160 } 161 162 struct Fixup { 163 pat: String, 164 rep: String, 165 } 166 167 fn write_binding_file(builder: Builder, file: &str, fixups: &[Fixup]) { 168 let out_file = OUTDIR_PATH.join(file); 169 if let Some(modified) = get_modified_time(&out_file) { 170 // Don't generate the file if nothing it depends on was modified. 171 let last_modified = LAST_MODIFIED.lock().unwrap(); 172 if *last_modified <= modified { 173 return; 174 } 175 } 176 let command_line_opts = builder.command_line_flags(); 177 let result = builder.generate(); 178 let mut result = match result { 179 Ok(bindings) => bindings.to_string(), 180 Err(_) => { 181 panic!( 182 "Failed to generate bindings, flags: {:?}", 183 command_line_opts 184 ); 185 }, 186 }; 187 188 for fixup in fixups.iter() { 189 result = Regex::new(&fixup.pat) 190 .unwrap() 191 .replace_all(&result, &*fixup.rep) 192 .into_owned() 193 .into(); 194 } 195 let bytes = result.into_bytes(); 196 File::create(&out_file) 197 .unwrap() 198 .write_all(&bytes) 199 .expect("Unable to write output"); 200 } 201 202 struct BuilderWithConfig<'a> { 203 builder: Builder, 204 config: &'a Table, 205 used_keys: HashSet<&'static str>, 206 } 207 impl<'a> BuilderWithConfig<'a> { 208 fn new(builder: Builder, config: &'a Table) -> Self { 209 BuilderWithConfig { 210 builder, 211 config, 212 used_keys: HashSet::new(), 213 } 214 } 215 216 fn handle_list<F>(self, key: &'static str, func: F) -> BuilderWithConfig<'a> 217 where 218 F: FnOnce(Builder, slice::Iter<'a, toml::Value>) -> Builder, 219 { 220 let mut builder = self.builder; 221 let config = self.config; 222 let mut used_keys = self.used_keys; 223 if let Some(list) = config.get(key) { 224 used_keys.insert(key); 225 builder = func(builder, list.as_array().unwrap().as_slice().iter()); 226 } 227 BuilderWithConfig { 228 builder, 229 config, 230 used_keys, 231 } 232 } 233 fn handle_items<F>(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a> 234 where 235 F: FnMut(Builder, &'a toml::Value) -> Builder, 236 { 237 self.handle_list(key, |b, iter| iter.fold(b, |b, item| func(b, item))) 238 } 239 fn handle_str_items<F>(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a> 240 where 241 F: FnMut(Builder, &'a str) -> Builder, 242 { 243 self.handle_items(key, |b, item| func(b, item.as_str().unwrap())) 244 } 245 fn handle_table_items<F>(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a> 246 where 247 F: FnMut(Builder, &'a Table) -> Builder, 248 { 249 self.handle_items(key, |b, item| func(b, item.as_table().unwrap())) 250 } 251 fn handle_common(self, fixups: &mut Vec<Fixup>) -> BuilderWithConfig<'a> { 252 self.handle_str_items("headers", |b, item| b.header(add_include(item))) 253 .handle_str_items("raw-lines", |b, item| b.raw_line(item)) 254 .handle_str_items("hide-types", |b, item| b.blocklist_type(item)) 255 .handle_table_items("fixups", |builder, item| { 256 fixups.push(Fixup { 257 pat: item["pat"].as_str().unwrap().into(), 258 rep: item["rep"].as_str().unwrap().into(), 259 }); 260 builder 261 }) 262 } 263 264 fn get_builder(self) -> Builder { 265 for key in self.config.keys() { 266 if !self.used_keys.contains(key.as_str()) { 267 panic!("Unknown key: {}", key); 268 } 269 } 270 self.builder 271 } 272 } 273 274 fn generate_structs() { 275 let builder = Builder::get_initial_builder() 276 .enable_cxx_namespaces() 277 .with_codegen_config(CodegenConfig::TYPES | CodegenConfig::VARS | CodegenConfig::FUNCTIONS); 278 let mut fixups = vec![]; 279 let builder = BuilderWithConfig::new(builder, CONFIG["structs"].as_table().unwrap()) 280 .handle_common(&mut fixups) 281 .handle_str_items("allowlist-functions", |b, item| b.allowlist_function(item)) 282 .handle_str_items("bitfield-enums", |b, item| b.bitfield_enum(item)) 283 .handle_str_items("rusty-enums", |b, item| b.rustified_enum(item)) 284 .handle_str_items("allowlist-vars", |b, item| b.allowlist_var(item)) 285 .handle_str_items("allowlist-types", |b, item| b.allowlist_type(item)) 286 .handle_str_items("opaque-types", |b, item| b.opaque_type(item)) 287 .handle_table_items("cbindgen-types", |b, item| { 288 let gecko = item["gecko"].as_str().unwrap(); 289 let servo = item["servo"].as_str().unwrap(); 290 b.blocklist_type(format!("mozilla::{}", gecko)) 291 .module_raw_line("root::mozilla", format!("pub use {} as {};", servo, gecko)) 292 }) 293 .handle_table_items("mapped-generic-types", |builder, item| { 294 let generic = item["generic"].as_bool().unwrap(); 295 let gecko = item["gecko"].as_str().unwrap(); 296 let servo = item["servo"].as_str().unwrap(); 297 let gecko_name = gecko.rsplit("::").next().unwrap(); 298 let gecko = gecko 299 .split("::") 300 .map(|s| format!("\\s*{}\\s*", s)) 301 .collect::<Vec<_>>() 302 .join("::"); 303 304 fixups.push(Fixup { 305 pat: format!("\\broot\\s*::\\s*{}\\b", gecko), 306 rep: format!("crate::gecko_bindings::structs::{}", gecko_name), 307 }); 308 builder.blocklist_type(gecko).raw_line(format!( 309 "pub type {0}{2} = {1}{2};", 310 gecko_name, 311 servo, 312 if generic { "<T>" } else { "" } 313 )) 314 }) 315 .get_builder(); 316 write_binding_file(builder, STRUCTS_FILE, &fixups); 317 } 318 319 fn setup_logging() -> bool { 320 struct BuildLogger { 321 file: Option<Mutex<fs::File>>, 322 filter: String, 323 } 324 325 impl log::Log for BuildLogger { 326 fn enabled(&self, meta: &log::Metadata) -> bool { 327 self.file.is_some() && meta.target().contains(&self.filter) 328 } 329 330 fn log(&self, record: &log::Record) { 331 if !self.enabled(record.metadata()) { 332 return; 333 } 334 335 let mut file = self.file.as_ref().unwrap().lock().unwrap(); 336 let _ = writeln!( 337 file, 338 "{} - {} - {} @ {}:{}", 339 record.level(), 340 record.target(), 341 record.args(), 342 record.file().unwrap_or("<unknown>"), 343 record.line().unwrap_or(0) 344 ); 345 } 346 347 fn flush(&self) { 348 if let Some(ref file) = self.file { 349 file.lock().unwrap().flush().unwrap(); 350 } 351 } 352 } 353 354 if let Some(path) = env::var_os("STYLO_BUILD_LOG") { 355 log::set_max_level(log::LevelFilter::Debug); 356 log::set_boxed_logger(Box::new(BuildLogger { 357 file: fs::File::create(path).ok().map(Mutex::new), 358 filter: env::var("STYLO_BUILD_FILTER") 359 .ok() 360 .unwrap_or_else(|| "bindgen".to_owned()), 361 })) 362 .expect("Failed to set logger."); 363 364 true 365 } else { 366 false 367 } 368 } 369 370 fn generate_atoms() { 371 let script = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()) 372 .join("gecko") 373 .join("regen_atoms.py"); 374 println!("cargo:rerun-if-changed={}", script.display()); 375 let status = Command::new(&*PYTHON) 376 .arg(&script) 377 .arg(DISTDIR_PATH.as_os_str()) 378 .arg(OUTDIR_PATH.as_os_str()) 379 .status() 380 .unwrap(); 381 if !status.success() { 382 exit(1); 383 } 384 } 385 386 pub fn generate() { 387 println!("cargo:rerun-if-changed=build_gecko.rs"); 388 fs::create_dir_all(&*OUTDIR_PATH).unwrap(); 389 setup_logging(); 390 generate_structs(); 391 generate_atoms(); 392 393 for path in ADDED_PATHS.lock().unwrap().iter() { 394 println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); 395 } 396 }