view libpam-sys/libpam-sys-test/build.rs @ 128:ad77f2af5ff4 default tip

Fix `rustc-check-cfg` in `libpam-sys/build.rs`.
author Paul Fisher <paul@pfish.zone>
date Mon, 30 Jun 2025 23:00:53 -0400
parents c77846f3a979
children
line wrap: on
line source

use bindgen::MacroTypeVariation;
use libpam_sys_impls::__pam_impl_enum__;
use proc_macro2::{Group, 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, Type, TypePath};

// We're using the macro directly so we can match exhaustively.
__pam_impl_enum__!();

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>",
            ],
            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>"],
            block_headers: vec!["sys/.*"],
            ..Default::default()
        },
        PamImpl::XSso => TestConfig {
            headers: vec!["\"xsso_pam_appl.h\""],
            ignore_consts: vec!["PAM_CRED_PRELIM_CHECK"],
            ..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()))
        .blocklist_type(".*")
        .blocklist_function(".*")
        .allowlist_var(".*")
        .default_macro_constant_type(MacroTypeVariation::Signed);
    for hdr in config.block_headers.iter() {
        builder = builder.blocklist_file(".*?/".to_owned() + hdr)
    }

    let generated = builder.generate().unwrap().to_string();
    let file = syn::parse_file(&generated).unwrap();
    let mut tests = vec![
        "use libpam_sys::*;".into(),
        "#[allow(deprecated, overflowing_literals)]".into(),
        "fn main() {".into(),
        format!(
            "assert_eq!(PamImpl::CURRENT, 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))
            .cloned()
            .map(|mut item| {
                item.ty = Box::new(Type::Path(TypePath {
                    qself: None,
                    path: format_ident!("i32").into(),
                }));
                format!(
                    "assert_eq!({tokens}, {name});",
                    tokens = item.expr.to_token_stream(),
                    name = item.ident
                )
            }),
    );
    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();

    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 {
            "pam_handle" => "struct pam_handle",
            "pam_conv" => "struct pam_conv",
            "pam_message" => "struct pam_message",
            "pam_response" => "struct pam_response",
            "AppData" => "void",
            other => 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(),
        &TokenStream::from_str("mut")
            .unwrap()
            .into_iter()
            .next()
            .unwrap(),
    )
    .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: &TokenTree) -> 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>,
    block_headers: 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())
    }
}