update SDL_mixer to SDL3_mixer 3.2.0
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Simple DirectMedia Layer
|
||||
# Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
|
||||
#
|
||||
# This software is provided 'as-is', without any express or implied
|
||||
# warranty. In no event will the authors be held liable for any damages
|
||||
# arising from the use of this software.
|
||||
#
|
||||
# Permission is granted to anyone to use this software for any purpose,
|
||||
# including commercial applications, and to alter it and redistribute it
|
||||
# freely, subject to the following restrictions:
|
||||
#
|
||||
# 1. The origin of this software must not be misrepresented; you must not
|
||||
# claim that you wrote the original software. If you use this software
|
||||
# in a product, an acknowledgment in the product documentation would be
|
||||
# appreciated but is not required.
|
||||
# 2. Altered source versions must be plainly marked as such, and must not be
|
||||
# misrepresented as being the original software.
|
||||
# 3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
# WHAT IS THIS?
|
||||
# When you add a public API to a SDL library, please run this script, make sure the
|
||||
# output looks sane (git diff, it adds to existing files), and commit it.
|
||||
# It keeps the export lists in sync with the API.
|
||||
#
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import pprint
|
||||
import re
|
||||
|
||||
RE_EXTERN_C = re.compile(r'.*extern[ "]*C[ "].*')
|
||||
RE_COMMENT_REMOVE_CONTENT = re.compile(r'\/\*.*\*/')
|
||||
RE_PARSING_FUNCTION = re.compile(r'(.*SDLCALL[^\(\)]*) ([a-zA-Z0-9_]+) *\((.*)\) *;.*')
|
||||
|
||||
#eg:
|
||||
# void (SDLCALL *callback)(void*, int)
|
||||
# \1(\2)\3
|
||||
RE_PARSING_CALLBACK = re.compile(r'([^\(\)]*)\(([^\(\)]+)\)(.*)')
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class SdlProjectSymbolProperties:
|
||||
include_dir: Path
|
||||
version_export_path: Path
|
||||
macos_exports_path: Path
|
||||
re_symbol: str
|
||||
|
||||
@classmethod
|
||||
def from_root(cls, project_root: Path) -> "SdlProjectSymbolProperties":
|
||||
wikiheaders_options = {}
|
||||
with (project_root / ".wikiheaders-options").open("r", newline="\n") as f:
|
||||
for line in f.readlines():
|
||||
key, value = line.split("=", 1)
|
||||
key, value = key.strip(), value.strip()
|
||||
wikiheaders_options[key] = value
|
||||
return cls(
|
||||
include_dir=project_root / wikiheaders_options["incsubdir"],
|
||||
version_export_path=project_root / "src" / (wikiheaders_options["projectfullname"] + ".sym"),
|
||||
macos_exports_path=project_root / "src" / (wikiheaders_options["projectfullname"] + ".exports"),
|
||||
re_symbol= wikiheaders_options["apiprefixregex"],
|
||||
)
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class SdlProcedure:
|
||||
retval: str
|
||||
name: str
|
||||
parameter: list[str]
|
||||
parameter_name: list[str]
|
||||
header: str
|
||||
comment: str
|
||||
|
||||
@property
|
||||
def variadic(self) -> bool:
|
||||
return "..." in self.parameter
|
||||
|
||||
|
||||
def parse_header(header_path: Path) -> list[SdlProcedure]:
|
||||
logger.debug("Parse header: %s", header_path)
|
||||
|
||||
header_procedures = []
|
||||
|
||||
parsing_function = False
|
||||
current_func = ""
|
||||
parsing_comment = False
|
||||
current_comment = ""
|
||||
ignore_wiki_documentation = False
|
||||
|
||||
with header_path.open() as f:
|
||||
for line in f:
|
||||
|
||||
# Skip lines if we're in a wiki documentation block.
|
||||
if ignore_wiki_documentation:
|
||||
if line.startswith("#endif"):
|
||||
ignore_wiki_documentation = False
|
||||
continue
|
||||
|
||||
# Discard wiki documentations blocks.
|
||||
if line.startswith("#ifdef SDL_WIKI_DOCUMENTATION_SECTION"):
|
||||
ignore_wiki_documentation = True
|
||||
continue
|
||||
|
||||
# Discard pre-processor directives ^#.*
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
|
||||
# Discard "extern C" line
|
||||
match = RE_EXTERN_C.match(line)
|
||||
if match:
|
||||
continue
|
||||
|
||||
# Remove one line comment // ...
|
||||
# eg: extern SDL_DECLSPEC SDL_hid_device * SDLCALL SDL_hid_open_path(const char *path, int bExclusive /* = false */)
|
||||
line = RE_COMMENT_REMOVE_CONTENT.sub('', line)
|
||||
|
||||
# Get the comment block /* ... */ across several lines
|
||||
match_start = "/*" in line
|
||||
match_end = "*/" in line
|
||||
if match_start and match_end:
|
||||
continue
|
||||
if match_start:
|
||||
parsing_comment = True
|
||||
current_comment = line
|
||||
continue
|
||||
if match_end:
|
||||
parsing_comment = False
|
||||
current_comment += line
|
||||
continue
|
||||
if parsing_comment:
|
||||
current_comment += line
|
||||
continue
|
||||
|
||||
# Get the function prototype across several lines
|
||||
if parsing_function:
|
||||
# Append to the current function
|
||||
current_func += " "
|
||||
current_func += line.strip()
|
||||
else:
|
||||
# if is contains "extern", start grabbing
|
||||
if "extern" not in line:
|
||||
continue
|
||||
# Start grabbing the new function
|
||||
current_func = line.strip()
|
||||
parsing_function = True
|
||||
|
||||
# If it contains ';', then the function is complete
|
||||
if ";" not in current_func:
|
||||
continue
|
||||
|
||||
# Got function/comment, reset vars
|
||||
parsing_function = False
|
||||
func = current_func
|
||||
comment = current_comment
|
||||
current_func = ""
|
||||
current_comment = ""
|
||||
|
||||
# Discard if it doesn't contain 'SDLCALL'
|
||||
if "SDLCALL" not in func:
|
||||
logger.debug(" Discard, doesn't have SDLCALL: %r", func)
|
||||
continue
|
||||
|
||||
# Discard if it contains 'SDLMAIN_DECLSPEC' (these are not SDL symbols).
|
||||
if "SDLMAIN_DECLSPEC" in func:
|
||||
logger.debug(" Discard, has SDLMAIN_DECLSPEC: %r", func)
|
||||
continue
|
||||
|
||||
logger.debug("Raw data: %r", func)
|
||||
|
||||
# Replace unusual stuff...
|
||||
func = func.replace(" SDL_PRINTF_VARARG_FUNC(1)", "")
|
||||
func = func.replace(" SDL_PRINTF_VARARG_FUNC(2)", "")
|
||||
func = func.replace(" SDL_PRINTF_VARARG_FUNC(3)", "")
|
||||
func = func.replace(" SDL_PRINTF_VARARG_FUNC(4)", "")
|
||||
func = func.replace(" SDL_PRINTF_VARARG_FUNCV(1)", "")
|
||||
func = func.replace(" SDL_PRINTF_VARARG_FUNCV(2)", "")
|
||||
func = func.replace(" SDL_PRINTF_VARARG_FUNCV(3)", "")
|
||||
func = func.replace(" SDL_PRINTF_VARARG_FUNCV(4)", "")
|
||||
func = func.replace(" SDL_WPRINTF_VARARG_FUNC(3)", "")
|
||||
func = func.replace(" SDL_WPRINTF_VARARG_FUNCV(3)", "")
|
||||
func = func.replace(" SDL_SCANF_VARARG_FUNC(2)", "")
|
||||
func = func.replace(" SDL_SCANF_VARARG_FUNCV(2)", "")
|
||||
func = func.replace(" SDL_ANALYZER_NORETURN", "")
|
||||
func = func.replace(" SDL_MALLOC", "")
|
||||
func = func.replace(" SDL_ALLOC_SIZE2(1, 2)", "")
|
||||
func = func.replace(" SDL_ALLOC_SIZE(2)", "")
|
||||
func = re.sub(r" SDL_ACQUIRE\(.*\)", "", func)
|
||||
func = re.sub(r" SDL_ACQUIRE_SHARED\(.*\)", "", func)
|
||||
func = re.sub(r" SDL_TRY_ACQUIRE\(.*\)", "", func)
|
||||
func = re.sub(r" SDL_TRY_ACQUIRE_SHARED\(.*\)", "", func)
|
||||
func = re.sub(r" SDL_RELEASE\(.*\)", "", func)
|
||||
func = re.sub(r" SDL_RELEASE_SHARED\(.*\)", "", func)
|
||||
func = re.sub(r" SDL_RELEASE_GENERIC\(.*\)", "", func)
|
||||
func = re.sub(r"([ (),])(SDL_IN_BYTECAP\([^)]*\))", r"\1", func)
|
||||
func = re.sub(r"([ (),])(SDL_OUT_BYTECAP\([^)]*\))", r"\1", func)
|
||||
func = re.sub(r"([ (),])(SDL_INOUT_Z_CAP\([^)]*\))", r"\1", func)
|
||||
func = re.sub(r"([ (),])(SDL_OUT_Z_CAP\([^)]*\))", r"\1", func)
|
||||
|
||||
# Should be a valid function here
|
||||
match = RE_PARSING_FUNCTION.match(func)
|
||||
if not match:
|
||||
logger.error("Cannot parse: %s", func)
|
||||
raise ValueError(func)
|
||||
|
||||
func_ret = match.group(1)
|
||||
func_name = match.group(2)
|
||||
func_params = match.group(3)
|
||||
|
||||
#
|
||||
# Parse return value
|
||||
#
|
||||
func_ret = func_ret.replace('extern', ' ')
|
||||
func_ret = func_ret.replace('SDLCALL', ' ')
|
||||
func_ret = func_ret.replace('SDL_DECLSPEC', ' ')
|
||||
func_ret, _ = re.subn('([ ]{2,})', ' ', func_ret)
|
||||
# Remove trailing spaces in front of '*'
|
||||
func_ret = func_ret.replace(' *', '*')
|
||||
func_ret = func_ret.strip()
|
||||
|
||||
#
|
||||
# Parse parameters
|
||||
#
|
||||
func_params = func_params.strip()
|
||||
if func_params == "":
|
||||
func_params = "void"
|
||||
|
||||
# Identify each function parameters with type and name
|
||||
# (eventually there are callbacks of several parameters)
|
||||
tmp = func_params.split(',')
|
||||
tmp2 = []
|
||||
param = ""
|
||||
for t in tmp:
|
||||
if param == "":
|
||||
param = t
|
||||
else:
|
||||
param = param + "," + t
|
||||
# Identify a callback or parameter when there is same count of '(' and ')'
|
||||
if param.count('(') == param.count(')'):
|
||||
tmp2.append(param.strip())
|
||||
param = ""
|
||||
|
||||
# Process each parameters, separation name and type
|
||||
func_param_type = []
|
||||
func_param_name = []
|
||||
for t in tmp2:
|
||||
if t == "void":
|
||||
func_param_type.append(t)
|
||||
func_param_name.append("")
|
||||
continue
|
||||
|
||||
if t == "...":
|
||||
func_param_type.append(t)
|
||||
func_param_name.append("")
|
||||
continue
|
||||
|
||||
param_name = ""
|
||||
|
||||
# parameter is a callback
|
||||
if '(' in t:
|
||||
match = RE_PARSING_CALLBACK.match(t)
|
||||
if not match:
|
||||
logger.error("cannot parse callback: %s", t)
|
||||
raise ValueError(t)
|
||||
a = match.group(1).strip()
|
||||
b = match.group(2).strip()
|
||||
c = match.group(3).strip()
|
||||
|
||||
try:
|
||||
(param_type, param_name) = b.rsplit('*', 1)
|
||||
except:
|
||||
param_type = t
|
||||
param_name = "param_name_not_specified"
|
||||
|
||||
# bug rsplit ??
|
||||
if param_name == "":
|
||||
param_name = "param_name_not_specified"
|
||||
|
||||
# reconstruct a callback name for future parsing
|
||||
func_param_type.append(a + " (" + param_type.strip() + " *REWRITE_NAME)" + c)
|
||||
func_param_name.append(param_name.strip())
|
||||
|
||||
continue
|
||||
|
||||
# array like "char *buf[]"
|
||||
has_array = False
|
||||
if t.endswith("[]"):
|
||||
t = t.replace("[]", "")
|
||||
has_array = True
|
||||
|
||||
# pointer
|
||||
if '*' in t:
|
||||
try:
|
||||
(param_type, param_name) = t.rsplit('*', 1)
|
||||
except:
|
||||
param_type = t
|
||||
param_name = "param_name_not_specified"
|
||||
|
||||
# bug rsplit ??
|
||||
if param_name == "":
|
||||
param_name = "param_name_not_specified"
|
||||
|
||||
val = param_type.strip() + "*REWRITE_NAME"
|
||||
|
||||
# Remove trailing spaces in front of '*'
|
||||
tmp = ""
|
||||
while val != tmp:
|
||||
tmp = val
|
||||
val = val.replace(' ', ' ')
|
||||
val = val.replace(' *', '*')
|
||||
# first occurrence
|
||||
val = val.replace('*', ' *', 1)
|
||||
val = val.strip()
|
||||
|
||||
else: # non pointer
|
||||
# cut-off last word on
|
||||
try:
|
||||
(param_type, param_name) = t.rsplit(' ', 1)
|
||||
except:
|
||||
param_type = t
|
||||
param_name = "param_name_not_specified"
|
||||
|
||||
val = param_type.strip() + " REWRITE_NAME"
|
||||
|
||||
# set back array
|
||||
if has_array:
|
||||
val += "[]"
|
||||
|
||||
func_param_type.append(val)
|
||||
func_param_name.append(param_name.strip())
|
||||
|
||||
new_proc = SdlProcedure(
|
||||
retval=func_ret, # Return value type
|
||||
name=func_name, # Function name
|
||||
comment=comment, # Function comment
|
||||
header=header_path.name, # Header file
|
||||
parameter=func_param_type, # List of parameters (type + anonymized param name 'REWRITE_NAME')
|
||||
parameter_name=func_param_name, # Real parameter name, or 'param_name_not_specified'
|
||||
)
|
||||
|
||||
header_procedures.append(new_proc)
|
||||
|
||||
if logger.getEffectiveLevel() <= logging.DEBUG:
|
||||
logger.debug("%s", pprint.pformat(new_proc))
|
||||
|
||||
return header_procedures
|
||||
|
||||
|
||||
# Dump API into a json file
|
||||
def full_API_json(path: Path, procedures: list[SdlProcedure]):
|
||||
with path.open('w', newline='') as f:
|
||||
json.dump([dataclasses.asdict(proc) for proc in procedures], f, indent=4, sort_keys=True)
|
||||
logger.info("dump API to '%s'", path)
|
||||
|
||||
|
||||
class CallOnce:
|
||||
def __init__(self, cb):
|
||||
self._cb = cb
|
||||
self._called = False
|
||||
def __call__(self, *args, **kwargs):
|
||||
if self._called:
|
||||
return
|
||||
self._called = True
|
||||
self._cb(*args, **kwargs)
|
||||
|
||||
|
||||
# Check public function comments are correct
|
||||
def print_check_comment_header():
|
||||
logger.warning("")
|
||||
logger.warning("Please fix following warning(s):")
|
||||
logger.warning("--------------------------------")
|
||||
|
||||
|
||||
def check_documentations(procedures: list[SdlProcedure]) -> None:
|
||||
|
||||
check_comment_header = CallOnce(print_check_comment_header)
|
||||
|
||||
warning_header_printed = False
|
||||
|
||||
# Check \param
|
||||
for proc in procedures:
|
||||
expected = len(proc.parameter)
|
||||
if expected == 1:
|
||||
if proc.parameter[0] == 'void':
|
||||
expected = 0
|
||||
count = proc.comment.count("\\param")
|
||||
if count != expected:
|
||||
# skip SDL_stdinc.h
|
||||
if proc.header != 'SDL_stdinc.h':
|
||||
# Warning mismatch \param and function prototype
|
||||
check_comment_header()
|
||||
logger.warning(" In file %s: function %s() has %d '\\param' but expected %d", proc.header, proc.name, count, expected)
|
||||
|
||||
# Warning check \param uses the correct parameter name
|
||||
# skip SDL_stdinc.h
|
||||
if proc.header != 'SDL_stdinc.h':
|
||||
for n in proc.parameter_name:
|
||||
if n != "" and "\\param " + n not in proc.comment and "\\param[out] " + n not in proc.comment:
|
||||
check_comment_header()
|
||||
logger.warning(" In file %s: function %s() missing '\\param %s'", proc.header, proc.name, n)
|
||||
|
||||
# Check \returns
|
||||
for proc in procedures:
|
||||
expected = 1
|
||||
if proc.retval == 'void':
|
||||
expected = 0
|
||||
|
||||
count = proc.comment.count("\\returns")
|
||||
if count != expected:
|
||||
# skip SDL_stdinc.h
|
||||
if proc.header != 'SDL_stdinc.h':
|
||||
# Warning mismatch \param and function prototype
|
||||
check_comment_header()
|
||||
logger.warning(" In file %s: function %s() has %d '\\returns' but expected %d" % (proc.header, proc.name, count, expected))
|
||||
|
||||
# Check \since
|
||||
for proc in procedures:
|
||||
expected = 1
|
||||
count = proc.comment.count("\\since")
|
||||
if count != expected:
|
||||
# skip SDL_stdinc.h
|
||||
if proc.header != 'SDL_stdinc.h':
|
||||
# Warning mismatch \param and function prototype
|
||||
check_comment_header()
|
||||
logger.warning(" In file %s: function %s() has %d '\\since' but expected %d" % (proc.header, proc.name, count, expected))
|
||||
|
||||
|
||||
# Parse 'sdl_dynapi_procs_h' file to find existing functions
|
||||
def find_existing_proc_names(project_properties: SdlProjectSymbolProperties) -> set[str]:
|
||||
versioned_symbols = set()
|
||||
re_version_export_symbol = re.compile(r'\s*(' + project_properties.re_symbol + r"[a-zA-Z0-9_]+);\s*")
|
||||
with project_properties.version_export_path.open() as f:
|
||||
for line in f:
|
||||
match = re_version_export_symbol.match(line)
|
||||
if not match:
|
||||
continue
|
||||
existing_func = match.group(1)
|
||||
versioned_symbols.add(existing_func)
|
||||
logger.debug("symbols from version script: %r", versioned_symbols)
|
||||
|
||||
macos_symbols = set()
|
||||
re_macos_export_symbol = re.compile(r'\s*_(' + project_properties.re_symbol + r"[a-zA-Z0-9_]+)\s*")
|
||||
with project_properties.macos_exports_path.open() as f:
|
||||
for line in f:
|
||||
match = re_macos_export_symbol.match(line)
|
||||
if not match:
|
||||
continue
|
||||
existing_func = match.group(1)
|
||||
macos_symbols.add(existing_func)
|
||||
logger.debug("symbols from macos exports file: %r", macos_symbols)
|
||||
|
||||
non_matching_symbols = (versioned_symbols - macos_symbols).union(macos_symbols - versioned_symbols)
|
||||
if non_matching_symbols:
|
||||
logger.error("Following symbols do not match: %r", non_matching_symbols)
|
||||
raise RuntimeError("Non-matching symbols", non_matching_symbols)
|
||||
return versioned_symbols
|
||||
|
||||
# Get list of SDL headers
|
||||
def get_header_list(project_properties: SdlProjectSymbolProperties) -> list[Path]:
|
||||
ret = []
|
||||
|
||||
for f in project_properties.include_dir.iterdir():
|
||||
# Only *.h files
|
||||
if f.is_file() and f.suffix == ".h":
|
||||
ret.append(f)
|
||||
else:
|
||||
logger.debug("Skip %s", f)
|
||||
|
||||
# Order headers for reproducible behavior
|
||||
ret.sort()
|
||||
|
||||
return ret
|
||||
|
||||
# Write the new API in files: _procs.h _overrides.h and .sym
|
||||
def add_dyn_api(proc: SdlProcedure, project_properties:SdlProjectSymbolProperties) -> None:
|
||||
decl_args: list[str] = []
|
||||
call_args = []
|
||||
for i, argtype in enumerate(proc.parameter):
|
||||
# Special case, void has no parameter name
|
||||
if argtype == "void":
|
||||
assert len(decl_args) == 0
|
||||
assert len(proc.parameter) == 1
|
||||
decl_args.append("void")
|
||||
continue
|
||||
|
||||
# Var name: a, b, c, ...
|
||||
varname = chr(ord('a') + i)
|
||||
|
||||
decl_args.append(argtype.replace("REWRITE_NAME", varname))
|
||||
if argtype != "...":
|
||||
call_args.append(varname)
|
||||
|
||||
macro_args = (
|
||||
proc.retval,
|
||||
proc.name,
|
||||
"({})".format(",".join(decl_args)),
|
||||
"({})".format(",".join(call_args)),
|
||||
"" if proc.retval == "void" else "return",
|
||||
)
|
||||
|
||||
# File: SDL_{image,mixer,net,ttf}.sym
|
||||
#
|
||||
# Add before "extra symbols go here" line
|
||||
with project_properties.version_export_path.open(newline="\n") as f:
|
||||
new_input = []
|
||||
for line in f:
|
||||
if "extra symbols go here" in line:
|
||||
new_input.append(f" {proc.name};\n")
|
||||
new_input.append(line)
|
||||
|
||||
with project_properties.version_export_path.open('w', newline='') as f:
|
||||
for line in new_input:
|
||||
f.write(line)
|
||||
|
||||
# File: SDL_{image,mixer,net,ttf}.exports
|
||||
#
|
||||
# Add before "extra symbols go here" line
|
||||
with project_properties.macos_exports_path.open(newline="\n") as f:
|
||||
new_input = []
|
||||
for line in f:
|
||||
if "extra symbols go here" in line:
|
||||
new_input.append(f"_{proc.name}\n")
|
||||
new_input.append(line)
|
||||
|
||||
with project_properties.macos_exports_path.open("w", newline="\n") as f:
|
||||
for line in new_input:
|
||||
f.write(line)
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.set_defaults(loglevel=logging.INFO)
|
||||
parser.add_argument('--dump', nargs='?', default=None, const="sdl.json", metavar="JSON", help='output all API into a .json file')
|
||||
parser.add_argument('--debug', action='store_const', const=logging.DEBUG, dest="loglevel", help='add debug traces')
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=args.loglevel, format='[%(levelname)s] %(message)s')
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
|
||||
project_properties = SdlProjectSymbolProperties.from_root(root)
|
||||
logger.debug("project_properties=%r", project_properties)
|
||||
|
||||
# Get list of SDL headers
|
||||
sdl_list_includes = get_header_list(project_properties)
|
||||
procedures = []
|
||||
for filename in sdl_list_includes:
|
||||
header_procedures = parse_header(filename)
|
||||
procedures.extend(header_procedures)
|
||||
|
||||
# Parse 'SDL_{image,mixer,ttf}.sym' file to find existing functions
|
||||
existing_proc_names = find_existing_proc_names(project_properties)
|
||||
for procedure in procedures:
|
||||
if procedure.name not in existing_proc_names:
|
||||
logger.info("NEW %s", procedure.name)
|
||||
add_dyn_api(procedure, project_properties)
|
||||
|
||||
if args.dump:
|
||||
# Dump API into a json file
|
||||
full_API_json(path=Path(args.dump), procedures=procedures)
|
||||
|
||||
# Check comment formatting
|
||||
check_documentations(procedures)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user