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
29#[derive(Debug)]
30struct PackageManager {
31 distro: FlowPlatformLinuxDistro,
32 sh: xshell::Shell,
33}
34
35impl PackageManager {
36 fn new(ctx: &NodeCtx<'_>) -> anyhow::Result<Self> {
37 let distro = match ctx.platform() {
38 FlowPlatform::Linux(linux_distribution) => linux_distribution,
39 _ => anyhow::bail!("Unsupported platform"),
40 };
41
42 let sh = xshell::Shell::new()?;
43
44 Ok(Self { distro, sh })
45 }
46
47 fn distro(&self) -> FlowPlatformLinuxDistro {
48 self.distro
49 }
50
51 fn query_cmd(&self, packages_to_check: &BTreeSet<String>) -> anyhow::Result<BTreeSet<String>> {
52 let Self { distro, sh } = self;
53
54 let output = match distro {
55 FlowPlatformLinuxDistro::Ubuntu => {
56 let fmt = "${binary:Package}\n";
57 xshell::cmd!(sh, "dpkg-query -W -f={fmt} {packages_to_check...}")
58 }
59 FlowPlatformLinuxDistro::Fedora => {
60 let fmt = "%{NAME}\n";
61 xshell::cmd!(sh, "rpm -q --queryformat={fmt} {packages_to_check...}")
62 }
63 FlowPlatformLinuxDistro::Arch => {
64 xshell::cmd!(sh, "pacman -Qq {packages_to_check...}")
65 }
66 FlowPlatformLinuxDistro::Unknown => anyhow::bail!("Unknown Linux distribution"),
67 }
68 .ignore_status()
69 .output()?;
70 let output = String::from_utf8(output.stdout)?;
71
72 let mut installed_packages = BTreeSet::new();
73 for ln in output.trim().lines() {
74 let package = match ln.split_once(':') {
75 Some((package, _arch)) => package,
76 None => ln,
77 };
78 let no_existing = installed_packages.insert(package.to_owned());
79 assert!(no_existing);
80 }
81
82 Ok(installed_packages)
83 }
84
85 fn update(&self) -> anyhow::Result<()> {
86 let Self { distro, sh } = self;
87
88 match distro {
89 FlowPlatformLinuxDistro::Ubuntu => xshell::cmd!(sh, "sudo apt-get update").run()?,
90 FlowPlatformLinuxDistro::Fedora => xshell::cmd!(sh, "sudo dnf update").run()?,
91 FlowPlatformLinuxDistro::Arch => (),
93 FlowPlatformLinuxDistro::Unknown => anyhow::bail!("Unknown Linux distribution"),
94 }
95
96 Ok(())
97 }
98
99 fn install(&self, packages: &BTreeSet<String>, interactive: bool) -> anyhow::Result<()> {
100 let Self { distro, sh } = self;
101
102 match distro {
103 FlowPlatformLinuxDistro::Ubuntu => {
104 let mut options = Vec::new();
105 if !interactive {
106 options.push("-y");
108 options.extend(["-o", "DPkg::Lock::Timeout=60"]);
110 }
111 xshell::cmd!(sh, "sudo apt-get install {options...} {packages...}").run()?;
112 }
113 FlowPlatformLinuxDistro::Fedora => {
114 let auto_accept = (!interactive).then_some("-y");
115 xshell::cmd!(sh, "sudo dnf install {auto_accept...} {packages...}").run()?;
116 }
117 FlowPlatformLinuxDistro::Arch => {
118 let auto_accept = (!interactive).then_some("--noconfirm");
119 xshell::cmd!(sh, "sudo pacman -S {auto_accept...} {packages...}").run()?;
120 }
121 FlowPlatformLinuxDistro::Unknown => anyhow::bail!("Unknown Linux distribution"),
122 }
123
124 Ok(())
125 }
126}
127
128new_flow_node!(struct Node);
129
130impl FlowNode for Node {
131 type Request = Request;
132
133 fn imports(_ctx: &mut ImportCtx<'_>) {}
134
135 fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
136 let mut skip_update = None;
137 let mut interactive = None;
138 let mut packages = BTreeSet::new();
139 let mut did_install = Vec::new();
140
141 for req in requests {
142 match req {
143 Request::Install {
144 package_names,
145 done,
146 } => {
147 packages.extend(package_names);
148 did_install.push(done);
149 }
150 Request::LocalOnlyInteractive(v) => {
151 same_across_all_reqs("LocalOnlyInteractive", &mut interactive, v)?
152 }
153 Request::LocalOnlySkipUpdate(v) => {
154 same_across_all_reqs("LocalOnlySkipUpdate", &mut skip_update, v)?
155 }
156 }
157 }
158
159 let packages = packages;
160 let (skip_update, interactive) =
161 if matches!(ctx.backend(), FlowBackend::Ado | FlowBackend::Github) {
162 if interactive.is_some() {
163 anyhow::bail!(
164 "can only use `LocalOnlyInteractive` when using the Local backend"
165 );
166 }
167
168 if skip_update.is_some() {
169 anyhow::bail!(
170 "can only use `LocalOnlySkipUpdate` when using the Local backend"
171 );
172 }
173
174 (false, false)
175 } else if matches!(ctx.backend(), FlowBackend::Local) {
176 (
177 skip_update.ok_or(anyhow::anyhow!(
178 "Missing essential request: LocalOnlySkipUpdate",
179 ))?,
180 interactive.ok_or(anyhow::anyhow!(
181 "Missing essential request: LocalOnlyInteractive",
182 ))?,
183 )
184 } else {
185 anyhow::bail!("unsupported backend")
186 };
187
188 if did_install.is_empty() {
191 return Ok(());
192 }
193
194 if !matches!(ctx.platform(), FlowPlatform::Linux(_)) {
198 ctx.emit_side_effect_step([], did_install);
199 return Ok(());
200 }
201
202 let pacman = PackageManager::new(ctx)?;
203 let persistent_dir = ctx.persistent_dir();
204 let need_install =
205 ctx.emit_rust_stepv("checking if packages need to be installed", |ctx| {
206 let persistent_dir = persistent_dir.claim(ctx);
207 let packages = packages.clone();
208 move |rt| {
209 if matches!(rt.backend(), FlowBackend::Local) && pacman.distro() == FlowPlatformLinuxDistro::Unknown {
212 log::error!("This Linux distribution is not actively supported at the moment.");
213 log::warn!("");
214 log::warn!("================================================================================");
215 log::warn!("You are running on an untested configuration, and may be required to manually");
216 log::warn!("install certain packages in order to build.");
217 log::warn!("");
218 log::warn!(" PROCEED WITH CAUTION");
219 log::warn!("");
220 log::warn!("================================================================================");
221
222 if let Some(persistent_dir) = persistent_dir {
223 let promptfile = rt.read(persistent_dir).join("unsupported_distro_prompt");
224
225 if !promptfile.exists() {
226 log::info!("Press [enter] to proceed, or [ctrl-c] to exit.");
227 log::info!("This interactive prompt will only appear once.");
228 let _ = std::io::stdin().read_line(&mut String::new());
229 fs_err::write(promptfile, [])?;
230 }
231 }
232
233 log::warn!("Proceeding anyways...");
234 return Ok(false)
235 }
236
237 let packages_to_check = &packages;
238 let installed_packages = pacman.query_cmd(packages_to_check)?;
239
240 Ok(installed_packages != packages)
244 }
245 });
246
247 let pacman = PackageManager::new(ctx)?;
248 ctx.emit_rust_step("installing packages", move |ctx| {
249 let packages = packages.clone();
250 let need_install = need_install.claim(ctx);
251 did_install.claim(ctx);
252 move |rt| {
253 let need_install = rt.read(need_install);
254
255 if !need_install {
256 return Ok(());
257 }
258 if !skip_update {
259 let mut i = 0;
261 while let Err(e) = pacman.update() {
262 i += 1;
263 if i == 5 || interactive {
264 return Err(e);
265 }
266 std::thread::sleep(std::time::Duration::from_secs(1));
267 }
268 }
269 pacman.install(&packages, interactive)?;
270
271 Ok(())
272 }
273 });
274
275 Ok(())
276 }
277}