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