flowey_lib_common/
install_rust.rs1use flowey::node::prelude::*;
8use std::collections::BTreeSet;
9use std::io::Write;
10
11new_flow_node!(struct Node);
12
13flowey_request! {
14 pub enum Request {
15 AutoInstall(bool),
20
21 IgnoreVersion(bool),
24
25 Version(String),
31
32 InstallTargetTriple(target_lexicon::Triple),
36
37 GetRustupToolchain(WriteVar<Option<String>>),
41
42 InstallComponent(String),
44
45 GetCargoHome(WriteVar<PathBuf>),
47
48 EnsureInstalled(WriteVar<SideEffect>),
50 }
51}
52
53impl FlowNode for Node {
54 type Request = Request;
55
56 fn imports(dep: &mut ImportCtx<'_>) {
57 dep.import::<crate::check_needs_relaunch::Node>();
58 }
59
60 fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
61 if !matches!(ctx.backend(), FlowBackend::Local | FlowBackend::Github) {
62 anyhow::bail!("only supported on the local and github backends at this time");
63 }
64
65 let mut ensure_installed = Vec::new();
66 let mut rust_toolchain = None;
67 let mut auto_install = None;
68 let mut ignore_version = None;
69 let mut additional_target_triples = BTreeSet::new();
70 let mut additional_components = BTreeSet::new();
71 let mut get_rust_toolchain = Vec::new();
72 let mut get_cargo_home = Vec::new();
73
74 for req in requests {
75 match req {
76 Request::EnsureInstalled(v) => ensure_installed.push(v),
77 Request::AutoInstall(v) => {
78 same_across_all_reqs("AutoInstall", &mut auto_install, v)?
79 }
80 Request::IgnoreVersion(v) => {
81 same_across_all_reqs("IgnoreVersion", &mut ignore_version, v)?
82 }
83 Request::Version(v) => same_across_all_reqs("Version", &mut rust_toolchain, v)?,
84 Request::InstallTargetTriple(s) => {
85 additional_target_triples.insert(s.to_string());
86 }
87 Request::InstallComponent(v) => {
88 additional_components.insert(v);
89 }
90 Request::GetRustupToolchain(v) => get_rust_toolchain.push(v),
91 Request::GetCargoHome(v) => get_cargo_home.push(v),
92 }
93 }
94
95 let ensure_installed = ensure_installed;
96 let auto_install =
97 auto_install.ok_or(anyhow::anyhow!("Missing essential request: AutoInstall",))?;
98 if !auto_install && matches!(ctx.backend(), FlowBackend::Github) {
99 anyhow::bail!("`AutoInstall` must be true when using the Github backend");
100 }
101 let ignore_version =
102 ignore_version.ok_or(anyhow::anyhow!("Missing essential request: IgnoreVersion",))?;
103 if ignore_version && matches!(ctx.backend(), FlowBackend::Github) {
104 anyhow::bail!("`IgnoreVersion` must be false when using the Github backend");
105 }
106 let rust_toolchain =
107 rust_toolchain.ok_or(anyhow::anyhow!("Missing essential request: RustToolchain"))?;
108 let additional_target_triples = additional_target_triples;
109 let additional_components = additional_components;
110 let get_rust_toolchain = get_rust_toolchain;
111 let get_cargo_home = get_cargo_home;
112
113 let rust_toolchain = (!ignore_version).then_some(rust_toolchain);
116
117 let check_rust_install = {
118 let rust_toolchain = rust_toolchain.clone();
119 let additional_target_triples = additional_target_triples.clone();
120 let additional_components = additional_components.clone();
121
122 move |_: &mut RustRuntimeServices<'_>| {
123 if which::which("cargo").is_err() {
124 anyhow::bail!("did not find `cargo` on $PATH");
125 }
126
127 let rust_toolchain = rust_toolchain.map(|s| format!("+{s}"));
128 let rust_toolchain = rust_toolchain.as_ref();
129
130 let sh = xshell::Shell::new()?;
132 xshell::cmd!(sh, "rustc {rust_toolchain...} -vV").run()?;
133
134 if let Ok(rustup) = which::which("rustup") {
136 for (thing, expected_things) in [
137 ("target", &additional_target_triples),
138 ("compoment", &additional_components),
139 ] {
140 let output = xshell::cmd!(
141 sh,
142 "{rustup} {rust_toolchain...} {thing} list --installed"
143 )
144 .ignore_status()
145 .output()?;
146 let stderr = String::from_utf8(output.stderr)?;
147 let stdout = String::from_utf8(output.stdout)?;
148
149 if stderr.contains("does not support components") {
156 log::warn!("Detected a non-standard `rustup default` toolchain!");
157 log::warn!(
158 "Will not be able to double-check that all required target-triples and components are available."
159 );
160 } else {
161 let mut installed_things = BTreeSet::new();
162
163 for line in stdout.lines() {
164 let triple = line.trim();
165 installed_things.insert(triple);
166 }
167
168 for expected_thing in expected_things {
169 if !installed_things.contains(expected_thing.as_str()) {
170 anyhow::bail!(
171 "missing required {thing}: {expected_thing}; to install: `rustup {thing} add {expected_thing}`"
172 )
173 }
174 }
175 }
176 }
177 } else {
178 log::warn!("`rustup` was not found!");
179 log::warn!(
180 "Unable to double-check that all target-triples and components are available."
181 )
182 }
183
184 anyhow::Ok(())
185 }
186 };
187
188 let check_is_installed = |write_cargo_bin: Option<
189 WriteVar<Option<crate::check_needs_relaunch::BinOrEnv>>,
190 >,
191 ensure_installed: Vec<WriteVar<SideEffect>>,
192 auto_install: bool,
193 ctx: &mut NodeCtx<'_>| {
194 if write_cargo_bin.is_some() || !ensure_installed.is_empty() {
195 if auto_install || matches!(ctx.backend(), FlowBackend::Github) {
196 let added_to_path = if matches!(ctx.backend(), FlowBackend::Github) {
197 Some(ctx.emit_rust_step("add default cargo home to path", |_| {
198 |_| {
199 let default_cargo_home = home::home_dir()
200 .context("Unable to get home dir")?
201 .join(".cargo")
202 .join("bin");
203 let github_path = std::env::var("GITHUB_PATH")?;
204 let mut github_path =
205 fs_err::File::options().append(true).open(github_path)?;
206 github_path
207 .write_all(default_cargo_home.as_os_str().as_encoded_bytes())?;
208 log::info!("Added {} to PATH", default_cargo_home.display());
209 Ok(())
210 }
211 }))
212 } else {
213 None
214 };
215
216 let rust_toolchain = rust_toolchain.clone();
217 ctx.emit_rust_step("install Rust", |ctx| {
218 let write_cargo_bin = if let Some(write_cargo_bin) = write_cargo_bin {
219 Some(write_cargo_bin.claim(ctx))
220 } else {
221 ensure_installed.claim(ctx);
222 None
223 };
224 added_to_path.claim(ctx);
225
226 move |rt: &mut RustRuntimeServices<'_>| {
227 if let Some(write_cargo_bin) = write_cargo_bin {
228 rt.write(write_cargo_bin, &Some(crate::check_needs_relaunch::BinOrEnv::Bin("cargo".to_string())));
229 }
230 let rust_toolchain = rust_toolchain.clone();
231 if check_rust_install.clone()(rt).is_ok() {
232 return Ok(());
233 }
234
235 let sh = xshell::Shell::new()?;
236 match rt.platform() {
237 FlowPlatform::Linux(_) => {
238 let interactive_prompt = Some("-y");
239 let mut default_toolchain = Vec::new();
240 if let Some(ver) = rust_toolchain {
241 default_toolchain.push("--default-toolchain".into());
242 default_toolchain.push(ver)
243 };
244
245 xshell::cmd!(
246 sh,
247 "curl --fail --proto =https --tlsv1.2 -sSf https://sh.rustup.rs -o rustup-init.sh"
248 )
249 .run()?;
250 xshell::cmd!(sh, "chmod +x ./rustup-init.sh").run()?;
251 xshell::cmd!(
252 sh,
253 "./rustup-init.sh {interactive_prompt...} {default_toolchain...}"
254 )
255 .run()?;
256 }
257 FlowPlatform::Windows => {
258 let interactive_prompt = Some("-y");
259 let mut default_toolchain = Vec::new();
260 if let Some(ver) = rust_toolchain {
261 default_toolchain.push("--default-toolchain".into());
262 default_toolchain.push(ver)
263 };
264
265 let arch = match rt.arch() {
266 FlowArch::X86_64 => "x86_64",
267 FlowArch::Aarch64 => "aarch64",
268 arch => anyhow::bail!("unsupported arch {arch}"),
269 };
270
271 xshell::cmd!(
272 sh,
273 "curl --fail -sSfLo rustup-init.exe https://win.rustup.rs/{arch} --output rustup-init"
274 ).run()?;
275 xshell::cmd!(
276 sh,
277 "./rustup-init.exe {interactive_prompt...} {default_toolchain...}"
278 )
279 .run()?;
280 },
281 platform => anyhow::bail!("unsupported platform {platform}"),
282 }
283
284 if !additional_target_triples.is_empty() {
285 xshell::cmd!(sh, "rustup target add {additional_target_triples...}")
286 .run()?;
287 }
288 if !additional_components.is_empty() {
289 xshell::cmd!(sh, "rustup component add {additional_components...}")
290 .run()?;
291 }
292
293 Ok(())
294 }
295 })
296 } else if let Some(write_cargo_bin) = write_cargo_bin {
297 ctx.emit_rust_step("ensure Rust is installed", |ctx| {
298 let write_cargo_bin = write_cargo_bin.claim(ctx);
299 move |rt| {
300 rt.write(
301 write_cargo_bin,
302 &Some(crate::check_needs_relaunch::BinOrEnv::Bin(
303 "cargo".to_string(),
304 )),
305 );
306
307 check_rust_install(rt)?;
308 Ok(())
309 }
310 })
311 } else {
312 ReadVar::from_static(()).into_side_effect()
313 }
314 } else {
315 ReadVar::from_static(()).into_side_effect()
316 }
317 };
318
319 let is_installed =
323 if !ensure_installed.is_empty() && matches!(ctx.backend(), FlowBackend::Local) {
324 let (read_bin, write_cargo_bin) = ctx.new_var();
325 ctx.req(crate::check_needs_relaunch::Params {
326 check: read_bin,
327 done: ensure_installed,
328 });
329 check_is_installed(Some(write_cargo_bin), Vec::new(), auto_install, ctx)
330 } else {
331 check_is_installed(None, ensure_installed, auto_install, ctx)
332 };
333
334 if !get_rust_toolchain.is_empty() {
335 ctx.emit_rust_step("detect active toolchain", |ctx| {
336 is_installed.clone().claim(ctx);
337 let get_rust_toolchain = get_rust_toolchain.claim(ctx);
338
339 move |rt| {
340 let rust_toolchain = match rust_toolchain {
341 Some(toolchain) => Some(toolchain),
342 None => {
343 let sh = xshell::Shell::new()?;
344 if let Ok(rustup) = which::which("rustup") {
345 let output =
360 xshell::cmd!(sh, "{rustup} show active-toolchain").output()?;
361 let stdout = String::from_utf8(output.stdout)?;
362 let line = stdout.lines().next().unwrap();
363 Some(line.split(' ').next().unwrap().into())
364 } else {
365 None
366 }
367 }
368 };
369
370 rt.write_all(get_rust_toolchain, &rust_toolchain);
371
372 Ok(())
373 }
374 });
375 }
376
377 if !get_cargo_home.is_empty() {
378 ctx.emit_rust_step("report $CARGO_HOME", |ctx| {
379 is_installed.claim(ctx);
380 let get_cargo_home = get_cargo_home.claim(ctx);
381 move |rt| {
382 let cargo_home = home::cargo_home()?;
383 rt.write_all(get_cargo_home, &cargo_home);
384
385 Ok(())
386 }
387 });
388 }
389
390 Ok(())
391 }
392}