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