diff --git CMakeLists.txt CMakeLists.txt index a76caf1..607bb47 100644 --- CMakeLists.txt +++ CMakeLists.txt @@ -78,6 +78,37 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Release") endif() +# QuickJS ships pre-generated bytecode (the builtin-*.h headers compiled into the +# library, and the gen/*.c blobs used by the CLI and examples). Bytecode is stored +# in native endianness and validated by an embedded checksum at load time (see +# bc_csum()/put_u32() in quickjs.c), so the committed little-endian files fail with +# a "checksum error" on big-endian hosts. On a native big-endian build we regenerate +# them from source with a freshly built qjsc, which emits native-endian bytecode. +# +# Cross-compilation cannot do this (qjsc would be built for the target and cannot run +# on the build machine), so we only enable it for native builds. Cross-compiling to +# a big-endian target still requires pre-generating the files with `make codegen` using +# a matching-endianness toolchain. +# QJS_REGEN_BYTECODE_FORCE forces regeneration regardless of detected endianness +# (useful to exercise the codegen path, or if detection is ever wrong). It has no +# effect when cross-compiling, where qjsc cannot be run on the build machine. +option(QJS_REGEN_BYTECODE_FORCE "Always regenerate bytecode from source with qjsc" OFF) +set(QJS_REGEN_BYTECODE OFF) +if(NOT CMAKE_CROSSCOMPILING) + include(TestBigEndian) + test_big_endian(QJS_IS_BIG_ENDIAN) + if(QJS_IS_BIG_ENDIAN OR QJS_REGEN_BYTECODE_FORCE) + set(QJS_REGEN_BYTECODE ON) + message(STATUS "Bytecode will be regenerated from source with qjsc") + endif() +elseif(DEFINED CMAKE_C_BYTE_ORDER AND CMAKE_C_BYTE_ORDER STREQUAL "BIG_ENDIAN") + message(WARNING + "Cross-compiling to a big-endian target: the committed bytecode " + "(builtin-*.h and gen/*.c) is little-endian and will fail at runtime " + "with a \"checksum error\". Pre-generate it with a matching-endianness " + "qjsc (see the `codegen` target in the Makefile) before building.") +endif() + message(STATUS "Building in ${CMAKE_BUILD_TYPE} mode") message(STATUS "Building with ${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER_VERSION} on ${CMAKE_SYSTEM}") @@ -304,7 +335,100 @@ if(M_LIBRARIES OR CMAKE_C_COMPILER_ID STREQUAL "TinyCC") list(APPEND qjs_libs m) endif() -add_library(qjs ${qjs_sources}) +# Bytecode regeneration (big-endian native builds only; see QJS_REGEN_BYTECODE above) +# +# The engine embeds three builtin bytecode blobs via #include (builtin-*.h) that are +# evaluated lazily (Array.fromAsync, Iterator.zip, Iterator.zipKeyed), plus the CLI +# and examples embed gen/*.c blobs. All must match the host endianness. We regenerate +# them into the build tree with a bootstrap qjsc and have the real qjs (and the CLI, +# examples, etc.) pick up the regenerated copies. +set(qjs_gen_dir ${CMAKE_CURRENT_BINARY_DIR}/gen) +set(qjs_real_sources ${qjs_sources}) +if(QJS_REGEN_BYTECODE) + # Self-contained bootstrap library + compiler, built from the *committed* + # (possibly wrong-endian) headers. Compiling JS to bytecode never touches the + # lazily-evaluated builtins, so a wrong-endian bootstrap engine generates correct + # native-endian bytecode even though it could not itself run that bytecode. Kept + # entirely separate from the real qjs so there is no build dependency cycle. + # + # qjsc.c pulls in quickjs-libc.h (js_std_dump_error, the module loader), so the + # bootstrap library must always include quickjs-libc.c to be self-contained. It is + # already part of qjs_sources when QJS_BUILD_LIBC is ON, so only add it otherwise. + set(qjs_bootstrap_sources ${qjs_sources}) + if(NOT QJS_BUILD_LIBC) + list(APPEND qjs_bootstrap_sources quickjs-libc.c) + endif() + add_library(qjs_bootstrap STATIC EXCLUDE_FROM_ALL ${qjs_bootstrap_sources}) + target_compile_definitions(qjs_bootstrap PRIVATE ${qjs_defines}) + target_include_directories(qjs_bootstrap PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(qjs_bootstrap PUBLIC ${qjs_libs}) + + add_executable(qjsc_bootstrap EXCLUDE_FROM_ALL qjsc.c) + target_compile_definitions(qjsc_bootstrap PRIVATE ${qjs_defines}) + target_link_libraries(qjsc_bootstrap PRIVATE qjs_bootstrap) + + # Regenerate one bytecode file into the build tree with the bootstrap compiler. + # The generated C symbol name derives from the input .js path, so we keep the + # same inputs and flags as the `codegen` target in the Makefile. -o must precede + # the input file: qjsc stops option scanning at the first non-option argument. + function(qjs_regen out_var relpath) + set(gen_out ${qjs_gen_dir}/${relpath}) + get_filename_component(gen_out_dir ${gen_out} DIRECTORY) + add_custom_command( + OUTPUT ${gen_out} + COMMAND ${CMAKE_COMMAND} -E make_directory ${gen_out_dir} + COMMAND qjsc_bootstrap -o ${gen_out} ${ARGN} + DEPENDS qjsc_bootstrap + COMMENT "Regenerating ${relpath} (native endianness)" + VERBATIM + ) + set(${out_var} ${gen_out} PARENT_SCOPE) + endfunction() + + # -n sets the script name embedded in the bytecode (used in stack traces); we + # match the committed files' names so the output is a byte-for-byte drop-in and + # the build-tree absolute path does not leak into the binary. + qjs_regen(gen_builtin_fromasync builtin-array-fromasync.h + -C -ss -n builtin-array-fromasync.js + ${CMAKE_CURRENT_SOURCE_DIR}/builtin-array-fromasync.js) + qjs_regen(gen_builtin_zip builtin-iterator-zip.h + -C -ss -n builtin-iterator-zip.js + ${CMAKE_CURRENT_SOURCE_DIR}/builtin-iterator-zip.js) + qjs_regen(gen_builtin_zip_keyed builtin-iterator-zip-keyed.h + -C -ss -n builtin-iterator-zip-keyed.js + ${CMAKE_CURRENT_SOURCE_DIR}/builtin-iterator-zip-keyed.js) + + # The builtin-*.h headers are #included by quickjs.c with quoted includes, which + # the compiler resolves relative to quickjs.c's own directory before any -I path. + # To make the regenerated headers win without mutating the source tree, we compile + # a copy of quickjs.c placed next to them in the build tree; its other quoted + # includes fall through to -I ${CMAKE_CURRENT_SOURCE_DIR}. + set(qjs_quickjs_copy ${qjs_gen_dir}/quickjs.c) + add_custom_command( + OUTPUT ${qjs_quickjs_copy} + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/quickjs.c ${qjs_quickjs_copy} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/quickjs.c + ${gen_builtin_fromasync} ${gen_builtin_zip} ${gen_builtin_zip_keyed} + COMMENT "Staging quickjs.c with regenerated builtin bytecode" + VERBATIM + ) + list(REMOVE_ITEM qjs_real_sources quickjs.c) + list(APPEND qjs_real_sources ${qjs_quickjs_copy}) +endif() + +# Resolve a gen/*.c bytecode source to the committed file, or a regenerated copy +# (in the build tree) on big-endian native builds. +function(qjs_bytecode_source out_var relpath) + if(NOT QJS_REGEN_BYTECODE) + set(${out_var} ${relpath} PARENT_SCOPE) + return() + endif() + qjs_regen(gen_out ${relpath} ${ARGN}) + set(${out_var} ${gen_out} PARENT_SCOPE) +endfunction() + +add_library(qjs ${qjs_real_sources}) target_compile_definitions(qjs PRIVATE ${qjs_defines}) target_include_directories(qjs PUBLIC $ @@ -378,9 +502,13 @@ target_link_libraries(qjsc PRIVATE qjs) # QuickJS CLI # +qjs_bytecode_source(qjs_gen_repl gen/repl.c + -ss -m -n repl.js ${CMAKE_CURRENT_SOURCE_DIR}/repl.js) +qjs_bytecode_source(qjs_gen_standalone gen/standalone.c + -ss -m -n standalone.js ${CMAKE_CURRENT_SOURCE_DIR}/standalone.js) add_executable(qjs_exe - gen/repl.c - gen/standalone.c + ${qjs_gen_repl} + ${qjs_gen_standalone} qjs.c ) add_qjs_libc_if_needed(qjs_exe) @@ -473,8 +601,10 @@ add_executable(unicode_gen EXCLUDE_FROM_ALL ) target_compile_definitions(unicode_gen PRIVATE ${qjs_defines}) +qjs_bytecode_source(qjs_gen_function_source gen/function_source.c + -e -n tests/function_source.js ${CMAKE_CURRENT_SOURCE_DIR}/tests/function_source.js) add_executable(function_source - gen/function_source.c + ${qjs_gen_function_source} ) add_qjs_libc_if_needed(function_source) target_include_directories(function_source PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) @@ -494,16 +624,20 @@ if(QJS_BUILD_EXAMPLES) ) endif() + qjs_bytecode_source(qjs_gen_hello gen/hello.c + -e -n examples/hello.js ${CMAKE_CURRENT_SOURCE_DIR}/examples/hello.js) add_executable(hello - gen/hello.c + ${qjs_gen_hello} ) add_qjs_libc_if_needed(hello) target_include_directories(hello PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_definitions(hello PRIVATE ${qjs_defines}) target_link_libraries(hello PRIVATE qjs) + qjs_bytecode_source(qjs_gen_hello_module gen/hello_module.c + -e -m -n examples/hello_module.js ${CMAKE_CURRENT_SOURCE_DIR}/examples/hello_module.js) add_executable(hello_module - gen/hello_module.c + ${qjs_gen_hello_module} ) add_qjs_libc_if_needed(hello_module) target_include_directories(hello_module PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) @@ -528,8 +662,10 @@ if(QJS_BUILD_EXAMPLES) target_link_options(point PRIVATE -undefined dynamic_lookup) endif() + qjs_bytecode_source(qjs_gen_test_fib gen/test_fib.c + -e -m -n examples/test_fib.js ${CMAKE_CURRENT_SOURCE_DIR}/examples/test_fib.js) add_executable(test_fib - gen/test_fib.c + ${qjs_gen_test_fib} ) add_qjs_libc_if_needed(test_fib) target_include_directories(test_fib PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})