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