flowey_lib_common/
install_rust.rs1use flowey::node::prelude::*;
8use std::collections::BTreeSet;
9use std::io::Write;
10
11new_flow_node_with_config!(struct Node);
12
13flowey_config! {
14 pub struct Config {
16 pub auto_install: Option<bool>,
21 pub ignore_version: Option<bool>,
24 pub version: Option<String>,
26 }
27}
28
29flowey_request! {
30 pub enum Request {
31 InstallTargetTriple(target_lexicon::Triple),
35
36 GetRustupToolchain(WriteVar<Option<String>>),
40
41 InstallComponent(String),
43
44 GetCargoHome(WriteVar<PathBuf>),
46
47 EnsureInstalled(WriteVar<SideEffect>),
49 }
50}
51
52impl FlowNodeWithConfig for Node {
53 type Request = Request;
54 type Config = Config;
55
56 fn imports(dep: &mut ImportCtx<'_>) {
57 dep.import::<crate::check_needs_relaunch::Node>();
58 }
59
60 fn emit(
61 config: Config,
62 requests: Vec<Self::Request>,
63 ctx: &mut NodeCtx<'_>,
64 ) -> anyhow::Result<()> {
65 let mut ensure_installed = Vec::new();
66 let mut additional_target_triples = BTreeSet::new();
67 let mut additional_components = BTreeSet::new();
68 let mut get_rust_toolchain = Vec::new();
69 let mut get_cargo_home = Vec::new();
70
71 for req in requests {
72 match req {
73 Request::EnsureInstalled(v) => ensure_installed.push(v),
74 Request::InstallTargetTriple(s) => {
75 additional_target_triples.insert(s.to_string());
76 }
77 Request::InstallComponent(v) => {
78 additional_components.insert(v);
79 }
80 Request::GetRustupToolchain(v) => get_rust_toolchain.push(v),
81 Request::GetCargoHome(v) => get_cargo_home.push(v),
82 }
83 }
84
85 let ensure_installed = ensure_installed;
86 let auto_install = config
87 .auto_install
88 .ok_or(anyhow::anyhow!("missing config: auto_install"))?;
89 if !auto_install && matches!(ctx.backend(), FlowBackend::Github) {
90 anyhow::bail!("`AutoInstall` must be true when using the Github backend");
91 }
92 let ignore_version = config
93 .ignore_version
94 .ok_or(anyhow::anyhow!("missing config: ignore_version"))?;
95 if ignore_version && matches!(ctx.backend(), FlowBackend::Github) {
96 anyhow::bail!("`IgnoreVersion` must be false when using the Github backend");
97 }
98 let rust_toolchain = config
99 .version
100 .ok_or(anyhow::anyhow!("missing config: version"))?;
101 let additional_target_triples = additional_target_triples;
102 let additional_components = additional_components;
103 let get_rust_toolchain = get_rust_toolchain;
104 let get_cargo_home = get_cargo_home;
105
106 let rust_toolchain = (!ignore_version).then_some(rust_toolchain);
109
110 let check_rust_install = {
111 let rust_toolchain = rust_toolchain.clone();
112 let additional_target_triples = additional_target_triples.clone();
113 let additional_components = additional_components.clone();
114
115 move |rt: &mut RustRuntimeServices<'_>| {
116 if flowey::shell_cmd!(rt, "cargo --version").run().is_err() {
117 anyhow::bail!("did not find `cargo` on $PATH");
118 }
119
120 let has_rustup = flowey::shell_cmd!(rt, "rustup --version").run().is_ok();
121
122 if has_rustup {
125 let rust_toolchain = rust_toolchain.as_ref().map(|s| format!("+{s}"));
126 let rust_toolchain = rust_toolchain.as_ref();
127 flowey::shell_cmd!(rt, "rustc {rust_toolchain...} -vV").run()?;
128 } else if let Some(ref version) = rust_toolchain {
129 let output = flowey::shell_cmd!(rt, "rustc -vV").output()?;
130 let stdout = String::from_utf8(output.stdout)?;
131 let installed_version = stdout
132 .lines()
133 .find_map(|line| line.strip_prefix("release: "))
134 .context("failed to parse rustc version output")?;
135 if installed_version != version.as_str() {
136 anyhow::bail!(
137 "required Rust {version}, found {installed_version} \
138 (rustup unavailable)"
139 );
140 }
141 } else {
142 flowey::shell_cmd!(rt, "rustc -vV").run()?;
143 }
144
145 if has_rustup {
147 let rust_toolchain = rust_toolchain.as_ref().map(|s| format!("+{s}"));
148 let rust_toolchain = rust_toolchain.as_ref();
149 for (thing, expected_things) in [
150 ("target", &additional_target_triples),
151 ("component", &additional_components),
152 ] {
153 let output = flowey::shell_cmd!(
154 rt,
155 "rustup {rust_toolchain...} {thing} list --installed"
156 )
157 .ignore_status()
158 .output()?;
159 let stderr = String::from_utf8(output.stderr)?;
160 let stdout = String::from_utf8(output.stdout)?;
161
162 if stderr.contains("does not support components") {
169 log::warn!("Detected a non-standard `rustup default` toolchain!");
170 log::warn!(
171 "Will not be able to double-check that all required target-triples and components are available."
172 );
173 } else {
174 let mut installed_things = BTreeSet::new();
175
176 for line in stdout.lines() {
177 let triple = line.trim();
178 installed_things.insert(triple);
179 }
180
181 for expected_thing in expected_things {
182 if !installed_things.contains(expected_thing.as_str()) {
183 anyhow::bail!(
184 "missing required {thing}: {expected_thing}; to install: `rustup {thing} add {expected_thing}`"
185 )
186 }
187 }
188 }
189 }
190 } else {
191 log::warn!("`rustup` was not found!");
192 log::warn!(
193 "Unable to double-check that all target-triples and components are available."
194 )
195 }
196
197 anyhow::Ok(())
198 }
199 };
200
201 let check_is_installed = |write_cargo_bin: Option<
202 WriteVar<Option<crate::check_needs_relaunch::BinOrEnv>>,
203 >,
204 ensure_installed: Vec<WriteVar<SideEffect>>,
205 auto_install: bool,
206 ctx: &mut NodeCtx<'_>| {
207 if write_cargo_bin.is_some() || !ensure_installed.is_empty() {
208 if auto_install || matches!(ctx.backend(), FlowBackend::Github) {
209 let added_to_path = if matches!(ctx.backend(), FlowBackend::Github) {
210 Some(ctx.emit_rust_step("add default cargo home to path", |_| {
211 |_| {
212 let default_cargo_home = home::home_dir()
213 .context("Unable to get home dir")?
214 .join(".cargo")
215 .join("bin");
216 let github_path = std::env::var("GITHUB_PATH")?;
217 let mut github_path =
218 fs_err::File::options().append(true).open(github_path)?;
219 github_path
220 .write_all(default_cargo_home.as_os_str().as_encoded_bytes())?;
221 log::info!("Added {} to PATH", default_cargo_home.display());
222 Ok(())
223 }
224 }))
225 } else {
226 None
227 };
228
229 let rust_toolchain = rust_toolchain.clone();
230 ctx.emit_rust_step("install Rust", |ctx| {
231 let write_cargo_bin = if let Some(write_cargo_bin) = write_cargo_bin {
232 Some(write_cargo_bin.claim(ctx))
233 } else {
234 ensure_installed.claim(ctx);
235 None
236 };
237 added_to_path.claim(ctx);
238
239 move |rt: &mut RustRuntimeServices<'_>| {
240 if let Some(write_cargo_bin) = write_cargo_bin {
241 rt.write(write_cargo_bin, &Some(crate::check_needs_relaunch::BinOrEnv::Bin("cargo".to_string())));
242 }
243
244 let rust_toolchain = rust_toolchain.clone();
245 if check_rust_install.clone()(rt).is_ok() {
246 return Ok(());
247 }
248
249 let cargo_available =
252 flowey::shell_cmd!(rt, "cargo --version").run().is_ok();
253 let rustup_available =
254 flowey::shell_cmd!(rt, "rustup --version").run().is_ok();
255 if cargo_available && !rustup_available
256 {
257 anyhow::bail!(
258 "Rust installation check failed and rustup is \
259 not available; Rust appears to be externally \
260 managed and cannot be installed by this node"
261 );
262 }
263
264 match rt.platform() {
265 FlowPlatform::Linux(_) => {
266 let interactive_prompt = Some("-y");
267 let mut default_toolchain = Vec::new();
268 if let Some(ver) = rust_toolchain {
269 default_toolchain.push("--default-toolchain".into());
270 default_toolchain.push(ver)
271 };
272
273 flowey::shell_cmd!(
274 rt,
275 "curl --fail --proto =https --tlsv1.2 -sSf https://sh.rustup.rs -o rustup-init.sh"
276 )
277 .run()?;
278 flowey::shell_cmd!(rt, "chmod +x ./rustup-init.sh").run()?;
279 flowey::shell_cmd!(
280 rt,
281 "./rustup-init.sh {interactive_prompt...} {default_toolchain...}"
282 )
283 .run()?;
284 }
285 FlowPlatform::Windows => {
286 let interactive_prompt = Some("-y");
287 let mut default_toolchain = Vec::new();
288 if let Some(ver) = rust_toolchain {
289 default_toolchain.push("--default-toolchain".into());
290 default_toolchain.push(ver)
291 };
292
293 let arch = match rt.arch() {
294 FlowArch::X86_64 => "x86_64",
295 FlowArch::Aarch64 => "aarch64",
296 arch => anyhow::bail!("unsupported arch {arch}"),
297 };
298
299 flowey::shell_cmd!(
300 rt,
301 "curl --fail -sSfLo rustup-init.exe https://win.rustup.rs/{arch}"
302 ).run()?;
303 flowey::shell_cmd!(
304 rt,
305 "./rustup-init.exe {interactive_prompt...} {default_toolchain...}"
306 )
307 .run()?;
308 },
309 platform => anyhow::bail!("unsupported platform {platform}"),
310 }
311
312 if !additional_target_triples.is_empty() {
313 flowey::shell_cmd!(rt, "rustup target add {additional_target_triples...}")
314 .run()?;
315 }
316 if !additional_components.is_empty() {
317 flowey::shell_cmd!(rt, "rustup component add {additional_components...}")
318 .run()?;
319 }
320
321 Ok(())
322 }
323 })
324 } else if let Some(write_cargo_bin) = write_cargo_bin {
325 ctx.emit_rust_step("ensure Rust is installed", |ctx| {
326 let write_cargo_bin = write_cargo_bin.claim(ctx);
327 move |rt| {
328 rt.write(
329 write_cargo_bin,
330 &Some(crate::check_needs_relaunch::BinOrEnv::Bin(
331 "cargo".to_string(),
332 )),
333 );
334
335 check_rust_install(rt)?;
336 Ok(())
337 }
338 })
339 } else {
340 ReadVar::from_static(()).into_side_effect()
341 }
342 } else {
343 ReadVar::from_static(()).into_side_effect()
344 }
345 };
346
347 let is_installed =
351 if !ensure_installed.is_empty() && matches!(ctx.backend(), FlowBackend::Local) {
352 let (read_bin, write_cargo_bin) = ctx.new_var();
353 ctx.req(crate::check_needs_relaunch::Params {
354 check: read_bin,
355 done: ensure_installed,
356 });
357 check_is_installed(Some(write_cargo_bin), Vec::new(), auto_install, ctx)
358 } else {
359 check_is_installed(None, ensure_installed, auto_install, ctx)
360 };
361
362 if !get_rust_toolchain.is_empty() {
363 ctx.emit_rust_step("detect active toolchain", |ctx| {
364 is_installed.clone().claim(ctx);
365 let get_rust_toolchain = get_rust_toolchain.claim(ctx);
366
367 move |rt| {
368 let has_rustup = flowey::shell_cmd!(rt, "rustup --version").run().is_ok();
369 let rust_toolchain = match rust_toolchain {
370 Some(toolchain) => {
371 if has_rustup {
372 Some(toolchain)
373 } else {
374 None
375 }
376 }
377 None => {
378 if has_rustup {
379 let output = flowey::shell_cmd!(rt, "rustup show active-toolchain")
394 .output()?;
395 let stdout = String::from_utf8(output.stdout)?;
396 let line = stdout
397 .lines()
398 .next()
399 .context("`rustup show active-toolchain` produced no output")?;
400 let toolchain = line.split(' ').next().context(format!(
401 "unexpected `rustup show active-toolchain` output: `{line}`"
402 ))?;
403 Some(toolchain.into())
404 } else {
405 None
406 }
407 }
408 };
409
410 rt.write_all(get_rust_toolchain, &rust_toolchain);
411
412 Ok(())
413 }
414 });
415 }
416
417 if !get_cargo_home.is_empty() {
418 ctx.emit_rust_step("report $CARGO_HOME", |ctx| {
419 is_installed.claim(ctx);
420 let get_cargo_home = get_cargo_home.claim(ctx);
421 move |rt| {
422 let cargo_home = home::cargo_home()?;
423 rt.write_all(get_cargo_home, &cargo_home);
424
425 Ok(())
426 }
427 });
428 }
429
430 Ok(())
431 }
432}