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::Unknown => anyhow::bail!("Unknown Linux distribution"),
64        }
65        .ignore_status()
66        .output()?;
67        let output = String::from_utf8(output.stdout)?;
68
69        let mut installed_packages = BTreeSet::new();
70        for ln in output.trim().lines() {
71            let package = match ln.split_once(':') {
72                Some((package, _arch)) => package,
73                None => ln,
74            };
75            let no_existing = installed_packages.insert(package.to_owned());
76            assert!(no_existing);
77        }
78
79        Ok(installed_packages)
80    }
81
82    fn update(&self) -> anyhow::Result<()> {
83        let Self { distro, sh } = self;
84
85        match distro {
86            FlowPlatformLinuxDistro::Ubuntu => xshell::cmd!(sh, "sudo apt-get update").run()?,
87            FlowPlatformLinuxDistro::Fedora => xshell::cmd!(sh, "sudo dnf update").run()?,
88            FlowPlatformLinuxDistro::Unknown => anyhow::bail!("Unknown Linux distribution"),
89        }
90
91        Ok(())
92    }
93
94    fn install(&self, packages: &BTreeSet<String>, interactive: bool) -> anyhow::Result<()> {
95        let Self { distro, sh } = self;
96
97        match distro {
98            FlowPlatformLinuxDistro::Ubuntu => {
99                let mut options = Vec::new();
100                if !interactive {
101                    // auto accept
102                    options.push("-y");
103                    // Wait for dpkg locks to be released when running in CI
104                    options.extend(["-o", "DPkg::Lock::Timeout=60"]);
105                }
106                xshell::cmd!(sh, "sudo apt-get install {options...} {packages...}").run()?;
107            }
108            FlowPlatformLinuxDistro::Fedora => {
109                let auto_accept = (!interactive).then_some("-y");
110                xshell::cmd!(sh, "sudo dnf install {auto_accept...} {packages...}").run()?;
111            }
112            FlowPlatformLinuxDistro::Unknown => anyhow::bail!("Unknown Linux distribution"),
113        }
114
115        Ok(())
116    }
117}
118
119new_flow_node!(struct Node);
120
121impl FlowNode for Node {
122    type Request = Request;
123
124    fn imports(_ctx: &mut ImportCtx<'_>) {}
125
126    fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
127        let mut skip_update = None;
128        let mut interactive = None;
129        let mut packages = BTreeSet::new();
130        let mut did_install = Vec::new();
131
132        for req in requests {
133            match req {
134                Request::Install {
135                    package_names,
136                    done,
137                } => {
138                    packages.extend(package_names);
139                    did_install.push(done);
140                }
141                Request::LocalOnlyInteractive(v) => {
142                    same_across_all_reqs("LocalOnlyInteractive", &mut interactive, v)?
143                }
144                Request::LocalOnlySkipUpdate(v) => {
145                    same_across_all_reqs("LocalOnlySkipUpdate", &mut skip_update, v)?
146                }
147            }
148        }
149
150        let packages = packages;
151        let (skip_update, interactive) =
152            if matches!(ctx.backend(), FlowBackend::Ado | FlowBackend::Github) {
153                if interactive.is_some() {
154                    anyhow::bail!(
155                        "can only use `LocalOnlyInteractive` when using the Local backend"
156                    );
157                }
158
159                if skip_update.is_some() {
160                    anyhow::bail!(
161                        "can only use `LocalOnlySkipUpdate` when using the Local backend"
162                    );
163                }
164
165                (false, false)
166            } else if matches!(ctx.backend(), FlowBackend::Local) {
167                (
168                    skip_update.ok_or(anyhow::anyhow!(
169                        "Missing essential request: LocalOnlySkipUpdate",
170                    ))?,
171                    interactive.ok_or(anyhow::anyhow!(
172                        "Missing essential request: LocalOnlyInteractive",
173                    ))?,
174                )
175            } else {
176                anyhow::bail!("unsupported backend")
177            };
178
179        // -- end of req processing -- //
180
181        if did_install.is_empty() {
182            return Ok(());
183        }
184
185        // maybe a questionable design choice... but we'll allow non-linux
186        // platforms from taking a dep on this, and simply report that it was
187        // installed.
188        if !matches!(ctx.platform(), FlowPlatform::Linux(_)) {
189            ctx.emit_side_effect_step([], did_install);
190            return Ok(());
191        }
192
193        let pacman = PackageManager::new(ctx)?;
194        let persistent_dir = ctx.persistent_dir();
195        let need_install =
196            ctx.emit_rust_stepv("checking if packages need to be installed", |ctx| {
197                let persistent_dir = persistent_dir.claim(ctx);
198                let packages = packages.clone();
199                move |rt| {
200                    // Provide the users an escape-hatch to run Linux flows on distributions that are not actively
201                    // supported at the moment.
202                    if matches!(rt.backend(), FlowBackend::Local) && pacman.distro() == FlowPlatformLinuxDistro::Unknown {
203                        log::error!("This Linux distribution is not actively supported at the moment.");
204                        log::warn!("");
205                        log::warn!("================================================================================");
206                        log::warn!("You are running on an untested configuration, and may be required to manually");
207                        log::warn!("install certain packages in order to build.");
208                        log::warn!("");
209                        log::warn!("                             PROCEED WITH CAUTION");
210                        log::warn!("");
211                        log::warn!("================================================================================");
212
213                        if let Some(persistent_dir) = persistent_dir {
214                            let promptfile = rt.read(persistent_dir).join("unsupported_distro_prompt");
215
216                            if !promptfile.exists() {
217                                log::info!("Press [enter] to proceed, or [ctrl-c] to exit.");
218                                log::info!("This interactive prompt will only appear once.");
219                                let _ = std::io::stdin().read_line(&mut String::new());
220                                fs_err::write(promptfile, [])?;
221                            }
222                        }
223
224                        log::warn!("Proceeding anyways...");
225                        return Ok(false)
226                    }
227
228                    let packages_to_check = &packages;
229                    let installed_packages  = pacman.query_cmd(packages_to_check)?;
230
231                    // the package manager won't re-install packages that are already
232                    // up-to-date, so this sort of coarse-grained signal should
233                    // be plenty sufficient.
234                    Ok(installed_packages != packages)
235                }
236            });
237
238        let pacman = PackageManager::new(ctx)?;
239        ctx.emit_rust_step("installing packages", move |ctx| {
240            let packages = packages.clone();
241            let need_install = need_install.claim(ctx);
242            did_install.claim(ctx);
243            move |rt| {
244                let need_install = rt.read(need_install);
245
246                if !need_install {
247                    return Ok(());
248                }
249                if !skip_update {
250                    // Retry on failure in CI
251                    let mut i = 0;
252                    while let Err(e) = pacman.update() {
253                        i += 1;
254                        if i == 5 || interactive {
255                            return Err(e);
256                        }
257                        std::thread::sleep(std::time::Duration::from_secs(1));
258                    }
259                }
260                pacman.install(&packages, interactive)?;
261
262                Ok(())
263            }
264        });
265
266        Ok(())
267    }
268}