Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3517994
Build natively on Windows for development (MSYS2 clang64)
AmyJeanes Sep 7, 2026
e33ffc9
ui, prefix, url_file: fixes from the first Windows launches
AmyJeanes Sep 7, 2026
27353bf
replay: fix the Windows crash at startup and unthrottled playback
AmyJeanes Sep 7, 2026
056049e
replay: render the console UI correctly on PDCurses
AmyJeanes Sep 7, 2026
afaf7e1
paths: put the download cache under the platform temp directory
AmyJeanes Sep 7, 2026
f103ef4
tests: make the suite collectable on Windows
AmyJeanes Sep 7, 2026
88b7139
windows: high resolution monotonic clock and a SIGINT-based Timeout
AmyJeanes Sep 7, 2026
bb54c39
manager: run and stop native processes on Windows
AmyJeanes Sep 7, 2026
8e6f3d9
swaglog: skip the zmq teardown at process exit on Windows
AmyJeanes Sep 7, 2026
b33c93f
loggerd/athena: xattrs as NTFS streams, temp dir and '/' keys on Windows
AmyJeanes Sep 7, 2026
8667d93
tests: make the messaging, loggerd, athena and tool tests pass on Win…
AmyJeanes Sep 7, 2026
f29b7ff
process replay: stop replayed processes on Windows
AmyJeanes Sep 7, 2026
434815d
tests: let the big-log logmessaged test wait for the daemon's writes
AmyJeanes Sep 7, 2026
4c4df3d
cabana: dark title bar on Windows in the dark theme
AmyJeanes Sep 7, 2026
b04ebf8
modeld: compile the driving and driver monitoring models on Windows
AmyJeanes Sep 7, 2026
0b2b776
manager, webcam: treat Windows like macOS for Linux-only pieces
AmyJeanes Sep 7, 2026
ba8aa9a
replay: decode with Direct3D 11 on Windows
AmyJeanes Sep 7, 2026
932ec66
manager: run the whole offroad stack on Windows
AmyJeanes Sep 7, 2026
5d62f4b
params: retry the file replacement on Windows sharing violations
AmyJeanes Sep 7, 2026
5a9ae25
tools: set up openpilot on Windows with op.sh
AmyJeanes Sep 7, 2026
f459ee8
CI: build and test on windows-latest
AmyJeanes Sep 8, 2026
b24d936
TEMP: Windows wheels from a pre-release index until commaai/dependenc…
AmyJeanes Sep 8, 2026
73522dc
TEMP: submodules from the fork branches until the series PRs merge
AmyJeanes Sep 8, 2026
4bd8010
pyproject: install teleoprtc on Windows
AmyJeanes Sep 7, 2026
3da7a5c
CI: run on pushes to the fork's windows-dev branch
AmyJeanes Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
* text=auto
* text=auto eol=lf

# to move existing files into LFS:
# git add --renormalize .
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- master
- windows-dev # --- FORK ONLY, NOT FOR UPSTREAM: CI on pushes to the fork's development branch ---
pull_request:
workflow_dispatch:
workflow_call:
Expand Down Expand Up @@ -76,6 +77,30 @@ jobs:
- name: Building openpilot
run: scons

build_windows:
name: build Windows
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v7
- uses: msys2/setup-msys2@v2
with:
msystem: CLANG64
path-type: inherit # Git for Windows and uv stay visible
- run: ./tools/op.sh setup
- name: Building openpilot
run: tools/op.sh build # activates the venv first: the MSYS2 toolchain's own python3 shadows it in this shell
- name: Software OpenGL for the UI tests
# the runner's display driver has no OpenGL; Mesa's llvmpipe opengl32.dll next to the interpreter wins the DLL search
run: |
pacman -S --needed --noconfirm "$MINGW_PACKAGE_PREFIX-mesa"
source .venv/Scripts/activate
cp /clang64/bin/opengl32.dll "$(dirname "$(python -c 'import sys; print(sys._base_executable)')")"
- name: Run unit tests
timeout-minutes: 20
run: tools/op.sh test
static_analysis:
name: static analysis
runs-on: ${{
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ bin/
*.os-*
*.so
*.a
*.dll
*.pyd
*.exe
st[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z]
*.unchunked
*.clb
Expand Down
12 changes: 8 additions & 4 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
[submodule "panda"]
path = panda
url = ../../commaai/panda.git
# --- TODO REMOVE AFTER THE SUBMODULE PRS MERGE (commaai/panda#2427): the pinned commit is on the fork branch behind that PR ---
url = https://github.com/AmyJeanes/panda.git
[submodule "opendbc"]
path = opendbc_repo
url = ../../commaai/opendbc.git
# --- TODO REMOVE AFTER THE SUBMODULE PRS MERGE (commaai/opendbc#3724): the pinned commit is on the fork branch behind that PR ---
url = https://github.com/AmyJeanes/opendbc.git
[submodule "msgq"]
path = msgq_repo
url = ../../commaai/msgq.git
# --- TODO REMOVE AFTER THE SUBMODULE PRS MERGE (commaai/msgq#709): the pinned commit is on the fork branch behind that PR ---
url = https://github.com/AmyJeanes/msgq.git
[submodule "rednose_repo"]
path = rednose_repo
url = ../../commaai/rednose.git
# --- TODO REMOVE AFTER THE SUBMODULE PRS MERGE (commaai/rednose#61): the pinned commit is on the fork branch behind that PR ---
url = https://github.com/AmyJeanes/rednose.git
[submodule "teleoprtc_repo"]
path = teleoprtc_repo
url = ../../commaai/teleoprtc
Expand Down
85 changes: 75 additions & 10 deletions SConstruct
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import sys
import sysconfig
import platform
import shlex
import shutil
import tempfile
import importlib
import numpy as np

Expand Down Expand Up @@ -43,16 +45,21 @@ if external_pythonpath := os.environ.get("PYTHONPATH"):
submodule_python_paths += [p for p in external_pythonpath.split(os.pathsep) if p and p not in submodule_python_paths]

# Detect platform
arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip()
WINDOWS = platform.system() == "Windows"
if platform.system() == "Darwin":
arch = "Darwin"
elif arch == "aarch64" and COMMA_HARDWARE:
arch = "comma_arm64"
elif WINDOWS:
arch = "Windows"
else:
arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip()
if arch == "aarch64" and COMMA_HARDWARE:
arch = "comma_arm64"
assert arch in [
"comma_arm64", # linux comma hardware (AGNOS) arm64
"aarch64", # linux pc arm64
"x86_64", # linux pc x64
"Darwin", # macOS arm64 (x86 not supported)
"Windows", # windows pc x64, development only (MSYS2 clang64 toolchain)
]

pkg_names = ['acados', 'capnproto', 'ffmpeg', 'json11', 'ncurses', 'zeromq', 'zstd']
Expand All @@ -65,13 +72,13 @@ ffmpeg = pkgs[pkg_names.index('ffmpeg')]
# TODO: drop the static fallback once device venvs have comma-deps-ffmpeg>=7.1.0.post94
_ffmpeg_lib_names = os.listdir(ffmpeg.LIB_DIR) if os.path.isdir(ffmpeg.LIB_DIR) else []
ffmpeg_shared = any(
n.startswith('libavcodec.so') or (n.startswith('libavcodec') and n.endswith('.dylib'))
n.startswith('libavcodec.so') or (n.startswith('libavcodec') and n.endswith(('.dylib', '.dll.a')))
for n in _ffmpeg_lib_names
)
ffmpeg_libs = ['avformat', 'avcodec', 'swresample', 'avutil']
if not ffmpeg_shared:
ffmpeg_libs += ['x264', 'z']
if arch != "Darwin":
if arch not in ("Darwin", "Windows"):
ffmpeg_libs += ['va', 'va-drm', 'drm']
acados_include_dirs = [
acados.INCLUDE_DIR,
Expand All @@ -88,15 +95,22 @@ acados_include_dirs = [
allowed_system_libs = {
"EGL", "GLESv2", "GL",
"dl", "drm", "gbm", "m", "pthread",
# Windows SDK import libraries
"opengl32", "gdi32", "winmm", "shell32", "user32", "advapi32", "ws2_32", "bcrypt", "ole32", "setupapi", "shlwapi", "dwmapi", "ntdll",
"iphlpapi", "rpcrt4",
}
# static libzmq/capnp need these on every Windows link; import libs only pull in what is referenced
windows_link_libs = ["pthread", "ws2_32", "iphlpapi", "rpcrt4", "bcrypt", "advapi32", "ole32", "user32", "shell32"] if WINDOWS else []

def _resolve_lib(env, name):
for d in env.Flatten(env.get('LIBPATH', [])):
p = Dir(str(d)).abspath
for ext in ('.a', '.so', '.dylib'):
for ext in ('.a', '.so', '.dylib', '.dll.a', '.lib'):
f = File(os.path.join(p, f'lib{name}{ext}'))
if f.exists() or f.has_builder():
return name
if WINDOWS and File(os.path.join(p, f'{name}.lib')).exists(): # MSVC-style import library, e.g. python312.lib
return name
if name in allowed_system_libs:
return name
raise SCons.Errors.UserError(f"Unexpected non-vendored library '{name}'")
Expand All @@ -114,11 +128,39 @@ def _libflags(target, source, env, for_signature):
libs.append(_resolve_lib(env, lib))
else:
libs.append(lib)
libs += [_resolve_lib(env, lib) for lib in windows_link_libs]
return _stripixes(env['LIBLINKPREFIX'], libs, env['LIBLINKSUFFIX'],
env['LIBPREFIXES'], env['LIBSUFFIXES'], env, env['LIBLITERALPREFIX'])

if WINDOWS:
# Run every build command through MSYS2 bash so the POSIX shell syntax used by the
# SConscripts (cd x && ..., VAR=1 ./script.py, shebang scripts) keeps working. This
# replaces the platform spawn so it also covers Environments created by submodules.
# bash must be resolved via PATH: CreateProcess searches System32 first, which
# would pick the WSL launcher.
import SCons.Platform.posix
import SCons.Platform.win32
_bash = shutil.which("bash")
if not _bash or "system32" in _bash.lower():
raise SCons.Errors.UserError("MSYS2 bash must be on PATH before System32 (run scons from an MSYS2 CLANG64 shell)")

def _bash_spawn(sh, escape, cmd, args, env):
# bash treats backslashes as escapes, so hand it SCons' Windows paths with forward
# slashes. Arguments carrying quotes (defines such as -DSWAGLOG="\"...\"") are kept as is.
args = [a if '"' in a else a.replace("\\", "/") for a in args]
return subprocess.call([_bash, "-c", " ".join(args)], env=env)
SCons.Platform.win32.spawn = _bash_spawn
SCons.Platform.win32.escape = SCons.Platform.posix.escape

# Windows child processes need the system variables cmd/python rely on; POSIX builds keep the strict env
_windows_env = {k: os.environ[k] for k in (
"SYSTEMROOT", "SYSTEMDRIVE", "COMSPEC", "PATHEXT", "TEMP", "TMP", "USERPROFILE", "HOMEDRIVE", "HOMEPATH", "HOME",
"APPDATA", "LOCALAPPDATA", "USERNAME", "NUMBER_OF_PROCESSORS", "PROCESSOR_ARCHITECTURE", "MSYSTEM", "MSYSTEM_PREFIX", "VIRTUAL_ENV",
) if k in os.environ} if WINDOWS else {}

env = Environment(
ENV={
**_windows_env,
"PATH": os.environ['PATH'],
"PYTHONPATH": os.pathsep.join(submodule_python_paths),
"ACADOS_SOURCE_DIR": acados.DIR,
Expand All @@ -132,7 +174,7 @@ env = Environment(
"-O2",
"-Wunused",
"-Werror",
"-Wshadow" if arch in ("Darwin", "comma_arm64") else "-Wshadow=local",
"-Wshadow" if arch in ("Darwin", "comma_arm64", "Windows") else "-Wshadow=local",
"-Wno-unknown-warning-option",
"-Wno-inconsistent-missing-override",
"-Wno-c99-designator",
Expand Down Expand Up @@ -163,7 +205,7 @@ env = Environment(
CYTHONCFILESUFFIX=".cpp",
COMPILATIONDB_USE_ABSPATH=True,
REDNOSE_ROOT="#rednose_repo",
tools=["default", "cython", "compilation_db", "rednose_filter"],
tools=["mingw" if WINDOWS else "default", "cython", "compilation_db", "rednose_filter"],
toolpath=["#msgq_repo/site_scons/site_tools", "#rednose_repo/site_scons/site_tools"],
)
# SCons' Darwin linker tool doesn't define the variables used to expand RPATH.
Expand All @@ -173,6 +215,20 @@ if arch == "Darwin":
env["_RPATH"] = "${_concat(RPATHPREFIX, RPATH, RPATHSUFFIX, __env__)}"
if arch != "comma_arm64":
env['_LIBFLAGS'] = _libflags
if WINDOWS:
# clang and lld through the mingw tool, whose defaults are gcc; shared libraries keep the lib prefix the SConscripts expect
env["CC"], env["CXX"] = "clang", "clang++"
env["SHLIBPREFIX"] = "lib"
# PE has no rpath; DLLs are found next to the executable or via PATH
env["_RPATH"] = ""
# Self-contained binaries: Python 3.8+ does not search PATH for the DLLs an extension
# module needs, so the libc++/winpthreads runtime must not be dynamic
env.Append(LINKFLAGS=["-static"])

# uv venvs on Windows only ship python.exe, but scripts and shebangs here expect python3
_python3 = os.path.join(os.path.dirname(sys.executable), "python3.exe")
if not os.path.exists(_python3):
shutil.copy2(sys.executable, _python3)

# Arch-specific flags and paths
if arch == "comma_arm64":
Expand All @@ -190,6 +246,10 @@ elif arch == "Darwin":
])
env.Append(CCFLAGS=["-DGL_SILENCE_DEPRECATION"])
env.Append(CXXFLAGS=["-DGL_SILENCE_DEPRECATION"])
elif arch == "Windows":
# -std=c++1z is strict ANSI; mingw then hides vasprintf, M_PI and friends without these
# ZMQ_STATIC: the vendored libzmq is a static archive, without it zmq.h asks for DLL imports
env.Append(CCFLAGS=["-D_GNU_SOURCE", "-D_USE_MATH_DEFINES", "-DZMQ_STATIC"])

_extra_cc = shlex.split(GetOption('ccflags') or '')
if _extra_cc:
Expand Down Expand Up @@ -217,13 +277,18 @@ if not GetOption('verbose'):

# ********** Cython build environment **********
envCython = env.Clone()
envCython["CPPPATH"] += [sysconfig.get_paths()['include'], np.get_include()]
# in a Windows venv sysconfig points at the (empty) venv Include dir, headers live with the base interpreter
envCython["CPPPATH"] += [sysconfig.get_paths(vars={"installed_base": sys.base_prefix})['include'], np.get_include()]
envCython["CCFLAGS"] += ["-Wno-#warnings", "-Wno-cpp", "-Wno-shadow", "-Wno-deprecated-declarations"]
envCython["CCFLAGS"].remove("-Werror")

envCython["LIBS"] = []
if arch == "Darwin":
envCython["LINKFLAGS"] = env["LINKFLAGS"] + ["-bundle", "-undefined", "dynamic_lookup"]
elif arch == "Windows":
envCython["LINKFLAGS"] = ["-shared", "-static"]
envCython["LIBPATH"] += [os.path.join(sys.base_prefix, "libs")]
envCython["LIBS"] += [f"python{sys.version_info.major}{sys.version_info.minor}"]
else:
envCython["LINKFLAGS"] = ["-pthread", "-shared"]

Expand All @@ -233,7 +298,7 @@ Export('envCython', 'np_version')
Export('env', 'arch', 'acados', 'ffmpeg_libs')

# Setup cache dir
cache_dir = '/data/scons_cache' if arch == "comma_arm64" else '/tmp/scons_cache'
cache_dir = '/data/scons_cache' if arch == "comma_arm64" else os.path.join(tempfile.gettempdir(), 'scons_cache') if WINDOWS else '/tmp/scons_cache'
cache_size_limit = 4e9 if "CI" in os.environ else 2e9
CacheDir(cache_dir)
Clean(["."], cache_dir)
Expand Down
8 changes: 8 additions & 0 deletions openpilot/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import sys
import time

if sys.platform == "win32" and sys.version_info < (3, 13):
# the GetTickCount64 based clock only ticks every 15.6 ms; Python 3.13 moved monotonic to QueryPerformanceCounter
# TODO: drop when the Python pin reaches 3.13
setattr(time, "monotonic", time.perf_counter) # noqa: B010 (a plain assignment is a type error for ty on Windows)
setattr(time, "monotonic_ns", time.perf_counter_ns) # noqa: B010
7 changes: 6 additions & 1 deletion openpilot/cereal/messaging/tests/test_messaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ def assert_carstate(cs1, cs2):
if isinstance(val1, numbers.Number):
assert val1 == val2, f"{f}: sent '{val1}' vs recvd '{val2}'"

def recv_one_retry_process(sock, timeout):
# module level: Windows spawns the process, and a socket cannot be pickled into it
messaging.recv_one_retry(messaging.sub_sock(sock, timeout=round(timeout * 1000)))


def delayed_send(delay, sock, dat):
def send_func():
sock.send(dat)
Expand Down Expand Up @@ -148,7 +153,7 @@ def test_recv_one_retry(self):
sub_sock = messaging.sub_sock(sock, timeout=round(sock_timeout*1000))

# wait 5 socket timeouts and make sure it's still retrying
p = multiprocessing.Process(target=messaging.recv_one_retry, args=(sub_sock,))
p = multiprocessing.Process(target=recv_one_retry_process, args=(sock, sock_timeout))
p.start()
time.sleep(sock_timeout*5)
assert p.is_alive()
Expand Down
3 changes: 2 additions & 1 deletion openpilot/cereal/messaging/tests/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def test_services(self, s):
assert service.decimation != 0

def test_generated_header(self):
with tempfile.NamedTemporaryFile(suffix=".h") as f:
with tempfile.NamedTemporaryFile(suffix=".h", delete_on_close=False) as f:
f.close() # Windows: other processes cannot open the file while it is open here
ret = subprocess.run(f"python3 {services.__file__} > {f.name} && clang++ {f.name} -std=c++11", shell=True).returncode
assert ret == 0, "generated services header is not valid C"
1 change: 1 addition & 0 deletions openpilot/common/SConscript
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ Import('env')

common_libs = [
'params.cc',
'prefix.cc',
'swaglog.cc',
'util.cc',
'ratekeeper.cc',
Expand Down
14 changes: 14 additions & 0 deletions openpilot/common/file.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#pragma once

// File primitives whose POSIX spelling differs on Windows. Kept apart from util.h: that header's Rect collides with
// the one MacTypes.h brings in when a translation unit also includes CoreFoundation on macOS (cabana's settings).
#ifdef _WIN32
#include <io.h>
inline int fsync(int fd) { return _commit(fd); }
#endif

namespace util {
// an exclusive lock held until fd closes, and a rename that replaces an existing target (rename() refuses to on Windows)
int lock_file_exclusive(int fd);
int replace_file(const char *from, const char *to);
} // namespace util
5 changes: 4 additions & 1 deletion openpilot/common/gpio.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import os
import fcntl
import ctypes
try:
import fcntl
except ImportError: # Windows: only importable there, the ioctls need comma hardware
fcntl = None
from functools import cache

def gpio_init(pin: int, output: bool) -> None:
Expand Down
Loading
Loading