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