#!/usr/bin/env python3 # Copyright 2011 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. """\ emcc - compiler helper script ============================= emcc is a drop-in replacement for a compiler like gcc or clang. See emcc --help for details. emcc can be influenced by a few environment variables: EMCC_DEBUG - "1" will log out useful information during compilation, as well as save each compiler step as an emcc-* file in the temp dir (by default /tmp/emscripten_temp). "2" will save additional emcc-* steps, that would normally not be separately produced (so this slows down compilation). """ # noqa: D205, D400, D415 import logging import os import shlex import shutil import sys import tarfile from dataclasses import dataclass from enum import Enum, auto, unique # This assert needs to happen early, before any too-recent python syntax is used. # In particular it needs to happen before we import any python file that uses the # `match` keyword. assert sys.version_info >= (3, 10), f'emscripten requires python 3.10 or above ({sys.executable} {sys.version})' from tools import ( building, cache, cmdline, compile, config, diagnostics, shared, system_libs, utils, ) from tools.cmdline import CLANG_FLAGS_WITH_ARGS, options from tools.response_file import substitute_response_files from tools.settings import COMPILE_TIME_SETTINGS, default_setting, settings, user_settings from tools.shared import DEBUG, DYLIB_EXTENSIONS, in_temp from tools.toolchain_profiler import ToolchainProfiler from tools.utils import exit_with_error, get_file_suffix, read_file, unsuffixed_basename logger = logging.getLogger('emcc') # In git checkouts of emscripten `bootstrap.py` exists to run post-checkout # steps. In packaged versions (e.g. emsdk) this file does not exist (because # it is excluded in tools/install.py) and these steps are assumed to have been # run already. if os.path.exists(utils.path_from_root('.git')) and os.path.exists(utils.path_from_root('bootstrap.py')): import bootstrap bootstrap.check() PREPROCESSED_EXTENSIONS = {'.i', '.ii'} ASSEMBLY_EXTENSIONS = {'.s'} HEADER_EXTENSIONS = {'.h', '.hxx', '.hpp', '.hh', '.H', '.HXX', '.HPP', '.HH'} SOURCE_EXTENSIONS = { '.c', '.i', # C '.cppm', '.pcm', '.cpp', '.cxx', '.cc', '.c++', '.CPP', '.CXX', '.C', '.CC', '.C++', '.ii', # C++ '.m', '.mi', '.mm', '.mii', # ObjC/ObjC++ '.bc', '.ll', # LLVM IR '.S', # asm with preprocessor os.devnull, # consider the special endingless filenames like /dev/null to be C } | PREPROCESSED_EXTENSIONS LINK_ONLY_FLAGS = { '--bind', '--closure', '--cpuprofiler', '--embed-file', '--emit-symbol-map', '--emrun', '--exclude-file', '--extern-post-js', '--extern-pre-js', '--ignore-dynamic-linking', '--js-library', '--js-transform', '--oformat', '--output_eol', '--output-eol', '--post-js', '--pre-js', '--preload-file', '--profiling-funcs', '--proxy-to-worker', '--shell-file', '--source-map-base', '--threadprofiler', '--use-preload-plugins', } @unique class Mode(Enum): # Used any time we are not linking, including PCH, pre-processing, etc COMPILE_ONLY = auto() # Only when --post-link is specified POST_LINK_ONLY = auto() # This is the default mode, in the absence of any flags such as -c, -E, etc COMPILE_AND_LINK = auto() @dataclass class LinkFlag: """Used to represent a linker flag. The flag value is stored along with a bool that distinguishes input files from non-files. A list of these is returned by separate_linker_flags. """ value: str is_file: int class EmccState: def __init__(self, args): self.mode = Mode.COMPILE_AND_LINK # Using tuple here to prevent accidental mutation self.orig_args = tuple(args) def create_reproduce_file(name, args): def make_relative(filename): filename = os.path.normpath(os.path.abspath(filename)) filename = os.path.splitdrive(filename)[1] filename = filename[1:] return filename root = unsuffixed_basename(name) with tarfile.open(name, 'w') as reproduce_file: reproduce_file.add(utils.path_from_root('emscripten-version.txt'), os.path.join(root, 'version.txt')) with shared.get_temp_files().get_file(suffix='.tar') as rsp_name: with open(rsp_name, 'w', encoding='utf-8') as rsp: ignore_next = False output_arg = None for arg in args: ignore = ignore_next ignore_next = False if arg.startswith('--reproduce='): continue if len(arg) > 2 and arg.startswith('-o'): rsp.write('-o\n') arg = arg[3:] output_arg = True ignore = True if output_arg: # If -o path contains directories, "emcc @response.txt" will likely # fail because the archive we are creating doesn't contain empty # directories for the output path (-o doesn't create directories). # Strip directories to prevent the issue. arg = os.path.basename(arg) output_arg = False if not arg.startswith('-') and not ignore: relpath = make_relative(arg) rsp.write(relpath + '\n') reproduce_file.add(arg, os.path.join(root, relpath)) else: rsp.write(arg + '\n') if ignore: continue if arg in CLANG_FLAGS_WITH_ARGS: ignore_next = True if arg == '-o': output_arg = True reproduce_file.add(rsp_name, os.path.join(root, 'response.txt')) @ToolchainProfiler.profile() def main(args): if shared.run_via_emxx: clang = shared.CLANG_CXX else: clang = shared.CLANG_CC # 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 # on, before we inject EMCC_CFLAGS. This is because tools like cmake and # autoconf will run `emcc -v` to determine the compiler version and we don't # want that to break for users of EMCC_CFLAGS. 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 # Additional compiler flags that we treat as if they were passed to us on the # commandline if EMCC_CFLAGS := os.environ.get('EMCC_CFLAGS'): args += shlex.split(EMCC_CFLAGS) if DEBUG: logger.warning(f'invocation: {shlex.join(args)} (in {os.getcwd()})') # Strip args[0] (program name) args = args[1:] # Handle some global flags # read response files very early on try: args = substitute_response_files(args) except OSError as e: exit_with_error(e) if '--help' in args: # Documentation for emcc and its options must be updated in: # site/source/docs/tools_reference/emcc.rst # This then gets built (via: `make -C site text`) to: # site/build/text/docs/tools_reference/emcc.txt # This then needs to be copied to its final home in docs/emcc.txt from where # we read it here. We have CI rules that ensure its always up-to-date. print(read_file(utils.path_from_root('docs/emcc.txt'))) print(''' ------------------------------------------------------------------ emcc: supported targets: llvm bitcode, WebAssembly, NOT elf (autoconf likes to see elf above to enable shared object support) ''') return 0 ## Process argument and setup the compiler state = EmccState(args) newargs = cmdline.parse_arguments(state.orig_args) if not shared.SKIP_SUBPROCS: shared.check_sanity() # 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. ''') 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 if '-dumpmachine' in args or '-print-target-triple' in args or '--print-target-triple' in args: print(shared.get_llvm_target()) return 0 if '-print-search-dirs' in args or '--print-search-dirs' in args: print(f'programs: ={config.LLVM_ROOT}') print(f'libraries: ={cache.get_lib_dir(absolute=True)}') return 0 if '-print-libgcc-file-name' in args or '--print-libgcc-file-name' in args: settings.limit_settings(None) compiler_rt = system_libs.Library.get_usable_variations()['libcompiler_rt'] print(compiler_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] system_libpath = cache.get_lib_dir(absolute=True) fullpath = os.path.join(system_libpath, libname) if os.path.isfile(fullpath): print(fullpath) else: print(libname) return 0 # End early-exit flag handling if 'EMMAKEN_NO_SDK' in os.environ: exit_with_error('EMMAKEN_NO_SDK is no longer supported. The standard -nostdlib and -nostdinc flags should be used instead') if 'EMMAKEN_COMPILER' in os.environ: exit_with_error('`EMMAKEN_COMPILER` is no longer supported.\n' + 'Please use the `LLVM_ROOT` and/or `COMPILER_WRAPPER` config settings instead') if 'EMMAKEN_CFLAGS' in os.environ: exit_with_error('`EMMAKEN_CFLAGS` is no longer supported, please use `EMCC_CFLAGS` instead') if 'EMCC_REPRODUCE' in os.environ: options.reproduce = os.environ['EMCC_REPRODUCE'] # 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) if '-print-resource-dir' in args or any(a.startswith('--print-prog-name') for a in args): shared.exec_process([clang] + compile.get_cflags(tuple(args)) + args) 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 if options.reproduce: create_reproduce_file(options.reproduce, args) if state.mode == Mode.POST_LINK_ONLY: if len(options.input_files) != 1: exit_with_error('--post-link requires a single input file') linker_args = separate_linker_flags(newargs)[1] linker_args = [f.value for f in linker_args] # Delay import of link.py to avoid processing this file when only compiling from tools import link # noqa: PLC0415 link.run_post_link(options.input_files[0], options, linker_args) return 0 # Compile source code to object files # When only compiling this function never returns. linker_args = phase_compile_inputs(options, state, newargs) if state.mode == Mode.COMPILE_AND_LINK: # Delay import of link.py to avoid processing this file when only compiling from tools import link return link.run(options, linker_args) else: logger.debug('stopping after compile phase') return 0 def separate_linker_flags(newargs): """Process argument list separating out compiler args and linker args. - Linker flags include input files and are returned a list of LinkFlag objects. - Compiler flags are those to be passed to `clang -c`. """ compiler_args = [] linker_args = [] def add_link_arg(flag, is_file=False): linker_args.append(LinkFlag(flag, is_file)) skip = False for i in range(len(newargs)): if skip: skip = False continue arg = newargs[i] if arg in CLANG_FLAGS_WITH_ARGS: skip = True def get_next_arg(): if len(newargs) <= i + 1: exit_with_error(f"option '{arg}' requires an argument") return newargs[i + 1] if not arg.startswith('-') or arg == '-': if not os.path.exists(arg) and arg != '-': exit_with_error('%s: No such file or directory ("%s" was expected to be an input file, based on the commandline arguments provided)', arg, arg) add_link_arg(arg, True) elif arg == '-z': add_link_arg(arg) add_link_arg(get_next_arg()) elif arg.startswith('-Wl,'): for flag in arg.split(',')[1:]: add_link_arg(flag) elif arg == '-Xlinker': add_link_arg(get_next_arg()) elif arg == '-s' or arg.startswith(('-l', '-L', '--js-library=', '-z', '-u')): add_link_arg(arg) elif not arg.startswith('-o') and arg not in {'-nostdlib', '-nostartfiles', '-nolibc', '-nodefaultlibs', '-s'}: # All other flags are for the compiler compiler_args.append(arg) if skip: compiler_args.append(get_next_arg()) return compiler_args, linker_args @ToolchainProfiler.profile_block('setup') def phase_setup(state): """Second phase: configure and setup the compiler based on the specified settings and arguments.""" has_header_inputs = any(get_file_suffix(f) in HEADER_EXTENSIONS for f in options.input_files) if options.post_link: state.mode = Mode.POST_LINK_ONLY elif has_header_inputs or options.dash_c or options.dash_S or options.syntax_only or options.dash_E or options.dash_M: state.mode = Mode.COMPILE_ONLY if state.mode == Mode.COMPILE_ONLY: for key in user_settings: if key not in COMPILE_TIME_SETTINGS: diagnostics.warning( 'unused-command-line-argument', "linker setting ignored during compilation: '%s'" % key) for arg in state.orig_args: if arg in LINK_ONLY_FLAGS: diagnostics.warning( 'unused-command-line-argument', "linker flag ignored during compilation: '%s'" % arg) if 'USE_PTHREADS' in user_settings: settings.PTHREADS = settings.USE_PTHREADS # Pthreads and Wasm Workers require targeting shared Wasm memory (SAB). if settings.PTHREADS or settings.WASM_WORKERS: settings.SHARED_MEMORY = 1 if 'DISABLE_EXCEPTION_CATCHING' in user_settings and 'EXCEPTION_CATCHING_ALLOWED' in user_settings: # 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'}: 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') if settings.EXCEPTION_CATCHING_ALLOWED: settings.DISABLE_EXCEPTION_CATCHING = 0 if settings.WASM_EXCEPTIONS: 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': 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 if 'DISABLE_EXCEPTION_CATCHING' in user_settings or 'DISABLE_EXCEPTION_THROWING' in user_settings: diagnostics.warning('emcc', 'you no longer need to pass DISABLE_EXCEPTION_CATCHING or DISABLE_EXCEPTION_THROWING when using Wasm exceptions') settings.DISABLE_EXCEPTION_CATCHING = 1 settings.DISABLE_EXCEPTION_THROWING = 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': exit_with_error('SUPPORT_LONGJMP=emscripten is not compatible with -fwasm-exceptions') if settings.DISABLE_EXCEPTION_THROWING and not settings.DISABLE_EXCEPTION_CATCHING: exit_with_error("DISABLE_EXCEPTION_THROWING was set (probably from -fno-exceptions) but is not compatible with enabling exception catching (DISABLE_EXCEPTION_CATCHING=0). If you don't want exceptions, set DISABLE_EXCEPTION_CATCHING to 1; if you do want exceptions, don't link with -fno-exceptions") if options.target.startswith('wasm64'): default_setting('MEMORY64', 1) if settings.MEMORY64 and options.target.startswith('wasm32'): exit_with_error('wasm32 target is not compatible with -sMEMORY64') # Wasm SjLj cannot be used with Emscripten EH if settings.SUPPORT_LONGJMP == 'wasm': # DISABLE_EXCEPTION_THROWING is 0 by default for Emscripten EH throwing, but # 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': 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': exit_with_error('SUPPORT_LONGJMP=wasm cannot be used with DISABLE_EXCEPTION_CATCHING=0') default_setting('DISABLE_EXCEPTION_THROWING', 1) # SUPPORT_LONGJMP=1 means the default SjLj handling mechanism, which is 'wasm' # if Wasm EH is used and 'emscripten' otherwise. if settings.SUPPORT_LONGJMP == 1: if settings.WASM_EXCEPTIONS: settings.SUPPORT_LONGJMP = 'wasm' else: settings.SUPPORT_LONGJMP = 'emscripten' @ToolchainProfiler.profile_block('compile inputs') def phase_compile_inputs(options, state, newargs): if shared.run_via_emxx: compiler = [shared.CLANG_CXX] else: compiler = [shared.CLANG_CC] if config.COMPILER_WRAPPER: logger.debug('using compiler wrapper: %s', config.COMPILER_WRAPPER) compiler.insert(0, config.COMPILER_WRAPPER) system_libs.ensure_sysroot() def get_clang_command(): return compiler + compile.get_cflags(state.orig_args) def get_clang_command_preprocessed(): return compiler + compile.get_clang_flags(state.orig_args) def get_clang_command_asm(): return compiler + compile.get_target_flags() if state.mode == Mode.COMPILE_ONLY: if options.output_file and get_file_suffix(options.output_file) == '.bc' and not settings.LTO and '-emit-llvm' not in state.orig_args: diagnostics.warning('emcc', '.bc output file suffix used without -flto or -emit-llvm. Consider using .o extension since emcc will output an object file, not a bitcode file') if all(get_file_suffix(i) in ASSEMBLY_EXTENSIONS for i in options.input_files): cmd = get_clang_command_asm() + newargs else: cmd = get_clang_command() + newargs shared.exec_process(cmd) assert False, 'exec_process should not return' # In COMPILE_AND_LINK we need to compile source files too, but we also need to # filter out the link flags assert state.mode == Mode.COMPILE_AND_LINK assert not options.dash_c compile_args, linker_args = separate_linker_flags(newargs) # Map of file basenames to how many times we've seen them. We use this to generate # unique `_NN` suffix for object files in cases when we are compiling multiple sources that # have the same basename. e.g. `foo/utils.c` and `bar/utils.c` on the same command line. seen_names = {} def uniquename(name): if name not in seen_names: # No suffix needed the first time we see given name. seen_names[name] = 1 return name unique_suffix = '_%d' % seen_names[name] seen_names[name] += 1 base, ext = os.path.splitext(name) return base + unique_suffix + ext def get_object_filename(input_file): objfile = unsuffixed_basename(input_file) + '.o' return in_temp(uniquename(objfile)) def compile_source_file(input_file): logger.debug(f'compiling source file: {input_file}') output_file = get_object_filename(input_file) ext = get_file_suffix(input_file) if ext in ASSEMBLY_EXTENSIONS: cmd = get_clang_command_asm() elif ext in PREPROCESSED_EXTENSIONS: cmd = get_clang_command_preprocessed() else: cmd = get_clang_command() if ext == '.pcm': cmd = [c for c in cmd if not c.startswith('-fprebuilt-module-path=')] cmd += compile_args + ['-c', input_file, '-o', output_file] if options.requested_debug == '-gsplit-dwarf': # When running in COMPILE_AND_LINK mode we compile objects to a temporary location # but we want the `.dwo` file to be generated in the current working directory, # like it is under clang. We could avoid this hack if we use the clang driver # to generate the temporary files, but that would also involve using the clang # driver to perform linking which would be a big change. cmd += ['-Xclang', '-split-dwarf-file', '-Xclang', unsuffixed_basename(input_file) + '.dwo'] cmd += ['-Xclang', '-split-dwarf-output', '-Xclang', unsuffixed_basename(input_file) + '.dwo'] shared.check_call(cmd) if not shared.SKIP_SUBPROCS: assert os.path.exists(output_file) if options.save_temps: shutil.copyfile(output_file, utils.unsuffixed_basename(input_file) + '.o') return output_file # Compile input files individually to temporary locations. for arg in linker_args: if not arg.is_file: continue input_file = arg.value file_suffix = get_file_suffix(input_file) if file_suffix in SOURCE_EXTENSIONS | ASSEMBLY_EXTENSIONS or (options.dash_c and file_suffix == '.bc'): arg.value = compile_source_file(input_file) elif file_suffix in DYLIB_EXTENSIONS: logger.debug(f'using shared library: {input_file}') elif building.is_ar(input_file): logger.debug(f'using static library: {input_file}') elif options.input_language: arg.value = compile_source_file(input_file) elif input_file == '-': exit_with_error('-E or -x required when input is from standard input') else: # Default to assuming the inputs are object files and pass them to the linker pass return [f.value for f in linker_args] if __name__ == '__main__': try: sys.exit(main(sys.argv)) except KeyboardInterrupt: logger.debug('KeyboardInterrupt') sys.exit(1)