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