Skip to main content

managed_lhapdf/
manager.rs

1//! Managing functions. These functions wrap the functions from LHAPDF that mail fail due to data
2//! not being downloaded. In that case we do the best to download them from locations and to a
3//! directory specified in our configuration file.
4
5use super::ffi::{self, PDFSet, PDF};
6use super::unmanaged;
7use super::{Error, Result};
8use cxx::UniquePtr;
9use flate2::read::GzDecoder;
10use fs2::FileExt;
11use serde::{Deserialize, Serialize};
12use std::env;
13use std::ffi::OsString;
14use std::fs::{self, File};
15use std::io::{self, ErrorKind, Write};
16use std::ops::Deref;
17use std::path::{Path, PathBuf};
18use std::sync::{Mutex, OnceLock};
19use tar::Archive;
20use url::Url;
21
22const LHAPDF_CONFIG: &str = "Verbosity: 1
23Interpolator: logcubic
24Extrapolator: continuation
25ForcePositive: 0
26AlphaS_Type: analytic
27MZ: 91.1876
28MUp: 0.002
29MDown: 0.005
30MStrange: 0.10
31MCharm: 1.29
32MBottom: 4.19
33MTop: 172.9
34Pythia6LambdaV5Compat: true
35";
36
37/// Configuration for this library.
38#[derive(Debug, Deserialize, Serialize)]
39#[serde(deny_unknown_fields)]
40pub struct Config {
41    lhapdf_data_path_read: Vec<PathBuf>,
42    lhapdf_data_path_write: PathBuf,
43    pdfsets_index_url: Url,
44    pdfset_urls: Vec<Url>,
45}
46
47impl Default for Config {
48    fn default() -> Self {
49        let mut config = Self {
50            lhapdf_data_path_read: vec![],
51            lhapdf_data_path_write: dirs::data_dir()
52                // if the data directory doesn't exist, first try the current directory and then a
53                // temporary directory
54                .unwrap_or_else(|| env::current_dir().unwrap_or_else(|_| env::temp_dir()))
55                .join("managed-lhapdf"),
56            // UNWRAP: a panic means the static string is malformed
57            pdfsets_index_url: Url::parse("https://lhapdfsets.web.cern.ch/current/pdfsets.index")
58                .unwrap(),
59            // UNWRAP: a panic means the static string is malformed
60            pdfset_urls: vec![Url::parse("https://lhapdfsets.web.cern.ch/current/").unwrap()],
61        };
62
63        // if there's an environment variable that the user set use its value
64        if let Some(os_str) = env::var_os("LHAPDF_DATA_PATH").or_else(|| env::var_os("LHAPATH")) {
65            let mut lhapdf_paths: Vec<_> =
66                // UNWRAP: if the string isn't valid unicode we can't proceed
67                os_str.to_str().unwrap().split(':').map(PathBuf::from).collect();
68
69            // we'll use the first entry to write to
70            config.lhapdf_data_path_write = lhapdf_paths.remove(0);
71            // we'll read from the remaining directories
72            config.lhapdf_data_path_read = lhapdf_paths;
73        }
74
75        config
76    }
77}
78
79fn get_url(url: &Url) -> Result<Box<dyn std::io::Read + Send + Sync + 'static>> {
80    ureq::request_url("GET", url)
81        .call()
82        .map_err(|err| match err {
83            // we need to catch 404 errors so we can potentially retry
84            ureq::Error::Status(404, _) => Error::Http404,
85            err @ _ => Error::Other(anyhow::Error::new(err)),
86        })
87        .map(ureq::Response::into_reader)
88}
89
90struct LhapdfData;
91
92impl Config {
93    /// Return the only instance of this type.
94    pub fn get() -> &'static Self {
95        static SINGLETON: OnceLock<Result<Config>> = OnceLock::new();
96
97        let config = SINGLETON.get_or_init(|| {
98            let config_path = dirs::config_dir()
99                .ok_or_else(|| Error::General("no configuration directory found".to_owned()))?;
100
101            // create the configuration directory if it doesn't exist yet - in practice this only
102            // happens in our CI
103            fs::create_dir_all(&config_path)?;
104
105            let config_path = config_path.join("managed-lhapdf.toml");
106
107            // TODO: it's possible that multiple processes try to create the default configuration
108            // file and/or that while the file is created, other processes try to read from it
109
110            // MSRV 1.77.0: use `File::create_new` instead
111            let config = match File::options()
112                .read(true)
113                .write(true)
114                .create_new(true)
115                .open(&config_path)
116            {
117                // the file didn't exist before
118                Ok(mut file) => {
119                    // use a default configuration
120                    let config = Config::default();
121                    file.write_all(toml::to_string_pretty(&config)?.as_bytes())?;
122                    config
123                }
124                Err(err) if err.kind() == ErrorKind::AlreadyExists => {
125                    // the file already exists, simply read it
126                    toml::from_str(&fs::read_to_string(&config_path)?)?
127                }
128                Err(err) => Err(err)?,
129            };
130
131            if let Some(lhapdf_data_path_write) = config.lhapdf_data_path_write() {
132                // create download directory for `lhapdf.conf`
133                fs::create_dir_all(lhapdf_data_path_write)?;
134
135                // MSRV 1.77.0: use `File::create_new` instead
136                if let Ok(mut file) = File::options()
137                    .read(true)
138                    .write(true)
139                    .create_new(true)
140                    .open(lhapdf_data_path_write.join("lhapdf.conf"))
141                {
142                    // if `lhapdf.conf` doesn't exist, create it
143                    file.write_all(LHAPDF_CONFIG.as_bytes())?;
144                }
145
146                let pdfsets_index = lhapdf_data_path_write.join("pdfsets.index");
147
148                // MSRV 1.77.0: use `File::create_new` instead
149                if let Ok(mut file) = File::options()
150                    .read(true)
151                    .write(true)
152                    .create_new(true)
153                    .open(pdfsets_index)
154                {
155                    // if `pdfsets.index` doesn't exist, download it
156                    let mut reader = get_url(config.pdfsets_index_url())?;
157                    io::copy(&mut reader, &mut file)?;
158                }
159            }
160
161            // we use the environment variable `LHAPDF_DATA_PATH` to let LHAPDF know where we've
162            // stored our PDFs
163
164            let lhapdf_data_path = config
165                .lhapdf_data_path_write()
166                .into_iter()
167                .chain(config.lhapdf_data_path_read.iter().map(Deref::deref))
168                .map(|path| path.as_os_str())
169                .collect::<Vec<_>>()
170                .join(&OsString::from(":"));
171            // as long as `static Config _cfg` in LHAPDF's `src/Config.cc` is `static` and not
172            // `thread_local`, this belongs here; otherwise move it out of the singleton
173            // initialization
174            unsafe { env::set_var("LHAPDF_DATA_PATH", lhapdf_data_path) };
175
176            Ok(config)
177        });
178
179        // TODO: change return type and propagate the result - difficult because we can't clone the
180        // error type
181        config.as_ref().unwrap()
182    }
183
184    /// Return the path where `managed-lhapdf` will download PDF sets and `pdfsets.index` to.
185    pub fn lhapdf_data_path_write(&self) -> Option<&Path> {
186        if self.lhapdf_data_path_write.as_os_str().is_empty() {
187            None
188        } else {
189            Some(&self.lhapdf_data_path_write)
190        }
191    }
192
193    /// Return the URL where the file `pdfsets.index` will downloaded from.
194    pub fn pdfsets_index_url(&self) -> &Url {
195        &self.pdfsets_index_url
196    }
197
198    /// Return the URLs that should be searched for PDF sets, if they are not available in the
199    /// local cache.
200    pub fn pdfset_urls(&self) -> &[Url] {
201        &self.pdfset_urls
202    }
203}
204
205impl From<toml::ser::Error> for Error {
206    fn from(err: toml::ser::Error) -> Self {
207        Self::Other(anyhow::Error::new(err))
208    }
209}
210
211impl From<toml::de::Error> for Error {
212    fn from(err: toml::de::Error) -> Self {
213        Self::Other(anyhow::Error::new(err))
214    }
215}
216
217impl From<url::ParseError> for Error {
218    fn from(err: url::ParseError) -> Self {
219        Self::Other(anyhow::Error::new(err))
220    }
221}
222
223impl LhapdfData {
224    fn get() -> &'static Mutex<Self> {
225        static SINGLETON: Mutex<LhapdfData> = Mutex::new(LhapdfData);
226        &SINGLETON
227    }
228
229    fn download_set(&self, name: &str, config: &Config) -> Result<()> {
230        if let Some(lhapdf_data_path_write) = config.lhapdf_data_path_write() {
231            let lock_file = File::create(lhapdf_data_path_write.join(format!("{name}.lock")))?;
232            lock_file.lock_exclusive()?;
233
234            for url in config.pdfset_urls() {
235                let response = get_url(&url.join(&format!("{name}.tar.gz"))?);
236
237                // if the URL didn't have the PDF set, try the next one
238                if let Err(Error::Http404) = response {
239                    continue;
240                }
241
242                Archive::new(GzDecoder::new(response?)).unpack(lhapdf_data_path_write)?;
243
244                // we found a PDF set, now it's LHAPDF's turn
245                break;
246            }
247
248            lock_file.unlock()?;
249        }
250
251        Ok(())
252    }
253
254    fn update_pdfsets_index(&self, config: &Config) -> Result<()> {
255        if let Some(lhapdf_data_path_write) = config.lhapdf_data_path_write() {
256            let lock_file = File::create(lhapdf_data_path_write.join("pdfsets.lock"))?;
257            lock_file.lock_exclusive()?;
258
259            // empty the `static thread_local` variable sitting in `getPDFIndex` to trigger the
260            // re-initialization of this variable
261            ffi::empty_lhaindex();
262
263            // download `pdfsets.index`
264            let mut reader = get_url(config.pdfsets_index_url())?;
265            io::copy(
266                &mut reader,
267                &mut File::create(lhapdf_data_path_write.join("pdfsets.index"))?,
268            )?;
269
270            lock_file.unlock()?;
271        }
272
273        Ok(())
274    }
275
276    pub fn pdf_name_and_member_via_lhaid(&self, lhaid: i32) -> Option<(String, i32)> {
277        unmanaged::pdf_name_and_member_via_lhaid(lhaid)
278    }
279
280    fn pdf_with_setname_and_member(&self, setname: &str, member: i32) -> Result<UniquePtr<PDF>> {
281        unmanaged::pdf_with_setname_and_member(setname, member)
282    }
283
284    fn pdfset_new(&self, setname: &str) -> Result<UniquePtr<PDFSet>> {
285        unmanaged::pdfset_new(setname)
286    }
287
288    fn set_verbosity(&self, verbosity: i32) {
289        unmanaged::set_verbosity(verbosity);
290    }
291
292    fn verbosity(&self) -> i32 {
293        unmanaged::verbosity()
294    }
295}
296
297pub fn pdf_name_and_member_via_lhaid(lhaid: i32) -> Option<(String, i32)> {
298    // this must be the first call before anything from LHAPDF
299    let config = Config::get();
300
301    // TODO: change return type of this function and handle the error properly
302    let lock = LhapdfData::get().lock().unwrap();
303
304    lock.pdf_name_and_member_via_lhaid(lhaid).or_else(|| {
305        // TODO: change return type of this function and handle the error properly
306        lock.update_pdfsets_index(config).unwrap();
307        lock.pdf_name_and_member_via_lhaid(lhaid)
308    })
309}
310
311pub fn pdf_with_setname_and_member(setname: &str, member: i32) -> Result<UniquePtr<PDF>> {
312    // this must be the first call before anything from LHAPDF
313    let config = Config::get();
314
315    // TODO: handle error properly
316    let lock = LhapdfData::get().lock().unwrap();
317
318    lock.pdf_with_setname_and_member(setname, member)
319        .or_else(|err: Error| {
320            // here we rely on exactly matching LHAPDF's exception string
321            if err.to_string() == format!("Info file not found for PDF set '{setname}'") {
322                lock.download_set(setname, config)
323                    .and_then(|()| lock.pdf_with_setname_and_member(setname, member))
324            } else {
325                Err(err)
326            }
327        })
328}
329
330pub fn pdfset_new(setname: &str) -> Result<UniquePtr<PDFSet>> {
331    // this must be the first call before anything from LHAPDF
332    let config = Config::get();
333
334    // TODO: handle error properly
335    let lock = LhapdfData::get().lock().unwrap();
336
337    lock.pdfset_new(setname).or_else(|err: Error| {
338        // here we rely on exactly matching LHAPDF's exception string
339        if err.to_string() == format!("Info file not found for PDF set '{setname}'") {
340            lock.download_set(setname, config)
341                .and_then(|()| lock.pdfset_new(setname))
342        } else {
343            Err(err)
344        }
345    })
346}
347
348pub fn set_verbosity(verbosity: i32) {
349    // this must be the first call before anything from LHAPDF
350    let _ = Config::get();
351
352    // TODO: handle error properly
353    let lock = LhapdfData::get().lock().unwrap();
354
355    lock.set_verbosity(verbosity);
356}
357
358pub fn verbosity() -> i32 {
359    // this must be the first call before anything from LHAPDF
360    let _ = Config::get();
361
362    // TODO: handle error properly
363    let lock = LhapdfData::get().lock().unwrap();
364
365    lock.verbosity()
366}