Skip to content

Commit cd513e5

Browse files
committed
New post
1 parent fa2a7db commit cd513e5

File tree

1 file changed

+259
-0
lines changed

1 file changed

+259
-0
lines changed
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
---
2+
layout: post
3+
title: "Script to keep .NET versions on my Mac updated"
4+
author: "Marcus Hammarberg"
5+
date: 2025-06-11 04:00:00
6+
tags:
7+
- .NET
8+
- Programming
9+
---
10+
11+
On my Mac I was struggling to keep different version of .NET updated. I tried Homebrew (which is the Mac-way) but that gave me more problems. I remember reading a [Scott Hanselman post](https://www.hanselman.com/blog/side-by-side-user-scoped-net-core-installations-on-linux-with-dotnetinstallsh) on how to install .NET with a script. And that is [an official way to install .NET](https://learn.microsoft.com/en-us/dotnet/core/install/macos#install-net-with-a-script).
12+
13+
But I wanted to automate this to be run as part of my dotfiles installation. Let me describe what I did.
14+
15+
<!-- excerpt-end -->
16+
17+
## Augmenting script with parameters
18+
19+
The script (`dotnet-install.sh`) takes a few parameters so that you can use different channels (like .NET 6, 8 and 9 for example) and architecture etc. I wanted to write a script that installs the latest supported for several channels. Which can be done with the following commands:
20+
21+
```bash
22+
curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- \
23+
--channel "6.0" \
24+
--install-dir "$HOME/.dotnet" \
25+
--arch "arm64" \
26+
> /dev/null
27+
28+
curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- \
29+
--channel "8.0" \
30+
--install-dir "$HOME/.dotnet" \
31+
--arch "arm64" \
32+
> /dev/null
33+
34+
curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- \
35+
--channel "9.0" \
36+
--install-dir "$HOME/.dotnet" \
37+
--arch "arm64" \
38+
> /dev/null
39+
40+
```
41+
42+
These commands will download the script and then execute it with `bash`. But that's a bit chatty, lets make it tighter with some variables:
43+
44+
45+
```bash
46+
#!/bin/bash
47+
INSTALL_DIR="$HOME/.dotnet"
48+
ARCH="arm64"
49+
SCRIPT_URL="https://dot.net/v1/dotnet-install.sh"
50+
CHANNELS=("6.0" "8.0" "9.0")
51+
52+
for CHANNEL in "${CHANNELS[@]}"; do
53+
echo "⬇️ Installing $CHANNEL SDK ($LATEST_VERSION)..."
54+
curl -sSL "$SCRIPT_URL" | bash -s -- \
55+
--channel "$CHANNEL" \
56+
--install-dir "$INSTALL_DIR" \
57+
--arch "$ARCH" \
58+
> /dev/null
59+
done
60+
```
61+
62+
In fact, let's create a function for the installation-part, which will make overall structure a bit more readable:
63+
64+
65+
66+
```bash
67+
#!/bin/bash
68+
69+
INSTALL_DIR="$HOME/.dotnet"
70+
ARCH="arm64"
71+
SCRIPT_URL="https://dot.net/v1/dotnet-install.sh"
72+
CHANNELS=("6.0" "8.0" "9.0")
73+
74+
# Add to path for script execution
75+
export DOTNET_ROOT="$INSTALL_DIR"
76+
export PATH="$DOTNET_ROOT:$PATH"
77+
78+
# Create install dir if needed
79+
mkdir -p "$INSTALL_DIR"
80+
81+
# Function: install latest SDK from a channel
82+
install_dotnet_channel() {
83+
local CHANNEL=$1
84+
85+
echo "⬇️ Installing $CHANNEL SDK ($LATEST_VERSION)..."
86+
curl -sSL "$SCRIPT_URL" | bash -s -- \
87+
--channel "$CHANNEL" \
88+
--install-dir "$INSTALL_DIR" \
89+
--arch "$ARCH" \
90+
> /dev/null
91+
92+
xattr -dr com.apple.quarantine "$INSTALL_DIR"
93+
echo "✅ Installed new .NET $CHANNEL SDK ($LATEST_VERSION)."
94+
}
95+
96+
# Main loop
97+
for CHANNEL in "${CHANNELS[@]}"; do
98+
install_dotnet_channel "$CHANNEL"
99+
done
100+
```
101+
102+
After executing this you can run `dotnet --list-sdks` to and see the installed versions.
103+
104+
## Removing old versions
105+
106+
A good start, but this will flood my installation directory with all the minor and patch versions. I don't really care - I just want the latest version of each.
107+
108+
Let's make the script do that. First we introduce a function to find the latest version to use for each channel. We can actually ask `dotnet-install.sh` to get to know that.
109+
110+
```bash
111+
get_latest_version() {
112+
local CHANNEL=$1
113+
curl -sSL "$SCRIPT_URL" | bash -s -- --channel "$CHANNEL" --dry-run --install-dir "$INSTALL_DIR" --arch "$ARCH" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1
114+
}
115+
```
116+
117+
Now we need to remove the version that is not the latest version. First - here's a function that removes all versions except the latest version:
118+
119+
```bash
120+
remove_old_versions() {
121+
local VERSION=$1
122+
local MAJOR=$(echo "$VERSION" | cut -d. -f1)
123+
local KEEP_VERSION=$VERSION
124+
125+
echo "🧹 Checking for SDKs with major version $MAJOR to clean up (except $KEEP_VERSION)..."
126+
127+
for sdk in "$INSTALL_DIR/sdk"/*; do
128+
if [[ -d "$sdk" ]]; then
129+
sdk_version=$(basename "$sdk")
130+
sdk_major=$(echo "$sdk_version" | cut -d. -f1)
131+
if [[ "$sdk_major" == "$MAJOR" && "$sdk_version" != "$KEEP_VERSION" ]]; then
132+
echo "🗑 Removing $sdk_version"
133+
rm -rf "$sdk"
134+
fi
135+
fi
136+
done
137+
}
138+
```
139+
140+
Bash is a bit weird to read, in that `local VERSION=$1` is the way to get the first parameter passed to the function. Like this `remove_old_versions "8.0.31"`.
141+
142+
Now we can put that together, with the installation-section we had before. Let's put that in a function too:
143+
144+
```bash
145+
install_dotnet_channel() {
146+
local CHANNEL=$1
147+
local LATEST_VERSION=$(get_latest_version "$CHANNEL")
148+
149+
if [[ -z "$LATEST_VERSION" ]]; then
150+
echo "❌ Could not determine latest version for $CHANNEL"
151+
return
152+
fi
153+
154+
if [[ ! -d "$INSTALL_DIR/sdk/$LATEST_VERSION" ]]; then
155+
remove_old_versions "$LATEST_VERSION"
156+
echo "⬇️ Installing $CHANNEL SDK ($LATEST_VERSION)..."
157+
curl -sSL "$SCRIPT_URL" | bash -s -- \
158+
--channel "$CHANNEL" \
159+
--install-dir "$INSTALL_DIR" \
160+
--arch "$ARCH" \
161+
> /dev/null
162+
163+
xattr -dr com.apple.quarantine "$INSTALL_DIR"
164+
echo "✅ Installed new .NET $CHANNEL SDK ($LATEST_VERSION)."
165+
fi
166+
}
167+
```
168+
169+
This function will be called like this `install_dotnet_channel "$CHANNEL"` and do the following:
170+
171+
1. First get the latest version for `$CHANNEL`
172+
1. `if [[ ! -d "$INSTALL_DIR/sdk/$LATEST_VERSION" ]]; then` checks if the latest version is *not* installed.
173+
1. In that case - we remove all versions that is not `$LATEST_VERSION`
174+
1. And then install the $CHANNEL latests version, as before.
175+
176+
And with that our script is done:
177+
178+
```bash
179+
#!/bin/bash
180+
181+
INSTALL_DIR="$HOME/.dotnet"
182+
ARCH="arm64"
183+
SCRIPT_URL="https://dot.net/v1/dotnet-install.sh"
184+
CHANNELS=("6.0" "8.0" "9.0")
185+
186+
# Add to path for script execution
187+
export DOTNET_ROOT="$INSTALL_DIR"
188+
export PATH="$DOTNET_ROOT:$PATH"
189+
190+
# Create install dir if needed
191+
mkdir -p "$INSTALL_DIR"
192+
193+
# Function: get latest version from a channel using dry-run
194+
get_latest_version() {
195+
local CHANNEL=$1
196+
curl -sSL "$SCRIPT_URL" | bash -s -- --channel "$CHANNEL" --dry-run --install-dir "$INSTALL_DIR" --arch "$ARCH" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1
197+
}
198+
199+
# Function: remove older SDKs from same major version
200+
remove_old_versions() {
201+
local VERSION=$1
202+
local MAJOR=$(echo "$VERSION" | cut -d. -f1)
203+
local KEEP_VERSION=$VERSION
204+
205+
echo "🧹 Checking for SDKs with major version $MAJOR to clean up (except $KEEP_VERSION)..."
206+
207+
for sdk in "$INSTALL_DIR/sdk"/*; do
208+
if [[ -d "$sdk" ]]; then
209+
sdk_version=$(basename "$sdk")
210+
sdk_major=$(echo "$sdk_version" | cut -d. -f1)
211+
if [[ "$sdk_major" == "$MAJOR" && "$sdk_version" != "$KEEP_VERSION" ]]; then
212+
echo "🗑 Removing $sdk_version"
213+
rm -rf "$sdk"
214+
fi
215+
fi
216+
done
217+
}
218+
219+
# Function: install latest SDK from a channel
220+
install_dotnet_channel() {
221+
local CHANNEL=$1
222+
local LATEST_VERSION=$(get_latest_version "$CHANNEL")
223+
224+
if [[ -z "$LATEST_VERSION" ]]; then
225+
echo "❌ Could not determine latest version for $CHANNEL"
226+
return
227+
fi
228+
229+
if [[ ! -d "$INSTALL_DIR/sdk/$LATEST_VERSION" ]]; then
230+
remove_old_versions "$LATEST_VERSION"
231+
echo "⬇️ Installing $CHANNEL SDK ($LATEST_VERSION)..."
232+
curl -sSL "$SCRIPT_URL" | bash -s -- \
233+
--channel "$CHANNEL" \
234+
--install-dir "$INSTALL_DIR" \
235+
--arch "$ARCH" \
236+
> /dev/null
237+
238+
xattr -dr com.apple.quarantine "$INSTALL_DIR"
239+
echo "✅ Installed new .NET $CHANNEL SDK ($LATEST_VERSION)."
240+
fi
241+
}
242+
243+
# Main loop
244+
for CHANNEL in "${CHANNELS[@]}"; do
245+
install_dotnet_channel "$CHANNEL"
246+
done
247+
```
248+
249+
## Using as part of my dotfiles
250+
251+
I have all my Mac configuration in a [dotfiles](https://github.com/marcusoftnet/dotfiles) directory. I've based that repository from others [developers greater than me](https://github.com/holman/dotfiles). And for that I'm grateful, because it gave me some tools and conventions that makes it very easy to keep my computer up-date.
252+
253+
After cloning the repository it will be initially setup using the [`bootstrap`](https://github.com/marcusoftnet/dotfiles/blob/master/script/bootstrap) script. After that all configuration will reside in and be dynamically linked to `~/.dotfiles`
254+
255+
One command that gets created is `dot` which will automatically go through all your sub-folders in `~/.dotfiles` and run commands like `install.sh`.
256+
257+
That's a good spot to put our script. I created a `/dotnet/install.sh` script and put the script from above, in there. That means that it will automatically check for new .NET versions every time I run `dot` at the command line.
258+
259+
Pretty neat!

0 commit comments

Comments
 (0)