apt_cmd/
apt_cache.rs

1// Copyright 2021-2022 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4use anyhow::Context;
5use as_result::{IntoResult, MapResult};
6use futures::stream::{Stream, StreamExt};
7use std::collections::HashMap;
8use std::io;
9use std::pin::Pin;
10use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
11use tokio::process::{Child, ChildStdout, Command};
12use tokio_stream::wrappers::LinesStream;
13
14pub type PackageStream = Pin<Box<dyn Stream<Item = String>>>;
15
16#[derive(Debug, Clone)]
17pub struct Policy {
18    pub package: String,
19    pub installed: String,
20    pub candidate: String,
21    pub version_table: HashMap<String, Vec<String>>,
22}
23
24pub type Policies = Pin<Box<dyn Stream<Item = Policy>>>;
25
26pub fn policies(lines: impl Stream<Item = io::Result<String>>) -> impl Stream<Item = Policy> {
27    async_stream::stream! {
28        futures::pin_mut!(lines);
29
30        let mut policy = Policy {
31            package: String::new(),
32            installed: String::new(),
33            candidate: String::new(),
34            version_table: HashMap::new()
35        };
36
37        while let Some(Ok(line)) = lines.next().await {
38            if line.is_empty() {
39                continue
40            }
41
42            if !line.starts_with(' ') {
43                policy.package = line;
44                if policy.package.ends_with(':') {
45                    policy.package.truncate(policy.package.len() - 1);
46                }
47                continue
48            }
49
50            let line = line.trim();
51
52            if line.starts_with('I') {
53                if let Some(v) = line.split_ascii_whitespace().nth(1) {
54                    policy.installed = v.to_owned();
55                }
56            } else if line.starts_with('C') {
57                if let Some(v) = line.split_ascii_whitespace().nth(1) {
58                    policy.candidate = v.to_owned();
59                }
60            } else if line.starts_with('V') {
61                // Start parsing the version table
62                let mut current_version = String::from("unknown");
63                while let Some(Ok(line)) = lines.next().await {
64
65
66                    if let Some(source) = line.strip_prefix("      ") {
67                        policy.version_table.entry(current_version.clone())
68                            .or_insert_with(Vec::new)
69                            .push(source.trim().to_owned());
70                    } else if let Some(version_and_pin) = line.strip_prefix(" *** ") {
71                        let mut current_version_and_pin = version_and_pin.trim().split_whitespace();
72                        if let Some(version) = current_version_and_pin.next() {
73                            current_version = version.to_owned();
74                        }
75                        // TODO: Store & use pin.
76                    } else if let Some(version_and_pin) = line.strip_prefix("   ") {
77                        let mut current_version_and_pin = version_and_pin.trim().split_whitespace();
78                        if let Some(version) = current_version_and_pin.next() {
79                            current_version = version.to_owned();
80                        }
81                        // TODO: Store & use pin.
82                    } else {
83                        yield policy.clone();
84                        policy.version_table.clear();
85                        policy.package = line;
86                        if policy.package.ends_with(':') {
87                            policy.package.truncate(policy.package.len() - 1);
88                        }
89                        break
90                    }
91                }
92            }
93        }
94
95        yield policy;
96    }
97}
98
99#[derive(AsMut, Deref, DerefMut)]
100#[as_mut(forward)]
101pub struct AptCache(Command);
102
103impl AptCache {
104    #[allow(clippy::new_without_default)]
105    pub fn new() -> Self {
106        let mut cmd = Command::new("apt-cache");
107        cmd.env("LANG", "C");
108        Self(cmd)
109    }
110
111    pub async fn depends<I, S>(mut self, packages: I) -> io::Result<(Child, ChildStdout)>
112    where
113        I: IntoIterator<Item = S>,
114        S: AsRef<std::ffi::OsStr>,
115    {
116        self.arg("depends");
117        self.args(packages);
118        self.spawn_with_stdout().await
119    }
120
121    pub async fn rdepends<I, S>(mut self, packages: I) -> io::Result<(Child, PackageStream)>
122    where
123        I: IntoIterator<Item = S>,
124        S: AsRef<std::ffi::OsStr>,
125    {
126        self.arg("rdepends");
127        self.args(packages);
128        self.stream_packages().await
129    }
130
131    pub async fn policy<S: AsRef<std::ffi::OsStr>>(
132        mut self,
133        packages: &[S],
134    ) -> anyhow::Result<(Child, Policies)> {
135        self.arg("policy");
136        self.args(packages);
137        self.env("LANG", "C");
138
139        let (child, stdout) = self.spawn_with_stdout().await?;
140
141        let lines = LinesStream::new(BufReader::new(stdout).lines());
142
143        let stream = Box::pin(policies(lines));
144
145        Ok((child, stream))
146    }
147
148    pub async fn predepends_of<'a>(
149        out: &'a mut String,
150        package: &'a str,
151    ) -> anyhow::Result<Vec<&'a str>> {
152        let (mut child, mut packages) = AptCache::new()
153            .rdepends(&[&package])
154            .await
155            .with_context(|| format!("failed to launch `apt-cache rdepends {}`", package))?;
156
157        let mut depends = Vec::new();
158        while let Some(package) = packages.next().await {
159            depends.push(package);
160        }
161
162        child
163            .wait()
164            .await
165            .map_result()
166            .with_context(|| format!("bad status from `apt-cache rdepends {}`", package))?;
167
168        let (mut child, mut stdout) = AptCache::new()
169            .depends(&depends)
170            .await
171            .with_context(|| format!("failed to launch `apt-cache depends {}`", package))?;
172
173        stdout
174            .read_to_string(out)
175            .await
176            .with_context(|| format!("failed to get output of `apt-cache depends {}`", package))?;
177
178        child.wait().await.map_result()?;
179
180        Ok(PreDependsIter::new(out.as_str(), package)?.collect::<Vec<_>>())
181    }
182
183    async fn stream_packages(self) -> io::Result<(Child, PackageStream)> {
184        let (child, stdout) = self.spawn_with_stdout().await?;
185
186        let mut lines = LinesStream::new(BufReader::new(stdout).lines()).skip(2);
187
188        let stream = async_stream::stream! {
189            while let Some(Ok(package)) = lines.next().await {
190                yield package.trim_start().to_owned();
191            }
192        };
193
194        Ok((child, Box::pin(stream)))
195    }
196
197    pub async fn status(mut self) -> io::Result<()> {
198        self.0.status().await?.into_result()
199    }
200
201    pub async fn spawn_with_stdout(self) -> io::Result<(Child, ChildStdout)> {
202        crate::utils::spawn_with_stdout(self.0).await
203    }
204}
205pub struct PreDependsIter<'a> {
206    lines: std::str::Lines<'a>,
207    predepend: &'a str,
208    active: &'a str,
209}
210
211impl<'a> PreDependsIter<'a> {
212    pub fn new(output: &'a str, predepend: &'a str) -> io::Result<Self> {
213        let mut lines = output.lines();
214
215        let active = lines.next().ok_or_else(|| {
216            io::Error::new(
217                io::ErrorKind::NotFound,
218                "expected the first line of the output of apt-cache depends to be a package name",
219            )
220        })?;
221
222        Ok(Self {
223            lines,
224            predepend,
225            active: active.trim(),
226        })
227    }
228}
229
230impl<'a> Iterator for PreDependsIter<'a> {
231    type Item = &'a str;
232
233    fn next(&mut self) -> Option<Self::Item> {
234        let mut found = false;
235        for line in &mut self.lines {
236            if !line.starts_with(' ') {
237                let prev = self.active;
238                self.active = line.trim();
239                if found {
240                    return Some(prev);
241                }
242            } else if !found && line.starts_with("  PreDepends: ") && &line[14..] == self.predepend
243            {
244                found = true;
245            }
246        }
247
248        None
249    }
250}