mirror of
https://github.com/PX4/PX4-Autopilot.git
synced 2026-05-09 22:08:56 +08:00
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:
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user