Skip to content

Instantly share code, notes, and snippets.

@sycomix
Created July 15, 2025 07:55
Show Gist options
  • Select an option

  • Save sycomix/f4eba3259bc751f1a1d9ee1f87228a69 to your computer and use it in GitHub Desktop.

Select an option

Save sycomix/f4eba3259bc751f1a1d9ee1f87228a69 to your computer and use it in GitHub Desktop.
WSL2 Customization
<#
.SYNOPSIS
A PowerShell script to create a custom WSL2 distribution tar file.
.DESCRIPTION
This script provides an interactive menu to select a base Linux distribution
and a variety of packages to build a personalized WSL2 environment. It uses
Docker to assemble the distro and then exports the filesystem as a .tar file.
.NOTES
Author: Gemini
Version: 2.1
Requires: Docker Desktop installed and running.
Changes:
- Fixed `Malformed entry` apt error by using a more robust `grep`/`cut` method to get distro info for Docker repo setup.
- Fixed `exec /install.sh: no such file or directory` error by converting script line endings from CRLF (Windows) to LF (Linux).
- Fixed NullArrayIndex error by explicitly creating a sorted array of distribution options.
- Implemented robust input validation for distribution selection.
- Implemented strict error checking for all Docker commands.
.EXAMPLE
.\Create-CustomWSLDistro.ps1
This will start the interactive menu to build your custom WSL distribution.
#>
#region Global Variables & Initial Checks
# Function to check if Docker is running
function Test-DockerConnection {
try {
docker ps -q > $null
return $true
} catch {
return $false
}
}
if (-not (Test-DockerConnection)) {
Write-Host "❌ Docker is not running or not found." -ForegroundColor Red
Write-Host "Please start Docker Desktop and ensure it's accessible from your terminal." -ForegroundColor Yellow
exit
}
# Define available distributions and their package managers/commands
$Distributions = @{
"Debian" = @{
Image = "debian:stable-slim"
PackageManager = "apt-get"
UpdateCmd = "apt-get update"
InstallCmd = "apt-get install -y --no-install-recommends"
CleanCmd = "apt-get clean && rm -rf /var/lib/apt/lists/*"
}
"Ubuntu" = @{
Image = "ubuntu:latest"
PackageManager = "apt-get"
UpdateCmd = "apt-get update"
InstallCmd = "apt-get install -y --no-install-recommends"
CleanCmd = "apt-get clean && rm -rf /var/lib/apt/lists/*"
}
"Fedora" = @{
Image = "fedora:latest"
PackageManager = "dnf"
UpdateCmd = "dnf makecache"
InstallCmd = "dnf install -y"
CleanCmd = "dnf clean all"
}
"Arch Linux" = @{
Image = "archlinux:latest"
PackageManager = "pacman"
UpdateCmd = "pacman -Sy"
InstallCmd = "pacman -S --noconfirm"
CleanCmd = "pacman -Scc --noconfirm"
}
}
# Define available packages categorized for selection
$PackageCategories = @{
"Essential Tools" = @(
"build-essential", "git", "curl", "wget", "unzip", "ca-certificates", "gnupg", "sudo" # 'build-essential' is for Debian/Ubuntu, will be adapted
)
"Shells & Terminals" = @(
"zsh", "fish", "tmux"
)
"Development Runtimes" = @(
"python3", "python3-pip", "nodejs", "ruby-full", "golang"
)
"System & Monitoring" = @(
"htop", "neofetch", "ncdu", "man-db", "command-not-found" # 'command-not-found' is distro-specific
)
"Common Editors" = @(
"neovim", "nano", "micro"
)
"Container Tools" = @(
"docker-ce-cli" # Special handling for Docker CLI
)
"AI & Specialized Runtimes" = @(
"rust", "nvidia-container-toolkit", "rocm", "openvino" # Complex, multi-step installations
)
}
#endregion
#region Menu Functions
# Function to display the main menu and get user selection
function Show-Menu {
param (
[string]$Title,
[array]$Options
)
Clear-Host
Write-Host "================ $Title ================" -ForegroundColor Cyan
for ($i = 0; $i -lt $Options.Count; $i++) {
Write-Host (" " + ($i + 1) + ". " + $Options[$i])
}
Write-Host "===============================================" -ForegroundColor Cyan
}
# Function to handle multi-select package menu
function Show-PackageMenu {
$SelectedPackages = [System.Collections.Generic.List[string]]::new()
$AllPackages = $PackageCategories.Keys | ForEach-Object { $Category = $_; $PackageCategories[$Category] | ForEach-Object { "$_ ($Category)" } }
while ($true) {
Clear-Host
Write-Host "========= Select Packages (Press 'd' when done) =========" -ForegroundColor Cyan
for ($i = 0; $i -lt $AllPackages.Count; $i++) {
$PackageDisplayName = $AllPackages[$i]
$PackageName = $PackageDisplayName.Split(' ')[0]
$IsSelected = $SelectedPackages.Contains($PackageName)
$DisplayColor = if ($IsSelected) { "Green" } else { "White" }
Write-Host (" [{0}] {1}. {2}" -f ($i + 1), $(if ($IsSelected) { "x" } else { " " }), $PackageDisplayName) -ForegroundColor $DisplayColor
}
Write-Host "==========================================================" -ForegroundColor Cyan
Write-Host "Enter number to toggle, 'd' to finish, 'a' to select all, 'n' for none."
$input = Read-Host "Your choice"
if ($input -eq 'd') { break }
if ($input -eq 'a') {
$SelectedPackages.Clear()
$PackageCategories.Values.ForEach({$_.ForEach({$SelectedPackages.Add($_)})})
continue
}
if ($input -eq 'n') {
$SelectedPackages.Clear()
continue
}
if ($input -match "^\d+$" -and [int]$input -ge 1 -and [int]$input -le $AllPackages.Count) {
$Index = [int]$input - 1
$PackageName = $AllPackages[$Index].Split(' ')[0]
if ($SelectedPackages.Contains($PackageName)) {
$SelectedPackages.Remove($PackageName)
} else {
$SelectedPackages.Add($PackageName)
}
} else {
Write-Host "Invalid input. Please try again." -ForegroundColor Yellow
Start-Sleep -Seconds 1
}
}
return $SelectedPackages
}
#endregion
#region Core Logic
# Function to adapt package names for the selected distro
function Adapt-PackageNames {
param(
[string]$DistroName,
[array]$Packages
)
$AdaptedPackages = @()
# These packages have complex installs handled separately in the script.
$SpecialPackages = @("rust", "nvidia-container-toolkit", "rocm", "openvino", "docker-ce-cli")
foreach ($pkg in $Packages) {
if ($SpecialPackages -contains $pkg) {
# Add the original name to be checked later in the install script
$AdaptedPackages += $pkg
continue
}
switch ($DistroName) {
"Fedora" {
switch ($pkg) {
"build-essential" { $AdaptedPackages += "@development-tools" }
"python3-pip" { $AdaptedPackages += "python3-pip" }
"ruby-full" { $AdaptedPackages += "ruby" }
"command-not-found" { $AdaptedPackages += "PackageKit-command-not-found" }
default { $AdaptedPackages += $pkg }
}
}
"Arch Linux" {
switch ($pkg) {
"build-essential" { $AdaptedPackages += "base-devel" }
"python3-pip" { $AdaptedPackages += "python-pip" }
"ruby-full" { $AdaptedPackages += "ruby" }
"sudo" { continue } # Already part of base image, do nothing.
"command-not-found" { $AdaptedPackages += "pkgfile" }
default { $AdaptedPackages += $pkg }
}
}
default { # Debian/Ubuntu
switch ($pkg) {
"golang" { $AdaptedPackages += "golang-go" }
default { $AdaptedPackages += $pkg }
}
}
}
}
return $AdaptedPackages
}
# Main script execution
function Start-DistroCreation {
# 1. Select Distribution
$DistroOptions = @($Distributions.Keys) | Sort-Object
Show-Menu -Title "Select a Base Distribution" -Options $DistroOptions
[int]$DistroChoice = 0
$RawInput = Read-Host "Enter the number for your choice"
if (-not ([int]::TryParse($RawInput, [ref]$DistroChoice)) -or $DistroChoice -lt 1 -or $DistroChoice -gt $DistroOptions.Count) {
Write-Host "❌ Invalid selection. Please enter a number between 1 and $($DistroOptions.Count). Exiting." -ForegroundColor Red
exit
}
$SelectedDistroName = $DistroOptions[$DistroChoice - 1]
$DistroInfo = $Distributions[$SelectedDistroName]
Write-Host "You selected: $SelectedDistroName" -ForegroundColor Green
Start-Sleep -Seconds 1
# 2. Select Packages
$SelectedPackages = Show-PackageMenu
if ($SelectedPackages.Count -eq 0) {
Write-Host "No packages selected. The distro will be minimal." -ForegroundColor Yellow
Start-Sleep -Seconds 2
} else {
Write-Host "You selected the following packages:" -ForegroundColor Green
$SelectedPackages | ForEach-Object { Write-Host "- $_" }
Start-Sleep -Seconds 2
}
# 3. Get Distro Name and Custom Identifiers
$DistroName = Read-Host "Enter a name for your new WSL distro (e.g., 'my-custom-debian')"
if (-not $DistroName) {
$DistroName = "custom-wsl-distro-$(Get-Date -Format 'yyyyMMddHHmm')"
Write-Host "No name entered. Using default: $DistroName" -ForegroundColor Yellow
}
Write-Host "`nYou can optionally set a custom OS name and version that will appear inside the distro." -ForegroundColor Cyan
$CustomPrettyName = Read-Host "Enter a custom OS Pretty Name (e.g., MyDevOS) [Optional]"
$CustomVersion = Read-Host "Enter a custom OS Version (e.g., 2025.1) [Optional]"
# 4. Build the Docker image
Write-Host "`nBuilding Docker container... This may take a while." -ForegroundColor Cyan
$ContainerName = "wsl-builder-$(Get-Random)"
$AllAdaptedPackages = Adapt-PackageNames -DistroName $SelectedDistroName -Packages $SelectedPackages
# Separate simple packages from complex ones
$SpecialPackageNames = @("rust", "nvidia-container-toolkit", "rocm", "openvino", "docker-ce-cli")
$SimplePackages = $AllAdaptedPackages | Where-Object { $SpecialPackageNames -notcontains $_ }
$SpecialPackages = $AllAdaptedPackages | Where-Object { $SpecialPackageNames -contains $_ }
# Validate that we have an image to run
if ([string]::IsNullOrEmpty($DistroInfo.Image)) {
Write-Host "❌ Critical error: Could not determine the Docker image for the selected distribution '$SelectedDistroName'." -ForegroundColor Red
exit
}
$InstallScript = @"
#!/bin/sh
set -e
export DEBIAN_FRONTEND=noninteractive
echo "Updating package lists..."
$($DistroInfo.UpdateCmd)
# Install simple packages first
if [ -n "$($SimplePackages -join ' ')" ]; then
echo "Installing base packages: $($SimplePackages -join ' ')"
$($DistroInfo.InstallCmd) $($SimplePackages -join ' ')
fi
# Add a default user 'wsluser' early so we can run commands as them
echo "Adding user 'wsluser'..."
useradd -m -s /bin/bash wsluser
echo "wsluser:wsluser" | chpasswd
echo "wsluser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# --- Handle Complex Installations ---
# Note: These are primarily configured for Debian/Ubuntu.
# Install Rust
if echo "$($SpecialPackages -join ' ')" | grep -q "rust"; then
echo "Installing Rust via rustup..."
su - wsluser -c "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"
fi
# Install Docker CLI
if echo "$($SpecialPackages -join ' ')" | grep -q "docker-ce-cli"; then
if [ "$($DistroInfo.PackageManager)" = "apt-get" ]; then
echo "Setting up Docker repository for docker-ce-cli..."
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
# FIXED: Using robust grep/cut to get distro info instead of sourcing /etc/os-release
echo "deb [arch=`$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/`$(grep '^ID=' /etc/os-release | cut -d'=' -f2) `$(grep 'VERSION_CODENAME=' /etc/os-release | cut -d'=' -f2) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
$($DistroInfo.UpdateCmd)
$($DistroInfo.InstallCmd) docker-ce-cli
fi
fi
# Install NVIDIA Container Toolkit
if echo "$($SpecialPackages -join ' ')" | grep -q "nvidia-container-toolkit"; then
if [ "$($DistroInfo.PackageManager)" = "apt-get" ]; then
echo "Setting up NVIDIA Container Toolkit repository..."
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
$($DistroInfo.UpdateCmd)
$($DistroInfo.InstallCmd) nvidia-container-toolkit
fi
fi
# Install ROCm (user-space tools)
if echo "$($SpecialPackages -join ' ')" | grep -q "rocm"; then
if [ "$($DistroInfo.PackageManager)" = "apt-get" ]; then
echo "Setting up AMD ROCm repository..."
mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.radeon.com/rocm/rocm.gpg.key | gpg --dearmor -o /etc/apt/keyrings/rocm.gpg
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/rocm/apt/6.0 ubuntu main" | tee /etc/apt/sources.list.d/rocm.list
$($DistroInfo.UpdateCmd)
$($DistroInfo.InstallCmd) rocm-dev
fi
fi
# Install OpenVINO
if echo "$($SpecialPackages -join ' ')" | grep -q "openvino"; then
if [ "$($DistroInfo.PackageManager)" = "apt-get" ]; then
echo "Setting up Intel OpenVINO repository..."
curl -fsSL https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB | gpg --dearmor -o /usr/share/keyrings/intel-openvino-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/intel-openvino-keyring.gpg] https://apt.repos.intel.com/openvino/2023 ubuntu22 main" | tee /etc/apt/sources.list.d/intel-openvino-2023.list
$($DistroInfo.UpdateCmd)
$($DistroInfo.InstallCmd) openvino-dev
fi
fi
# --- Customize OS Release Info ---
CUSTOM_PRETTY_NAME="$($CustomPrettyName)"
CUSTOM_VERSION="$($CustomVersion)"
if [ -n "$CUSTOM_PRETTY_NAME" ]; then
echo "Customizing OS release information..."
CUSTOM_ID=`$(echo "$CUSTOM_PRETTY_NAME" | tr '[:upper:]' '[:lower:]' | tr -cd '[:alnum:]._-')
VERSION_ID_VAL=${CUSTOM_VERSION:-"1.0"}
CODENAME_VAL=`$(echo "$VERSION_ID_VAL" | tr '[:upper:]' '[:lower:]' | tr -cd '[:alnum:]')
# Overwrite /etc/os-release with new and some old info
if [ -f /etc/os-release ]; then
. /etc/os-release
fi
echo "PRETTY_NAME=\"$CUSTOM_PRETTY_NAME\"" > /etc/os-release
echo "NAME=\"$CUSTOM_PRETTY_NAME\"" >> /etc/os-release
echo "ID=$CUSTOM_ID" >> /etc/os-release
echo "VERSION_ID=\"$VERSION_ID_VAL\"" >> /etc/os-release
echo "VERSION=\"$VERSION_ID_VAL ($CUSTOM_PRETTY_NAME)\"" >> /etc/os-release
if [ -n "$ID_LIKE" ]; then echo "ID_LIKE=$ID_LIKE" >> /etc/os-release; fi
if [ -n "$HOME_URL" ]; then echo "HOME_URL=\"$HOME_URL\"" >> /etc/os-release; fi
if [ -n "$SUPPORT_URL" ]; then echo "SUPPORT_URL=\"$SUPPORT_URL\"" >> /etc/os-release; fi
if [ -n "$BUG_REPORT_URL" ]; then echo "BUG_REPORT_URL=\"$BUG_REPORT_URL\"" >> /etc/os-release; fi
# Overwrite /etc/lsb-release
echo "DISTRIB_ID=$CUSTOM_ID" > /etc/lsb-release
echo "DISTRIB_RELEASE=$VERSION_ID_VAL" >> /etc/lsb-release
echo "DISTRIB_CODENAME=$CODENAME_VAL" >> /etc/lsb-release
echo "DISTRIB_DESCRIPTION=\"$CUSTOM_PRETTY_NAME\"" >> /etc/lsb-release
fi
echo "Cleaning up..."
$($DistroInfo.CleanCmd)
rm -f /install.sh
"@
# Create a temporary script file
$TempScriptPath = Join-Path $env:TEMP "install.sh"
$LfInstallScript = $InstallScript.Replace("`r`n", "`n")
$LfInstallScript | Set-Content -Path $TempScriptPath -Encoding Ascii -NoNewline
try {
# Start the container
docker run -d --name $ContainerName $($DistroInfo.Image) tail -f /dev/null
if ($LASTEXITCODE -ne 0) { throw "Failed to create Docker container. Is Docker running and is the image '$($DistroInfo.Image)' valid?" }
# Copy the install script to the container
docker cp $TempScriptPath "$($ContainerName):/install.sh"
if ($LASTEXITCODE -ne 0) { throw "Failed to copy install script to container." }
# Make the script executable and run it
docker exec $ContainerName chmod +x /install.sh
if ($LASTEXITCODE -ne 0) { throw "Failed to make install script executable." }
docker exec --interactive --tty $ContainerName /install.sh
if ($LASTEXITCODE -ne 0) { throw "Failed during package installation inside the container." }
Write-Host "✅ Container build complete." -ForegroundColor Green
} catch {
Write-Host "❌ An error occurred during the Docker build process." -ForegroundColor Red
Write-Host "This could be a Docker connection issue. Please ensure Docker Desktop is running." -ForegroundColor Yellow
Write-Host "Error details: $($_.Exception.Message)"
docker rm -f $ContainerName
Remove-Item $TempScriptPath -ErrorAction SilentlyContinue
exit
} finally {
Remove-Item $TempScriptPath -ErrorAction SilentlyContinue
}
# 5. Export the tar file
$ExportPath = "$pwd\$($DistroName).tar"
Write-Host "Exporting filesystem to '$ExportPath'..." -ForegroundColor Cyan
try {
docker export $ContainerName -o $ExportPath
if ($LASTEXITCODE -ne 0) { throw "Failed to export container filesystem." }
Write-Host "✅ Successfully exported '$($DistroName).tar'" -ForegroundColor Green
} catch {
Write-Host "❌ Failed to export the tar file." -ForegroundColor Red
Write-Host $_.Exception.Message
exit
} finally {
# Clean up the container
Write-Host "Cleaning up build container..."
docker rm -f $ContainerName
}
# 6. Final Instructions
$InstallLocation = "$env:LOCALAPPDATA\$DistroName"
Write-Host "`n🎉 Your custom WSL distro is ready! 🎉" -ForegroundColor Magenta
Write-Host "To install it, run the following commands in PowerShell:"
Write-Host "--------------------------------------------------------"
Write-Host "wsl --import $DistroName '$InstallLocation' '$ExportPath' --version 2" -ForegroundColor Yellow
Write-Host "wsl -d $DistroName" -ForegroundColor Yellow
Write-Host "--------------------------------------------------------"
Write-Host "NOTE: The default user is 'wsluser' with password 'wsluser'."
Write-Host "This user has passwordless sudo access."
if ($SpecialPackages -match "nvidia|rocm") {
Write-Host "IMPORTANT: For NVIDIA or ROCm tools to work, you MUST have the corresponding" -ForegroundColor Yellow
Write-Host "drivers installed on your Windows host machine." -ForegroundColor Yellow
}
}
# Run the main function
Start-DistroCreation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment