diff --git a/.github/workflows/cibuildgem.yaml b/.github/workflows/cibuildgem.yaml new file mode 100644 index 0000000..69ff5d1 --- /dev/null +++ b/.github/workflows/cibuildgem.yaml @@ -0,0 +1,98 @@ +name: "Package and release gems with precompiled binaries" +on: + workflow_dispatch: + inputs: + release: + description: "If the whole build passes on all platforms, release the gems on RubyGems.org" + required: false + type: boolean + default: false +env: + CIBUILDGEM: 1 +jobs: + compile: + timeout-minutes: 20 + name: "Cross compile the gem on different ruby versions" + strategy: + matrix: + os: ["macos-latest", "ubuntu-22.04"] + runs-on: "${{ matrix.os }}" + steps: + - name: "Checkout code" + uses: "actions/checkout@v5" + - name: "Install autotools" + if: "${{ runner.os == 'macOS' }}" + run: brew install autoconf automake libtool + - name: "Setup Ruby" + uses: "ruby/setup-ruby@v1" + with: + ruby-version: "3.3.9" + bundler-cache: true + - name: "Run cibuildgem" + uses: "shopify/cibuildgem/.github/actions/cibuildgem@main" + with: + step: "compile" + test: + timeout-minutes: 20 + name: "Run the test suite" + needs: compile + strategy: + matrix: + os: ["macos-latest", "ubuntu-22.04"] + rubies: ["4.0", "3.4", "3.3"] + type: ["cross", "native"] + runs-on: "${{ matrix.os }}" + steps: + - name: "Checkout code" + uses: "actions/checkout@v5" + - name: "Install autotools" + if: "${{ runner.os == 'macOS' && matrix.type == 'native' }}" + run: brew install autoconf automake libtool + - name: "Setup Ruby" + uses: "ruby/setup-ruby@v1" + with: + ruby-version: "${{ matrix.rubies }}" + bundler-cache: true + - name: "Run cibuildgem" + uses: "shopify/cibuildgem/.github/actions/cibuildgem@main" + with: + step: "test_${{ matrix.type }}" + install: + timeout-minutes: 15 + name: "Verify the gem can be installed" + needs: test + strategy: + matrix: + os: ["macos-latest", "ubuntu-22.04"] + runs-on: "${{ matrix.os }}" + steps: + - name: "Install autotools" + if: "${{ runner.os == 'macOS' }}" + run: brew install autoconf automake libtool + - name: "Setup Ruby" + uses: "ruby/setup-ruby@v1" + with: + ruby-version: "4.0.0" + - name: "Run cibuildgem" + uses: "shopify/cibuildgem/.github/actions/cibuildgem@main" + with: + step: "install" + release: + environment: release + permissions: + id-token: write + contents: read + timeout-minutes: 5 + if: ${{ inputs.release }} + name: "Release all gems with RubyGems" + needs: install + runs-on: "ubuntu-latest" + steps: + - name: "Setup Ruby" + uses: "ruby/setup-ruby@v1" + with: + ruby-version: "4.0.0" + - name: "Run cibuildgem" + uses: "shopify/cibuildgem/.github/actions/cibuildgem@main" + with: + step: "release" diff --git a/.rubocop.yml b/.rubocop.yml index 28acc9b..d7ca007 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,8 +1,8 @@ AllCops: Exclude: - 'tmp/**/*' - TargetRubyVersion: 2.3 -Metrics/LineLength: + TargetRubyVersion: 3.3 +Layout/LineLength: Enabled: false Metrics/BlockLength: Enabled: false diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 145a778..0000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -sudo: false -language: ruby -rvm: - - 2.3.8 - - 2.4.6 - - 2.5.5 - - 2.6.3 -script: - - bundle install - - bundle exec rake -addons: - apt: - packages: - - libonig-dev -cache: - directories: - - /home/travis/.rvm/ diff --git a/README.md b/README.md index 4ffa01f..2d61e20 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,21 @@ Ruby bindings for jq. see [http://stedolan.github.io/jq/](http://stedolan.github.io/jq/). [![Gem Version](https://badge.fury.io/rb/ruby-jq.svg)](http://badge.fury.io/rb/ruby-jq) -[![Build Status](https://travis-ci.org/winebarrel/ruby-jq.svg?branch=master)](https://travis-ci.org/winebarrel/ruby-jq) -## Prerequisites +## Precompiled binaries -jq requires the Oniguruma library to provide regex support. To install Oniguruma -for your system, please follow the instructions in the [jq FAQ](https://github.com/stedolan/jq/wiki/FAQ#installation). +Precompiled platform gems are published for `x86_64-linux` and `arm64-darwin` +(one binary per supported minor Ruby version), so installing on those platforms +requires no compiler or build tools. They are built and released with +[cibuildgem](https://github.com/Shopify/cibuildgem) via the +`.github/workflows/cibuildgem.yaml` workflow. + +On other platforms the source gem is installed instead: it downloads and +statically compiles [OpsLevel's fork of jq](https://github.com/OpsLevel/jq) +(jq 1.6 lineage where `env`/`$ENV` always return an empty object) along with +[Onigmo](https://github.com/k-takata/Onigmo) for regex support, so the built +extension is self-contained. Building from source requires autoconf, automake, +and libtool. ## Installation diff --git a/Rakefile b/Rakefile index 51ee036..b754c81 100644 --- a/Rakefile +++ b/Rakefile @@ -9,6 +9,11 @@ RSpec::Core::RakeTask.new('spec') Rake::ExtensionTask.new('jq_core') do |ext| ext.ext_dir = 'ext' + ext.lib_dir = 'lib/jq' + ext.cross_compiling do |spec| + # Platform gems ship precompiled binaries and never build from source. + spec.dependencies.reject! { |dep| dep.name == 'mini_portile2' } + end end RuboCop::RakeTask.new do |task| diff --git a/ext/extconf.rb b/ext/extconf.rb index 35a6b2a..57c97ee 100644 --- a/ext/extconf.rb +++ b/ext/extconf.rb @@ -7,16 +7,30 @@ def using_system_libraries? end unless using_system_libraries? - message "Buildling jq using packaged libraries.\n" + message "Building jq using packaged libraries.\n" require 'rubygems' require 'mini_portile2' - recipe = MiniPortile.new('jq', '1.6') - recipe.files = ['https://github.com/stedolan/jq/archive/jq-1.6.tar.gz'] + onigmo = MiniPortile.new('onigmo', '6.2.0') + onigmo.files = ['https://github.com/k-takata/Onigmo/releases/download/Onigmo-6.2.0/onigmo-6.2.0.tar.gz'] + onigmo.configure_options = [ + '--disable-shared', + '--enable-static', + '--with-pic' + ] + onigmo.cook + + # OpsLevel fork of jq: `env`/`$ENV` always return an empty object. + recipe = MiniPortile.new('jq', '1.6.opslevel') + recipe.files = ['https://github.com/OpsLevel/jq/archive/845f4206dd8a82355325bafffbb54a5155cf67ec.tar.gz'] recipe.configure_options = [ - '--enable-shared', - '--disable-maintainer-mode' + '--disable-maintainer-mode', + '--disable-docs', + '--disable-shared', + '--enable-static', + '--with-pic', + "--with-oniguruma=#{onigmo.path}" ] class << recipe def configure @@ -26,10 +40,12 @@ def configure end recipe.cook recipe.activate - $LIBPATH = ["#{recipe.path}/lib"] | $LIBPATH # rubocop:disable Style/GlobalVars + $LIBPATH = ["#{recipe.path}/lib", "#{onigmo.path}/lib"] | $LIBPATH # rubocop:disable Style/GlobalVars $CPPFLAGS << " -I#{recipe.path}/include" # rubocop:disable Style/GlobalVars + + abort 'libonigmo not found' unless have_library('onigmo') end abort 'libjq not found' unless have_library('jq') -create_makefile('jq_core') +create_makefile('jq/jq_core') diff --git a/lib/jq.rb b/lib/jq.rb index 5bd6e18..dec0f6b 100644 --- a/lib/jq.rb +++ b/lib/jq.rb @@ -4,7 +4,14 @@ require 'tempfile' require 'stringio' -require 'jq_core' +begin + # Load the precompiled binary matching the current Ruby, if this is a + # platform-specific gem packaged by cibuildgem. + require "jq/#{RUBY_VERSION[/\d+\.\d+/]}/jq_core" +rescue LoadError + # Fall back to the extension compiled from source at install time. + require 'jq/jq_core' +end require 'jq/version' require 'jq/parser' diff --git a/lib/jq/version.rb b/lib/jq/version.rb index 6bd6bbe..06c921c 100644 --- a/lib/jq/version.rb +++ b/lib/jq/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module JQ - VERSION = '0.2.2' + VERSION = '0.3.0' end diff --git a/ruby-jq.gemspec b/ruby-jq.gemspec index e8e7582..37a575c 100644 --- a/ruby-jq.gemspec +++ b/ruby-jq.gemspec @@ -20,6 +20,8 @@ Gem::Specification.new do |spec| # spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ['lib'] + spec.required_ruby_version = '>= 3.3' + spec.add_dependency 'multi_json' spec.add_runtime_dependency 'mini_portile2', '>= 2.2.0' diff --git a/spec/jq_spec.rb b/spec/jq_spec.rb index aca42dd..71833ee 100644 --- a/spec/jq_spec.rb +++ b/spec/jq_spec.rb @@ -149,7 +149,7 @@ specify 'read from file (> 4096)' do Tempfile.open("ruby-jq.spec.#{$PROCESS_ID}") do |src| - src.puts('[' + (1..10).map { <<~JSON }.join(',') + ']') + src.puts("[#{(1..10).map { <<~JSON }.join(',')}]") { "glossary": { "title": "example glossary", @@ -260,6 +260,16 @@ expect(JQ(src).search('.menu.popup.menuitem[].value')).to eq(%w[New Open Close]) end + specify 'regex support (requires onigmo)' do + expect(JQ('["foo", "bar", "baz"]').search('.[] | select(test("^ba"))')).to eq(%w[bar baz]) + expect(JQ('"foo123bar"').search('capture("(?[0-9]+)")')).to eq([{ 'digits' => '123' }]) + end + + specify 'env is always empty (OpsLevel jq fork)' do + expect(JQ('null').search('env')).to eq([{}]) + expect(JQ('null').search('$ENV')).to eq([{}]) + end + specify 'compile error' do expect do JQ('{}').search('...') diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index df6ad54..7e85de1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -$LOAD_PATH << File.dirname(__FILE__) + '/../lib' +$LOAD_PATH << "#{File.dirname(__FILE__)}/../lib" require 'tempfile' require 'jq' require 'jq/extend'