Mercurial > crates > nonstick
view libpam-sys/libpam-sys-test/build.rs @ 131:a632a8874131
Get all the Linux-PAM functions into libpam-sys, and get tests right.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Wed, 02 Jul 2025 02:24:21 -0400 |
parents | 80c07e5ab22f |
children | 0b6a17f8c894 |
line wrap: on
line source
use bindgen::MacroTypeVariation; use libpam_sys_impls::__pam_impl_enum__; use proc_macro2::{Group, Ident, TokenStream, TokenTree}; use quote::{format_ident, ToTokens}; use std::path::Path; use std::process::Command; use std::str::FromStr; use std::{env, fs}; use syn::{Item, ItemConst}; // We're using the macro directly so we can match exhaustively. __pam_impl_enum__!(); const REDIR_FD: &str = "pam_modutil_redirect_fd"; fn main() { let config = match PamImpl::CURRENT { PamImpl::LinuxPam => TestConfig { headers: vec![ "<security/_pam_types.h>", "<security/pam_appl.h>", "<security/pam_ext.h>", "<security/pam_modules.h>", "<security/pam_modutil.h>", ], allow_types: vec![REDIR_FD], ignore_consts: vec![ "__LINUX_PAM__", "__LINUX_PAM_MINOR__", "PAM_AUTHTOK_RECOVER_ERR", ], ..Default::default() }, PamImpl::OpenPam => TestConfig { headers: vec![ "<security/pam_types.h>", "<security/openpam.h>", "<security/pam_appl.h>", "<security/pam_constants.h>", ], ignore_consts: vec!["OPENPAM_VERSION", "OPENPAM_RELEASE", "PAM_SOEXT"], ..Default::default() }, PamImpl::Sun => TestConfig { headers: vec!["<security/pam_appl.h>", "<security/pam_modules.h>"], ..Default::default() }, PamImpl::XSso => TestConfig { headers: vec!["\"xsso_pam_appl.h\""], ..Default::default() }, }; generate_const_test(&config); generate_ctest(&config); } fn generate_const_test(config: &TestConfig) { let mut builder = bindgen::Builder::default() .header_contents("_.h", &config.header_contents()) .merge_extern_blocks(true) .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) .allowlist_var("(OPEN)?PAM_.*") .default_macro_constant_type(MacroTypeVariation::Signed); for &typ in config.allow_types.iter() { builder = builder.allowlist_type(typ); } let generated = builder.generate().unwrap(); generated.write_to_file(test_file("bindgen.rs")).unwrap(); let file = syn::parse_file(&generated.to_string()).unwrap(); let mut tests = vec![ "\ #[allow(dead_code, non_camel_case_types, non_upper_case_globals)] mod generated { include!(\"bindgen.rs\"); } #[allow(deprecated, overflowing_literals)] fn main() { " .into(), format!( "assert_eq!(libpam_sys::PamImpl::CURRENT, libpam_sys::PamImpl::{:?});", PamImpl::CURRENT ), ]; tests.extend( file.items .iter() .filter_map(|item| { if let Item::Const(item) = item { Some(item) } else { None } }) .filter(|&item| config.should_check_const(item)) .map(|item| { let name = item.ident.to_string(); if let Some(stripped) = name.strip_prefix(&format!("{REDIR_FD}_")) { format!("assert_eq!(generated::{name} as i32, libpam_sys::{REDIR_FD}::{stripped}.into());") } else { format!("assert_eq!(generated::{name}, libpam_sys::{name});") } }), ); tests.push("}".into()); let const_test = test_file("constant_test.rs"); fs::write(&const_test, tests.join("\n")).unwrap(); rustfmt(&const_test); } fn generate_ctest(config: &TestConfig) { let mut test = ctest::TestGenerator::new(); test.cfg("_hack_impl", Some(&format!("{:?}", PamImpl::CURRENT))); for &header in config.headers.iter() { if header.starts_with('"') { test.include(env::var("CARGO_MANIFEST_DIR").unwrap()); } test.header(&header[1..header.len() - 1]); } // These are opaque structs. test.skip_struct(|name| matches!(name, "pam_handle" | "AppData")); test.skip_type(|name| matches!(name, "ConversationCallback" | "CleanupCallback")); test.type_name(|name, is_struct, is_union| { assert!(!is_union); // we scabbin' match (name, is_struct) { ("AppData", _) => "void".into(), (REDIR_FD, _) => format!("enum {REDIR_FD}"), ("passwd", _) => "struct passwd".into(), ("group", _) => "struct group".into(), ("spwd", _) => "struct spwd".into(), (name, true) => format!("struct {name}"), (other, false) => other.into(), } }); // // Welcome to THE HACK ZONE. // // Define away constness because the various PAM implementations // have different const annotations and this will surely drive you crazy. test.define("const", Some("")); // Also replace all the `const`s with `mut`s in the ffi.rs file. let file_contents = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../src/ffi.rs")); let deconsted_file = test_file("ffi.rs"); remove_consts(file_contents, &deconsted_file); test.generate(&deconsted_file, "ctest.rs"); } fn remove_consts(file_contents: &str, out_file: impl AsRef<Path>) { let deconstified = deconstify( TokenStream::from_str(file_contents).unwrap(), &format_ident!("mut"), ) .to_string(); let out_file = out_file.as_ref(); fs::write(out_file, deconstified).unwrap(); rustfmt(out_file) } fn rustfmt(file: impl AsRef<Path>) { let status = Command::new(env!("CARGO")) .args(["fmt", "--", file.as_ref().to_str().unwrap()]) .status() .unwrap(); assert!(status.success(), "rustfmt exited with code {status}"); } fn deconstify(stream: TokenStream, mut_token: &Ident) -> TokenStream { TokenStream::from_iter(stream.into_iter().map(|token| { match token { TokenTree::Group(g) => { TokenTree::Group(Group::new(g.delimiter(), deconstify(g.stream(), mut_token))) .into_token_stream() } TokenTree::Ident(id) if id == "const" => mut_token.into_token_stream(), other => other.into_token_stream(), } })) } fn test_file(name: impl AsRef<str>) -> String { format!("{}/{}", env::var("OUT_DIR").unwrap(), name.as_ref()) } #[derive(Default)] struct TestConfig { headers: Vec<&'static str>, allow_types: Vec<&'static str>, ignore_consts: Vec<&'static str>, } impl TestConfig { fn header_contents(&self) -> String { let vec: Vec<_> = self .headers .iter() .map(|h| format!("#include {h}\n")) .collect(); vec.join("") } fn should_check_const(&self, item: &ItemConst) -> bool { !self .ignore_consts .contains(&item.ident.to_string().as_ref()) } }