flowey_lib_common/
install_dist_pkg.rs1use flowey::node::prelude::*;
12use std::collections::BTreeSet;
13
14flowey_request! {
15 pub enum Request {
16 LocalOnlyInteractive(bool),
18 LocalOnlySkipUpdate(bool),
21 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 FlowPlatformLinuxDistro::AzureLinux => (),
77 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 options.push("-y");
100 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 if did_install.is_empty() {
189 return Ok(());
190 }
191
192 if !matches!(ctx.platform(), FlowPlatform::Linux(_)) {
196 ctx.emit_side_effect_step([], did_install);
197 return Ok(());
198 }
199
200 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 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 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 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}