Mercurial > crates > nonstick
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 } |