Skip to main content

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