pkgcraft/pkg/ebuild/
raw.rs

1use std::sync::{Arc, OnceLock};
2use std::time::Duration;
3use std::{fmt, fs};
4
5use camino::Utf8PathBuf;
6use indexmap::IndexMap;
7
8use crate::bash;
9use crate::dep::{Cpv, Dep};
10use crate::eapi::{self, Eapi};
11use crate::error::Error;
12use crate::macros::bool_not_equal;
13use crate::pkg::{Package, RepoPackage, make_pkg_traits};
14use crate::repo::ebuild::cache::{Cache, CacheEntry};
15use crate::repo::{EbuildRepo, Repository};
16use crate::traits::{FilterLines, Intersects};
17
18use super::metadata::Metadata;
19
20#[derive(Clone)]
21struct InternalEbuildRawPkg {
22    pub(super) cpv: Cpv,
23    pub(super) repo: EbuildRepo,
24    pub(super) eapi: &'static Eapi,
25    data: Arc<str>,
26    chksum: String,
27    tree: OnceLock<bash::Tree>,
28}
29
30#[derive(Clone)]
31pub struct EbuildRawPkg(Arc<InternalEbuildRawPkg>);
32
33make_pkg_traits!(EbuildRawPkg);
34
35impl fmt::Debug for EbuildRawPkg {
36    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
37        write!(f, "EbuildRawPkg {{ {self} }}")
38    }
39}
40
41impl EbuildRawPkg {
42    pub(crate) fn try_new(cpv: Cpv, repo: &EbuildRepo) -> crate::Result<Self> {
43        let relpath = cpv.relpath();
44        let data =
45            fs::read_to_string(repo.path().join(&relpath)).map_err(|e| Error::InvalidPkg {
46                cpv: Box::new(cpv.clone()),
47                repo: repo.to_string(),
48                err: Box::new(Error::InvalidValue(format!(
49                    "failed reading ebuild: {relpath}: {e}"
50                ))),
51            })?;
52
53        let eapi = Self::parse_eapi(&data).map_err(|error| Error::InvalidPkg {
54            cpv: Box::new(cpv.clone()),
55            repo: repo.to_string(),
56            err: Box::new(error),
57        })?;
58
59        let chksum = repo.metadata().cache().chksum(&data);
60        let repo = repo.clone();
61        let tree = Default::default();
62        Ok(Self(Arc::new(InternalEbuildRawPkg {
63            cpv,
64            repo,
65            eapi,
66            data: data.into(),
67            chksum,
68            tree,
69        })))
70    }
71
72    /// Get the parsed EAPI from the given ebuild data content.
73    fn parse_eapi(data: &str) -> crate::Result<&'static Eapi> {
74        let s = data
75            .filter_lines()
76            .next()
77            .and_then(|(_, s)| s.strip_prefix("EAPI="))
78            .map(|s| {
79                s.split_once('#')
80                    .map(|(v, _)| v.trim())
81                    .unwrap_or_else(|| s.trim())
82            })
83            .unwrap_or("0");
84
85        eapi::parse_value(s)?.parse()
86    }
87
88    /// Return the path of the package's ebuild relative to the repository root.
89    pub fn relpath(&self) -> Utf8PathBuf {
90        self.0.cpv.relpath()
91    }
92
93    /// Return the absolute path of the package's ebuild.
94    pub fn path(&self) -> Utf8PathBuf {
95        self.0.repo.path().join(self.relpath())
96    }
97
98    /// Return the package's ebuild file content.
99    pub fn data(&self) -> &str {
100        &self.0.data
101    }
102
103    /// Return the checksum of the package's ebuild file content.
104    pub fn chksum(&self) -> &str {
105        &self.0.chksum
106    }
107
108    /// Return the bash parse tree for the ebuild.
109    pub fn tree(&self) -> &bash::Tree {
110        self.0.tree.get_or_init(|| {
111            // HACK: figure out better method for self-referential lifetimes
112            let data = Arc::clone(&self.0.data);
113            bash::Tree::new(data)
114        })
115    }
116
117    /// Try to deserialize the package's metadata from the cache.
118    fn get_metadata(&self) -> crate::Result<Metadata> {
119        self.0
120            .repo
121            .metadata()
122            .cache()
123            .get(self)
124            .ok_or_else(|| Error::InvalidValue(format!("{self}: missing metadata entry")))
125            .flatten()
126            .and_then(|entry| entry.to_metadata(self))
127    }
128
129    /// Deserialize or regenerate a package's metadata.
130    pub(crate) fn metadata(&self, regen_on_failure: bool) -> crate::Result<Metadata> {
131        self.get_metadata().or_else(|e| {
132            if regen_on_failure {
133                self.0
134                    .repo
135                    .pool()
136                    .metadata_task(&self.0.repo)
137                    .force(true)
138                    .run(&self.0.cpv)?;
139                self.get_metadata()
140            } else {
141                Err(e)
142            }
143        })
144    }
145
146    /// Return the mapping of global environment variables exported by the package.
147    pub fn env(&self) -> crate::Result<IndexMap<String, String>> {
148        let repo = &self.0.repo;
149        self.0.repo.pool().env(repo, &self.0.cpv)
150    }
151
152    /// Return the time duration required to source the package.
153    pub fn duration(&self) -> crate::Result<Duration> {
154        let repo = &self.0.repo;
155        self.0.repo.pool().duration(repo, &self.0.cpv)
156    }
157}
158
159impl Package for EbuildRawPkg {
160    fn eapi(&self) -> &'static Eapi {
161        self.0.eapi
162    }
163
164    fn cpv(&self) -> &Cpv {
165        &self.0.cpv
166    }
167}
168
169impl RepoPackage for EbuildRawPkg {
170    type Repo = EbuildRepo;
171
172    fn repo(&self) -> Self::Repo {
173        self.0.repo.clone()
174    }
175}
176
177impl Intersects<Dep> for EbuildRawPkg {
178    fn intersects(&self, dep: &Dep) -> bool {
179        bool_not_equal!(self.cpn(), dep.cpn());
180
181        if dep.slot_dep().is_some() {
182            return false;
183        }
184
185        if dep.use_deps().is_some() {
186            return false;
187        }
188
189        if let Some(val) = dep.repo() {
190            bool_not_equal!(self.0.repo.name(), val);
191        }
192
193        if let Some(val) = dep.version() {
194            self.cpv().version().intersects(val)
195        } else {
196            true
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use crate::config::Config;
204    use crate::eapi::EAPI8;
205    use crate::repo::ebuild::EbuildRepoBuilder;
206    use crate::test::test_data;
207
208    use super::*;
209
210    #[test]
211    fn display_and_debug() {
212        let data = test_data();
213        let repo = data.ebuild_repo("metadata").unwrap();
214        let pkg = repo.iter_raw().next().unwrap().unwrap();
215        let s = pkg.to_string();
216        assert!(format!("{pkg:?}").contains(&s));
217    }
218
219    #[test]
220    fn relpath() {
221        let data = test_data();
222        let repo = data.ebuild_repo("metadata").unwrap();
223        let raw_pkg = repo.get_pkg_raw("optional/none-8").unwrap();
224        assert_eq!(raw_pkg.relpath(), "optional/none/none-8.ebuild");
225    }
226
227    #[test]
228    fn path() {
229        let data = test_data();
230        let repo = data.ebuild_repo("metadata").unwrap();
231        let raw_pkg = repo.get_pkg_raw("optional/none-8").unwrap();
232        assert_eq!(raw_pkg.path(), repo.path().join("optional/none/none-8.ebuild"));
233    }
234
235    #[test]
236    fn data() {
237        let mut config = Config::default();
238        let mut temp = EbuildRepoBuilder::new().build().unwrap();
239        let repo = config.add_repo(&temp).unwrap().into_ebuild().unwrap();
240        config.finalize().unwrap();
241
242        let data = indoc::indoc! {r#"
243            EAPI=8
244            DESCRIPTION="testing data content"
245            SLOT=0
246        "#};
247        temp.create_ebuild_from_str("cat/pkg-1", data).unwrap();
248        let raw_pkg = repo.get_pkg_raw("cat/pkg-1").unwrap();
249        assert_eq!(raw_pkg.data(), data);
250        assert!(!raw_pkg.chksum().is_empty());
251    }
252
253    #[test]
254    fn traits() {
255        let data = test_data();
256        let repo = data.ebuild_repo("metadata").unwrap();
257        let raw_pkg = repo.get_pkg_raw("optional/none-8").unwrap();
258        assert_eq!(raw_pkg.eapi(), &*EAPI8);
259        assert_eq!(raw_pkg.cpv().to_string(), "optional/none-8");
260        assert_eq!(&raw_pkg.repo(), repo);
261    }
262
263    #[test]
264    fn intersects_dep() {
265        let data = test_data();
266        let repo = data.ebuild_repo("commands").unwrap();
267        let raw_pkg = repo.get_pkg_raw("cat/pkg-1").unwrap();
268
269        for (s, expected) in [
270            ("cat/pkg", true),
271            ("=cat/pkg-0", false),
272            ("=cat/pkg-1", true),
273            ("cat/pkg:0", false),
274            ("cat/pkg:0/1", false),
275            ("cat/pkg[u]", false),
276            ("cat/pkg::test", false),
277            ("cat/pkg::commands", true),
278        ] {
279            let dep: Dep = s.parse().unwrap();
280            assert_eq!(raw_pkg.intersects(&dep), expected, "failed for {s}");
281        }
282    }
283}