oro_common/
build_manifest.rs

1use std::{
2    collections::HashMap,
3    ffi::OsStr,
4    path::{Path, PathBuf},
5};
6
7use serde::{Deserialize, Serialize};
8use walkdir::WalkDir;
9
10use crate::{Bin, Directories, Manifest};
11
12#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14struct RawBuildManifest {
15    #[serde(default)]
16    pub name: Option<String>,
17
18    #[serde(default)]
19    pub bin: Option<Bin>,
20
21    #[serde(default)]
22    pub directories: Option<Directories>,
23
24    #[serde(default)]
25    pub scripts: HashMap<String, String>,
26}
27
28/// Manifest intended for use with the `build` step in orogene's installer. It
29/// reads and normalizes a package.json's bins (including the
30/// `directories.bin` field), and its scripts object.
31#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct BuildManifest {
34    /// Mapping of bin name to the relative path to the script/binary.
35    #[serde(default)]
36    pub bin: HashMap<String, PathBuf>,
37
38    /// package.json scripts object.
39    #[serde(default)]
40    pub scripts: HashMap<String, String>,
41}
42
43impl BuildManifest {
44    /// Create a new [`BuildManifest`] from a given path to a full manifest (package.json),
45    /// normalizing its bin field (or its `directories.bin`) into a plain HashMap.
46    pub fn from_path(path: impl AsRef<Path>) -> std::io::Result<Self> {
47        let path = path.as_ref();
48        let pkg_str = std::fs::read_to_string(path)?;
49        let raw: RawBuildManifest = serde_json::from_str(&pkg_str)?;
50        Self::normalize(raw)
51    }
52
53    /// Create a new [`BuildManifest`] from an already fully loaded [`Manifest`],
54    /// normalizing its bin field (or its `directories.bin`) into a plain HashMap.
55    pub fn from_manifest(manifest: &Manifest) -> std::io::Result<Self> {
56        // This is a bit ineffecient but honestly it's not a big deal,
57        // we already did a bunch of I/O to get the Manifest.
58        let raw = RawBuildManifest {
59            name: manifest.name.clone(),
60            bin: manifest.bin.clone(),
61            directories: manifest.directories.clone(),
62            scripts: manifest.scripts.clone(),
63        };
64        Self::normalize(raw)
65    }
66
67    fn normalize(raw: RawBuildManifest) -> std::io::Result<Self> {
68        let mut bin_map = HashMap::new();
69        if let Some(Bin::Hash(bins)) = raw.bin {
70            for (name, bin) in bins {
71                bin_map.insert(name, bin);
72            }
73        } else if let Some(Bin::Str(bin)) = raw.bin {
74            if let Some(name) = raw.name {
75                bin_map.insert(name, PathBuf::from(bin));
76            }
77        } else if let Some(Bin::Array(bins)) = raw.bin {
78            for bin in bins {
79                let name = bin
80                    .as_path()
81                    .file_name()
82                    .ok_or_else(|| {
83                        std::io::Error::new(
84                            std::io::ErrorKind::InvalidData,
85                            format!("invalid bin name: {}", bin.to_string_lossy()),
86                        )
87                    })?
88                    .to_string_lossy()
89                    .to_string();
90                bin_map.insert(name, bin);
91            }
92        } else if let Some(Directories {
93            bin: Some(bin_dir), ..
94        }) = raw.directories
95        {
96            for entry in WalkDir::new(bin_dir) {
97                let entry = entry?;
98                let path = entry.path();
99                if path.starts_with(".") {
100                    continue;
101                }
102                if let Some(file_name) = path.file_name() {
103                    bin_map.insert(file_name.to_string_lossy().to_string(), path.into());
104                }
105            }
106        };
107        let mut normalized = HashMap::new();
108        for (name, bin) in &bin_map {
109            let base = Path::new(name).file_name();
110            if base.is_none() || base == Some(OsStr::new("")) {
111                continue;
112            }
113            let base = Path::new("/")
114                .join(Path::new(
115                    &base
116                        .unwrap()
117                        .to_string_lossy()
118                        .to_string()
119                        .replace(['\\', ':'], "/"),
120                ))
121                .strip_prefix(
122                    #[cfg(windows)]
123                    "\\",
124                    #[cfg(not(windows))]
125                    "/",
126                )
127                .expect("We added this ourselves")
128                .file_name()
129                .map(PathBuf::from);
130            if base.is_none() || base == Some(PathBuf::from("")) {
131                continue;
132            }
133
134            let base = base.unwrap();
135
136            let bin_target = Path::new("/")
137                .join(bin.to_string_lossy().to_string())
138                .strip_prefix(
139                    #[cfg(windows)]
140                    "\\",
141                    #[cfg(not(windows))]
142                    "/",
143                )
144                .expect("We added this ourselves")
145                .to_path_buf();
146            if bin_target == Path::new("") {
147                continue;
148            }
149
150            normalized.insert(base.to_string_lossy().to_string(), bin_target);
151        }
152        Ok(Self {
153            bin: normalized,
154            scripts: raw.scripts,
155        })
156    }
157}