buck_resources/
lib.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under both the MIT license found in the
5 * LICENSE-MIT file in the root directory of this source tree and the Apache
6 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
7 * of this source tree.
8 */
9
10mod manifest;
11
12use std::collections::HashMap;
13use std::env;
14use std::fs;
15use std::io;
16use std::path::PathBuf;
17
18use once_cell::sync::OnceCell;
19use serde::de::DeserializeSeed as _;
20use thiserror::Error;
21
22use crate::manifest::ResourcesMap;
23
24#[derive(Debug, Error)]
25pub enum BuckResourcesError {
26    #[error("Failed to look up our own executable path")]
27    NoCurrentExe { source: io::Error },
28
29    #[error(
30        "Failed to read manifest file: `{manifest_path}`. \
31        Are you maybe running `buck1`? `rust_binary` only supports `resources` under `buck2`!"
32    )]
33    ReadFailed {
34        manifest_path: PathBuf,
35        source: io::Error,
36    },
37
38    #[error("Failed to parse manifest file: `{manifest_path}`")]
39    ParsingFailed {
40        manifest_path: PathBuf,
41        source: serde_json::Error,
42    },
43
44    #[error("No resource named `{name}` found in manifest file: `{manifest_path}`")]
45    NoSuchResource {
46        name: String,
47        manifest_path: PathBuf,
48    },
49
50    #[error(
51        "Resource `{name}` points to invalid path `{resource_path}` in manifest `{manifest_path}`"
52    )]
53    BadResourcePath {
54        name: String,
55        resource_path: PathBuf,
56        manifest_path: PathBuf,
57        source: io::Error,
58    },
59}
60
61/// Look up a resource based on a manifest file. Built to work seamlessly
62/// with `resources` defined in a `rust_binary` target, but in principle
63/// it would work with any correct manifest file.
64///
65/// Resources follow the naming format:
66///
67/// ```text
68/// {PATH_TO_TARGETS_FOLDER}/{TARGET_NAME}
69/// ```
70///
71/// So for `//path/to:target`, the resource is named `path/to/target`.
72///
73/// Still unsure about a resource path? Inspect the JSON manifest file
74/// found in the `BuckResourcesError`.
75///
76/// * Manifest location: `$CUR_EXE.resources.json`, where `$CUR_EXE` is
77///   the absolute path of the currently executing binary.
78/// * Relative paths in the manifest are resolved relative to the location
79///   of the currently executing binary.
80pub fn get<S>(name: S) -> Result<PathBuf, BuckResourcesError>
81where
82    S: AsRef<str>,
83{
84    static MANIFEST: OnceCell<(PathBuf, HashMap<String, PathBuf>)> = OnceCell::new();
85
86    let (manifest_path, manifest) = MANIFEST.get_or_try_init(|| {
87        let manifest_path = match env::current_exe() {
88            Ok(mut value) => {
89                value.as_mut_os_string().push(".resources.json");
90                value
91            }
92            Err(source) => {
93                return Err(BuckResourcesError::NoCurrentExe { source });
94            }
95        };
96
97        let data = match fs::read(&manifest_path) {
98            Ok(x) => x,
99            Err(source) => {
100                return Err(BuckResourcesError::ReadFailed {
101                    manifest_path,
102                    source,
103                });
104            }
105        };
106
107        let base_dir = manifest_path.parent().unwrap_or(&manifest_path);
108
109        let deserializer = &mut serde_json::Deserializer::from_slice(&data);
110        let manifest = match ResourcesMap::new(base_dir).deserialize(deserializer) {
111            Ok(x) => x,
112            Err(source) => {
113                return Err(BuckResourcesError::ParsingFailed {
114                    manifest_path,
115                    source,
116                });
117            }
118        };
119
120        Ok((manifest_path, manifest))
121    })?;
122
123    if let Some(resource_path) = manifest.get(name.as_ref()) {
124        dunce::canonicalize(resource_path).map_err(|source| BuckResourcesError::BadResourcePath {
125            name: name.as_ref().to_owned(),
126            resource_path: resource_path.clone(),
127            manifest_path: manifest_path.clone(),
128            source,
129        })
130    } else {
131        Err(BuckResourcesError::NoSuchResource {
132            name: name.as_ref().to_owned(),
133            manifest_path: manifest_path.clone(),
134        })
135    }
136}