flowey_lib_common/
install_dotnet_cli.rs1use flowey::node::prelude::*;
14
15flowey_config! {
16 pub struct Config {
18 pub version: Option<String>,
22 pub auto_install: Option<bool>,
26 }
27}
28
29flowey_request! {
30 pub enum Request {
31 DotnetBin(WriteVar<PathBuf>),
33 }
34}
35
36new_flow_node_with_config!(struct Node);
37
38impl FlowNodeWithConfig for Node {
39 type Request = Request;
40 type Config = Config;
41
42 fn imports(_ctx: &mut ImportCtx<'_>) {}
43
44 fn emit(
45 config: Config,
46 requests: Vec<Self::Request>,
47 ctx: &mut NodeCtx<'_>,
48 ) -> anyhow::Result<()> {
49 let mut broadcast_dotnet_bin = Vec::new();
50
51 for req in requests {
52 match req {
53 Request::DotnetBin(outvar) => broadcast_dotnet_bin.push(outvar),
54 }
55 }
56
57 if broadcast_dotnet_bin.is_empty() {
58 return Ok(());
59 }
60
61 let version = config
62 .version
63 .ok_or(anyhow::anyhow!("missing config: version"))?;
64 let auto_install = config.auto_install;
65
66 match ctx.backend() {
69 FlowBackend::Ado => Self::emit_ado(ctx, broadcast_dotnet_bin, version),
70 FlowBackend::Local => {
71 let auto_install = auto_install
72 .ok_or(anyhow::anyhow!("Missing essential request: AutoInstall"))?;
73 Self::emit_local(ctx, broadcast_dotnet_bin, version, auto_install)
74 }
75 FlowBackend::Github => Self::emit_github(ctx, broadcast_dotnet_bin),
76 }
77 }
78}
79
80impl Node {
81 fn emit_ado(
82 ctx: &mut NodeCtx<'_>,
83 broadcast_dotnet_bin: Vec<WriteVar<PathBuf>>,
84 version: String,
85 ) -> anyhow::Result<()> {
86 let ado_version = if version.matches('.').count() < 2 {
89 format!("{version}.x")
90 } else {
91 version
92 };
93
94 let (dotnet_installed, claim_dotnet_installed) = ctx.new_var::<SideEffect>();
95 ctx.emit_ado_step("Install .NET SDK", move |ctx| {
96 claim_dotnet_installed.claim(ctx);
97 move |_| {
98 format!(
99 r#"
100 - task: UseDotNet@2
101 inputs:
102 packageType: sdk
103 version: '{ado_version}'
104 "#
105 )
106 }
107 });
108
109 ctx.emit_rust_step("report dotnet install", move |ctx| {
110 dotnet_installed.claim(ctx);
111 let broadcast_dotnet_bin = broadcast_dotnet_bin.claim(ctx);
112 move |rt| {
113 let dotnet_bin = which::which(rt.platform().binary("dotnet")).map_err(|_| {
114 anyhow::anyhow!("dotnet not found on PATH after UseDotNet task")
115 })?;
116 rt.write_all(broadcast_dotnet_bin, &dotnet_bin);
117 Ok(())
118 }
119 });
120
121 Ok(())
122 }
123
124 fn emit_local(
125 ctx: &mut NodeCtx<'_>,
126 broadcast_dotnet_bin: Vec<WriteVar<PathBuf>>,
127 version: String,
128 auto_install: bool,
129 ) -> anyhow::Result<()> {
130 if auto_install {
131 let persistent_dir = ctx.persistent_dir();
132
133 ctx.emit_rust_step("install dotnet", |ctx| {
134 let persistent_dir = persistent_dir.clone().claim(ctx);
135 let broadcast_dotnet_bin = broadcast_dotnet_bin.claim(ctx);
136 move |rt| {
137 if let Some(existing_dotnet) = find_dotnet_on_path(rt) {
138 log::info!("found existing dotnet at {}", existing_dotnet.display());
139 rt.write_all(broadcast_dotnet_bin, &existing_dotnet);
140 return Ok(());
141 }
142
143 let install_dir = rt
145 .read(persistent_dir)
146 .ok_or(anyhow::anyhow!(
147 "dotnet is not on PATH and no persistent directory is configured. \
148 Please install the .NET SDK manually: \
149 https://dotnet.microsoft.com/download"
150 ))?
151 .join("dotnet");
152
153 let dotnet_bin_name = rt.platform().binary("dotnet");
154 let dotnet_bin_path = install_dir.join(&dotnet_bin_name);
155
156 if !dotnet_bin_path.exists() {
157 log::info!(
158 "dotnet not found on PATH or at {}, installing...",
159 dotnet_bin_path.display()
160 );
161
162 fs_err::create_dir_all(&install_dir)?;
163
164 match rt.platform() {
165 FlowPlatform::Windows => {
166 let install_script_url = "https://dot.net/v1/dotnet-install.ps1";
167 let install_script_path = install_dir
168 .parent()
169 .unwrap_or(&install_dir)
170 .join("dotnet-install.ps1");
171
172 flowey::shell_cmd!(
173 rt,
174 "curl --fail -sSL -o {install_script_path} {install_script_url}"
175 )
176 .run()?;
177
178 flowey::shell_cmd!(
179 rt,
180 "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File {install_script_path}
181 -Channel {version}
182 -InstallDir {install_dir}
183 -NoPath
184 "
185 )
186 .run()?;
187 }
188 FlowPlatform::Linux(_) | FlowPlatform::MacOs => {
189 let install_script_url = "https://dot.net/v1/dotnet-install.sh";
190 let install_script_path = install_dir
191 .parent()
192 .unwrap_or(&install_dir)
193 .join("dotnet-install.sh");
194
195 flowey::shell_cmd!(
196 rt,
197 "curl --fail -sSL -o {install_script_path} {install_script_url}"
198 )
199 .run()?;
200
201 flowey::shell_cmd!(rt, "chmod +x {install_script_path}").run()?;
202
203 flowey::shell_cmd!(
204 rt,
205 "{install_script_path}
206 --channel {version}
207 --install-dir {install_dir}
208 --no-path
209 "
210 )
211 .run()?;
212 }
213 platform => {
214 anyhow::bail!("unsupported platform for dotnet install: {platform}")
215 }
216 }
217
218 if !dotnet_bin_path.exists() {
219 anyhow::bail!(
220 "dotnet installation completed but binary not found at {}",
221 dotnet_bin_path.display()
222 );
223 }
224 }
225
226 log::info!("using dotnet at {}", dotnet_bin_path.display());
227 rt.write_all(broadcast_dotnet_bin, &dotnet_bin_path);
228 Ok(())
229 }
230 });
231 } else {
232 ctx.emit_rust_step("ensure dotnet is installed", |ctx| {
234 let broadcast_dotnet_bin = broadcast_dotnet_bin.claim(ctx);
235 move |rt| {
236 let dotnet_bin = find_dotnet_on_path(rt).ok_or_else(|| {
237 anyhow::anyhow!(
238 "dotnet is not installed. Please install the .NET SDK: \
239 https://dotnet.microsoft.com/download"
240 )
241 })?;
242 rt.write_all(broadcast_dotnet_bin, &dotnet_bin);
243 Ok(())
244 }
245 });
246 }
247
248 Ok(())
249 }
250
251 fn emit_github(
252 ctx: &mut NodeCtx<'_>,
253 broadcast_dotnet_bin: Vec<WriteVar<PathBuf>>,
254 ) -> anyhow::Result<()> {
255 ctx.emit_rust_step("resolve dotnet", |ctx| {
258 let broadcast_dotnet_bin = broadcast_dotnet_bin.claim(ctx);
259 move |rt| {
260 let dotnet_bin = which::which(rt.platform().binary("dotnet")).map_err(|_| {
261 anyhow::anyhow!(
262 "dotnet not found on PATH. \
263 Add a `uses: actions/setup-dotnet` step to your workflow."
264 )
265 })?;
266 rt.write_all(broadcast_dotnet_bin, &dotnet_bin);
267 Ok(())
268 }
269 });
270
271 Ok(())
272 }
273}
274
275fn find_dotnet_on_path(rt: &mut RustRuntimeServices<'_>) -> Option<PathBuf> {
278 let path = which::which("dotnet").ok()?;
279 if crate::_util::running_in_wsl(rt) {
280 let is_windows_exe = path
281 .extension()
282 .and_then(|ext| ext.to_str())
283 .map(|ext| ext.eq_ignore_ascii_case("exe"))
284 .unwrap_or(false);
285 if is_windows_exe {
286 log::warn!(
287 "ignoring Windows dotnet.exe at {} on WSL; \
288 a native Linux dotnet is required",
289 path.display()
290 );
291 return None;
292 }
293 }
294 Some(path)
295}