Allow certain command line settings to be additive For certain command line settings (specifically most settings that take a list) it is useful to be able to add to the existing setting rather than replace it. For example, the `-sEXPORTED_FUNCTIONS` argument. In a build system where different components can inject link flags it makes sense that each component be able add to the list of exported functions without clobbering the current list. We have had this feature requested several times in the past. For example, from the bazel toolchain folks.
diff --git a/ChangeLog.md b/ChangeLog.md index 33c4551..e7153fb 100644 --- a/ChangeLog.md +++ b/ChangeLog.md
@@ -20,6 +20,11 @@ 3.1.45 (in development) ----------------------- +- Command line settings that accept lists are add to existing occurances + of the setting rather than replacing them. For example, specifying + `-sEXPORTED_FUNCTIONS=foo -sEXPORTED_FUNCTIONS=bar` is now equivalent to + `-sEXPORTED_FUNCTIONS=foo,bar`. This is useful in build systems where + separate components each want to contribute to this list. 3.1.44 - 07/25/23 -----------------
diff --git a/emcc.py b/emcc.py index 085364c..502e70c 100755 --- a/emcc.py +++ b/emcc.py
@@ -54,7 +54,7 @@ from tools import webassembly from tools import config from tools import cache -from tools.settings import user_settings, settings, MEM_SIZE_SETTINGS, COMPILE_TIME_SETTINGS +from tools.settings import user_settings, settings, MEM_SIZE_SETTINGS, COMPILE_TIME_SETTINGS, APPENDING_SETTINGS from tools.utils import read_file, write_file, read_binary, delete_file, removeprefix logger = logging.getLogger('emcc') @@ -417,7 +417,7 @@ setattr(settings, name, new_default) -def apply_user_settings(): +def apply_setting(cmdline_settings): """Take a map of users settings {NAME: VALUE} and apply them to the global settings object. """ @@ -425,7 +425,7 @@ # Stash a copy of all available incoming APIs before the user can potentially override it settings.ALL_INCOMING_MODULE_JS_API = settings.INCOMING_MODULE_JS_API + EXTRA_INCOMING_JS_API - for key, value in user_settings.items(): + for key, value in cmdline_settings: if key in settings.internal_settings: exit_with_error('%s is an internal setting and cannot be set from command line', key) @@ -459,6 +459,10 @@ except Exception as e: exit_with_error('a problem occurred in evaluating the content after a "-s", specifically "%s=%s": %s', key, value, str(e)) + if key in APPENDING_SETTINGS: + value += getattr(settings, key) + + user_settings[user_key] = value setattr(settings, user_key, value) if key == 'EXPORTED_FUNCTIONS': @@ -1426,23 +1430,22 @@ explicit_settings_changes, newargs = parse_s_args(newargs) settings_changes += explicit_settings_changes + cmdline_settings = [] for s in settings_changes: key, value = s.split('=', 1) key, value = normalize_boolean_setting(key, value) - user_settings[key] = value - - # STRICT is used when applying settings so it needs to be applied first before - # calling `apply_user_settings`. - strict_cmdline = user_settings.get('STRICT') - if strict_cmdline: - settings.STRICT = int(strict_cmdline) + # STRICT is used when applying settings so it needs to be applied first before + # calling `apply_setting`. + if key == 'STRICT' and value: + settings.STRICT = int(value) + cmdline_settings.append((key, value)) # Apply user -jsD settings for s in user_js_defines: settings[s[0]] = s[1] # Apply -s settings in newargs here (after optimization levels, so they can override them) - apply_user_settings() + apply_setting(cmdline_settings) return options, newargs @@ -1629,7 +1632,7 @@ # If we get here then the user specified both DISABLE_EXCEPTION_CATCHING and EXCEPTION_CATCHING_ALLOWED # on the command line. This is no longer valid so report either an error or a warning (for # backwards compat with the old `DISABLE_EXCEPTION_CATCHING=2` - if user_settings['DISABLE_EXCEPTION_CATCHING'] in ('0', '2'): + if user_settings['DISABLE_EXCEPTION_CATCHING'] in (0, 2): diagnostics.warning('deprecated', 'DISABLE_EXCEPTION_CATCHING=X is no longer needed when specifying EXCEPTION_CATCHING_ALLOWED') else: exit_with_error('DISABLE_EXCEPTION_CATCHING and EXCEPTION_CATCHING_ALLOWED are mutually exclusive') @@ -1638,9 +1641,9 @@ settings.DISABLE_EXCEPTION_CATCHING = 0 if settings.WASM_EXCEPTIONS: - if user_settings.get('DISABLE_EXCEPTION_CATCHING') == '0': + if user_settings.get('DISABLE_EXCEPTION_CATCHING') == 0: exit_with_error('DISABLE_EXCEPTION_CATCHING=0 is not compatible with -fwasm-exceptions') - if user_settings.get('DISABLE_EXCEPTION_THROWING') == '0': + if user_settings.get('DISABLE_EXCEPTION_THROWING') == 0: exit_with_error('DISABLE_EXCEPTION_THROWING=0 is not compatible with -fwasm-exceptions') # -fwasm-exceptions takes care of enabling them, so users aren't supposed to # pass them explicitly, regardless of their values @@ -1649,7 +1652,7 @@ settings.DISABLE_EXCEPTION_CATCHING = 1 settings.DISABLE_EXCEPTION_THROWING = 1 - if user_settings.get('ASYNCIFY') == '1': + if user_settings.get('ASYNCIFY') == 1: diagnostics.warning('emcc', 'ASYNCIFY=1 is not compatible with -fwasm-exceptions. Parts of the program that mix ASYNCIFY and exceptions will not compile.') if user_settings.get('SUPPORT_LONGJMP') == 'emscripten': @@ -1672,11 +1675,11 @@ # Wasm SjLj cannot be used with Emscripten EH. We error out if # DISABLE_EXCEPTION_THROWING=0 is explicitly requested by the user; # otherwise we disable it here. - if user_settings.get('DISABLE_EXCEPTION_THROWING') == '0': + if user_settings.get('DISABLE_EXCEPTION_THROWING') == 0: exit_with_error('SUPPORT_LONGJMP=wasm cannot be used with DISABLE_EXCEPTION_THROWING=0') # We error out for DISABLE_EXCEPTION_CATCHING=0, because it is 1 by default # and this can be 0 only if the user specifies so. - if user_settings.get('DISABLE_EXCEPTION_CATCHING') == '0': + if user_settings.get('DISABLE_EXCEPTION_CATCHING') == 0: exit_with_error('SUPPORT_LONGJMP=wasm cannot be used with DISABLE_EXCEPTION_CATCHING=0') default_setting('DISABLE_EXCEPTION_THROWING', 1) @@ -1991,7 +1994,7 @@ # For users that opt out of WARN_ON_UNDEFINED_SYMBOLS we assume they also # want to opt out of ERROR_ON_UNDEFINED_SYMBOLS. - if user_settings.get('WARN_ON_UNDEFINED_SYMBOLS') == '0': + if user_settings.get('WARN_ON_UNDEFINED_SYMBOLS') == 0: default_setting('ERROR_ON_UNDEFINED_SYMBOLS', 0) # It is unlikely that developers targeting "native web" APIs with MINIMAL_RUNTIME need
diff --git a/test/test_other.py b/test/test_other.py index 1e6ce27..bf3eabe 100644 --- a/test/test_other.py +++ b/test/test_other.py
@@ -13635,3 +13635,24 @@ '-Wno-experimental', '--extern-post-js', test_file('other/test_memory64_proxies.js')]) self.run_js('a.out.js') + + def test_settings_append(self): + create_file('pre.js', ''' + Module.onRuntimeInitialized = () => { + _foo(); + } + ''') + create_file('test.c', r''' + #include <stdio.h> + + void foo() { + printf("foo\n"); + } + + int main() { + printf("main\n"); + return 0; + } + ''') + expected = 'foo\nmain\n' + self.do_runf('test.c', expected, emcc_args=['--pre-js=pre.js', '-sEXPORTED_FUNCTIONS=_foo', '-sEXPORTED_FUNCTIONS=_main'])
diff --git a/tools/settings.py b/tools/settings.py index f890a53..7862071 100644 --- a/tools/settings.py +++ b/tools/settings.py
@@ -85,6 +85,20 @@ 'RUNTIME_LINKED_LIBS', }.union(PORTS_SETTINGS) +# Settings for which repeated occurances add to a list rather then replacing +# the current one. +APPENDING_SETTINGS = { + 'EXPORTED_FUNCTIONS', + 'DEFAULT_LIBRARY_FUNCS_TO_INCLUDE', + 'EXPORTED_RUNTIME_METHODS', + 'SIGNATURE_CONVERSIONS', + 'EXCEPTION_CATCHING_ALLOWED', + 'ASYNCIFY_IMPORTS', + 'ASYNCIFY_REMOVE', + 'ASYNCIFY_ADD', + 'ASYNCIFY_ONLY', + 'ASYNCIFY_EXPORTS', +} # Settings that don't need to be externalized when serializing to json because they # are not used by the JS compiler.