Skip to content

Commit 9e9a5b5

Browse files
add raylib package (#31) (#31)
Vendor raylib C library + pyray Python bindings (CFFI) from commaai's raylib and raylib-python-cffi forks. Supports PLATFORM_DESKTOP (X11/GL) and PLATFORM_COMMA (DRM/EGL/GLES) backends, auto-detected via /TICI. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e46683e commit 9e9a5b5

File tree

12 files changed

+1752
-0
lines changed

12 files changed

+1752
-0
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ members = [
1212
"ncurses",
1313
"openssl3",
1414
"python3-dev",
15+
"raylib",
1516
"zeromq",
1617
"zstd",
1718
]

raylib/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
raylib-src/
2+
raylib/install/
3+
raylib/*.modified
4+
raylib/_raylib_cffi*

raylib/build.sh

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
5+
cd "$DIR"
6+
7+
INSTALL_DIR="$DIR/raylib/install"
8+
9+
# Idempotent: skip if already built
10+
if [ -f "$INSTALL_DIR/lib/libraylib.a" ]; then
11+
echo "raylib already present, skipping build."
12+
exit 0
13+
fi
14+
15+
NJOBS="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 2)"
16+
17+
# Detect platform: PLATFORM_COMMA for comma devices, PLATFORM_DESKTOP otherwise
18+
RAYLIB_PLATFORM="${RAYLIB_PLATFORM:-PLATFORM_DESKTOP}"
19+
if [ -f /TICI ]; then
20+
RAYLIB_PLATFORM="PLATFORM_COMMA"
21+
fi
22+
export RAYLIB_PLATFORM
23+
24+
# Install build dependencies
25+
if [[ "$(uname)" == "Linux" ]]; then
26+
if [ "$RAYLIB_PLATFORM" = "PLATFORM_COMMA" ]; then
27+
# comma device: needs DRM/EGL/GLES headers (usually already present on AGNOS)
28+
# apt may fail on devices due to read-only rootfs or package conflicts — that's OK
29+
if command -v apt-get &>/dev/null; then
30+
if [ "$(id -u)" -eq 0 ]; then
31+
apt-get update && apt-get install -y libdrm-dev libgbm-dev libgles2-mesa-dev libegl1-mesa-dev || true
32+
else
33+
sudo apt-get update && sudo apt-get install -y libdrm-dev libgbm-dev libgles2-mesa-dev libegl1-mesa-dev || true
34+
fi
35+
fi
36+
else
37+
# desktop: needs X11/GL dev packages
38+
if command -v dnf &>/dev/null; then
39+
dnf install -y libX11-devel libXcursor-devel libXrandr-devel libXinerama-devel libXi-devel mesa-libGL-devel
40+
elif command -v apt-get &>/dev/null; then
41+
if [ "$(id -u)" -eq 0 ]; then
42+
apt-get update && apt-get install -y libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev libgl-dev
43+
else
44+
sudo apt-get update && sudo apt-get install -y libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev libgl-dev
45+
fi
46+
fi
47+
fi
48+
fi
49+
50+
# Clone and build raylib C library
51+
RAYLIB_COMMIT="aa6ade09ac4bfb2847a356535f2d9f87e49ab089"
52+
53+
if [ ! -d "raylib-src" ]; then
54+
git clone -b master --no-tags https://github.com/commaai/raylib.git raylib-src
55+
fi
56+
57+
cd raylib-src
58+
git fetch origin "$RAYLIB_COMMIT"
59+
git reset --hard "$RAYLIB_COMMIT"
60+
git clean -xdff .
61+
62+
cd src
63+
make -j"$NJOBS" PLATFORM="$RAYLIB_PLATFORM"
64+
65+
cd "$DIR"
66+
67+
# Install lib + headers
68+
rm -rf "$INSTALL_DIR"
69+
mkdir -p "$INSTALL_DIR"/{lib,include}
70+
71+
cp raylib-src/src/libraylib.a "$INSTALL_DIR/lib/"
72+
cp raylib-src/src/raylib.h raylib-src/src/raymath.h raylib-src/src/rlgl.h "$INSTALL_DIR/include/"
73+
74+
# Download raygui header
75+
RAYGUI_COMMIT="76b36b597edb70ffaf96f046076adc20d67e7827"
76+
curl -fsSLo "$INSTALL_DIR/include/raygui.h" \
77+
"https://raw.githubusercontent.com/raysan5/raygui/$RAYGUI_COMMIT/src/raygui.h"
78+
79+
# Clean up source
80+
rm -rf raylib-src
81+
82+
echo "Installed raylib to $INSTALL_DIR"
83+
du -sh "$INSTALL_DIR"

raylib/pyproject.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[build-system]
2+
requires = ["setuptools>=64", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "raylib"
7+
version = "5.5.0.2"
8+
description = "raylib + pyray Python bindings (commaai fork)"
9+
requires-python = ">=3.8"
10+
dependencies = ["cffi>=1.17.1"]
11+
12+
[tool.setuptools.packages.find]
13+
include = ["raylib*", "pyray*"]
14+
15+
[tool.setuptools.package-data]
16+
raylib = ["install/**/*"]

raylib/pyray/__init__.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Copyright (c) 2021 Richard Smith and others
2+
#
3+
# This program and the accompanying materials are made available under the
4+
# terms of the Eclipse Public License 2.0 which is available at
5+
# http://www.eclipse.org/legal/epl-2.0.
6+
#
7+
# This Source Code may also be made available under the following Secondary
8+
# licenses when the conditions for such availability set forth in the Eclipse
9+
# Public License, v. 2.0 are satisfied: GNU General Public License, version 2
10+
# with the GNU Classpath Exception which is
11+
# available at https://www.gnu.org/software/classpath/license.html.
12+
#
13+
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
14+
import re
15+
import weakref
16+
from array import array
17+
18+
from raylib import rl, ffi
19+
from raylib.colors import *
20+
21+
try:
22+
from raylib.defines import *
23+
except AttributeError:
24+
print("sorry deprecated enums dont work on dynamic version")
25+
26+
from inspect import getmembers, isbuiltin
27+
28+
current_module = __import__(__name__)
29+
30+
31+
def _underscore(word: str) -> str:
32+
word = re.sub('2D$', '_2d', word)
33+
word = re.sub('3D$', '_3d', word)
34+
word = re.sub(r"([A-Z]+)([A-Z][a-z])", r'\1_\2', word)
35+
word = re.sub(r"([a-z\d])([A-Z])", r'\1_\2', word)
36+
word = word.replace("-", "_")
37+
return word.lower()
38+
39+
40+
def _wrap_function(original_func):
41+
c_args = [str(x) for x in ffi.typeof(original_func).args]
42+
number_of_args = len(c_args)
43+
c_arg_is_pointer = [x.kind == 'pointer' for x in ffi.typeof(original_func).args]
44+
c_arg_is_string = [str(x) == "<ctype 'char *'>" for x in ffi.typeof(original_func).args]
45+
# c_arg_is_void_pointer = [str(x) == "<ctype 'void *'>" for x in ffi.typeof(original_func).args]
46+
47+
def wrapped_func(*args):
48+
args = list(args) # tuple is immutable, converting it to mutable list is faster than constructing new list!
49+
for i in range(number_of_args):
50+
try:
51+
arg = args[i]
52+
except IndexError:
53+
raise RuntimeError(f"function requires {number_of_args} arguments but you supplied {len(args)}")
54+
if c_arg_is_pointer[i]:
55+
if c_arg_is_string[i]: # we assume c_arg is 'const char *'
56+
try: # if it's a non-const 'char *' then user should be supplying a ctype pointer, not a Python
57+
# string
58+
args[i] = arg.encode('utf-8') # in that case this conversion will fail
59+
except AttributeError: # but those functions are uncommon, so quicker on average to try the
60+
# conversion
61+
pass # and ignore the exception
62+
# if user supplied a Python string but c_arg is a 'char *' not a 'const char *' then we ought to raise
63+
# exception because its an out
64+
# parameter and user should supply a ctype pointer, but we cant because cffi cant detect 'const'
65+
# so we would have to get the info from raylib.json
66+
elif c_args[i] == "<ctype 'char * *'>" and type(arg) is list:
67+
args[i] = [ffi.new("char[]", x.encode('utf-8')) for x in arg]
68+
elif is_cdata(arg) and "*" not in str(arg):
69+
args[i] = ffi.addressof(arg)
70+
elif arg is None:
71+
args[i] = ffi.NULL
72+
elif not is_cdata(arg):
73+
if c_args[i] == "<ctype '_Bool *'>":
74+
raise TypeError(
75+
f"Argument {i} ({arg}) must be a ctype bool, please create one with: pyray.ffi.new('bool "
76+
f"*', True)")
77+
elif c_args[i] == "<ctype 'int *'>":
78+
raise TypeError(
79+
f"Argument {i} ({arg}) must be a ctype int, please create one with: pyray.ffi.new('int "
80+
f"*', 1)")
81+
elif c_args[i] == "<ctype 'float *'>":
82+
raise TypeError(
83+
f"Argument {i} ({arg}) must be a ctype float, please create one with: pyray.ffi.new("
84+
f"'float *', 1.0)")
85+
elif c_args[i] == "<ctype 'void *'>":
86+
# we could assume it's a string and try to convert it but we would have to be sure it's
87+
# const. that seems reasonable assumption for char* but i'm not confident it is for void*
88+
raise TypeError(
89+
f"Argument {i} ({arg}) must be a cdata pointer. Type is void so I don't know what type it "
90+
f"should be."
91+
"If it's a const string you can create it with pyray.ffi.new('char []', b\"whatever\") . "
92+
"If it's a float you can create it with pyray.ffi.new('float *', 1.0)")
93+
94+
result = original_func(*args)
95+
if result is None:
96+
return
97+
elif is_cdata(result) and str(result).startswith("<cdata 'char *'"):
98+
if str(result) == "<cdata 'char *' NULL>":
99+
return ""
100+
else:
101+
return ffi.string(result).decode('utf-8')
102+
else:
103+
return result
104+
105+
# apparently pypy and cpython produce different types so check for both
106+
def is_cdata(arg):
107+
return str(type(arg)) == "<class '_cffi_backend.__CDataOwn'>" or str(
108+
type(arg)) == "<class '_cffi_backend._CDataBase'>"
109+
110+
return wrapped_func
111+
112+
113+
global_weakkeydict = weakref.WeakKeyDictionary()
114+
115+
116+
def _make_struct_constructor_function(struct):
117+
def func(*args):
118+
# print(struct, args)
119+
modified_args = []
120+
for (field, arg) in zip(ffi.typeof(struct).fields, args):
121+
# print("arg:", str(arg), "field:", field[1], "field type:", field[1].type, "type(arg):", str(type(arg)))
122+
if arg is None:
123+
arg = ffi.NULL
124+
elif (field[1].type.kind == 'pointer'
125+
and (str(type(arg)) == "<class 'numpy.ndarray'>"
126+
or isinstance(arg, (array, bytes, bytearray, memoryview)))):
127+
arg = ffi.from_buffer(field[1].type, arg)
128+
modified_args.append(arg)
129+
s = ffi.new(f"struct {struct} *", modified_args)[0]
130+
global_weakkeydict[s] = modified_args
131+
return s
132+
133+
return func
134+
135+
136+
for name, attr in getmembers(rl):
137+
# print(name, attr)
138+
uname = _underscore(name)
139+
if isbuiltin(attr) or str(type(attr)) == "<class '_cffi_backend.__FFIFunctionWrapper'>" or str(
140+
type(attr)) == "<class '_cffi_backend._CDataBase'>":
141+
# print(attr.__call__)
142+
# print(attr.__doc__)
143+
# print(dir(attr))
144+
# print(dir(attr.__repr__))
145+
f = _wrap_function(attr)
146+
setattr(current_module, uname, f)
147+
else:
148+
setattr(current_module, name, attr)
149+
150+
for struct in ffi.list_types()[0]:
151+
f = _make_struct_constructor_function(struct)
152+
setattr(current_module, struct, f)
153+
154+
# overwrite ffi enums with our own
155+
from raylib.enums import *
156+
157+
158+
def text_format(*args):
159+
raise RuntimeError("Use Python f-strings etc rather than calling text_format().")

raylib/raylib/__init__.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import os
2+
3+
DIR = os.path.join(os.path.dirname(__file__), "install")
4+
LIB_DIR = os.path.join(DIR, "lib")
5+
INCLUDE_DIR = os.path.join(DIR, "include")
6+
7+
8+
def smoketest():
9+
assert os.path.isfile(os.path.join(LIB_DIR, "libraylib.a")), "libraylib.a not found"
10+
assert os.path.isfile(os.path.join(INCLUDE_DIR, "raylib.h")), "raylib.h not found"
11+
12+
13+
# Build CFFI extension on first import if not already compiled
14+
def _ensure_cffi_built():
15+
import glob
16+
import subprocess
17+
import sys
18+
pkg_dir = os.path.dirname(__file__)
19+
if not glob.glob(os.path.join(pkg_dir, "_raylib_cffi*")):
20+
build_script = os.path.join(pkg_dir, "build.py")
21+
if os.path.isfile(build_script) and os.path.isfile(os.path.join(LIB_DIR, "libraylib.a")):
22+
try:
23+
subprocess.check_call([sys.executable, build_script], cwd=os.path.dirname(pkg_dir))
24+
except subprocess.CalledProcessError:
25+
pass
26+
27+
_ensure_cffi_built()
28+
29+
# CFFI bindings (available when graphics libraries are present)
30+
try:
31+
from ._raylib_cffi import ffi, lib as rl
32+
from raylib._raylib_cffi.lib import * # noqa: F403
33+
from raylib.colors import * # noqa: F403
34+
from raylib.defines import * # noqa: F403
35+
from .version import __version__
36+
except (ImportError, OSError):
37+
pass

0 commit comments

Comments
 (0)