diff --git a/embuilder.py b/embuilder.py index 076d58a39f23f..9c963feee516a 100755 --- a/embuilder.py +++ b/embuilder.py @@ -110,6 +110,8 @@ 'giflib', ] +ports.read_ports() + PORTS = sorted(list(ports.ports_by_name.keys()) + list(ports.port_variants.keys())) temp_files = shared.get_temp_files() diff --git a/emcc.py b/emcc.py index 8870027348fcf..ad03a6b87561e 100644 --- a/emcc.py +++ b/emcc.py @@ -287,6 +287,8 @@ def apply_user_settings(): if key == 'WASM_OBJECT_FILES': settings.LTO = 0 if value else 'full' + ports.check_port_options(settings) + def cxx_to_c_compiler(cxx): # Convert C++ compiler name into C compiler name diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 1c1048be5f9d9..84b7888d3ae41 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -1700,6 +1700,56 @@ Specify the GLFW version that is being linked against. Only relevant, if you are linking against the GLFW library. Valid options are 2 for GLFW2 and 3 for GLFW3. +.. _ports: + +PORTS +===== + +Specify which ports to use. If there is only one port to use, it can be +specified this way -sPORTS=port. If multiple ports are need, you +specify it that way: -sPORTS=[port1,port2]. + +.. note:: Contrib ports are contributed by the wider community and supported on a "best effort" basis. Since they are not run as part of emscripten CI they are not always guaranteed to build or function. + +Available contrib ports: + +.. _ports=contrib.example: + +PORTS=contrib.example +--------------------- + +Port Contrib Example + +`Project information `_ +License: MIT license + +.. _ports=contrib.glfw3: + +PORTS=contrib.glfw3 +------------------- + +This project is an emscripten port of glfw written in C++ for the web/webassembly platform + +Available options: +- contrib.glfw3:DISABLE_WARNING : Disable all warnings +- contrib.glfw3:DISABLE_JOYSTICK : Disable support for joystick (due to polling, it can help to disable joystick support if not needed) +- contrib.glfw3:DISABLE_MULTI_WINDOW_SUPPORT : Disable support for multiple windows if not needed + +`Project information `_ +License: Apache 2.0 license + + +.. _port_options: + +PORT_OPTIONS +============ + +Specify which options to use when building ports. The syntax is the following: +-sPORT_OPTIONS=port:option or -sPORT_OPTIONS=[port1:option1,port2:option2] +when multiple port/options need to be provided. + +.. note:: Applicable during both linking and compilation + .. _wasm: WASM @@ -1894,6 +1944,7 @@ USE_ICU ======= 1 = use icu from emscripten-ports +Alternate syntax: -sPORTS=icu .. note:: Applicable during both linking and compilation @@ -1903,6 +1954,7 @@ USE_ZLIB ======== 1 = use zlib from emscripten-ports +Alternate syntax: -sPORTS=zlib .. note:: Applicable during both linking and compilation @@ -1912,6 +1964,7 @@ USE_BZIP2 ========= 1 = use bzip2 from emscripten-ports +Alternate syntax: -sPORTS=bzip2 .. note:: Applicable during both linking and compilation @@ -1921,6 +1974,7 @@ USE_GIFLIB ========== 1 = use giflib from emscripten-ports +Alternate syntax: -sPORTS=giflib .. note:: Applicable during both linking and compilation @@ -1930,6 +1984,7 @@ USE_LIBJPEG =========== 1 = use libjpeg from emscripten-ports +Alternate syntax: -sPORTS=libjpeg .. note:: Applicable during both linking and compilation @@ -1939,6 +1994,7 @@ USE_LIBPNG ========== 1 = use libpng from emscripten-ports +Alternate syntax: -sPORTS=libpng .. note:: Applicable during both linking and compilation @@ -1948,6 +2004,7 @@ USE_REGAL ========= 1 = use Regal from emscripten-ports +Alternate syntax: -sPORTS=regal .. note:: Applicable during both linking and compilation @@ -1957,6 +2014,7 @@ USE_BOOST_HEADERS ================= 1 = use Boost headers from emscripten-ports +Alternate syntax: -sPORTS=boost_headers .. note:: Applicable during both linking and compilation @@ -1966,6 +2024,7 @@ USE_BULLET ========== 1 = use bullet from emscripten-ports +Alternate syntax: -sPORTS=bullet .. note:: Applicable during both linking and compilation @@ -1975,6 +2034,7 @@ USE_VORBIS ========== 1 = use vorbis from emscripten-ports +Alternate syntax: -sPORTS=vorbis .. note:: Applicable during both linking and compilation @@ -1984,6 +2044,7 @@ USE_OGG ======= 1 = use ogg from emscripten-ports +Alternate syntax: -sPORTS=ogg .. note:: Applicable during both linking and compilation @@ -1993,6 +2054,7 @@ USE_MPG123 ========== 1 = use mpg123 from emscripten-ports +Alternate syntax: -sPORTS=mpg123 .. note:: Applicable during both linking and compilation @@ -2002,6 +2064,7 @@ USE_FREETYPE ============ 1 = use freetype from emscripten-ports +Alternate syntax: -sPORTS=freetype .. note:: Applicable during both linking and compilation @@ -2021,6 +2084,7 @@ USE_HARFBUZZ ============ 1 = use harfbuzz from harfbuzz upstream +Alternate syntax: -sPORTS=harfbuzz .. note:: Applicable during both linking and compilation @@ -2030,6 +2094,7 @@ USE_COCOS2D =========== 3 = use cocos2d v3 from emscripten-ports +Alternate syntax: -sPORTS=cocos2d .. note:: Applicable during both linking and compilation @@ -2039,6 +2104,7 @@ USE_MODPLUG =========== 1 = use libmodplug from emscripten-ports +Alternate syntax: -sPORTS=libmodplug .. note:: Applicable during both linking and compilation @@ -2063,6 +2129,7 @@ USE_SQLITE3 =========== 1 = use sqlite3 from emscripten-ports +Alternate syntax: -sPORTS=sqlite3 .. note:: Applicable during both linking and compilation diff --git a/src/settings.js b/src/settings.js index bb417e8cc564e..e3ecbc102e31f 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1339,6 +1339,18 @@ var EMSCRIPTEN_TRACING = false; // [link] var USE_GLFW = 0; +// Specify which ports to use. If there is only one port to use, it can be +// specified this way -sPORTS=port. If multiple ports are need, you +// specify it that way: -sPORTS=[port1,port2]. +// [link] +var PORTS = []; + +// Specify which options to use when building ports. The syntax is the following: +// -sPORT_OPTIONS=port:option or -sPORT_OPTIONS=[port1:option1,port2:option2] +// when multiple port/options need to be provided. +// [compile+link] +var PORT_OPTIONS = []; + // Whether to use compile code to WebAssembly. Set this to 0 to compile to JS // instead of wasm. // @@ -1475,54 +1487,67 @@ var USE_SDL_TTF = 1; var USE_SDL_NET = 1; // 1 = use icu from emscripten-ports +// Alternate syntax: -sPORTS=icu // [compile+link] var USE_ICU = false; // 1 = use zlib from emscripten-ports +// Alternate syntax: -sPORTS=zlib // [compile+link] var USE_ZLIB = false; // 1 = use bzip2 from emscripten-ports +// Alternate syntax: -sPORTS=bzip2 // [compile+link] var USE_BZIP2 = false; // 1 = use giflib from emscripten-ports +// Alternate syntax: -sPORTS=giflib // [compile+link] var USE_GIFLIB = false; // 1 = use libjpeg from emscripten-ports +// Alternate syntax: -sPORTS=libjpeg // [compile+link] var USE_LIBJPEG = false; // 1 = use libpng from emscripten-ports +// Alternate syntax: -sPORTS=libpng // [compile+link] var USE_LIBPNG = false; // 1 = use Regal from emscripten-ports +// Alternate syntax: -sPORTS=regal // [compile+link] var USE_REGAL = false; // 1 = use Boost headers from emscripten-ports +// Alternate syntax: -sPORTS=boost_headers // [compile+link] var USE_BOOST_HEADERS = false; // 1 = use bullet from emscripten-ports +// Alternate syntax: -sPORTS=bullet // [compile+link] var USE_BULLET = false; // 1 = use vorbis from emscripten-ports +// Alternate syntax: -sPORTS=vorbis // [compile+link] var USE_VORBIS = false; // 1 = use ogg from emscripten-ports +// Alternate syntax: -sPORTS=ogg // [compile+link] var USE_OGG = false; // 1 = use mpg123 from emscripten-ports +// Alternate syntax: -sPORTS=mpg123 // [compile+link] var USE_MPG123 = false; // 1 = use freetype from emscripten-ports +// Alternate syntax: -sPORTS=freetype // [compile+link] var USE_FREETYPE = false; @@ -1532,14 +1557,17 @@ var USE_FREETYPE = false; var USE_SDL_MIXER = 1; // 1 = use harfbuzz from harfbuzz upstream +// Alternate syntax: -sPORTS=harfbuzz // [compile+link] var USE_HARFBUZZ = false; // 3 = use cocos2d v3 from emscripten-ports +// Alternate syntax: -sPORTS=cocos2d // [compile+link] var USE_COCOS2D = 0; // 1 = use libmodplug from emscripten-ports +// Alternate syntax: -sPORTS=libmodplug // [compile+link] var USE_MODPLUG = false; @@ -1553,6 +1581,7 @@ var SDL2_IMAGE_FORMATS = []; var SDL2_MIXER_FORMATS = ["ogg"]; // 1 = use sqlite3 from emscripten-ports +// Alternate syntax: -sPORTS=sqlite3 // [compile+link] var USE_SQLITE3 = false; diff --git a/test/browser/test_contrib_ports.c b/test/browser/test_contrib_ports.c new file mode 100644 index 0000000000000..ab7a043e390b3 --- /dev/null +++ b/test/browser/test_contrib_ports.c @@ -0,0 +1,28 @@ +/* + * Copyright 2024 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + */ + +#include +#include +#include +#include + +int main() { + // from one contrib port + assert(contrib_example() == 12); + + // from another contrib port + assert(glfwInit() == GLFW_TRUE); + + GLFWwindow* window = glfwCreateWindow(320, 200, "test_glfw3_port", 0, 0); + assert(window != 0); + // this call ensures that it uses the right port + assert(emscripten_glfw_is_window_fullscreen(window) == EM_FALSE); + glfwTerminate(); + + + return 0; +} diff --git a/test/test_browser.py b/test/test_browser.py index 5182b42d66a3d..fae76098d2b6c 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -2998,6 +2998,10 @@ def test_glfw_events(self): def test_glfw3_hi_dpi_aware(self): self.btest_exit('test_glfw3_hi_dpi_aware.c', args=['-sUSE_GLFW=3', '-lGL']) + @requires_graphics_hardware + def test_contrib_ports(self): + self.btest_exit('test_contrib_ports.c', args=['-sPORTS=[contrib.example,contrib.glfw3]', '-sPORT_OPTIONS=contrib.glfw3:DISABLE_WARNING', '-lGL']) + @requires_graphics_hardware @no_wasm64('SDL2 + wasm64') @parameterized({ diff --git a/tools/link.py b/tools/link.py index 97882439c9eb4..df56cb57d27c1 100644 --- a/tools/link.py +++ b/tools/link.py @@ -2931,6 +2931,8 @@ def run(linker_inputs, options, state, newargs): logger.debug('stopping after linking to object file') return 0 + phase_calculate_system_libraries(state, linker_arguments, newargs) + js_syms = {} if (not settings.SIDE_MODULE or settings.ASYNCIFY) and not shared.SKIP_SUBPROCS: js_info = get_js_sym_info() @@ -2953,8 +2955,6 @@ def add_js_deps(sym): settings.ASYNCIFY_IMPORTS_EXCEPT_JS_LIBS = settings.ASYNCIFY_IMPORTS[:] settings.ASYNCIFY_IMPORTS += ['*.' + x for x in js_info['asyncFuncs']] - phase_calculate_system_libraries(state, linker_arguments, newargs) - phase_link(linker_arguments, wasm_target, js_syms) # Special handling for when the user passed '-Wl,--version'. In this case the linker diff --git a/tools/maint/update_settings_docs.py b/tools/maint/update_settings_docs.py index 06570c2e41926..bc63dfa9ffbe9 100755 --- a/tools/maint/update_settings_docs.py +++ b/tools/maint/update_settings_docs.py @@ -26,6 +26,7 @@ sys.path.append(root_dir) from tools.utils import path_from_root, read_file, safe_ensure_dirs +from tools import ports header = '''\ .. _settings-reference: @@ -56,11 +57,11 @@ output_file = path_from_root('site/source/docs/tools_reference/settings_reference.rst') -def write_setting(f, setting_name, comment, tags): +def write_setting(f, setting_name, comment, tags, level='='): # Convert markdown backticks to rst double backticks f.write('\n.. _' + setting_name.lower() + ':\n') f.write('\n' + setting_name + '\n') - f.write('=' * len(setting_name) + '\n\n') + f.write(level * len(setting_name) + '\n\n') f.write(comment + '\n') for tag in tags: for t in tag.split(): @@ -68,6 +69,24 @@ def write_setting(f, setting_name, comment, tags): f.write('\n.. note:: ' + all_tags[t] + '\n') +def write_contrib_ports(f): + f.write('\n.. note:: Contrib ports are contributed by the wider community and ' + + 'supported on a "best effort" basis. Since they are not run as part ' + + 'of emscripten CI they are not always guaranteed to build or function.') + f.write('\n\nAvailable contrib ports:\n') + for port in ports.ports: + if port.is_contrib: + comment = port.project_description() + if port.available_options(): + comment += '\n\nAvailable options:' + for name, description in port.available_options().items(): + comment += f'\n- {name} : {description}' + comment += f'\n\n`Project information <{port.project_url()}>`_' + comment += f'\nLicense: {port.project_license()}' + write_setting(f, f'PORTS={port.name}', comment, [], '-') + f.write('\n') + + def write_file(f): f.write(header) @@ -95,11 +114,14 @@ def write_file(f): setting_name = line.split()[1] comment = '\n'.join(current_comment).strip() write_setting(f, setting_name, comment, current_tags) + if setting_name == 'PORTS': + write_contrib_ports(f) current_comment = [] current_tags = [] def main(args): + ports.read_ports() if '--check' in args: safe_ensure_dirs(path_from_root('out')) tmp_output = path_from_root('out/settings_reference.rst') diff --git a/tools/ports/__init__.py b/tools/ports/__init__.py index 50b2c333e476a..5fa5265c5ebd1 100644 --- a/tools/ports/__init__.py +++ b/tools/ports/__init__.py @@ -31,39 +31,72 @@ logger = logging.getLogger('ports') +def load_port_by_name(name): + if name.startswith('contrib.'): + expected_attrs = ['get', 'clear', 'show', 'project_url', 'project_description', 'project_license'] + try: + port = __import__(name, globals(), level=1, fromlist=[None]) + port.is_contrib = True + port.show = lambda: f'{port.name} (-sPORTS={port.name}; {port.project_license()})' + except ModuleNotFoundError: + utils.exit_with_error(f'Invalid contrib port name: {name}') + else: + expected_attrs = ['get', 'clear', 'show'] + try: + port = __import__(name, globals(), level=1) + port.is_contrib = False + except ModuleNotFoundError: + utils.exit_with_error(f'Invalid port name: {name}') + + port.name = name + ports.append(port) + ports_by_name[name] = port + for a in expected_attrs: + assert hasattr(port, a), 'port %s is missing %s' % (port, a) + if not hasattr(port, 'process_dependencies'): + port.process_dependencies = lambda x: [] + if not hasattr(port, 'linker_setup'): + port.linker_setup = lambda x, y: 0 + if not hasattr(port, 'deps'): + port.deps = [] + if not hasattr(port, 'process_args'): + port.process_args = lambda x: [] + if not hasattr(port, 'variants'): + # port variants (default: no variants) + port.variants = {} + if not hasattr(port, 'options'): + # port options (default: no options) + port.options = {} + port.available_options = lambda: {} + else: + port.available_options = lambda: {port.name + ':' + option: desc for option, desc in port.options.items()} + + for variant, extra_settings in port.variants.items(): + if variant in port_variants: + utils.exit_with_error('duplicate port variant: %s' % variant) + port_variants[variant] = (port.name, extra_settings) + + return port + + +def get_port_by_name(name): + return ports_by_name[name] if name in ports_by_name else load_port_by_name(name) + + @ToolchainProfiler.profile() def read_ports(): - expected_attrs = ['get', 'clear', 'show', 'needed'] for filename in os.listdir(ports_dir): if not filename.endswith('.py') or filename == '__init__.py': continue filename = os.path.splitext(filename)[0] - port = __import__(filename, globals(), level=1) - ports.append(port) - port.name = filename - ports_by_name[port.name] = port - for a in expected_attrs: - assert hasattr(port, a), 'port %s is missing %s' % (port, a) - if not hasattr(port, 'process_dependencies'): - port.process_dependencies = lambda x: 0 - if not hasattr(port, 'linker_setup'): - port.linker_setup = lambda x, y: 0 - if not hasattr(port, 'deps'): - port.deps = [] - if not hasattr(port, 'process_args'): - port.process_args = lambda x: [] - if not hasattr(port, 'variants'): - # port variants (default: no variants) - port.variants = {} - - for variant, extra_settings in port.variants.items(): - if variant in port_variants: - utils.exit_with_error('duplicate port variant: %s' % variant) - port_variants[variant] = (port.name, extra_settings) - - for dep in port.deps: - if dep not in ports_by_name: - utils.exit_with_error('unknown dependency in port: %s' % dep) + get_port_by_name(filename) + + contrib_dir = os.path.join(ports_dir, 'contrib') + for filename in os.listdir(contrib_dir): + if not filename.endswith('.py') or filename == '__init__.py': + continue + filename = os.path.splitext(filename)[0] + get_port_by_name('contrib.' + filename) def get_all_files_under(dirname): @@ -349,10 +382,11 @@ def dfs(node): def resolve_dependencies(port_set, settings): - def add_deps(node): - node.process_dependencies(settings) - for d in node.deps: - dep = ports_by_name[d] + def add_deps(p): + all_dependencies = set(p.deps) + all_dependencies.union(p.process_dependencies(settings)) + for d in all_dependencies: + dep = get_port_by_name(d) if dep not in port_set: port_set.add(dep) add_deps(dep) @@ -361,13 +395,77 @@ def add_deps(node): add_deps(port) +def get_official_ports(settings): + official_ports = set() + if settings.USE_BOOST_HEADERS == 1: + official_ports.add('boost_headers') + if settings.USE_BULLET == 1: + official_ports.add('bullet') + if settings.USE_BZIP2: + official_ports.add('bzip2') + if settings.USE_COCOS2D == 3: + official_ports.add('cocos2d') + if settings.USE_FREETYPE: + official_ports.add('freetype') + if settings.USE_GIFLIB: + official_ports.add('giflib') + if settings.USE_HARFBUZZ: + official_ports.add('harfbuzz') + if settings.USE_ICU: + official_ports.add('icu') + if settings.USE_LIBJPEG: + official_ports.add('libjpeg') + if settings.USE_MODPLUG: + official_ports.add('libmodplug') + if settings.USE_LIBPNG: + official_ports.add('libpng') + if settings.USE_MPG123: + official_ports.add('mpg123') + if settings.USE_OGG: + official_ports.add('ogg') + if settings.USE_REGAL: + official_ports.add('regal') + if settings.USE_SDL == 2: + official_ports.add('sdl2') + if settings.USE_SDL_GFX == 2: + official_ports.add('sdl2_gfx') + if settings.USE_SDL_IMAGE == 2: + official_ports.add('sdl2_image') + if settings.USE_SDL_MIXER == 2: + official_ports.add('sdl2_mixer') + if settings.USE_SDL_NET == 2: + official_ports.add('sdl2_net') + if settings.USE_SDL_TTF == 2: + official_ports.add('sdl2_ttf') + if settings.USE_SQLITE3: + official_ports.add('sqlite3') + if settings.USE_VORBIS: + official_ports.add('vorbis') + if settings.USE_ZLIB: + official_ports.add('zlib') + return official_ports + + def get_needed_ports(settings): # Start with directly needed ports, and transitively add dependencies - needed = set(p for p in ports if p.needed(settings)) + needed_port_names = get_official_ports(settings) + needed_port_names = needed_port_names.union(settings.PORTS) + needed = set(get_port_by_name(n) for n in needed_port_names) resolve_dependencies(needed, settings) return needed +def check_port_options(settings): + for option in settings.PORT_OPTIONS: + parts = option.split(':') + if len(parts) != 2: + utils.exit_with_error(f'Invalid port option: {option}. Syntax is :