appsignal/appsignal

View on GitHub
Rakefile

Summary

Maintainability
Test Coverage
require "bundler"
require "rubygems/package_task"
require "fileutils"

VERSION_MANAGERS = {
  :chruby => {
    :env => "#!/bin/bash\nsource /usr/local/opt/chruby/share/chruby/chruby.sh",
    :switch_command => lambda { |version| "chruby #{version}" }
  },
  :rbenv => {
    :env => "#!/bin/bash",
    :switch_command => lambda { |version| "rbenv local #{version}" }
  },
  :rvm => {
    :env => "#!/bin/bash --login",
    :switch_command => lambda { |version| "rvm use --default #{version}" }
  }
}.freeze

def env_map(key, value)
  {
    "name" => key,
    "value" => value
  }
end

def build_task(ruby_version, type = nil)
  {
    "name" => "Ruby #{ruby_version}#{type ? " - #{type}" : nil}",
    "dependencies" => ["Validation"],
    "task" => {
      "prologue" => {
        "commands" => [
          "./support/bundler_wrapper exec rake extension:install"
        ]
      },
      "jobs" => []
    }
  }
end

namespace :build_matrix do
  namespace :semaphore do
    task :generate do
      yaml = YAML.load_file("build_matrix.yml")
      matrix = yaml["matrix"]
      defaults = matrix["defaults"]
      semaphore = yaml["semaphore"]

      builds = []
      matrix["ruby"].each do |ruby|
        ruby_version = ruby["ruby"]
        ruby_primary_block = build_task(ruby_version)
        ruby_secondary_block = build_task(ruby_version, "Gems").tap do |t|
          t["dependencies"] = ["Ruby #{ruby_version}"]
        end
        gemset_for_ruby(ruby, matrix).each do |gem|
          next if excluded_for_ruby?(gem, ruby)

          env = [
            env_map("RUBY_VERSION", ruby_version),
            env_map("GEMSET", gem["gem"]),
            env_map("BUNDLE_GEMFILE", "gemfiles/#{gem["gem"]}.gemfile")
          ]
          rubygems = gem["rubygems"] || ruby["rubygems"] || defaults["rubygems"]
          env << env_map("_RUBYGEMS_VERSION", rubygems) if rubygems
          bundler = gem["bundler"] || ruby["bundler"] || defaults["bundler"]
          env << env_map("_BUNDLER_VERSION", bundler) if bundler

          job = {
            "name" => "Ruby #{ruby_version} for #{gem["gem"]}",
            "env_vars" => env,
            "commands" => ["./support/bundler_wrapper exec rake test"]
          }
          if gem["gem"] == "no_dependencies"
            ruby_primary_block["task"]["jobs"] << job
          else
            ruby_secondary_block["task"]["jobs"] << job
          end
        end
        builds << ruby_primary_block
        if ruby_secondary_block["task"]["jobs"].count.nonzero?
          builds << ruby_secondary_block
        end
      end
      semaphore["blocks"] += builds

      header = "# DO NOT EDIT\n" \
        "# This is a generated file by the `rake build_matrix:semaphore:generate` task.\n" \
        "# See `build_matrix.yml` for the build matrix.\n" \
        "# Generate this file with `rake build_matrix:semaphore:generate`.\n"
      generated_yaml = header + YAML.dump(semaphore)
      File.write(".semaphore/semaphore.yml", generated_yaml)
      puts "Generated `.semaphore/semaphore.yml`"
      puts "Task count: #{builds.length}"
      puts "Job count: #{builds.sum { |block| block["task"]["jobs"].count }}"
    end

    task :validate => :generate do
      `git status | grep .semaphore/semaphore.yml 2>&1`
      if $?.exitstatus.zero? # rubocop:disable Style/SpecialGlobalVars
        puts "The `.semaphore/semaphore.yml` is modified. The changes were not committed."
        puts "Please run `rake build_matrix:semaphore:generate` and commit the changes."
        exit 1
      end
    end
  end

  namespace :local do
    task :generate do
      yaml = YAML.load_file("build_matrix.yml")
      matrix = yaml["matrix"]
      defaults = matrix["defaults"]

      VERSION_MANAGERS.each do |version_manager, config|
        out = []
        out << config[:env]
        out << "rm -f .ruby-version"
        out << "echo 'Using #{version_manager}'"
        matrix["ruby"].each do |ruby|
          ruby_version = ruby["ruby"]
          out << "echo 'Switching to #{ruby_version}'"
          out << "#{config[:switch_command].call(ruby_version)} || { echo 'Switching Ruby failed'; exit 1; }"
          out << "ruby -v"
          out << "echo 'Compiling extension'"
          out << "./support/bundler_wrapper exec rake extension:install"
          out << "rm -f gemfiles/*.gemfile.lock"
          gemset_for_ruby(ruby, matrix).each do |gem|
            next if excluded_for_ruby?(gem, ruby)
            gemfile = gem["gem"]
            out << "echo 'Bundling #{gemfile} in #{ruby_version}'"
            rubygems = gem["rubygems"] || ruby["rubygems"] || defaults["rubygems"]
            rubygems_version = "env _RUBYGEMS_VERSION=#{rubygems_version}" if rubygems
            bundler = gem["bundler"] || ruby["bundler"] || defaults["bundler"]
            bundler_version = "env _BUNDLER_VERSION=#{bundler}" if bundler
            gemfile_env = "env BUNDLE_GEMFILE=gemfiles/#{gemfile}.gemfile"
            out << "#{bundler_version} #{rubygems_version} ./support/install_deps"
            out << "#{bundler_version} #{gemfile_env} ./support/bundler_wrapper install --quiet || { echo 'Bundling failed'; exit 1; }"
            out << "echo 'Running #{gemfile} in #{ruby_version}'"
            out << "#{bundler_version} #{gemfile_env} ./support/bundler_wrapper exec rspec || { echo 'Running specs failed'; exit 1; }"
          end
          out << ""
        end
        out << "rm -f .ruby-version"
        out << "echo 'Successfully ran specs for all environments'"

        script = "bundle_and_spec_all_#{version_manager}"
        FileUtils.rm_f(script)
        File.open(script, "w") do |file|
          file.write out.join("\n")
        end
        File.chmod(0o775, script)
        puts "Generated #{script}"
      end
    end
  end

  def gemset_for_ruby(ruby, matrix)
    gems = matrix["gems"]
    if ruby["gems"]
      # Only a specific gemset for this Ruby
      selected_gems = matrix["gemsets"].fetch(ruby["gems"])
      gems.select { |g| selected_gems.include?(g["gem"]) }
    else
      # All gems for this Ruby
      gems
    end
  end

  def excluded_for_ruby?(gem, ruby)
    (gem.dig("exclude", "ruby") || []).include?(ruby["ruby"])
  end
end

namespace :build do
  def base_gemspec
    eval(File.read("appsignal.gemspec")) # rubocop:disable Security/Eval
  end

  def modify_base_gemspec
    base_gemspec.tap do |s|
      yield s
    end
  end

  def define_build_task(task_name, base_gemspec, &block)
    Gem::PackageTask.new(base_gemspec, &block)
  rescue StandardError => error
    puts "Warning: An error occurred defining `build:#{task_name}:gem` Rake task."
    puts "This task will not be availble."
    if ENV["DEBUG"]
      puts "#{error}: #{error.message}"
      puts error.backtrace
    else
      puts "For more information, run the same command with `DEBUG=true`."
    end
    puts
  end

  namespace :ruby do
    # Extension default set in `appsignal.gemspec`
    define_build_task(:ruby, base_gemspec) { |_pkg| }
  end

  namespace :jruby do
    spec = modify_base_gemspec do |s|
      s.platform = "java"
      # Override extensions config with JRuby extension installer
      # Default set in `appsignal.gemspec`
      s.extensions = %w[ext/Rakefile]
      s.add_dependency "ffi"
    end

    define_build_task(:jruby, spec) { |_pkg| }
  end

  desc "Build all gem versions"
  task :all => ["ruby:gem", "jruby:gem"]

  desc "Clean up all gem build artifacts"
  task :clean do
    FileUtils.rm_rf File.expand_path("../pkg", __FILE__)
  end
end

namespace :publish do
  VERSION_FILE = "lib/appsignal/version.rb".freeze
  CHANGELOG_FILE = "CHANGELOG.md".freeze

  def changes
    git_status_to_array(`git status -s -u`)
  end

  def git_status_to_array(changes)
    changes.split("\n").each { |change| change.gsub!(/^.. /, "") }
  end

  def current_branch
    `git rev-parse --abbrev-ref HEAD`.chomp
  end

  task :check_requirements do
    unless changes.empty?
      puts "ERROR: There should be no uncommitted file changes."
      exit 1
    end
    unless ENV["EDITOR"]
      puts "ERROR: $EDITOR environment variable should be set."
      exit 1
    end
  end

  task :configure_version do
    puts "\n# Configuring new gem version"

    system "$EDITOR #{VERSION_FILE}"
    unless changes.member?(VERSION_FILE)
      puts "ERROR: Please actually change the gem version in: #{VERSION_FILE}"
      exit 1
    end

    puts "\n# Updating the changelog"
    system "$EDITOR #{CHANGELOG_FILE}"
  end

  task :push_gem_packages do
    puts "\n# Pushing gem packages"
    Dir.chdir("#{File.dirname(__FILE__)}/pkg") do
      Dir["*.gem"].each do |gem_package|
        puts "## Publishing gem package: #{gem_package}"
        system "gem push #{gem_package}"
      end
    end
  end

  task :tag_and_push_version do
    # Make sure to load the new version number
    Appsignal.send(:remove_const, :VERSION)
    load File.expand_path(VERSION_FILE)
    version = "v#{Appsignal::VERSION}"

    begin
      puts `git commit -am 'Bump to #{version} [ci skip]'`
      puts "# Creating tag #{version}"
      puts `git tag #{version}`
      puts `git push origin #{version}`
      puts `git push origin #{current_branch}`
    rescue
      puts "ERROR: Tag '#{version}' already exists"
      exit 1
    end
  end

  task :build => "build:clean" do
    # Shell out to build so the new version is loaded in the gemspec.
    `rake build:all`
  end
end
task :publish => [
  "publish:check_requirements",
  "publish:configure_version",
  "publish:build",
  "publish:push_gem_packages",
  "publish:tag_and_push_version"
]

desc "Install the AppSignal gem, extension and all possible dependencies."
task :install => "extension:install" do
  Bundler.with_clean_env do
    GEMFILES.each do |gemfile|
      system "bundle --gemfile gemfiles/#{gemfile}.gemfile"
    end
  end
end

task :spec_all_gemfiles do
  GEMFILES.each do |gemfile|
    puts "Running tests for #{gemfile}"
    unless system("env BUNDLE_GEMFILE=gemfiles/#{gemfile}.gemfile bundle exec rspec")
      raise "Not successful"
    end
  end
end

task :console do
  require "irb"
  require "irb/completion"
  require "appsignal"

  Appsignal.config = Appsignal::Config.new(".", :console)

  ARGV.clear
  IRB.start
end

namespace :extension do
  desc "Install the AppSignal gem extension"
  task :install => :clean do
    if RUBY_PLATFORM == "java"
      system "cd ext && rake"
    else
      system "cd ext && ruby extconf.rb && make clean && make"
    end
  end

  desc "Clean the AppSignal gem extension directory of installation artifacts"
  task :clean do
    system <<-COMMAND
      cd ext &&
        rm -f appsignal.bundle \
          appsignal-agent \
          appsignal.h \
          appsignal_extension.o \
          appsignal_extension.so \
          appsignal_extension.bundle \
          install.report \
          libappsignal.* \
          appsignal.version \
          Makefile \
          mkmf.log
    COMMAND
  end
end

begin
  require "rspec/core/rake_task"
  desc "Run the AppSignal gem test suite."
  RSpec::Core::RakeTask.new :test do |t|
    is_jruby = defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
    unless is_jruby
      t.rspec_opts = "--exclude-pattern=spec/lib/appsignal/extension/jruby_spec.rb"
    end
  end
rescue LoadError # rubocop:disable Lint/HandleExceptions
  # When running rake install, there is no RSpec yet.
end

task :default => [:generate_bundle_and_spec_all, :spec_all_gemfiles]