Skip to content

Add external ports + options support to embuilder #21345

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

Merged
merged 8 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
14 changes: 14 additions & 0 deletions embuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ def get_all_tasks():
return get_system_tasks()[1] + PORTS


def handle_port_error(message):
raise Exception(message)


def main():
all_build_start_time = time.time()

Expand Down Expand Up @@ -289,6 +293,16 @@ def main():
clear_port(what)
if do_build:
build_port(what)
elif ':' in what or what.endswith('.py'):
try:
name = ports.handle_use_port_arg(settings, what, lambda message: handle_port_error(message))
except Exception as e:
logger.error(f'Build target invalid `{what}` | {e}')
return 1
if do_clear:
clear_port(name)
if do_build:
build_port(name)
else:
logger.error('unfamiliar build target: ' + what)
return 1
Expand Down
7 changes: 5 additions & 2 deletions test/other/ports/external.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@


def get_lib_name(settings):
return 'lib_external.a'
if opts['dependency']:
return f'lib_external-{opts["dependency"]}.a'
else:
return 'lib_external.a'


def get(ports, settings, shared):
Expand Down Expand Up @@ -58,5 +61,5 @@ def process_dependencies(settings):
deps.append(opts['dependency'])


def handle_options(options):
def handle_options(options, error_handler):
opts.update(options)
25 changes: 25 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -2418,6 +2418,31 @@ def test_external_ports(self):
self.assertFalse(os.path.exists('a4.out.js'))
self.assertContained('Unknown dependency `invalid` for port `external`', stderr)

@crossplatform
def test_embuilder_with_use_port_syntax(self):
if config.FROZEN_CACHE:
self.skipTest("test doesn't work with frozen cache")
self.run_process([EMBUILDER, 'build', 'sdl2_image:formats=png,jpg', '--force'])
self.assertExists(os.path.join(config.CACHE, 'sysroot', 'lib', 'wasm32-emscripten', 'libSDL2_image_jpg-png.a'))

@crossplatform
def test_embuilder_external_ports(self):
if config.FROZEN_CACHE:
self.skipTest("test doesn't work with frozen cache")
simple_port_path = test_file("other/ports/simple.py")
# embuilder handles external port target that ends with .py
self.run_process([EMBUILDER, 'build', f'{simple_port_path}', '--force'])
self.assertExists(os.path.join(config.CACHE, 'sysroot', 'lib', 'wasm32-emscripten', 'lib_simple.a'))
# embuilder handles external port target that contains port options
external_port_path = test_file("other/ports/external.py")
self.run_process([EMBUILDER, 'build', f'{external_port_path}:value1=12:value2=36', '--force'])
self.assertExists(os.path.join(config.CACHE, 'sysroot', 'lib', 'wasm32-emscripten', 'lib_external.a'))
# embuilder handles external port target that contains port options (influences library name,
# like sdl2_image:formats=png)
external_port_path = test_file("other/ports/external.py")
self.run_process([EMBUILDER, 'build', f'{external_port_path}:dependency=sdl2', '--force'])
self.assertExists(os.path.join(config.CACHE, 'sysroot', 'lib', 'wasm32-emscripten', 'lib_external-sdl2.a'))

def test_link_memcpy(self):
# memcpy can show up *after* optimizations, so after our opportunity to link in libc, so it must be special-cased
create_file('main.c', r'''
Expand Down
20 changes: 12 additions & 8 deletions tools/ports/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,10 @@ 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):
def handle_use_port_arg(settings, arg, error_handler=None):
if not error_handler:
def error_handler(message):
handle_use_port_error(arg, message)
# Ignore ':' in first or second char of string since we could be dealing with a windows drive separator
pos = arg.find(':', 2)
if pos != -1:
Expand All @@ -422,27 +425,28 @@ def handle_use_port_arg(settings, arg):
if name.endswith('.py'):
port_file_path = name
if not os.path.isfile(port_file_path):
handle_use_port_error(arg, f'not a valid port path: {port_file_path}')
error_handler(f'not a valid port path: {port_file_path}')
name = load_port_by_path(port_file_path)
elif name not in ports_by_name:
handle_use_port_error(arg, f'invalid port name: `{name}`')
error_handler(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}`')
error_handler(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')
error_handler(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}')
error_handler(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]}`')
error_handler(f'duplicate option `{nv[0]}`')
options_dict[nv[0]] = nv[1]
port.handle_options(options_dict)
port.handle_options(options_dict, error_handler)
return name


def get_needed_ports(settings):
Expand Down
4 changes: 2 additions & 2 deletions tools/ports/contrib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ additional components:

1. A handler function defined this way:
```python
def handle_options(options):
def handle_options(options, error_handler):
# options is of type Dict[str, str]
# in case of error, use utils.exit_with_error('error message')
# in case of error, use error_handler('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
Expand Down
5 changes: 2 additions & 3 deletions tools/ports/contrib/glfw3.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
# found in the LICENSE file.

import os
from tools import utils
from typing import Dict

TAG = '1.0.4'
Expand Down Expand Up @@ -83,9 +82,9 @@ def process_args(ports):
return ['-isystem', ports.get_include_dir('contrib.glfw3')]


def handle_options(options):
def handle_options(options, error_handler):
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}')
error_handler(f'{option} is expecting a boolean, got {value}')
2 changes: 1 addition & 1 deletion tools/ports/sdl2_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def process_dependencies(settings):
settings.USE_LIBJPEG = 1


def handle_options(options):
def handle_options(options, error_handler):
opts['formats'].update({format.lower().strip() for format in options['formats'].split(',')})


Expand Down