Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add options support to contrib ports (contrib ports part 3) #21276

Merged
merged 15 commits into from
Feb 8, 2024
Merged
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
6 changes: 4 additions & 2 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 24 additions & 3 deletions site/source/docs/compiling/Building-Projects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/emscripten-core/emscripten/blob/main/system/bin/sdl-config>`_. 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 <https://github.com/emscripten-core/emscripten/blob/main/system/bin/sdl-config>`_.
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.
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
18 changes: 18 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
30 changes: 28 additions & 2 deletions tools/ports/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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]
ypujante marked this conversation as resolved.
Show resolved Hide resolved
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):
Expand Down
26 changes: 26 additions & 0 deletions tools/ports/contrib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
45 changes: 41 additions & 4 deletions tools/ports/contrib/glfw3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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')
Expand All @@ -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)

Expand All @@ -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}')
34 changes: 27 additions & 7 deletions tools/ports/sdl2_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# found in the LICENSE file.

import os
from typing import Dict, Set

TAG = 'release-2.6.0'
HASH = '2175d11a90211871f2289c8d57b31fe830e4b46af7361925c2c30cd521c1c677d2ee244feb682b6d3909cf085129255934751848fc81b480ea410952d990ffe0'
Expand All @@ -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 != '':
Expand All @@ -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)
Expand All @@ -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)'
Loading