Skip to content

Commit d709da7

Browse files
committed
feat(dotnet): add support for runtime-only installs
1 parent 37997e7 commit d709da7

File tree

4 files changed

+136
-20
lines changed

4 files changed

+136
-20
lines changed

docs/lang/dotnet.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,42 @@ version.
7878
| Install location | `DOTNET_ROOT` | `installs/dotnet/<version>/` |
7979
| Multi-targeting | Works out of the box | Requires switching versions |
8080

81+
## Runtime-only Installs
82+
83+
By default, mise installs the full .NET SDK. If you only need to _run_ .NET applications without building them and without the added overhead of the SDK, you can install just the runtime using the `runtime` inline option:
84+
85+
```sh
86+
mise use dotnet[runtime=dotnet]@8.0.14
87+
dotnet --list-runtimes
88+
```
89+
90+
### Valid runtime values
91+
92+
| Value | Framework | Use case |
93+
| -------------- | ---------------------------- | ------------------------ |
94+
| dotnet | Microsoft.NETCore.App | Console apps, libraries |
95+
| aspnetcore | Microsoft.AspNetCore.App | ASP.NET Core web apps |
96+
| windowsdesktop | Microsoft.WindowsDesktop.App | WPF / WinForms (Windows) |
97+
98+
### Example: mix SDK and runtime
99+
100+
You can install a full SDK for development alongside a runtime for a production-like environment:
101+
102+
```toml
103+
[tools]
104+
dotnet = ["9", { version = "8.0.14", runtime = "dotnet" }]
105+
```
106+
107+
::: warning
108+
109+
- **Version numbers are runtime versions**, not SDK versions. For example, `8.0.14` refers to .NET Runtime 8.0.14, not SDK 8.0.14. Check the [.NET release notes](https://github.com/dotnet/core/tree/main/release-notes) for available runtime versions.
110+
- Runtime-only installs do **not** include the SDK build tools. Commands like `dotnet build` and `dotnet publish` will not be available, and `dotnet --version` will not report an SDK version.
111+
:::
112+
113+
::: tip
114+
Only exact runtime versions are supported (e.g., `dotnet[runtime=dotnet]@8.0.14`). Channel syntax like `@8` is not currently supported for runtime installs, as it resolves against SDK versions rather than runtime versions.
115+
:::
116+
81117
## Environment Variables
82118

83119
The plugin sets the following environment variables:

e2e/core/test_dotnet

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,18 @@ cat <<EOF >global.json
1616
EOF
1717

1818
assert_contains "mise x -- dotnet --version" "8.0.408"
19+
20+
# Prepare and test environment is clean for runtime-only install tests
21+
mise uninstall "dotnet[runtime=dotnet]@8.0.13" 2>/dev/null || true
22+
assert_not_contains "mise x dotnet@8.0.408 -- dotnet --list-runtimes" "Microsoft.NETCore.App 8.0.13"
23+
24+
# Test runtime-only install: version NOT bundled by SDK 8.0.408, `--list-runtimes`
25+
assert_contains "mise x dotnet[runtime=dotnet]@8.0.13 -- dotnet --list-runtimes" "Microsoft.NETCore.App 8.0.13"
26+
27+
# Test runtime uninstall cleans up shared directory
28+
mise uninstall "dotnet[runtime=dotnet]@8.0.13"
29+
assert_not_contains "mise x dotnet@8.0.408 -- dotnet --list-runtimes" "Microsoft.NETCore.App 8.0.13"
30+
31+
# Prepare by removing any stale install and test (uncached) invalid runtime option returns a validation error
32+
mise uninstall "dotnet[runtime=invalid]@8.0.12" 2>/dev/null || mise uninstall "dotnet@8.0.12" 2>/dev/null || true
33+
assert_fail_contains "mise x dotnet[runtime=invalid]@8.0.12 -- true" "Invalid runtime option"

src/cli/uninstall.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,20 +125,27 @@ impl Uninstall {
125125
.filter(|v| v.starts_with(&query))
126126
.collect_vec(),
127127
};
128-
let mut tvs = matches
129-
.into_iter()
130-
.map(|v| {
131-
let tvr = ToolRequest::new(backend.ba().clone(), v, ToolSource::Unknown)?;
132-
let tv = ToolVersion::new(tvr, v.into());
133-
Ok((backend.clone(), tv))
134-
})
135-
.collect::<Result<Vec<_>>>()?;
128+
129+
let mut tvs = Vec::new();
130+
136131
if let Some(tvr) = &ta.tvr {
137132
tvs.push((
138133
backend.clone(),
139134
tvr.resolve(config, &Default::default()).await?,
140135
));
141136
}
137+
138+
tvs.extend(
139+
matches
140+
.into_iter()
141+
.map(|v| {
142+
let tvr = ToolRequest::new(backend.ba().clone(), v, ToolSource::Unknown)?;
143+
let tv = ToolVersion::new(tvr, v.into());
144+
Ok((backend.clone(), tv))
145+
})
146+
.collect::<Result<Vec<_>>>()?,
147+
);
148+
142149
if tvs.is_empty() {
143150
warn!(
144151
"no versions found for {}",

src/plugins/core/dotnet.rs

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ impl DotnetPlugin {
3636
}
3737

3838
async fn test_dotnet(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> {
39+
if tv.request.options().get("runtime").is_some() {
40+
// Skip version check for runtime-only installs — `dotnet --version` exits non-zero without an SDK
41+
return Ok(());
42+
}
3943
ctx.pr.set_message("dotnet --version".into());
4044
CmdLineRunner::new(DOTNET_BIN)
4145
.with_pr(ctx.pr.as_ref())
@@ -120,6 +124,17 @@ impl Backend for DotnetPlugin {
120124
};
121125
file::create_dir_all(&install_dir)?;
122126

127+
// Read and validate runtime options
128+
let runtime = tv.request.options().get("runtime").map(|s| s.to_string());
129+
if let Some(ref rt) = runtime
130+
&& runtime_framework_name(rt).is_none()
131+
{
132+
return Err(eyre::eyre!(
133+
"Invalid runtime option '{}'. Valid options: dotnet, aspnetcore, windowsdesktop",
134+
rt
135+
));
136+
}
137+
123138
// Download install script (always refresh to pick up upstream fixes)
124139
let script_path = install_script_path();
125140
file::create_dir_all(script_path.parent().unwrap())?;
@@ -131,9 +146,10 @@ impl Backend for DotnetPlugin {
131146
file::make_executable(&script_path)?;
132147

133148
// Run install script
149+
let install_type = if runtime.is_some() { "Runtime" } else { "SDK" };
134150
ctx.pr
135-
.set_message(format!("Installing .NET SDK {}", tv.version));
136-
install_cmd(&script_path, &install_dir, &tv.version)
151+
.set_message(format!("Installing .NET {} {}", install_type, tv.version));
152+
install_cmd(&script_path, &install_dir, &tv.version, runtime.as_deref())
137153
.with_pr(ctx.pr.as_ref())
138154
.envs(self.exec_env(&ctx.config, &ctx.ts, &tv).await?)
139155
.execute()?;
@@ -158,10 +174,25 @@ impl Backend for DotnetPlugin {
158174
if Self::is_isolated() {
159175
// Isolated: mise handles removal of install_path by default
160176
} else {
161-
// Shared: only remove this SDK version from the shared root
162-
let sdk_dir = dotnet_root().join("sdk").join(&tv.version);
163-
if sdk_dir.exists() {
164-
file::remove_all(&sdk_dir)?;
177+
let runtime = tv.request.options().get("runtime").map(|s| s.to_string());
178+
if let Some(rt) = runtime {
179+
// Runtime: remove the shared runtime directory for this version
180+
let Some(framework) = runtime_framework_name(&rt) else {
181+
return Ok(());
182+
};
183+
let runtime_dir = dotnet_root()
184+
.join("shared")
185+
.join(framework)
186+
.join(&tv.version);
187+
if runtime_dir.exists() {
188+
file::remove_all(&runtime_dir)?;
189+
}
190+
} else {
191+
// SDK: only remove this SDK version from the shared root
192+
let sdk_dir = dotnet_root().join("sdk").join(&tv.version);
193+
if sdk_dir.exists() {
194+
file::remove_all(&sdk_dir)?;
195+
}
165196
}
166197
}
167198
Ok(())
@@ -207,6 +238,15 @@ impl Backend for DotnetPlugin {
207238
}
208239
}
209240

241+
fn runtime_framework_name(runtime: &str) -> Option<&'static str> {
242+
match runtime {
243+
"dotnet" => Some("Microsoft.NETCore.App"),
244+
"aspnetcore" => Some("Microsoft.AspNetCore.App"),
245+
"windowsdesktop" => Some("Microsoft.WindowsDesktop.App"),
246+
_ => None,
247+
}
248+
}
249+
210250
fn dotnet_root() -> PathBuf {
211251
Settings::get()
212252
.dotnet
@@ -243,18 +283,32 @@ fn install_script_url() -> &'static str {
243283
}
244284

245285
#[cfg(unix)]
246-
fn install_cmd<'a>(script_path: &Path, install_dir: &Path, version: &str) -> CmdLineRunner<'a> {
247-
CmdLineRunner::new(script_path)
286+
fn install_cmd<'a>(
287+
script_path: &Path,
288+
install_dir: &Path,
289+
version: &str,
290+
runtime: Option<&str>,
291+
) -> CmdLineRunner<'a> {
292+
let mut cmd = CmdLineRunner::new(script_path)
248293
.arg("--install-dir")
249294
.arg(install_dir)
250295
.arg("--version")
251296
.arg(version)
252-
.arg("--no-path")
297+
.arg("--no-path");
298+
if let Some(rt) = runtime {
299+
cmd = cmd.arg("--runtime").arg(rt);
300+
}
301+
cmd
253302
}
254303

255304
#[cfg(windows)]
256-
fn install_cmd<'a>(script_path: &Path, install_dir: &Path, version: &str) -> CmdLineRunner<'a> {
257-
CmdLineRunner::new("powershell")
305+
fn install_cmd<'a>(
306+
script_path: &Path,
307+
install_dir: &Path,
308+
version: &str,
309+
runtime: Option<&str>,
310+
) -> CmdLineRunner<'a> {
311+
let mut cmd = CmdLineRunner::new("powershell")
258312
.arg("-ExecutionPolicy")
259313
.arg("Bypass")
260314
.arg("-File")
@@ -263,7 +317,11 @@ fn install_cmd<'a>(script_path: &Path, install_dir: &Path, version: &str) -> Cmd
263317
.arg(install_dir)
264318
.arg("-Version")
265319
.arg(version)
266-
.arg("-NoPath")
320+
.arg("-NoPath");
321+
if let Some(rt) = runtime {
322+
cmd = cmd.arg("-Runtime").arg(rt);
323+
}
324+
cmd
267325
}
268326

269327
// --- Microsoft releases API types ---

0 commit comments

Comments
 (0)