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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ 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
install: mingw-w64-clang-x86_64-git # the README's first step, the setup installs the rest
- run: ./tools/op.sh setup
- name: Building openpilot
run: tools/op.sh build # activates the venv: the msys2 shell puts its own python3 ahead of it
- name: Run the tool tests
timeout-minutes: 15
run: tools/op.sh test openpilot/common openpilot/cereal/messaging openpilot/tools/cabana openpilot/tools/jotpluggler openpilot/tools/lib openpilot/system/ui openpilot/test_native.py
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
55 changes: 48 additions & 7 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 @@ -46,14 +48,18 @@ if external_pythonpath := os.environ.get("PYTHONPATH"):
arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip()
if platform.system() == "Darwin":
arch = "Darwin"
elif platform.system() == "Windows":
arch = "Windows"
elif 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)
]
WINDOWS = arch == "Windows"

pkg_names = ['acados', 'capnproto', 'ffmpeg', 'json11', 'ncurses', 'zeromq', 'zstd']
pkgs = [importlib.import_module(name) for name in pkg_names]
Expand All @@ -65,13 +71,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,12 +94,15 @@ acados_include_dirs = [
allowed_system_libs = {
"EGL", "GLESv2", "GL",
"dl", "drm", "gbm", "m", "pthread",
"opengl32", "gdi32", "winmm", "shell32", "user32", "setupapi", # Windows SDK import libraries
}
# 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"] 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'):
f = File(os.path.join(p, f'lib{name}{ext}'))
if f.exists() or f.has_builder():
return name
Expand All @@ -114,11 +123,28 @@ def _libflags(target, source, env, for_signature):
libs.append(_resolve_lib(env, lib))
else:
libs.append(lib)
libs += windows_link_libs
return _stripixes(env['LIBLINKPREFIX'], libs, env['LIBLINKSUFFIX'],
env['LIBPREFIXES'], env['LIBSUFFIXES'], env, env['LIBLITERALPREFIX'])

if WINDOWS:
# build commands run through MSYS2 bash: the SConscripts use POSIX shell syntax, the submodules' Environments too
import SCons.Platform.posix
import SCons.Platform.win32
_bash = shutil.which("bash")
if not _bash or "system32" in _bash.lower(): # System32's bash.exe is the WSL launcher
raise SCons.Errors.UserError("run scons from an MSYS2 CLANG64 shell")

def _bash_spawn(sh, escape, cmd, args, env):
args = [a if '"' in a else a.replace("\\", "/") for a in args] # bash reads backslashes as escapes; quoted defines stay
return subprocess.call([_bash, "-c", " ".join(args)], env=env)
SCons.Platform.win32.spawn = _bash_spawn
SCons.Platform.win32.escape = SCons.Platform.posix.escape

env = Environment(
ENV={
# Windows processes need the system root (DLLs), a temp dir and ccache's cache dir
**({k: os.environ[k] for k in ("SYSTEMROOT", "TEMP", "TMP", "LOCALAPPDATA") if k in os.environ} if WINDOWS else {}),
"PATH": os.environ['PATH'],
"PYTHONPATH": os.pathsep.join(submodule_python_paths),
"ACADOS_SOURCE_DIR": acados.DIR,
Expand All @@ -132,7 +158,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 +189,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 +199,14 @@ 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"] = ""
# static runtime: Python does not search PATH for the DLLs an extension module needs
env.Append(LINKFLAGS=["-static"])

# Arch-specific flags and paths
if arch == "comma_arm64":
Expand All @@ -190,6 +224,9 @@ elif arch == "Darwin":
])
env.Append(CCFLAGS=["-DGL_SILENCE_DEPRECATION"])
env.Append(CXXFLAGS=["-DGL_SILENCE_DEPRECATION"])
elif arch == "Windows":
# strict -std=c++1z hides vasprintf and M_PI in mingw's headers; the vendored libzmq is a static archive
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 @@ -224,6 +261,9 @@ 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["LIBS"] += [File(f"{sys.base_prefix}/libs/python{sys.version_info.major}{sys.version_info.minor}.lib")]
else:
envCython["LINKFLAGS"] = ["-pthread", "-shared"]

Expand All @@ -233,7 +273,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')
cache_size_limit = 4e9 if "CI" in os.environ else 2e9
CacheDir(cache_dir)
Clean(["."], cache_dir)
Expand Down Expand Up @@ -287,9 +327,10 @@ SConscript([
'openpilot/selfdrive/pandad/SConscript',
'openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript',
'openpilot/selfdrive/locationd/SConscript',
'openpilot/selfdrive/modeld/SConscript',
'openpilot/selfdrive/ui/SConscript',
])
if arch != "Windows": # modeld needs tinygrad's compiled model, Linux/macOS only
SConscript(['openpilot/selfdrive/modeld/SConscript'])

# Build desktop-only tools
if GetOption('extras') and arch != "comma_arm64":
Expand Down
4 changes: 2 additions & 2 deletions openpilot/cereal/messaging/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def log_from_bytes(dat: bytes, struct: capnp.lib.capnp._StructModule = log.Event
def new_message(service: str | None, size: int | None = None, **kwargs) -> capnp.lib.capnp._DynamicStructBuilder:
args = {
'valid': False,
'logMonoTime': int(time.monotonic() * 1e9),
'logMonoTime': int(time.perf_counter() * 1e9),
**kwargs
}
dat = log.Event.new_message(**args)
Expand Down Expand Up @@ -240,7 +240,7 @@ def update(self, timeout: int = 100) -> None:
# non-blocking receive for non-polled sockets
for s in self.non_polled_services:
msgs.append(recv_one_or_none(self.sock[s]))
self.update_msgs(time.monotonic(), msgs)
self.update_msgs(time.perf_counter(), msgs)

def update_msgs(self, cur_time: float, msgs: list[capnp.lib.capnp._DynamicStructReader]) -> None:
self.frame += 1
Expand Down
13 changes: 9 additions & 4 deletions 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 All @@ -54,7 +59,7 @@ def test_new_message(self, evt):
msg = messaging.new_message(evt)
except capnp.lib.capnp.KjException:
msg = messaging.new_message(evt, random.randrange(200))
assert (time.monotonic() - msg.logMonoTime) < 0.1
assert (time.perf_counter() - msg.logMonoTime) < 0.1
assert not msg.valid
assert evt == msg.which()

Expand Down Expand Up @@ -148,17 +153,17 @@ 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()
p.terminate()

# wait 5 socket timeouts before sending
msg = random_carstate()
start_time = time.monotonic()
start_time = time.perf_counter()
delayed_send(sock_timeout*5, pub_sock, msg.to_bytes())
recvd = messaging.recv_one_retry(sub_sock)
assert (time.monotonic() - start_time) >= sock_timeout*5
assert (time.perf_counter() - start_time) >= sock_timeout*5
assert isinstance(recvd, capnp._DynamicStructReader)
assert_carstate(msg.carState, recvd.carState)
4 changes: 2 additions & 2 deletions openpilot/cereal/messaging/tests/test_pub_sub_master.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ def test_update_timeout(self):
sock = random_sock()
sm = messaging.SubMaster([sock,])
timeout = random.randrange(10, 30)
start_time = time.monotonic()
start_time = time.perf_counter()
sm.update(timeout)
t = time.monotonic() - start_time
t = time.perf_counter() - start_time
assert t >= timeout/1000.
assert t < 0.1
assert not any(sm.updated.values())
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"
8 changes: 6 additions & 2 deletions openpilot/common/esim/lpa.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@

import atexit
import base64
import fcntl
import hashlib
import os
import requests
import subprocess
import sys
import termios
import time

if sys.platform == "win32":
fcntl = termios = None # POSIX only; the device modem is unused on a Windows dev build
else:
import fcntl
import termios

from collections.abc import Callable, Generator
from contextlib import contextmanager
from typing import Any
Expand Down
3 changes: 2 additions & 1 deletion openpilot/common/gpio.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os
import fcntl
import ctypes
from functools import cache

Expand Down Expand Up @@ -83,6 +82,8 @@ def gpiochip_get_ro_value_fd(label: str, gpiochip_id: int, pin: int) -> int:
rq.eventflags = GPIOEVENT_REQUEST_BOTH_EDGES
rq.label = label.encode('utf-8')[:31] + b'\0'

import fcntl # POSIX only, keep the module importable on Windows

fd = os.open(f"/dev/gpiochip{gpiochip_id}", os.O_RDONLY)
fcntl.ioctl(fd, GPIO_GET_LINEEVENT_IOCTL, rq)
os.close(fd)
Expand Down
Loading
Loading