comparison 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
comparison
equal deleted inserted replaced
189:b2456d274576 190:995aca290452
1 //! Information about the PAM implementation you're using right now. 1 #![allow(clippy::needless_doctest_main)]
2 //! 2 //! An enumeration of PAM implementations and tools to detect them.
3 //! This module contains constants and values that can be used at build-script, 3 //!
4 //! compile, and run time to determine what PAM implementation you're using. 4 //! # Configuration
5 //! 5 //!
6 //! ## Always available 6 //! When used at compile time, this crate uses the target OS by default,
7 //! 7 //! but can be overridden with the `LIBPAMSYS_IMPL` environment variable.
8 //! [`PamImpl::CURRENT`] will tell you what version of PAM you're using. 8 //! See the documentation of [`build_target_impl`] for details.
9 //! It can be imported in any Rust code, from build scripts to runtime. 9 //!
10 //! 10 //! # Detecting PAM
11 //! ## Compile time 11 //!
12 //! ## Build time
12 //! 13 //!
13 //! Use [`enable_pam_impl_cfg`] in your `build.rs` to generate custom `#[cfg]`s 14 //! Use [`enable_pam_impl_cfg`] in your `build.rs` to generate custom `#[cfg]`s
14 //! for conditional compilation based on PAM implementation. 15 //! for conditional compilation based on PAM implementation.
15 //! 16 //!
16 //! ``` 17 //! To detect the implementation that will be used at runtime, use the
17 //! // Your package's build.rs: 18 //! [`build_target_impl`] function.
18 //! 19 //!
19 //! fn main() { 20 //! ## Run time
20 //! // Also available at libpam_sys::pam_impl::enable_pam_impl_cfg(). 21 //!
21 //! libpam_sys_impls::enable_pam_impl_cfg(); 22 //! The implementation of PAM installed on the machine where the code is running
22 //! // whatever else you do in your build script. 23 //! can be detected with [`currently_installed`], or you can use
23 //! } 24 //! [`os_default`] to see what implementation is used on a given target.
24 //! ``` 25
25 //! 26 use std::env;
26 //! This will set the current `pam_impl` as well as registering all known 27 use std::env::VarError;
27 //! PAM implementations with `rustc-check-cfg` to get cfg-checking. 28 use std::ffi::c_void;
28 //! 29 use std::ptr::NonNull;
29 //! The names that appear in the `cfg` variables are the same as the values 30
30 //! in the [`PamImpl`] enum. 31 /// An enum that knows its own values.
31 //! 32 macro_rules! self_aware_enum {
32 //! ```ignore 33 (
33 //! #[cfg(pam_impl = "OpenPam")] 34 $(#[$enumeta:meta])*
34 //! fn openpam_specific_func(handle: *const libpam_sys::pam_handle) { 35 $viz:vis enum $name:ident {
35 //! let environ = libpam_sys::pam_getenvlist(handle); 36 $(
36 //! // ... 37 $(#[$itemeta:meta])*
37 //! libpam_sys::openpam_free_envlist() 38 $item:ident,
38 //! } 39 )*
39 //! 40 }
40 //! // This will give you a warning since "UnknownImpl" is not in the cfg. 41 ) => {
41 //! #[cfg(not(pam_impl = "UnknownImpl"))] 42 $(#[$enumeta])*
42 //! fn do_something() { 43 $viz enum $name {
43 //! // ... 44 $(
44 //! } 45 $(#[$itemeta])*
45 //! ``` 46 $item,
46 //! 47 )*
47 //! The [`pam_impl_name!`] macro will expand to this same value, currently 48 }
48 #![doc = concat!("`", env!("LIBPAMSYS_IMPL"), "`.")] 49
49 50 // The implementations in this block are private for now
50 mod pam_impl; 51 // to avoid putting a contract into the public API.
51 52 #[allow(dead_code)]
52 #[doc(inline)] 53 impl $name {
53 pub use pam_impl::*; 54 /// Iterator over the items in the enum. For internal use.
55 pub(crate) fn items() -> Vec<Self> {
56 vec![$(Self::$item),*]
57 }
58
59 /// Attempts to parse the enum from the string. For internal use.
60 pub(crate) fn try_from(value: &str) -> Result<Self, String> {
61 match value {
62 $(stringify!($item) => Ok(Self::$item),)*
63 _ => Err(value.into()),
64 }
65 }
66 }
67 };
68 }
69
70 self_aware_enum! {
71 /// The PAM implementations supported by `libpam-sys`.
72 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
73 #[non_exhaustive]
74 pub enum PamImpl {
75 /// [Linux-PAM] is provided by most Linux distributions.
76 ///
77 /// [Linux-PAM]: https://github.com/linux-pam/linux-pam
78 LinuxPam,
79 /// [OpenPAM] is used by most BSDs, including Mac OS X.
80 ///
81 /// [OpenPAM]: https://git.des.dev/OpenPAM/OpenPAM
82 OpenPam,
83 /// Illumos and Solaris use a derivative of [Sun's implementation][sun].
84 ///
85 /// [sun]: https://code.illumos.org/plugins/gitiles/illumos-gate/+/refs/heads/master/usr/src/lib/libpam
86 Sun,
87 /// Only the functionality and constants in [the PAM spec].
88 ///
89 /// [the PAM spec]: https://pubs.opengroup.org/onlinepubs/8329799/toc.htm
90 XSso,
91 }
92 }
93
94 #[allow(clippy::needless_doctest_main)]
95 /// Generates `cargo` directives for build scripts to enable `cfg(pam_impl)`.
96 ///
97 /// Print this in your `build.rs` script to be able to use the custom `pam_impl`
98 /// configuration directive.
99 ///
100 /// ```
101 /// // Your package's build.rs:
102 ///
103 /// fn main() {
104 /// // Also available at libpam_sys::pam_impl::enable_pam_impl_cfg().
105 /// libpam_sys_impls::enable_pam_impl_cfg();
106 /// // whatever else you do in your build script.
107 /// }
108 /// ```
109 ///
110 /// This will set the current `pam_impl` as well as registering all known
111 /// PAM implementations with `rustc-check-cfg` to get cfg-checking.
112 ///
113 /// The names that appear in the `cfg` variables are the same as the values
114 /// in the [`PamImpl`] enum.
115 ///
116 /// ```ignore
117 /// #[cfg(pam_impl = "OpenPam")]
118 /// fn openpam_specific_func(handle: *const libpam_sys::pam_handle) {
119 /// let environ = libpam_sys::pam_getenvlist(handle);
120 /// // ...
121 /// libpam_sys::openpam_free_envlist()
122 /// }
123 ///
124 /// // This will give you a warning since "UnknownImpl" is not a known
125 /// // PAM implementation.
126 /// #[cfg(not(pam_impl = "UnknownImpl"))]
127 /// fn do_something() {
128 /// // ...
129 /// }
130 /// ```
131 pub fn enable_pam_impl_cfg() {
132 println!("{}", pam_impl_cfg_string())
133 }
134
135 /// [`enable_pam_impl_cfg`], but returned as a string.
136 pub fn pam_impl_cfg_string() -> String {
137 generate_cfg(build_target_impl())
138 }
139
140 fn generate_cfg(pam_impl: Option<PamImpl>) -> String {
141 let impls: Vec<_> = PamImpl::items()
142 .into_iter()
143 .map(|i| format!(r#""{i:?}""#))
144 .collect();
145 let mut lines = vec![
146 format!(
147 "cargo:rustc-check-cfg=cfg(pam_impl, values({impls}))",
148 impls = impls.join(",")
149 ),
150 "cargo:rustc-cfg=pam_impl".into(),
151 ];
152 if let Some(pam_impl) = pam_impl {
153 lines.push("cargo:rustc-cfg=pam_impl".into());
154 lines.push(format!("cargo:rustc-cfg=pam_impl=\"{pam_impl:?}\""));
155 }
156 lines.join("\n")
157 }
158
159 /// The strategy to use to detect PAM.
160 enum Detect {
161 /// Use the default PAM implementation based on the target OS.
162 TargetDefault,
163 /// Detect the installed implementation.
164 Installed,
165 /// Use the named version of PAM.
166 Specified(PamImpl),
167 }
168
169 const INSTALLED: &str = "__installed__";
170
171 /// For `build.rs` use: Detects the PAM implementation that should be used
172 /// for the target of the currently-running build script.
173 ///
174 /// # Configuration
175 ///
176 /// The PAM implementation selected depends upon the value of the
177 /// `LIBPAMSYS_IMPL` environment variable.
178 ///
179 /// - Empty or unset (default): Use the default PAM implementation for the
180 /// Cargo target OS (as specified by `CARGO_CFG_TARGET_OS`).
181 /// - Linux: Linux-PAM
182 /// - BSD (and Mac): OpenPAM
183 /// - Illumos/Solaris: Sun PAM
184 /// - `__installed__`: Use the PAM implementation installed on the host system.
185 /// This opens the `libpam` library and looks for specific functions.
186 /// - The name of a [PamImpl] member: Use that PAM implementation.
187 ///
188 /// # Panics
189 ///
190 /// If an unknown PAM implementation is provided in `LIBPAMSYS_IMPL`.
191 pub fn build_target_impl() -> Option<PamImpl> {
192 let detection = match env::var("LIBPAMSYS_IMPL").as_deref() {
193 Ok("") | Err(VarError::NotPresent) => Detect::TargetDefault,
194 Ok(INSTALLED) => Detect::Installed,
195 Ok(val) => Detect::Specified(PamImpl::try_from(val).unwrap_or_else(|_| {
196 panic!(
197 "unknown PAM implementation {val:?}. \
198 valid LIBPAMSYS_IMPL values are {:?}, \
199 {INSTALLED:?} to use the currently-installed version, \
200 or unset to use the OS default",
201 PamImpl::items()
202 )
203 })),
204 Err(other) => panic!("Couldn't detect PAM version: {other}"),
205 };
206 match detection {
207 Detect::TargetDefault => env::var("CARGO_CFG_TARGET_OS")
208 .ok()
209 .as_deref()
210 .and_then(os_default),
211 Detect::Installed => currently_installed(),
212 Detect::Specified(other) => Some(other),
213 }
214 }
215
216 /// Gets the PAM version based on the target OS.
217 ///
218 /// The target OS name passed in is one of the [Cargo target OS values][os].
219 ///
220 /// [os]: https://doc.rust-lang.org/reference/conditional-compilation.html#r-cfg.target_os.values
221 pub fn os_default(target_os: &str) -> Option<PamImpl> {
222 match target_os {
223 "linux" => Some(PamImpl::LinuxPam),
224 "macos" | "freebsd" | "netbsd" | "dragonfly" | "openbsd" => Some(PamImpl::OpenPam),
225 "illumos" | "solaris" => Some(PamImpl::Sun),
226 _ => None,
227 }
228 }
229
230 /// The version of LibPAM installed on this machine (as found by `dlopen`).
231 pub fn currently_installed() -> Option<PamImpl> {
232 LibPam::open().map(|lib| {
233 if lib.has(b"pam_syslog\0") {
234 PamImpl::LinuxPam
235 } else if lib.has(b"_openpam_log\0") {
236 PamImpl::OpenPam
237 } else if lib.has(b"__pam_get_authtok\0") {
238 PamImpl::Sun
239 } else {
240 PamImpl::XSso
241 }
242 })
243 }
244
245 struct LibPam(NonNull<c_void>);
246
247 impl LibPam {
248 fn open() -> Option<Self> {
249 let dlopen = |s: &[u8]| unsafe { libc::dlopen(s.as_ptr().cast(), libc::RTLD_LAZY) };
250 NonNull::new(dlopen(b"libpam.so\0"))
251 .or_else(|| NonNull::new(dlopen(b"libpam.dylib\0")))
252 .map(Self)
253 }
254
255 fn has(&self, name: &[u8]) -> bool {
256 let symbol = unsafe { libc::dlsym(self.0.as_ptr(), name.as_ptr().cast()) };
257 !symbol.is_null()
258 }
259 }
260
261 impl Drop for LibPam {
262 fn drop(&mut self) {
263 unsafe {
264 libc::dlclose(self.0.as_ptr());
265 }
266 }
267 }