diff --git a/.github/workflows/native-gem-precompilation.yml b/.github/workflows/native-gem-precompilation.yml new file mode 100644 index 0000000000..3015bc6d72 --- /dev/null +++ b/.github/workflows/native-gem-precompilation.yml @@ -0,0 +1,204 @@ +name: Native gem precompilation +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +on: + push: + branches: + - main + tags: + - v* + pull_request: + paths: + - Rakefile + - Gemfile + - Gemfile.lock + - prism.gemspec + - ext/prism/** + - lib/prism.rb + - bin/test-gem-* + - bin/build-gems + - .github/workflows/native-gem-precompilation.yml + workflow_dispatch: + +jobs: + native_setup: + name: "Setup" + runs-on: ubuntu-latest + outputs: + rcd_image_version: ${{ steps.rcd_image_version.outputs.rcd_image_version }} + steps: + - uses: actions/checkout@v6 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4" + bundler-cache: true + - id: rcd_image_version + run: bundle exec ruby -e 'require "rake_compiler_dock"; puts "rcd_image_version=#{RakeCompilerDock::IMAGE_VERSION}"' >> $GITHUB_OUTPUT + + build_source_gem: + name: "build source" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4" + bundler-cache: true + - run: ./bin/test-gem-build gems ruby + - uses: actions/upload-artifact@v7 + with: + name: source-gem + path: gems + retention-days: 1 + + install_source_gem: + needs: build_source_gem + name: "test source ${{ matrix.os }} ${{ matrix.ruby }}" + strategy: + fail-fast: false + matrix: + os: [ubuntu, macos, windows] + ruby: ["3.3", "3.4", "4.0"] + runs-on: ${{ matrix.os }}-latest + steps: + - if: matrix.os == 'windows' + name: configure git crlf + run: | + git config --system core.autocrlf false + git config --system core.eol lf + - uses: actions/checkout@v6 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + - uses: actions/download-artifact@v8 + with: + name: source-gem + path: gems + - run: ./bin/test-gem-install gems + shell: sh + + build_native_gem: + needs: native_setup + name: "build native ${{ matrix.platform }}" + strategy: + fail-fast: false + matrix: + platform: + - aarch64-linux-gnu + - aarch64-linux-musl + - aarch64-mingw-ucrt + - arm-linux-gnu + - arm-linux-musl + - arm64-darwin + - x64-mingw-ucrt + - x86_64-darwin + - x86_64-linux-gnu + - x86_64-linux-musl + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - run: | + docker run --rm -v $PWD:/work -w /work \ + ghcr.io/rake-compiler/rake-compiler-dock-image:${{ needs.native_setup.outputs.rcd_image_version }}-mri-${{ matrix.platform }} \ + ./bin/test-gem-build gems ${{ matrix.platform }} + - uses: actions/upload-artifact@v7 + with: + name: "cruby-${{ matrix.platform }}-gem" + path: gems + retention-days: 1 + + test_linux_native: + name: "${{ matrix.platform }} ${{ matrix.ruby }}" + needs: build_native_gem + strategy: + fail-fast: false + matrix: + platform: + - aarch64-linux-gnu + - aarch64-linux-musl + - arm-linux-gnu + - arm-linux-musl + - x86_64-linux-gnu + - x86_64-linux-musl + ruby: ["3.3", "3.4", "4.0"] + include: + # musl platforms need alpine image and build-base + - { platform: aarch64-linux-musl, docker_tag: "-alpine", bootstrap: "apk add build-base linux-headers yaml-dev &&" } + - { platform: arm-linux-musl, docker_tag: "-alpine", bootstrap: "apk add build-base linux-headers yaml-dev &&" } + - { platform: x86_64-linux-musl, docker_tag: "-alpine", bootstrap: "apk add build-base linux-headers yaml-dev &&" } + # docker platform for each platform + - { platform: aarch64-linux-gnu, runner: ubuntu-24.04-arm, docker_platform: "--platform=linux/arm64" } + - { platform: aarch64-linux-musl, runner: ubuntu-24.04-arm, docker_platform: "--platform=linux/arm64" } + - { platform: arm-linux-gnu, runner: ubuntu-24.04-arm, docker_platform: "--platform=linux/arm/v7" } + - { platform: arm-linux-musl, runner: ubuntu-24.04-arm, docker_platform: "--platform=linux/arm/v7" } + runs-on: ${{ matrix.runner || 'ubuntu-latest' }} + steps: + - uses: actions/checkout@v6 + - uses: actions/download-artifact@v8 + with: + name: cruby-${{ matrix.platform }}-gem + path: gems + - run: | + docker run --rm -v $PWD:/work -w /work \ + ${{ matrix.docker_platform }} ruby:${{ matrix.ruby }}${{ matrix.docker_tag }} \ + sh -c " + ${{ matrix.bootstrap }} + ./bin/test-gem-install ./gems + " + + test_darwin_native: + name: "${{ matrix.platform }} ${{ matrix.ruby }}" + needs: build_native_gem + strategy: + fail-fast: false + matrix: + os: [macos-15, macos-15-intel] + ruby: ["3.3", "3.4", "4.0"] + include: + - os: macos-15 + platform: arm64-darwin + - os: macos-15-intel + platform: x86_64-darwin + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v6 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "${{ matrix.ruby }}" + - uses: actions/download-artifact@v8 + with: + name: cruby-${{ matrix.platform }}-gem + path: gems + - run: ./bin/test-gem-install gems + + test_windows_native: + name: "${{ matrix.platform }} ${{ matrix.ruby }}" + needs: build_native_gem + strategy: + fail-fast: false + matrix: + platform: [x64-mingw-ucrt, aarch64-mingw-ucrt] + ruby: ["3.3", "3.4", "4.0"] + include: + - { platform: x64-mingw-ucrt, os: windows-latest } + - { platform: aarch64-mingw-ucrt, os: windows-11-arm } + exclude: + - { platform: aarch64-mingw-ucrt, ruby: "3.3" } + runs-on: ${{ matrix.os }} + steps: + - name: configure git crlf + run: | + git config --system core.autocrlf false + git config --system core.eol lf + - uses: actions/checkout@v6 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "${{ matrix.ruby }}" + - uses: actions/download-artifact@v8 + with: + name: cruby-${{ matrix.platform }}-gem + path: gems + - run: ./bin/test-gem-install gems + shell: bash diff --git a/.github/workflows/publish-gem.yml b/.github/workflows/publish-gem.yml deleted file mode 100644 index 87d6cf5657..0000000000 --- a/.github/workflows/publish-gem.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Publish gem to rubygems.org - -on: - push: - tags: - - 'v*' - -permissions: - contents: read - -jobs: - push: - if: github.repository == 'ruby/prism' - runs-on: ubuntu-latest - - environment: - name: rubygems.org - url: https://rubygems.org/gems/prism - - permissions: - contents: write - id-token: write - - steps: - - name: Harden Runner - uses: step-security/harden-runner@v2 - with: - egress-policy: audit - - - uses: actions/checkout@v6 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: "3.4" - bundler-cache: true - - - name: Publish to RubyGems - uses: rubygems/release-gem@v1 diff --git a/Gemfile b/Gemfile index abcb824ddb..4f3551357a 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ gem "benchmark-ips" gem "parser" gem "rake" gem "rake-compiler" +gem "rake-compiler-dock", "~> 1.12.0" gem "ruby_parser" gem "test-unit" diff --git a/Gemfile.lock b/Gemfile.lock index f42c601c6f..6c738e2e7e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,6 +13,12 @@ GEM erb (6.0.2) erb (6.0.2-java) ffi (1.17.3) + ffi (1.17.3-aarch64-linux-gnu) + ffi (1.17.3-aarch64-linux-musl) + ffi (1.17.3-arm64-darwin) + ffi (1.17.3-x86_64-darwin) + ffi (1.17.3-x86_64-linux-gnu) + ffi (1.17.3-x86_64-linux-musl) io-console (0.8.2) io-console (0.8.2-java) irb (1.17.0) @@ -25,8 +31,20 @@ GEM nokogiri (1.19.1) mini_portile2 (~> 2.8.2) racc (~> 1.4) + nokogiri (1.19.1-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.1-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.19.1-arm64-darwin) + racc (~> 1.4) nokogiri (1.19.1-java) racc (~> 1.4) + nokogiri (1.19.1-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.19.1-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.1-x86_64-linux-musl) + racc (~> 1.4) onigmo (0.1.0) parser (3.3.10.2) ast (~> 2.4.1) @@ -46,6 +64,7 @@ GEM rake (13.3.1) rake-compiler (1.3.1) rake + rake-compiler-dock (1.12.0) rdoc (7.2.0) erb psych (>= 4.0.0) @@ -64,8 +83,14 @@ GEM tsort (0.2.0) PLATFORMS + aarch64-linux-gnu + aarch64-linux-musl + arm64-darwin java ruby + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES benchmark-ips @@ -76,6 +101,7 @@ DEPENDENCIES prism! rake rake-compiler + rake-compiler-dock (~> 1.12.0) rdoc ruby_memcheck ruby_parser diff --git a/Rakefile b/Rakefile index eb96249985..b418d3025c 100644 --- a/Rakefile +++ b/Rakefile @@ -3,6 +3,15 @@ require "bundler/gem_tasks" require "rake/clean" +begin + require "rake_compiler_dock" +rescue LoadError + # rake_compiler_dock is not available in versioned Gemfiles (gemfiles/2.7/, etc.) + # that don't include the gem. The native gem tasks will not be defined. +end + +PRISM_SPEC = Bundler.load_gemspec("prism.gemspec") + task default: %i[compile test] require_relative "templates/template" @@ -28,14 +37,55 @@ task build: [:check_manifest, :templates] # the C extension task "compile:prism" => ["templates"] # must be before the ExtensionTask is created +RCD_CROSS_PLATFORMS = %w[ + aarch64-linux-gnu + aarch64-linux-musl + aarch64-mingw-ucrt + arm-linux-gnu + arm-linux-musl + arm64-darwin + x64-mingw-ucrt + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl +] + +if defined?(RakeCompilerDock) + RakeCompilerDock.set_ruby_cc_version(">= 3.3") + + namespace "gem" do + RCD_CROSS_PLATFORMS.each do |platform| + desc "build native gem for #{platform}" + task platform do + RakeCompilerDock.sh(<<~EOF, platform: platform, verbose: true) + gem install bundler --no-document && + bundle && + bundle exec rake gem:#{platform}:build + EOF + end + + namespace platform do + # this runs in the rake-compiler-dock docker container + task "build" => ["templates"] do + Rake::Task["native:#{platform}"].invoke + Rake::Task["pkg/#{PRISM_SPEC.full_name}-#{Gem::Platform.new(platform)}.gem"].invoke + end + end + end + + desc "build native gem for all platforms" + task "all" => [RCD_CROSS_PLATFORMS, "build"].flatten + end +end + if RUBY_ENGINE == "ruby" and !ENV["PRISM_FFI_BACKEND"] require "rake/extensiontask" - Rake::ExtensionTask.new(:compile) do |ext| - ext.name = "prism" + Rake::ExtensionTask.new("prism", PRISM_SPEC) do |ext| ext.ext_dir = "ext/prism" ext.lib_dir = "lib/prism" - ext.gem_spec = Gem::Specification.load("prism.gemspec") + ext.cross_compile = true + ext.cross_platform = RCD_CROSS_PLATFORMS end elsif RUBY_ENGINE == "jruby" require "rake/javaextensiontask" @@ -46,10 +96,32 @@ elsif RUBY_ENGINE == "jruby" ext.ext_dir = "java/api" ext.lib_dir = "tmp" ext.release = "21" - ext.gem_spec = Gem::Specification.load("prism.gemspec") + ext.gem_spec = PRISM_SPEC end end +desc "Temporarily set VERSION to a unique timestamp" +task "set-version-to-timestamp" do + # this task is used by bin/test-gem-build + # to test building, packaging, and installing a precompiled gem + version_re = /spec\.version = "(.*)"/ + + gemspec_path = File.join(__dir__, "prism.gemspec") + gemspec_contents = File.read(gemspec_path) + + current_version_string = version_re.match(gemspec_contents)[1] + current_version = Gem::Version.new(current_version_string) + + fake_version = Gem::Version.new(format("%s.test.%s", current_version.bump, Time.now.strftime("%Y.%m%d.%H%M"))) + + unless gemspec_contents.gsub!(version_re, "spec.version = \"#{fake_version}\"") + raise("Could not hack the VERSION constant") + end + + File.write(gemspec_path, gemspec_contents) + puts "NOTE: wrote version as \"#{fake_version}\"" +end + # So `rake clobber` will delete generated files CLOBBER.concat(Prism::Template::TEMPLATES) CLOBBER.concat(["build"]) diff --git a/bin/build-gems b/bin/build-gems new file mode 100755 index 0000000000..b83acf7993 --- /dev/null +++ b/bin/build-gems @@ -0,0 +1,53 @@ +#! /usr/bin/env bash +# +# script to build gems for all relevant platforms +# +set -o errexit +set -o nounset +set -x + +rm -rf tmp pkg gems +mkdir -p gems + +# prelude: vendor dependencies +bundle update +bundle package + +# safety check: let's check that things work +bundle exec rake clean clobber +bundle exec rake compile test + +# package the gems, including precompiled native +bundle exec rake clean clobber +bundle exec rake gem:all +cp -v pkg/prism*.gem gems + +# test those gem files! +for gemfile in gems/*.gem ; do + ./bin/test-gem-file-contents "$gemfile" +done + +# smoke test: install and load the native gem for the host platform +host_gem=$(ruby -e ' + platform = Gem::Platform.local + # normalize: remove version from platform string (e.g., "x86_64-linux-gnu" not "x86_64-linux-gnu6.2") + normalized = [platform.cpu, platform.os, platform.version].compact.join("-") + gem = Dir.glob("gems/prism-*-#{normalized}.gem").first + gem ||= Dir.glob("gems/prism-*-#{platform.cpu}-#{platform.os}*.gem").first + puts gem.to_s +') +if [ -n "$host_gem" ] ; then + echo "--- smoke test: install and load $host_gem ---" + test_dir=$(mktemp -d) + GEM_HOME="$test_dir" GEM_PATH="$test_dir" gem install --no-document "$host_gem" + GEM_HOME="$test_dir" GEM_PATH="$test_dir" ruby -e 'require "prism"; Prism.parse("1 + 2"); puts "smoke test passed: #{Prism::VERSION}"' + rm -rf "$test_dir" + echo "--- smoke test passed ---" +else + echo "--- skipping install smoke test (no native gem for this host platform) ---" +fi + +# checksums should be included in the release notes +pushd gems + ls *.gem | sort | xargs shasum -a 256 +popd diff --git a/bin/test-gem-build b/bin/test-gem-build new file mode 100755 index 0000000000..58bdcd21f9 --- /dev/null +++ b/bin/test-gem-build @@ -0,0 +1,34 @@ +#! /usr/bin/env bash +# +# run as part of CI +# +if [[ $# -lt 2 ]] ; then + echo "usage: $(basename $0) " + exit 1 +fi + +set -e -u + +OUTPUT_DIR=$1 +BUILD_NATIVE_GEM=$2 + +test -e /etc/os-release && cat /etc/os-release + +set -x + +bundle config set without development +bundle config set frozen false +bundle install --local || bundle install +bundle exec rake set-version-to-timestamp + +if [[ "${BUILD_NATIVE_GEM}" == "ruby" ]] ; then + bundle exec rake build +else + bundle exec rake gem:${BUILD_NATIVE_GEM}:build +fi + +./bin/test-gem-file-contents pkg/*.gem + +mkdir -p ${OUTPUT_DIR} +cp -v pkg/*.gem ${OUTPUT_DIR} +ls -l ${OUTPUT_DIR}/* diff --git a/bin/test-gem-file-contents b/bin/test-gem-file-contents new file mode 100755 index 0000000000..610c4ca21f --- /dev/null +++ b/bin/test-gem-file-contents @@ -0,0 +1,218 @@ +#! /usr/bin/env ruby +# +# this script is intended to run as part of the CI test suite. +# +# it inspects the contents of a gem file -- both the files and the gemspec -- to ensure we're +# packaging what we expect, and that we're not packaging anything we don't expect. +# +# this file isn't in the `test/` subdirectory because it's intended to be run standalone against a +# built gem file (and not against the source code or behavior of the gem itself). +# +require "bundler/inline" + +gemfile do + source "https://rubygems.org" + gem "minitest" + gem "minitest-reporters" +end + +require "yaml" + +def usage_and_exit(message = nil) + puts "ERROR: #{message}" if message + puts "USAGE: #{File.basename(__FILE__)} [options]" + exit(1) +end + +usage_and_exit if ARGV.include?("-h") +usage_and_exit unless (gemfile = ARGV[0]) +usage_and_exit("#{gemfile} does not exist") unless File.file?(gemfile) +usage_and_exit("#{gemfile} is not a gem") unless /\.gem$/.match?(gemfile) +gemfile = File.expand_path(gemfile) + +gemfile_contents = Dir.mktmpdir do |dir| + Dir.chdir(dir) do + unless system("tar", "-xf", gemfile, "data.tar.gz") + raise "could not unpack gem #{gemfile}" + end + + `tar -ztf data.tar.gz`.split("\n") + end +end + +gemspec = Dir.mktmpdir do |dir| + Dir.chdir(dir) do + unless system("tar", "-xf", gemfile, "metadata.gz") + raise "could not unpack gem #{gemfile}" + end + + YAML.unsafe_load(`gunzip -c metadata.gz`) + end +end + +if ARGV.include?("-v") + puts "---------- gemfile contents ----------" + puts gemfile_contents + puts + puts "---------- gemspec ----------" + puts gemspec.to_ruby + puts +end + +require "minitest/autorun" +require "minitest/reporters" +Minitest::Reporters.use!([Minitest::Reporters::SpecReporter.new]) + +puts "Testing '#{gemfile}' (#{gemspec.platform})" +describe File.basename(gemfile) do + let(:native_ruby_versions) { ["3.3", "3.4", "4.0"] } + + describe "setup" do + it "gemfile contains some files" do + actual = gemfile_contents.length + assert_operator(actual, :>, 10, "expected gemfile to contain more than #{actual} files") + end + + it "gemspec is a Gem::Specification" do + assert_equal(Gem::Specification, gemspec.class) + end + end + + describe "all platforms" do + ["lib"].each do |dir| + it "contains every ruby file in #{dir}/" do + committed_files = `git ls-files #{dir}`.split("\n").grep(/\.rb$/) + generated_files = `git ls-files templates/lib/prism/*.rb.erb`.split("\n").map { |f| f.delete_prefix("templates/").delete_suffix(".erb") } + expected_files = Set.new(committed_files + generated_files) + + skip "looks like this isn't a git repository" if expected_files.empty? + + actual_files = Set.new(gemfile_contents.select { |f| f.start_with?("#{dir}/") }.grep(/\.rb$/)) + assert_equal(expected_files, actual_files) + end + end + + ["test"].each do |dir| + it "does not contain files from #{dir}/" do + actual = gemfile_contents.select { |f| f.start_with?("#{dir}/") }.grep(/\.rb$/) + assert_empty(actual) + end + end + + it "does not contain the Gemfile" do + refute_includes(gemfile_contents, "Gemfile") + end + end + + if gemspec.platform == Gem::Platform::RUBY + describe "ruby platform" do + it "contains C source files in ext/" do + assert_operator(gemfile_contents.count { |f| File.fnmatch?("ext/**/*.c", f) }, :>=, 2) + end + + it "contains header files" do + assert_operator(gemfile_contents.count { |f| File.fnmatch?("ext/**/*.h", f) }, :>=, 1) + assert_operator(gemfile_contents.count { |f| File.fnmatch?("include/**/*.h", f) }, :>=, 20) + end + + it "contains C source files in src/" do + assert_operator(gemfile_contents.count { |f| File.fnmatch?("src/*.c", f) }, :>=, 10) + end + + it "contains extconf.rb" do + assert_includes(gemfile_contents, "ext/prism/extconf.rb") + end + + it "has extensions set" do + assert_includes(gemspec.extensions, "ext/prism/extconf.rb") + end + + it "sets required_ruby_version appropriately" do + assert( + gemspec.required_ruby_version.satisfied_by?(Gem::Version.new("2.7")), + "required_ruby_version='#{gemspec.required_ruby_version}' should support ruby 2.7" + ) + native_ruby_versions.each do |v| + assert( + gemspec.required_ruby_version.satisfied_by?(Gem::Version.new(v)), + "required_ruby_version='#{gemspec.required_ruby_version}' should support ruby #{v}" + ) + end + end + end + end + + if gemspec.platform.is_a?(Gem::Platform) && gemspec.platform.cpu + describe "native platform" do + # aarch64-mingw-ucrt only has cross-rubies for 3.4+, all other platforms have all versions + let(:expected_ruby_versions) do + if gemspec.platform.to_s == "aarch64-mingw-ucrt" + native_ruby_versions.reject { |v| v == "3.3" } + else + native_ruby_versions + end + end + + it "contains expected shared library files" do + expected_ruby_versions.each do |version| + actual = gemfile_contents.find do |p| + File.fnmatch?("lib/prism/#{version}/prism.{so,bundle}", p, File::FNM_EXTGLOB) + end + assert(actual, "expected to find shared library file for ruby #{version}") + end + + actual = gemfile_contents.find do |p| + File.fnmatch?("lib/prism/prism.{so,bundle}", p, File::FNM_EXTGLOB) + end + refute(actual, "did not expect to find shared library file in lib/prism") + + actual = gemfile_contents.find_all do |p| + File.fnmatch?("lib/prism/**/*.{so,bundle}", p, File::FNM_EXTGLOB) + end + assert_equal( + expected_ruby_versions.length, + actual.length, + "expected exactly #{expected_ruby_versions.length} shared library files, found: #{actual.inspect}" + ) + end + + it "has extensions cleared" do + assert_empty(gemspec.extensions) + end + + it "sets required_ruby_version appropriately" do + expected_ruby_versions.each do |v| + assert( + gemspec.required_ruby_version.satisfied_by?(Gem::Version.new(v)), + "required_ruby_version='#{gemspec.required_ruby_version}' should support ruby #{v}" + ) + end + + # verify unsupported versions are excluded + unsupported_ruby_versions = native_ruby_versions - expected_ruby_versions + unsupported_ruby_versions.each do |v| + refute( + gemspec.required_ruby_version.satisfied_by?(Gem::Version.new(v)), + "required_ruby_version='#{gemspec.required_ruby_version}' should NOT support ruby #{v}" + ) + end + + refute( + gemspec.required_ruby_version.satisfied_by?(Gem::Version.new("2.7")), + "required_ruby_version='#{gemspec.required_ruby_version}' should NOT support ruby 2.7" + ) + + refute( + gemspec.required_ruby_version.satisfied_by?(Gem::Version.new("3.2")), + "required_ruby_version='#{gemspec.required_ruby_version}' should NOT support ruby 3.2" + ) + + # verify the upper bound is set (required_ruby_version should not be open-ended) + refute( + gemspec.required_ruby_version.satisfied_by?(Gem::Version.new("9.9")), + "required_ruby_version='#{gemspec.required_ruby_version}' should have an upper bound" + ) + end + end + end +end diff --git a/bin/test-gem-install b/bin/test-gem-install new file mode 100755 index 0000000000..e7e10c301e --- /dev/null +++ b/bin/test-gem-install @@ -0,0 +1,30 @@ +#! /usr/bin/env sh +# +# run as part of CI +# +if [ $# -lt 1 ] ; then + echo "usage: $(basename $0) " + exit 1 +fi + +GEMS_DIR=$1 +shift + +test -e /etc/os-release && cat /etc/os-release + +set -e -x -u + +cd $GEMS_DIR + + gemfile=$(ls *.gem | head -n1) + ls -l ${gemfile} + gem install --no-document ${gemfile} + gem list -d prism + +cd .. + +bundle config set without development +bundle install --local || bundle install + +rm -rf lib ext # ensure we don't use the local files +rake test diff --git a/docs/releasing.md b/docs/releasing.md index 6d0f892926..c1bf2de287 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -71,5 +71,36 @@ git push ## Publishing -* Update the GitHub release page with a copy of the latest entry in the `CHANGELOG.md` file. -* Push a new tag to the GitHub repository, following the `vX.Y.Z` format. +* Build all gems (source + native) locally: + +```sh +bin/build-gems +``` + +This script will: +1. Run `bundle update` and `bundle package` to vendor dependencies. +2. Run a safety check (`compile` and `test`). +3. Build all gems via `rake gem:all` (source gem + native gems for all platforms using rake-compiler-dock). +4. Verify all built gems with `bin/test-gem-file-contents`. +5. Print SHA256 checksums for inclusion in release notes. + +* Push native gems first, then the source gem last (so that when users see the new version, native gems are already available): + +```sh +# push native gems first +for gem in gems/prism-*-*.gem ; do + gem push "$gem" +done + +# push source gem last +gem push gems/prism-${PRISM_VERSION}.gem +``` + +* Push a new tag to the GitHub repository, following the `vX.Y.Z` format: + +```sh +git tag "v${PRISM_VERSION}" +git push origin "v${PRISM_VERSION}" +``` + +* Update the GitHub release page with a copy of the latest entry in the `CHANGELOG.md` file, including the SHA256 checksums from the build output. diff --git a/lib/prism.rb b/lib/prism.rb index 8f0342724a..4b893873fa 100644 --- a/lib/prism.rb +++ b/lib/prism.rb @@ -136,7 +136,32 @@ def self.find(callable, rubyvm: !!defined?(RubyVM)) # The C extension is the default backend on CRuby. Prism::BACKEND = :CEXT - require "prism/prism" + begin + # native precompiled gems package shared libraries in /lib/prism/ + require_relative "prism/#{RUBY_VERSION[/\d+\.\d+/]}/prism" + rescue LoadError => e + if e.message.include?("GLIBC") + warn(<<~EOM) + + ERROR: It looks like you're trying to use Prism as a precompiled native gem on a system + with an unsupported version of glibc. + + #{e.message} + + If that's the case, then please install Prism via the `ruby` platform gem: + gem install prism --platform=ruby + or, in your Gemfile: + gem "prism", force_ruby_platform: true + + EOM + raise e + end + + # use "require" instead of "require_relative" because non-native gems will place C extension + # files in Gem::BasicSpecification#extension_dir after compilation (during normal installation), + # which is in $LOAD_PATH but not necessarily relative to this file + require "prism/prism" + end else # The FFI backend is used on other Ruby implementations. Prism::BACKEND = :FFI