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