flowey_lib_common/
install_dist_pkg.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Globally install a package via `apt` on DEB-based Linux systems,
5//! or `dnf` on RPM-based ones.
6//!
7//! This is a temporary solution, and this file will be split in
8//! two in the future to have two flowey Nodes.
9//! GitHub issue: <https://github.com/microsoft/openvmm/issues/90>
10
11use flowey::node::prelude::*;
12use std::collections::BTreeSet;
13
14flowey_request! {
15    pub enum Request {
16        /// Whether to prompt the user before installing packages
17        LocalOnlyInteractive(bool),
18        /// Whether to skip the `apt-update` step, and allow stale
19        /// packages
20        LocalOnlySkipUpdate(bool),
21        /// Install the specified package(s)
22        Install {
23            package_names: Vec<String>,
24            done: WriteVar<SideEffect>,
25        },
26    }
27}
28
29#[derive(Debug)]
30struct PackageManager {
31    distro: FlowPlatformLinuxDistro,
32    sh: xshell::Shell,
33}
34
35impl PackageManager {
36    fn new(ctx: &NodeCtx<'_>) -> anyhow::Result<Self> {
37        let distro = match ctx.platform() {
38            FlowPlatform::Linux(linux_distribution) => linux_distribution,
39            _ => anyhow::bail!("Unsupported platform"),
40        };
41
42        let sh = xshell::Shell::new()?;
43
44        Ok(Self { distro, sh })
45    }
46
47    fn distro(&self) -> FlowPlatformLinuxDistro {
48        self.distro
49    }
50
51    fn query_cmd(&self, packages_to_check: &BTreeSet<String>) -> anyhow::Result<BTreeSet<String>> {
52        let Self { distro, sh } = self;
53
54        let output = match distro {
55            FlowPlatformLinuxDistro::Ubuntu => {
56                let fmt = "${binary:Package}\n";
57                xshell::cmd!(sh, "dpkg-query -W -f={fmt} {packages_to_check...}")
58            }
59            FlowPlatformLinuxDistro::Fedora => {
60                let fmt = "%{NAME}\n";
61                xshell::cmd!(sh, "rpm -q --queryformat={fmt} {packages_to_check...}")
62            }
63            FlowPlatformLinuxDistro::Arch => {
64                xshell::cmd!(sh, "pacman -Qq {packages_to_check...}")
65            }
66            FlowPlatformLinuxDistro::Unknown => anyhow::bail!("Unknown Linux distribution"),
67        }
68        .ignore_status()
69        .output()?;
70        let output = String::from_utf8(output.stdout)?;
71
72        let mut installed_packages = BTreeSet::new();
73        for ln in output.trim().lines() {
74            let package = match ln.split_once(':') {
75                Some((package, _arch)) => package,
76                None => ln,
77            };
78            let no_existing = installed_packages.insert(package.to_owned());
79            assert!(no_existing);
80        }
81
82        Ok(installed_packages)
83    }
84
85    fn update(&self) -> anyhow::Result<()> {
86        let Self { distro, sh } = self;
87
88        match distro {
89            FlowPlatformLinuxDistro::Ubuntu => xshell::cmd!(sh, "sudo apt-get update").run()?,
90            FlowPlatformLinuxDistro::Fedora => xshell::cmd!(sh, "sudo dnf update").run()?,
91            // Running `pacman -Sy` without a full system update can break everything; do nothing
92            FlowPlatformLinuxDistro::Arch => (),
93            FlowPlatformLinuxDistro::Unknown => anyhow::bail!("Unknown Linux distribution"),
94        }
95
96        Ok(())
97    }
98
99    fn install(&self, packages: &BTreeSet<String>, interactive: bool) -> anyhow::Result<()> {
100        let Self { distro, sh } = self;
101
102        match distro {
103            FlowPlatformLinuxDistro::Ubuntu => {
104                let mut options = Vec::new();
105                if !interactive {
106                    // auto accept
107                    options.push("-y");
108                    // Wait for dpkg locks to be released when running in CI
109                    options.extend(["-o", "DPkg::Lock::Timeout=60"]);
110                }
111                xshell::cmd!(sh, "sudo apt-get install {options...} {packages...}").run()?;
112            }
113            FlowPlatformLinuxDistro::Fedora => {
114                let auto_accept = (!interactive).then_some("-y");
115                xshell::cmd!(sh, "sudo dnf install {auto_accept...} {packages...}").run()?;
116            }
117            FlowPlatformLinuxDistro::Arch => {
118                let auto_accept = (!interactive).then_some("--noconfirm");
119                xshell::cmd!(sh, "sudo pacman -S {auto_accept...} {packages...}").run()?;
120            }
121            FlowPlatformLinuxDistro::Unknown => anyhow::bail!("Unknown Linux distribution"),
122        }
123
124        Ok(())
125    }
126}
127
128new_flow_node!(struct Node);
129
130impl FlowNode for Node {
131    type Request = Request;
132
133    fn imports(_ctx: &mut ImportCtx<'_>) {}
134
135    fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
136        let mut skip_update = None;
137        let mut interactive = None;
138        let mut packages = BTreeSet::new();
139        let mut did_install = Vec::new();
140
141        for req in requests {
142            match req {
143                Request::Install {
144                    package_names,
145                    done,
146                } => {
147                    packages.extend(package_names);
148                    did_install.push(done);
149                }
150                Request::LocalOnlyInteractive(v) => {
151                    same_across_all_reqs("LocalOnlyInteractive", &mut interactive, v)?
152                }
153                Request::LocalOnlySkipUpdate(v) => {
154                    same_across_all_reqs("LocalOnlySkipUpdate", &mut skip_update, v)?
155                }
156            }
157        }
158
159        let packages = packages;
160        let (skip_update, interactive) =
161            if matches!(ctx.backend(), FlowBackend::Ado | FlowBackend::Github) {
162                if interactive.is_some() {
163                    anyhow::bail!(
164                        "can only use `LocalOnlyInteractive` when using the Local backend"
165                    );
166                }
167
168                if skip_update.is_some() {
169                    anyhow::bail!(
170                        "can only use `LocalOnlySkipUpdate` when using the Local backend"
171                    );
172                }
173
174                (false, false)
175            } else if matches!(ctx.backend(), FlowBackend::Local) {
176                (
177                    skip_update.ok_or(anyhow::anyhow!(
178                        "Missing essential request: LocalOnlySkipUpdate",
179                    ))?,
180                    interactive.ok_or(anyhow::anyhow!(
181                        "Missing essential request: LocalOnlyInteractive",
182                    ))?,
183                )
184            } else {
185                anyhow::bail!("unsupported backend")
186            };
187
188        // -- end of req processing -- //
189
190        if did_install.is_empty() {
191            return Ok(());
192        }
193
194        // maybe a questionable design choice... but we'll allow non-linux
195        // platforms from taking a dep on this, and simply report that it was
196        // installed.
197        if !matches!(ctx.platform(), FlowPlatform::Linux(_)) {
198            ctx.emit_side_effect_step([], did_install);
199            return Ok(());
200        }
201
202        let pacman = PackageManager::new(ctx)?;
203        let persistent_dir = ctx.persistent_dir();
204        let need_install =
205            ctx.emit_rust_stepv("checking if packages need to be installed", |ctx| {
206                let persistent_dir = persistent_dir.claim(ctx);
207                let packages = packages.clone();
208                move |rt| {
209                    // Provide the users an escape-hatch to run Linux flows on distributions that are not actively
210                    // supported at the moment.
211                    if matches!(rt.backend(), FlowBackend::Local) && pacman.distro() == FlowPlatformLinuxDistro::Unknown {
212                        log::error!("This Linux distribution is not actively supported at the moment.");
213                        log::warn!("");
214                        log::warn!("================================================================================");
215                        log::warn!("You are running on an untested configuration, and may be required to manually");
216                        log::warn!("install certain packages in order to build.");
217                        log::warn!("");
218                        log::warn!("                             PROCEED WITH CAUTION");
219                        log::warn!("");
220                        log::warn!("================================================================================");
221
222                        if let Some(persistent_dir) = persistent_dir {
223                            let promptfile = rt.read(persistent_dir).join("unsupported_distro_prompt");
224
225                            if !promptfile.exists() {
226                                log::info!("Press [enter] to proceed, or [ctrl-c] to exit.");
227                                log::info!("This interactive prompt will only appear once.");
228                                let _ = std::io::stdin().read_line(&mut String::new());
229                                fs_err::write(promptfile, [])?;
230                            }
231                        }
232
233                        log::warn!("Proceeding anyways...");
234                        return Ok(false)
235                    }
236
237                    let packages_to_check = &packages;
238                    let installed_packages  = pacman.query_cmd(packages_to_check)?;
239
240                    // the package manager won't re-install packages that are already
241                    // up-to-date, so this sort of coarse-grained signal should
242                    // be plenty sufficient.
243                    Ok(installed_packages != packages)
244                }
245            });
246
247        let pacman = PackageManager::new(ctx)?;
248        ctx.emit_rust_step("installing packages", move |ctx| {
249            let packages = packages.clone();
250            let need_install = need_install.claim(ctx);
251            did_install.claim(ctx);
252            move |rt| {
253                let need_install = rt.read(need_install);
254
255                if !need_install {
256                    return Ok(());
257                }
258                if !skip_update {
259                    // Retry on failure in CI
260                    let mut i = 0;
261                    while let Err(e) = pacman.update() {
262                        i += 1;
263                        if i == 5 || interactive {
264                            return Err(e);
265                        }
266                        std::thread::sleep(std::time::Duration::from_secs(1));
267                    }
268                }
269                pacman.install(&packages, interactive)?;
270
271                Ok(())
272            }
273        });
274
275        Ok(())
276    }
277}