flowey_lib_common/
install_dist_pkg.rs1use flowey::node::prelude::*;
12use std::collections::BTreeSet;
13
14flowey_config! {
15 pub struct Config {
17 pub interactive: Option<bool>,
19 pub skip_update: Option<bool>,
21 }
22}
23
24flowey_request! {
25 pub enum Request {
26 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 FlowPlatformLinuxDistro::AzureLinux => (),
82 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 options.push("-y");
105 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 if did_install.is_empty() {
187 return Ok(());
188 }
189
190 if !matches!(ctx.platform(), FlowPlatform::Linux(_)) {
194 ctx.emit_side_effect_step([], did_install);
195 return Ok(());
196 }
197
198 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 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 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 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}