| |
| |
| |
| |
| |
|
|
| """\ |
| 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). |
| """ |
|
|
| import logging |
| import os |
| import shlex |
| import shutil |
| import sys |
| import tarfile |
| from dataclasses import dataclass |
| from enum import Enum, auto, unique |
|
|
| |
| |
| |
| 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') |
|
|
| |
| |
| |
| |
| 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', |
| '.cppm', '.pcm', '.cpp', '.cxx', '.cc', '.c++', '.CPP', '.CXX', '.C', '.CC', '.C++', '.ii', |
| '.m', '.mi', '.mm', '.mii', |
| '.bc', '.ll', |
| '.S', |
| os.devnull, |
| } | 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): |
| |
| COMPILE_ONLY = auto() |
| |
| POST_LINK_ONLY = auto() |
| |
| 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 |
| |
| 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: |
| |
| |
| |
| |
| 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 |
|
|
| |
| |
| |
| |
| |
| if len(args) == 2 and args[1] == '-v': |
| |
| print(cmdline.version_string(), file=sys.stderr) |
| return shared.check_call([clang, '-v'] + compile.get_target_flags(), check=False).returncode |
|
|
| |
| |
| 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()})') |
|
|
| |
| args = args[1:] |
|
|
| |
|
|
| |
| try: |
| args = substitute_response_files(args) |
| except OSError as e: |
| exit_with_error(e) |
|
|
| if '--help' in args: |
| |
| |
| |
| |
| |
| |
| 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 |
|
|
| |
| state = EmccState(args) |
| newargs = cmdline.parse_arguments(state.orig_args) |
|
|
| if not shared.SKIP_SUBPROCS: |
| shared.check_sanity() |
|
|
| |
|
|
| 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: |
| 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 |
|
|
| |
|
|
| 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'] |
|
|
| |
| |
| 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: |
| |
| |
| 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] |
| |
| from tools import link |
| link.run_post_link(options.input_files[0], options, linker_args) |
| return 0 |
|
|
| |
| |
| linker_args = phase_compile_inputs(options, state, newargs) |
|
|
| if state.mode == Mode.COMPILE_AND_LINK: |
| |
| 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'}: |
| |
| 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 |
|
|
| |
| 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 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') |
| |
| |
| 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') |
|
|
| |
| if settings.SUPPORT_LONGJMP == 'wasm': |
| |
| |
| |
| |
| if user_settings.get('DISABLE_EXCEPTION_THROWING') == '0': |
| exit_with_error('SUPPORT_LONGJMP=wasm cannot be used with DISABLE_EXCEPTION_THROWING=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) |
|
|
| |
| |
| 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' |
|
|
| |
| |
| assert state.mode == Mode.COMPILE_AND_LINK |
| assert not options.dash_c |
| compile_args, linker_args = separate_linker_flags(newargs) |
|
|
| |
| |
| |
| seen_names = {} |
|
|
| def uniquename(name): |
| if name not in seen_names: |
| |
| 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': |
| |
| |
| |
| |
| |
| 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 |
|
|
| |
| 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: |
| |
| 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) |
|
|