Skip to content

Commit 0f4a1fa

Browse files
authored
Add external ports + options support to embuilder (#21345)
1 parent 9710247 commit 0f4a1fa

File tree

8 files changed

+75
-26
lines changed

8 files changed

+75
-26
lines changed

embuilder.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from tools import shared
2424
from tools import system_libs
2525
from tools import ports
26+
from tools import utils
2627
from tools.settings import settings
2728
from tools.system_libs import USE_NINJA
2829

@@ -169,6 +170,10 @@ def get_all_tasks():
169170
return get_system_tasks()[1] + PORTS
170171

171172

173+
def handle_port_error(target, message):
174+
utils.exit_with_error(f'error building port `{target}` | {message}')
175+
176+
172177
def main():
173178
all_build_start_time = time.time()
174179

@@ -289,6 +294,12 @@ def main():
289294
clear_port(what)
290295
if do_build:
291296
build_port(what)
297+
elif ':' in what or what.endswith('.py'):
298+
name = ports.handle_use_port_arg(settings, what, lambda message: handle_port_error(what, message))
299+
if do_clear:
300+
clear_port(name)
301+
if do_build:
302+
build_port(name)
292303
else:
293304
logger.error('unfamiliar build target: ' + what)
294305
return 1

test/other/ports/external.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323

2424

2525
def get_lib_name(settings):
26-
return 'lib_external.a'
26+
if opts['dependency']:
27+
return f'lib_external-{opts["dependency"]}.a'
28+
else:
29+
return 'lib_external.a'
2730

2831

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

6063

61-
def handle_options(options):
64+
def handle_options(options, error_handler):
6265
opts.update(options)

test/test_other.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2416,7 +2416,7 @@ def test_external_ports(self):
24162416
# testing invalid dependency
24172417
stderr = self.expect_fail([EMCC, test_file('other/test_external_ports.c'), f'--use-port={external_port_path}:dependency=invalid', '-o', 'a4.out.js'])
24182418
self.assertFalse(os.path.exists('a4.out.js'))
2419-
self.assertContained('Unknown dependency `invalid` for port `external`', stderr)
2419+
self.assertContained('unknown dependency `invalid` for port `external`', stderr)
24202420

24212421
def test_link_memcpy(self):
24222422
# memcpy can show up *after* optimizations, so after our opportunity to link in libc, so it must be special-cased
@@ -14557,16 +14557,16 @@ def test_js_preprocess_pre_post(self):
1455714557
def test_use_port_errors(self, compiler):
1455814558
stderr = self.expect_fail([compiler, test_file('hello_world.c'), '--use-port=invalid', '-o', 'out.js'])
1455914559
self.assertFalse(os.path.exists('out.js'))
14560-
self.assertContained('Error with `--use-port=invalid` | invalid port name: `invalid`', stderr)
14560+
self.assertContained('error with `--use-port=invalid` | invalid port name: `invalid`', stderr)
1456114561
stderr = self.expect_fail([compiler, test_file('hello_world.c'), '--use-port=sdl2:opt1=v1', '-o', 'out.js'])
1456214562
self.assertFalse(os.path.exists('out.js'))
14563-
self.assertContained('Error with `--use-port=sdl2:opt1=v1` | no options available for port `sdl2`', stderr)
14563+
self.assertContained('error with `--use-port=sdl2:opt1=v1` | no options available for port `sdl2`', stderr)
1456414564
stderr = self.expect_fail([compiler, test_file('hello_world.c'), '--use-port=sdl2_image:format=jpg', '-o', 'out.js'])
1456514565
self.assertFalse(os.path.exists('out.js'))
14566-
self.assertContained('Error with `--use-port=sdl2_image:format=jpg` | `format` is not supported', stderr)
14566+
self.assertContained('error with `--use-port=sdl2_image:format=jpg` | `format` is not supported', stderr)
1456714567
stderr = self.expect_fail([compiler, test_file('hello_world.c'), '--use-port=sdl2_image:formats', '-o', 'out.js'])
1456814568
self.assertFalse(os.path.exists('out.js'))
14569-
self.assertContained('Error with `--use-port=sdl2_image:formats` | `formats` is missing a value', stderr)
14569+
self.assertContained('error with `--use-port=sdl2_image:formats` | `formats` is missing a value', stderr)
1457014570
stderr = self.expect_fail([compiler, test_file('hello_world.c'), '--use-port=sdl2_image:formats=jpg:formats=png', '-o', 'out.js'])
1457114571
self.assertFalse(os.path.exists('out.js'))
14572-
self.assertContained('Error with `--use-port=sdl2_image:formats=jpg:formats=png` | duplicate option `formats`', stderr)
14572+
self.assertContained('error with `--use-port=sdl2_image:formats=jpg:formats=png` | duplicate option `formats`', stderr)

test/test_sanity.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,28 @@ def test_embuilder_wildcards(self):
748748
self.run_process([EMBUILDER, 'build', 'libwebgpu*'])
749749
self.assertGreater(len(glob.glob(glob_match)), 3)
750750

751+
def test_embuilder_with_use_port_syntax(self):
752+
restore_and_set_up()
753+
self.run_process([EMBUILDER, 'build', 'sdl2_image:formats=png,jpg', '--force'])
754+
self.assertExists(os.path.join(config.CACHE, 'sysroot', 'lib', 'wasm32-emscripten', 'libSDL2_image_jpg-png.a'))
755+
self.assertContained('error building port `sdl2_image:formats=invalid` | invalid is not a supported format', self.do([EMBUILDER, 'build', 'sdl2_image:formats=invalid', '--force']))
756+
757+
def test_embuilder_external_ports(self):
758+
restore_and_set_up()
759+
simple_port_path = test_file("other/ports/simple.py")
760+
# embuilder handles external port target that ends with .py
761+
self.run_process([EMBUILDER, 'build', f'{simple_port_path}', '--force'])
762+
self.assertExists(os.path.join(config.CACHE, 'sysroot', 'lib', 'wasm32-emscripten', 'lib_simple.a'))
763+
# embuilder handles external port target that contains port options
764+
external_port_path = test_file("other/ports/external.py")
765+
self.run_process([EMBUILDER, 'build', f'{external_port_path}:value1=12:value2=36', '--force'])
766+
self.assertExists(os.path.join(config.CACHE, 'sysroot', 'lib', 'wasm32-emscripten', 'lib_external.a'))
767+
# embuilder handles external port target that contains port options (influences library name,
768+
# like sdl2_image:formats=png)
769+
external_port_path = test_file("other/ports/external.py")
770+
self.run_process([EMBUILDER, 'build', f'{external_port_path}:dependency=sdl2', '--force'])
771+
self.assertExists(os.path.join(config.CACHE, 'sysroot', 'lib', 'wasm32-emscripten', 'lib_external-sdl2.a'))
772+
751773
def test_binaryen_version(self):
752774
restore_and_set_up()
753775
with open(EM_CONFIG, 'a') as f:

tools/ports/__init__.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ def add_deps(node):
398398
node.process_dependencies(settings)
399399
for d in node.deps:
400400
if d not in ports_by_name:
401-
utils.exit_with_error(f'Unknown dependency `{d}` for port `{node.name}`')
401+
utils.exit_with_error(f'unknown dependency `{d}` for port `{node.name}`')
402402
dep = ports_by_name[d]
403403
if dep not in port_set:
404404
port_set.add(dep)
@@ -409,10 +409,13 @@ def add_deps(node):
409409

410410

411411
def handle_use_port_error(arg, message):
412-
utils.exit_with_error(f'Error with `--use-port={arg}` | {message}')
412+
utils.exit_with_error(f'error with `--use-port={arg}` | {message}')
413413

414414

415-
def handle_use_port_arg(settings, arg):
415+
def handle_use_port_arg(settings, arg, error_handler=None):
416+
if not error_handler:
417+
def error_handler(message):
418+
handle_use_port_error(arg, message)
416419
# Ignore ':' in first or second char of string since we could be dealing with a windows drive separator
417420
pos = arg.find(':', 2)
418421
if pos != -1:
@@ -422,27 +425,28 @@ def handle_use_port_arg(settings, arg):
422425
if name.endswith('.py'):
423426
port_file_path = name
424427
if not os.path.isfile(port_file_path):
425-
handle_use_port_error(arg, f'not a valid port path: {port_file_path}')
428+
error_handler(f'not a valid port path: {port_file_path}')
426429
name = load_port_by_path(port_file_path)
427430
elif name not in ports_by_name:
428-
handle_use_port_error(arg, f'invalid port name: `{name}`')
431+
error_handler(f'invalid port name: `{name}`')
429432
ports_needed.add(name)
430433
if options:
431434
port = ports_by_name[name]
432435
if not hasattr(port, 'handle_options'):
433-
handle_use_port_error(arg, f'no options available for port `{name}`')
436+
error_handler(f'no options available for port `{name}`')
434437
else:
435438
options_dict = {}
436439
for name_value in options.split(':'):
437440
nv = name_value.split('=', 1)
438441
if len(nv) != 2:
439-
handle_use_port_error(arg, f'`{name_value}` is missing a value')
442+
error_handler(f'`{name_value}` is missing a value')
440443
if nv[0] not in port.OPTIONS:
441-
handle_use_port_error(arg, f'`{nv[0]}` is not supported; available options are {port.OPTIONS}')
444+
error_handler(f'`{nv[0]}` is not supported; available options are {port.OPTIONS}')
442445
if nv[0] in options_dict:
443-
handle_use_port_error(arg, f'duplicate option `{nv[0]}`')
446+
error_handler(f'duplicate option `{nv[0]}`')
444447
options_dict[nv[0]] = nv[1]
445-
port.handle_options(options_dict)
448+
port.handle_options(options_dict, error_handler)
449+
return name
446450

447451

448452
def get_needed_ports(settings):

tools/ports/contrib/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,18 @@ additional components:
2323

2424
1. A handler function defined this way:
2525
```python
26-
def handle_options(options):
26+
def handle_options(options, error_handler):
2727
# options is of type Dict[str, str]
28-
# in case of error, use utils.exit_with_error('error message')
28+
# in case of error, use error_handler('error message')
29+
# note that error_handler is guaranteed to never return
2930
```
3031
2. A dictionary called `OPTIONS` (type `Dict[str, str]`) where each key is the
3132
name of the option and the value is a short description of what it does
3233

3334
When emscripten detects that options have been provided, it parses them and
3435
check that they are valid option names for this port (using `OPTIONS`). It then
3536
calls the handler function with these (valid) options. If you detect an error
36-
with a value, you should use `tools.utils.exit_with_error` to report the
37+
with a value, you should use the error handler provided to report the
3738
failure.
3839

3940
> ### Note

tools/ports/contrib/glfw3.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
# found in the LICENSE file.
55

66
import os
7-
from tools import utils
87
from typing import Dict
98

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

8584

86-
def handle_options(options):
85+
def handle_options(options, error_handler):
8786
for option, value in options.items():
8887
if value.lower() in {'true', 'false'}:
8988
opts[option] = value.lower() == 'true'
9089
else:
91-
utils.exit_with_error(f'{option} is expecting a boolean, got {value}')
90+
error_handler(f'{option} is expecting a boolean, got {value}')

tools/ports/sdl2_image.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
'formats': 'A comma separated list of formats (ex: --use-port=sdl2_image:formats=png,jpg)'
2020
}
2121

22+
SUPPORTED_FORMATS = {'avif', 'bmp', 'gif', 'jpg', 'jxl', 'lbm', 'pcx', 'png',
23+
'pnm', 'qoi', 'svg', 'tga', 'tif', 'webp', 'xcf', 'xpm', 'xv'}
24+
2225
# user options (from --use-port)
2326
opts: Dict[str, Set] = {
2427
'formats': set()
@@ -88,8 +91,14 @@ def process_dependencies(settings):
8891
settings.USE_LIBJPEG = 1
8992

9093

91-
def handle_options(options):
92-
opts['formats'].update({format.lower().strip() for format in options['formats'].split(',')})
94+
def handle_options(options, error_handler):
95+
formats = options['formats'].split(',')
96+
for format in formats:
97+
format = format.lower().strip()
98+
if format not in SUPPORTED_FORMATS:
99+
error_handler(f'{format} is not a supported format')
100+
else:
101+
opts['formats'].add(format)
93102

94103

95104
def show():

0 commit comments

Comments
 (0)