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.