From f7f4ca4cf4f64a1d3311dc8869c7bd8bae3175c9 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Wed, 12 Nov 2025 16:57:21 -0800 Subject: [PATCH 1/4] Enable real dynamic linking with `-shared` by default This change essentially disables `FAKE_DYLIBS` by default, which in turn means `-shared` will now produce dynamic libraries by default. If you want the old behaviour you now need `-sFAKE_DYLIBS`. --- ChangeLog.md | 5 ++ cmake/Modules/Platform/Emscripten.cmake | 9 +- .../source/docs/compiling/Dynamic-Linking.rst | 21 +++-- .../tools_reference/settings_reference.rst | 12 +-- src/settings.js | 12 +-- test/common.py | 6 +- test/test_core.py | 40 ++++++--- test/test_other.py | 86 ++++++++----------- tools/building.py | 16 ++-- tools/cmdline.py | 3 +- tools/link.py | 13 +-- 11 files changed, 120 insertions(+), 103 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 406092d387896..a9b3f4e8fb396 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -49,6 +49,11 @@ See docs/process.md for more on how version tagging works. e.g. Intel x64 Macs and Windows-on-ARM, downloading Java SE Development Kit 21.0.11 from https://www.oracle.com/europe/java/technologies/downloads/#java21 is required in order to use Emscripten's Closure Compiler integration. +- The `FAKE_DYLIBS` setting is now disabled by default. This means that + `-shared` will produce real dynamic libraries by default (`-sSIDE_MODULE` is + implied). Also, if you include real dynamic libraries in your link comment + emscripten will now automatically produce a dynamically linked program + (`-sMAIN_MODULE=2` is implied). (#25930) 5.0.7 - 04/30/26 ---------------- diff --git a/cmake/Modules/Platform/Emscripten.cmake b/cmake/Modules/Platform/Emscripten.cmake index 16a3b6583706d..9919234ef0212 100644 --- a/cmake/Modules/Platform/Emscripten.cmake +++ b/cmake/Modules/Platform/Emscripten.cmake @@ -18,7 +18,14 @@ set(CMAKE_SYSTEM_NAME Emscripten) set(CMAKE_SYSTEM_VERSION 1) set(CMAKE_CROSSCOMPILING TRUE) -set_property(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS FALSE) + +# Certain cmake versions are not compatible with dynnamic linking due to +# https://gitlab.kitware.com/cmake/cmake/-/work_items/27240 +if (("${CMAKE_VERSION}" VERSION_GREATER_EQUAL "4.2.0" AND "${CMAKE_VERSION}" VERSION_LESS "4.2.6") OR + ("${CMAKE_VERSION}" VERSION_GREATER_EQUAL "4.3.0" AND "${CMAKE_VERSION}" VERSION_LESS "4.3.3")) + message(WARNING "This version of cmake (${CMAKE_VERSION}) does not support emscripten shared libraries. Use cmake < 4.2.0 or cmake > 4.3.3 if you need shared library support") + set_property(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS FALSE) +endif() # Advertise Emscripten as a 32-bit platform (as opposed to # CMAKE_SYSTEM_PROCESSOR=x86_64 for 64-bit platform), since some projects (e.g. diff --git a/site/source/docs/compiling/Dynamic-Linking.rst b/site/source/docs/compiling/Dynamic-Linking.rst index 82cea6e8260f9..b443152fa1e44 100644 --- a/site/source/docs/compiling/Dynamic-Linking.rst +++ b/site/source/docs/compiling/Dynamic-Linking.rst @@ -111,7 +111,8 @@ before your application starts to run. - Build one part of your code as the main module, linking it using ``-sMAIN_MODULE`` (See :ref:`MAIN_MODULE`). - Build other parts of your code as side modules, linking it using - ``-sSIDE_MODULE`` (See :ref:`SIDE_MODULE`). + ``-shared``. You can also used the emscripten-specific :ref:`SIDE_MODULE` + setting which does the same thing by default. For the main module the output suffix should be ``.js`` (the WebAssembly file will be generated alongside it just like normal). For the side @@ -149,19 +150,17 @@ Building Dynamic Libraries using ``-shared`` ============================================ In traditional toolchains the ``-shared`` flag is used to generated dynamic -libraries. However, because dynamic linking in Emscripten comes with caveats -and has some overhead, Emscripten does not currently produce real dynamic -libraries when this flag is used. Instead, Emscripten will produce a fake -dynamic library (along with a warning) that is actually a single static object -file. When your main program is linked against this fake dynamic library it -gets linked into your main program like any other object file. +libraries. Historically, due to early limitations, Emscripten would produce a +fake dynamic library (along with a warning) when the ``-shared`` flag was used. +When your main program is linked against these fake dynamic library they would +be linked into your main program like regular static other object files. -The reason for this behaviour is to allow projects (and build systems) that -assume a working ``-shared`` flag to build successfully (albeit using static -linking). +These days Emscripten will produce real dynamic libraries by default and +``-shared`` is essentially the same as ``-sSIDE_MODULE``. This behaviour can be controlled using the :ref:`FAKE_DYLIBS` settings. If you -disable `FAKE_DYLIBS` then ``-shared`` will act like ``-sSIDE_MODULE``. +prefer the older behaviour with fake dynamic libraryies you can enable this +setting. Code Size ========= diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 18a330773defe..0352e6f3d7da7 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3356,13 +3356,13 @@ Default value: false FAKE_DYLIBS =========== -This setting changes the behaviour of the ``-shared`` flag. The default -setting of ``true`` means the ``-shared`` flag actually produces a normal -object file (i.e. ``ld -r``). Setting this to false will cause ``-shared`` -to behave like :ref:`SIDE_MODULE` and produce a dynamically linked -library. +This setting changes the behaviour of the ``-shared`` flag. When set to true +you get the old emscripten behaviour where the ``-shared`` flag actually +produces a normal object file (i.e. ``ld -r``). When set to true (the +default) the ``-shared`` flag is equivelent to :ref:`SIDE_MODULE` and will +produce a Wasn dynamic library. -Default value: true +Default value: false .. _executable: diff --git a/src/settings.js b/src/settings.js index 80e2e72989245..6ab6bdbbcd5de 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2201,12 +2201,12 @@ var GROWABLE_ARRAYBUFFERS = false; // indirectly using `importScripts` var CROSS_ORIGIN = false; -// This setting changes the behaviour of the ``-shared`` flag. The default -// setting of ``true`` means the ``-shared`` flag actually produces a normal -// object file (i.e. ``ld -r``). Setting this to false will cause ``-shared`` -// to behave like :ref:`SIDE_MODULE` and produce a dynamically linked -// library. -var FAKE_DYLIBS = true; +// This setting changes the behaviour of the ``-shared`` flag. When set to true +// you get the old emscripten behaviour where the ``-shared`` flag actually +// produces a normal object file (i.e. ``ld -r``). When set to true (the +// default) the ``-shared`` flag is equivelent to :ref:`SIDE_MODULE` and will +// produce a Wasn dynamic library. +var FAKE_DYLIBS = false; // Add a #! line to generated JS file and make it executable. This is useful // for building command line tools that run under node. diff --git a/test/common.py b/test/common.py index 740b5aaca2372..bd810f69eef5e 100644 --- a/test/common.py +++ b/test/common.py @@ -1517,7 +1517,7 @@ def get_poppler_library(self, env_init=None): return poppler + freetype - def get_zlib_library(self, cmake, cflags=None): + def get_zlib_library(self, cmake, cflags=None, target='libz.a'): assert cmake or not WINDOWS, 'on windows, get_zlib_library only supports cmake' old_args = self.cflags.copy() @@ -1531,12 +1531,12 @@ def get_zlib_library(self, cmake, cflags=None): # https://github.com/emscripten-core/emscripten/issues/16908 is fixed self.cflags.append('-Wno-pointer-sign') if cmake: - rtn = self.get_library(os.path.join('third_party', 'zlib'), os.path.join('libz.a'), + rtn = self.get_library(os.path.join('third_party', 'zlib'), target, configure=['cmake', '.'], make=['cmake', '--build', '.', '--'], make_args=[]) else: - rtn = self.get_library(os.path.join('third_party', 'zlib'), os.path.join('libz.a'), make_args=['libz.a']) + rtn = self.get_library(os.path.join('third_party', 'zlib'), target, make_args=['libz.a', target]) self.cflags = old_args return rtn diff --git a/test/test_core.py b/test/test_core.py index c0fa0736b390c..bdbba9497412b 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -4051,8 +4051,8 @@ def dylink_testf(self, main, side=None, expected=None, force_c=False, main_cflag shutil.move(so_file, so_file + '.orig') - # Verify that building with -sSIDE_MODULE is essentially the same as building with `-shared -fPIC -sFAKE_DYLIBS=0`. - flags = ['-shared', '-fPIC', '-sFAKE_DYLIBS=0'] + # Verify that building with -sSIDE_MODULE is essentially the same as building with `-shared -fPIC` + flags = ['-shared', '-fPIC'] if isinstance(side, list): # side is just a library self.run_process([EMCC] + side + self.get_cflags() + flags + ['-o', so_file]) @@ -5079,7 +5079,8 @@ def do_run(src, expected_output, cflags=None): @with_dylink_reversed def test_dylink_dot_a(self): - # .a linking must force all .o files inside it, when in a shared module + # Tests that it's possible to link a .a archive into a dynamic library using + # `-Wl,--whole-archive` create_file('third.c', 'int sidef() { return 36; }') create_file('fourth.c', 'int sideg() { return 17; }') @@ -5097,7 +5098,7 @@ def test_dylink_dot_a(self): } ''', # contents of libfourth.a must be included, even if they aren't referred to! - side=['libfourth.a', 'third.o'], + side=['-Wl,--whole-archive', 'libfourth.a', '-Wl,--no-whole-archive', 'third.o'], expected=['sidef: 36, sideg: 17.\n'], force_c=True) @with_dylink_reversed @@ -5141,16 +5142,33 @@ def test_dylink_spaghetti(self): ''']) @needs_make('mingw32-make') - @with_dylink_reversed - def test_dylink_zlib(self): - zlib_archive = self.get_zlib_library(cmake=WINDOWS, cflags=['-fPIC']) + @needs_dylink + @parameterized({ + # Cmake support for dyanamic libraries is currently broken. + # which means cmake won't build shared libraries yet. + # https://gitlab.kitware.com/cmake/cmake/-/work_items/27240 + 'cmake': (True,), + 'configure': (False,), + }) + def test_dylink_zlib(self, cmake): + if cmake: + output = self.run_process(['cmake', '--version'], stdout=PIPE).stdout + cmake_version = output.splitlines()[0].split()[-1].strip() + cmake_version = tuple(int(part) for part in cmake_version.split('.')) + # We don't support dynamic linking with certain versions of cmake + # See https://gitlab.kitware.com/cmake/cmake/-/work_items/27240 + if (cmake_version >= (4, 2, 0) and cmake_version < (4, 2, 6)) or \ + (cmake_version >= (4, 3, 0) and cmake_version < (4, 3, 3)): + self.skipTest(f'incompatible cmake version {cmake_version}') + zlib_archive = self.get_zlib_library(cmake=cmake, target='libz.so.1.2.5', cflags=['-fPIC'])[0] + zlib_basename = os.path.basename(zlib_archive) + shutil.copyfile(zlib_archive, zlib_basename) # example.c uses K&R style function declarations self.cflags.append('-Wno-deprecated-non-prototype') self.cflags.append('-I' + test_file('third_party/zlib')) - self.dylink_test(main=read_file(test_file('third_party/zlib/example.c')), - side=zlib_archive, - expected=read_file(test_file('core/test_zlib.out')), - force_c=True) + self.do_runf(test_file('third_party/zlib/example.c'), + read_file(test_file('core/test_zlib.out')), + cflags=['-L.', zlib_archive]) # @with_dylink_reversed # def test_dylink_bullet(self): diff --git a/test/test_other.py b/test/test_other.py index fd278b852d4d9..baa933aa8c38e 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -932,7 +932,8 @@ def test_cmake_explicit_generator(self): # use -Wno-dev to suppress an irrelevant warning about the test files only. cmd = [EMCMAKE, 'cmake', '-GNinja', '-Wno-dev', test_file('cmake/cpp_lib')] self.run_process(cmd) - self.assertExists(self.get_dir() + '/build.ninja') + self.assertExists('build.ninja') + self.run_process(['ninja', '-v']) # Tests that it's possible to pass C++17 or GNU++17 build modes to CMake by building code that # needs C++17 (embind) @@ -1175,7 +1176,7 @@ def test_odd_suffixes(self): for suffix in ('lo',): self.clear() print(suffix) - self.run_process([EMCC, test_file('hello_world.c'), '-shared', '-o', 'binary.' + suffix]) + self.run_process([EMCC, test_file('hello_world.c'), '-shared', '-fPIC', '-o', 'binary.' + suffix]) self.run_process([EMCC, 'binary.' + suffix]) self.assertContained('Hello, world!', self.run_js('a.out.js')) @@ -1360,11 +1361,11 @@ def test_multiply_defined_libsymbols(self): # we model shared libraries using regular object files. Without special handling # fake `libA.so` could get linked multiple times. self.cflags.remove('-Werror') - self.emcc('libA.c', ['-shared', '-o', 'libA.so']) + self.emcc('libA.c', ['-sFAKE_DYLIBS', '-shared', '-fPIC', '-o', 'libA.so']) - err = self.emcc('a2.c', ['-shared', '-L.', '-lA', '-o', 'liba2.so'], stderr=PIPE).stderr + err = self.emcc('a2.c', ['-sFAKE_DYLIBS', '-shared', '-fPIC', '-L.', '-lA', '-o', 'liba2.so'], stderr=PIPE).stderr self.assertContained('emcc: warning: ignoring dynamic library libA.so when generating an object file', err) - err = self.emcc('b2.c', ['-shared', '-L.', '-lA', '-o', 'libb2.so'], stderr=PIPE).stderr + err = self.emcc('b2.c', ['-sFAKE_DYLIBS', '-shared', '-fPIC', '-L.', '-lA', '-o', 'libb2.so'], stderr=PIPE).stderr self.assertContained('emcc: warning: ignoring dynamic library libA.so when generating an object file', err) self.do_runf('main.c', 'result: 1', cflags=['-L.', '-lA', 'liba2.so', 'libb2.so']) @@ -1495,7 +1496,12 @@ def test_link_group_bitcode(self): # We deliberately ignore duplicate input files in order to allow # "libA.so" on the command line twice. This is not really .so support # and the .so files are really object files. - def test_redundant_link(self): + @parameterized({ + '': ([],), + 'fake_dylibs': (['-sFAKE_DYLIBS'],), + }) + def test_redundant_link(self, args): + self.cflags += args create_file('libA.c', 'int mult() { return 1; }') create_file('main.c', r''' #include @@ -1507,7 +1513,7 @@ def test_redundant_link(self): ''') self.cflags.remove('-Werror') - self.emcc('libA.c', ['-shared', '-o', 'libA.so']) + self.emcc('libA.c', ['-fPIC', '-shared', '-o', 'libA.so']) self.emcc('main.c', ['libA.so', 'libA.so', '-o', 'a.out.js']) self.assertContained('result: 1', self.run_js('a.out.js')) @@ -2155,9 +2161,9 @@ def test_multidynamic_link(self, link_flags, lib_suffix): ''') # Build libfile normally into an .so - self.run_process([EMCC, 'libdir/libfile.c', '-shared', '-o', 'libdir/libfile.so' + lib_suffix]) + self.run_process([EMCC, 'libdir/libfile.c', '-sFAKE_DYLIBS', '-shared', '-fPIC', '-o', 'libdir/libfile.so' + lib_suffix]) # Build libother and dynamically link it to libfile - self.run_process([EMCC, '-Llibdir', 'libdir/libother.c'] + link_flags + ['-shared', '-o', 'libdir/libother.so']) + self.run_process([EMCC, '-Llibdir', 'libdir/libother.c'] + link_flags + ['-sFAKE_DYLIBS', '-shared', '-fPIC', '-o', 'libdir/libother.so']) # Build the main file, linking in both the libs self.run_process([EMCC, '-Llibdir', os.path.join('main.c')] + link_flags + ['-lother', '-c']) print('...') @@ -2462,17 +2468,17 @@ def test_dylink_library_search(self): } ''') - # By deafult we use static linking and prefer libside.a - self.do_runf('main.c', 'static linking used\n', cflags=['-L.', '-lside']) + # By default we use dynamic linking + self.do_runf('main.c', 'dynamic linking used\n', cflags=['-L.', '-lside']) - # When using -sMAIN_MODULE we choose the dyanmic library - self.do_runf('main.c', 'dynamic linking used\n', cflags=['-sMAIN_MODULE=2', '-L.', '-lside']) + # When using -sMAIN_MODULE=0 we explictly opt out of dynammic linking + self.do_runf('main.c', 'static linking used\n', cflags=['-sMAIN_MODULE=0', '-L.', '-lside']) - # Same for `-sFAKE_DYLIBS=0 - self.do_runf('main.c', 'dynamic linking used\n', cflags=['-sFAKE_DYLIBS=0', '-L.', '-lside']) + # Same for `-sFAKE_DYLIBS + self.do_runf('main.c', 'static linking used\n', cflags=['-sFAKE_DYLIBS', '-L.', '-lside']) # With can also force static linking using `-Bstatic` linker falgs - self.do_runf('main.c', 'static linking used\n', cflags=['-sMAIN_MODULE=2', '-L.', '-Bstatic', '-lside']) + self.do_runf('main.c', 'static linking used\n', cflags=['-L.', '-Bstatic', '-lside']) def test_js_link(self): create_file('before.js', ''' @@ -4955,20 +4961,6 @@ def test_valid_abspath_2(self): self.run_process(cmd) self.assertContained('Hello, world!', self.run_js('a.out.js')) - def test_warn_dylibs(self): - shared_suffixes = ['.so', '.dylib', '.dll'] - - for suffix in ('.o', '.bc', '.so', '.dylib', '.js', '.html'): - print(suffix) - cmd = [EMCC, test_file('hello_world.c'), '-o', 'out' + suffix] - if suffix in {'.o', '.bc'}: - cmd.append('-c') - if suffix in {'.dylib', '.so'}: - cmd.append('-shared') - err = self.run_process(cmd, stderr=PIPE).stderr - warning = 'linking a library with `-shared` will emit a static object file' - self.assertContainedIf(warning, err, suffix in shared_suffixes) - @crossplatform @parameterized({ 'O2': [['-O2']], @@ -8464,12 +8456,12 @@ def test_side_module_ignore(self): self.run_process([EMCC, test_file('hello_world.c'), '-sSIDE_MODULE', '-o', 'libside.so']) # Attempting to link statically against a side module (libside.so) should fail. - self.assert_fail([EMCC, '-L.', '-lside'], 'wasm-ld: error: unable to find library -lside') + self.assert_fail([EMCC, '-L.', '-Bstatic', '-lside'], 'wasm-ld: error: unable to find library -lside') # But a static library in the same location (libside.a) should take precedence. self.run_process([EMCC, test_file('hello_world.c'), '-c']) self.run_process([EMAR, 'cr', 'libside.a', 'hello_world.o']) - self.run_process([EMCC, '-L.', '-lside']) + self.run_process([EMCC, '-L.', '-Bstatic', '-lside']) self.assertContained('Hello, world!', self.run_js('a.out.js')) @is_slow_test @@ -12131,42 +12123,40 @@ def test_err(self): def test_euidaccess(self): self.do_other_test('test_euidaccess.c') - def test_shared_flag(self): - create_file('side.c', 'int foo;') - self.run_process([EMCC, '-shared', 'side.c', '-o', 'libother.so']) + def test_fake_dylibs(self): + create_file('other.c', 'int foo = 10;') + self.run_process([EMCC, '-shared', '-sFAKE_DYLIBS', '-fPIC', 'other.c', '-o', 'libother.so']) + self.assertIsObjectFile('libother.so') - # Test that `-shared` flag causes object file generation but gives a warning - err = self.run_process([EMCC, '-shared', test_file('hello_world.c'), '-o', 'out.foo', 'libother.so'], stderr=PIPE).stderr - self.assertContained('linking a library with `-shared` will emit a static object', err) + # Test that `-sFAKE_DYLIBS` flag causes object file generation and will generate a warning about + # dylink dependencies being ignored. + err = self.run_process([EMCC, '-shared', '-sFAKE_DYLIBS', '-fPIC', test_file('hello_world.c'), '-o', 'out.foo', 'libother.so'], stderr=PIPE).stderr self.assertContained('emcc: warning: ignoring dynamic library libother.so when generating an object file, this will need to be included explicitly in the final link', err) self.assertIsObjectFile('out.foo') # Test that adding `-sFAKE_DYLIBS=0` build a real side module err = self.run_process([EMCC, '-shared', '-fPIC', '-sFAKE_DYLIBS=0', test_file('hello_world.c'), '-o', 'out.foo', 'libother.so'], stderr=PIPE).stderr - self.assertNotContained('linking a library with `-shared` will emit a static object', err) self.assertNotContained('emcc: warning: ignoring dynamic library libother.so when generating an object file, this will need to be included explicitly in the final link', err) self.assertIsWasmDylib('out.foo') # Test that using an executable output name overrides the `-shared` flag, but produces a warning. - err = self.run_process([EMCC, '-shared', test_file('hello_world.c'), '-o', 'out.js'], + err = self.run_process([EMCC, '-shared', '-sFAKE_DYLIBS', '-fPIC', test_file('hello_world.c'), '-o', 'out.js'], stderr=PIPE).stderr self.assertContained('warning: -shared/-r used with executable output suffix', err) self.run_js('out.js') def test_shared_soname(self): - self.run_process([EMCC, '-shared', '-Wl,-soname', '-Wl,libfoo.so.13', test_file('hello_world.c'), '-lc', '-o', 'libfoo.so']) + self.run_process([EMCC, '-shared', '-sFAKE_DYLIBS', '-Wl,-soname', '-Wl,libfoo.so.13', test_file('hello_world.c'), '-lc', '-o', 'libfoo.so']) self.run_process([EMCC, '-sSTRICT', 'libfoo.so']) self.assertContained('Hello, world!', self.run_js('a.out.js')) - def test_shared_and_side_module_flag(self): - # Test that `-shared` and `-sSIDE_MODULE` flag causes wasm dylib generation without a warning. - err = self.run_process([EMCC, '-shared', '-sSIDE_MODULE', test_file('hello_world.c'), '-o', 'out.foo'], stderr=PIPE).stderr - self.assertNotContained('linking a library with `-shared` will emit a static object', err) + def test_shared_flag(self): + # Test that `-shared` flag causes wasm dylib generation + self.run_process([EMCC, '-shared', '-fPIC', test_file('hello_world.c'), '-o', 'out.foo']) self.assertIsWasmDylib('out.foo') - # Test that `-shared` and `-sSIDE_MODULE` flag causes wasm dylib generation without a warning even if given executable output name. - err = self.run_process([EMCC, '-shared', '-sSIDE_MODULE', test_file('hello_world.c'), '-o', 'out.wasm'], - stderr=PIPE).stderr + # Test that `-shared` causes wasm dylib generation warning even if given executable output name. + err = self.run_process([EMCC, '-shared', '-fPIC', test_file('hello_world.c'), '-o', 'out.wasm'], stderr=PIPE).stderr self.assertNotContained('warning: -shared/-r used with executable output suffix', err) self.assertIsWasmDylib('out.wasm') diff --git a/tools/building.py b/tools/building.py index f2a60385d353c..3cb7ba014c4be 100644 --- a/tools/building.py +++ b/tools/building.py @@ -72,7 +72,7 @@ def get_building_env(): env['AR'] = EMAR env['LD'] = EMCC env['NM'] = LLVM_NM - env['LDSHARED'] = EMCC + env['LDSHARED'] = f'{EMCC} -shared' env['RANLIB'] = EMRANLIB env['EMSCRIPTEN_TOOLS'] = path_from_root('tools') env['HOST_CC'] = CLANG_CC @@ -165,6 +165,12 @@ def lld_flags_for_executable(external_symbols): stub = create_stub_object(external_symbols) cmd.append(stub) + if settings.FAKE_DYLIBS or (not settings.MAIN_MODULE and not settings.SIDE_MODULE): + cmd.append('-Bstatic') + else: + # wasm-ld still defaults to static linking by default. If that ever changes, we can remove this line. + cmd.append('-Bdynamic') + if not settings.ERROR_ON_UNDEFINED_SYMBOLS: cmd.append('--import-undefined') @@ -184,9 +190,6 @@ def lld_flags_for_executable(external_symbols): not settings.ASYNCIFY): cmd.append('--strip-debug') - if settings.LINKABLE: - cmd.append('--export-dynamic') - if settings.LTO and not settings.EXIT_RUNTIME: # The WebAssembly backend can generate new references to `__cxa_atexit` at # LTO time. This `-u` flag forces the `__cxa_atexit` symbol to be @@ -203,7 +206,6 @@ def lld_flags_for_executable(external_symbols): c_exports = [e for e in c_exports if e not in external_symbols] c_exports += settings.REQUIRED_EXPORTS if settings.MAIN_MODULE: - cmd.append('-Bdynamic') c_exports += side_module_external_deps(external_symbols) for export in c_exports: if settings.ERROR_ON_UNDEFINED_SYMBOLS: @@ -227,6 +229,8 @@ def lld_flags_for_executable(external_symbols): if not settings.LINKABLE: cmd.append('--no-export-dynamic') else: + if settings.LINKABLE: + cmd.append('--export-dynamic') cmd.append('--export-table') if settings.ALLOW_TABLE_GROWTH: cmd.append('--growable-table') @@ -281,7 +285,7 @@ def lld_flags(args): # Emscripten currently expects linkable output (SIDE_MODULE/MAIN_MODULE) to # include all archive contents. - if settings.LINKABLE: + if settings.LINKABLE and (settings.FAKE_DYLIBS or not settings.SIDE_MODULE): args.insert(0, '--whole-archive') args.append('--no-whole-archive') diff --git a/tools/cmdline.py b/tools/cmdline.py index 1b2f6efd033a1..b66249da8f46a 100644 --- a/tools/cmdline.py +++ b/tools/cmdline.py @@ -54,8 +54,7 @@ @unique class OFormat(Enum): - # Output a relocatable object file. We use this - # today for `-r` and `-shared`. + # Output a relocatable object file. i.e. `-r` linker flag OBJECT = auto() WASM = auto() JS = auto() diff --git a/tools/link.py b/tools/link.py index 58f82cd407338..3d3d838da3733 100644 --- a/tools/link.py +++ b/tools/link.py @@ -805,17 +805,14 @@ def phase_linker_setup(options, linker_args): # noqa: C901, PLR0912, PLR0915 apply_library_settings(linker_args) - if settings.SIDE_MODULE or settings.MAIN_MODULE: - default_setting('FAKE_DYLIBS', 0) - if options.shared and not settings.FAKE_DYLIBS: default_setting('SIDE_MODULE', 1) - if not settings.FAKE_DYLIBS: + if not settings.SIDE_MODULE and not settings.FAKE_DYLIBS: options.dylibs = get_dylibs(options, linker_args) - # If there are any dynamically linked libraries on the command line then - # need to enable `MAIN_MODULE` in order to produce JS code that can load them. - if not settings.MAIN_MODULE and not settings.SIDE_MODULE and options.dylibs: + # If there are any dynamic libraries on the command line then enable + # `MAIN_MODULE` by default in order to produce JS code that can load them. + if options.dylibs and not settings.MAIN_MODULE: default_setting('MAIN_MODULE', 2) linker_args += calc_extra_ldflags(options) @@ -917,8 +914,6 @@ def phase_linker_setup(options, linker_args): # noqa: C901, PLR0912, PLR0915 if final_suffix in EXECUTABLE_EXTENSIONS: diagnostics.warning('emcc', '-shared/-r used with executable output suffix. This behaviour is deprecated. Please remove -shared/-r to build an executable or avoid the executable suffix (%s) when building object files.' % final_suffix) else: - if options.shared and 'FAKE_DYLIBS' not in user_settings: - diagnostics.warning('emcc', 'linking a library with `-shared` will emit a static object file (FAKE_DYLIBS defaults to true). If you want to build a runtime shared library use the SIDE_MODULE or FAKE_DYLIBS=0.') options.oformat = OFormat.OBJECT if not options.oformat: From f28581fe9b214cffcf3366ecd3a2eb5cf733aed9 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Wed, 20 May 2026 15:41:40 -0700 Subject: [PATCH 2/4] feedback --- test/test_core.py | 3 --- test/test_other.py | 1 - 2 files changed, 4 deletions(-) diff --git a/test/test_core.py b/test/test_core.py index bdbba9497412b..5c850d54dea51 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -5144,9 +5144,6 @@ def test_dylink_spaghetti(self): @needs_make('mingw32-make') @needs_dylink @parameterized({ - # Cmake support for dyanamic libraries is currently broken. - # which means cmake won't build shared libraries yet. - # https://gitlab.kitware.com/cmake/cmake/-/work_items/27240 'cmake': (True,), 'configure': (False,), }) diff --git a/test/test_other.py b/test/test_other.py index baa933aa8c38e..0f8a7620c79eb 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -933,7 +933,6 @@ def test_cmake_explicit_generator(self): cmd = [EMCMAKE, 'cmake', '-GNinja', '-Wno-dev', test_file('cmake/cpp_lib')] self.run_process(cmd) self.assertExists('build.ninja') - self.run_process(['ninja', '-v']) # Tests that it's possible to pass C++17 or GNU++17 build modes to CMake by building code that # needs C++17 (embind) From 9dfed2eb72a8ac0226f4556ebc4ae12345057795 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Thu, 21 May 2026 11:40:47 -0700 Subject: [PATCH 3/4] feedback --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index a9b3f4e8fb396..a130576b8f94b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -51,7 +51,7 @@ See docs/process.md for more on how version tagging works. is required in order to use Emscripten's Closure Compiler integration. - The `FAKE_DYLIBS` setting is now disabled by default. This means that `-shared` will produce real dynamic libraries by default (`-sSIDE_MODULE` is - implied). Also, if you include real dynamic libraries in your link comment + implied). Also, if you include real dynamic libraries in your link command emscripten will now automatically produce a dynamically linked program (`-sMAIN_MODULE=2` is implied). (#25930) From b305b6da1fd0b6991116612fdbc49123b18b4288 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Thu, 21 May 2026 12:48:25 -0700 Subject: [PATCH 4/4] fix tests --- test/common.py | 6 +++++- test/test_core.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/test/common.py b/test/common.py index bd810f69eef5e..faf500209c2ec 100644 --- a/test/common.py +++ b/test/common.py @@ -1531,8 +1531,12 @@ def get_zlib_library(self, cmake, cflags=None, target='libz.a'): # https://github.com/emscripten-core/emscripten/issues/16908 is fixed self.cflags.append('-Wno-pointer-sign') if cmake: + if target == 'libz.a': + cmake_cmd = ['cmake', '-DBUILD_SHARED_LIBS=OFF', '.'] + else: + cmake_cmd = ['cmake', '.'] rtn = self.get_library(os.path.join('third_party', 'zlib'), target, - configure=['cmake', '.'], + configure=cmake_cmd, make=['cmake', '--build', '.', '--'], make_args=[]) else: diff --git a/test/test_core.py b/test/test_core.py index 5c850d54dea51..1c437d8149c01 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -6973,7 +6973,7 @@ def line_splitter(data): 'codec/CMakeFiles/j2k_to_image.dir/__/common/color.c.o', 'codec/CMakeFiles/j2k_to_image.dir/__/common/getopt.c.o', 'bin/libopenjpeg.a'], - configure=['cmake', '.'], + configure=['cmake', '.', '-DBUILD_SHARED_LIBS=OFF'], # configure_args=['--enable-tiff=no', '--enable-jp3d=no', '--enable-png=no'], make_args=[]) # no -j 2, since parallel builds can fail