Merge branch 'main' into shared-wasmgc-pthread-create
diff --git a/em++.py b/em++.py
index 2ab74f0..407180d 100755
--- a/em++.py
+++ b/em++.py
@@ -7,9 +7,6 @@
 import sys
 
 import emcc
-from tools import shared
-
-shared.run_via_emxx = True
 
 if __name__ == '__main__':
   try:
diff --git a/emcc.py b/emcc.py
index 6ca8497..9bf2e2d 100644
--- a/emcc.py
+++ b/emcc.py
@@ -43,6 +43,7 @@
   compile,
   config,
   diagnostics,
+  ports,
   shared,
   system_libs,
   utils,
@@ -195,12 +196,98 @@
     return output.strip()
 
 
+def get_clang():
+  if shared.run_via_emxx:
+    return shared.CLANG_CXX
+  else:
+    return shared.CLANG_CC
+
+
+def handle_early_exit_flags(args, newargs):
+  if '--version' in args:
+    print(cmdline.version_string())
+    print('''\
+Copyright (C) 2026 the Emscripten authors (see AUTHORS.txt)
+This is free and open source software under the MIT license.
+There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+''')
+    return True
+
+  if '-dumpversion' in args: # gcc's doc states "Print the compiler version [...] and don't do anything else."
+    print(utils.EMSCRIPTEN_VERSION)
+    return True
+
+  # Sadly we cannot rely on PASSTHROUGH_FLAGS for -print-search-dirs or -print-libgcc-file-name
+  # because there is no way to tell clang today about our custom library paths.
+  # TODO: Teach clang about emscripten's library layout so we can remove this code.
+  if '-print-search-dirs' in args or '--print-search-dirs' in args:
+    print(f'programs: ={config.LLVM_ROOT}')
+    resource_dir = get_clang_resource_dir(args)
+    libdir = cache.get_lib_dir(absolute=True)
+    print(f'libraries: ={resource_dir}{os.pathsep}{libdir}')
+    return True
+
+  if '-print-libgcc-file-name' in args or '--print-libgcc-file-name' in args:
+    settings.limit_settings(None)
+    clang_rt = system_libs.Library.get_usable_variations()['libclang_rt.builtins']
+    print(clang_rt.get_path(absolute=True))
+    return True
+
+  print_file_name = [a for a in args if a.startswith(('-print-file-name=', '--print-file-name='))]
+  if print_file_name:
+    libname = print_file_name[-1].split('=')[1]
+    resource_dir = get_clang_resource_dir(args)
+    system_libpath = cache.get_lib_dir(absolute=True)
+    for dirname in (resource_dir, system_libpath):
+      fullpath = os.path.join(dirname, libname)
+      if os.path.isfile(fullpath):
+        print(fullpath)
+        break
+    else:
+      print(libname)
+    return True
+
+  if any(a in PASSTHROUGH_FLAGS for a in args) or any(a.startswith(p) for p in PASSTHROUGH_PREFIXES for a in args):
+    # For several -print-xxx-name flags we just defer to clang rather than
+    # trying to re-implement the logic.
+    shared.exec_process([get_clang(), *compile.get_cflags(tuple(args)), *newargs])
+    assert False, 'exec_process should not return'
+
+  if options.clear_cache:
+    logger.info('clearing cache as requested by --clear-cache: `%s`', cache.cachedir)
+    cache.erase()
+    shared.perform_sanity_checks() # this is a good time for a sanity check
+    return True
+
+  if options.clear_ports:
+    logger.info('clearing ports and cache as requested by --clear-ports')
+    ports.clear()
+    cache.erase()
+    shared.perform_sanity_checks() # this is a good time for a sanity check
+    return True
+
+  if options.check:
+    print(cmdline.version_string(), file=sys.stderr)
+    shared.check_sanity(force=True)
+    return True
+
+  if options.show_ports:
+    ports.show_ports()
+    return True
+
+  if '--cflags' in args:
+    # Just print the flags we pass to clang and exit.  We need to do this after
+    # phase_setup because the setup sets things like SUPPORT_LONGJMP.
+    cflags = compile.get_cflags(x for x in args if x != '--cflags')
+    print(shlex.join(cflags))
+    return True
+
+  return False
+
+
 @ToolchainProfiler.profile()
 def main(args):
-  if shared.run_via_emxx:
-    clang = shared.CLANG_CXX
-  else:
-    clang = shared.CLANG_CC
+  shared.run_via_emxx = os.path.basename(args[0]).startswith('em++')
 
   # Special case the handling of `-v` because it has a special/different meaning
   # when used with no other arguments.  In particular, we must handle this early
@@ -210,7 +297,7 @@
   if len(args) == 2 and args[1] == '-v':
     # autoconf likes to see 'GNU' in the output to enable shared object support
     print(cmdline.version_string(), file=sys.stderr)
-    return shared.check_call([clang, '-v', *compile.get_target_flags()], check=False).returncode
+    return shared.check_call([get_clang(), '-v', *compile.get_target_flags()], check=False).returncode
 
   # Additional compiler flags that we treat as if they were passed to us on the
   # commandline
@@ -255,75 +342,18 @@
   if not shared.SKIP_SUBPROCS:
     shared.check_sanity()
 
+  for port in options.use_ports:
+    ports.handle_use_port_arg(settings, port)
+
   # For internal consistency, ensure we don't attempt to read or write any link time
   # settings until we reach the linking phase.
   settings.limit_settings(COMPILE_TIME_SETTINGS)
 
   phase_setup(state)
 
-  # Begin early-exit flag handling.
-
-  if '--version' in args:
-    print(cmdline.version_string())
-    print('''\
-Copyright (C) 2026 the Emscripten authors (see AUTHORS.txt)
-This is free and open source software under the MIT license.
-There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-''')
+  if handle_early_exit_flags(args, newargs):
     return 0
 
-  if '-dumpversion' in args: # gcc's doc states "Print the compiler version [...] and don't do anything else."
-    print(utils.EMSCRIPTEN_VERSION)
-    return 0
-
-  # Sadly we cannot rely on PASSTHROUGH_FLAGS for -print-search-dirs or -print-libgcc-file-name
-  # because there is no way to tell clang today about our custom library paths.
-  # TODO: Teach clang about emscripten's library layout so we can remove this code.
-  if '-print-search-dirs' in args or '--print-search-dirs' in args:
-    print(f'programs: ={config.LLVM_ROOT}')
-    resource_dir = get_clang_resource_dir(args)
-    libdir = cache.get_lib_dir(absolute=True)
-    print(f'libraries: ={resource_dir}{os.pathsep}{libdir}')
-    return 0
-
-  if '-print-libgcc-file-name' in args or '--print-libgcc-file-name' in args:
-    settings.limit_settings(None)
-    clang_rt = system_libs.Library.get_usable_variations()['libclang_rt.builtins']
-    print(clang_rt.get_path(absolute=True))
-    return 0
-
-  print_file_name = [a for a in args if a.startswith(('-print-file-name=', '--print-file-name='))]
-  if print_file_name:
-    libname = print_file_name[-1].split('=')[1]
-    resource_dir = get_clang_resource_dir(args)
-    system_libpath = cache.get_lib_dir(absolute=True)
-    for dirname in (resource_dir, system_libpath):
-      fullpath = os.path.join(dirname, libname)
-      if os.path.isfile(fullpath):
-        print(fullpath)
-        break
-    else:
-      print(libname)
-    return 0
-
-  if 'EMCC_REPRODUCE' in os.environ:
-    options.reproduce = os.environ['EMCC_REPRODUCE']
-
-  if any(a in PASSTHROUGH_FLAGS for a in args) or any(a.startswith(p) for p in PASSTHROUGH_PREFIXES for a in args):
-    # For several -print-xxx-name flags we just defer to clang rather than
-    # trying to re-implement the logic.
-    shared.exec_process([clang, *compile.get_cflags(tuple(args)), *newargs])
-    assert False, 'exec_process should not return'
-
-  if '--cflags' in args:
-    # Just print the flags we pass to clang and exit.  We need to do this after
-    # phase_setup because the setup sets things like SUPPORT_LONGJMP.
-    cflags = compile.get_cflags(x for x in args if x != '--cflags')
-    print(shlex.join(cflags))
-    return 0
-
-  # End early-exit flag handling
-
   if options.reproduce:
     create_reproduce_file(options.reproduce, args)
 
@@ -493,10 +523,7 @@
 
 @ToolchainProfiler.profile_block('compile inputs')
 def phase_compile_inputs(state, newargs):
-  if shared.run_via_emxx:
-    compiler = [shared.CLANG_CXX]
-  else:
-    compiler = [shared.CLANG_CC]
+  compiler = [get_clang()]
 
   if config.COMPILER_WRAPPER:
     logger.debug('using compiler wrapper: %s', config.COMPILER_WRAPPER)
diff --git a/src/lib/libbrowser.js b/src/lib/libbrowser.js
index 5844baa..7e37cf9 100644
--- a/src/lib/libbrowser.js
+++ b/src/lib/libbrowser.js
@@ -259,7 +259,9 @@
       if (!Browser.fullscreenHandlersInstalled) {
         Browser.fullscreenHandlersInstalled = true;
         document.addEventListener('fullscreenchange', fullscreenChange);
+#if MIN_SAFARI_VERSION < 160400
         document.addEventListener('webkitfullscreenchange', fullscreenChange);
+#endif
       }
 
       // create a new parent to ensure the canvas has no siblings. this allows browsers to optimize full screen performance when its parent is the full screen root
diff --git a/test/emsdk_version.txt b/test/emsdk_version.txt
index 787e37c..8dfa358 100644
--- a/test/emsdk_version.txt
+++ b/test/emsdk_version.txt
@@ -1 +1 @@
-ded6d7cb437bc9b669358e096340af318db11038
+f121500928924d2a62bae43f4d6f07045fd17993
diff --git a/test/sockets/webrtc_host.c b/test/sockets/webrtc_host.c
deleted file mode 100644
index cdbc3ee..0000000
--- a/test/sockets/webrtc_host.c
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright 2013 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 <errno.h>
-#include <sys/types.h>
-#include <sys/socket.h>
-#include <netinet/in.h>
-#include <arpa/inet.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-#include <sys/ioctl.h>
-#include <assert.h>
-#ifdef __EMSCRIPTEN__
-#include <emscripten.h>
-#endif
-
-#define EXPECTED_BYTES 5
-#define BUFLEN 16
-
-int result = 0;
-int sock;
-char buf[BUFLEN];
-char expected[] = "emscripten";
-struct sockaddr_in si_host,
-                   si_peer;
-struct iovec iov[1];
-struct msghdr hdr;
-int done = 0;
-
-void iter() {
-  int n;
-  n = recvmsg(sock, &hdr, 0);
-
-  if(0 < n) {
-    done = 1;
-    fprintf(stderr, "received %d bytes: %s", n, (char*)hdr.msg_iov[0].iov_base);
-
-    shutdown(sock, SHUT_RDWR);
-    close(sock);
-
-#ifdef __EMSCRIPTEN__
-    if(strlen((char*)hdr.msg_iov[0].iov_base) == strlen(expected) &&
-       0 == strncmp((char*)hdr.msg_iov[0].iov_base, expected, strlen(expected))) {
-      result = 1;
-    }
-    REPORT_RESULT(result);
-    exit(EXIT_SUCCESS);
-    emscripten_cancel_main_loop();
-#endif
-  } else if(EWOULDBLOCK != errno) {
-    perror("recvmsg failed");
-    exit(EXIT_FAILURE);
-    emscripten_cancel_main_loop();
-  }
-}
-
-int main(void)
-{
-  memset(&si_host, 0, sizeof(struct sockaddr_in));
-  memset(&si_peer, 0, sizeof(struct sockaddr_in));
-
-  si_host.sin_family = AF_INET;
-  si_host.sin_port = htons(8991);
-  si_host.sin_addr.s_addr = htonl(INADDR_ANY);
-
-  if(-1 == (sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP))) {
-    perror("cannot create host socket");
-    exit(EXIT_FAILURE);
-  }
-
-  if(-1 == bind(sock, (struct sockaddr*)&si_host, sizeof(struct sockaddr))) {
-    perror("cannot bind host socket");
-    exit(EXIT_FAILURE);
-  }
-
-  iov[0].iov_base = buf;
-  iov[0].iov_len = sizeof(buf);
-
-  memset (&hdr, 0, sizeof (struct msghdr));
-
-  hdr.msg_name = &si_peer;
-  hdr.msg_namelen = sizeof(struct sockaddr_in);
-  hdr.msg_iov = iov;
-  hdr.msg_iovlen = 1;
-
-#ifdef __EMSCRIPTEN__
-  emscripten_set_main_loop(iter, 0, 0);
-#else
-  while (!done) iter();
-#endif
-
-  return EXIT_SUCCESS;
-}
diff --git a/test/sockets/webrtc_peer.c b/test/sockets/webrtc_peer.c
deleted file mode 100644
index 0b827d3..0000000
--- a/test/sockets/webrtc_peer.c
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 2013 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 <errno.h>
-#include <sys/types.h>
-#include <sys/socket.h>
-#include <netinet/in.h>
-#include <arpa/inet.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-#include <sys/ioctl.h>
-#include <assert.h>
-#ifdef __EMSCRIPTEN__
-#include <emscripten.h>
-#endif
-
-#define EXPECTED_BYTES 5
-#define BUFLEN 16
-#define HOST_ADDR "10.0.0.1"
-
-int result = 0;
-int sock;
-char buf[16] = "emscripten";
-struct sockaddr_in si_host;
-struct iovec iov[1];
-struct msghdr hdr;
-int done = 0;
-
-void iter() {
-  int n;
-  n = sendmsg(sock, &hdr, 0);
-
-  if(0 < n) {
-    done = 1;
-    fprintf(stderr, "sent %d bytes: %s", n, (char*)hdr.msg_iov[0].iov_base);
-
-    shutdown(sock, SHUT_RDWR);
-    close(sock);
-
-    exit(EXIT_SUCCESS);
-    emscripten_cancel_main_loop();
-  } else if(EWOULDBLOCK != errno) {
-    perror("sendmsg failed");
-    exit(EXIT_FAILURE);
-    emscripten_cancel_main_loop();
-  }
-}
-
-int main(void)
-{
-  memset(&si_host, 0, sizeof(struct sockaddr_in));
-
-  si_host.sin_family = AF_INET;
-  si_host.sin_port = htons(8991);
-  if(0 == inet_pton(AF_INET, HOST_ADDR, &si_host.sin_addr)) {
-    perror("inet_aton failed");
-    exit(EXIT_FAILURE);
-  }
-
-  if(-1 == (sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP))) {
-    perror("cannot create socket");
-    exit(EXIT_FAILURE);
-  }
-
-  iov[0].iov_base = buf;
-  iov[0].iov_len = sizeof(buf);
-
-  memset (&hdr, 0, sizeof (struct msghdr));
-
-  hdr.msg_name = &si_host;
-  hdr.msg_namelen = sizeof(struct sockaddr_in);
-  hdr.msg_iov = iov;
-  hdr.msg_iovlen = 1;
-
-#ifdef __EMSCRIPTEN__
-  emscripten_set_main_loop(iter, 0, 0);
-#else
-  while (!done) iter();
-#endif
-
-  return EXIT_SUCCESS;
-}
diff --git a/test/test_other.py b/test/test_other.py
index 41421b3..92913a0 100644
--- a/test/test_other.py
+++ b/test/test_other.py
@@ -14314,10 +14314,9 @@
     expected = 'Stack overflow! Stack cookie has been overwritten at 0x[a-f0-9]*, expected hex dwords 0x89bacdfe and 0x02135467, but received 0xaaaaaaa0 0xfffffff0'
     self.do_runf('test.c', expected, regex=True, cflags=args + ['-sSTACK_OVERFLOW_CHECK=1'], assert_returncode=NON_ZERO)
 
-  @crossplatform
-  def test_reproduce(self):
+  def do_test_reproduce(self, args):
     ensure_dir('tmp')
-    self.run_process([EMCC, '-sASSERTIONS=1', '--reproduce=foo.tar', '-otmp/out.js', test_file('hello_world.c')])
+    self.run_process([EMCC, '-sASSERTIONS=1', '-otmp/out.js', test_file('hello_world.c'), *args])
     self.assertExists('foo.tar')
     names = []
     root = os.path.splitdrive(path_from_root())[1][1:]
@@ -14346,6 +14345,14 @@
     response = response.replace(root, '<root>')
     self.assertTextDataIdentical(expected, response)
 
+  @crossplatform
+  def test_reproduce(self):
+    self.do_test_reproduce(['--reproduce=foo.tar'])
+
+  @with_env_modify({'EMCC_REPRODUCE': 'foo.tar'})
+  def test_reproduce_env(self):
+    self.do_test_reproduce([])
+
   def test_min_browser_version(self):
     expected = 'emcc: error: MIN_SAFARI_VERSION=140100 is not compatible with WASM_BIGINT (MIN_SAFARI_VERSION=150000 or above required)'
     self.assert_fail([EMCC, test_file('hello_world.c'), '-Werror', '-sWASM_BIGINT', '-sMIN_SAFARI_VERSION=140100'], expected)
diff --git a/tools/cmdline.py b/tools/cmdline.py
index e77e5f6..1b6d02b 100644
--- a/tools/cmdline.py
+++ b/tools/cmdline.py
@@ -8,7 +8,6 @@
 import os
 import re
 import shlex
-import sys
 from enum import Enum, auto, unique
 from subprocess import PIPE
 
@@ -53,6 +52,9 @@
 
 
 class EmccOptions:
+  check = False
+  clear_cache = False
+  clear_ports = False
   cpu_profiler = False
   dash_E = False
   dash_M = False
@@ -94,10 +96,11 @@
   pre_js: list[str] = [] # before all js
   preload_files: list[str] = []
   relocatable = False
-  reproduce = None
+  reproduce = os.getenv('EMCC_REPRODUCE')  # None by default.
   requested_debug = None
   sanitize: set[str] = set()
   sanitize_minimal_runtime = False
+  show_ports = False
   s_args: list[str] = []
   save_temps = False
   shared = False
@@ -106,6 +109,7 @@
   syntax_only = False
   target = ''
   use_closure_compiler = None
+  use_ports: list[str] = []
   use_preload_cache = False
   use_preload_plugins = False
   valid_abspaths: list[str] = []
@@ -204,10 +208,6 @@
 
   To revalidate these numbers, run `ruff check --select=C901,PLR091`.
   """
-  # TODO(sbc): Remove this import, or move it to the top, once we resolve the
-  # circular dependency issue with ports/__init__.py -> system_libs.py -> cmdline.py
-  from tools import ports
-  should_exit = False
   skip = False
   builtin_settings = set(settings.keys())
   LEGACY_ARGS = {'--js-opts', '--llvm-opts', '--llvm-lto', '--memory-init-file'}
@@ -265,7 +265,7 @@
     def consume_arg_file():
       name = consume_arg()
       if not os.path.isfile(name):
-        exit_with_error("'%s': file not found: '%s'" % (arg, name))
+        exit_with_error(f"'{arg}': file not found: '{name}'")
       return name
 
     if arg in LEGACY_FLAGS:
@@ -383,7 +383,7 @@
             diagnostics.warning('deprecated', 'please replace -g4 with -gsource-map')
             settings.GENERATE_SOURCE_MAP = 1
           elif debug_level > 4:
-            exit_with_error("unknown argument: '%s'", arg)
+            exit_with_error(f"unknown argument: '{arg}'")
       else:
         if debug_level.startswith('force_dwarf'):
           exit_with_error('gforce_dwarf was a temporary option and is no longer necessary (use -g)')
@@ -460,23 +460,13 @@
       # libraries)
       os.environ['EM_CACHE'] = config.CACHE
     elif check_flag('--clear-cache'):
-      logger.info('clearing cache as requested by --clear-cache: `%s`', cache.cachedir)
-      cache.erase()
-      shared.perform_sanity_checks() # this is a good time for a sanity check
-      should_exit = True
+      options.clear_cache = True
     elif check_flag('--clear-ports'):
-      logger.info('clearing ports and cache as requested by --clear-ports')
-      ports.clear()
-      cache.erase()
-      shared.perform_sanity_checks() # this is a good time for a sanity check
-      should_exit = True
+      options.clear_ports = True
     elif check_flag('--check'):
-      print(version_string(), file=sys.stderr)
-      shared.check_sanity(force=True)
-      should_exit = True
+      options.check = True
     elif check_flag('--show-ports'):
-      ports.show_ports()
-      should_exit = True
+      options.show_ports = True
     elif check_arg('--valid-abspath'):
       options.valid_abspaths.append(consume_arg())
     elif arg.startswith(('-I', '-L')):
@@ -587,7 +577,7 @@
       if options.target not in {'wasm32', 'wasm64', 'wasm64-unknown-emscripten', 'wasm32-unknown-emscripten'}:
         exit_with_error(f'unsupported target: {options.target} (emcc only supports wasm64-unknown-emscripten and wasm32-unknown-emscripten)')
     elif check_arg('--use-port'):
-      ports.handle_use_port_arg(settings, consume_arg())
+      options.use_ports.append(consume_arg())
     elif arg in {'-c', '--precompile'}:
       options.dash_c = True
     elif arg == '-S':
@@ -624,9 +614,6 @@
     elif arg and (arg == '-' or not arg.startswith('-')):
       options.input_files.append(arg)
 
-  if should_exit:
-    sys.exit(0)
-
   return [a for a in newargs if a]
 
 
@@ -635,7 +622,7 @@
   value = value.strip()
   match = re.match(r'^(\d+)\s*([kmgt]?b)?$', value, re.I)
   if not match:
-    exit_with_error("invalid byte size `%s`.  Valid suffixes are: kb, mb, gb, tb" % value)
+    exit_with_error(f'invalid byte size `{value}`.  Valid suffixes are: kb, mb, gb, tb')
   value, suffix = match.groups()
   value = int(value)
   if suffix: