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 |rt: &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 flowey::shell_cmd!(rt, "rustc {rust_toolchain...} -vV").run()?;
128
129 if let Ok(rustup) = which::which("rustup") {
131 for (thing, expected_things) in [
132 ("target", &additional_target_triples),
133 ("component", &additional_components),
134 ] {
135 let output = flowey::shell_cmd!(
136 rt,
137 "{rustup} {rust_toolchain...} {thing} list --installed"
138 )
139 .ignore_status()
140 .output()?;
141 let stderr = String::from_utf8(output.stderr)?;
142 let stdout = String::from_utf8(output.stdout)?;
143
144 if stderr.contains("does not support components") {
151 log::warn!("Detected a non-standard `rustup default` toolchain!");
152 log::warn!(
153 "Will not be able to double-check that all required target-triples and components are available."
154 );
155 } else {
156 let mut installed_things = BTreeSet::new();
157
158 for line in stdout.lines() {
159 let triple = line.trim();
160 installed_things.insert(triple);
161 }
162
163 for expected_thing in expected_things {
164 if !installed_things.contains(expected_thing.as_str()) {
165 anyhow::bail!(
166 "missing required {thing}: {expected_thing}; to install: `rustup {thing} add {expected_thing}`"
167 )
168 }
169 }
170 }
171 }
172 } else {
173 log::warn!("`rustup` was not found!");
174 log::warn!(
175 "Unable to double-check that all target-triples and components are available."
176 )
177 }
178
179 anyhow::Ok(())
180 }
181 };
182
183 let check_is_installed = |write_cargo_bin: Option<
184 WriteVar<Option<crate::check_needs_relaunch::BinOrEnv>>,
185 >,
186 ensure_installed: Vec<WriteVar<SideEffect>>,
187 auto_install: bool,
188 ctx: &mut NodeCtx<'_>| {
189 if write_cargo_bin.is_some() || !ensure_installed.is_empty() {
190 if auto_install || matches!(ctx.backend(), FlowBackend::Github) {
191 let added_to_path = if matches!(ctx.backend(), FlowBackend::Github) {
192 Some(ctx.emit_rust_step("add default cargo home to path", |_| {
193 |_| {
194 let default_cargo_home = home::home_dir()
195 .context("Unable to get home dir")?
196 .join(".cargo")
197 .join("bin");
198 let github_path = std::env::var("GITHUB_PATH")?;
199 let mut github_path =
200 fs_err::File::options().append(true).open(github_path)?;
201 github_path
202 .write_all(default_cargo_home.as_os_str().as_encoded_bytes())?;
203 log::info!("Added {} to PATH", default_cargo_home.display());
204 Ok(())
205 }
206 }))
207 } else {
208 None
209 };
210
211 let rust_toolchain = rust_toolchain.clone();
212 ctx.emit_rust_step("install Rust", |ctx| {
213 let write_cargo_bin = if let Some(write_cargo_bin) = write_cargo_bin {
214 Some(write_cargo_bin.claim(ctx))
215 } else {
216 ensure_installed.claim(ctx);
217 None
218 };
219 added_to_path.claim(ctx);
220
221 move |rt: &mut RustRuntimeServices<'_>| {
222 if let Some(write_cargo_bin) = write_cargo_bin {
223 rt.write(write_cargo_bin, &Some(crate::check_needs_relaunch::BinOrEnv::Bin("cargo".to_string())));
224 }
225 let rust_toolchain = rust_toolchain.clone();
226 if check_rust_install.clone()(rt).is_ok() {
227 return Ok(());
228 }
229
230 match rt.platform() {
231 FlowPlatform::Linux(_) => {
232 let interactive_prompt = Some("-y");
233 let mut default_toolchain = Vec::new();
234 if let Some(ver) = rust_toolchain {
235 default_toolchain.push("--default-toolchain".into());
236 default_toolchain.push(ver)
237 };
238
239 flowey::shell_cmd!(
240 rt,
241 "curl --fail --proto =https --tlsv1.2 -sSf https://sh.rustup.rs -o rustup-init.sh"
242 )
243 .run()?;
244 flowey::shell_cmd!(rt, "chmod +x ./rustup-init.sh").run()?;
245 flowey::shell_cmd!(
246 rt,
247 "./rustup-init.sh {interactive_prompt...} {default_toolchain...}"
248 )
249 .run()?;
250 }
251 FlowPlatform::Windows => {
252 let interactive_prompt = Some("-y");
253 let mut default_toolchain = Vec::new();
254 if let Some(ver) = rust_toolchain {
255 default_toolchain.push("--default-toolchain".into());
256 default_toolchain.push(ver)
257 };
258
259 let arch = match rt.arch() {
260 FlowArch::X86_64 => "x86_64",
261 FlowArch::Aarch64 => "aarch64",
262 arch => anyhow::bail!("unsupported arch {arch}"),
263 };
264
265 flowey::shell_cmd!(
266 rt,
267 "curl --fail -sSfLo rustup-init.exe https://win.rustup.rs/{arch} --output rustup-init"
268 ).run()?;
269 flowey::shell_cmd!(
270 rt,
271 "./rustup-init.exe {interactive_prompt...} {default_toolchain...}"
272 )
273 .run()?;
274 },
275 platform => anyhow::bail!("unsupported platform {platform}"),
276 }
277
278 if !additional_target_triples.is_empty() {
279 flowey::shell_cmd!(rt, "rustup target add {additional_target_triples...}")
280 .run()?;
281 }
282 if !additional_components.is_empty() {
283 flowey::shell_cmd!(rt, "rustup component add {additional_components...}")
284 .run()?;
285 }
286
287 Ok(())
288 }
289 })
290 } else if let Some(write_cargo_bin) = write_cargo_bin {
291 ctx.emit_rust_step("ensure Rust is installed", |ctx| {
292 let write_cargo_bin = write_cargo_bin.claim(ctx);
293 move |rt| {
294 rt.write(
295 write_cargo_bin,
296 &Some(crate::check_needs_relaunch::BinOrEnv::Bin(
297 "cargo".to_string(),
298 )),
299 );
300
301 check_rust_install(rt)?;
302 Ok(())
303 }
304 })
305 } else {
306 ReadVar::from_static(()).into_side_effect()
307 }
308 } else {
309 ReadVar::from_static(()).into_side_effect()
310 }
311 };
312
313 let is_installed =
317 if !ensure_installed.is_empty() && matches!(ctx.backend(), FlowBackend::Local) {
318 let (read_bin, write_cargo_bin) = ctx.new_var();
319 ctx.req(crate::check_needs_relaunch::Params {
320 check: read_bin,
321 done: ensure_installed,
322 });
323 check_is_installed(Some(write_cargo_bin), Vec::new(), auto_install, ctx)
324 } else {
325 check_is_installed(None, ensure_installed, auto_install, ctx)
326 };
327
328 if !get_rust_toolchain.is_empty() {
329 ctx.emit_rust_step("detect active toolchain", |ctx| {
330 is_installed.clone().claim(ctx);
331 let get_rust_toolchain = get_rust_toolchain.claim(ctx);
332
333 move |rt| {
334 let rust_toolchain = match rust_toolchain {
335 Some(toolchain) => Some(toolchain),
336 None => {
337 if let Ok(rustup) = which::which("rustup") {
338 let output =
353 flowey::shell_cmd!(rt, "{rustup} show active-toolchain")
354 .output()?;
355 let stdout = String::from_utf8(output.stdout)?;
356 let line = stdout.lines().next().unwrap();
357 Some(line.split(' ').next().unwrap().into())
358 } else {
359 None
360 }
361 }
362 };
363
364 rt.write_all(get_rust_toolchain, &rust_toolchain);
365
366 Ok(())
367 }
368 });
369 }
370
371 if !get_cargo_home.is_empty() {
372 ctx.emit_rust_step("report $CARGO_HOME", |ctx| {
373 is_installed.claim(ctx);
374 let get_cargo_home = get_cargo_home.claim(ctx);
375 move |rt| {
376 let cargo_home = home::cargo_home()?;
377 rt.write_all(get_cargo_home, &cargo_home);
378
379 Ok(())
380 }
381 });
382 }
383
384 Ok(())
385 }
386}