From 8353c370479f9a6b4d711807b12f55ee37709d03 Mon Sep 17 00:00:00 2001 From: Yan Pujante Date: Thu, 8 Feb 2024 12:07:37 -0800 Subject: [PATCH] Add options support to contrib ports (#21276) --- ChangeLog.md | 6 ++- .../docs/compiling/Building-Projects.rst | 27 +++++++++-- test/test_browser.py | 2 +- test/test_other.py | 18 ++++++++ tools/ports/__init__.py | 30 ++++++++++++- tools/ports/contrib/README.md | 26 +++++++++++ tools/ports/contrib/glfw3.py | 45 +++++++++++++++++-- tools/ports/sdl2_image.py | 34 +++++++++++--- 8 files changed, 169 insertions(+), 19 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index d7637df843cd1..8ce86f8021de0 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -37,9 +37,11 @@ See docs/process.md for more on how version tagging works. POINTER_SIZE }}}` and `{{{ makeGetValue(..) }}}` to be used in pre/post JS files, just like they can be in JS library files. (#21227) - Added concept of contrib ports which are ports contributed by the wider - community and supported on a "best effort" basis. A first contrib port is + community and supported on a "best effort" basis. See + `tools/ports/contrib/README.md` for details.A first contrib port is available via `--use-port=contrib.glfw3`: an emscripten port of glfw written - in C++ with many features like support for multiple windows. (#21244) + in C++ with many features like support for multiple windows. (#21244 and + #21276) 3.1.53 - 01/29/24 diff --git a/site/source/docs/compiling/Building-Projects.rst b/site/source/docs/compiling/Building-Projects.rst index 46a306f14d613..eddd406aae396 100644 --- a/site/source/docs/compiling/Building-Projects.rst +++ b/site/source/docs/compiling/Building-Projects.rst @@ -225,11 +225,30 @@ You should see some notifications about SDL2 being used, and built if it wasn't To see a list of all available ports, run ``emcc --show-ports``. -.. note:: *SDL_image* has also been added to ports, use it with ``--use-port=sdl2_image``. For ``sdl2_image`` to be useful, you generally need to specify the image formats you are planning on using with e.g. ``-sSDL2_IMAGE_FORMATS='["bmp","png","xpm","jpg"]'``. This will also ensure that ``IMG_Init`` works properly when you specify those formats. Alternatively, you can use ``emcc --use-preload-plugins`` and ``--preload-file`` your images, so the browser codecs decode them (see :ref:`preloading-files`). A code path in the ``sdl2_image`` port will load through :c:func:`emscripten_get_preloaded_image_data`, but then your calls to ``IMG_Init`` with those image formats will fail (as while the images will work through preloading, IMG_Init reports no support for those formats, as it doesn't have support compiled in - in other words, IMG_Init does not report support for formats that only work through preloading).``` +.. note:: *SDL_image* has also been added to ports, use it with + ``--use-port=sdl2_image``. For ``sdl2_image`` to be useful, you generally + need to specify the image formats you are planning on using with e.g. + ``--use-port=sdl2_image:formats=bmp,png,xpm,jpg``. This will also ensure that + ``IMG_Init`` works properly when you specify those formats. Alternatively, + you can use ``emcc --use-preload-plugins`` and ``--preload-file`` your + images, so the browser codecs decode them (see :ref:`preloading-files`). + A code path in the ``sdl2_image`` port will load through + :c:func:`emscripten_get_preloaded_image_data`, but then your calls to + ``IMG_Init`` with those image formats will fail (as while the images will + work through preloading, IMG_Init reports no support for those formats, as + it doesn't have support compiled in - in other words, ``IMG_Init`` does not + report support for formats that only work through preloading). .. note:: *SDL_net* has also been added to ports, use it with ``--use-port=sdl2_net``. -.. note:: Emscripten also has support for older SDL1, which is built-in. If you do not specify SDL2 as in the command above, then SDL1 is linked in and the SDL1 include paths are used. SDL1 has support for *sdl-config*, which is present in `system/bin `_. Using the native *sdl-config* may result in compilation or missing-symbol errors. You will need to modify the build system to look for files in **emscripten/system** or **emscripten/system/bin** in order to use the Emscripten *sdl-config*. +.. note:: Emscripten also has support for older SDL1, which is built-in. + If you do not specify SDL2 as in the command above, then SDL1 is linked in + and the SDL1 include paths are used. SDL1 has support for *sdl-config*, + which is present in `system/bin `_. + Using the native *sdl-config* may result in compilation or missing-symbol errors. + You will need to modify the build system to look for files in + **emscripten/system** or **emscripten/system/bin** in order to use the + Emscripten *sdl-config*. .. note:: You can also build a library from ports in a manual way if you prefer that, but then you will need to also apply the python logic that ports does. @@ -238,7 +257,9 @@ To see a list of all available ports, run ``emcc --show-ports``. it's better to use the ports version as it is what is tested and known to work. -.. note:: Since emscripten 3.1.54, ``--use-port`` is the preferred syntax to use a port in your project. The legacy syntax (for example ``-sUSE_SDL2``, ``-sUSE_SDL_IMAGE=2``) remains available. +.. note:: Since emscripten 3.1.54, ``--use-port`` is the preferred syntax to + use a port in your project. The legacy syntax (for example ``-sUSE_SDL2``, + ``-sUSE_SDL_IMAGE=2``) remains available. Contrib ports diff --git a/test/test_browser.py b/test/test_browser.py index 5002cf3a56f48..8fb6550edd298 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -3024,7 +3024,7 @@ def test_sdl2_image_formats(self): self.btest_exit('test_sdl2_image.c', 600, args=[ '--preload-file', 'screenshot.jpg', '-DSCREENSHOT_DIRNAME="/"', '-DSCREENSHOT_BASENAME="screenshot.jpg"', '-DBITSPERPIXEL=24', '-DNO_PRELOADED', - '-sUSE_SDL=2', '-sUSE_SDL_IMAGE=2', '-sSDL2_IMAGE_FORMATS=jpg' + '--use-port=sdl2', '--use-port=sdl2_image:formats=jpg' ]) @no_wasm64('SDL2 + wasm64') diff --git a/test/test_other.py b/test/test_other.py index 1144b396f8d78..3eca9b8dd5bc2 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -14515,3 +14515,21 @@ def test_js_preprocess_pre_post(self): self.do_runf(test_file('hello_world.c'), 'assertions enabled\n4', emcc_args=['-sASSERTIONS=1']) self.do_runf(test_file('hello_world.c'), 'assertions disabled\n4', emcc_args=['-sASSERTIONS=0']) self.assertNotContained('#preprocess', read_file('hello_world.js')) + + @with_both_compilers + def test_use_port_errors(self, compiler): + stderr = self.expect_fail([compiler, test_file('hello_world.c'), '--use-port=invalid', '-o', 'out.js']) + self.assertFalse(os.path.exists('out.js')) + self.assertContained('Error with --use-port=invalid | invalid port name: invalid', stderr) + stderr = self.expect_fail([compiler, test_file('hello_world.c'), '--use-port=sdl2:opt1=v1', '-o', 'out.js']) + self.assertFalse(os.path.exists('out.js')) + self.assertContained('Error with --use-port=sdl2:opt1=v1 | no options available for port sdl2', stderr) + stderr = self.expect_fail([compiler, test_file('hello_world.c'), '--use-port=sdl2_image:format=jpg', '-o', 'out.js']) + self.assertFalse(os.path.exists('out.js')) + self.assertContained('Error with --use-port=sdl2_image:format=jpg | format is not supported', stderr) + stderr = self.expect_fail([compiler, test_file('hello_world.c'), '--use-port=sdl2_image:formats', '-o', 'out.js']) + self.assertFalse(os.path.exists('out.js')) + self.assertContained('Error with --use-port=sdl2_image:formats | formats is missing a value', stderr) + stderr = self.expect_fail([compiler, test_file('hello_world.c'), '--use-port=sdl2_image:formats=jpg:formats=png', '-o', 'out.js']) + self.assertFalse(os.path.exists('out.js')) + self.assertContained('Error with --use-port=sdl2_image:formats=jpg:formats=png | duplicate option formats', stderr) diff --git a/tools/ports/__init__.py b/tools/ports/__init__.py index 9ceb3b63e37e6..dd84aebb3cdc0 100644 --- a/tools/ports/__init__.py +++ b/tools/ports/__init__.py @@ -68,6 +68,8 @@ def validate_port(port): expected_attrs = ['get', 'clear', 'show'] if port.is_contrib: expected_attrs += ['URL', 'DESCRIPTION', 'LICENSE'] + if hasattr(port, 'handle_options'): + expected_attrs += ['OPTIONS'] for a in expected_attrs: assert hasattr(port, a), 'port %s is missing %s' % (port, a) @@ -393,10 +395,34 @@ def add_deps(node): add_deps(port) -def handle_use_port_arg(settings, name): +def handle_use_port_error(arg, message): + utils.exit_with_error(f'Error with --use-port={arg} | {message}') + + +def handle_use_port_arg(settings, arg): + args = arg.split(':', 1) + name, options = args[0], None + if len(args) == 2: + options = args[1] if name not in ports_by_name: - utils.exit_with_error(f'Invalid port name: {name} used with --use-port') + handle_use_port_error(arg, f'invalid port name: {name}') ports_needed.add(name) + if options: + port = ports_by_name[name] + if not hasattr(port, 'handle_options'): + handle_use_port_error(arg, f'no options available for port {name}') + else: + options_dict = {} + for name_value in options.split(':'): + nv = name_value.split('=', 1) + if len(nv) != 2: + handle_use_port_error(arg, f'{name_value} is missing a value') + if nv[0] not in port.OPTIONS: + handle_use_port_error(arg, f'{nv[0]} is not supported; available options are {port.OPTIONS}') + if nv[0] in options_dict: + handle_use_port_error(arg, f'duplicate option {nv[0]}') + options_dict[nv[0]] = nv[1] + port.handle_options(options_dict) def get_needed_ports(settings): diff --git a/tools/ports/contrib/README.md b/tools/ports/contrib/README.md index f5b3b40d3a2a3..670b53cbd90ab 100644 --- a/tools/ports/contrib/README.md +++ b/tools/ports/contrib/README.md @@ -15,5 +15,31 @@ of information: is about * `LICENSE`: the license used by the project/port +A contrib port can have options using the syntax +`--use-port=name:opt1=v1:opt2=v2`. + +If you want to support options, then your port needs to provide 2 +additional components: + +1. A handler function defined this way: +```python +def handle_options(options): + # options is of type Dict[str, str] + # in case of error, use utils.exit_with_error('error message') +``` +2. A dictionary called `OPTIONS` (type `Dict[str, str]`) where each key is the + name of the option and the value is a short description of what it does + +When emscripten detects that options have been provided, it parses them and +check that they are valid option names for this port (using `OPTIONS`). It then +calls the handler function with these (valid) options. If you detect an error +with a value, you should use `tools.utils.exit_with_error` to report the +failure. + +> ### Note +> If the options influence the way the library produced by the port is built, +> you must ensure that the library name accounts for these options. Check +> `glfw3.py` for an example of ports with options. + After adding a contrib port, you should consider modifying the documentation under `site/source/docs/compiling/Contrib-Ports.rst`. \ No newline at end of file diff --git a/tools/ports/contrib/glfw3.py b/tools/ports/contrib/glfw3.py index 60caa4231620d..7e83fdd301280 100644 --- a/tools/ports/contrib/glfw3.py +++ b/tools/ports/contrib/glfw3.py @@ -4,6 +4,8 @@ # found in the LICENSE file. import os +from tools import utils +from typing import Dict TAG = '1.0.4' HASH = 'c3c96718e5d2b37df434a46c4a93ddfd9a768330d33f0d6ce2d08c139752894c2421cdd0fefb800fe41fafc2bbe58c8f22b8aa2849dc4fc6dde686037215cfad' @@ -13,14 +15,33 @@ DESCRIPTION = 'This project is an emscripten port of GLFW written in C++ for the web/webassembly platform' LICENSE = 'Apache 2.0 license' +OPTIONS = { + 'disableWarning': 'Boolean to disable warnings emitted by the library', + 'disableJoystick': 'Boolean to disable support for joystick entirely', + 'disableMultiWindow': 'Boolean to disable multi window support which makes the code smaller and faster' +} + +# user options (from --use-port) +opts: Dict[str, bool] = { + 'disableWarning': False, + 'disableJoystick': False, + 'disableMultiWindow': False +} + def get_lib_name(settings): - return 'lib_contrib.glfw3.a' + return ('lib_contrib.glfw3' + + ('-nw' if opts['disableWarning'] else '') + + ('-nj' if opts['disableJoystick'] else '') + + ('-sw' if opts['disableMultiWindow'] else '') + + '.a') def get(ports, settings, shared): # get the port - ports.fetch_project('contrib.glfw3', f'https://github.com/pongasoft/emscripten-glfw/releases/download/v{TAG}/emscripten-glfw3-{TAG}.zip', sha512hash=HASH) + ports.fetch_project('contrib.glfw3', + f'https://github.com/pongasoft/emscripten-glfw/releases/download/v{TAG}/emscripten-glfw3-{TAG}.zip', + sha512hash=HASH) def create(final): root_path = os.path.join(ports.get_dir(), 'contrib.glfw3') @@ -29,8 +50,16 @@ def create(final): for source_include_path in source_include_paths: ports.install_headers(source_include_path, target='GLFW') - # this should be an option but better to disable for now... - flags = ['-DEMSCRIPTEN_GLFW3_DISABLE_WARNING'] + flags = [] + + if opts['disableWarning']: + flags += ['-DEMSCRIPTEN_GLFW3_DISABLE_WARNING'] + + if opts['disableJoystick']: + flags += ['-DEMSCRIPTEN_GLFW3_DISABLE_JOYSTICK'] + + if opts['disableMultiWindow']: + flags += ['-DEMSCRIPTEN_GLFW3_DISABLE_MULTI_WINDOW_SUPPORT'] ports.build_port(source_path, final, 'contrib.glfw3', includes=source_include_paths, flags=flags) @@ -52,3 +81,11 @@ def linker_setup(ports, settings): # includes def process_args(ports): return ['-isystem', ports.get_include_dir('contrib.glfw3')] + + +def handle_options(options): + for option, value in options.items(): + if value.lower() in {'true', 'false'}: + opts[option] = value.lower() == 'true' + else: + utils.exit_with_error(f'{option} is expecting a boolean, got {value}') diff --git a/tools/ports/sdl2_image.py b/tools/ports/sdl2_image.py index f6029de06ad67..0adf74a0e5660 100644 --- a/tools/ports/sdl2_image.py +++ b/tools/ports/sdl2_image.py @@ -4,6 +4,7 @@ # found in the LICENSE file. import os +from typing import Dict, Set TAG = 'release-2.6.0' HASH = '2175d11a90211871f2289c8d57b31fe830e4b46af7361925c2c30cd521c1c677d2ee244feb682b6d3909cf085129255934751848fc81b480ea410952d990ffe0' @@ -14,14 +15,26 @@ 'sdl2_image_png': {'SDL2_IMAGE_FORMATS': ["png"]}, } +OPTIONS = { + 'formats': 'A comma separated list of formats (ex: --use-port=sdl2_image:formats=png,jpg)' +} + +# user options (from --use-port) +opts: Dict[str, Set] = { + 'formats': set() +} + def needed(settings): return settings.USE_SDL_IMAGE == 2 +def get_formats(settings): + return set(settings.SDL2_IMAGE_FORMATS).union(opts['formats']) + + def get_lib_name(settings): - settings.SDL2_IMAGE_FORMATS.sort() - formats = '-'.join(settings.SDL2_IMAGE_FORMATS) + formats = '-'.join(sorted(get_formats(settings))) libname = 'libSDL2_image' if formats != '': @@ -44,13 +57,15 @@ def create(final): defs = ['-O2', '-sUSE_SDL=2', '-Wno-format-security'] - for fmt in settings.SDL2_IMAGE_FORMATS: + formats = get_formats(settings) + + for fmt in formats: defs.append('-DLOAD_' + fmt.upper()) - if 'png' in settings.SDL2_IMAGE_FORMATS: + if 'png' in formats: defs += ['-sUSE_LIBPNG'] - if 'jpg' in settings.SDL2_IMAGE_FORMATS: + if 'jpg' in formats: defs += ['-sUSE_LIBJPEG'] ports.build_port(src_dir, final, 'sdl2_image', flags=defs, srcs=srcs) @@ -64,13 +79,18 @@ def clear(ports, settings, shared): def process_dependencies(settings): settings.USE_SDL = 2 - if 'png' in settings.SDL2_IMAGE_FORMATS: + formats = get_formats(settings) + if 'png' in formats: deps.append('libpng') settings.USE_LIBPNG = 1 - if 'jpg' in settings.SDL2_IMAGE_FORMATS: + if 'jpg' in formats: deps.append('libjpeg') settings.USE_LIBJPEG = 1 +def handle_options(options): + opts['formats'].update({format.lower().strip() for format in options['formats'].split(',')}) + + def show(): return 'sdl2_image (-sUSE_SDL_IMAGE=2 or --use-port=sdl2_image; zlib license)'