Skip to content

Instantly share code, notes, and snippets.

@mmitchel
Last active April 12, 2026 13:25
Show Gist options
  • Select an option

  • Save mmitchel/4ca91fe57c21189476b9af4ae2a60893 to your computer and use it in GitHub Desktop.

Select an option

Save mmitchel/4ca91fe57c21189476b9af4ae2a60893 to your computer and use it in GitHub Desktop.
Ubuntu-24.04 WSL Instance

README-WSL2 for Ubuntu

Configuration for All Instances running on WSL

  • Install content to file at %USERPROFILE%.wslconfig
    [wsl2]
    defaultVhdSize=549755813888
    networkingMode=Mirrored
    firewall=false
    # processors=8
    # swap=4GB
    # memory=16GB
    [experimental]
    autoMemoryReclaim=Disabled
    hostAddressLoopback=true
    

Windows Preparation

  • Execute (CMD.EXE)

    setx.exe GNUPGHOME %USERPROFILE%\.gnupg
  • Execute (CMD.EXE)

    setx.exe WSLENV USERNAME:USERPROFILE/p
  • Execute (CMD.EXE)

    wsl.exe --shutdown
  • Execute (CMD.EXE)

    wsl.exe --update
  • Terminate all instances of Windows Terminal

    • Pick up needed environment variables

Configuration for Ubuntu 24.04 LTS running on WSL

  • Install Ubuntu-24.04.user-data at %USERPROFILE%.cloud-init\Ubuntu-24.04.user-data

    • Update apt.http_proxy as necessary
  • Execute (CMD.EXE)

    wsl.exe --unregister Ubuntu-24.04
  • Execute (CMD.EXE)

    wsl.exe --install Ubuntu-24.04
  • Execute (CMD.EXE)

    wsl.exe --list --running

    until Ubunu-24.04 is not shown

  • Terminate all instances of Windows Terminal

    • Pick up updated terminal profiles
  • Wait ~60 seconds

  • Start new instances of Windows Terminal CLI prompts

Interop with USB Mass Storage

  • Insure WSL Instance Ubuntu-24.04 is running

  • Install from https://github.com/dorssel/usbipd-win, v5.x

  • Insert USB flash disk

  • Execute Elevated Privileges (CMD.EXE)

    usbipd.exe list
    usbipd.exe unbind --all
    usbipd.exe bind --force --busid BUSID
    usbipd.exe attach --wsl --busid BUSID
  • Execute Elevated Privileges (CMD.EXE)

    usbipd.exe detach --all
    usbipd.exe unbind --all
#cloud-config
# Installation Information
# https://gist.github.com/mmitchel/4ca91fe57c21189476b9af4ae2a60893
### USER DEFINED ###########################################
# apt:
# http_proxy: 'http://windows_user:windows_password@10.63.136.30:8080/'
### INSTANCE SPECIFIC ######################################
locale: en_US.UTF-8
users:
- name: user
gecos: WSL Instance
plain_text_passwd: password
groups: [adm,dialout,disk,cdrom,floppy,sudo,audio,dip,video,plugdev,netdev]
shell: /bin/bash
homedir: /home/user
sudo: ['ALL=(ALL) NOPASSWD: ALL']
write_files:
# Overwrite the original
- path: /etc/wsl.conf
owner: 'root:root'
permissions: '0644'
defer: false
append: false
content: |
[boot]
systemd=true
[user]
default=user
[automount]
enabled=true
mountFsTab=true
options="case=off,fmask=0077,dmask=0077"
[network]
hostname=wsl
[interop]
enabled=true
appendWindowsPath=true
[time]
useWindowsTimezone=true
# Overwrite the original
- path: /etc/wsl-distribution.conf
owner: 'root:root'
permissions: '0644'
defer: false
append: false
content: |
[oobe]
command = /usr/lib/wsl/wsl-setup
defaultUid = 1000
defaultName = Ubuntu-24.04
[shortcut]
icon = /usr/share/wsl/ubuntu.ico
[windowsterminal]
ProfileTemplate = /usr/share/wsl/terminal-profile.json
# Overwrite the original
- path: /usr/lib/wsl/wsl-setup
owner: 'root:root'
permissions: '0755'
defer: false
append: false
content: |
#!/bin/bash
set -euo pipefail
echo "Provisioning the new WSL instance $WSL_DISTRO_NAME"
echo "This might take a while..."
# Wait for cloud-init to finish if systemd and its service is enabled
# by running the script located at the same dir as this one.
this_dir=$(dirname "$(realpath $0)")
source "${this_dir}/wait-for-cloud-init"
echo "Terminate this WSL instance by exiting. Upon termination, set"
echo "required environment variables by executing the following:"
echo " setx.exe GNUPGHOME %USERPROFILE%\.gnupg"
echo " setx.exe WSLENV USERNAME:USERPROFILE/p"
echo "Finally, terminate all Windows Terminal instances. Allow WSL"
echo "about 60 seconds to terminate. Start a new Windows Terminal"
echo "and open an Ubuntu-24.04 instance."
# Overwrite the original
- path: /usr/lib/wsl/wait-for-cloud-init
owner: 'root:root'
permissions: '0755'
defer: false
append: false
content: |
#!/bin/bash
set -euo pipefail
if status=$(LANG=C systemctl is-system-running 2>/dev/null) || [ "${status}" != "offline" ] && systemctl is-enabled --quiet cloud-init-local.service 2>/dev/null; then
cloud-init status --wait > /dev/null 2>&1 || true
touch /etc/cloud/cloud-init.disabled || true
fi
- path: /etc/profile.d/wsl-instance.sh
owner: 'root:root'
permissions: '0644'
content: |
# WSL Instance
if [ "$USER" != "root" ] ; then
if [ -n "$USERPROFILE" ] ; then
if [ -d "$USERPROFILE" ] ; then
mkdir -p "$USERPROFILE/.config" && ln -sf "$USERPROFILE/.config" $HOME/.config && rm -f $HOME/.config/.config
mkdir -p "$USERPROFILE/.gnupg" && ln -sf "$USERPROFILE/.gnupg" $HOME/.gnupg && rm -f $HOME/.gnupg/.gnupg
mkdir -p "$USERPROFILE/.local/bin"
export PATH="$USERPROFILE/.local/bin:$PATH"
[[ -d $HOME/.ssh && ! -h $HOME/.ssh ]] && mv -f $HOME/.ssh $HOME/.ssh.old
mkdir -p "$USERPROFILE/.ssh" && ln -sf "$USERPROFILE/.ssh" $HOME/.ssh && rm -f $HOME/.ssh/.ssh
touch "$USERPROFILE/.netrc" && ln -sf "$USERPROFILE/.netrc" $HOME/.netrc
touch "$USERPROFILE/.s3cfg" && ln -sf "$USERPROFILE/.s3cfg" $HOME/.s3cfg
rm -fr $HOME/.config/systemd/user
rm -fr $HOME/.config/systemd/user.control
fi
fi
fi
- path: /etc/sysctl.d/wsl-instance.conf
owner: 'root:root'
permissions: '0644'
content: |
fs.inotify.max_user_watches = 524288
- path: /etc/smbcredentials
owner: 'root:root'
permissions: '0600'
content: |
username=windows_user
password=windows_password
- path: /etc/sysctl.d/wsl-instance.conf
owner: 'root:root'
permissions: '0644'
content: |
fs.inotify.max_user_watches = 524288
- path: /etc/udev/rules.d/dm-control.rules
owner: 'root:root'
permissions: '0644'
content: |
# Allow members of the 'disk' group to access device-mapper control device
# This enables non-root LVM operations when user is in 'disk' group
# https://udev.freedesktop.org/
# NOTE: Must run early before device-mapper subsystem initializes
# device-mapper control device (character device)
SUBSYSTEM=="misc", KERNEL=="mapper/control", GROUP="disk", MODE="0660"
KERNEL=="mapper/control", GROUP="disk", MODE="0660"
# All device-mapper devices (block devices)
SUBSYSTEM=="block", KERNEL=="dm-*", GROUP="disk", MODE="0660"
KERNEL=="dm-*", GROUP="disk", MODE="0660"
# device-mapper character devices
SUBSYSTEM=="char", KERNEL=="device-mapper", GROUP="disk", MODE="0660"
# Loop devices (already covered by other rules but included for completeness)
KERNEL=="loop[0-9]*", GROUP="disk", MODE="0660"
SUBSYSTEM=="block", KERNEL=="loop[0-9]*", GROUP="disk", MODE="0660"
- path: /etc/tmpfiles.d/dm-control.conf
owner: 'root:root'
permissions: '0644'
content: |
z /dev/mapper/control 0660 root disk - -
mounts:
- ["# C:","/mnt/c","drvfs","defaults,nosuid,nodev,x-mount.mkdir","0","0"]
- ["# M:","/mnt/m","drvfs","defaults,nosuid,nodev,x-mount.mkdir","0","0"]
- ["# /src/path","/mnt/bind","none","bind,defaults,x-mount.mkdir","0","0"]
- ["# //10.236.1.99/share","/mnt/share","cifs","credentials=/etc/smbcredentials,nosuid,nodev,x-mount.mkdir","0","0" ]
package_reboot_if_required: true
package_update: true
package_upgrade: true
packages:
# Development
- build-essential
- chrpath
- cpio
- debianutils
- diffstat
- file
- gawk
- gcc
- git
- git-lfs
- iputils-ping
- libacl1
- liblz4-tool
- locales
- python3
- python3-git
- python3-jinja2
- python3-pexpect
- python3-pip
- python3-subunit
- socat
- texinfo
- unzip
- wget
- xz-utils
- zstd
- lz4
# General
# - wslu
- curl
- gnupg2
- htop
- jq
- pciutils
- usbutils
- python3-paho-mqtt
- python3-serial
- python3-serial-asyncio
- python3-paramiko
- python3-venv
- uidmap
- libxml2-utils
- docker.io
- docker-compose
# Disk and Filesystem
- rsync
- s3cmd
- dosfstools
- e2fsprogs
- btrfs-progs
- cryptsetup
- ostree
- cifs-utils
- parted
- lvm2
runcmd:
# Setup repo tool
- |
mkdir -p /usr/local/bin
curl https://storage.googleapis.com/git-repo-downloads/repo > /usr/local/bin/repo
chmod a+x /usr/local/bin/repo
# Setup ~user
- |
rsync -av /etc/skel/ /home/user/
touch /home/user/.hushlogin
chown -R user:user /home/user
# Setup ~root
- |
mkdir -p /srv/repo
chmod 1777 /srv/repo
touch /root/.hushlogin
usermod -aG docker user
- |
# Final poweroff
shutdown --poweroff now
#cloud-config
# Installation Information
# https://gist.github.com/mmitchel/4ca91fe57c21189476b9af4ae2a60893
### USER DEFINED ###########################################
# apt:
# http_proxy: 'http://windows_user:windows_password@10.63.136.30:8080/'
### INSTANCE SPECIFIC ######################################
locale: en_US.UTF-8
users:
- name: user
gecos: WSL Instance
plain_text_passwd: password
groups: [adm,dialout,disk,cdrom,floppy,sudo,audio,dip,video,plugdev,netdev]
shell: /bin/bash
homedir: /home/user
sudo: ['ALL=(ALL) NOPASSWD: ALL']
write_files:
# Overwrite the original
- path: /etc/wsl.conf
owner: 'root:root'
permissions: '0644'
defer: false
append: false
content: |
[boot]
systemd=true
[user]
default=user
[automount]
enabled=true
mountFsTab=true
options="case=off,fmask=0077,dmask=0077"
[network]
hostname=wsl
[interop]
enabled=true
appendWindowsPath=true
[time]
useWindowsTimezone=true
# Overwrite the original
- path: /etc/wsl-distribution.conf
owner: 'root:root'
permissions: '0644'
defer: false
append: false
content: |
[oobe]
command = /usr/lib/wsl/wsl-setup
defaultUid = 1000
defaultName = Ubuntu-24.04
[shortcut]
icon = /usr/share/wsl/ubuntu.ico
[windowsterminal]
ProfileTemplate = /usr/share/wsl/terminal-profile.json
# Overwrite the original
- path: /usr/lib/wsl/wsl-setup
owner: 'root:root'
permissions: '0755'
defer: false
append: false
content: |
#!/bin/bash
set -euo pipefail
echo "Provisioning the new WSL instance $WSL_DISTRO_NAME"
echo "This might take a while..."
# Wait for cloud-init to finish if systemd and its service is enabled
# by running the script located at the same dir as this one.
this_dir=$(dirname "$(realpath $0)")
source "${this_dir}/wait-for-cloud-init"
echo "Terminate this WSL instance by exiting. Upon termination, set"
echo "required environment variables by executing the following:"
echo " setx.exe GNUPGHOME %USERPROFILE%\.gnupg"
echo " setx.exe WSLENV USERNAME:USERPROFILE/p"
echo "Finally, terminate all Windows Terminal instances. Allow WSL"
echo "about 60 seconds to terminate. Start a new Windows Terminal"
echo "and open an Ubuntu-24.04 instance."
- path: /etc/profile.d/wsl-instance.sh
owner: 'root:root'
permissions: '0644'
content: |
# WSL Instance
if [ "$USER" != "root" ] ; then
if [ -n "$USERPROFILE" ] ; then
if [ -d "$USERPROFILE" ] ; then
mkdir -p "$USERPROFILE/.config" && ln -sf "$USERPROFILE/.config" $HOME/.config && rm -f $HOME/.config/.config
mkdir -p "$USERPROFILE/.gnupg" && ln -sf "$USERPROFILE/.gnupg" $HOME/.gnupg && rm -f $HOME/.gnupg/.gnupg
mkdir -p "$USERPROFILE/.local/bin"
export PATH="$USERPROFILE/.local/bin:$PATH"
[[ -d $HOME/.ssh && ! -h $HOME/.ssh ]] && mv -f $HOME/.ssh $HOME/.ssh.old
mkdir -p "$USERPROFILE/.ssh" && ln -sf "$USERPROFILE/.ssh" $HOME/.ssh && rm -f $HOME/.ssh/.ssh
touch "$USERPROFILE/.netrc" && ln -sf "$USERPROFILE/.netrc" $HOME/.netrc
touch "$USERPROFILE/.s3cfg" && ln -sf "$USERPROFILE/.s3cfg" $HOME/.s3cfg
rm -fr $HOME/.config/systemd/user
rm -fr $HOME/.config/systemd/user.control
fi
fi
fi
- path: /etc/sysctl.d/wsl-instance.conf
owner: 'root:root'
permissions: '0644'
content: |
fs.inotify.max_user_watches = 524288
- path: /etc/smbcredentials
owner: 'root:root'
permissions: '0600'
content: |
username=windows_user
password=windows_password
- path: /etc/sysctl.d/wsl-instance.conf
owner: 'root:root'
permissions: '0644'
content: |
fs.inotify.max_user_watches = 524288
- path: /etc/udev/rules.d/dm-control.rules
owner: 'root:root'
permissions: '0644'
content: |
# Allow members of the 'disk' group to access device-mapper control device
# This enables non-root LVM operations when user is in 'disk' group
# https://udev.freedesktop.org/
# NOTE: Must run early before device-mapper subsystem initializes
# device-mapper control device (character device)
SUBSYSTEM=="misc", KERNEL=="mapper/control", GROUP="disk", MODE="0660"
KERNEL=="mapper/control", GROUP="disk", MODE="0660"
# All device-mapper devices (block devices)
SUBSYSTEM=="block", KERNEL=="dm-*", GROUP="disk", MODE="0660"
KERNEL=="dm-*", GROUP="disk", MODE="0660"
# device-mapper character devices
SUBSYSTEM=="char", KERNEL=="device-mapper", GROUP="disk", MODE="0660"
# Loop devices (already covered by other rules but included for completeness)
KERNEL=="loop[0-9]*", GROUP="disk", MODE="0660"
SUBSYSTEM=="block", KERNEL=="loop[0-9]*", GROUP="disk", MODE="0660"
- path: /etc/tmpfiles.d/dm-control.conf
owner: 'root:root'
permissions: '0644'
content: |
z /dev/mapper/control 0660 root disk - -
mounts:
- ["# C:","/mnt/c","drvfs","defaults,nosuid,nodev,x-mount.mkdir","0","0"]
- ["# M:","/mnt/m","drvfs","defaults,nosuid,nodev,x-mount.mkdir","0","0"]
- ["# /src/path","/mnt/bind","none","bind,defaults,x-mount.mkdir","0","0"]
- ["# //10.236.1.99/share","/mnt/share","cifs","credentials=/etc/smbcredentials,nosuid,nodev,x-mount.mkdir","0","0" ]
package_reboot_if_required: true
package_update: true
package_upgrade: true
packages:
# Development
- build-essential
- chrpath
- cpio
- debianutils
- diffstat
- file
- gawk
- gcc
- git
- git-lfs
- iputils-ping
- libacl1
- liblz4-tool
- locales
- python3
- python3-git
- python3-jinja2
- python3-pexpect
- python3-pip
- python3-subunit
- socat
- texinfo
- unzip
- wget
- xz-utils
- zstd
- lz4
# General
# - wslu
- curl
- gnupg2
- htop
- jq
- pciutils
- usbutils
- python3-paho-mqtt
- python3-serial
- python3-serial-asyncio
- python3-paramiko
- python3-venv
- uidmap
- libxml2-utils
- docker.io
- docker-compose
# Disk and Filesystem
- rsync
- s3cmd
- dosfstools
- e2fsprogs
- btrfs-progs
- cryptsetup
- ostree
- cifs-utils
- parted
- lvm2
runcmd:
# Setup repo tool
- |
mkdir -p /usr/local/bin
curl https://storage.googleapis.com/git-repo-downloads/repo > /usr/local/bin/repo
chmod a+x /usr/local/bin/repo
# Setup ~user
- |
rsync -av /etc/skel/ /home/user/
touch /home/user/.hushlogin
chown -R user:user /home/user
# Setup ~root
- |
mkdir -p /srv/repo
chmod 1777 /srv/repo
touch /root/.hushlogin
usermod -aG docker user
- |
# Final poweroff
shutdown --poweroff now
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[switch]$SystemWide
)
# Ensure script stops on errors.
$ErrorActionPreference = "Stop"
# Force TLS 1.2+ for all web requests.
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13
function Test-IsAdministrator {
$currentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($currentIdentity)
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Get-OsArchitecture {
[OutputType([string])]
param()
# Detect OS architecture rather than process bitness.
$osArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()
switch ($osArch) {
"x64" { return "x64" }
"arm64" { return "arm64" }
default {
throw "Unsupported OS architecture for usbipd-win MSI selection: $osArch"
}
}
}
function Install-UsbipdWin {
[OutputType([pscustomobject])]
param(
[Parameter(Mandatory = $true)]
[bool]$IsAdministrator
)
$result = [ordered]@{
Installed = 0
Skipped = 0
Failed = 0
}
if ($null -ne (Get-Command -Name "usbipd" -ErrorAction SilentlyContinue)) {
Write-Host "usbipd-win is already installed, skipping." -ForegroundColor Gray
$result.Skipped++
return [pscustomobject]$result
}
if (-not $IsAdministrator) {
Write-Host "usbipd-win install requires an elevated PowerShell session. Re-run as Administrator." -ForegroundColor Red
$result.Failed++
return [pscustomobject]$result
}
$usbipdTempDir = Join-Path $env:TEMP ("usbipd-win-" + [guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $usbipdTempDir -Force | Out-Null
try {
$targetArch = Get-OsArchitecture
Write-Host "Detected OS architecture: $targetArch" -ForegroundColor Gray
Write-Host "Resolving latest usbipd-win MSI from GitHub releases..." -ForegroundColor Gray
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/dorssel/usbipd-win/releases/latest" -Headers @{ "User-Agent" = "PowerShell" }
$msiAssets = $release.assets | Where-Object { $_.name -match "\\.msi$" }
$msiAsset = $msiAssets | Where-Object { $_.name -match "(?i)(^|[-_.])$targetArch([-.]|\\.msi$)" } | Select-Object -First 1
if (-not $msiAsset -and $msiAssets.Count -eq 1) {
# Fallback when a release exposes exactly one MSI.
$msiAsset = $msiAssets[0]
Write-Host "No architecture tag found in MSI name; using sole MSI asset: $($msiAsset.name)" -ForegroundColor Yellow
}
if (-not $msiAsset) {
$available = ($msiAssets | ForEach-Object { $_.name }) -join ", "
Write-Host "Unable to locate a usbipd-win MSI for architecture '$targetArch'. Available MSI assets: $available" -ForegroundColor Red
$result.Failed++
return [pscustomobject]$result
}
$msiPath = Join-Path $usbipdTempDir $msiAsset.name
if ($PSCmdlet.ShouldProcess("$($msiAsset.browser_download_url)", "Download usbipd-win MSI")) {
Write-Host "Downloading: $($msiAsset.name)..." -ForegroundColor Gray
Invoke-WebRequest -Uri $msiAsset.browser_download_url -OutFile $msiPath -UseBasicParsing
}
if ($PSCmdlet.ShouldProcess($msiPath, "Install usbipd-win MSI")) {
$msiArgs = @("/i", "`"$msiPath`"", "/qn", "/norestart")
$proc = Start-Process -FilePath "msiexec.exe" -ArgumentList $msiArgs -Wait -PassThru -NoNewWindow
if ($proc.ExitCode -eq 0 -or $proc.ExitCode -eq 3010) {
Write-Host "usbipd-win installed successfully." -ForegroundColor Green
if ($proc.ExitCode -eq 3010) {
Write-Host "A reboot is required to complete installation changes." -ForegroundColor Yellow
}
$result.Installed++
try {
$usbipdVersion = (& usbipd --version 2>$null | Select-Object -First 1).Trim()
if ($usbipdVersion) {
Write-Host "usbipd-win version: $usbipdVersion" -ForegroundColor Green
}
else {
Write-Host "usbipd-win installed, but version output was empty." -ForegroundColor Yellow
}
}
catch {
Write-Host "usbipd-win installed, but failed to read version: $($_.Exception.Message)" -ForegroundColor Yellow
}
}
else {
Write-Host "usbipd-win MSI installation failed with exit code $($proc.ExitCode)." -ForegroundColor Red
$result.Failed++
}
}
}
catch {
Write-Host "usbipd-win installation failed: $($_.Exception.Message)" -ForegroundColor Red
$result.Failed++
}
finally {
Remove-Item -Path $usbipdTempDir -Recurse -Force -ErrorAction SilentlyContinue
}
return [pscustomobject]$result
}
# Ubuntu Mono font files sourced from the Google Fonts repository (Ubuntu Font Licence).
$fontFiles = @(
@{ Name = "UbuntuMono-Regular.ttf"; Url = "https://github.com/google/fonts/raw/main/ufl/ubuntumono/UbuntuMono-Regular.ttf" }
@{ Name = "UbuntuMono-Bold.ttf"; Url = "https://github.com/google/fonts/raw/main/ufl/ubuntumono/UbuntuMono-Bold.ttf" }
@{ Name = "UbuntuMono-Italic.ttf"; Url = "https://github.com/google/fonts/raw/main/ufl/ubuntumono/UbuntuMono-Italic.ttf" }
@{ Name = "UbuntuMono-BoldItalic.ttf"; Url = "https://github.com/google/fonts/raw/main/ufl/ubuntumono/UbuntuMono-BoldItalic.ttf" }
)
# Registry display names used by Windows font enumeration.
$fontRegistryNames = @{
"UbuntuMono-Regular.ttf" = "Ubuntu Mono Regular (TrueType)"
"UbuntuMono-Bold.ttf" = "Ubuntu Mono Bold (TrueType)"
"UbuntuMono-Italic.ttf" = "Ubuntu Mono Italic (TrueType)"
"UbuntuMono-BoldItalic.ttf" = "Ubuntu Mono Bold Italic (TrueType)"
}
$isAdmin = Test-IsAdministrator
if ($SystemWide -and -not $isAdmin) {
Write-Error "System-wide installation requires elevated privileges. Run as Administrator or omit -SystemWide for a per-user install."
exit 1
}
# Determine install destination and registry hive.
if ($SystemWide -or $isAdmin) {
$fontDestination = "$env:SystemRoot\Fonts"
$registryPath = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Fonts"
$installScope = "system-wide"
}
else {
$fontDestination = Join-Path $env:LOCALAPPDATA "Microsoft\Windows\Fonts"
$registryPath = "HKCU:\Software\Microsoft\Windows NT\CurrentVersion\Fonts"
$installScope = "per-user"
}
if (-not (Test-Path $fontDestination)) {
New-Item -ItemType Directory -Path $fontDestination -Force | Out-Null
}
Write-Host "Installing Ubuntu Mono fonts ($installScope) to: $fontDestination" -ForegroundColor Cyan
$tempDir = Join-Path $env:TEMP ("ubuntu-mono-fonts-" + [guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
$installed = 0
$skipped = 0
$failed = 0
foreach ($font in $fontFiles) {
$destPath = Join-Path $fontDestination $font.Name
if (Test-Path $destPath) {
Write-Host " Already installed, skipping: $($font.Name)" -ForegroundColor Gray
$skipped++
continue
}
$tempPath = Join-Path $tempDir $font.Name
Write-Host " Downloading: $($font.Name)..." -ForegroundColor Gray
try {
Invoke-WebRequest -Uri $font.Url -OutFile $tempPath -UseBasicParsing
}
catch {
Write-Host " Failed to download $($font.Name): $($_.Exception.Message)" -ForegroundColor Red
$failed++
continue
}
if ($PSCmdlet.ShouldProcess($destPath, "Install font")) {
try {
Copy-Item -Path $tempPath -Destination $destPath -Force
$regName = $fontRegistryNames[$font.Name]
if ($regName) {
# Per-user registry stores the full path; system registry stores only the filename.
$regValue = if ($installScope -eq "per-user") { $destPath } else { $font.Name }
Set-ItemProperty -Path $registryPath -Name $regName -Value $regValue -Type String -Force
}
Write-Host " Installed: $($font.Name)" -ForegroundColor Green
$installed++
}
catch {
Write-Host " Failed to install $($font.Name): $($_.Exception.Message)" -ForegroundColor Red
$failed++
}
}
}
Write-Host ""
Write-Host "Font installation summary: installed=$installed skipped=$skipped failed=$failed" -ForegroundColor Yellow
Write-Host ""
Write-Host "Installing usbipd-win..." -ForegroundColor Cyan
$usbipdResult = Install-UsbipdWin -IsAdministrator:$isAdmin
Write-Host "usbipd-win summary: installed=$($usbipdResult.Installed) skipped=$($usbipdResult.Skipped) failed=$($usbipdResult.Failed)" -ForegroundColor Yellow
$totalFailed = $failed + $usbipdResult.Failed
if ($totalFailed -gt 0) {
exit 1
}
if ($installed -gt 0) {
Write-Host "Ubuntu Mono fonts installed successfully. Applications may need to be restarted to pick up new fonts." -ForegroundColor Green
}
}
finally {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[ValidateSet("Quick", "Full")]
[string]$Mode = "Full"
)
# Ensure script stops on errors.
$ErrorActionPreference = "Stop"
# Optimize-VHD comes from the Windows feature:
# - Microsoft-Hyper-V-Management-PowerShell
# Runtime execution additionally requires Hyper-V platform/WMI provider availability.
# If platform pieces are missing or unavailable on this edition, the script falls back to diskpart compact.
function Test-IsAdministrator {
$currentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($currentIdentity)
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Get-WslVhdxFiles {
[OutputType([System.IO.FileInfo[]])]
param()
$registryBasePaths = @()
$lxssRoot = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Lxss"
if (Test-Path $lxssRoot) {
$registryBasePaths = Get-ChildItem $lxssRoot -ErrorAction SilentlyContinue |
ForEach-Object {
(Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue).BasePath
} |
Where-Object { $_ -and (Test-Path $_) }
}
$allUsersWslPaths = @()
if (Test-Path "C:\Users") {
$allUsersWslPaths = Get-ChildItem -Path "C:\Users" -Directory -ErrorAction SilentlyContinue |
ForEach-Object { Join-Path $_.FullName "AppData\Local\wsl" } |
Where-Object { Test-Path $_ }
}
$pathsToScan = @(
(Join-Path $env:LOCALAPPDATA "Packages")
(Join-Path $env:LOCALAPPDATA "wsl")
(Join-Path $env:LOCALAPPDATA "Docker\wsl")
(Join-Path $env:PROGRAMDATA "DockerDesktop\vm-data")
) + $registryBasePaths + $allUsersWslPaths |
Where-Object { $_ -and (Test-Path $_) } |
Sort-Object -Unique
if (-not $pathsToScan) {
return @()
}
# Common WSL and WSL-adjacent virtual disk naming patterns.
$filePatterns = @("*.vhdx", "*.vhd")
$nameFilters = @("ext4.vhdx", "disk.vhdx", "docker_data.vhdx", "docker-desktop-data.vhdx")
$results = foreach ($path in $pathsToScan) {
foreach ($pattern in $filePatterns) {
Get-ChildItem -Path $path -Recurse -File -Filter $pattern -ErrorAction SilentlyContinue
}
}
$results |
Where-Object {
$_.Length -gt 0 -and (
$nameFilters -contains $_.Name -or
$_.DirectoryName -match "LocalState|\\wsl\\\{" -or
$_.FullName -match "\\Docker\\wsl\\"
)
} |
Sort-Object -Property FullName -Unique
}
function Invoke-DiskPartCompact {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
$scriptPath = Join-Path $env:TEMP ("diskpart-compact-" + [guid]::NewGuid().ToString("N") + ".txt")
$diskpartScript = @(
"select vdisk file=`"$Path`""
"attach vdisk readonly"
"compact vdisk"
"detach vdisk"
)
try {
Set-Content -Path $scriptPath -Value $diskpartScript -Encoding ASCII
$output = & diskpart.exe /s $scriptPath 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) {
throw "diskpart failed with exit code $LASTEXITCODE. Output: $output"
}
if ($output -notmatch "DiskPart successfully compacted the virtual disk file") {
throw "diskpart did not report successful compact. Output: $output"
}
}
finally {
Remove-Item -Path $scriptPath -Force -ErrorAction SilentlyContinue
}
}
function Invoke-WslLinuxCleanup {
[OutputType([void])]
param()
# wsl --list --quiet may return UTF-16 wide chars on some Windows builds; strip null bytes.
$rawList = & wsl.exe --list --quiet 2>$null
$distros = $rawList -replace '\0', '' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
if (-not $distros) {
Write-Host "No WSL distributions found for Linux pre-cleanup." -ForegroundColor Yellow
return
}
# Ordered: free space first, then TRIM so the compactor sees zeroed blocks.
$cleanupSteps = @(
@{ Desc = "Remove APT package cache"; Cmd = "apt-get clean -y 2>/dev/null || true" }
@{ Desc = "Remove orphaned packages"; Cmd = "apt-get autoremove -y 2>/dev/null || true" }
@{ Desc = "Truncate systemd journal"; Cmd = "journalctl --vacuum-size=50M 2>/dev/null || true" }
@{ Desc = "Clear temporary files"; Cmd = "rm -rf /tmp/* /var/tmp/* 2>/dev/null || true" }
@{ Desc = "Prune Docker resources"; Cmd = "command -v docker >/dev/null 2>&1 && docker system prune -f || true" }
@{ Desc = "TRIM free blocks (fstrim)"; Cmd = "fstrim -av 2>/dev/null || true" }
)
foreach ($distro in $distros) {
Write-Host "Running Linux cleanup on: $distro" -ForegroundColor Cyan
foreach ($step in $cleanupSteps) {
Write-Host " [$distro] $($step.Desc)..." -ForegroundColor Gray
try {
& wsl.exe -d $distro -u root -- bash -c $step.Cmd 2>&1 | Out-Null
}
catch {
Write-Host " [$distro] '$($step.Desc)' failed (non-fatal): $($_.Exception.Message)" -ForegroundColor DarkYellow
}
}
Write-Host " [$distro] Linux cleanup complete." -ForegroundColor Green
}
}
try {
if (-not (Test-IsAdministrator)) {
Write-Error "This script requires elevated privileges. Start PowerShell with 'Run as Administrator' and run the script again."
exit 1
}
# Command availability check: this is true when Hyper-V PowerShell management module is installed.
$canUseOptimizeVhd = $null -ne (Get-Command -Name "Optimize-VHD" -ErrorAction SilentlyContinue)
Write-Host "Running Linux filesystem cleanup in WSL distributions..." -ForegroundColor Yellow
Invoke-WslLinuxCleanup
Write-Host "Stopping all WSL instances..." -ForegroundColor Yellow
& wsl.exe --shutdown
Write-Host "Searching for WSL virtual disks..." -ForegroundColor Yellow
$vhdxFiles = Get-WslVhdxFiles
if (-not $vhdxFiles) {
throw "No WSL virtual disks were found under expected locations."
}
$optimized = 0
$failed = 0
foreach ($file in $vhdxFiles) {
$target = $file.FullName
if ($PSCmdlet.ShouldProcess($target, "Optimize WSL disk")) {
try {
Write-Host "Optimizing disk: $target" -ForegroundColor Cyan
if ($canUseOptimizeVhd) {
try {
Optimize-VHD -Path $target -Mode $Mode
Write-Host "Optimization complete via Optimize-VHD: $target" -ForegroundColor Green
}
catch {
# Common failure case: Hyper-V WMI/provider classes are unavailable.
Write-Host "Optimize-VHD failed, falling back to diskpart compact for: $target" -ForegroundColor Yellow
Invoke-DiskPartCompact -Path $target
Write-Host "Optimization complete via diskpart: $target" -ForegroundColor Green
}
}
else {
Write-Host "Optimize-VHD not available, using diskpart compact for: $target" -ForegroundColor Yellow
Invoke-DiskPartCompact -Path $target
Write-Host "Optimization complete via diskpart: $target" -ForegroundColor Green
}
$optimized++
}
catch {
Write-Host "Failed to optimize $target : $($_.Exception.Message)" -ForegroundColor Red
$failed++
}
}
}
Write-Host "Optimization summary: optimized=$optimized failed=$failed total=$($vhdxFiles.Count)" -ForegroundColor Yellow
if ($failed -gt 0) {
exit 1
}
}
catch {
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}

Comments are disabled for this gist.