Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 79 additions & 28 deletions scripts/bootstrap-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

.DESCRIPTION
Prepares a Windows host for NemoClaw by enabling WSL 2, installing an
Ubuntu WSL distro when needed, installing Docker Desktop, verifying Docker
Ubuntu 24.04 WSL distro when needed, installing Docker Desktop, verifying Docker
from WSL, and then opening Ubuntu so the user can run the standard
curl|bash installer in a native Linux terminal:

Expand All @@ -17,7 +17,7 @@
onboarding to scripts/install.sh and nemoclaw onboard.

.PARAMETER DistroName
WSL distro to install/use. Defaults to Ubuntu.
WSL distro to install/use. Defaults to Ubuntu-24.04.

.PARAMETER InstallerUrl
NemoClaw installer URL to print in the final WSL handoff command.
Expand All @@ -41,7 +41,7 @@

[CmdletBinding()]
param(
[string]$DistroName = 'Ubuntu',
[string]$DistroName = 'Ubuntu-24.04',
[string]$InstallerUrl = 'https://www.nvidia.com/nemoclaw.sh',
[string]$InstallerArgs = '',
[bool]$InstallDockerDesktop = $true,
Expand All @@ -62,7 +62,6 @@ $script:DockerDesktopExe = 'C:\Program Files\Docker\Docker\Docker Desktop.exe'
$script:DockerCli = 'C:\Program Files\Docker\Docker\resources\bin\docker.exe'
$script:WingetDockerId = 'Docker.DockerDesktop'
$script:InstallerWindowTitle = "NVIDIA NemoClaw Installer ($PID)"
$script:InstallDistroAtHandoff = $false

function Write-Status {
param(
Expand Down Expand Up @@ -653,18 +652,84 @@ function Ensure-WslDistroVersion2 {
Write-Status "$Name is now WSL 2."
}

function Get-WslInstallCommandText {
param([Parameter(Mandatory)] [string]$Name)

return "wsl --install $Name"
}

function Write-WslUbuntuRequiredNotice {
param([Parameter(Mandatory)] [string]$Name)

Write-Host ''
if ($Name -eq 'Ubuntu-24.04') {
Write-Host 'NemoClaw on Windows ARM requires WSL2 Ubuntu 24.04.' -ForegroundColor Yellow
Write-Host 'Please run: wsl --install Ubuntu-24.04' -ForegroundColor Yellow
Write-Host 'Then re-run this installer.' -ForegroundColor Yellow
} else {
Write-Host "NemoClaw on Windows requires a WSL2 distro named $Name." -ForegroundColor Yellow
Write-Host "Please run: $(Get-WslInstallCommandText -Name $Name)" -ForegroundColor Yellow
Write-Host 'Then re-run this installer.' -ForegroundColor Yellow
}
Write-Host ''
}

function Wait-WslDistroRegistration {
param(
[Parameter(Mandatory)] [string]$Name,
[int]$TimeoutSeconds = 300
)

$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
$distros = Get-WslDistros
if ($distros -contains $Name) {
return $true
}
Start-Sleep -Seconds 5
}
return $false
}

function Install-WslDistro {
param([Parameter(Mandatory)] [string]$Name)

$wsl = Resolve-WslExe
$displayCommand = Get-WslInstallCommandText -Name $Name
Write-Status "$Name is not registered. Installing with: $displayCommand"
$installExitCode = Invoke-NativeCommand -FilePath $wsl -ArgumentList @('--install', $Name)
if ($installExitCode -ne 0) {
Write-Status -Level WARN "$displayCommand failed with exit code $installExitCode."
Write-WslUbuntuRequiredNotice -Name $Name
throw "Could not install WSL distro '$Name'."
}

if (-not (Wait-WslDistroRegistration -Name $Name)) {
Write-WslUbuntuRequiredNotice -Name $Name
throw "WSL distro '$Name' was not registered after running $displayCommand."
}
}

function Ensure-UbuntuWsl {
$wsl = Resolve-WslExe
$script:InstallDistroAtHandoff = $false

$installedDistro = $false
$distros = Get-WslDistros
if ($distros -notcontains $DistroName) {
Write-Status "$DistroName is not registered yet. It will be installed during the final Ubuntu handoff."
$script:InstallDistroAtHandoff = $true
return
Install-WslDistro -Name $DistroName
$installedDistro = $true
$distros = Get-WslDistros
if ($distros -notcontains $DistroName) {
Write-WslUbuntuRequiredNotice -Name $DistroName
throw "WSL distro '$DistroName' is still not registered after install."
}
}

Write-Status "WSL distro already registered: $DistroName"
if ($installedDistro) {
Write-Status "WSL distro registered: $DistroName"
} else {
Write-Status "WSL distro already registered: $DistroName"
}

Ensure-WslDistroVersion2 -Name $DistroName

Expand Down Expand Up @@ -781,11 +846,6 @@ function Get-NemoClawInstallerCommand {
function Open-UbuntuForInstaller {
$wsl = Resolve-WslExe
try {
if ($script:InstallDistroAtHandoff) {
Start-Process -FilePath $wsl -ArgumentList @('--install', '-d', $DistroName) | Out-Null
return
}

$ubuntuLauncher = Resolve-UbuntuLauncher
$windowsTerminal = Get-Command 'wt.exe' -ErrorAction SilentlyContinue
if ($windowsTerminal) {
Expand All @@ -803,9 +863,6 @@ function Open-UbuntuForInstaller {
Start-Process -FilePath $wsl -ArgumentList @('-d', $DistroName) | Out-Null
} catch {
Write-Status -Level WARN "Could not open $DistroName automatically: $($_.Exception.Message)"
if ($script:InstallDistroAtHandoff) {
throw
}
}
}

Expand All @@ -814,11 +871,7 @@ function Write-InstallerHandoff {
Write-Host ''
Write-Host 'Windows preparation is complete.' -ForegroundColor Green
Write-Host ''
if ($script:InstallDistroAtHandoff) {
Write-Host "Ubuntu will install and launch now. After first-run setup completes, run this command inside Ubuntu to install NemoClaw:" -ForegroundColor Cyan
} else {
Write-Host "An Ubuntu window is opening. Run this command inside Ubuntu to install NemoClaw:" -ForegroundColor Cyan
}
Write-Host "An Ubuntu window is opening. Run this command inside Ubuntu to install NemoClaw:" -ForegroundColor Cyan
Write-Host ''
Write-Host " $installerCommand" -ForegroundColor White
Write-Host ''
Expand All @@ -837,15 +890,13 @@ function Invoke-Main {
Ensure-UbuntuWsl
Install-DockerDesktop
Start-DockerDesktop
if ($script:InstallDistroAtHandoff) {
Write-Status "Skipping Docker-in-WSL verification until $DistroName is installed and first-run setup completes."
} else {
Ensure-DockerWslIntegration
}
Ensure-DockerWslIntegration
Write-DockerDesktopNotice
Unregister-ResumeRunOnce
Write-Status 'Windows preparation completed successfully.'
Write-InstallerHandoff
}

Invoke-Main
if ($env:NEMOCLAW_BOOTSTRAP_WINDOWS_SOURCE_ONLY -ne '1') {
Invoke-Main
}
132 changes: 132 additions & 0 deletions test/bootstrap-windows.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

const BOOTSTRAP_WINDOWS = path.join(
import.meta.dirname,
"..",
"scripts",
"bootstrap-windows.ps1",
);

function resolvePowerShell() {
for (const command of ["pwsh", "powershell"]) {
const result = spawnSync(
command,
["-NoLogo", "-NoProfile", "-Command", "$PSVersionTable.PSVersion"],
{ encoding: "utf8" },
);
if (result.status === 0) return command;
}
return null;
}

const POWERSHELL = resolvePowerShell();
const itPowerShell = POWERSHELL ? it : it.skip;

function runPowerShellHarness(script: string) {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-bootstrap-windows-"));
const harness = path.join(tmp, "harness.ps1");
try {
fs.writeFileSync(harness, script);
const result = spawnSync(
POWERSHELL ?? "pwsh",
["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", harness],
{
encoding: "utf8",
env: {
...process.env,
NEMOCLAW_BOOTSTRAP_WINDOWS_SOURCE_ONLY: "1",
SystemRoot: process.env.SystemRoot ?? "C:\\Windows",
},
},
);
return {
stdout: result.stdout,
stderr: result.stderr,
status: result.status ?? 1,
};
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
}

describe("Windows bootstrap WSL distro preflight", () => {
itPowerShell("installs Ubuntu 24.04 before continuing when WSL has no registered distro", () => {
const result = runPowerShellHarness(`
$ErrorActionPreference = 'Stop'
. ${JSON.stringify(BOOTSTRAP_WINDOWS)}

$script:getDistroCalls = 0
$script:nativeCalls = @()
$script:statusMessages = @()

function Resolve-WslExe { return 'wsl.exe' }
function Get-WslDistros {
$script:getDistroCalls += 1
if ($script:getDistroCalls -eq 1) { return @() }
return @('Ubuntu-24.04')
}
function Invoke-NativeCommand {
param([string]$FilePath, [string[]]$ArgumentList = @(), [switch]$SuppressOutput)
$script:nativeCalls += ,@($FilePath, ($ArgumentList -join ' '))
return 0
}
function Invoke-NativeCommandOutput {
param([string]$FilePath, [string[]]$ArgumentList = @(), [switch]$MergeError)
return [pscustomobject]@{ ExitCode = 0; Output = 'WSL_OK' }
}
function Ensure-WslDistroVersion2 { param([string]$Name) }
function Write-Status { param([string]$Message, [string]$Level = 'INFO') $script:statusMessages += $Message }

Ensure-UbuntuWsl

[pscustomobject]@{
nativeCalls = $script:nativeCalls
statusMessages = $script:statusMessages
} | ConvertTo-Json -Compress
`);

expect(result.status).toBe(0);
expect(result.stderr).toBe("");
const parsed = JSON.parse(result.stdout.trim());
expect(parsed.nativeCalls).toContainEqual(["wsl.exe", "--install Ubuntu-24.04"]);
expect(parsed.nativeCalls).toContainEqual(["wsl.exe", "--set-default Ubuntu-24.04"]);
expect(parsed.statusMessages).toContain("WSL distro registered: Ubuntu-24.04");
});

itPowerShell("prints the issue 3974 guidance when Ubuntu 24.04 cannot be installed", () => {
const result = runPowerShellHarness(`
$ErrorActionPreference = 'Stop'
. ${JSON.stringify(BOOTSTRAP_WINDOWS)}

function Resolve-WslExe { return 'wsl.exe' }
function Get-WslDistros { return @() }
function Invoke-NativeCommand {
param([string]$FilePath, [string[]]$ArgumentList = @(), [switch]$SuppressOutput)
return 42
}
function Write-Status { param([string]$Message, [string]$Level = 'INFO') Write-Host $Message }

try {
Ensure-UbuntuWsl
Write-Host 'UNEXPECTED_SUCCESS'
exit 3
} catch {
Write-Host "CAUGHT: $($_.Exception.Message)"
}
`);

expect(result.status).toBe(0);
expect(result.stderr).toBe("");
expect(result.stdout).toContain("NemoClaw on Windows ARM requires WSL2 Ubuntu 24.04.");
expect(result.stdout).toContain("Please run: wsl --install Ubuntu-24.04");
expect(result.stdout).toContain("Then re-run this installer.");
expect(result.stdout).toContain("CAUGHT: Could not install WSL distro 'Ubuntu-24.04'.");
});
});
Loading