Mercurial > crates > nonstick
diff libpam-sys/libpam-sys-impls/src/lib.rs @ 190:995aca290452
Restructure the way libpam-sys-impls works to fix cross-compilation.
The previous structure of libpam-sys-impls meant that things got
confusing (including for me) between what constants were build-time
and what constants were run-time. This broke cross-compilation.
This simplifies the way that works so that `libpam-sys-impls` has
*no* build script itself and is intended mostly as a library to be
included in other libraries' build scripts (while also exporting
the PamImpl enum).
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Sat, 02 Aug 2025 18:47:46 -0400 |
parents | 0730f5f2ee2a |
children |
line wrap: on
line diff
--- a/libpam-sys/libpam-sys-impls/src/lib.rs Thu Jul 31 15:42:12 2025 -0400 +++ b/libpam-sys/libpam-sys-impls/src/lib.rs Sat Aug 02 18:47:46 2025 -0400 @@ -1,53 +1,267 @@ -//! Information about the PAM implementation you're using right now. +#![allow(clippy::needless_doctest_main)] +//! An enumeration of PAM implementations and tools to detect them. //! -//! This module contains constants and values that can be used at build-script, -//! compile, and run time to determine what PAM implementation you're using. +//! # Configuration //! -//! ## Always available +//! When used at compile time, this crate uses the target OS by default, +//! but can be overridden with the `LIBPAMSYS_IMPL` environment variable. +//! See the documentation of [`build_target_impl`] for details. //! -//! [`PamImpl::CURRENT`] will tell you what version of PAM you're using. -//! It can be imported in any Rust code, from build scripts to runtime. +//! # Detecting PAM //! -//! ## Compile time +//! ## Build time //! //! Use [`enable_pam_impl_cfg`] in your `build.rs` to generate custom `#[cfg]`s //! for conditional compilation based on PAM implementation. //! -//! ``` -//! // Your package's build.rs: +//! To detect the implementation that will be used at runtime, use the +//! [`build_target_impl`] function. //! -//! fn main() { -//! // Also available at libpam_sys::pam_impl::enable_pam_impl_cfg(). -//! libpam_sys_impls::enable_pam_impl_cfg(); -//! // whatever else you do in your build script. -//! } -//! ``` -//! -//! This will set the current `pam_impl` as well as registering all known -//! PAM implementations with `rustc-check-cfg` to get cfg-checking. -//! -//! The names that appear in the `cfg` variables are the same as the values -//! in the [`PamImpl`] enum. +//! ## Run time //! -//! ```ignore -//! #[cfg(pam_impl = "OpenPam")] -//! fn openpam_specific_func(handle: *const libpam_sys::pam_handle) { -//! let environ = libpam_sys::pam_getenvlist(handle); -//! // ... -//! libpam_sys::openpam_free_envlist() -//! } -//! -//! // This will give you a warning since "UnknownImpl" is not in the cfg. -//! #[cfg(not(pam_impl = "UnknownImpl"))] -//! fn do_something() { -//! // ... -//! } -//! ``` -//! -//! The [`pam_impl_name!`] macro will expand to this same value, currently -#![doc = concat!("`", env!("LIBPAMSYS_IMPL"), "`.")] +//! The implementation of PAM installed on the machine where the code is running +//! can be detected with [`currently_installed`], or you can use +//! [`os_default`] to see what implementation is used on a given target. + +use std::env; +use std::env::VarError; +use std::ffi::c_void; +use std::ptr::NonNull; + +/// An enum that knows its own values. +macro_rules! self_aware_enum { + ( + $(#[$enumeta:meta])* + $viz:vis enum $name:ident { + $( + $(#[$itemeta:meta])* + $item:ident, + )* + } + ) => { + $(#[$enumeta])* + $viz enum $name { + $( + $(#[$itemeta])* + $item, + )* + } + + // The implementations in this block are private for now + // to avoid putting a contract into the public API. + #[allow(dead_code)] + impl $name { + /// Iterator over the items in the enum. For internal use. + pub(crate) fn items() -> Vec<Self> { + vec![$(Self::$item),*] + } + + /// Attempts to parse the enum from the string. For internal use. + pub(crate) fn try_from(value: &str) -> Result<Self, String> { + match value { + $(stringify!($item) => Ok(Self::$item),)* + _ => Err(value.into()), + } + } + } + }; +} + +self_aware_enum! { + /// The PAM implementations supported by `libpam-sys`. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + #[non_exhaustive] + pub enum PamImpl { + /// [Linux-PAM] is provided by most Linux distributions. + /// + /// [Linux-PAM]: https://github.com/linux-pam/linux-pam + LinuxPam, + /// [OpenPAM] is used by most BSDs, including Mac OS X. + /// + /// [OpenPAM]: https://git.des.dev/OpenPAM/OpenPAM + OpenPam, + /// Illumos and Solaris use a derivative of [Sun's implementation][sun]. + /// + /// [sun]: https://code.illumos.org/plugins/gitiles/illumos-gate/+/refs/heads/master/usr/src/lib/libpam + Sun, + /// Only the functionality and constants in [the PAM spec]. + /// + /// [the PAM spec]: https://pubs.opengroup.org/onlinepubs/8329799/toc.htm + XSso, + } +} + +#[allow(clippy::needless_doctest_main)] +/// Generates `cargo` directives for build scripts to enable `cfg(pam_impl)`. +/// +/// Print this in your `build.rs` script to be able to use the custom `pam_impl` +/// configuration directive. +/// +/// ``` +/// // Your package's build.rs: +/// +/// fn main() { +/// // Also available at libpam_sys::pam_impl::enable_pam_impl_cfg(). +/// libpam_sys_impls::enable_pam_impl_cfg(); +/// // whatever else you do in your build script. +/// } +/// ``` +/// +/// This will set the current `pam_impl` as well as registering all known +/// PAM implementations with `rustc-check-cfg` to get cfg-checking. +/// +/// The names that appear in the `cfg` variables are the same as the values +/// in the [`PamImpl`] enum. +/// +/// ```ignore +/// #[cfg(pam_impl = "OpenPam")] +/// fn openpam_specific_func(handle: *const libpam_sys::pam_handle) { +/// let environ = libpam_sys::pam_getenvlist(handle); +/// // ... +/// libpam_sys::openpam_free_envlist() +/// } +/// +/// // This will give you a warning since "UnknownImpl" is not a known +/// // PAM implementation. +/// #[cfg(not(pam_impl = "UnknownImpl"))] +/// fn do_something() { +/// // ... +/// } +/// ``` +pub fn enable_pam_impl_cfg() { + println!("{}", pam_impl_cfg_string()) +} + +/// [`enable_pam_impl_cfg`], but returned as a string. +pub fn pam_impl_cfg_string() -> String { + generate_cfg(build_target_impl()) +} -mod pam_impl; +fn generate_cfg(pam_impl: Option<PamImpl>) -> String { + let impls: Vec<_> = PamImpl::items() + .into_iter() + .map(|i| format!(r#""{i:?}""#)) + .collect(); + let mut lines = vec![ + format!( + "cargo:rustc-check-cfg=cfg(pam_impl, values({impls}))", + impls = impls.join(",") + ), + "cargo:rustc-cfg=pam_impl".into(), + ]; + if let Some(pam_impl) = pam_impl { + lines.push("cargo:rustc-cfg=pam_impl".into()); + lines.push(format!("cargo:rustc-cfg=pam_impl=\"{pam_impl:?}\"")); + } + lines.join("\n") +} + +/// The strategy to use to detect PAM. +enum Detect { + /// Use the default PAM implementation based on the target OS. + TargetDefault, + /// Detect the installed implementation. + Installed, + /// Use the named version of PAM. + Specified(PamImpl), +} + +const INSTALLED: &str = "__installed__"; -#[doc(inline)] -pub use pam_impl::*; +/// For `build.rs` use: Detects the PAM implementation that should be used +/// for the target of the currently-running build script. +/// +/// # Configuration +/// +/// The PAM implementation selected depends upon the value of the +/// `LIBPAMSYS_IMPL` environment variable. +/// +/// - Empty or unset (default): Use the default PAM implementation for the +/// Cargo target OS (as specified by `CARGO_CFG_TARGET_OS`). +/// - Linux: Linux-PAM +/// - BSD (and Mac): OpenPAM +/// - Illumos/Solaris: Sun PAM +/// - `__installed__`: Use the PAM implementation installed on the host system. +/// This opens the `libpam` library and looks for specific functions. +/// - The name of a [PamImpl] member: Use that PAM implementation. +/// +/// # Panics +/// +/// If an unknown PAM implementation is provided in `LIBPAMSYS_IMPL`. +pub fn build_target_impl() -> Option<PamImpl> { + let detection = match env::var("LIBPAMSYS_IMPL").as_deref() { + Ok("") | Err(VarError::NotPresent) => Detect::TargetDefault, + Ok(INSTALLED) => Detect::Installed, + Ok(val) => Detect::Specified(PamImpl::try_from(val).unwrap_or_else(|_| { + panic!( + "unknown PAM implementation {val:?}. \ + valid LIBPAMSYS_IMPL values are {:?}, \ + {INSTALLED:?} to use the currently-installed version, \ + or unset to use the OS default", + PamImpl::items() + ) + })), + Err(other) => panic!("Couldn't detect PAM version: {other}"), + }; + match detection { + Detect::TargetDefault => env::var("CARGO_CFG_TARGET_OS") + .ok() + .as_deref() + .and_then(os_default), + Detect::Installed => currently_installed(), + Detect::Specified(other) => Some(other), + } +} + +/// Gets the PAM version based on the target OS. +/// +/// The target OS name passed in is one of the [Cargo target OS values][os]. +/// +/// [os]: https://doc.rust-lang.org/reference/conditional-compilation.html#r-cfg.target_os.values +pub fn os_default(target_os: &str) -> Option<PamImpl> { + match target_os { + "linux" => Some(PamImpl::LinuxPam), + "macos" | "freebsd" | "netbsd" | "dragonfly" | "openbsd" => Some(PamImpl::OpenPam), + "illumos" | "solaris" => Some(PamImpl::Sun), + _ => None, + } +} + +/// The version of LibPAM installed on this machine (as found by `dlopen`). +pub fn currently_installed() -> Option<PamImpl> { + LibPam::open().map(|lib| { + if lib.has(b"pam_syslog\0") { + PamImpl::LinuxPam + } else if lib.has(b"_openpam_log\0") { + PamImpl::OpenPam + } else if lib.has(b"__pam_get_authtok\0") { + PamImpl::Sun + } else { + PamImpl::XSso + } + }) +} + +struct LibPam(NonNull<c_void>); + +impl LibPam { + fn open() -> Option<Self> { + let dlopen = |s: &[u8]| unsafe { libc::dlopen(s.as_ptr().cast(), libc::RTLD_LAZY) }; + NonNull::new(dlopen(b"libpam.so\0")) + .or_else(|| NonNull::new(dlopen(b"libpam.dylib\0"))) + .map(Self) + } + + fn has(&self, name: &[u8]) -> bool { + let symbol = unsafe { libc::dlsym(self.0.as_ptr(), name.as_ptr().cast()) }; + !symbol.is_null() + } +} + +impl Drop for LibPam { + fn drop(&mut self) { + unsafe { + libc::dlclose(self.0.as_ptr()); + } + } +}