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        if packages.is_empty() || !matches!(ctx.platform(), FlowPlatform::Linux(_)) {
191            ctx.emit_side_effect_step([], did_install);
192            return Ok(());
193        }
194
195        // Explicitly fail on installation requests in Nix environments.
196        if matches!(
197            ctx.platform(),
198            FlowPlatform::Linux(FlowPlatformLinuxDistro::Nix)
199        ) {
200            anyhow::bail!(
201                "Nix environments cannot install packages. Dependencies should be managed by Nix. Attempted to install {:?}",
202                packages
203            );
204        }
205
206        let distro = match ctx.platform() {
207            FlowPlatform::Linux(d) => d,
208            _ => unreachable!(),
209        };
210
211        let persistent_dir = ctx.persistent_dir();
212        let need_install =
213            ctx.emit_rust_stepv("checking if packages need to be installed", |ctx| {
214                let persistent_dir = persistent_dir.claim(ctx);
215                let packages = packages.clone();
216                move |rt| {
217                    // Provide the users an escape-hatch to run Linux flows on distributions that are not actively
218                    // supported at the moment.
219                    if matches!(rt.backend(), FlowBackend::Local) && distro == FlowPlatformLinuxDistro::Unknown {
220                        log::error!("This Linux distribution is not actively supported at the moment.");
221                        log::warn!("");
222                        log::warn!("================================================================================");
223                        log::warn!("You are running on an untested configuration, and may be required to manually");
224                        log::warn!("install certain packages in order to build.");
225                        log::warn!("");
226                        log::warn!("                             PROCEED WITH CAUTION");
227                        log::warn!("");
228                        log::warn!("================================================================================");
229
230                        if let Some(persistent_dir) = persistent_dir {
231                            let promptfile = rt.read(persistent_dir).join("unsupported_distro_prompt");
232
233                            if !promptfile.exists() {
234                                log::info!("Press [enter] to proceed, or [ctrl-c] to exit.");
235                                log::info!("This interactive prompt will only appear once.");
236                                let _ = std::io::stdin().read_line(&mut String::new());
237                                fs_err::write(promptfile, [])?;
238                            }
239                        }
240
241                        log::warn!("Proceeding anyways...");
242                        return Ok(false)
243                    }
244
245                    let packages_to_check = &packages;
246                    let installed_packages = query_installed_packages(rt, distro, packages_to_check)?;
247
248                    // the package manager won't re-install packages that are already
249                    // up-to-date, so this sort of coarse-grained signal should
250                    // be plenty sufficient.
251                    Ok(installed_packages != packages)
252                }
253            });
254
255        ctx.emit_rust_step("installing packages", move |ctx| {
256            let packages = packages.clone();
257            let need_install = need_install.claim(ctx);
258            did_install.claim(ctx);
259            move |rt| {
260                let need_install = rt.read(need_install);
261
262                if !need_install {
263                    return Ok(());
264                }
265                if !skip_update {
266                    // Retry on failure in CI
267                    let mut i = 0;
268                    while let Err(e) = update_packages(rt, distro) {
269                        i += 1;
270                        if i == 5 || interactive {
271                            return Err(e);
272                        }
273                        std::thread::sleep(std::time::Duration::from_secs(1));
274                    }
275                }
276                install_packages(rt, distro, &packages, interactive)?;
277
278                Ok(())
279            }
280        });
281
282        Ok(())
283    }
284}