From e8b07548222d03546f2a0e433e88e5511b13362c Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Tue, 5 May 2026 21:06:50 -0700 Subject: [PATCH] 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 --- Tools/px_process_events.py | 7 +- Tools/setup/ros2_pixi_setup.ps1 | 225 +++++++++++++ Tools/setup/windows.ps1 | 423 +++++++++++++++++++++++++ Tools/simulation/sitl_multiple_run.ps1 | 105 ++++++ Tools/simulation/sitl_multiple_run.sh | 16 +- 5 files changed, 773 insertions(+), 3 deletions(-) create mode 100644 Tools/setup/ros2_pixi_setup.ps1 create mode 100644 Tools/setup/windows.ps1 create mode 100644 Tools/simulation/sitl_multiple_run.ps1 diff --git a/Tools/px_process_events.py b/Tools/px_process_events.py index 14d618eeb8..6004156790 100755 --- a/Tools/px_process_events.py +++ b/Tools/px_process_events.py @@ -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", diff --git a/Tools/setup/ros2_pixi_setup.ps1 b/Tools/setup/ros2_pixi_setup.ps1 new file mode 100644 index 0000000000..f02ac9a299 --- /dev/null +++ b/Tools/setup/ros2_pixi_setup.ps1 @@ -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\\ 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_.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--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--YYYYMMDD -> zip name ros2--YYYYMMDD-windows-release-amd64.zip + if ($tag -notmatch '^release-(?[a-z]+)-(?\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." diff --git a/Tools/setup/windows.ps1 b/Tools/setup/windows.ps1 new file mode 100644 index 0000000000..1e2abf9c7c --- /dev/null +++ b/Tools/setup/windows.ps1 @@ -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 `\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 \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 /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." diff --git a/Tools/simulation/sitl_multiple_run.ps1 b/Tools/simulation/sitl_multiple_run.ps1 new file mode 100644 index 0000000000..9d538fde49 --- /dev/null +++ b/Tools/simulation/sitl_multiple_run.ps1 @@ -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-; 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 +} diff --git a/Tools/simulation/sitl_multiple_run.sh b/Tools/simulation/sitl_multiple_run.sh index 39554a622d..64a4ed4a18 100755 --- a/Tools/simulation/sitl_multiple_run.sh +++ b/Tools/simulation/sitl_multiple_run.sh @@ -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))