feat(tools): add Windows native setup helpers

Add PowerShell setup scripts for native Windows development and ROS 2 Pixi-based releases. The Windows setup script provisions the MSVC or MinGW toolchain paths used by native SITL, while the ROS 2 helper downloads a binary release, prepares its Pixi environment, and emits reusable activation wrappers.

Also add a PowerShell multi-instance SITL runner and make the existing shell runner work from Git Bash by detecting px4.exe and using taskkill when pkill is unavailable. Event generation now accepts @response files so long source lists do not exceed the Windows command-line limit.

Signed-off-by: Nuno Marques <n.marques21@hotmail.com>
This commit is contained in:
Nuno Marques
2026-05-05 21:06:50 -07:00
parent 1ba3591fb6
commit e8b0754822
5 changed files with 773 additions and 3 deletions
+6 -1
View File
@@ -49,7 +49,12 @@ import codecs
def main():
# Parse command line arguments
parser = argparse.ArgumentParser(description="Process events definitions.")
# Allow @file argument expansion so the (very long) source-file list can
# be passed via a response file. Required on Windows, where the
# 8191-char cmd.exe line limit otherwise truncates the build's full
# `--src-path file1 file2 …` invocation.
parser = argparse.ArgumentParser(description="Process events definitions.",
fromfile_prefix_chars='@')
parser.add_argument("-s", "--src-path",
default=["../src"],
metavar="PATH",
+225
View File
@@ -0,0 +1,225 @@
# ros2_pixi_setup.ps1 — provision a Pixi-based ROS 2 environment on Windows
# native and produce a reusable activation wrapper.
#
# Why Pixi: starting with Jazzy, the OSRF Windows ROS 2 binary release ships
# with a `pixi.toml` and `preinstall_setup_windows.py`. The runtime expects a
# conda-forge-provisioned environment (created by `pixi install`), not the
# legacy VS2019 + chocolatey toolchain used for Humble/Iron.
#
# What this script does (idempotent, no admin required):
# 1. Resolves the ROS 2 binary release zip URL for the requested -Distro
# (jazzy = current LTS default, rolling, or latest = newest non-prerelease)
# via the GitHub Releases API, with a hardcoded fallback when rate-limited.
# 2. Downloads (and caches under $env:TEMP) the zip and extracts it to
# C:\opt\ros\<Distro>\ if not already populated. Pass -Force to re-download.
# 3. Installs pixi to %USERPROFILE%\.pixi\bin if missing.
# 4. Runs `pixi install` against the extracted distro.
# 5. Runs preinstall_setup_windows.py to patch hardcoded shebangs in the
# release .py files / colcon setup files to point at the pixi env's
# python.exe.
# 6. Writes a per-distro activation wrapper at
# $env:TEMP\activate_ros2_<distro>.ps1 (and, for the default distro,
# a generic $env:TEMP\activate_ros2.ps1 alias) that future sessions
# dot-source to get a working `ros2`, `colcon`, `python`, etc. on PATH.
# Pass -WithVcvars when colcon/CMake builds need MSVC (vcvars64.bat).
#
# Usage:
# .\Tools\setup\ros2_pixi_setup.ps1 # default: Jazzy LTS
# .\Tools\setup\ros2_pixi_setup.ps1 -Distro rolling # Rolling
# .\Tools\setup\ros2_pixi_setup.ps1 -Distro latest # newest non-prerelease
# .\Tools\setup\ros2_pixi_setup.ps1 -RosRoot D:\ros2_jazzy
# .\Tools\setup\ros2_pixi_setup.ps1 -Force # re-download release zip
#
# After setup, a typical e2e session looks like:
# . $env:TEMP\activate_ros2.ps1 # ros2 only (default distro)
# . $env:TEMP\activate_ros2_rolling.ps1 # ros2 from Rolling
# . $env:TEMP\activate_ros2.ps1 -WithVcvars # +MSVC for colcon
# ros2 topic list
[CmdletBinding()]
param(
[ValidateSet('jazzy', 'rolling', 'latest')]
[string]$Distro = 'jazzy',
[string]$RosRoot,
[string]$OutFile,
[string]$VcvarsBat = 'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat',
[switch]$Force
)
$ErrorActionPreference = 'Stop'
function L($msg) {
Write-Host ("[ros2_pixi_setup] " + $msg)
}
# Hardcoded fallback release tags per distro, used when the GitHub Releases API
# is unreachable / rate-limited (unauthenticated = 60 req/hr). Bump as new
# releases land. Tag format: release-<distro>-YYYYMMDD.
$FallbackTags = @{
'jazzy' = 'release-jazzy-20260128'
'rolling' = 'release-rolling-20260128'
}
function Resolve-Ros2ReleaseUrl {
param([string]$Distro)
$api = 'https://api.github.com/repos/ros2/ros2/releases?per_page=30'
$tag = $null
try {
$headers = @{ 'User-Agent' = 'px4-ros2-pixi-setup' }
$releases = Invoke-RestMethod -Uri $api -Headers $headers -TimeoutSec 15
if ($Distro -eq 'latest') {
# First non-prerelease, regardless of distro family.
$rel = $releases | Where-Object { -not $_.prerelease } | Select-Object -First 1
} else {
$rel = $releases | Where-Object { -not $_.prerelease -and $_.tag_name -match $Distro } | Select-Object -First 1
}
if ($null -ne $rel) { $tag = $rel.tag_name }
} catch {
L "GitHub Releases API lookup failed ($($_.Exception.Message)); falling back to hardcoded tag."
}
if ([string]::IsNullOrWhiteSpace($tag)) {
if ($Distro -eq 'latest') {
$tag = $FallbackTags['jazzy'] # safest default if we can't query
L "Falling back to hardcoded tag '$tag' for -Distro latest (Jazzy)."
} elseif ($FallbackTags.ContainsKey($Distro)) {
$tag = $FallbackTags[$Distro]
L "Falling back to hardcoded tag '$tag' for -Distro $Distro."
} else {
throw "Unable to resolve a release tag for distro '$Distro'."
}
}
# Tag form: release-<distro>-YYYYMMDD -> zip name ros2-<distro>-YYYYMMDD-windows-release-amd64.zip
if ($tag -notmatch '^release-(?<d>[a-z]+)-(?<date>\d{8})$') {
throw "Unexpected ros2 release tag format: '$tag'"
}
$resolvedDistro = $matches['d']
$date = $matches['date']
$zipName = "ros2-$resolvedDistro-$date-windows-release-amd64.zip"
$url = "https://github.com/ros2/ros2/releases/download/$tag/$zipName"
return [pscustomobject]@{
Distro = $resolvedDistro
Tag = $tag
Date = $date
ZipName = $zipName
Url = $url
}
}
# 0. Resolve distro / release / install path
$release = Resolve-Ros2ReleaseUrl -Distro $Distro
$effectiveDistro = $release.Distro
L ("Resolved: distro=" + $effectiveDistro + " tag=" + $release.Tag + " url=" + $release.Url)
if ([string]::IsNullOrWhiteSpace($RosRoot)) {
$RosRoot = "C:\opt\ros\$effectiveDistro"
}
if ([string]::IsNullOrWhiteSpace($OutFile)) {
$OutFile = "$env:TEMP\activate_ros2_${effectiveDistro}.ps1"
}
L "Install root: $RosRoot"
L "Activation wrapper: $OutFile"
# 1. Download + extract release zip if needed
$cachedZip = Join-Path $env:TEMP $release.ZipName
if (-not (Test-Path "$RosRoot\pixi.toml")) {
if ($Force -and (Test-Path $cachedZip)) {
L "Removing cached zip due to -Force: $cachedZip"
Remove-Item $cachedZip -Force
}
if (-not (Test-Path $cachedZip)) {
L "Downloading $($release.Url) -> $cachedZip ..."
Invoke-WebRequest -Uri $release.Url -OutFile $cachedZip -UseBasicParsing
} else {
L "Reusing cached release zip: $cachedZip"
}
if (-not (Test-Path $RosRoot)) {
New-Item -ItemType Directory -Path $RosRoot -Force | Out-Null
}
L "Extracting to $RosRoot ..."
Expand-Archive -Path $cachedZip -DestinationPath $RosRoot -Force
}
# 2. Pixi
$pixiBin = "$env:USERPROFILE\.pixi\bin"
if (-not (Get-Command pixi -ErrorAction SilentlyContinue) -and -not (Test-Path "$pixiBin\pixi.exe")) {
L "Installing pixi..."
try { Invoke-Expression (Invoke-WebRequest -UseBasicParsing https://pixi.sh/install.ps1).Content } catch { L "pixi install warning: $_" }
}
$env:Path = "$pixiBin;$env:Path"
if (-not (Get-Command pixi -ErrorAction SilentlyContinue)) {
throw "pixi not found on PATH after install attempt (looked in $pixiBin)"
}
L ("pixi version: " + ((& pixi --version) -join ' '))
# 3. Validate distro and run pixi install
if (-not (Test-Path "$RosRoot\pixi.toml")) {
throw "pixi.toml not found at $RosRoot. Extract the ROS 2 binary release zip there first."
}
L "Running 'pixi install' in $RosRoot ..."
Push-Location $RosRoot
try {
& pixi install
if ($LASTEXITCODE -ne 0) { throw "pixi install failed (rc=$LASTEXITCODE)" }
} finally { Pop-Location }
# 4. Patch shebangs (idempotent)
$preinstall = Join-Path $RosRoot 'preinstall_setup_windows.py'
if (Test-Path $preinstall) {
L "Running preinstall_setup_windows.py to patch shebangs..."
Push-Location $RosRoot
try {
& pixi run --manifest-path "$RosRoot\pixi.toml" python preinstall_setup_windows.py | Select-Object -Last 5
} finally { Pop-Location }
} else {
L "preinstall_setup_windows.py not found in $RosRoot, skipping shebang patch."
}
# 5. Write reusable activation wrapper
L "Writing activation wrapper to $OutFile ..."
$wrapper = @"
# Auto-generated by Tools/setup/ros2_pixi_setup.ps1
# Distro: $effectiveDistro (release tag: $($release.Tag))
# Dot-source to activate ROS 2 (Pixi/conda-forge) in the current PowerShell session.
# Usage:
# . `$env:TEMP\activate_ros2_${effectiveDistro}.ps1 # ros2 / colcon / python
# . `$env:TEMP\activate_ros2_${effectiveDistro}.ps1 -WithVcvars # also load MSVC vcvars64
param([switch]`$WithVcvars)
`$env:Path = "`$env:USERPROFILE\.pixi\bin;`$env:Path"
`$hook = & pixi shell-hook --manifest-path '$RosRoot\pixi.toml' --shell powershell 2>`$null
if (`$LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace(`$hook)) { throw 'pixi shell-hook failed' }
Invoke-Expression (`$hook -join "`n")
. '$RosRoot\local_setup.ps1'
if (`$WithVcvars) {
`$vc = '$VcvarsBat'
if (-not (Test-Path `$vc)) { throw "vcvars64.bat not found at `$vc" }
`$tmp = [System.IO.Path]::GetTempFileName()
`$batLine = '"' + `$vc + '" && set'
& cmd.exe /c `$batLine > `$tmp 2>`$null
Get-Content `$tmp | ForEach-Object {
if (`$_ -match '^([^=]+)=(.*)`$') {
Set-Item -Path "Env:`$(`$matches[1])" -Value `$matches[2]
}
}
Remove-Item `$tmp -Force
}
"@
$wrapper | Out-File -FilePath $OutFile -Encoding utf8 -Force
# Also publish a generic 'activate_ros2.ps1' alias pointing at the same content,
# so the legacy command (used in docs / muscle memory) keeps working for the
# most recently installed distro.
$genericOut = "$env:TEMP\activate_ros2.ps1"
if ($OutFile -ne $genericOut) {
$wrapper | Out-File -FilePath $genericOut -Encoding utf8 -Force
L "Also wrote alias wrapper to $genericOut (points at $effectiveDistro)."
}
L "Done. Sanity check:"
& powershell -NoProfile -ExecutionPolicy Bypass -Command ". '$OutFile'; ros2 --help 2>&1 | Select-Object -First 3"
L "OK. Source '$OutFile' in future sessions."
+423
View File
@@ -0,0 +1,423 @@
<#
.SYNOPSIS
Set up a native Windows development environment for PX4 SITL.
.DESCRIPTION
Installs the toolchain required to build px4_sitl_default natively on
Windows 10 or 11, using either MSVC (the CI-tested default) or
MinGW-w64 via MSYS2. Mirrors what Tools/setup/ubuntu.sh does on Linux.
The script is idempotent: re-running it skips packages that are already
installed. Package installs are performed via winget; if winget is not
available the script falls back to Chocolatey for the few packages that
cannot be installed any other way (currently only GNU make).
Run from an elevated PowerShell prompt:
Set-ExecutionPolicy -Scope Process Bypass
.\Tools\setup\windows.ps1
To pick a build path:
.\Tools\setup\windows.ps1 -Toolchain MSVC # default, CI-tested
.\Tools\setup\windows.ps1 -Toolchain MinGW # MSYS2 mingw-w64
.\Tools\setup\windows.ps1 -Toolchain Both
.PARAMETER Toolchain
Which build path to install. One of: MSVC, MinGW, Both. Default: MSVC.
.PARAMETER NoBuildTools
Skip the Visual Studio Build Tools install (assume the user already has
Visual Studio 2022 with the "Desktop development with C++" workload).
.PARAMETER NoPip
Skip the pip install step (assume Python deps are already installed).
.NOTES
See docs/en/dev_setup/dev_env_windows_native.md for the full guide.
#>
[CmdletBinding()]
param(
[ValidateSet('MSVC', 'MinGW', 'Both')]
[string]$Toolchain = 'MSVC',
[switch]$NoBuildTools,
[switch]$NoPip
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Write-Section($Message) {
Write-Host ""
Write-Host "==> $Message" -ForegroundColor Cyan
}
function Test-Command($Name) {
# Restrict to real applications/scripts on PATH and ignore PowerShell
# functions and aliases. Otherwise a same-named function defined in the
# caller's session (e.g. a test stub) would silently satisfy this check.
[bool](Get-Command -Name $Name -CommandType Application,ExternalScript -ErrorAction SilentlyContinue)
}
# Refresh $env:Path from the machine + user registry hives. winget installs
# update the registry PATH but do NOT propagate it to the current process,
# so any post-install command (`python -m pip ...`, `make ...`) would fail
# in the same shell unless we re-pull PATH ourselves.
function Update-PathFromRegistry {
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$combined = @($machinePath, $userPath) | Where-Object { $_ } | ForEach-Object { $_.TrimEnd(';') }
if ($combined.Count -gt 0) { $env:Path = ($combined -join ';') }
}
# Locate a tool that winget just installed. Refreshes PATH from the registry
# first, then falls back to a list of well-known install locations. Returns
# the full path to the executable, or $null if it cannot be found.
function Resolve-Tool($Name, [string[]]$ExtraSearchPaths = @()) {
if (Test-Command $Name) { return (Get-Command $Name -CommandType Application,ExternalScript).Source }
Update-PathFromRegistry
if (Test-Command $Name) { return (Get-Command $Name -CommandType Application,ExternalScript).Source }
foreach ($candidate in $ExtraSearchPaths) {
if (Test-Path $candidate) {
$dir = Split-Path -Parent $candidate
$procPathEntries = $env:Path.Split(';', [StringSplitOptions]::RemoveEmptyEntries)
if (-not ($procPathEntries | Where-Object { $_.TrimEnd('\') -ieq $dir.TrimEnd('\') })) {
$env:Path = "$env:Path;$dir"
}
return $candidate
}
}
return $null
}
function Install-Winget($Id, $Override = $null) {
Write-Host " winget install $Id"
# NOTE: do NOT name this $args -- that's an automatic variable inside every
# function, and `& winget @args` would splat the function's auto-args (here
# empty) rather than the intended array.
$wingetArgs = @('install', '--id', $Id, '-e',
'--source', 'winget',
'--accept-package-agreements',
'--accept-source-agreements',
'--disable-interactivity')
if ($Override) { $wingetArgs += @('--override', $Override) }
& winget @wingetArgs
# winget exit codes that mean "nothing to do" -- treat as success:
# -1978335189 (0x8A15002B) APPINSTALLER_CLI_ERROR_NO_APPLICABLE_UPGRADE
# -1978335212 (0x8A150014) APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE
# -1978335215 (0x8A150011) APPINSTALLER_CLI_ERROR_PACKAGE_ALREADY_INSTALLED
$okCodes = @(0, -1978335189, -1978335212, -1978335215)
if ($okCodes -notcontains $LASTEXITCODE) {
throw "winget install $Id failed with exit code $LASTEXITCODE"
}
}
# ---------------------------------------------------------------------------
# Pre-flight checks
# ---------------------------------------------------------------------------
# Visual Studio Build Tools writes to C:\Program Files and winget's
# machine-scope installs require elevation, so fail fast with an actionable
# error rather than partway through a 5+ GB install.
$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw @"
This script must be run from an ELEVATED PowerShell prompt.
1. Press Start, type 'PowerShell'
2. Right-click 'Windows PowerShell' and choose 'Run as administrator'
3. cd to the PX4-Autopilot directory and re-run:
Set-ExecutionPolicy -Scope Process Bypass
.\Tools\setup\windows.ps1
"@
}
if (-not (Test-Command 'winget')) {
throw "winget is not on PATH. Install 'App Installer' from the Microsoft Store, then re-run this script."
}
# ---------------------------------------------------------------------------
# Common dependencies (both toolchains)
# ---------------------------------------------------------------------------
Write-Section "Installing common build dependencies"
Install-Winget 'Git.Git'
Install-Winget 'Python.Python.3.11'
Install-Winget 'Kitware.CMake'
Install-Winget 'Ninja-build.Ninja'
# Pull any PATH entries the four installs above just registered into the
# current process so the make / python / cmake checks below see them without
# requiring the user to open a new shell first.
Update-PathFromRegistry
# Git for Windows installs `git.exe` to `C:\Program Files\Git\cmd` (which is
# what its installer puts on the system PATH), but the PX4 top-level Makefile
# also calls into `sed`, `awk`, `dirname`, `[`, and `ps` -- none of which ship
# with Windows and all of which live in `<git>\usr\bin`. Without that directory
# on PATH, `make px4_sitl_default` aborts in the parameter / version pre-build
# steps even with a working MSVC compiler. Probe the registry for the actual
# Git install location (in case the user installed to a non-default path) and
# fall back to the default `C:\Program Files\Git`.
$gitRoot = $null
$gitUninstallKeys = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
foreach ($key in $gitUninstallKeys) {
$entry = Get-ItemProperty $key -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -like 'Git*' -and $_.InstallLocation -and (Test-Path (Join-Path $_.InstallLocation 'usr\bin')) } |
Select-Object -First 1
if ($entry) { $gitRoot = $entry.InstallLocation.TrimEnd('\'); break }
}
if (-not $gitRoot) { $gitRoot = "${env:ProgramFiles}\Git" }
$gitUsrBin = Join-Path $gitRoot 'usr\bin'
if (Test-Path $gitUsrBin) {
# Mirror the dedup logic used for `mingw64\bin` further down: split the
# user PATH on ';' and compare entries case-insensitively to avoid false
# negatives from trailing slashes and to avoid appending duplicates on
# re-run.
$userPathGit = [Environment]::GetEnvironmentVariable('Path', 'User')
$pathEntriesGit = @()
if ($userPathGit) { $pathEntriesGit = $userPathGit.Split(';', [StringSplitOptions]::RemoveEmptyEntries) }
$alreadyOnPathGit = $pathEntriesGit | Where-Object { $_.TrimEnd('\') -ieq $gitUsrBin.TrimEnd('\') }
if (-not $alreadyOnPathGit) {
Write-Host " Adding $gitUsrBin to user PATH (provides sed/awk/dirname/ps for the PX4 Makefile)"
$newPathGit = if ($userPathGit) { "$userPathGit;$gitUsrBin" } else { $gitUsrBin }
[Environment]::SetEnvironmentVariable('Path', $newPathGit, 'User')
}
# Also expose `usr\bin` in the current process so a `make px4_sitl_default`
# in this same shell finds the GNU tools without requiring a new shell.
$procPathEntriesGit = $env:Path.Split(';', [StringSplitOptions]::RemoveEmptyEntries)
if (-not ($procPathEntriesGit | Where-Object { $_.TrimEnd('\') -ieq $gitUsrBin.TrimEnd('\') })) {
$env:Path = "$env:Path;$gitUsrBin"
}
} else {
Write-Host " WARNING: Git Bash 'usr\bin' not found at $gitUsrBin."
Write-Host " PX4's Makefile needs sed/awk/dirname/ps from Git Bash."
Write-Host " If Git is installed elsewhere, add <git-root>\usr\bin to PATH manually."
}
# GNU make is not on the default winget source. The PX4 top-level Makefile
# is required by `make px4_sitl_default`, so install it from chocolatey if
# present; otherwise fall back to ezwinports' winget package which ships a
# native Win32 make.exe.
if (-not (Test-Command 'make')) {
Write-Section "Installing GNU make"
if (Test-Command 'choco') {
& choco install -y make --no-progress
# choco exit code 0 = installed, 1641/3010 = reboot required (benign).
# Anything else is a real failure.
$okChocoCodes = @(0, 1641, 3010)
if ($okChocoCodes -notcontains $LASTEXITCODE) {
throw "choco install make failed with exit code $LASTEXITCODE"
}
} else {
Install-Winget 'ezwinports.make'
}
# Both choco's make and ezwinports.make register their bin directory on
# the registry PATH but not on the running process's PATH; refresh so a
# subsequent `make px4_sitl_default` in this session can find make.exe.
$makePath = Resolve-Tool 'make' @(
'C:\ProgramData\chocolatey\bin\make.exe',
"${env:ProgramFiles}\ezwinports\make-*\bin\make.exe",
"${env:LOCALAPPDATA}\Microsoft\WinGet\Packages\ezwinports.make_Microsoft.Winget.Source_*\bin\make.exe"
)
if (-not $makePath) {
Write-Host " WARNING: make was installed but is not on PATH for the current shell."
Write-Host " Open a NEW shell before running 'make px4_sitl_default'."
}
}
# ---------------------------------------------------------------------------
# MSVC toolchain
# ---------------------------------------------------------------------------
if ($Toolchain -in @('MSVC', 'Both')) {
Write-Section "Installing MSVC toolchain"
if ($NoBuildTools) {
Write-Host " -NoBuildTools set; assuming Visual Studio 2022 is already installed."
} else {
# The VCTools workload ("C++ build tools") pulls in the MSVC core
# IDE bits but lists the latest MSVC v143 toolset and the Windows
# 11 SDK only as *Recommended* (not Required) components, so the
# latest SDK is NOT installed unless we add it explicitly. Pin the
# newest Windows 11 SDK (26100) and add the MSVC x64/x86 toolset
# plus the C++ CMake tools so builds are reproducible.
# Reference: https://learn.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-build-tools?view=vs-2022
$vsOverride = '--quiet --wait --norestart --nocache ' +
'--add Microsoft.VisualStudio.Workload.VCTools ' +
'--add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ' +
'--add Microsoft.VisualStudio.Component.VC.CMake.Project ' +
'--add Microsoft.VisualStudio.Component.Windows11SDK.26100'
Install-Winget 'Microsoft.VisualStudio.2022.BuildTools' $vsOverride
}
}
# ---------------------------------------------------------------------------
# MinGW toolchain (MSYS2)
# ---------------------------------------------------------------------------
if ($Toolchain -in @('MinGW', 'Both')) {
Write-Section "Installing MSYS2 (MinGW-w64 toolchain)"
Install-Winget 'MSYS2.MSYS2'
# MSYS2 installs to C:\msys64 by default but can be relocated via a custom
# --location override on the installer. Probe the Uninstall registry key
# for the actual InstallLocation before falling back to the default path.
$msys2Root = $null
$uninstallKeys = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
foreach ($key in $uninstallKeys) {
$entry = Get-ItemProperty $key -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -like 'MSYS2*' -and $_.InstallLocation } |
Select-Object -First 1
if ($entry) { $msys2Root = $entry.InstallLocation.TrimEnd('\'); break }
}
if (-not $msys2Root) { $msys2Root = 'C:\msys64' }
$msys2Bash = Join-Path $msys2Root 'usr\bin\bash.exe'
if (-not (Test-Path $msys2Bash)) {
throw @"
MSYS2 was installed but bash.exe was not found at:
$msys2Bash
To recover:
1. Verify the install: winget list MSYS2.MSYS2
2. If MSYS2 was installed to a non-default location, set the install root
and re-run this script, OR install the MinGW toolchain manually from an
MSYS2 shell:
pacman -S --needed mingw-w64-x86_64-toolchain mingw-w64-x86_64-ccache
3. If MSYS2 is missing entirely, reinstall:
winget install MSYS2.MSYS2 -e --source winget
"@
}
$mingwBin = Join-Path $msys2Root 'mingw64\bin'
# `pacman -Syu` can update the MSYS2 runtime itself, in which case it
# forcibly closes its shell and requires a second invocation to finish.
# Run it twice so a runtime upgrade on the first pass converges on the
# second; the second pass is a near-instant no-op when nothing changed.
Write-Host " Updating MSYS2 package database (pass 1/2)"
& $msys2Bash -lc 'pacman -Syu --noconfirm --noprogressbar'
if ($LASTEXITCODE -ne 0) {
throw "MSYS2 'pacman -Syu' failed with exit code $LASTEXITCODE"
}
Write-Host " Updating MSYS2 package database (pass 2/2)"
& $msys2Bash -lc 'pacman -Syu --noconfirm --noprogressbar'
if ($LASTEXITCODE -ne 0) {
throw "MSYS2 'pacman -Syu' (second pass) failed with exit code $LASTEXITCODE"
}
Write-Host " Installing mingw-w64-x86_64 toolchain"
& $msys2Bash -lc 'pacman -S --noconfirm --noprogressbar --needed mingw-w64-x86_64-toolchain mingw-w64-x86_64-ccache'
if ($LASTEXITCODE -ne 0) {
throw "MSYS2 'pacman -S mingw-w64-x86_64-toolchain' failed with exit code $LASTEXITCODE"
}
# Toolchain-mingw-w64-x86_64.cmake searches <msys2>/mingw64/bin first,
# but the wrapper .cmd shims it generates need the directory on PATH at
# build time too. ($mingwBin was set above from the MSYS2 install root.)
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
# Split on ';' and compare entries case-insensitively to avoid false
# negatives from a substring search (e.g. trailing slashes) and to avoid
# appending a duplicate on re-run.
$pathEntries = @()
if ($userPath) { $pathEntries = $userPath.Split(';', [StringSplitOptions]::RemoveEmptyEntries) }
$alreadyOnPath = $pathEntries | Where-Object { $_.TrimEnd('\') -ieq $mingwBin.TrimEnd('\') }
if (-not $alreadyOnPath) {
Write-Host " Adding $mingwBin to user PATH"
$newPath = if ($userPath) { "$userPath;$mingwBin" } else { $mingwBin }
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
}
# Also expose mingw64\bin in the current process so any subsequent steps
# (e.g. a follow-up `make` in the same shell) can find gcc/g++ without
# the user having to spawn a new shell first.
$procPathEntries = $env:Path.Split(';', [StringSplitOptions]::RemoveEmptyEntries)
if (-not ($procPathEntries | Where-Object { $_.TrimEnd('\') -ieq $mingwBin.TrimEnd('\') })) {
$env:Path = "$env:Path;$mingwBin"
}
}
# ---------------------------------------------------------------------------
# Python build-time dependencies
# ---------------------------------------------------------------------------
if (-not $NoPip) {
Write-Section "Installing Python build dependencies"
# winget installs Python.Python.3.11 to the user's AppData and updates the
# registry PATH, but the running PowerShell session inherited PATH at
# startup and will NOT see the new entry. Refresh PATH from the registry
# and, if python is still missing, fall back to the `py` launcher and to
# the well-known Python 3.11 install locations. This is the difference
# between a one-shot setup and a "open a new shell and re-run" loop.
$pythonExe = Resolve-Tool 'python' @(
"${env:LOCALAPPDATA}\Programs\Python\Python311\python.exe",
"${env:ProgramFiles}\Python311\python.exe",
"${env:ProgramFiles(x86)}\Python311\python.exe"
)
if (-not $pythonExe) {
# Last-resort: the Python launcher (`py.exe`) ships in C:\Windows and
# is on the per-machine PATH that survives a fresh PowerShell session.
$pyLauncher = Resolve-Tool 'py' @('C:\Windows\py.exe')
if ($pyLauncher) {
Write-Host " python.exe not on PATH for this session; using 'py -3.11' instead."
$pythonExe = $pyLauncher
$pythonArgs = @('-3.11')
} else {
throw @"
Python was installed but neither 'python' nor 'py' is on PATH for this shell.
This is usually a transient PATH-propagation issue after a winget install.
To recover, open a NEW PowerShell window and re-run with -NoBuildTools so the
script skips straight to the pip step:
.\Tools\setup\windows.ps1 -NoBuildTools
"@
}
} else {
$pythonArgs = @()
}
# Mirrors the package set installed by .github/workflows/compile_windows.yml.
# kconfiglib is required by cmake/kconfig.cmake; on the Ubuntu MinGW CI
# runner it is provided by the `python3-kconfiglib` apt package, on the
# MSVC runner and here it must come from pip.
& $pythonExe @pythonArgs -m pip install --upgrade pip
if ($LASTEXITCODE -ne 0) { throw "pip self-upgrade failed with exit code $LASTEXITCODE" }
# empy is pinned <4 to match Tools/setup/requirements.txt: PX4's msg /
# uxrce_dds / flight_tasks / zenoh code generators use the empy 3.x
# `em.Interpreter(..., options={em.RAW_OPT: True, em.BUFFERED_OPT: True})`
# API which was removed in empy 4.x. The Ubuntu CI runners get the right
# version via the `python3-empy` apt package (3.3.x); on Windows pip would
# otherwise pull empy 4.x and break code generation.
& $pythonExe @pythonArgs -m pip install jinja2 pyyaml toml numpy packaging jsonschema future "empy>=3.3,<4" pyros-genmsg kconfiglib
if ($LASTEXITCODE -ne 0) { throw "pip install failed with exit code $LASTEXITCODE" }
}
# ---------------------------------------------------------------------------
# Done
# ---------------------------------------------------------------------------
Write-Section "Setup complete"
Write-Host ""
Write-Host "Open a NEW shell so PATH changes take effect, then build PX4 SITL:"
Write-Host ""
if ($Toolchain -in @('MSVC', 'Both')) {
Write-Host " MSVC build (from 'x64 Native Tools Command Prompt for VS 2022'):"
Write-Host " cd PX4-Autopilot"
Write-Host " make px4_sitl_default"
Write-Host ""
}
if ($Toolchain -in @('MinGW', 'Both')) {
Write-Host " MinGW build (from PowerShell):"
Write-Host " cd PX4-Autopilot"
Write-Host " `$env:CMAKE_ARGS = '-DCMAKE_TOOLCHAIN_FILE=Toolchain-mingw-w64-x86_64'"
Write-Host " make px4_sitl_default"
Write-Host ""
}
Write-Host "Run a SIH simulation from the build output:"
Write-Host " `$env:PX4_SIM_MODEL = 'sihsim_quadx'"
Write-Host " build\px4_sitl_default\bin\px4.exe -d build\px4_sitl_default\etc"
Write-Host ""
Write-Host "See docs/en/dev_setup/dev_env_windows_native.md for the full guide."
+105
View File
@@ -0,0 +1,105 @@
<#
.SYNOPSIS
Run multiple instances of the 'px4' binary on Windows, without starting
an external simulator. Mirrors Tools/simulation/sitl_multiple_run.sh -
same args, same per-instance working directories, same logfile names.
.DESCRIPTION
For developers on PowerShell or CMD (the .sh script also works under
Git Bash on Windows). Assumes px4 is already built with the specified
build target (e.g., px4_sitl_default).
Pass 0 for SitlNum to just stop everything currently running:
.\Tools\simulation\sitl_multiple_run.ps1 0
.PARAMETER SitlNum
Number of instances to start. Defaults to 2.
.PARAMETER SimModel
Value to export as PX4_SIM_MODEL. Defaults to 'gazebo-classic_iris' to
match the .sh script; set to 'sihsim_quadx' for a Windows-friendly run
that needs no external simulator.
.PARAMETER BuildTarget
Name of the cmake build directory under build/. Defaults to
'px4_sitl_default'.
.EXAMPLE
.\Tools\simulation\sitl_multiple_run.ps1 3 sihsim_quadx px4_sitl_default
.EXAMPLE
# From CMD:
powershell -ExecutionPolicy Bypass -File Tools\simulation\sitl_multiple_run.ps1 2 sihsim_quadx
#>
param(
[int]$SitlNum = 2,
[string]$SimModel = 'gazebo-classic_iris',
[string]$BuildTarget = 'px4_sitl_default'
)
$ErrorActionPreference = 'Stop'
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$SrcPath = (Resolve-Path (Join-Path $ScriptDir '..\..')).Path
$BuildPath = Join-Path $SrcPath "build\$BuildTarget"
# Prefer px4.exe; fall back to px4 so this also runs against a WSL/MinGW
# layout that drops the suffix.
$Px4Bin = Join-Path $BuildPath 'bin\px4.exe'
if (-not (Test-Path $Px4Bin)) { $Px4Bin = Join-Path $BuildPath 'bin\px4' }
if ($SitlNum -gt 0 -and -not (Test-Path $Px4Bin)) {
Write-Error "px4 binary not found in $BuildPath\bin"
}
Write-Host 'killing running instances'
Get-Process -Name 'px4' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
# Clean up stale lock files left by force-killed instances. Each instance
# uses %TEMP%\px4_lock-<N>; if the previous owner was killed before it could
# unlink, the next start sees "already running" and aborts.
Get-ChildItem -Path "$env:TEMP" -Filter 'px4_lock-*' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
# Clear stale per-instance log files from previous runs. These are not
# truncated by the new launch (Start-Process -RedirectStandardOutput appends),
# so leftover content from a different rcS / different model can mislead
# debugging by appearing alongside fresh output.
if ($SitlNum -gt 0) {
for ($n = 0; $n -lt $SitlNum; $n++) {
$stale = Join-Path $BuildPath "instance_$n"
if (Test-Path $stale) {
Remove-Item -Path (Join-Path $stale 'out.log') -Force -ErrorAction SilentlyContinue
Remove-Item -Path (Join-Path $stale 'err.log') -Force -ErrorAction SilentlyContinue
}
}
}
$env:PX4_SIM_MODEL = $SimModel
for ($n = 0; $n -lt $SitlNum; $n++) {
$WorkingDir = Join-Path $BuildPath "instance_$n"
if (-not (Test-Path $WorkingDir)) { New-Item -ItemType Directory -Path $WorkingDir | Out-Null }
Write-Host "starting instance $n in $WorkingDir"
# Spawn px4.exe directly with redirected stdout/stderr.
#
# Earlier revisions wrapped each launch in `cmd.exe /c "px4.exe ... > out.log"`,
# which had a fatal flaw at higher instance counts: every cmd.exe child
# inherits the launcher's console, and when that console is torn down
# (cmd.exe exiting after spawning px4, or the launcher PowerShell window
# closing) Windows broadcasts CTRL_CLOSE_EVENT to every attached process.
# PX4's SetConsoleCtrlHandler maps CTRL_CLOSE_EVENT to SIGINT and shuts
# down cleanly, so multi-instance launches died silently a few seconds
# after rcS finished -- no error, empty stderr, just gone.
#
# Start-Process with -RedirectStandard* writes both streams to per-instance
# log files without involving cmd.exe, and -WindowStyle Hidden gives each
# child its own (hidden) console so it survives the launcher exiting.
$stdoutPath = Join-Path $WorkingDir 'out.log'
$stderrPath = Join-Path $WorkingDir 'err.log'
Start-Process -FilePath $Px4Bin `
-ArgumentList @('-i', $n, '-d', "$BuildPath\etc") `
-WorkingDirectory $WorkingDir `
-RedirectStandardOutput $stdoutPath `
-RedirectStandardError $stderrPath `
-WindowStyle Hidden | Out-Null
}
+14 -2
View File
@@ -7,6 +7,9 @@
# ./Tools/simulation/sitl_multiple_run.sh 3 sihsim_quadx px4_sitl_sih
# ./Tools/simulation/sitl_multiple_run.sh 2 gazebo-classic_iris px4_sitl_default
# ./Tools/simulation/sitl_multiple_run.sh # defaults: 2 instances, gazebo-classic_iris, px4_sitl_default
#
# Pass 0 instances to just stop everything currently running:
# ./Tools/simulation/sitl_multiple_run.sh 0
sitl_num=${1:-2}
sim_model=${2:-gazebo-classic_iris}
@@ -17,8 +20,17 @@ src_path="$SCRIPT_DIR/../../"
build_path=${src_path}/build/${build_target}
# Pick px4 vs px4.exe so this script also runs from Git Bash on Windows.
px4_bin="$build_path/bin/px4"
[ -x "$px4_bin.exe" ] && px4_bin="$px4_bin.exe"
echo "killing running instances"
pkill -x px4 || true
if command -v pkill >/dev/null 2>&1; then
pkill -x px4 || true
fi
if command -v taskkill >/dev/null 2>&1; then
taskkill //IM px4.exe //F >/dev/null 2>&1 || true
fi
sleep 1
@@ -31,7 +43,7 @@ while [ $n -lt $sitl_num ]; do
pushd "$working_dir" &>/dev/null
echo "starting instance $n in $(pwd)"
$build_path/bin/px4 -i $n -d "$build_path/etc" >out.log 2>err.log &
"$px4_bin" -i $n -d "$build_path/etc" >out.log 2>err.log &
popd &>/dev/null
n=$(($n + 1))