diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cbea867..fdb4c02 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ jobs: rubocop: name: Lint (Rubocop) runs-on: ubuntu-20.04 - container: ruby:2.6 + container: ruby:2.7 steps: - name: Checkout uses: actions/checkout@v2 @@ -30,8 +30,12 @@ jobs: outputs: GEM_VERSION: ${{ steps.set-metadata.outputs.GEM_VERSION }} runs-on: ubuntu-20.04 - container: ruby:2.6 + container: ruby:2.7 steps: + - name: Update Rubygems and Bundler + run: | + gem update --system 3.3.26 + gem install bundler -v '~> 2.3.26' - name: Checkout uses: actions/checkout@v2 - name: Bundle @@ -118,6 +122,14 @@ jobs: - name: Inject V8 run: | ./libexec/inject-libv8 ${{ steps.set-metadata.outputs.NODE_VERSION }} + - name: Test V8 in C++ + if: matrix.platform != 'arm64' + run: | + cd test/gtest + cmake -S . -B build + cd build + cmake --build . + ./c_v8_tests - name: Build binary gem run: | bundle exec rake binary @@ -191,10 +203,10 @@ jobs: run: | case ${{ matrix.libc }} in gnu) - echo 'ruby:2.6' + echo 'ruby:2.7' ;; musl) - echo 'ruby:2.6-alpine' + echo 'ruby:2.7-alpine' ;; esac | tee container_image echo "::set-output name=image::$(cat container_image)" @@ -203,12 +215,20 @@ jobs: echo "::set-output name=id::$(cat container_id)" - name: Install Alpine system dependencies if: ${{ matrix.libc == 'musl' }} - run: docker exec -w "${PWD}" ${{ steps.container.outputs.id }} apk add --no-cache build-base linux-headers bash python3 git curl tar + run: docker exec -w "${PWD}" ${{ steps.container.outputs.id }} apk add --no-cache build-base linux-headers bash python3 git curl tar cmake + - name: Install Debian system dependencies + if: ${{ matrix.libc == 'gnu' }} + run: | + docker exec -w "${PWD}" ${{ steps.container.outputs.id }} apt-get update + docker exec -w "${PWD}" ${{ steps.container.outputs.id }} apt-get install -y cmake - name: Install Debian cross-compiler if: ${{ matrix.libc == 'gnu' && matrix.platform != 'amd64' }} run: | - docker exec -w "${PWD}" ${{ steps.container.outputs.id }} apt-get update docker exec -w "${PWD}" ${{ steps.container.outputs.id }} apt-get install -y binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu g++-aarch64-linux-gnu + - name: Update Rubygems and Bundler + run: | + docker exec -w "${PWD}" ${{ steps.container.outputs.id }} gem update --system 3.3.26 + docker exec -w "${PWD}" ${{ steps.container.outputs.id }} gem install bundler -v '~> 2.3.26' - name: Checkout uses: actions/checkout@v2 - name: Bundle @@ -245,6 +265,10 @@ jobs: - name: Inject V8 run: | docker exec -w "${PWD}" ${{ steps.container.outputs.id }} ./libexec/inject-libv8 ${{ steps.set-metadata.outputs.NODE_VERSION }} + - name: Test V8 in C++ + if: matrix.platform != 'arm64' + run: | + docker exec -w "${PWD}" ${{ steps.container.outputs.id }} bash -c "cd test/gtest && cmake -S . -B build && cd build && cmake --build . && ctest" - name: Build binary gem run: | docker exec -w "${PWD}" ${{ steps.container.outputs.id }} bundle exec rake binary[${{ steps.platform.outputs.ruby_target_platform }}] @@ -266,12 +290,6 @@ jobs: - amd64 # other platforms would need emulation, which is way too slow container: - - image: ruby:2.6 - version: '2.6' - libc: gnu - - image: ruby:2.6-alpine - version: '2.6' - libc: musl - image: ruby:2.7 version: '2.7' libc: gnu @@ -298,6 +316,10 @@ jobs: - name: Install Alpine system dependencies if: ${{ matrix.container.libc == 'musl' }} run: apk add --no-cache build-base linux-headers bash python3 git curl tar + - name: Update Rubygems and Bundler + run: | + gem update --system 3.3.26 + gem install bundler -v '~> 2.3.26' - name: Set metadata id: set-metadata run: | @@ -312,6 +334,7 @@ jobs: run: gem install --verbose pkg/libv8-node-${{ needs.build-ruby.outputs.GEM_VERSION }}.gem - name: Test with mini_racer run: | + export BUNDLE_FORCE_RUBY_PLATFORM=y git clone https://github.com/rubyjs/mini_racer.git test/mini_racer --depth 1 cd test/mini_racer git fetch origin refs/pull/261/head @@ -364,7 +387,6 @@ jobs: fail-fast: false matrix: version: - - '2.6' - '2.7' - '3.0' - '3.1' @@ -378,9 +400,6 @@ jobs: - gnu - musl include: - - version: '2.6' - platform: 'arm64' - libc: 'gnu' - version: '2.7' platform: 'arm64' libc: 'gnu' @@ -418,6 +437,10 @@ jobs: - name: Install Alpine system dependencies if: ${{ matrix.libc == 'musl' }} run: docker exec -w "${PWD}" ${{ steps.container.outputs.id }} apk add --no-cache build-base git libstdc++ + - name: Update Rubygems and Bundler + run: | + docker exec -w "${PWD}" ${{ steps.container.outputs.id }} gem update --system 3.3.26 + docker exec -w "${PWD}" ${{ steps.container.outputs.id }} gem install bundler -v '~> 2.3.26' - name: Set metadata id: set-metadata run: | diff --git a/.rubocop.yml b/.rubocop.yml index a3ec089..e57f216 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,9 +1,12 @@ AllCops: + SuggestExtensions: false + NewCops: disable TargetRubyVersion: 2.0 Exclude: - src/**/* - pkg/**/* - vendor/**/* + - test/**/* Naming/FileName: Exclude: @@ -26,3 +29,6 @@ Style/WordArray: Style/PerlBackrefs: Enabled: false + +Gemspec/RequiredRubyVersion: + Enabled: false diff --git a/Dockerfile b/Dockerfile index d48c278..ce6c983 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,8 @@ FROM ruby:${RUBY_VERSION} RUN test ! -f /etc/alpine-release || apk add --no-cache build-base bash python2 python3 git curl tar +RUN gem update --system 3.3.26 && gem install bundler -v '~> 2.3.26' + RUN mkdir -p /code WORKDIR /code diff --git a/Makefile b/Makefile index 0861453..319d559 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,9 @@ gem: pkg/libv8-node-$(VERSION)-$(CPU)-$(OS).gem test: test/$(CPU)-$(OS) +ctest: vendor/v8 + cd test/gtest && cmake -S . -B build && cd build && cmake --build . && ctest + src/node-v$(NODE_VERSION).tar.gz: ./libexec/download-node $(NODE_VERSION) diff --git a/Rakefile b/Rakefile index ade35ea..99467ee 100644 --- a/Rakefile +++ b/Rakefile @@ -23,9 +23,7 @@ task :compile, [:platform] => [] do |_, args| local_platform = Gem::Platform.local target_platform = Gem::Platform.new(ENV['RUBY_TARGET_PLATFORM'] || args.to_h[:platform] || Gem::Platform.local) - if target_platform.os == 'darwin' - target_platform.instance_eval { @version = nil } - end + target_platform.instance_eval { @version = nil } if target_platform.os == 'darwin' puts "local platform: #{local_platform}" puts "target platform: #{target_platform}" @@ -46,9 +44,7 @@ task :binary, [:platform] => [:compile] do |_, args| local_platform = Gem::Platform.local.dup target_platform = Gem::Platform.new(ENV['RUBY_TARGET_PLATFORM'] || args.to_h[:platform] || Gem::Platform.local) - if target_platform.os == 'darwin' - target_platform.instance_eval { @version = nil } - end + target_platform.instance_eval { @version = nil } if target_platform.os == 'darwin' puts "local platform: #{local_platform}" puts "target platform: #{target_platform}" diff --git a/ext/libv8-node/builder.rb b/ext/libv8-node/builder.rb index 84eae98..dab2771 100644 --- a/ext/libv8-node/builder.rb +++ b/ext/libv8-node/builder.rb @@ -1,4 +1,4 @@ -unless $LOAD_PATH.include?(File.expand_path('../../lib', __dir__)) +unless $LOAD_PATH.include?(File.expand_path('../../lib', __dir__)) # rubocop:disable Style/IfUnlessModifier $LOAD_PATH.unshift(File.expand_path('../../lib', __dir__)) end require 'libv8/node/version' diff --git a/ext/libv8-node/location.rb b/ext/libv8-node/location.rb index 9b4cf68..07acff8 100644 --- a/ext/libv8-node/location.rb +++ b/ext/libv8-node/location.rb @@ -45,7 +45,7 @@ def configure(context = MkmfContext.new) def verify_installation! include_paths = Libv8::Node::Paths.include_paths - unless include_paths.detect { |p| Pathname(p).join('v8.h').exist? } + unless include_paths.detect { |p| Pathname(p).join('v8.h').exist? } # rubocop:disable Style/IfUnlessModifier raise(HeaderNotFound, "Unable to locate 'v8.h' in the libv8 header paths: #{include_paths.inspect}") end diff --git a/libexec/build-libv8 b/libexec/build-libv8 index 07bb500..309c8e9 100755 --- a/libexec/build-libv8 +++ b/libexec/build-libv8 @@ -17,12 +17,6 @@ BUILDTYPE="${BUILDTYPE:-Release}" cd "${src}/node-v${version}" -if command -v python3 >/dev/null 2>&1; then - PYTHON="${PYTHON:-python3}" -else - PYTHON="${PYTHON:-python2}" -fi - configure_flags='--openssl-no-asm --without-npm --shared --with-intl=full-icu' eval "$("${libexec}/platform")" @@ -33,10 +27,18 @@ ${CC} -v ${CXX} -v # shellcheck disable=SC2086 -"${PYTHON}" configure ${configure_flags} +./configure ${configure_flags} make BUILDTYPE="${BUILDTYPE}" config.gypi make BUILDTYPE="${BUILDTYPE}" "out/Makefile" +# workaround for node specifying `-msign-return-address=all` in ALL `CFLAGS` for aarch64 builds +# (if the host isn't also aarch64, this flag causes a compiler error) + +# shellcheck disable=SC2154 # these variables are defined by `eval`ing the output of the platform script above +if [ "$host_platform" != "$target_platform" ] && [ "${target_platform%%-*}" = "aarch64" ]; then + find . -iname "*.host.mk" -exec sed -i '/-msign-return-address/d' {} ';' +fi + export PATH="${PWD}/out/tools/bin:${PATH}" make -j"${NJOBS}" -C out BUILDTYPE="${BUILDTYPE}" V=0 diff --git a/libexec/build-monolith b/libexec/build-monolith index 62c916f..b6ffc15 100755 --- a/libexec/build-monolith +++ b/libexec/build-monolith @@ -21,13 +21,14 @@ platform=$(uname) rm -f "${LIBV8_MONOLITH}" case "${platform}" in "SunOS") - /usr/xpg4/bin/find . -path "./torque_*/**/*.o" -or -path "./v8*/**/*.o" -or -path "./icu*/**/*.o" | sort | uniq | xargs ar cq "${LIBV8_MONOLITH}" + /usr/xpg4/bin/find . '(' '!' -path './icutools/deps/icu-small/source/stubdata/stubdata.o' ')' -path "./torque_*/**/*.o" -or -path "./v8*/**/*.o" -or -path "./icu*/**/*.o" | sort | uniq | xargs ar cq "${LIBV8_MONOLITH}" ;; "Darwin") - /usr/bin/find . -path "./torque_*/**/*.o" -or -path "./v8*/**/*.o" -or -path "./icu*/**/*.o" | sort | uniq | xargs /usr/bin/ar -cq "${LIBV8_MONOLITH}" + /usr/bin/find . '(' '!' -path './icutools/deps/icu-small/source/stubdata/stubdata.o' ')' -path "./torque_*/**/*.o" -or -path "./v8*/**/*.o" -or -path "./icu*/**/*.o" | sort | uniq | xargs /usr/bin/ar -cq "${LIBV8_MONOLITH}" ;; "Linux") - find . -path "./torque_*/**/*.o" -or -path "./v8*/**/*.o" -or -path "./icu*/**/*.o" | sort | uniq | xargs ar -cq "${LIBV8_MONOLITH}" + find . '(' '!' -path './icutools/deps/icu-small/source/stubdata/stubdata.o' ')' -and '(' -path "./torque_*/**/*.o" -or -path "./v8*/**/*.o" -or -path "./icu*/**/*.o" ')' | sort | uniq | xargs ar -cqSP "${LIBV8_MONOLITH}" + ar -sP "${LIBV8_MONOLITH}" ;; *) echo "Unsupported platform: ${platform}" diff --git a/libexec/inject-libv8 b/libexec/inject-libv8 index 353d4ed..2142e1e 100755 --- a/libexec/inject-libv8 +++ b/libexec/inject-libv8 @@ -39,8 +39,35 @@ for lib in libv8_monolith.a; do mkdir -p "${dir}" rm -f "${dir}/${lib}" - echo "${BASEDIR}/out/${BUILDTYPE}/${lib} -> ${dir}/${lib}" - "${STRIP}" -S -x -o "${dir}/${lib}" "${lib}" + if [ "$STRIP_NEEDS_EXTRACT" = "y" ]; then + # manual extract/strip objects/build archive sequence + # because `strip` can't deal with these + # (presumably due to that folder issue mentioned below) + ( + tmpdir="$(mktemp -d)" + trap 'rm -r "$tmpdir"' EXIT + mkdir "$tmpdir/stage" + cd "$tmpdir/stage" + + # create folders named in `ar` archive (`ar -x` fails to create these) + "$AR" "$ARLISTFLAGS" "$BASEDIR/out/$BUILDTYPE/$lib" | while read -r path; do + dirname "$path" + done | uniq | xargs mkdir -p + "$AR" "$AREXTRACTFLAGS" "$BASEDIR/out/${BUILDTYPE}/$lib" + + # strip all objects + "$FIND" -type f -exec "$STRIP" -Sx {} + + + # rebuild the archive + "$FIND" -type f -exec "$AR" "$ARCOLLECTFLAGS" "../$lib" {} + + $ARBUILDSYMBOLS "../$lib" + mv "../$lib" "$dir/$lib" + ) + echo "${BASEDIR}/out/${BUILDTYPE}/${lib} -> ${dir}/${lib}" + else + echo "${BASEDIR}/out/${BUILDTYPE}/${lib} -> ${dir}/${lib}" + "${STRIP}" -S -x -o "${dir}/${lib}" "${lib}" + fi done mkdir -p "${top}/ext/libv8-node" diff --git a/libexec/platform b/libexec/platform index 582ec8b..91343aa 100755 --- a/libexec/platform +++ b/libexec/platform @@ -15,6 +15,14 @@ elif command -v cc >/dev/null 2>&1; then fi STRIP="${STRIP:-strip}" +AR="${AR:-ar}" +AREXTRACTFLAGS="${AREXTRACTFLAGS:--x}" +ARLISTFLAGS="${ARLISTFLAGS:--t}" +ARCOLLECTFLAGS="${ARCOLLECTFLAGS:-cqS}" +# this is the command to build the symbol table in an ar archive. +ARBUILDSYMBOLS="${ARBUILDSYMBOLS:-ranlib}" +FIND="${FIND:-find}" +STRIP_NEEDS_EXTRACT="${STRIP_NEEDS_EXTRACT:-n}" triple=$(${CC} -dumpmachine) host_platform="${triple}" @@ -63,6 +71,11 @@ case "${host_platform}" in CXX="${CXX:-/opt/local/gcc7/bin/g++}" STRIP="gstrip" ;; + *linux*) + STRIP_NEEDS_EXTRACT="y" + ARCOLLECTFLAGS="-cqSP" + ARBUILDSYMBOLS="${AR} -sP" + ;; esac if [ "${host_platform}" != "${target_platform}" ]; then @@ -146,6 +159,14 @@ export CC='${CC}' export CXX='${CXX}' host_platform='${host_platform}' target_platform='${target_platform}' +STRIP='$STRIP' +AR='$AR' +AREXTRACTFLAGS='$AREXTRACTFLAGS' +ARLISTFLAGS='$ARLISTFLAGS' +ARCOLLECTFLAGS='$ARCOLLECTFLAGS' +ARBUILDSYMBOLS='$ARBUILDSYMBOLS' +FIND='$FIND' +STRIP_NEEDS_EXTRACT='$STRIP_NEEDS_EXTRACT' EOF if [ -n "${CC_host:-}" ]; then cat < 12' - s.add_development_dependency 'rubocop', '~> 0.50.0' + s.add_development_dependency 'rubocop', '~> 1.44.0' end diff --git a/test/gtest/.gitignore b/test/gtest/.gitignore new file mode 100644 index 0000000..86201ed --- /dev/null +++ b/test/gtest/.gitignore @@ -0,0 +1,14 @@ +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps +build +lib +bin diff --git a/test/gtest/CMakeLists.txt b/test/gtest/CMakeLists.txt new file mode 100644 index 0000000..915947c --- /dev/null +++ b/test/gtest/CMakeLists.txt @@ -0,0 +1,51 @@ +cmake_minimum_required(VERSION 3.14) +project(gtest) + +# GoogleTest requires at least C++14 +set(CMAKE_CXX_STANDARD 14) + +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip +) +# For Windows: Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +enable_testing() + +include_directories(${CMAKE_SOURCE_DIR}/../../vendor/v8/include/) + +add_executable( + c_v8_tests + c_v8_tests.cc +) +target_link_libraries( + c_v8_tests + GTest::gtest_main +) + +string(TOLOWER ${CMAKE_SYSTEM_NAME} system_name) +string(TOLOWER ${CMAKE_SYSTEM_PROCESSOR} system_arch) + +set(vendor_arch "${system_arch}-${system_name}") + +if(${system_name} STREQUAL "linux") + try_compile(is_glibc ${CMAKE_BINARY_DIR}/check_glibc ${CMAKE_SOURCE_DIR}/check_glibc.c) + if(NOT is_glibc) + # assume non-glibc is musl-libc + string(APPEND vendor_arch "-musl") + endif() +endif() + +message(STATUS "Detected vendor architecture directory: ${vendor_arch}") + +# TODO?: Detect and support ruby-arch builds? +target_link_libraries(c_v8_tests ${CMAKE_SOURCE_DIR}/../../vendor/v8/${vendor_arch}/libv8/obj/libv8_monolith.a) + +# This has to be after the v8 monolith for some build setups. +target_link_libraries(c_v8_tests dl) + +include(GoogleTest) +gtest_discover_tests(c_v8_tests) diff --git a/test/gtest/Framework.h b/test/gtest/Framework.h new file mode 100644 index 0000000..ed54c59 --- /dev/null +++ b/test/gtest/Framework.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include + +struct Framework { + typedef std::function basic_main; + typedef std::function iso_main; + typedef std::function&)> ctx_main; + + inline static void run(basic_main main) { + std::shared_ptr platform = v8::platform::NewDefaultPlatform(); + v8::V8::InitializePlatform(platform.get()); + v8::V8::Initialize(); + main(); + + v8::V8::Dispose(); + v8::V8::ShutdownPlatform(); + } + + inline static void runWithIsolateRaw(iso_main main) { + Framework::run([main]() -> void { + v8::Isolate::CreateParams p; + p.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator(); + v8::Isolate* iso = v8::Isolate::New(p); + + main(iso); + + iso->Dispose(); + delete p.array_buffer_allocator; + }); + } + + inline static void runWithIsolate(iso_main main) { + Framework::runWithIsolateRaw([main](v8::Isolate* iso) -> void { + v8::Locker lock { iso }; + v8::Isolate::Scope iScope { iso }; + v8::HandleScope hScope { iso }; + + main(iso); + }); + } + + inline static void runWithContext(ctx_main main) { + Framework::runWithIsolate([main](v8::Isolate* iso) -> void { + v8::Local ctx = v8::Context::New(iso); + v8::Context::Scope cScope { ctx }; + + main(ctx); + }); + } +}; \ No newline at end of file diff --git a/test/gtest/c_v8_tests.cc b/test/gtest/c_v8_tests.cc new file mode 100644 index 0000000..28d4345 --- /dev/null +++ b/test/gtest/c_v8_tests.cc @@ -0,0 +1,18 @@ +#include + +#include +#include + +#include "Framework.h" + +// Demonstrate some basic assertions. +TEST(FRLocaleTest, LocaleTests) { + Framework::runWithContext([](v8::Local& ctx) -> void { + v8::Local script = v8::Script::Compile(ctx, v8::String::NewFromUtf8Literal(ctx->GetIsolate(), "new Date('April 28 2021').toLocaleDateString('fr-FR');")).ToLocalChecked(); + v8::Local result = script->Run(ctx).ToLocalChecked(); + v8::Local resultStr = result->ToString(ctx).ToLocalChecked(); + v8::String::Utf8Value resultUTF8(ctx->GetIsolate(), resultStr); + + EXPECT_STREQ(*resultUTF8, "28/04/2021"); + }); +} diff --git a/test/gtest/check_glibc.c b/test/gtest/check_glibc.c new file mode 100644 index 0000000..38e7e4b --- /dev/null +++ b/test/gtest/check_glibc.c @@ -0,0 +1,10 @@ +#warning "This is not a source file -- it exists purely to allow CMake to distinguish between glibc and musl-libc. If you see this message, you are likely doing something incorrectly." + +#include + +#ifndef __GLIBC__ +#error "__GLIBC__ is undef -- not glibc!" +#endif + +int main() { } +