├── config └── cdn.yml ├── vendor ├── jce │ ├── local_policy.jar │ └── US_export_policy.jar ├── dotenv.rb ├── syck_hack.rb └── lpxc.rb ├── .gitignore ├── bin ├── release ├── detect └── compile ├── lib ├── language_pack │ ├── version.rb │ ├── no_lockfile.rb │ ├── disable_deploys.rb │ ├── metadata.rb │ ├── helpers │ │ ├── stale_file_cleaner.rb │ │ ├── plugin_installer.rb │ │ ├── node_installer.rb │ │ ├── bundler_cache.rb │ │ ├── rake_runner.rb │ │ └── bundler_wrapper.rb │ ├── rack.rb │ ├── rails41.rb │ ├── fetcher.rb │ ├── cache.rb │ ├── instrument.rb │ ├── rails2.rb │ ├── ruby_version.rb │ ├── rails4.rb │ ├── rails3.rb │ ├── shell_helpers.rb │ ├── base.rb │ └── ruby.rb └── language_pack.rb ├── README.md ├── spec ├── rails23_spec.rb ├── default_cache_spec.rb ├── no_lockfile_spec.rb ├── multibuildpack_spec.rb ├── helpers │ ├── fetcher_spec.rb │ ├── bundler_wrapper_spec.rb │ ├── shell_spec.rb │ ├── stale_file_cleaner_spec.rb │ ├── rake_runner_spec.rb │ └── ruby_version_spec.rb ├── rack_spec.rb ├── gem_detect_errors_spec.rb ├── upgrade_ruby_spec.rb ├── stack_spec.rb ├── user_env_compile_edge_case_spec.rb ├── bugs_spec.rb ├── rails41_spec.rb ├── ruby_spec.rb ├── spec_helper.rb ├── rails3_spec.rb ├── rails4_spec.rb └── rubies_spec.rb ├── Gemfile ├── .travis.yml ├── LICENSE ├── hatchet.json ├── Gemfile.lock ├── support └── s3 │ ├── hmac │ └── s3 ├── Rakefile └── CHANGELOG.md /config/cdn.yml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /vendor/jce/local_policy.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/travis-pro/heroku-buildpack-ruby/HEAD/vendor/jce/local_policy.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | repos/* 2 | .DS_Store 3 | vendor/bundler/* 4 | vendor/bundle/* 5 | .env 6 | .ruby-version 7 | buildpacks/* 8 | .anvil/ 9 | -------------------------------------------------------------------------------- /vendor/jce/US_export_policy.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/travis-pro/heroku-buildpack-ruby/HEAD/vendor/jce/US_export_policy.jar -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'pathname' 4 | 5 | release = Pathname.new(ARGV.first).join("tmp/heroku-buildpack-release-step.yml") 6 | puts release.read if release.exist? 7 | -------------------------------------------------------------------------------- /bin/detect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'pathname' 4 | 5 | if Pathname.new(ARGV.first).join("Gemfile").exist? 6 | puts "Ruby" 7 | exit 0 8 | else 9 | puts "no" 10 | exit 1 11 | end 12 | -------------------------------------------------------------------------------- /lib/language_pack/version.rb: -------------------------------------------------------------------------------- 1 | require "language_pack/base" 2 | 3 | # This file is automatically generated by rake 4 | module LanguagePack 5 | class LanguagePack::Base 6 | BUILDPACK_VERSION = "v127" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DO NOT USE 2 | 3 | We shoul not be using this buildpack any more. If you find an app using it, be sure to 4 | add https://github.com/travis-ci/unlimited-jce-policy-jdk7 to `Gemfile`, remove 5 | `$BUILDPACK_URL` from the Heroku app, and deploy. 6 | -------------------------------------------------------------------------------- /vendor/dotenv.rb: -------------------------------------------------------------------------------- 1 | # imports contents of .env file into ENV 2 | file = File.expand_path("../../.env", __FILE__) 3 | if File.exists?(file) 4 | File.read(file).split("\n").map {|x| x.split("=") }.each do |k,v| 5 | ENV[k.strip] = v.strip 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/rails23_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | describe "Rails 2.3.x" do 4 | it "should deploy on ruby 1.8.7" do 5 | Hatchet::Runner.new("rails23_mri_187").deploy do |app, heroku| 6 | add_database(app, heroku) 7 | expect(successful_body(app)).to eq("hello") 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | group :development, :test do 4 | gem "heroku_hatchet" 5 | gem "rspec-core" 6 | gem "rspec-expectations" 7 | gem "excon" 8 | gem "rake" 9 | gem "parallel_tests" 10 | gem 'rspec-retry' 11 | gem "netrc" 12 | gem "git", github: "hone/ruby-git", branch: "master" 13 | end 14 | -------------------------------------------------------------------------------- /spec/default_cache_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Default Cache" do 4 | it "gets loaded successfully" do 5 | pending("needs dep-tracker work") 6 | Hatchet::Runner.new("default_ruby").deploy do |app| 7 | expect(app.output).to match("loading default bundler cache") 8 | end 9 | end 10 | end 11 | 12 | -------------------------------------------------------------------------------- /spec/no_lockfile_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "No Lockfile" do 4 | it "should not deploy" do 5 | Hatchet::Runner.new("no_lockfile", allow_failure: true).deploy do |app| 6 | expect(app).not_to be_deployed 7 | expect(app.output).to include("Gemfile.lock required") 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/language_pack/no_lockfile.rb: -------------------------------------------------------------------------------- 1 | require "language_pack" 2 | require "language_pack/base" 3 | 4 | class LanguagePack::NoLockfile < LanguagePack::Base 5 | def self.use? 6 | !File.exists?("Gemfile.lock") 7 | end 8 | 9 | def name 10 | "Ruby/NoLockfile" 11 | end 12 | 13 | def compile 14 | error "Gemfile.lock required. Please check it in." 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/multibuildpack_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Multibuildpack" do 4 | it "works with node" do 5 | Hatchet::Runner.new("node_multi", buildpack_url: "https://github.com/ddollar/heroku-buildpack-multi.git").deploy do |app| 6 | puts app.output 7 | expect(app.output).to match("Node Version in Ruby buildpack is: v0.10.3") 8 | end 9 | end 10 | end 11 | 12 | -------------------------------------------------------------------------------- /spec/helpers/fetcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Fetches" do 4 | 5 | it "bundler" do 6 | Dir.mktmpdir do |dir| 7 | Dir.chdir(dir) do 8 | fetcher = LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL) 9 | fetcher.fetch_untar("#{LanguagePack::Ruby::BUNDLER_GEM_PATH}.tgz") 10 | expect(`ls bin`).to match("bundle") 11 | end 12 | end 13 | end 14 | end 15 | 16 | -------------------------------------------------------------------------------- /spec/rack_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | describe "Rack" do 4 | it "should not overwrite already set environment variables" do 5 | custom_env = "FFFUUUUUUU" 6 | app = Hatchet::Runner.new("default_ruby") 7 | app.setup! 8 | app.set_config("RACK_ENV" => custom_env) 9 | expect(app.run("env")).to match(custom_env) 10 | 11 | app.deploy do |app| 12 | expect(app.run("env")).to match(custom_env) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/gem_detect_errors_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Raise errors on specific gems" do 4 | it "should should raise on sqlite3" do 5 | Hatchet::Runner.new("sqlite3_gemfile", allow_failure: true).deploy do |app| 6 | expect(app).not_to be_deployed 7 | expect(app.output).to include("Detected sqlite3 gem which is not supported") 8 | expect(app.output).to include("devcenter.heroku.com/articles/sqlite3") 9 | end 10 | end 11 | end 12 | 13 | -------------------------------------------------------------------------------- /bin/compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # sync output 4 | $stdout.sync = true 5 | 6 | $:.unshift File.expand_path("../../lib", __FILE__) 7 | require "language_pack" 8 | require "language_pack/shell_helpers" 9 | 10 | LanguagePack::Instrument.trace 'compile', 'app.compile' do 11 | if pack = LanguagePack.detect(ARGV[0], ARGV[1]) 12 | LanguagePack::ShellHelpers.initialize_env(ARGV[2]) 13 | pack.topic("Compiling #{pack.name}") 14 | pack.log("compile") do 15 | pack.compile 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/language_pack/disable_deploys.rb: -------------------------------------------------------------------------------- 1 | require "language_pack" 2 | require "language_pack/base" 3 | 4 | class LanguagePack::DisableDeploys < LanguagePack::Base 5 | def self.use? 6 | File.exist?("Gemfile") 7 | end 8 | 9 | def name 10 | "Ruby/DisableDeploys" 11 | end 12 | 13 | def compile 14 | error "Ruby deploys have been temporarily disabled due to a Rubygems.org security breach.\nPlease see https://status.heroku.com/incidents/489 for more info and a workaround if you need to deploy." 15 | end 16 | end 17 | 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: ruby 3 | rvm: 4 | - 2.0.0 5 | 6 | before_script: bundle exec rake hatchet:setup_travis 7 | 8 | # Run tests in parallel 9 | script: bundle exec parallel_rspec -n 11 spec/ 10 | 11 | after_script: 12 | - heroku keys:remove $USER@`hostname` 13 | 14 | env: 15 | global: 16 | - HATCHET_RETRIES=3 17 | - IS_RUNNING_ON_TRAVIS=true 18 | - HATCHET_DEPLOY_STRATEGY=git 19 | - HATCHET_APP_LIMIT=80 20 | # sets the HEROKU_API_KEY to a dummy user for Heroku deploys 21 | - secure: QvDqQQV/Gtk1Og5s8879i+mYLdK6WtVkZMKlCWvJrztYwcRMOGsVVbXjq5EkEJCfxs4GWv7KqXfPPPLDS5Wumzkt4zXVh3ZS+rzQZ2IYC0XbKZYt2e14ZpSTgDUY20J0Ex/GG5dTulJaG9FBe452UDiqYrianE4p8h8w18JBfCs= 22 | -------------------------------------------------------------------------------- /spec/upgrade_ruby_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Upgrading ruby apps" do 4 | it "upgrades from 2.0.0 to 2.1.0" do 5 | Hatchet::Runner.new("mri_200").deploy do |app| 6 | expect(app.run("ruby -v")).to match("2.0.0") 7 | 8 | `echo "" > Gemfile; echo "" > Gemfile.lock` 9 | puts `env BUNDLE_GEMFILE=./Gemfile bundle install`.inspect 10 | `echo "ruby '2.1.0'" > Gemfile` 11 | `git add -A; git commit -m update-ruby` 12 | app.push! 13 | expect(app.output).to match("2.1.0") 14 | expect(app.run("ruby -v")).to match("2.1.0") 15 | expect(app.output).to match("Ruby version change detected") 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/helpers/bundler_wrapper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "BundlerWrapper" do 4 | 5 | before(:each) do 6 | if ENV['RUBYOPT'] 7 | @original_rubyopt = ENV['RUBYOPT'] 8 | ENV['RUBYOPT'] = ENV['RUBYOPT'].sub('-rbundler/setup', '') 9 | end 10 | 11 | @bundler = LanguagePack::Helpers::BundlerWrapper.new 12 | end 13 | 14 | after(:each) do 15 | if ENV['RUBYOPT'] 16 | ENV['RUBYOPT'] = @original_rubyopt 17 | end 18 | 19 | @bundler.clean 20 | end 21 | 22 | it "detects windows gemfiles" do 23 | Hatchet::App.new("rails4_windows_mri193").in_directory do |dir| 24 | expect(@bundler.install.windows_gemfile_lock?).to be_true 25 | end 26 | end 27 | end 28 | 29 | -------------------------------------------------------------------------------- /lib/language_pack/metadata.rb: -------------------------------------------------------------------------------- 1 | require "language_pack" 2 | require "language_pack/base" 3 | 4 | class LanguagePack::Metadata 5 | FOLDER = "vendor/heroku" 6 | 7 | def initialize(cache) 8 | if cache 9 | @cache = cache 10 | @cache.load FOLDER 11 | end 12 | end 13 | 14 | def read(key) 15 | full_key = "#{FOLDER}/#{key}" 16 | File.read(full_key) if exists?(key) 17 | end 18 | 19 | def exists?(key) 20 | full_key = "#{FOLDER}/#{key}" 21 | File.exists?(full_key) && !Dir.exists?(full_key) 22 | end 23 | 24 | def write(key, value, isave = true) 25 | FileUtils.mkdir_p(FOLDER) 26 | 27 | full_key = "#{FOLDER}/#{key}" 28 | File.open(full_key, 'w') {|f| f.puts value } 29 | save if isave 30 | end 31 | 32 | def save 33 | @cache ? @cache.store(FOLDER) : false 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/stack_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "spec_helper" 2 | 3 | describe "Stack Changes" do 4 | it "should reinstall gems on stack change" do 5 | Hatchet::Runner.new("mri_200").deploy do |app, heroku| 6 | heroku.put_stack(app.name, "cedar-14") 7 | `git commit --allow-empty -m "cedar-14 migrate"` 8 | 9 | app.push! 10 | puts app.output 11 | expect(app.output).to match("Installing rack 1.5.0") 12 | expect(app.output).to match("Changing stack") 13 | end 14 | end 15 | 16 | it "should not reinstall gems if the stack did not change" do 17 | Hatchet::Runner.new("mri_200").deploy do |app, heroku| 18 | heroku.put_stack(app.name, "cedar") 19 | `git commit --allow-empty -m "cedar migrate"` 20 | 21 | app.push! 22 | puts app.output 23 | expect(app.output).to match("Using rack 1.5.0") 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/helpers/shell_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | 4 | class FakeShell 5 | include LanguagePack::ShellHelpers 6 | end 7 | 8 | 9 | describe "ShellHelpers" do 10 | it "format ugly keys correctly" do 11 | env = {%Q{ un"matched } => "bad key"} 12 | result = FakeShell.new.command_options_to_string("bundle install", env: env) 13 | expected = %r{env \\ un\\\"matched\\ =bad\\ key bash -c bundle\\ install 2>&1} 14 | expect(result.strip).to match(expected) 15 | end 16 | 17 | it "formats ugly values correctly" do 18 | env = {"BAD VALUE" => %Q{ )(*&^%$#'$'\n''@!~\'\ }} 19 | result = FakeShell.new.command_options_to_string("bundle install", env: env) 20 | expected = %r{env BAD\\ VALUE=\\ \\\)\\\(\\\*\\&\\\^\\%\\\$\\#\\'\\\$\\''\n'\\'\\'@\\!\\~\\'\\ bash -c bundle\\ install 2>&1} 21 | expect(result.strip).to match(expected) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/language_pack/helpers/stale_file_cleaner.rb: -------------------------------------------------------------------------------- 1 | class LanguagePack::Helpers::StaleFileCleaner 2 | FILE_STAT_CACHE = Hash.new {|h, k| h[k] = File.stat(k) } 3 | 4 | def initialize(dir) 5 | @dir = dir 6 | raise "need or dir" if @dir.nil? 7 | end 8 | 9 | def clean_over(limit) # limit in bytes 10 | old_files_over(limit).each {|asset| FileUtils.rm(asset) } 11 | end 12 | 13 | def glob 14 | "#{@dir}/**/*" 15 | end 16 | 17 | def files 18 | @files ||= Dir[glob].reject {|file| File.directory?(file) } 19 | end 20 | 21 | def sorted_files 22 | @sorted ||= files.sort_by {|a| FILE_STAT_CACHE[a].mtime } 23 | end 24 | 25 | def total_size 26 | @size ||= sorted_files.inject(0) {|sum, asset| sum += FILE_STAT_CACHE[asset].size } 27 | end 28 | 29 | 30 | def old_files_over(limit) 31 | diff = total_size - limit 32 | sorted_files.take_while {|asset| diff -= FILE_STAT_CACHE[asset].size if diff > 0 } || [] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/user_env_compile_edge_case_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "User env compile" do 4 | it "should not cause problems with warnings" do 5 | app = Hatchet::Runner.new("mri_210") 6 | app.setup! 7 | app.set_config("RUBY_HEAP_MIN_SLOTS" => "1000000") 8 | app.deploy do |app| 9 | expect(app.run("bundle version")).to match(LanguagePack::Ruby::BUNDLER_VERSION) 10 | end 11 | end 12 | 13 | it "DATABASE_URL is present even without user-env-compile" do 14 | Hatchet::Runner.new("database_url_expected_in_rakefile").deploy do |app| 15 | expect(app.output).to match("Asset precompilation completed") 16 | end 17 | end 18 | 19 | it "allows weird characters in the env" do 20 | app = Hatchet::Runner.new("rails41_scaffold") 21 | app.setup! 22 | app.set_config("BAD VALUE" => %Q{ )(*&^%$#'$'\n''@!~\'\ }) 23 | app.set_config(%Q{ un"matched } => "bad key" ) 24 | app.deploy do |app| 25 | expect(app.output).to match("Launching") 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/bugs_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | describe "Bugs" do 4 | context "MRI 1.8.7" do 5 | it "should install nokogiri" do 6 | Hatchet::Runner.new("mri_187_nokogiri").deploy do |app| 7 | expect(app.output).to match("Installing nokogiri") 8 | expect(app.output).to match("Your bundle is complete!") 9 | end 10 | end 11 | end 12 | 13 | it "nokogiri should use the system libxml2" do 14 | Hatchet::Runner.new("nokogiri_160").deploy do |app| 15 | expect(app.output).to match("nokogiri") 16 | expect(app.run("bundle exec nokogiri -v")).not_to include("WARNING: Nokogiri was built against LibXML version") 17 | end 18 | end 19 | 20 | context "database connections" do 21 | it "fails with better error message" do 22 | Hatchet::Runner.new("connect_to_database_on_first_push", allow_failure: true).deploy do |app| 23 | expect(app.output).to match("https://devcenter.heroku.com/articles/pre-provision-database") 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License: 2 | 3 | Copyright (C) 2012 Heroku, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /lib/language_pack/helpers/plugin_installer.rb: -------------------------------------------------------------------------------- 1 | require "language_pack/shell_helpers" 2 | 3 | module LanguagePack 4 | module Helpers 5 | # Takes an array of plugin names and vendor_url 6 | # fetches plugins from url, installs them 7 | class PluginsInstaller 8 | attr_accessor :plugins, :vendor_url 9 | include LanguagePack::ShellHelpers 10 | 11 | def initialize(plugins, vendor_url = LanguagePack::Base::VENDOR_URL) 12 | @plugins = plugins || [] 13 | @vendor_url = vendor_url 14 | end 15 | 16 | # vendors all the plugins into the slug 17 | def install 18 | return true unless plugins.any? 19 | plugins.each { |plugin| vendor(plugin) } 20 | end 21 | 22 | def plugin_dir(name = "") 23 | Pathname.new("vendor/plugins").join(name) 24 | end 25 | 26 | # vendors an individual plugin 27 | # @param [String] name of the plugin 28 | def vendor(name) 29 | directory = plugin_dir(name) 30 | return true if directory.exist? 31 | directory.mkpath 32 | Dir.chdir(directory) do |dir| 33 | run("curl #{vendor_url}/#{name}.tgz -s -o - | tar xzf -") 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/rails41_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | describe "Rails 4.1.x" do 4 | it "should detect rails successfully" do 5 | Hatchet::App.new('rails41_scaffold').in_directory do 6 | expect(LanguagePack::Rails41.use?).to eq(true) 7 | end 8 | Hatchet::App.new('rails41_scaffold').in_directory do 9 | expect(LanguagePack::Rails4.use?).to eq(false) 10 | end 11 | end 12 | 13 | it "should be able to run a migration without heroku specific database.yml" do 14 | Hatchet::Runner.new("rails41_scaffold").deploy do |app, heroku| 15 | add_database(app, heroku) 16 | expect(app.output).not_to include("Writing config/database.yml to read from DATABASE_URL") 17 | expect(app.run("rake db:migrate")).to include("20140218165801 CreatePeople") 18 | end 19 | end 20 | 21 | it "should handle secrets.yml properly" do 22 | Hatchet::Runner.new("rails41_scaffold").deploy do |app, heroku| 23 | add_database(app, heroku) 24 | ReplRunner.new(:rails_console, "heroku run bin/rails console -a #{app.name}").run do |console| 25 | console.run("ENV['SECRET_KEY_BASE'] == Rails.application.config.secrets.secret_key_base") {|result| expect(result).not_to eq("true") } 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/language_pack/rack.rb: -------------------------------------------------------------------------------- 1 | require "language_pack" 2 | require "language_pack/ruby" 3 | 4 | # Rack Language Pack. This is for any non-Rails Rack apps like Sinatra. 5 | class LanguagePack::Rack < LanguagePack::Ruby 6 | 7 | # detects if this is a valid Rack app by seeing if "config.ru" exists 8 | # @return [Boolean] true if it's a Rack app 9 | def self.use? 10 | instrument "rack.use" do 11 | bundler.gem_version('rack') 12 | end 13 | end 14 | 15 | def name 16 | "Ruby/Rack" 17 | end 18 | 19 | def default_config_vars 20 | instrument "rack.default_config_vars" do 21 | super.merge({ 22 | "RACK_ENV" => env("RACK_ENV") || "production" 23 | }) 24 | end 25 | end 26 | 27 | def default_process_types 28 | instrument "rack.default_process_types" do 29 | # let's special case thin here if we detect it 30 | web_process = bundler.has_gem?("thin") ? 31 | "bundle exec thin start -R config.ru -e $RACK_ENV -p $PORT" : 32 | "bundle exec rackup config.ru -p $PORT" 33 | 34 | super.merge({ 35 | "web" => web_process 36 | }) 37 | end 38 | end 39 | 40 | private 41 | 42 | # sets up the profile.d script for this buildpack 43 | def setup_profiled 44 | super 45 | set_env_default "RACK_ENV", "production" 46 | end 47 | 48 | end 49 | 50 | -------------------------------------------------------------------------------- /spec/helpers/stale_file_cleaner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Cleans Stale Files" do 4 | 5 | it "removes files if they go over the limit" do 6 | file_size = 1000 7 | Dir.mktmpdir do |dir| 8 | old_file = create_file_with_size_in(file_size, dir) 9 | sleep 1 # need mtime of files to be different 10 | new_file = create_file_with_size_in(file_size, dir) 11 | 12 | expect(old_file.exist?).to be_true 13 | expect(new_file.exist?).to be_true 14 | 15 | ::LanguagePack::Helpers::StaleFileCleaner.new(dir).clean_over(2*file_size - 50) 16 | 17 | expect(old_file.exist?).to be_false 18 | expect(new_file.exist?).to be_true 19 | end 20 | end 21 | 22 | it "leaves files if they are under the limit" do 23 | file_size = 1000 24 | Dir.mktmpdir do |dir| 25 | old_file = create_file_with_size_in(file_size, dir) 26 | sleep 1 # need mtime of files to be different 27 | new_file = create_file_with_size_in(file_size, dir) 28 | 29 | expect(old_file.exist?).to be_true 30 | expect(new_file.exist?).to be_true 31 | dir_size = File.stat(dir) 32 | 33 | ::LanguagePack::Helpers::StaleFileCleaner.new(dir).clean_over(2*file_size + 50) 34 | 35 | expect(old_file.exist?).to be_true 36 | expect(new_file.exist?).to be_true 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/language_pack/helpers/node_installer.rb: -------------------------------------------------------------------------------- 1 | class LanguagePack::NodeInstaller 2 | MODERN_NODE_VERSION = "0.10.30" 3 | MODERN_BINARY_PATH = "node-v#{MODERN_NODE_VERSION}-linux-x64" 4 | 5 | LEGACY_NODE_VERSION = "0.4.7" 6 | LEGACY_BINARY_PATH = "node-#{LEGACY_NODE_VERSION}" 7 | 8 | NODEJS_BASE_URL = "http://nodejs.org/dist/v#{MODERN_NODE_VERSION}/" 9 | 10 | def initialize(stack) 11 | @fetchers = { 12 | modern: LanguagePack::Fetcher.new(NODEJS_BASE_URL), 13 | legacy: LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL, LanguagePack::Base::DEFAULT_LEGACY_STACK) 14 | } 15 | @legacy = stack == LanguagePack::Base::DEFAULT_LEGACY_STACK 16 | end 17 | 18 | def version 19 | if @legacy 20 | LEGACY_NODE_VERSION 21 | else 22 | MODERN_NODE_VERSION 23 | end 24 | end 25 | 26 | def binary_path 27 | if @legacy 28 | LEGACY_BINARY_PATH 29 | else 30 | MODERN_BINARY_PATH 31 | end 32 | end 33 | 34 | def install 35 | if @legacy 36 | @fetchers[:legacy].fetch_untar("#{LEGACY_BINARY_PATH}.tgz") 37 | else 38 | node_bin = "#{MODERN_BINARY_PATH}/bin/node" 39 | @fetchers[:modern].fetch_untar("#{MODERN_BINARY_PATH}.tar.gz", "#{MODERN_BINARY_PATH}/bin/node") 40 | FileUtils.mv(node_bin, ".") 41 | FileUtils.rm_rf(MODERN_BINARY_PATH) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/language_pack/rails41.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | require "language_pack" 3 | require "language_pack/rails4" 4 | 5 | class LanguagePack::Rails41 < LanguagePack::Rails4 6 | # detects if this is a Rails 4.x app 7 | # @return [Boolean] true if it's a Rails 4.x app 8 | def self.use? 9 | instrument "rails4.use" do 10 | rails_version = bundler.gem_version('railties') 11 | return false unless rails_version 12 | is_rails4 = rails_version >= Gem::Version.new('4.1.0.beta1') && 13 | rails_version < Gem::Version.new('5.0.0') 14 | return is_rails4 15 | end 16 | end 17 | 18 | def create_database_yml 19 | instrument 'ruby.create_database_yml' do 20 | end 21 | end 22 | 23 | def setup_profiled 24 | instrument 'setup_profiled' do 25 | super 26 | set_env_default "SECRET_KEY_BASE", app_secret 27 | end 28 | end 29 | 30 | def default_config_vars 31 | super.merge({ 32 | "SECRET_KEY_BASE" => env("SECRET_KEY_BASE") || app_secret 33 | }) 34 | end 35 | 36 | private 37 | def app_secret 38 | key = "secret_key_base" 39 | 40 | @app_secret ||= begin 41 | if @metadata.exists?(key) 42 | @metadata.read(key).chomp 43 | else 44 | secret = SecureRandom.hex(64) 45 | @metadata.write(key, secret) 46 | 47 | secret 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/language_pack.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | require 'benchmark' 3 | 4 | # General Language Pack module 5 | module LanguagePack 6 | module Helpers 7 | end 8 | 9 | # detects which language pack to use 10 | # @param [Array] first argument is a String of the build directory 11 | # @return [LanguagePack] the {LanguagePack} detected 12 | def self.detect(*args) 13 | Instrument.instrument 'detect' do 14 | Dir.chdir(args.first) 15 | 16 | pack = [ NoLockfile, Rails41, Rails4, Rails3, Rails2, Rack, Ruby ].detect do |klass| 17 | klass.use? 18 | end 19 | 20 | return pack ? pack.new(*args) : nil 21 | end 22 | end 23 | 24 | end 25 | 26 | 27 | $:.unshift File.expand_path("../../vendor", __FILE__) 28 | $:.unshift File.expand_path("..", __FILE__) 29 | 30 | require 'dotenv' 31 | require 'language_pack/shell_helpers' 32 | require 'language_pack/instrument' 33 | require "language_pack/helpers/plugin_installer" 34 | require "language_pack/helpers/stale_file_cleaner" 35 | require "language_pack/helpers/rake_runner" 36 | require "language_pack/helpers/bundler_wrapper" 37 | 38 | require "language_pack/ruby" 39 | require "language_pack/rack" 40 | require "language_pack/rails2" 41 | require "language_pack/rails3" 42 | require "language_pack/disable_deploys" 43 | require "language_pack/rails4" 44 | require "language_pack/rails41" 45 | require "language_pack/no_lockfile" 46 | -------------------------------------------------------------------------------- /spec/ruby_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | describe "Ruby apps" do 4 | describe "Rake detection" do 5 | context "default" do 6 | it "adds default process types" do 7 | Hatchet::Runner.new('empty-procfile').deploy do |app| 8 | app.run("console") do |console| 9 | console.run("'hello' + 'world'") {|result| expect(result).to match('helloworld')} 10 | end 11 | end 12 | end 13 | end 14 | 15 | context "Ruby 1.8.7" do 16 | it "doesn't run rake tasks if no rake gem" do 17 | Hatchet::Runner.new('mri_187_no_rake').deploy do |app, heroku| 18 | expect(app.output).not_to include("foo") 19 | end 20 | end 21 | 22 | it "runs a rake task if the gem exists" do 23 | Hatchet::Runner.new('mri_187_rake').deploy do |app, heroku| 24 | expect(app.output).to include("foo") 25 | end 26 | end 27 | end 28 | 29 | context "Ruby 1.9+" do 30 | it "runs rake tasks if no rake gem" do 31 | Hatchet::Runner.new('mri_200_no_rake').deploy do |app, heroku| 32 | expect(app.output).to include("foo") 33 | end 34 | end 35 | 36 | it "runs a rake task if the gem exists" do 37 | Hatchet::Runner.new('mri_200_rake').deploy do |app, heroku| 38 | expect(app.output).to include("foo") 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/language_pack/fetcher.rb: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | require "language_pack/shell_helpers" 3 | 4 | module LanguagePack 5 | class Fetcher 6 | include ShellHelpers 7 | CDN_YAML_FILE = File.expand_path("../../../config/cdn.yml", __FILE__) 8 | 9 | def initialize(host_url, stack = nil) 10 | @config = load_config 11 | @host_url = fetch_cdn(host_url) 12 | @host_url += File.basename(stack) if stack 13 | end 14 | 15 | def fetch(path) 16 | curl = curl_command("-O #{@host_url.join(path)}") 17 | run!(curl) 18 | end 19 | 20 | def fetch_untar(path, files_to_extract = nil) 21 | curl = curl_command("#{@host_url.join(path)} -s -o") 22 | run!("#{curl} - | tar zxf - #{files_to_extract}") 23 | end 24 | 25 | def fetch_bunzip2(path, files_to_extract = nil) 26 | curl = curl_command("#{@host_url.join(path)} -s -o") 27 | run!("#{curl} - | tar jxf - #{files_to_extract}") 28 | end 29 | 30 | private 31 | def curl_command(command) 32 | "set -o pipefail; curl --fail --retry 3 --retry-delay 1 --connect-timeout #{curl_connect_timeout_in_seconds} --max-time #{curl_timeout_in_seconds} #{command}" 33 | end 34 | 35 | def curl_timeout_in_seconds 36 | ENV['CURL_TIMEOUT'] || 30 37 | end 38 | 39 | def curl_connect_timeout_in_seconds 40 | ENV['CURL_CONNECT_TIMEOUT'] || 3 41 | end 42 | 43 | def load_config 44 | YAML.load_file(CDN_YAML_FILE) || {} 45 | end 46 | 47 | def fetch_cdn(url) 48 | url = @config[url] || url 49 | Pathname.new(url) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/language_pack/helpers/bundler_cache.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | require "fileutils" 3 | require "language_pack/cache" 4 | 5 | # manipulating the `vendor/bundle` Bundler cache directory. 6 | # supports storing the cache in a "stack" directory 7 | class LanguagePack::BundlerCache 8 | attr_reader :bundler_dir 9 | 10 | # @param [LanguagePack::Cache] cache object 11 | # @param [String] stack buildpack is running on 12 | def initialize(cache, stack = nil) 13 | @cache = cache 14 | @stack = stack 15 | @bundler_dir = Pathname.new("vendor/bundle") 16 | @stack_dir = @stack ? Pathname.new(@stack) + @bundler_dir : @bundler_dir 17 | end 18 | 19 | # removes the bundler cache dir BOTH in the cache and local directory 20 | def clear(stack = nil) 21 | stack ||= @stack 22 | @cache.clear(stack) 23 | @bundler_dir.rmtree 24 | end 25 | 26 | # converts to cache directory to support stacks. only copy contents if the stack hasn't changed 27 | # @param [Boolean] denote if there's a stack change or not 28 | def convert_stack(stack_change) 29 | @cache.cache_copy(@bundler_dir, @stack_dir) unless stack_change 30 | @cache.clear(@bundler_dir) 31 | end 32 | 33 | # detects if using the non stack directory layout 34 | def old? 35 | @cache.exists?(@bundler_dir) 36 | end 37 | 38 | def exists? 39 | @cache.exists?(@stack_dir) 40 | end 41 | 42 | # writes cache contents to cache store 43 | def store 44 | @cache.clear(@stack_dir) 45 | @cache.store(@bundler_dir, @stack_dir) 46 | end 47 | 48 | # loads cache contents from the cache store 49 | def load 50 | @cache.load(@stack_dir, @bundler_dir) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /hatchet.json: -------------------------------------------------------------------------------- 1 | { 2 | "rake": [ 3 | "sharpstone/asset_precompile_fail", 4 | "sharpstone/asset_precompile_pass", 5 | "sharpstone/asset_precompile_not_found", 6 | "sharpstone/database_url_expected_in_rakefile", 7 | "sharpstone/connect_to_database_on_first_push", 8 | "sharpstone/no_rakefile", 9 | "sharpstone/bad_rakefile", 10 | "sharpstone/mri_187_no_rake", 11 | "sharpstone/mri_187_rake", 12 | "sharpstone/mri_200_no_rake", 13 | "sharpstone/mri_200_rake" 14 | ], 15 | "bundler": [ 16 | "sharpstone/bad_gemfile_on_platform", 17 | "sharpstone/git_gemspec", 18 | "sharpstone/no_lockfile", 19 | "sharpstone/sqlite3_gemfile", 20 | "sharpstone/nokogiri_160" 21 | ], 22 | "ruby": [ 23 | "sharpstone/mri_187", 24 | "sharpstone/mri_193_p484", 25 | "sharpstone/ruby_193_jruby_173", 26 | "sharpstone/ruby_193_jruby_176", 27 | "sharpstone/empty-procfile" 28 | ], 29 | "rack": [ 30 | "sharpstone/default_ruby", 31 | "sharpstone/mri_187_nokogiri", 32 | "sharpstone/mri_192", 33 | "sharpstone/mri_193", 34 | "sharpstone/mri_200", 35 | "sharpstone/mri_210" 36 | ], 37 | "rails2": [ 38 | "sharpstone/rails23_mri_187" 39 | ], 40 | "rails3": [ 41 | "sharpstone/rails3_mri_193", 42 | "sharpstone/railties3_mri_193", 43 | "sharpstone/rails3_12factor", 44 | "sharpstone/rails3_one_plugin", 45 | "sharpstone/rails3_runtime_assets", 46 | "sharpstone/rails3-fail-assets-compile" 47 | ], 48 | "rails4": [ 49 | "sharpstone/rails4-manifest", 50 | "sharpstone/rails3-to-4-no-bin", 51 | "sharpstone/rails4_windows_mri193", 52 | "sharpstone/rails4-fail-assets-compile" 53 | ], 54 | "rails41": [ 55 | "sharpstone/rails41_scaffold" 56 | ], 57 | "multibuildpack": [ 58 | "sharpstone/node_multi" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /spec/helpers/rake_runner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Rake Runner" do 4 | it "runs rake tasks that exist" do 5 | Hatchet::App.new('asset_precompile_pass').in_directory do 6 | rake = LanguagePack::Helpers::RakeRunner.new.load_rake_tasks! 7 | task = rake.task("assets:precompile") 8 | task.invoke 9 | 10 | expect(task.status).to eq(:pass) 11 | expect(task.output).to match("success!") 12 | expect(task.time).not_to be_nil 13 | end 14 | end 15 | 16 | it "detects when rake tasks fail" do 17 | Hatchet::App.new('asset_precompile_fail').in_directory do 18 | rake = LanguagePack::Helpers::RakeRunner.new.load_rake_tasks! 19 | task = rake.task("assets:precompile") 20 | task.invoke 21 | 22 | expect(task.status).to eq(:fail) 23 | expect(task.output).to match("assets:precompile fails") 24 | expect(task.time).not_to be_nil 25 | end 26 | end 27 | 28 | it "can show errors from bad Rakefiles" do 29 | Hatchet::App.new('bad_rakefile').in_directory do 30 | rake = LanguagePack::Helpers::RakeRunner.new.load_rake_tasks! 31 | task = rake.task("assets:precompile") 32 | expect(rake.rakefile_can_load?).to be_false 33 | expect(task.task_defined?).to be_false 34 | end 35 | end 36 | 37 | it "detects if task is missing" do 38 | Hatchet::App.new('asset_precompile_not_found').in_directory do 39 | task = LanguagePack::Helpers::RakeRunner.new.task("assets:precompile") 40 | expect(task.task_defined?).to be_false 41 | end 42 | end 43 | 44 | it "detects when no rakefile is present" do 45 | Hatchet::App.new('no_rakefile').in_directory do 46 | runner = LanguagePack::Helpers::RakeRunner.new 47 | expect(runner.rakefile_can_load?).to be_false 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/language_pack/cache.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | require "language_pack" 3 | 4 | # Manipulates/handles contents of the cache directory 5 | class LanguagePack::Cache 6 | # @param [String] path to the cache store 7 | def initialize(cache_path) 8 | @cache_base = Pathname.new(cache_path) 9 | end 10 | 11 | # removes the the specified path from the cache 12 | # @param [String] relative path from the cache_base 13 | def clear(path) 14 | target = (@cache_base + path) 15 | target.exist? && target.rmtree 16 | end 17 | 18 | # write cache contents 19 | # @param [String] path of contents to store. it will be stored using this a relative path from the cache_base. 20 | # @param [String] relative path to store the cache contents, if nil it will assume the from path 21 | def store(from, path = nil) 22 | path ||= from 23 | copy from, (@cache_base + path) 24 | end 25 | 26 | # load cache contents 27 | # @param [String] relative path of the cache contents 28 | # @param [String] path of where to store it locally, if nil, assume same relative path as the cache contents 29 | def load(path, dest = nil) 30 | dest ||= path 31 | copy (@cache_base + path), dest 32 | end 33 | 34 | # copy cache contents 35 | # @param [String] source directory 36 | # @param [String] destination directory 37 | def copy(from, to) 38 | return false unless File.exist?(from) 39 | FileUtils.mkdir_p File.dirname(to) 40 | system("cp -a #{from}/. #{to}") 41 | end 42 | 43 | # copy contents between to places in the cache 44 | # @param [String] source cache directory 45 | # @param [String] destination directory 46 | def cache_copy(from,to) 47 | copy(@cache_base + from, @cache_base + to) 48 | end 49 | 50 | # check if the cache content exists 51 | # @param [String] relative path of the cache contents 52 | # @param [Boolean] true if the path exists in the cache and false if otherwise 53 | def exists?(path) 54 | File.exists?(@cache_base + path) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/hone/ruby-git.git 3 | revision: 264836fcff3c037d8d8fc44bd770b150b46fdc4e 4 | branch: master 5 | specs: 6 | git (1.2.6) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | activesupport (4.1.4) 12 | i18n (~> 0.6, >= 0.6.9) 13 | json (~> 1.7, >= 1.7.7) 14 | minitest (~> 5.1) 15 | thread_safe (~> 0.1) 16 | tzinfo (~> 1.1) 17 | anvil-cli (0.16.1) 18 | progress (~> 2.4.0) 19 | rest-client (~> 1.6.7) 20 | thor (~> 0.15.2) 21 | diff-lcs (1.1.3) 22 | excon (0.38.0) 23 | heroku-api (0.3.19) 24 | excon (~> 0.38) 25 | multi_json (~> 1.8) 26 | heroku_hatchet (1.3.4) 27 | activesupport (~> 4) 28 | anvil-cli (~> 0) 29 | excon (~> 0) 30 | heroku-api (~> 0) 31 | repl_runner (~> 0.0.3) 32 | rrrretry (~> 1) 33 | thor (~> 0) 34 | threaded (~> 0) 35 | i18n (0.6.11) 36 | json (1.8.1) 37 | mime-types (1.25.1) 38 | minitest (5.4.0) 39 | multi_json (1.10.1) 40 | netrc (0.7.7) 41 | parallel (0.6.5) 42 | parallel_tests (0.13.1) 43 | parallel 44 | progress (2.4.0) 45 | rake (10.0.4) 46 | rdoc (4.1.1) 47 | json (~> 1.4) 48 | repl_runner (0.0.3) 49 | activesupport 50 | rest-client (1.6.8) 51 | mime-types (~> 1.16) 52 | rdoc (>= 2.4.2) 53 | rrrretry (1.0.0) 54 | rspec (2.2.0) 55 | rspec-core (~> 2.2) 56 | rspec-expectations (~> 2.2) 57 | rspec-mocks (~> 2.2) 58 | rspec-core (2.13.1) 59 | rspec-expectations (2.12.1) 60 | diff-lcs (~> 1.1.3) 61 | rspec-mocks (2.13.1) 62 | rspec-retry (0.2.1) 63 | rspec 64 | thor (0.15.4) 65 | thread_safe (0.3.4) 66 | threaded (0.0.4) 67 | tzinfo (1.2.1) 68 | thread_safe (~> 0.1) 69 | 70 | PLATFORMS 71 | ruby 72 | 73 | DEPENDENCIES 74 | excon 75 | git! 76 | heroku_hatchet 77 | netrc 78 | parallel_tests 79 | rake 80 | rspec-core 81 | rspec-expectations 82 | rspec-retry 83 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/core' 2 | require 'hatchet' 3 | require 'fileutils' 4 | require 'hatchet' 5 | require 'rspec/retry' 6 | require 'language_pack' 7 | 8 | require 'language_pack' 9 | 10 | ENV['RACK_ENV'] = 'test' 11 | 12 | RSpec.configure do |config| 13 | config.filter_run focused: true unless ENV['IS_RUNNING_ON_TRAVIS'] 14 | config.run_all_when_everything_filtered = true 15 | config.alias_example_to :fit, focused: true 16 | config.full_backtrace = true 17 | config.verbose_retry = true # show retry status in spec process 18 | config.default_retry_count = 2 if ENV['IS_RUNNING_ON_TRAVIS'] # retry all tests that fail again 19 | 20 | config.expect_with :rspec do |c| 21 | c.syntax = :expect 22 | end 23 | config.mock_with :none 24 | end 25 | 26 | def git_repo 27 | "https://github.com/heroku/heroku-buildpack-ruby.git" 28 | end 29 | 30 | def add_database(app, heroku) 31 | Hatchet::RETRIES.times.retry do 32 | heroku.post_addon(app.name, 'heroku-postgresql:hobby-dev') 33 | _, value = heroku.get_config_vars(app.name).body.detect {|key, value| key.match(/HEROKU_POSTGRESQL_[A-Z]+_URL/) } 34 | heroku.put_config_vars(app.name, 'DATABASE_URL' => value) 35 | end 36 | end 37 | 38 | def successful_body(app, options = {}) 39 | retry_limit = options[:retry_limit] || 50 40 | Excon.get("http://#{app.name}.herokuapp.com", :idempotent => true, :expects => 200, :retry_limit => retry_limit).body 41 | end 42 | 43 | def create_file_with_size_in(size, dir) 44 | name = File.join(dir, SecureRandom.hex(16)) 45 | File.open(name, 'w') {|f| f.print([ 1 ].pack("C") * size) } 46 | Pathname.new name 47 | end 48 | 49 | 50 | ReplRunner.register_commands(:console) do |config| 51 | config.terminate_command "exit" # the command you use to end the 'rails console' 52 | config.startup_timeout 60 # seconds to boot 53 | config.return_char "\n" # the character that submits the command 54 | config.sync_stdout "STDOUT.sync = true" # force REPL to not buffer standard out 55 | end -------------------------------------------------------------------------------- /vendor/syck_hack.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | 3 | # Hack to handle syck's DefaultKey bug 4 | # 5 | # This file is always loaded AFTER either syck or psych are already 6 | # loaded. It then looks at what constants are available and creates 7 | # a consistent view on all rubys. 8 | # 9 | # All this is so that there is always a YAML::Syck::DefaultKey 10 | # class no matter if the full yaml library has loaded or not. 11 | # 12 | 13 | $: << ENV['BUNDLER_LIB_PATH'] if ENV['BUNDLER_LIB_PATH'] 14 | require 'bundler/psyched_yaml' 15 | 16 | module YAML 17 | # In newer 1.9.2, there is a Syck toplevel constant instead of it 18 | # being underneith YAML. If so, reference it back under YAML as 19 | # well. 20 | if defined? ::Syck 21 | Syck = ::Syck 22 | 23 | # Otherwise, if there is no YAML::Syck, then we've got just psych 24 | # loaded, so lets define a stub for DefaultKey. 25 | elsif !defined? YAML::Syck 26 | module Syck 27 | class DefaultKey 28 | end 29 | end 30 | end 31 | 32 | # Now that we've got something that is always here, define #to_s 33 | # so when code tries to use this, it at least just shows up like it 34 | # should. 35 | module Syck 36 | class DefaultKey 37 | def to_s 38 | '=' 39 | end 40 | end 41 | end 42 | end 43 | 44 | # Sometime in the 1.9 dev cycle, the Syck constant was moved from under YAML 45 | # to be a toplevel constant. So gemspecs created under these versions of Syck 46 | # will have references to Syck::DefaultKey. 47 | # 48 | # So we need to be sure that we reference Syck at the toplevel too so that 49 | # we can always load these kind of gemspecs. 50 | # 51 | if !defined?(Syck) 52 | Syck = YAML::Syck 53 | end 54 | 55 | # Now that we've got Syck setup in all the right places, store 56 | # a reference to the DefaultKey class inside Gem. We do this so that 57 | # if later on YAML, etc are redefined, we've still got a consistent 58 | # place to find the DefaultKey class for comparison. 59 | 60 | module Gem 61 | SyckDefaultKey = YAML::Syck::DefaultKey 62 | end 63 | 64 | # :startdoc: 65 | -------------------------------------------------------------------------------- /lib/language_pack/instrument.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark' 2 | require 'stringio' 3 | require 'lpxc' 4 | require 'date' 5 | require 'language_pack/ruby' 6 | 7 | module LanguagePack 8 | module Instrument 9 | def self.bench_msg(message, level = 0, start_time, end_time, duration, build_id, buildpack_version) 10 | out.puts "measure.#{message}.start=#{start_time} measure.#{message}.end=#{end_time} measure.#{message}.duration=#{duration} measure.#{message}.level=#{level} measure.#{message}.build_id=#{build_id} request_id=#{request_id} measure.#{message}.buildpack_version=#{buildpack_version} measure.#{message}.buildpack=#{buildpack_name} " 11 | end 12 | 13 | def self.instrument(cat, title = "", *args) 14 | ret = nil 15 | start_time = DateTime.now.iso8601(6) 16 | duration = Benchmark.realtime do 17 | yield_with_block_depth do 18 | ret = yield 19 | end 20 | end 21 | end_time = DateTime.now.iso8601(6) 22 | bench_msg(cat, block_depth, start_time, end_time, duration, build_id, buildpack_version) 23 | 24 | ret 25 | end 26 | 27 | def self.out 28 | Thread.current[:out] ||= ENV['LOGPLEX_DEFAULT_TOKEN'] ? Lpxc.new(batch_size: 1) : StringIO.new 29 | end 30 | 31 | def self.trace(name, *args, &blk) 32 | ret = nil 33 | block_depth = 0 34 | 35 | instrument(name) { blk.call } 36 | end 37 | 38 | def self.yield_with_block_depth 39 | self.block_depth += 1 40 | yield 41 | ensure 42 | self.block_depth -= 1 43 | end 44 | 45 | def self.block_depth 46 | Thread.current[:block_depth] || 0 47 | end 48 | 49 | def self.block_depth=(value) 50 | Thread.current[:block_depth] = value 51 | end 52 | 53 | def self.build_id 54 | ENV['REQUEST_ID'] || ENV['SLUG_ID'] 55 | end 56 | 57 | def self.request_id 58 | ENV['REQUEST_ID'] 59 | end 60 | 61 | def self.buildpack_version 62 | LanguagePack::Ruby::BUILDPACK_VERSION 63 | end 64 | 65 | def self.buildpack_name 66 | LanguagePack::Ruby::NAME 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/language_pack/rails2.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | require "language_pack" 3 | require "language_pack/rack" 4 | 5 | # Rails 2 Language Pack. This is for any Rails 2.x apps. 6 | class LanguagePack::Rails2 < LanguagePack::Ruby 7 | # detects if this is a valid Rails 2 app 8 | # @return [Boolean] true if it's a Rails 2 app 9 | def self.use? 10 | instrument "rails2.use" do 11 | rails_version = bundler.gem_version('rails') 12 | return false unless rails_version 13 | is_rails2 = rails_version >= Gem::Version.new('2.0.0') && 14 | rails_version < Gem::Version.new('3.0.0') 15 | return is_rails2 16 | end 17 | end 18 | 19 | def name 20 | "Ruby/Rails" 21 | end 22 | 23 | def default_config_vars 24 | instrument "rails2.default_config_vars" do 25 | super.merge({ 26 | "RAILS_ENV" => env("RAILS_ENV") || "production", 27 | "RACK_ENV" => env("RACK_ENV") || "production", 28 | }) 29 | end 30 | end 31 | 32 | def default_process_types 33 | instrument "rails2.default_process_types" do 34 | web_process = bundler.has_gem?("thin") ? 35 | "bundle exec thin start -e $RAILS_ENV -p $PORT" : 36 | "bundle exec ruby script/server -p $PORT" 37 | 38 | super.merge({ 39 | "web" => web_process, 40 | "worker" => "bundle exec rake jobs:work", 41 | "console" => "bundle exec script/console" 42 | }) 43 | end 44 | end 45 | 46 | def compile 47 | instrument "rails2.compile" do 48 | install_plugins 49 | super 50 | end 51 | end 52 | 53 | private 54 | 55 | def install_plugins 56 | instrument "rails2.install_plugins" do 57 | plugins = ["rails_log_stdout"].reject { |plugin| bundler.has_gem?(plugin) } 58 | topic "Rails plugin injection" 59 | LanguagePack::Helpers::PluginsInstaller.new(plugins).install 60 | end 61 | end 62 | 63 | # most rails apps need a database 64 | # @return [Array] shared database addon 65 | def add_dev_database_addon 66 | ['heroku-postgresql:hobby-dev'] 67 | end 68 | 69 | # sets up the profile.d script for this buildpack 70 | def setup_profiled 71 | super 72 | set_env_default "RACK_ENV", "production" 73 | set_env_default "RAILS_ENV", "production" 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /spec/rails3_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | describe "Rails 3.x" do 4 | it "should deploy on ruby 1.9.3" do 5 | Hatchet::Runner.new("rails3_mri_193").deploy do |app, heroku| 6 | expect(app.output).to include("Asset precompilation completed") 7 | add_database(app, heroku) 8 | 9 | expect(app.output).to match("WARNING") 10 | expect(app.output).to match("Add 'rails_12factor' gem to your Gemfile to skip plugin injection") 11 | 12 | ls = app.run("ls vendor/plugins") 13 | expect(ls).to match("rails3_serve_static_assets") 14 | expect(ls).to match("rails_log_stdout") 15 | 16 | expect(successful_body(app)).to eq("hello") 17 | end 18 | end 19 | 20 | it "should not have warnings when using the rails_12factor gem" do 21 | Hatchet::Runner.new("rails3_12factor").deploy do |app, heroku| 22 | add_database(app, heroku) 23 | expect(app.output).not_to match("Add 'rails_12factor' gem to your Gemfile to skip plugin injection") 24 | expect(successful_body(app)).to eq("hello") 25 | end 26 | end 27 | 28 | it "should only display the correct plugin warning" do 29 | Hatchet::Runner.new("rails3_one_plugin").deploy do |app, heroku| 30 | add_database(app, heroku) 31 | expect(app.output).not_to match("rails_log_stdout") 32 | expect(app.output).to match("rails3_serve_static_assets") 33 | expect(app.output).to match("Add 'rails_12factor' gem to your Gemfile to skip plugin injection") 34 | expect(successful_body(app)).to eq("hello") 35 | end 36 | end 37 | 38 | context "when not using the rails gem" do 39 | it "should deploy on ruby 1.9.3" do 40 | Hatchet::Runner.new("railties3_mri_193").deploy do |app, heroku| 41 | expect(app.output).to include("Asset precompilation completed") 42 | add_database(app, heroku) 43 | expect(app.output).to match("Ruby/Rails") 44 | expect(successful_body(app)).to eq("hello") 45 | end 46 | end 47 | end 48 | 49 | it "fails compile if assets:precompile fails" do 50 | Hatchet::Runner.new("rails3-fail-assets-compile", allow_failure: true).deploy do |app, heroku| 51 | expect(app.output).to include("raising on assets:precompile on purpose") 52 | expect(app).not_to be_deployed 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/rails4_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | describe "Rails 4.0.x" do 4 | it "should detect rails successfully" do 5 | Hatchet::App.new('rails4-manifest').in_directory do 6 | expect(LanguagePack::Rails4.use?).to eq(true) 7 | end 8 | Hatchet::App.new('rails4-manifest').in_directory do 9 | expect(LanguagePack::Rails3.use?).to eq(false) 10 | end 11 | end 12 | 13 | it "should deploy on ruby 2.0.0" do 14 | Hatchet::Runner.new("rails4-manifest").deploy do |app, heroku| 15 | add_database(app, heroku) 16 | expect(app.output).to include("Detected manifest file, assuming assets were compiled locally") 17 | expect(app.output).not_to match("Include 'rails_12factor' gem to enable all platform features") 18 | end 19 | end 20 | 21 | it "upgraded from 3 to 4 missing ./bin still works" do 22 | Hatchet::Runner.new("rails3-to-4-no-bin").deploy do |app, heroku| 23 | expect(app.output).to include("Asset precompilation completed") 24 | add_database(app, heroku) 25 | 26 | expect(app.output).to match("WARNING") 27 | expect(app.output).to match("Include 'rails_12factor' gem to enable all platform features") 28 | 29 | app.run("rails console") do |console| 30 | console.run("'hello' + 'world'") {|result| expect(result).to match('helloworld')} 31 | end 32 | end 33 | end 34 | 35 | it "works with windows" do 36 | Hatchet::Runner.new("rails4_windows_mri193").deploy do |app, heroku| 37 | result = app.run("rails -v") 38 | expect(result).to match("4.0.0") 39 | 40 | result = app.run("rake -T") 41 | expect(result).to match("assets:precompile") 42 | 43 | result = app.run("bundle show rails") 44 | expect(result).to match("rails-4.0.0") 45 | 46 | expect(app.output).to match("Removing `Gemfile.lock`") 47 | 48 | before_final_warnings = app.output.split("Bundle completed").first 49 | expect(before_final_warnings).to match("Removing `Gemfile.lock`") 50 | end 51 | end 52 | 53 | it "fails compile if assets:precompile fails" do 54 | Hatchet::Runner.new("rails4-fail-assets-compile", allow_failure: true).deploy do |app, heroku| 55 | expect(app.output).to include("raising on assets:precompile on purpose") 56 | expect(app).not_to be_deployed 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /support/s3/hmac: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Implement HMAC functionality on top of the OpenSSL digest functions. 3 | # licensed under the terms of the GNU GPL v2 4 | # Copyright 2007 Victor Lowther 5 | 6 | die() { 7 | echo $* 8 | exit 1 9 | } 10 | 11 | check_deps() { 12 | local res=0 13 | while [ $# -ne 0 ]; do 14 | which "${1}" >& /dev/null || { res=1; echo "${1} not found."; } 15 | shift 16 | done 17 | (( res == 0 )) || die "aborting." 18 | } 19 | 20 | # write a byte (passed as hex) to stdout 21 | write_byte() { 22 | # $1 = byte to write 23 | printf "\\x$(printf "%x" ${1})" 24 | } 25 | 26 | # make an hmac pad out of a key. 27 | # this is not the most secure way of doing it, but it is 28 | # the most expedient. 29 | make_hmac_pad() { 30 | # using key in file $1 and byte in $2, create the appropriate hmac pad 31 | # Pad keys out to $3 bytes 32 | # if key is longer than $3, use hash $4 to hash the key first. 33 | local x y a size remainder oifs 34 | (( remainder = ${3} )) 35 | # in case someone else was messing with IFS. 36 | for x in $(echo -n "${1}" | od -v -t u1 | cut -b 9-); 37 | do 38 | write_byte $((${x} ^ ${2})) 39 | (( remainder -= 1 )) 40 | done 41 | for ((y=0; remainder - y ;y++)); do 42 | write_byte $((0 ^ ${2})) 43 | done 44 | } 45 | 46 | # utility functions for making hmac pads 47 | hmac_ipad() { 48 | make_hmac_pad "${1}" 0x36 ${2} "${3}" 49 | } 50 | 51 | hmac_opad() { 52 | make_hmac_pad "${1}" 0x5c ${2} "${3}" 53 | } 54 | 55 | # hmac something 56 | do_hmac() { 57 | # $1 = algo to use. Must be one that openssl knows about 58 | # $2 = keyfile to use 59 | # $3 = file to hash. uses stdin if none is given. 60 | # accepts input on stdin, leaves it on stdout. 61 | # Output is binary, if you want something else pipe it accordingly. 62 | local blocklen keysize x 63 | case "${1}" in 64 | sha) blocklen=64 ;; 65 | sha1) blocklen=64 ;; 66 | md5) blocklen=64 ;; 67 | md4) blocklen=64 ;; 68 | sha256) blocklen=64 ;; 69 | sha512) blocklen=128 ;; 70 | *) die "Unknown hash ${1} passed to hmac!" ;; 71 | esac 72 | cat <(hmac_ipad ${2} ${blocklen} "${1}") "${3:--}" | openssl dgst "-${1}" -binary | \ 73 | cat <(hmac_opad ${2} ${blocklen} "${1}") - | openssl dgst "-${1}" -binary 74 | } 75 | 76 | [[ ${1} ]] || die "Must pass the name of the hash function to use to ${0}". 77 | 78 | check_deps od openssl 79 | do_hmac "${@}" 80 | -------------------------------------------------------------------------------- /spec/rubies_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | describe "Ruby Versions" do 4 | it "should allow patchlevels" do 5 | Hatchet::Runner.new("mri_193_p484").deploy do |app| 6 | version = '1.9.3p484' 7 | expect(app.output).to match("ruby-1.9.3-p484") 8 | expect(app.run('ruby -v')).to match(version) 9 | end 10 | end 11 | 12 | 13 | it "should deploy ruby 1.8.7 properly" do 14 | Hatchet::Runner.new("mri_187").deploy do |app| 15 | version = '1.8.7' 16 | expect(app.output).to match(version) 17 | expect(app.run('ruby -v')).to match(version) 18 | end 19 | end 20 | 21 | it "should deploy ruby 1.9.2 properly" do 22 | Hatchet::Runner.new("mri_192").deploy do |app| 23 | version = '1.9.2' 24 | expect(app.output).to match(version) 25 | expect(app.run('ruby -v')).to match(version) 26 | end 27 | end 28 | 29 | it "should deploy ruby 1.9.2 properly (git)" do 30 | Hatchet::GitApp.new("mri_192", buildpack: git_repo).deploy do |app| 31 | version = '1.9.2' 32 | expect(app.output).to match(version) 33 | expect(app.run('ruby -v')).to match(version) 34 | end 35 | end 36 | 37 | it "should deploy ruby 1.9.3 properly" do 38 | Hatchet::Runner.new("mri_193").deploy do |app| 39 | version = '1.9.3' 40 | expect(app.output).to match(version) 41 | expect(app.run('ruby -v')).to match(version) 42 | end 43 | end 44 | 45 | it "should deploy ruby 2.0.0 properly" do 46 | Hatchet::Runner.new("mri_200").deploy do |app| 47 | version = '2.0.0' 48 | expect(app.output).to match(version) 49 | expect(app.run('ruby -v')).to match(version) 50 | 51 | expect(app.output).to match("devcenter.heroku.com/articles/ruby-default-web-server") 52 | end 53 | end 54 | 55 | it "should deploy jruby 1.7.3 (legacy jdk) properly" do 56 | Hatchet::AnvilApp.new("ruby_193_jruby_173").deploy do |app| 57 | expect(app.output).to match("Installing JVM: openjdk1.7.0_25") 58 | expect(app.output).to match("ruby-1.9.3-jruby-1.7.3") 59 | expect(app.output).not_to include("OpenJDK 64-Bit Server VM warning") 60 | expect(app.run('ruby -v')).to match("jruby 1.7.3") 61 | end 62 | end 63 | 64 | it "should deploy jruby 1.7.6 (latest jdk) properly" do 65 | Hatchet::AnvilApp.new("ruby_193_jruby_176").deploy do |app| 66 | expect(app.output).to match("Installing JVM: openjdk7-latest") 67 | expect(app.output).to match("ruby-1.9.3-jruby-1.7.6") 68 | expect(app.output).not_to include("OpenJDK 64-Bit Server VM warning") 69 | expect(app.run('ruby -v')).to match("jruby 1.7.6") 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/language_pack/ruby_version.rb: -------------------------------------------------------------------------------- 1 | require "language_pack/shell_helpers" 2 | 3 | module LanguagePack 4 | class RubyVersion 5 | class BadVersionError < StandardError 6 | def initialize(output = "") 7 | msg = "Can not parse Ruby Version:\n" 8 | msg << "Valid versions listed on: https://devcenter.heroku.com/articles/ruby-support\n" 9 | msg << output 10 | super msg 11 | end 12 | end 13 | 14 | DEFAULT_VERSION_NUMBER = "2.0.0" 15 | DEFAULT_VERSION = "ruby-#{DEFAULT_VERSION_NUMBER}" 16 | LEGACY_VERSION_NUMBER = "1.9.2" 17 | LEGACY_VERSION = "ruby-#{LEGACY_VERSION_NUMBER}" 18 | RUBY_VERSION_REGEX = %r{ 19 | (?\d+\.\d+\.\d+){0} 20 | (?p\d+){0} 21 | (?\w+){0} 22 | (?.+){0} 23 | 24 | ruby-\g(-\g)?(-\g-\g)? 25 | }x 26 | 27 | attr_reader :set, :version, :version_without_patchlevel, :patchlevel, :engine, :ruby_version, :engine_version 28 | include LanguagePack::ShellHelpers 29 | 30 | def initialize(bundler_output, app = {}) 31 | @set = nil 32 | @bundler_output = bundler_output 33 | @app = app 34 | set_version 35 | parse_version 36 | 37 | @version_without_patchlevel = @version.sub(/-p[\d]+/, '') 38 | end 39 | 40 | def rake_is_vendored? 41 | Gem::Version.new(self.ruby_version) >= Gem::Version.new("1.9") 42 | end 43 | 44 | def default? 45 | @version == none 46 | end 47 | 48 | # determine if we're using jruby 49 | # @return [Boolean] true if we are and false if we aren't 50 | def jruby? 51 | engine == :jruby 52 | end 53 | 54 | # determine if we're using rbx 55 | # @return [Boolean] true if we are and false if we aren't 56 | def rbx? 57 | engine == :rbx 58 | end 59 | 60 | # determines if a build ruby is required 61 | # @return [Boolean] true if a build ruby is required 62 | def build? 63 | engine == :ruby && %w(1.8.7 1.9.2).include?(ruby_version) 64 | end 65 | 66 | # convert to a Gemfile ruby DSL incantation 67 | # @return [String] the string representation of the Gemfile ruby DSL 68 | def to_gemfile 69 | if @engine == :ruby 70 | "ruby '#{ruby_version}'" 71 | else 72 | "ruby '#{ruby_version}', :engine => '#{engine}', :engine_version => '#{engine_version}'" 73 | end 74 | end 75 | 76 | private 77 | 78 | def none 79 | if @app[:is_new] 80 | DEFAULT_VERSION 81 | elsif @app[:last_version] 82 | @app[:last_version] 83 | else 84 | LEGACY_VERSION 85 | end 86 | end 87 | 88 | def set_version 89 | if @bundler_output.empty? 90 | @set = false 91 | @version = none 92 | else 93 | @set = :gemfile 94 | @version = @bundler_output 95 | end 96 | end 97 | 98 | def parse_version 99 | md = RUBY_VERSION_REGEX.match(version) 100 | raise BadVersionError.new("'#{version}' is not valid") unless md 101 | @ruby_version = md[:ruby_version] 102 | @patchlevel = md[:patchlevel] 103 | @engine_version = md[:engine_version] || @ruby_version 104 | @engine = (md[:engine] || :ruby).to_sym 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/language_pack/rails4.rb: -------------------------------------------------------------------------------- 1 | require "language_pack" 2 | require "language_pack/rails3" 3 | 4 | # Rails 4 Language Pack. This is for all Rails 4.x apps. 5 | class LanguagePack::Rails4 < LanguagePack::Rails3 6 | ASSETS_CACHE_LIMIT = 52428800 # bytes 7 | 8 | # detects if this is a Rails 4.x app 9 | # @return [Boolean] true if it's a Rails 4.x app 10 | def self.use? 11 | instrument "rails4.use" do 12 | rails_version = bundler.gem_version('railties') 13 | return false unless rails_version 14 | is_rails4 = rails_version >= Gem::Version.new('4.0.0.beta') && 15 | rails_version < Gem::Version.new('4.1.0.beta1') 16 | return is_rails4 17 | end 18 | end 19 | 20 | def name 21 | "Ruby/Rails" 22 | end 23 | 24 | def default_process_types 25 | instrument "rails4.default_process_types" do 26 | super.merge({ 27 | "web" => "bin/rails server -p $PORT -e $RAILS_ENV", 28 | "console" => "bin/rails console" 29 | }) 30 | end 31 | end 32 | 33 | def build_bundler 34 | instrument "rails4.build_bundler" do 35 | super 36 | end 37 | end 38 | 39 | def compile 40 | instrument "rails4.compile" do 41 | super 42 | end 43 | end 44 | 45 | private 46 | 47 | def install_plugins 48 | instrument "rails4.install_plugins" do 49 | return false if bundler.has_gem?('rails_12factor') 50 | plugins = ["rails_serve_static_assets", "rails_stdout_logging"].reject { |plugin| bundler.has_gem?(plugin) } 51 | return false if plugins.empty? 52 | 53 | warn <<-WARNING 54 | Include 'rails_12factor' gem to enable all platform features 55 | See https://devcenter.heroku.com/articles/rails-integration-gems for more information. 56 | WARNING 57 | # do not install plugins, do not call super 58 | end 59 | end 60 | 61 | def public_assets_folder 62 | "public/assets" 63 | end 64 | 65 | def default_assets_cache 66 | "tmp/cache/assets" 67 | end 68 | 69 | def run_assets_precompile_rake_task 70 | instrument "rails4.run_assets_precompile_rake_task" do 71 | log("assets_precompile") do 72 | if Dir.glob('public/assets/manifest-*.json').any? 73 | puts "Detected manifest file, assuming assets were compiled locally" 74 | return true 75 | end 76 | 77 | precompile = rake.task("assets:precompile") 78 | return true unless precompile.is_defined? 79 | 80 | topic("Preparing app for Rails asset pipeline") 81 | 82 | @cache.load public_assets_folder 83 | @cache.load default_assets_cache 84 | 85 | precompile.invoke(env: rake_env) 86 | 87 | if precompile.success? 88 | log "assets_precompile", :status => "success" 89 | puts "Asset precompilation completed (#{"%.2f" % precompile.time}s)" 90 | 91 | puts "Cleaning assets" 92 | rake.task("assets:clean").invoke(env: rake_env) 93 | 94 | cleanup_assets_cache 95 | @cache.store public_assets_folder 96 | @cache.store default_assets_cache 97 | else 98 | precompile_fail(precompile.output) 99 | end 100 | end 101 | end 102 | end 103 | 104 | def cleanup_assets_cache 105 | instrument "rails4.cleanup_assets_cache" do 106 | LanguagePack::Helpers::StaleFileCleaner.new(default_assets_cache).clean_over(ASSETS_CACHE_LIMIT) 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/language_pack/helpers/rake_runner.rb: -------------------------------------------------------------------------------- 1 | class LanguagePack::Helpers::RakeRunner 2 | include LanguagePack::ShellHelpers 3 | 4 | class RakeTask 5 | ALLOWED = [:pass, :fail, :no_load, :not_found] 6 | include LanguagePack::ShellHelpers 7 | 8 | attr_accessor :output, :time, :task, :status, :task_defined, :rakefile_can_load 9 | 10 | alias :rakefile_can_load? :rakefile_can_load 11 | alias :task_defined? :task_defined 12 | alias :is_defined? :task_defined 13 | 14 | def initialize(task, options = {}) 15 | @task = task 16 | @default_options = {user_env: true}.merge(options) 17 | @status = :nil 18 | @output = "" 19 | end 20 | 21 | def success? 22 | status == :pass 23 | end 24 | 25 | def status? 26 | @status && @status != :nil 27 | end 28 | 29 | def status 30 | raise "Status not set for #{self.inspect}" if @status == :nil 31 | raise "Not allowed status: #{@status} for #{self.inspect}" unless ALLOWED.include?(@status) 32 | @status 33 | end 34 | 35 | def invoke(options = {}) 36 | options = @default_options.merge(options) 37 | quiet_option = options.delete(:quiet) 38 | 39 | puts "Running: rake #{task}" unless quiet_option 40 | time = Benchmark.realtime do 41 | cmd = "rake #{task}" 42 | 43 | if quiet_option 44 | self.output = run("rake #{task}", options) 45 | else 46 | self.output = pipe("rake #{task}", options) 47 | end 48 | end 49 | self.time = time 50 | 51 | if $?.success? 52 | self.status = :pass 53 | else 54 | self.status = :fail 55 | end 56 | return self 57 | end 58 | end 59 | 60 | def initialize(has_rake_gem = true) 61 | @has_rake = has_rake_gem && has_rakefile? 62 | if !@has_rake 63 | @rake_tasks = "" 64 | @rakefile_can_load = false 65 | end 66 | end 67 | 68 | def cannot_load_rakefile? 69 | !rakefile_can_load? 70 | end 71 | 72 | def rakefile_can_load? 73 | @rakefile_can_load 74 | end 75 | 76 | def instrument(*args, &block) 77 | LanguagePack::Instrument.instrument(*args, &block) 78 | end 79 | 80 | def load_rake_tasks(options = {}) 81 | instrument "ruby.rake_task_defined" do 82 | @rake_tasks ||= RakeTask.new("-P --trace").invoke(options.merge(quiet: true)).output 83 | @rakefile_can_load ||= $?.success? 84 | @rake_tasks 85 | end 86 | end 87 | 88 | def load_rake_tasks!(options = {}) 89 | out = load_rake_tasks(options) 90 | msg = "Could not detect rake tasks\n" 91 | msg << "ensure you can run `$ bundle exec rake -P` against your app with no environment variables present\n" 92 | msg << "and using the production group of your Gemfile.\n" 93 | msg << "This may be intentional, if you expected rake tasks to be run\n" 94 | msg << "cancel the build (CTRL+C) and fix the error then commit the fix:\n" 95 | msg << out 96 | puts msg if cannot_load_rakefile? 97 | return self 98 | end 99 | 100 | def task_defined?(task) 101 | return false if cannot_load_rakefile? 102 | @task_available ||= Hash.new {|hash, key| hash[key] = @rake_tasks.match(/\s#{key}\s/) } 103 | @task_available[task] 104 | end 105 | 106 | def not_found?(task) 107 | !task_defined?(task) 108 | end 109 | 110 | def task(rake_task, options = {}) 111 | t = RakeTask.new(rake_task, options) 112 | t.task_defined = task_defined?(rake_task) 113 | t.rakefile_can_load = rakefile_can_load? 114 | t 115 | end 116 | 117 | def invoke(task, options = {}) 118 | self.task(task, options).invoke 119 | end 120 | 121 | private 122 | 123 | def has_rakefile? 124 | %W{ Rakefile rakefile rakefile.rb Rakefile.rb}.detect {|file| File.exist?(file) } 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/language_pack/helpers/bundler_wrapper.rb: -------------------------------------------------------------------------------- 1 | class LanguagePack::Helpers::BundlerWrapper 2 | include LanguagePack::ShellHelpers 3 | 4 | class GemfileParseError < StandardError 5 | def initialize(error) 6 | msg = "There was an error parsing your Gemfile, we cannot continue\n" 7 | msg << error 8 | super msg 9 | end 10 | end 11 | 12 | VENDOR_URL = LanguagePack::Base::VENDOR_URL # coupling 13 | DEFAULT_FETCHER = LanguagePack::Fetcher.new(VENDOR_URL) # coupling 14 | BUNDLER_DIR_NAME = LanguagePack::Ruby::BUNDLER_GEM_PATH # coupling 15 | BUNDLER_PATH = File.expand_path("../../../../tmp/#{BUNDLER_DIR_NAME}", __FILE__) 16 | GEMFILE_PATH = Pathname.new "./Gemfile" 17 | 18 | attr_reader :bundler_path 19 | 20 | def initialize(options = {}) 21 | @fetcher = options[:fetcher] || DEFAULT_FETCHER 22 | @bundler_path = options[:bundler_path] || File.join(Dir.mktmpdir, "#{BUNDLER_DIR_NAME}") 23 | @gemfile_path = options[:gemfile_path] || GEMFILE_PATH 24 | @bundler_tar = options[:bundler_tar] || "#{BUNDLER_DIR_NAME}.tgz" 25 | @gemfile_lock_path = "#{@gemfile_path}.lock" 26 | @orig_bundle_gemfile = ENV['BUNDLE_GEMFILE'] 27 | ENV['BUNDLE_GEMFILE'] = @gemfile_path.to_s 28 | @path = Pathname.new "#{@bundler_path}/gems/#{BUNDLER_DIR_NAME}/lib" 29 | end 30 | 31 | def install 32 | fetch_bundler 33 | $LOAD_PATH << @path 34 | require "bundler" 35 | self 36 | end 37 | 38 | def clean 39 | ENV['BUNDLE_GEMFILE'] = @orig_bundle_gemfile 40 | FileUtils.remove_entry_secure(bundler_path) if Dir.exist?(bundler_path) 41 | end 42 | 43 | def has_gem?(name) 44 | specs.key?(name) 45 | end 46 | 47 | def gem_version(name) 48 | instrument "ruby.gem_version" do 49 | if spec = specs[name] 50 | spec.version 51 | end 52 | end 53 | end 54 | 55 | # detects whether the Gemfile.lock contains the Windows platform 56 | # @return [Boolean] true if the Gemfile.lock was created on Windows 57 | def windows_gemfile_lock? 58 | platforms.detect do |platform| 59 | /mingw|mswin/.match(platform.os) if platform.is_a?(Gem::Platform) 60 | end 61 | end 62 | 63 | def specs 64 | @specs ||= lockfile_parser.specs.each_with_object({}) {|spec, hash| hash[spec.name] = spec } 65 | end 66 | 67 | def platforms 68 | @platforms ||= lockfile_parser.platforms 69 | end 70 | 71 | def version 72 | Bundler::VERSION 73 | end 74 | 75 | def instrument(*args, &block) 76 | LanguagePack::Instrument.instrument(*args, &block) 77 | end 78 | 79 | def ruby_version 80 | instrument 'detect_ruby_version' do 81 | env = { "PATH" => "#{bundler_path}/bin:#{ENV['PATH']}", 82 | "RUBYLIB" => File.join(bundler_path, "gems", BUNDLER_DIR_NAME, "lib"), 83 | "GEM_PATH" => "#{bundler_path}:#{ENV["GEM_PATH"]}" 84 | } 85 | command = "bundle platform --ruby" 86 | 87 | output = run_stdout(command, user_env: true, env: env) 88 | raise GemfileParseError.new(run(command, user_env: true, env: env)) unless $?.success? 89 | if output.match(/No ruby version specified/) 90 | "" 91 | else 92 | output.chomp.sub('(', '').sub(')', '').sub("p", " p").split.join('-') 93 | end 94 | end 95 | end 96 | 97 | def lockfile_parser 98 | @lockfile_parser ||= parse_gemfile_lock 99 | end 100 | 101 | private 102 | def fetch_bundler 103 | instrument 'fetch_bundler' do 104 | return true if Dir.exists?(bundler_path) 105 | FileUtils.mkdir_p(bundler_path) 106 | Dir.chdir(bundler_path) do 107 | @fetcher.fetch_untar(@bundler_tar) 108 | end 109 | Dir["bin/*"].each {|path| `chmod 755 #{path}` } 110 | end 111 | end 112 | 113 | def parse_gemfile_lock 114 | instrument 'parse_bundle' do 115 | gemfile_contents = File.read(@gemfile_lock_path) 116 | Bundler::LockfileParser.new(gemfile_contents) 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/language_pack/rails3.rb: -------------------------------------------------------------------------------- 1 | require "language_pack" 2 | require "language_pack/rails2" 3 | 4 | # Rails 3 Language Pack. This is for all Rails 3.x apps. 5 | class LanguagePack::Rails3 < LanguagePack::Rails2 6 | # detects if this is a Rails 3.x app 7 | # @return [Boolean] true if it's a Rails 3.x app 8 | def self.use? 9 | instrument "rails3.use" do 10 | rails_version = bundler.gem_version('railties') 11 | return false unless rails_version 12 | is_rails3 = rails_version >= Gem::Version.new('3.0.0') && 13 | rails_version < Gem::Version.new('4.0.0') 14 | return is_rails3 15 | end 16 | end 17 | 18 | def name 19 | "Ruby/Rails" 20 | end 21 | 22 | def default_process_types 23 | instrument "rails3.default_process_types" do 24 | # let's special case thin here 25 | web_process = bundler.has_gem?("thin") ? 26 | "bundle exec thin start -R config.ru -e $RAILS_ENV -p $PORT" : 27 | "bundle exec rails server -p $PORT" 28 | 29 | super.merge({ 30 | "web" => web_process, 31 | "console" => "bundle exec rails console" 32 | }) 33 | end 34 | end 35 | 36 | def compile 37 | instrument "rails3.compile" do 38 | super 39 | end 40 | end 41 | 42 | private 43 | 44 | def install_plugins 45 | instrument "rails3.install_plugins" do 46 | return false if bundler.has_gem?('rails_12factor') 47 | plugins = {"rails_log_stdout" => "rails_stdout_logging", "rails3_serve_static_assets" => "rails_serve_static_assets" }. 48 | reject { |plugin, gem| bundler.has_gem?(gem) } 49 | return false if plugins.empty? 50 | plugins.each do |plugin, gem| 51 | warn "Injecting plugin '#{plugin}'" 52 | end 53 | warn "Add 'rails_12factor' gem to your Gemfile to skip plugin injection" 54 | LanguagePack::Helpers::PluginsInstaller.new(plugins.keys).install 55 | end 56 | end 57 | 58 | # runs the tasks for the Rails 3.1 asset pipeline 59 | def run_assets_precompile_rake_task 60 | instrument "rails3.run_assets_precompile_rake_task" do 61 | log("assets_precompile") do 62 | if File.exists?("public/assets/manifest.yml") 63 | puts "Detected manifest.yml, assuming assets were compiled locally" 64 | return true 65 | end 66 | 67 | precompile = rake.task("assets:precompile") 68 | return true unless precompile.is_defined? 69 | 70 | topic("Preparing app for Rails asset pipeline") 71 | 72 | precompile.invoke(env: rake_env) 73 | 74 | if precompile.success? 75 | log "assets_precompile", :status => "success" 76 | puts "Asset precompilation completed (#{"%.2f" % precompile.time}s)" 77 | else 78 | precompile_fail(precompile.output) 79 | end 80 | end 81 | end 82 | end 83 | 84 | def rake_env 85 | if user_env_hash.empty? 86 | default_env = { 87 | "RAILS_GROUPS" => ENV["RAILS_GROUPS"] || "assets", 88 | "RAILS_ENV" => ENV["RAILS_ENV"] || "production", 89 | "DATABASE_URL" => database_url 90 | } 91 | else 92 | default_env = { 93 | "RAILS_GROUPS" => "assets", 94 | "RAILS_ENV" => "production", 95 | "DATABASE_URL" => database_url 96 | } 97 | end 98 | default_env.merge(user_env_hash) 99 | end 100 | 101 | # generate a dummy database_url 102 | def database_url 103 | instrument "rails3.setup_database_url_env" do 104 | # need to use a dummy DATABASE_URL here, so rails can load the environment 105 | return env("DATABASE_URL") if env("DATABASE_URL") 106 | scheme = 107 | if bundler.has_gem?("pg") || bundler.has_gem?("jdbc-postgres") 108 | "postgres" 109 | elsif bundler.has_gem?("mysql") 110 | "mysql" 111 | elsif bundler.has_gem?("mysql2") 112 | "mysql2" 113 | elsif bundler.has_gem?("sqlite3") || bundler.has_gem?("sqlite3-ruby") 114 | "sqlite3" 115 | end 116 | "#{scheme}://user:pass@127.0.0.1/dbname" 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/language_pack/shell_helpers.rb: -------------------------------------------------------------------------------- 1 | require "shellwords" 2 | 3 | class NoShellEscape < String 4 | def shellescape 5 | self 6 | end 7 | end 8 | 9 | module LanguagePack 10 | module ShellHelpers 11 | @@user_env_hash = {} 12 | 13 | def self.user_env_hash 14 | @@user_env_hash 15 | end 16 | 17 | def user_env_hash 18 | @@user_env_hash 19 | end 20 | 21 | def env(var) 22 | ENV[var] || user_env_hash[var] 23 | end 24 | 25 | def self.blacklist?(key) 26 | %w(PATH GEM_PATH GEM_HOME GIT_DIR).include?(key) 27 | end 28 | 29 | def self.initialize_env(path) 30 | env_dir = Pathname.new("#{path}") 31 | if env_dir.exist? && env_dir.directory? 32 | env_dir.each_child do |file| 33 | key = file.basename.to_s 34 | value = file.read.strip 35 | user_env_hash[key] = value unless blacklist?(key) 36 | end 37 | end 38 | end 39 | 40 | # display error message and stop the build process 41 | # @param [String] error message 42 | def error(message) 43 | Kernel.puts " !" 44 | message.split("\n").each do |line| 45 | Kernel.puts " ! #{line.strip}" 46 | end 47 | Kernel.puts " !" 48 | log "exit", :error => message if respond_to?(:log) 49 | exit 1 50 | end 51 | 52 | def run!(command, options = {}) 53 | result = run(command, options) 54 | error("Command: '#{command}' failed unexpectedly:\n#{result}") unless $?.success? 55 | return result 56 | end 57 | 58 | # doesn't do any special piping. stderr won't be redirected. 59 | # @param [String] command to be run 60 | # @return [String] output of stdout 61 | def run_no_pipe(command, options = {}) 62 | run(command, options.merge({:out => ""})) 63 | end 64 | 65 | # run a shell command and pipe stderr to stdout 66 | # @param [String] command 67 | # @option options [String] :out the IO redirect of the command 68 | # @option options [Hash] :env explicit environment to run command in 69 | # @option options [Boolean] :user_env whether or not a user's environment variables will be loaded 70 | def run(command, options = {}) 71 | %x{ #{command_options_to_string(command, options)} } 72 | end 73 | 74 | # run a shell command and pipe stderr to /dev/null 75 | # @param [String] command to be run 76 | # @return [String] output of stdout 77 | def run_stdout(command, options = {}) 78 | options[:out] ||= '2>/dev/null' 79 | run(command, options) 80 | end 81 | 82 | def command_options_to_string(command, options) 83 | options[:env] ||= {} 84 | options[:out] ||= "2>&1" 85 | options[:env] = user_env_hash.merge(options[:env]) if options[:user_env] 86 | env = options[:env].map {|key, value| "#{key.shellescape}=#{value.shellescape}" }.join(" ") 87 | "/usr/bin/env #{env} bash -c #{command.shellescape} #{options[:out]} " 88 | end 89 | 90 | # run a shell command and stream the output 91 | # @param [String] command to be run 92 | def pipe(command, options = {}) 93 | output = "" 94 | IO.popen(command_options_to_string(command, options)) do |io| 95 | until io.eof? 96 | buffer = io.gets 97 | output << buffer 98 | puts buffer 99 | end 100 | end 101 | 102 | output 103 | end 104 | 105 | # display a topic message 106 | # (denoted by ----->) 107 | # @param [String] topic message to be displayed 108 | def topic(message) 109 | Kernel.puts "-----> #{message}" 110 | $stdout.flush 111 | end 112 | 113 | # display a message in line 114 | # (indented by 6 spaces) 115 | # @param [String] message to be displayed 116 | def puts(message) 117 | message.to_s.split("\n").each do |line| 118 | super " #{line.strip}" 119 | end 120 | $stdout.flush 121 | end 122 | 123 | def warn(message, options = {}) 124 | if options.key?(:inline) ? options[:inline] : false 125 | Kernel.puts "###### WARNING:" 126 | puts message 127 | Kernel.puts "" 128 | end 129 | @warnings ||= [] 130 | @warnings << message 131 | end 132 | 133 | def deprecate(message) 134 | @deprecations ||= [] 135 | @deprecations << message 136 | end 137 | 138 | def noshellescape(string) 139 | NoShellEscape.new(string) 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /spec/helpers/ruby_version_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "RubyVersion" do 4 | before(:each) do 5 | if ENV['RUBYOPT'] 6 | @original_rubyopt = ENV['RUBYOPT'] 7 | ENV['RUBYOPT'] = ENV['RUBYOPT'].sub('-rbundler/setup', '') 8 | end 9 | @bundler = LanguagePack::Helpers::BundlerWrapper.new 10 | end 11 | 12 | after(:each) do 13 | if ENV['RUBYOPT'] 14 | ENV['RUBYOPT'] = @original_rubyopt 15 | end 16 | @bundler.clean 17 | end 18 | 19 | it "correctly handles patch levels" do 20 | Hatchet::App.new("mri_193_p484").in_directory do |dir| 21 | ruby_version = LanguagePack::RubyVersion.new(@bundler.install.ruby_version, is_new: true) 22 | version_number = "1.9.3" 23 | version = "ruby-#{version_number}" 24 | expect(ruby_version.version_without_patchlevel).to eq(version) 25 | expect(ruby_version.patchlevel).to eq("p484") 26 | expect(ruby_version.engine_version).to eq(version_number) 27 | expect(ruby_version.engine).to eq(:ruby) 28 | end 29 | end 30 | 31 | it "correctly sets ruby version for bundler specified versions" do 32 | Hatchet::App.new("mri_193").in_directory do |dir| 33 | ruby_version = LanguagePack::RubyVersion.new(@bundler.install.ruby_version, is_new: true) 34 | version_number = "1.9.3" 35 | version = "ruby-#{version_number}" 36 | expect(ruby_version.version_without_patchlevel).to eq(version) 37 | expect(ruby_version.engine_version).to eq(version_number) 38 | expect(ruby_version.to_gemfile).to eq("ruby '#{version_number}'") 39 | expect(ruby_version.engine).to eq(:ruby) 40 | end 41 | end 42 | 43 | it "correctly sets default ruby versions" do 44 | Hatchet::App.new("default_ruby").in_directory do |dir| 45 | ruby_version = LanguagePack::RubyVersion.new(@bundler.install.ruby_version, is_new: true) 46 | version_number = LanguagePack::RubyVersion::DEFAULT_VERSION_NUMBER 47 | version = LanguagePack::RubyVersion::DEFAULT_VERSION 48 | expect(ruby_version.version_without_patchlevel).to eq(version) 49 | expect(ruby_version.engine_version).to eq(version_number) 50 | expect(ruby_version.to_gemfile).to eq("ruby '#{version_number}'") 51 | expect(ruby_version.engine).to eq(:ruby) 52 | expect(ruby_version.default?).to eq(true) 53 | end 54 | end 55 | 56 | it "correctly sets default legacy version" do 57 | Hatchet::App.new("default_ruby").in_directory do |dir| 58 | ruby_version = LanguagePack::RubyVersion.new(@bundler.install.ruby_version, is_new: false) 59 | version_number = LanguagePack::RubyVersion::LEGACY_VERSION_NUMBER 60 | version = LanguagePack::RubyVersion::LEGACY_VERSION 61 | expect(ruby_version.version_without_patchlevel).to eq(version) 62 | expect(ruby_version.engine_version).to eq(version_number) 63 | expect(ruby_version.to_gemfile).to eq("ruby '#{version_number}'") 64 | expect(ruby_version.engine).to eq(:ruby) 65 | end 66 | end 67 | 68 | it "detects Ruby 2.0.0" do 69 | Hatchet::App.new("mri_200").in_directory do |dir| 70 | ruby_version = LanguagePack::RubyVersion.new(@bundler.install.ruby_version, is_new: true) 71 | version_number = "2.0.0" 72 | version = "ruby-#{version_number}" 73 | expect(ruby_version.version_without_patchlevel).to eq(version) 74 | expect(ruby_version.engine_version).to eq(version_number) 75 | expect(ruby_version.to_gemfile).to eq("ruby '#{version_number}'") 76 | expect(ruby_version.engine).to eq(:ruby) 77 | end 78 | end 79 | 80 | 81 | it "detects non mri engines" do 82 | Hatchet::App.new("ruby_193_jruby_173").in_directory do |dir| 83 | ruby_version = LanguagePack::RubyVersion.new(@bundler.install.ruby_version, is_new: true) 84 | version_number = "1.9.3" 85 | engine_version = "1.7.3" 86 | engine = :jruby 87 | version = "ruby-#{version_number}-#{engine}-#{engine_version}" 88 | to_gemfile = "ruby '#{version_number}', :engine => '#{engine}', :engine_version => '#{engine_version}'" 89 | expect(ruby_version.version_without_patchlevel).to eq(version) 90 | expect(ruby_version.engine_version).to eq(engine_version) 91 | expect(ruby_version.to_gemfile).to eq(to_gemfile) 92 | expect(ruby_version.engine).to eq(engine) 93 | end 94 | end 95 | 96 | it "surfaces error message from bundler" do 97 | bundle_error_msg = "Zomg der was a problem in da gemfile" 98 | error_klass = LanguagePack::Helpers::BundlerWrapper::GemfileParseError 99 | Hatchet::App.new("bad_gemfile_on_platform").in_directory do |dir| 100 | @bundler = LanguagePack::Helpers::BundlerWrapper.new().install 101 | expect {LanguagePack::RubyVersion.new(@bundler.ruby_version)}.to raise_error(error_klass, /#{Regexp.escape(bundle_error_msg)}/) 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/language_pack/base.rb: -------------------------------------------------------------------------------- 1 | require "language_pack" 2 | require "pathname" 3 | require "yaml" 4 | require "digest/sha1" 5 | require "language_pack/shell_helpers" 6 | require "language_pack/cache" 7 | require "language_pack/helpers/bundler_cache" 8 | require "language_pack/metadata" 9 | require "language_pack/fetcher" 10 | require "language_pack/instrument" 11 | 12 | Encoding.default_external = Encoding::UTF_8 if defined?(Encoding) 13 | 14 | # abstract class that all the Ruby based Language Packs inherit from 15 | class LanguagePack::Base 16 | include LanguagePack::ShellHelpers 17 | 18 | VENDOR_URL = ENV['BUILDPACK_VENDOR_URL'] || "https://s3-external-1.amazonaws.com/heroku-buildpack-ruby" 19 | DEFAULT_LEGACY_STACK = "cedar" 20 | 21 | attr_reader :build_path, :cache 22 | 23 | # changes directory to the build_path 24 | # @param [String] the path of the build dir 25 | # @param [String] the path of the cache dir this is nil during detect and release 26 | def initialize(build_path, cache_path=nil) 27 | self.class.instrument "base.initialize" do 28 | @build_path = build_path 29 | @stack = ENV["STACK"] 30 | @cache = LanguagePack::Cache.new(cache_path) if cache_path 31 | @metadata = LanguagePack::Metadata.new(@cache) 32 | @bundler_cache = LanguagePack::BundlerCache.new(@cache, @stack) 33 | @id = Digest::SHA1.hexdigest("#{Time.now.to_f}-#{rand(1000000)}")[0..10] 34 | @warnings = [] 35 | @deprecations = [] 36 | @fetchers = {:buildpack => LanguagePack::Fetcher.new(VENDOR_URL) } 37 | 38 | Dir.chdir build_path 39 | end 40 | end 41 | 42 | def instrument(*args, &block) 43 | self.class.instrument(*args, &block) 44 | end 45 | 46 | def self.instrument(*args, &block) 47 | LanguagePack::Instrument.instrument(*args, &block) 48 | end 49 | 50 | def self.===(build_path) 51 | raise "must subclass" 52 | end 53 | 54 | # name of the Language Pack 55 | # @return [String] the result 56 | def name 57 | raise "must subclass" 58 | end 59 | 60 | # list of default addons to install 61 | def default_addons 62 | raise "must subclass" 63 | end 64 | 65 | # config vars to be set on first push. 66 | # @return [Hash] the result 67 | # @not: this is only set the first time an app is pushed to. 68 | def default_config_vars 69 | raise "must subclass" 70 | end 71 | 72 | # process types to provide for the app 73 | # Ex. for rails we provide a web process 74 | # @return [Hash] the result 75 | def default_process_types 76 | raise "must subclass" 77 | end 78 | 79 | # this is called to build the slug 80 | def compile 81 | write_release_yaml 82 | instrument 'base.compile' do 83 | Kernel.puts "" 84 | @warnings.each do |warning| 85 | Kernel.puts "###### WARNING:" 86 | puts warning 87 | Kernel.puts "" 88 | end 89 | if @deprecations.any? 90 | topic "DEPRECATIONS:" 91 | puts @deprecations.join("\n") 92 | end 93 | end 94 | end 95 | 96 | def write_release_yaml 97 | release = {} 98 | release["addons"] = default_addons 99 | release["config_vars"] = default_config_vars 100 | release["default_process_types"] = default_process_types 101 | FileUtils.mkdir("tmp") unless File.exists?("tmp") 102 | File.open("tmp/heroku-buildpack-release-step.yml", 'w') do |f| 103 | f.write(release.to_yaml) 104 | end 105 | 106 | unless File.exist?("Procfile") 107 | msg = "No Procfile detected, using the default web server (webrick)\n" 108 | msg << "https://devcenter.heroku.com/articles/ruby-default-web-server" 109 | warn msg 110 | end 111 | end 112 | 113 | # log output 114 | # Ex. log "some_message", "here", :someattr="value" 115 | def log(*args) 116 | args.concat [:id => @id] 117 | args.concat [:framework => self.class.to_s.split("::").last.downcase] 118 | 119 | start = Time.now.to_f 120 | log_internal args, :start => start 121 | 122 | if block_given? 123 | begin 124 | ret = yield 125 | finish = Time.now.to_f 126 | log_internal args, :status => "complete", :finish => finish, :elapsed => (finish - start) 127 | return ret 128 | rescue StandardError => ex 129 | finish = Time.now.to_f 130 | log_internal args, :status => "error", :finish => finish, :elapsed => (finish - start), :message => ex.message 131 | raise ex 132 | end 133 | end 134 | end 135 | 136 | private ################################## 137 | 138 | # sets up the environment variables for the build process 139 | def setup_language_pack_environment 140 | end 141 | 142 | def add_to_profiled(string) 143 | FileUtils.mkdir_p "#{build_path}/.profile.d" 144 | File.open("#{build_path}/.profile.d/ruby.sh", "a") do |file| 145 | file.puts string 146 | end 147 | end 148 | 149 | def set_env_default(key, val) 150 | add_to_profiled "export #{key}=${#{key}:-#{val}}" 151 | end 152 | 153 | def set_env_override(key, val) 154 | add_to_profiled %{export #{key}="#{val.gsub('"','\"')}"} 155 | end 156 | 157 | def log_internal(*args) 158 | message = build_log_message(args) 159 | %x{ logger -p user.notice -t "slugc[$$]" "buildpack-ruby #{message}" } 160 | end 161 | 162 | def build_log_message(args) 163 | args.map do |arg| 164 | case arg 165 | when Float then "%0.2f" % arg 166 | when Array then build_log_message(arg) 167 | when Hash then arg.map { |k,v| "#{k}=#{build_log_message([v])}" }.join(" ") 168 | else arg 169 | end 170 | end.join(" ") 171 | end 172 | end 173 | 174 | -------------------------------------------------------------------------------- /support/s3/s3: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # basic amazon s3 operations 3 | # Licensed under the terms of the GNU GPL v2 4 | # Copyright 2007 Victor Lowther 5 | 6 | set -e 7 | 8 | basedir="$( cd -P "$( dirname "$0" )" && pwd )" 9 | PATH="$basedir:$PATH" 10 | 11 | # print a message and bail 12 | die() { 13 | echo $* 14 | exit 1 15 | } 16 | 17 | # check to see if the variable name passed exists and holds a value. 18 | # Die if it does not. 19 | check_or_die() { 20 | [[ ${!1} ]] || die "Environment variable ${1} is not set." 21 | } 22 | 23 | # check to see if we have all the needed S3 variables defined. 24 | # Bail if we do not. 25 | check_s3() { 26 | local sak x 27 | for x in S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY; do 28 | check_or_die ${x}; 29 | done 30 | sak="$(echo -n $S3_SECRET_ACCESS_KEY | wc -c)" 31 | (( ${sak%%[!0-9 ]*} == 40 )) || \ 32 | die "S3 Secret Access Key is not exactly 40 bytes long. Please fix it." 33 | } 34 | # check to see if our external dependencies exist 35 | check_dep() { 36 | local res=0 37 | while [[ $# -ne 0 ]]; do 38 | which "${1}" >& /dev/null || { res=1; echo "${1} not found."; } 39 | shift 40 | done 41 | (( res == 0 )) || die "aborting." 42 | } 43 | 44 | check_deps() { 45 | check_dep openssl date hmac cat grep curl 46 | check_s3 47 | } 48 | 49 | urlenc() { 50 | # $1 = string to url encode 51 | # output is on stdout 52 | # we don't urlencode everything, just enough stuff. 53 | echo -n "${1}" | 54 | sed 's/%/%25/g 55 | s/ /%20/g 56 | s/#/%23/g 57 | s/\$/%24/g 58 | s/\&/%26/g 59 | s/+/%2b/g 60 | s/,/%2c/g 61 | s/:/%3a/g 62 | s/;/%3b/g 63 | s/?/%3f/g 64 | s/@/%40/g 65 | s/ /%09/g' 66 | } 67 | 68 | xmldec() { 69 | # no parameters. 70 | # accept input on stdin, put it on stdout. 71 | # patches accepted to get more stuff 72 | sed 's/\"/\"/g 73 | s/\&/\&/g 74 | s/\<//g' 76 | } 77 | 78 | ## basic S3 functionality. x-amz-header functionality is not implemented. 79 | # make an S3 signature string, which will be output on stdout. 80 | s3_signature_string() { 81 | # $1 = HTTP verb 82 | # $2 = date string, must be in UTC 83 | # $3 = bucket name, if any 84 | # $4 = resource path, if any 85 | # $5 = content md5, if any 86 | # $6 = content MIME type, if any 87 | # $7 = canonicalized headers, if any 88 | # signature string will be output on stdout 89 | local verr="Must pass a verb to s3_signature_string!" 90 | local verb="${1:?verr}" 91 | local bucket="${3}" 92 | local resource="${4}" 93 | local derr="Must pass a date to s3_signature_string!" 94 | local date="${2:?derr}" 95 | local mime="${6}" 96 | local md5="${5}" 97 | local headers="${7}" 98 | printf "%s\n%s\n%s\n%s\n%s\n%s%s" \ 99 | "${verb}" "${md5}" "${mime}" "${date}" \ 100 | "${headers}" "${bucket}" "${resource}" | \ 101 | hmac sha1 "${S3_SECRET_ACCESS_KEY}" | openssl base64 -e -a 102 | } 103 | 104 | # cheesy, but it is the best way to have multiple headers. 105 | curl_headers() { 106 | # each arg passed will be output on its own line 107 | local parms=$# 108 | for ((;$#;)); do 109 | echo "header = \"${1}\"" 110 | shift 111 | done 112 | } 113 | 114 | s3_curl() { 115 | # invoke curl to do all the heavy HTTP lifting 116 | # $1 = method (one of GET, PUT, or DELETE. HEAD is not handled yet.) 117 | # $2 = remote bucket. 118 | # $3 = remote name 119 | # $4 = local name. 120 | local bucket remote date sig md5 arg inout headers 121 | # header handling is kinda fugly, but it works. 122 | bucket="${2:+/${2}}/" # slashify the bucket 123 | remote="$(urlenc "${3}")" # if you don't, strange things may happen. 124 | stdopts="--connect-timeout 10 --fail --silent" 125 | [[ $CURL_S3_DEBUG == true ]] && stdopts="${stdopts} --show-error --fail" 126 | case "${1}" in 127 | GET) arg="-o" inout="${4:--}" # stdout if no $4 128 | ;; 129 | PUT) [[ ${2} ]] || die "PUT can has bucket?" 130 | if [[ ! ${3} ]]; then 131 | arg="-X PUT" 132 | headers[${#headers[@]}]="Content-Length: 0" 133 | elif [[ -f ${4} ]]; then 134 | md5="$(openssl dgst -md5 -binary "${4}"|openssl base64 -e -a)" 135 | arg="-T" inout="${4}" 136 | headers[${#headers[@]}]="x-amz-acl: public-read" 137 | headers[${#headers[@]}]="Expect: 100-continue" 138 | else 139 | die "Cannot write non-existing file ${4}" 140 | fi 141 | ;; 142 | DELETE) arg="-X DELETE" 143 | ;; 144 | HEAD) arg="-I" ;; 145 | *) die "Unknown verb ${1}. It probably would not have worked anyways." ;; 146 | esac 147 | date="$(TZ=UTC date '+%a, %e %b %Y %H:%M:%S %z')" 148 | sig=$(s3_signature_string ${1} "${date}" "${bucket}" "${remote}" "${md5}" "" "x-amz-acl:public-read") 149 | 150 | headers[${#headers[@]}]="Authorization: AWS ${S3_ACCESS_KEY_ID}:${sig}" 151 | headers[${#headers[@]}]="Date: ${date}" 152 | [[ ${md5} ]] && headers[${#headers[@]}]="Content-MD5: ${md5}" 153 | curl ${arg} "${inout}" ${stdopts} -o - -K <(curl_headers "${headers[@]}") \ 154 | "http://s3.amazonaws.com${bucket}${remote}" 155 | return $? 156 | } 157 | 158 | s3_put() { 159 | # $1 = remote bucket to put it into 160 | # $2 = remote name to put 161 | # $3 = file to put. This must be present if $2 is. 162 | s3_curl PUT "${1}" "${2}" "${3:-${2}}" 163 | return $? 164 | } 165 | 166 | s3_get() { 167 | # $1 = bucket to get file from 168 | # $2 = remote file to get 169 | # $3 = local file to get into. Will be overwritten if it exists. 170 | # If this contains a path, that path must exist before calling this. 171 | s3_curl GET "${1}" "${2}" "${3:-${2}}" 172 | return $? 173 | } 174 | 175 | s3_test() { 176 | # same args as s3_get, but uses the HEAD verb instead of the GET verb. 177 | s3_curl HEAD "${1}" "${2}" >/dev/null 178 | return $? 179 | } 180 | 181 | # Hideously ugly, but it works well enough. 182 | s3_buckets() { 183 | s3_get |grep -o '[^>]*' |sed 's/<[^>]*>//g' |xmldec 184 | return $? 185 | } 186 | 187 | # this will only return the first thousand entries, alas 188 | # Mabye some kind soul can fix this without writing an XML parser in bash? 189 | # Also need to add xml entity handling. 190 | s3_list() { 191 | # $1 = bucket to list 192 | [ "x${1}" == "x" ] && return 1 193 | s3_get "${1}" |grep -o '[^>]*' |sed 's/<[^>]*>//g'| xmldec 194 | return $? 195 | } 196 | 197 | s3_delete() { 198 | # $1 = bucket to delete from 199 | # $2 = item to delete 200 | s3_curl DELETE "${1}" "${2}" 201 | return $? 202 | } 203 | 204 | # because this uses s3_list, it suffers from the same flaws. 205 | s3_rmrf() { 206 | # $1 = bucket to delete everything from 207 | s3_list "${1}" | while read f; do 208 | s3_delete "${1}" "${f}"; 209 | done 210 | } 211 | 212 | check_deps 213 | case $1 in 214 | put) shift; s3_put "$@" ;; 215 | get) shift; s3_get "$@" ;; 216 | rm) shift; s3_delete "$@" ;; 217 | ls) shift; s3_list "$@" ;; 218 | test) shift; s3_test "$@" ;; 219 | buckets) s3_buckets ;; 220 | rmrf) shift; s3_rmrf "$@" ;; 221 | *) die "Unknown command ${1}." 222 | ;; 223 | esac 224 | -------------------------------------------------------------------------------- /vendor/lpxc.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | require 'net/http' 3 | require 'uri' 4 | require 'thread' 5 | require 'timeout' 6 | 7 | class Lpxc 8 | 9 | #After parsing opts and initializing defaults, the initializer 10 | #will start 2 threads. One thread for sending HTTP requests and another 11 | #thread for flushing log messages to the outlet thread periodically. 12 | #:hash => {}:: A data structure for grouping log messages by token. 13 | #:request_queue => SizedQueue.new:: Contains HTTP requests ready for outlet thread to deliver to logplex. 14 | #:default_token => nil:: You can specify a token that will be used for any call to Lpxc#puts that doesn't include a token. 15 | #:structured_data => '-':: Structured-data field for syslog headers. Ignored by logplex. 16 | #:msgid => '-':: Msg ID field for syslog headers. Ignored by logplex. 17 | #:procid => 'lpxc':: Proc ID field for syslog headers. This will show up in the Heroku logs tail command as: app [lpxc]. 18 | #:hostname => 'myhost':: Hostname field for syslog headers. Ignored by logplex. 19 | #:max_reqs_per_conn => 1_000:: Number of requests before we re-establish our keep-alive connection to logplex. 20 | #:conn_timeout => 2:: Number of seconds before timing out a sindle request to logplex. 21 | #:batch_size => 300:: Max number of log messages inside single HTTP request. 22 | #:flush_interval => 0.5:: Fractional number of seconds before flushing all log messages in buffer to logplex. 23 | #:logplex_url => \'https://east.logplex.io/logs':: HTTP server that will accept our log messages. 24 | #:disable_delay_flush => nil:: Force flush only batch_size is reached. 25 | def initialize(opts={}) 26 | @hash_lock = Mutex.new 27 | @hash = opts[:hash] || Hash.new 28 | @request_queue = opts[:request_queue] || SizedQueue.new(1) 29 | @default_token = opts[:default_token] || ENV['LOGPLEX_DEFAULT_TOKEN'] 30 | @structured_data = opts[:structured_data] || "-" 31 | @msgid = opts[:msgid] || "-" 32 | @procid = opts[:procid] || "lpxc" 33 | @hostname = opts[:hostname] || "myhost" 34 | @max_reqs_per_conn = opts[:max_reqs_per_conn] || 1_000 35 | @conn_timeout = opts[:conn_timeout] || 2 36 | @batch_size = opts[:batch_size] || 300 37 | @flush_interval = opts[:flush_interval] || 0.5 38 | @logplex_url = URI(opts[:logplex_url] || ENV["LOGPLEX_URL"] || 39 | raise("Must set logplex url.")) 40 | 41 | #Keep track of the number of requests that the outlet 42 | #is processing. This value is used by the wait function. 43 | @req_in_flight = 0 44 | 45 | #Initialize the last_flush to an arbitrary time. 46 | @last_flush = Time.now + @flush_interval 47 | 48 | #Start the processing threads. 49 | Thread.new {outlet} 50 | Thread.new {delay_flush} if opts[:disable_delay_flush].nil? 51 | end 52 | 53 | #The interface to publish logs into the stream. 54 | #This function will set the log message to the current time in UTC. 55 | #If the buffer for this token's log messages is full, it will flush the buffer. 56 | def puts(msg, tok=@default_token) 57 | @hash_lock.synchronize do 58 | #Messages are grouped by their token since 1 http request 59 | #to logplex must only contain log messages belonging to a single token. 60 | q = @hash[tok] ||= SizedQueue.new(@batch_size) 61 | #This call will block if the queue is full. 62 | #However this should never happen since the next command will flush 63 | #the queue if we add the last item. 64 | q.enq({:t => Time.now.utc, :token => tok, :msg => msg}) 65 | flush if q.size == q.max 66 | end 67 | end 68 | 69 | #Wait until all of the data has been cleared from memory. 70 | #This is useful if you don't want your program to exit before 71 | #we are able to deliver log messages to logplex. 72 | def wait 73 | sleep(0.1) until 74 | @hash.length.zero? && 75 | @request_queue.empty? && 76 | @req_in_flight.zero? 77 | end 78 | 79 | private 80 | 81 | #Take a lock to read all of the buffered messages. 82 | #Once we have read the messages, we make 1 http request for the batch. 83 | #We pass the request off into the request queue so that the request 84 | #can be sent to LOGPLEX_URL. 85 | def flush 86 | @hash.each do |tok, msgs| 87 | #Copy the messages from the queue into the payload array. 88 | payloads = [] 89 | msgs.size.times {payloads << msgs.deq} 90 | return if payloads.nil? || payloads.empty? 91 | 92 | #Use the payloads array to build a string that will be 93 | #used as the http body for the logplex request. 94 | body = "" 95 | payloads.flatten.each do |payload| 96 | body += "#{fmt(payload)}" 97 | end 98 | 99 | #Build a new HTTP request and place it into the queue 100 | #to be processed by the HTTP connection. 101 | req = Net::HTTP::Post.new(@logplex_url.path) 102 | req.basic_auth("token", tok) 103 | req.add_field('Content-Type', 'application/logplex-1') 104 | req.body = body 105 | @request_queue.enq(req) 106 | @hash.delete(tok) 107 | @last_flush = Time.now 108 | end 109 | end 110 | 111 | 112 | #This method must be called in order for the messages to be sent to Logplex. 113 | #This method also spawns a thread that allows the messages to be batched. 114 | #Messages are flushed from memory every 500ms or when we have 300 messages, 115 | #whichever comes first. 116 | def delay_flush 117 | loop do 118 | begin 119 | if interval_ready? 120 | @hash_lock.synchronize {flush} 121 | end 122 | sleep(0.01) 123 | rescue => e 124 | end 125 | end 126 | end 127 | 128 | def interval_ready? 129 | (Time.now.to_f - @last_flush.to_f).abs >= @flush_interval 130 | end 131 | 132 | #Format the user message into RFC5425 format. 133 | #This method also prepends the length to the message. 134 | def fmt(data) 135 | pkt = "<190>1 " 136 | pkt += "#{data[:t].strftime("%Y-%m-%dT%H:%M:%S+00:00")} " 137 | pkt += "#{@hostname} " 138 | pkt += "#{data[:token]} " 139 | pkt += "#{@procid} " 140 | pkt += "#{@msgid} " 141 | pkt += "#{@structured_data} " 142 | pkt += data[:msg] 143 | "#{pkt.size} #{pkt}" 144 | end 145 | 146 | #We use a keep-alive connection to send data to LOGPLEX_URL. 147 | #Each request will contain one or more log messages. 148 | def outlet 149 | loop do 150 | http = Net::HTTP.new(@logplex_url.host, @logplex_url.port) 151 | http.use_ssl = true if @logplex_url.scheme == 'https' 152 | begin 153 | http.start do |conn| 154 | num_reqs = 0 155 | while num_reqs < @max_reqs_per_conn 156 | #Blocks waiting for a request. 157 | req = @request_queue.deq 158 | @req_in_flight += 1 159 | resp = nil 160 | begin 161 | Timeout::timeout(@conn_timeout) {resp = conn.request(req)} 162 | rescue => e 163 | next 164 | ensure 165 | @req_in_flight -= 1 166 | end 167 | num_reqs += 1 168 | end 169 | end 170 | rescue => e 171 | ensure 172 | http.finish if http.started? 173 | end 174 | end 175 | end 176 | 177 | end 178 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | require "tmpdir" 3 | require 'hatchet/tasks' 4 | 5 | S3_BUCKET_NAME = "heroku-buildpack-ruby" 6 | VENDOR_URL = "https://s3.amazonaws.com/#{S3_BUCKET_NAME}" 7 | 8 | def s3_tools_dir 9 | File.expand_path("../support/s3", __FILE__) 10 | end 11 | 12 | def s3_upload(tmpdir, name) 13 | sh("#{s3_tools_dir}/s3 put #{S3_BUCKET_NAME} #{name}.tgz #{tmpdir}/#{name}.tgz") 14 | end 15 | 16 | def vendor_plugin(git_url, branch = nil) 17 | name = File.basename(git_url, File.extname(git_url)) 18 | Dir.mktmpdir("#{name}-") do |tmpdir| 19 | FileUtils.rm_rf("#{tmpdir}/*") 20 | 21 | Dir.chdir(tmpdir) do 22 | sh "git clone #{git_url} ." 23 | sh "git checkout origin/#{branch}" if branch 24 | FileUtils.rm_rf("#{name}/.git") 25 | sh("tar czvf #{tmpdir}/#{name}.tgz *") 26 | s3_upload(tmpdir, name) 27 | end 28 | end 29 | end 30 | 31 | def in_gem_env(gem_home, &block) 32 | old_gem_home = ENV['GEM_HOME'] 33 | old_gem_path = ENV['GEM_PATH'] 34 | ENV['GEM_HOME'] = ENV['GEM_PATH'] = gem_home.to_s 35 | 36 | yield 37 | 38 | ENV['GEM_HOME'] = old_gem_home 39 | ENV['GEM_PATH'] = old_gem_path 40 | end 41 | 42 | def install_gem(gem_name, version) 43 | name = "#{gem_name}-#{version}" 44 | Dir.mktmpdir("#{gem_name}-#{version}") do |tmpdir| 45 | Dir.chdir(tmpdir) do |dir| 46 | FileUtils.rm_rf("#{tmpdir}/*") 47 | 48 | in_gem_env(tmpdir) do 49 | sh("unset RUBYOPT; gem install #{gem_name} --version #{version} --no-ri --no-rdoc --env-shebang") 50 | sh("rm #{gem_name}-#{version}.gem") 51 | sh("rm -rf cache/#{gem_name}-#{version}.gem") 52 | sh("tar czvf #{tmpdir}/#{name}.tgz *") 53 | s3_upload(tmpdir, name) 54 | end 55 | end 56 | end 57 | end 58 | 59 | desc "update plugins" 60 | task "plugins:update" do 61 | vendor_plugin "http://github.com/heroku/rails_log_stdout.git", "legacy" 62 | vendor_plugin "http://github.com/pedro/rails3_serve_static_assets.git" 63 | vendor_plugin "http://github.com/hone/rails31_enable_runtime_asset_compilation.git" 64 | end 65 | 66 | desc "install vendored gem" 67 | task "gem:install", :gem, :version do |t, args| 68 | gem = args[:gem] 69 | version = args[:version] 70 | 71 | install_gem(gem, version) 72 | end 73 | 74 | desc "generate ruby versions manifest" 75 | task "ruby:manifest" do 76 | require 'rexml/document' 77 | require 'yaml' 78 | 79 | document = REXML::Document.new(`curl https://#{S3_BUCKET_NAME}.s3.amazonaws.com`) 80 | rubies = document.elements.to_a("//Contents/Key").map {|node| node.text }.select {|text| text.match(/^(ruby|rbx|jruby)-\\\\d+\\\\.\\\\d+\\\\.\\\\d+(-p\\\\d+)?/) } 81 | 82 | Dir.mktmpdir("ruby_versions-") do |tmpdir| 83 | name = 'ruby_versions.yml' 84 | File.open(name, 'w') {|file| file.puts(rubies.to_yaml) } 85 | sh("#{s3_tools_dir}/s3 put #{S3_BUCKET_NAME} #{name} #{name}") 86 | end 87 | end 88 | 89 | namespace :buildpack do 90 | require 'netrc' 91 | require 'excon' 92 | require 'json' 93 | require 'time' 94 | require 'cgi' 95 | require 'git' 96 | require 'fileutils' 97 | require 'digest/md5' 98 | require 'securerandom' 99 | 100 | def connection 101 | @connection ||= begin 102 | user, password = Netrc.read["api.heroku.com"] 103 | Excon.new("https://#{CGI.escape(user)}:#{password}@buildkits.herokuapp.com") 104 | end 105 | end 106 | 107 | def latest_release 108 | @latest_release ||= begin 109 | buildpack_name = "heroku/ruby" 110 | response = connection.get(path: "buildpacks/#{buildpack_name}/revisions") 111 | releases = JSON.parse(response.body) 112 | 113 | # { 114 | # "tar_link": "https://codon-buildpacks.s3.amazonaws.com/buildpacks/heroku/ruby-v84.tgz", 115 | # "created_at": "2013-11-06T18:55:04Z", 116 | # "published_by": "richard@heroku.com", 117 | # "id": 84 118 | # } 119 | releases.map! do |a| 120 | a["created_at"] = Time.parse(a["created_at"]) 121 | a 122 | end.sort! { |a,b| b["created_at"] <=> a["created_at"] } 123 | releases.first 124 | end 125 | end 126 | 127 | def new_version 128 | @new_version ||= "v#{latest_release["id"] + 1}" 129 | end 130 | 131 | def git 132 | @git ||= Git.open(".") 133 | end 134 | 135 | desc "increment buildpack version" 136 | task :increment do 137 | version_file = './lib/language_pack/version' 138 | require './lib/language_pack' 139 | require version_file 140 | 141 | if LanguagePack::Base::BUILDPACK_VERSION != new_version 142 | stashes = nil 143 | 144 | if git.status.changed.any? 145 | stashes = Git::Stashes.new(git) 146 | stashes.save("WIP") 147 | end 148 | 149 | File.open("#{version_file}.rb", 'w') do |file| 150 | file.puts <<-FILE 151 | require "language_pack/base" 152 | 153 | # This file is automatically generated by rake 154 | module LanguagePack 155 | class LanguagePack::Base 156 | BUILDPACK_VERSION = "#{new_version}" 157 | end 158 | end 159 | FILE 160 | end 161 | 162 | git.add "#{version_file}.rb" 163 | git.commit "bump to #{new_version}" 164 | 165 | stashes.pop if stashes 166 | 167 | puts "Bumped to #{new_version}" 168 | else 169 | puts "Already on #{new_version}" 170 | end 171 | end 172 | 173 | def changelog_entry? 174 | File.read("./CHANGELOG.md").split("\n").any? {|line| line.match(/^## #{new_version}/) } 175 | end 176 | 177 | desc "check if there's a changelog for the new version" 178 | task :changelog do 179 | if changelog_entry? 180 | puts "Changelog for #{new_version} exists" 181 | else 182 | puts "Please add a changelog entry for #{new_version}" 183 | end 184 | end 185 | 186 | def github_remote 187 | @github_remote ||= git.remotes.detect {|remote| remote.url.match(%r{heroku/heroku-buildpack-ruby.git$}) } 188 | 189 | end 190 | 191 | def git_push_master 192 | puts "Pushing master" 193 | git.push(github_remote, 'master', false) 194 | $?.success? 195 | end 196 | 197 | desc "update master branch" 198 | task :git_push_master do 199 | git_push_master 200 | end 201 | 202 | desc "stage a tarball of the buildpack" 203 | task :stage do 204 | Dir.mktmpdir("heroku-buildpack-ruby") do |tmpdir| 205 | Git.clone(File.expand_path("."), 'heroku-buildpack-ruby', path: tmpdir) 206 | Dir.chdir(tmpdir) do |dir| 207 | streamer = lambda do |chunk, remaining_bytes, total_bytes| 208 | File.open("ruby.tgz", "w") {|file| file.print(chunk) } 209 | end 210 | Excon.get(latest_release["tar_link"], :response_block => streamer) 211 | Dir.chdir("heroku-buildpack-ruby") do |dir| 212 | sh "tar xzf ../ruby.tgz .env" 213 | sh "tar czf ../buildpack.tgz * .env" 214 | end 215 | 216 | @digest = Digest::MD5.hexdigest(File.read("buildpack.tgz")) 217 | end 218 | 219 | filename = "buildpacks/#{@digest}.tgz" 220 | puts "Writing to #{filename}" 221 | FileUtils.mkdir_p("buildpacks/") 222 | FileUtils.cp("#{tmpdir}/buildpack.tgz", filename) 223 | FileUtils.cp("#{tmpdir}/buildpack.tgz", "buildpacks/buildpack.tgz") 224 | end 225 | end 226 | 227 | def multipart_form_data(buildpack_file_path) 228 | body = '' 229 | boundary = SecureRandom.hex(4) 230 | data = File.open(buildpack_file_path) 231 | 232 | data.binmode if data.respond_to?(:binmode) 233 | data.pos = 0 if data.respond_to?(:pos=) 234 | 235 | body << "--#{boundary}" << Excon::CR_NL 236 | body << %{Content-Disposition: form-data; name="buildpack"; filename="#{File.basename(buildpack_file_path)}"} << Excon::CR_NL 237 | body << 'Content-Type: application/x-gtar' << Excon::CR_NL 238 | body << Excon::CR_NL 239 | body << File.read(buildpack_file_path) 240 | body << Excon::CR_NL 241 | body << "--#{boundary}--" << Excon::CR_NL 242 | 243 | { 244 | :headers => { 'Content-Type' => %{multipart/form-data; boundary="#{boundary}"} }, 245 | :body => body 246 | } 247 | end 248 | 249 | task :publish do 250 | buildpack_name = "heroku/ruby" 251 | puts "Publishing #{buildpack_name} buildpack" 252 | resp = connection.post(multipart_form_data("buildpacks/buildpack.tgz").merge(path: "/buildpacks/#{buildpack_name}")) 253 | puts resp.status 254 | puts resp.body 255 | end 256 | 257 | desc "tag a release" 258 | task :tag do 259 | tagged_version = 260 | if @new_version.nil? 261 | "v#{latest_release["id"]}" 262 | else 263 | new_version 264 | end 265 | 266 | git.add_tag(tagged_version) 267 | puts "Created tag #{tagged_version}" 268 | 269 | puts "Pushing tag to remote #{github_remote}" 270 | git.push(github_remote, nil, true) 271 | end 272 | 273 | desc "release a new version of the buildpack" 274 | task :release do 275 | Rake::Task["buildpack:increment"].invoke 276 | raise "Please add a changelog entry for #{new_version}" unless changelog_entry? 277 | raise "Can't push to master" unless git_push_master 278 | Rake::Task["buildpack:stage"].invoke 279 | Rake::Task["buildpack:publish"].invoke 280 | Rake::Task["buildpack:tag"].invoke 281 | end 282 | end 283 | 284 | begin 285 | require 'rspec/core/rake_task' 286 | 287 | desc "Run specs" 288 | RSpec::Core::RakeTask.new(:spec) do |t| 289 | t.rspec_opts = %w(-fs --color) 290 | #t.ruby_opts = %w(-w) 291 | end 292 | task :default => :spec 293 | rescue LoadError => e 294 | end 295 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Master 2 | 3 | ## v127 (9/18/2014) 4 | 5 | * rbx is now stack aware 6 | 7 | ## v126 (8/4/2014) 8 | 9 | * fix bundler cache clearing on ruby version change 10 | * vendor the jvm when yui-compressor is detected 11 | 12 | ## v125 (8/1/2014) 13 | 14 | * bump to node 0.10.30 on cedar-14 15 | 16 | ## v124 (8/1/2014) 17 | 18 | * use node 0.10.29 on cedar-14 19 | * properly use vendored jvm, so not to be dependent on java on the stack image 20 | 21 | ## v123 (7/25/2014) 22 | 23 | * fix permission denied edge cases when copying the bundler cache with minitest 24 | 25 | ## v122 (7/25/2014) 26 | 27 | * handle bundler cache for stack changes on existing apps 28 | 29 | ## v121 (6/30/2014) 30 | 31 | * on new apps, source default envs instead of replacing them 32 | * support different stacks for new apps 33 | 34 | ## v120 (6/16/2014) 35 | 36 | * Bump bundler to 1.6.3 which includes improved dependency resolver 37 | 38 | ## v119 (5/9/2014) 39 | 40 | * Temporarily disable default ruby cache 41 | 42 | ## v118 (5/6/2014) 43 | 44 | * Ruby version detection now loads user environment variables 45 | 46 | ## v117 (4/14/2014) 47 | 48 | Features: 49 | 50 | 51 | Bugfixes: 52 | 53 | * fix anvil use case of multibuildpack with node 54 | 55 | 56 | ## v116 (4/10/2014) 57 | 58 | Features: 59 | 60 | 61 | Bugfixes: 62 | 63 | * Revert back to Bundler 1.5.2 64 | 65 | 66 | ## v115 (4/9/2014) 67 | 68 | Features: 69 | 70 | 71 | Bugfixes: 72 | 73 | * Add default process types to all apps deployed regardless of `Procfile` 74 | 75 | ## v114 (4/9/2014) 76 | 77 | Features: 78 | 79 | * Bundler 1.6.1 80 | * Warn when not using a Procfile (looking at you webrick) 81 | 82 | Bugfixes: 83 | 84 | 85 | ## v113 (4/8/2014) 86 | 87 | Features: 88 | 89 | * use heroku-buildpack-nodejs's node binary 90 | * `CURL_CONNECT_TIMEOUT` and `CURL_TIMEOUT` are configurable as ENV vars 91 | 92 | Bugfixes: 93 | 94 | * Don't double print "Running: rake assets:precompile" on Ruby apps 95 | 96 | 97 | ## v112 (3/27/2014) 98 | 99 | Features: 100 | 101 | 102 | Bugfixes: 103 | 104 | * compile psych with libyaml 0.1.6 for CVE-2014-2525 105 | 106 | ## v111 (3/20/2014) 107 | 108 | Features: 109 | 110 | 111 | Bugfixes: 112 | 113 | * spelling 114 | 115 | 116 | ## v110 (3/20/2014) 117 | 118 | Features: 119 | 120 | * Better message when running `assets:precompile` without a database 121 | 122 | Bugfixes: 123 | 124 | ## v108 (2/27/2014) 125 | 126 | Features: 127 | 128 | * parse Bundler patchlevel option 129 | 130 | Bugfixes: 131 | 132 | * don't let users step on themselves by replacing `env` in `$PATH` 133 | 134 | ## v107 (2/26/2014) 135 | 136 | Features: 137 | 138 | Bugfixes: 139 | 140 | * more shellescaping bug fixes 141 | 142 | 143 | ## v105 144 | 145 | Rollbacked to v103 146 | 147 | 148 | ## v104 (2/26/2014) 149 | 150 | Features: 151 | 152 | Bugfixes: 153 | 154 | * fix bugs in shellescaping (#231) 155 | 156 | 157 | ## v103 (2/18/2014) 158 | 159 | Features: 160 | 161 | * Rails 4.1.0 Support. Stop writing database.yml and support for secrets.yml by generating SECRET_KEY_BASE for users. 162 | 163 | Bugfixes: 164 | 165 | 166 | ## v102 (2/6/2014) 167 | 168 | Features: 169 | 170 | Bugfixes: 171 | 172 | * use blacklist of env vars, so users can't break the build process 173 | 174 | 175 | ## v101 (2/5/2014) 176 | 177 | Features: 178 | 179 | Bugfixes: 180 | 181 | * fix rake detection when DATABASE_URL is not present 182 | * support BUNDLE_WITHOUT when using ponies 183 | * quote ponies env vars, so build doesn't break 184 | 185 | 186 | ## v100 (2/4/2014) 187 | 188 | Features: 189 | 190 | Bugfixes: 191 | 192 | * compile psych with libyaml 0.1.5 for CVE-2013-6393 193 | 194 | ## v99 (2/4/2014) 195 | 196 | Features: 197 | 198 | * Noop 199 | 200 | Bugfixes: 201 | 202 | 203 | ## v98 (1/30/2014) 204 | 205 | Features: 206 | 207 | Bugfixes: 208 | 209 | * Use vendored JDK binary during build 210 | 211 | 212 | ## v97 (1/30/2014) 213 | 214 | Features: 215 | 216 | Bugfixes: 217 | 218 | * Actually finalize method rename to `install_bundler_in_app` 219 | 220 | 221 | ## v96 (1/29/2014) 222 | 223 | Features: 224 | 225 | Bugfixes: 226 | 227 | * Finalize method rename to `install_bundler_in_app` 228 | 229 | ## v95 230 | 231 | Rollback to v93 232 | 233 | ## v94 (1/29/2014) 234 | 235 | Features: 236 | 237 | Bugfixes: 238 | 239 | * Fixed `uninitialized constant Rake::DSL` error when running rake tasks on Ruby 1.9.2 240 | 241 | ## v93 (01/28/2014) 242 | 243 | Features: 244 | 245 | * buildpack-env-arg (ponies) support 246 | 247 | Bugfixes: 248 | 249 | ## v92 (01/27/2014) 250 | 251 | Features: 252 | 253 | Bugfixes: 254 | 255 | * Only display rake error messages if a `Rakefile` exists 256 | * when detecting for ruby version, don't use stderr messages 257 | 258 | ## v91 (01/16/2014) 259 | 260 | Features: 261 | 262 | * Parallel gem installation with bundler 1.5.2 263 | 264 | Bugfixes: 265 | 266 | 267 | ## v90 (01/09/2014) 268 | 269 | Features: 270 | 271 | * Rollback v89 due to bug in bundler 1.5.1 272 | 273 | Bugfixes: 274 | 275 | ## v89 (01/09/2014) 276 | 277 | Features: 278 | 279 | * Use most recent version of bundler with support for parallel Gem installation 280 | 281 | Bugfixes: 282 | 283 | ## v86 (12/11/2013) 284 | 285 | Features: 286 | 287 | Bugfixes: 288 | 289 | * Windows warnings will now display before bundle install, this prevents an un-resolvable `Gemfile` from erroring which previously prevented the warning roll up from being shown. When this happened the developer did not see that we are clearing the `Gemfile.lock` from the git repository when bundled on a windows machine. 290 | * Checks for `public/assets/manifest*.json` and `public/assets/manifest.yml` will now come before Rake task detection introduced in v85. 291 | 292 | ## v85 (12/05/2013) 293 | 294 | Features: 295 | 296 | 297 | Bugfixes: 298 | 299 | * Any errors in a Rakefile will now be explicitly shown as such instead of hidden in a `assets:precompile` task detection failure (#171) 300 | * Now using correct default "hobby" database #179 301 | 302 | ## v84 (11/06/2013) 303 | 304 | Features: 305 | 306 | * Any Ruby app with a rake `assets:precompile` task present that does not run successfully will now fail. This matches the current behavior of Rails 3 and 4 deploys. 307 | 308 | 309 | Bugfixes: 310 | 311 | * Fix default gem cache 312 | 313 | ## v83 (10/29/2013) 314 | 315 | Features: 316 | 317 | * RubyVersion extracted into its own class 318 | * Release no longer requires language_pack 319 | * Detect no longer requires language_pack 320 | * Downloads with curl now retry on failed connections, pass exit status appropriately 321 | 322 | Bugfixes: 323 | 324 | * Errors in Gemfiles will no longer show up as bad ruby versions #36 325 | * Fix warning warning libjffi-1.2.so on < JRuby 1.7.3 326 | 327 | ## v82 (10/28/2013) 328 | 329 | Bugfixes: 330 | 331 | * Rails 3 deploys that do not successfully run `assets:precompile` will now fail. 332 | 333 | ## v81 (10/15/2013) 334 | 335 | Features: 336 | 337 | * add Default Bundler Cache for new Ruby 2.0.0 apps 338 | * use Virginia S3 bucket instead of Cloudfront 339 | 340 | ## v80 (9/23/2013) 341 | 342 | Features: 343 | 344 | * Cache 50mb of Rails 4 intermediate cache 345 | * Support for Ruby 2.1.0 346 | 347 | Bugfixes: 348 | 349 | * Disable invoke dynamic on JRuby by default until JDK stabalizes it 350 | 351 | ## v79 (9/3/2013) 352 | 353 | Bugfixes: 354 | 355 | * Remove LPXC debug output when `DEBUG` env var is set (#141) 356 | * Symlink ruby.exe, so Rails 4 bins work for Windows (#139) 357 | 358 | ## v78 (8/28/2013) 359 | 360 | Features: 361 | 362 | * Don't add plugins if already gems 363 | 364 | Bugfixes: 365 | 366 | * Fix issue #127 Race condition with LPXC 367 | 368 | ## v77 (8/5/2013) 369 | 370 | Features: 371 | 372 | * Force nokogiri to compile with system libs 373 | 374 | ## v76 (7/29/2013) 375 | 376 | Bugfixes: 377 | 378 | * fix request_id for instrumentation to follow standard 379 | 380 | ## v75 (7/29/2013) 381 | 382 | Features: 383 | 384 | * add request_id to instrumentation 385 | * switchover to rubinius hosted rbx binaries 386 | 387 | Bugfixes: 388 | 389 | * OpenJDK version was rolled back, stop special casing JRuby 1.7.3. 390 | 391 | ## v74 (7/24/2013) 392 | 393 | Bugfixes: 394 | 395 | * Lock JRuby 1.7.3 and lower to older version of JDK due to 396 | 397 | ## v73 (7/23/2013) 398 | 399 | * Revert to v69 due to asset:precompile bugs 400 | 401 | ## v72 (7/23/2013) 402 | 403 | Bugfixes: 404 | 405 | * Fix rake task detection for Rails 3 (@hynkle, #118) 406 | 407 | ## v71 (7/18/2013) 408 | 409 | * Revert to v69 due to asset:precompile bugs 410 | 411 | ## v70 (7/18/2013) 412 | 413 | Bugfixes: 414 | 415 | * Don't silently fail rake task checks (@gabrielg, #34) 416 | 417 | ## v69 (7/16/2013) 418 | 419 | Bugfixes: 420 | 421 | * Add spacing to end of instrumentation 422 | 423 | ## v68 (7/16/2013) 424 | 425 | Features: 426 | 427 | * Log buildpack name and entering rails3/4 compile 428 | 429 | ## v67 (7/10/2013) 430 | 431 | Features: 432 | 433 | * Fetcher uses CDN if available 434 | * Add buildpack_version to the instrumentation output 435 | 436 | Bugfixes: 437 | 438 | * Don't print DEBUG messages for lxpc when env var is present 439 | * Fix ruby gemfile warning line for JRuby 440 | 441 | ## v66 (7/9/2013) 442 | 443 | Bugfixes: 444 | 445 | * Include logtoken properly 446 | 447 | ## v65 (7/9/2013) 448 | 449 | Features: 450 | 451 | * Instrument timing infrastructure for the buildpack 452 | 453 | Bugfixes: 454 | 455 | * Fix DATABASE_URL to use jdbc-postgres for JRuby (@jkrall, #116) 456 | 457 | ## v64 (6/19/2013) 458 | 459 | Features: 460 | 461 | * only download one copy of bundler per process (@dpiddy, #69) 462 | * roll up all warnings for end of push output 463 | * write database.yml for Rails 4 464 | 465 | Bugfixes: 466 | 467 | * fix sqlite3 error messaging detection 468 | 469 | ## v63 (6/17/2013) 470 | 471 | Features: 472 | 473 | * Lock default ruby if default ruby is used 474 | * Change default ruby to 2.0.0 475 | * Stop using the stack image ruby and always vendor ruby 476 | 477 | ## v62 (5/21/2013) 478 | 479 | Bugfixes: 480 | 481 | * Correctly detect asset manifest files in Rails 4 482 | * Fix jruby 1.8.7 bundler/psych require bug 483 | 484 | ## v61 (4/18/2013) 485 | 486 | Features: 487 | 488 | * Start caching the rubygems version used. 489 | 490 | Bugfixes: 491 | 492 | * Rebuild bundler cache if rubygems 2 is detected. Bugfixes in later rubygems. 493 | 494 | ## v60 (4/17/2013) 495 | 496 | Security: 497 | 498 | * Disable Java RMI Remote Classloading for CVE-2013-1537, 499 | 500 | ## v59 (4/4/2013) 501 | 502 | Bugfixes: 503 | 504 | * Change JVM S3 bucket 505 | 506 | ## v58 (3/19/2013) 507 | 508 | Bugfixes: 509 | 510 | * Fix ruby 1.8.7 not being able to compile native extensions 511 | 512 | ## v57 (3/18/2013) 513 | 514 | Bugfixes: 515 | 516 | * Fix git gemspec bug in bundler 517 | 518 | ## v56 (3/11/2013) 519 | 520 | Bugfixes: 521 | 522 | * Upgrade bundler to 1.3.2 to fix --dry-clean/Would have removed bug in bundle clean, part 2. 523 | 524 | ## v55 (3/7/2013) 525 | 526 | Bugfixes: 527 | 528 | * Revert back to Bundler 1.3.0.pre.5, see https://gist.github.com/mattonrails/e063caf86962995e7ba0 529 | 530 | ## v54 (3/7/2013) 531 | 532 | Bugfixes: 533 | 534 | * Upgrade bundler to 1.3.2 to fix --dry-clean/Would have removed bug in bundle clean 535 | 536 | ## v53 (3/6/2013) 537 | 538 | Bugfixes: 539 | 540 | * bin/detect for Rails 3 and 4 will use railties for detection vs the rails gem 541 | * bin/detect does not error out when Gemfile + Gemfile.lock are missing 542 | 543 | ## v52 (2/25/2013) 544 | 545 | Bugfixes: 546 | 547 | * Revert back to 1.3.0.pre.5 due to bundler warnings 548 | 549 | ## v51 (2/25/2013) 550 | 551 | Features: 552 | 553 | * Initial Rails 4 beta support 554 | * Upgrade bundler to 1.3.0 555 | 556 | Bugfixes: 557 | 558 | * Better buildpack detection through Gemfile.lock gems 559 | 560 | ## v50 (1/31/2013) 561 | 562 | Features: 563 | 564 | * Restore ruby deploys back to normal 565 | 566 | ## v49 (1/30/2013) 567 | 568 | Features: 569 | 570 | * Re-enable ruby deploys for apps just using the heroku cache 571 | * Display ruby version change when busting the cache 572 | 573 | ## v48 (1/30/2013) 574 | 575 | Features: 576 | 577 | * Update deploy error message copy to link to status incident. 578 | 579 | ## v47 (1/30/2013) 580 | 581 | Features: 582 | 583 | * Disable ruby deploys due to rubygems.org compromise 584 | 585 | ## v46 (1/10/2013) 586 | 587 | Features: 588 | 589 | * Upgrade Bundler to 1.3.0.pre.5 590 | * bundler binstubs now go in vendor/bundle/bin 591 | 592 | ## v45 (12/14/2012) 593 | 594 | Features: 595 | 596 | * Stop setting env vars in bin/release now that login-shell is released 597 | * Enable Invoke Dynamic on JRuby by default 598 | * GEM_PATH is now updated on each push 599 | 600 | ## v44 (12/14/2012) 601 | 602 | Faulty Release 603 | 604 | ## v43 (12/13/2012) 605 | 606 | Features: 607 | 608 | * Upgrade Bundler to 1.3.0.pre.2 609 | 610 | ## v42 (11/26/2012) 611 | 612 | Features: 613 | 614 | * Upgrade Bundler to 1.2.2 to fix Ruby 2.0.0/YAML issues 615 | 616 | ## v41 (11/1/2012) 617 | 618 | Features: 619 | 620 | * Enable ruby 2.0.0 support for testing 621 | 622 | ## v40 (10/14/2012) 623 | 624 | Features: 625 | 626 | * Cache version of the buildpack we used to deploy 627 | * Purge cache when v38 is detected 628 | 629 | ## v39 (10/14/2012) 630 | 631 | Bugfixes: 632 | 633 | * Don't display cache clearing message for new apps 634 | * Actually clear bundler cache on ruby version change 635 | 636 | ## v38 (10/14/2012) 637 | 638 | Bugfixes: 639 | 640 | * Stop bundle cache from continually growing 641 | 642 | ## v37 (10/12/2012) 643 | 644 | Bugfixes: 645 | 646 | * Remove temporary workaround from v36. 647 | * Clear bundler cache upon Ruby version change 648 | 649 | ## v36 (10/12/2012) 650 | 651 | Bugfixes: 652 | 653 | * Always clear the cache for ruby 1.9.3 as a temporary workaround due to the security upgrade 654 | 655 | ## v35 (9/19/2012) 656 | 657 | Features: 658 | 659 | * Upgrade to Bundler 1.2.1 660 | * Display bundle clean output 661 | * More resilent to rubygems.org API outages 662 | 663 | Bugfixes: 664 | 665 | * `bundle clean` works again 666 | 667 | ## v34 (8/30/2012) 668 | 669 | Features: 670 | 671 | * Upgrade to Bundler 1.2.0 672 | 673 | ## v33 (8/9/2012) 674 | 675 | Features: 676 | 677 | * Upgrade to Bundler 1.2.0.rc.2 678 | * vendor JDK7 for JRuby, but disable invoke dynamic 679 | 680 | ## v29 (7/19/2012) 681 | 682 | Features: 683 | 684 | * support .profile.d/ruby.sh 685 | * sync stdout so that the buildpack streams even in non-interactive shells 686 | * Upgrade to Bundler 1.2.0.rc 687 | 688 | ## v28 (7/16/2012) 689 | 690 | Features: 691 | 692 | * Vendor OpenJDK6 into slug when using JRuby 693 | * ruby version support for ruby 1.8.7 via bundler's ruby DSL 694 | 695 | Bugfixes: 696 | 697 | * sqlite3 error gets displayed again 698 | 699 | ## v27 (6/14/2012) 700 | 701 | Bugfixes: 702 | 703 | * Remove `vendor/bundle` message only appears when dir actually exists 704 | 705 | ## v26 (6/14/2012) 706 | 707 | Features: 708 | 709 | * print message when assets:precompile finishes successfully 710 | * Remove `vendor/bundle` if user commits it to their git repo. 711 | 712 | ## v25 (6/12/2012) 713 | 714 | Features: 715 | 716 | * support "ruby-xxx-jruby-yyy" for jruby detection packages 717 | 718 | ## v24 (6/7/2012) 719 | 720 | Features: 721 | 722 | * removes bundler cache in the slug, to minimize slug size (@stevenh512, #16) 723 | * optimize push time with caching 724 | 725 | ## v23 (5/8/2012) 726 | 727 | Bugfixes: 728 | 729 | * fix ruby version bug with "fatal:-Not-a-git-repository" 730 | 731 | ## v22 (5/7/2012) 732 | 733 | Features: 734 | 735 | * bundler 1.2.0.pre 736 | * ruby version support for ruby 1.9.2/1.9.3 via bundler's ruby DSL 737 | 738 | Deprecation: 739 | 740 | * ENV['RUBY_VERSION'] in favor of bundler's ruby DSL 741 | 742 | ## v21 (3/21/2012) 743 | 744 | Features: 745 | 746 | * bundler 1.1.2 747 | 748 | ## v20 (3/12/2012) 749 | 750 | Features: 751 | 752 | * bundler 1.1.0 \o/ 753 | 754 | ## v19 (1/25/2012) 755 | 756 | Bugfixes: 757 | 758 | * fix native extension building for rbx 2.0.0dev 759 | 760 | ## v18 (1/18/2012) 761 | 762 | Features: 763 | 764 | * JRuby support 765 | * rbx 2.0.0dev support 766 | 767 | Bugfixes: 768 | 769 | * force db password to be a string in the yaml file 770 | 771 | ## v17 (12/29/2011) 772 | 773 | Features: 774 | 775 | * bundler 1.1.rc.7 776 | 777 | ## v16 (12/29/2011) 778 | 779 | Features: 780 | 781 | * pass DATABASE_URL to rails 3.1 assets:precompile rake task detection 782 | 783 | ## v15 (12/27/2011) 784 | 785 | Features: 786 | 787 | * bundler 1.1.rc.6 788 | 789 | ## v14 (12/22/2011) 790 | 791 | Bugfixes: 792 | 793 | * stop freedom patching syck in ruby 1.9.3+ 794 | 795 | ## v13 (12/15/2011) 796 | 797 | Features: 798 | 799 | * bundler 1.1.rc.5 800 | 801 | ## v12 (12/13/2011) 802 | 803 | Bugfixes: 804 | 805 | * syck workaround for yaml/psych issues 806 | 807 | ## v11 (12/12/2011) 808 | 809 | Features: 810 | 811 | * bundler 1.1.rc.3 812 | 813 | ## v10 (11/23/2011) 814 | 815 | Features: 816 | 817 | * bundler binstubs 818 | * dynamic slug_vendor_base detection 819 | 820 | Bugfixes: 821 | 822 | * don't show sqlite3 error if it's in a bundle without group on failed bundle install 823 | 824 | ## v9 (11/14/2011) 825 | 826 | Features: 827 | 828 | * rbx 1.2.4 support 829 | * print out RUBY_VERSION being used 830 | 831 | Bugfixes: 832 | 833 | * don't leave behind ruby_versions.yml 834 | 835 | ## v8 (11/8/2011) 836 | 837 | Features: 838 | 839 | * use vm as part of RUBY_VERSION 840 | 841 | ## v7 (11/8/2011) 842 | 843 | Features: 844 | 845 | * ruby 1.9.3 support 846 | * specify ruby versions using RUBY_VERSION build var 847 | 848 | Bugfixes: 849 | 850 | * move "bin/" to the front of the PATH, so apps can override existing bins 851 | 852 | ## v6 (11/2/2011) 853 | 854 | Features: 855 | 856 | * add sqlite3 warning when detected on bundle install error 857 | 858 | Bugfixes: 859 | 860 | * Change gem detection to use lockfile parser 861 | * use `$RACK_ENV` when thin is detected for rack apps 862 | -------------------------------------------------------------------------------- /lib/language_pack/ruby.rb: -------------------------------------------------------------------------------- 1 | require "tmpdir" 2 | require "digest/md5" 3 | require "benchmark" 4 | require "rubygems" 5 | require "language_pack" 6 | require "language_pack/base" 7 | require "language_pack/ruby_version" 8 | require "language_pack/helpers/node_installer" 9 | require "language_pack/version" 10 | 11 | # base Ruby Language Pack. This is for any base ruby app. 12 | class LanguagePack::Ruby < LanguagePack::Base 13 | NAME = "ruby" 14 | LIBYAML_VERSION = "0.1.6" 15 | LIBYAML_PATH = "libyaml-#{LIBYAML_VERSION}" 16 | BUNDLER_VERSION = "1.6.3" 17 | BUNDLER_GEM_PATH = "bundler-#{BUNDLER_VERSION}" 18 | JVM_BASE_URL = "http://heroku-jdk.s3.amazonaws.com" 19 | LATEST_JVM_VERSION = "openjdk7-latest" 20 | LEGACY_JVM_VERSION = "openjdk1.7.0_25" 21 | DEFAULT_RUBY_VERSION = "ruby-2.0.0" 22 | RBX_BASE_URL = "http://binaries.rubini.us/heroku" 23 | NODE_BP_PATH = "vendor/node/bin" 24 | 25 | # detects if this is a valid Ruby app 26 | # @return [Boolean] true if it's a Ruby app 27 | def self.use? 28 | instrument "ruby.use" do 29 | File.exist?("Gemfile") 30 | end 31 | end 32 | 33 | def self.bundler 34 | @bundler ||= LanguagePack::Helpers::BundlerWrapper.new.install 35 | end 36 | 37 | def bundler 38 | self.class.bundler 39 | end 40 | 41 | def initialize(build_path, cache_path=nil) 42 | super(build_path, cache_path) 43 | @fetchers[:mri] = LanguagePack::Fetcher.new(VENDOR_URL, @stack) 44 | @fetchers[:jvm] = LanguagePack::Fetcher.new(JVM_BASE_URL) 45 | @fetchers[:rbx] = LanguagePack::Fetcher.new(RBX_BASE_URL, @stack) 46 | @node_installer = LanguagePack::NodeInstaller.new(@stack) 47 | end 48 | 49 | def name 50 | "Ruby" 51 | end 52 | 53 | def default_addons 54 | instrument "ruby.default_addons" do 55 | add_dev_database_addon 56 | end 57 | end 58 | 59 | def default_config_vars 60 | instrument "ruby.default_config_vars" do 61 | vars = { 62 | "LANG" => env("LANG") || "en_US.UTF-8" 63 | } 64 | 65 | ruby_version.jruby? ? vars.merge({ 66 | "JAVA_OPTS" => default_java_opts, 67 | "JRUBY_OPTS" => default_jruby_opts, 68 | "JAVA_TOOL_OPTIONS" => default_java_tool_options 69 | }) : vars 70 | end 71 | end 72 | 73 | def default_process_types 74 | instrument "ruby.default_process_types" do 75 | { 76 | "rake" => "bundle exec rake", 77 | "console" => "bundle exec irb" 78 | } 79 | end 80 | end 81 | 82 | def compile 83 | instrument 'ruby.compile' do 84 | # check for new app at the beginning of the compile 85 | new_app? 86 | Dir.chdir(build_path) 87 | remove_vendor_bundle 88 | install_ruby 89 | install_jvm 90 | setup_language_pack_environment 91 | setup_profiled 92 | allow_git do 93 | install_bundler_in_app 94 | build_bundler 95 | post_bundler 96 | create_database_yml 97 | install_binaries 98 | run_assets_precompile_rake_task 99 | end 100 | super 101 | end 102 | end 103 | 104 | private 105 | 106 | # the base PATH environment variable to be used 107 | # @return [String] the resulting PATH 108 | def default_path 109 | # need to remove bin/ folder since it links 110 | # to the wrong --prefix ruby binstubs 111 | # breaking require. This only applies to Ruby 1.9.2 and 1.8.7. 112 | safe_binstubs = binstubs_relative_paths - ["bin"] 113 | paths = [ 114 | ENV["PATH"], 115 | "bin", 116 | system_paths, 117 | ] 118 | paths.unshift("#{slug_vendor_jvm}/bin") if ruby_version.jruby? 119 | paths.unshift(safe_binstubs) 120 | 121 | paths.join(":") 122 | end 123 | 124 | def binstubs_relative_paths 125 | [ 126 | "bin", 127 | bundler_binstubs_path, 128 | "#{slug_vendor_base}/bin" 129 | ] 130 | end 131 | 132 | def system_paths 133 | "/usr/local/bin:/usr/bin:/bin" 134 | end 135 | 136 | # the relative path to the bundler directory of gems 137 | # @return [String] resulting path 138 | def slug_vendor_base 139 | instrument 'ruby.slug_vendor_base' do 140 | if @slug_vendor_base 141 | @slug_vendor_base 142 | elsif ruby_version.ruby_version == "1.8.7" 143 | @slug_vendor_base = "vendor/bundle/1.8" 144 | else 145 | @slug_vendor_base = run_no_pipe(%q(ruby -e "require 'rbconfig';puts \"vendor/bundle/#{RUBY_ENGINE}/#{RbConfig::CONFIG['ruby_version']}\"")).chomp 146 | error "Problem detecting bundler vendor directory: #{@slug_vendor_base}" unless $?.success? 147 | @slug_vendor_base 148 | end 149 | end 150 | end 151 | 152 | # the relative path to the vendored ruby directory 153 | # @return [String] resulting path 154 | def slug_vendor_ruby 155 | "vendor/#{ruby_version.version_without_patchlevel}" 156 | end 157 | 158 | # the relative path to the vendored jvm 159 | # @return [String] resulting path 160 | def slug_vendor_jvm 161 | "vendor/jvm" 162 | end 163 | 164 | # the absolute path of the build ruby to use during the buildpack 165 | # @return [String] resulting path 166 | def build_ruby_path 167 | "/tmp/#{ruby_version.version_without_patchlevel}" 168 | end 169 | 170 | # fetch the ruby version from bundler 171 | # @return [String, nil] returns the ruby version if detected or nil if none is detected 172 | def ruby_version 173 | instrument 'ruby.ruby_version' do 174 | return @ruby_version if @ruby_version 175 | new_app = !File.exist?("vendor/heroku") 176 | last_version_file = "buildpack_ruby_version" 177 | last_version = nil 178 | last_version = @metadata.read(last_version_file).chomp if @metadata.exists?(last_version_file) 179 | 180 | @ruby_version = LanguagePack::RubyVersion.new(bundler.ruby_version, 181 | is_new: new_app, 182 | last_version: last_version) 183 | return @ruby_version 184 | end 185 | end 186 | 187 | # default JAVA_OPTS 188 | # return [String] string of JAVA_OPTS 189 | def default_java_opts 190 | "-Xmx384m -Xss512k -XX:+UseCompressedOops -Dfile.encoding=UTF-8" 191 | end 192 | 193 | # default JRUBY_OPTS 194 | # return [String] string of JRUBY_OPTS 195 | def default_jruby_opts 196 | "-Xcompile.invokedynamic=false" 197 | end 198 | 199 | # default JAVA_TOOL_OPTIONS 200 | # return [String] string of JAVA_TOOL_OPTIONS 201 | def default_java_tool_options 202 | "-Djava.rmi.server.useCodebaseOnly=true" 203 | end 204 | 205 | # list the available valid ruby versions 206 | # @note the value is memoized 207 | # @return [Array] list of Strings of the ruby versions available 208 | def ruby_versions 209 | return @ruby_versions if @ruby_versions 210 | 211 | Dir.mktmpdir("ruby_versions-") do |tmpdir| 212 | Dir.chdir(tmpdir) do 213 | @fetchers[:buildpack].fetch("ruby_versions.yml") 214 | @ruby_versions = YAML::load_file("ruby_versions.yml") 215 | end 216 | end 217 | 218 | @ruby_versions 219 | end 220 | 221 | # sets up the environment variables for the build process 222 | def setup_language_pack_environment 223 | instrument 'ruby.setup_language_pack_environment' do 224 | ENV["PATH"] += ":bin" if ruby_version.jruby? 225 | setup_ruby_install_env 226 | ENV["PATH"] += ":#{node_bp_bin_path}" if node_js_installed? 227 | 228 | # TODO when buildpack-env-args rolls out, we can get rid of 229 | # ||= and the manual setting below 230 | config_vars = default_config_vars.each do |key, value| 231 | ENV[key] ||= value 232 | end 233 | 234 | ENV["GEM_PATH"] = slug_vendor_base 235 | ENV["GEM_HOME"] = slug_vendor_base 236 | ENV["PATH"] = default_path 237 | end 238 | end 239 | 240 | # sets up the profile.d script for this buildpack 241 | def setup_profiled 242 | instrument 'setup_profiled' do 243 | set_env_override "GEM_PATH", "$HOME/#{slug_vendor_base}:$GEM_PATH" 244 | set_env_default "LANG", "en_US.UTF-8" 245 | set_env_override "PATH", binstubs_relative_paths.map {|path| "$HOME/#{path}" }.join(":") + ":$PATH" 246 | 247 | if ruby_version.jruby? 248 | set_env_default "JAVA_OPTS", default_java_opts 249 | set_env_default "JRUBY_OPTS", default_jruby_opts 250 | set_env_default "JAVA_TOOL_OPTIONS", default_java_tool_options 251 | end 252 | end 253 | end 254 | 255 | # install the vendored ruby 256 | # @return [Boolean] true if it installs the vendored ruby and false otherwise 257 | def install_ruby 258 | instrument 'ruby.install_ruby' do 259 | return false unless ruby_version 260 | 261 | invalid_ruby_version_message = <= Gem::Version.new("1.7.4") 342 | LATEST_JVM_VERSION 343 | else 344 | LEGACY_JVM_VERSION 345 | end 346 | 347 | topic "Installing JVM: #{jvm_version}" 348 | 349 | FileUtils.mkdir_p(slug_vendor_jvm) 350 | Dir.chdir(slug_vendor_jvm) do 351 | @fetchers[:jvm].fetch_untar("#{jvm_version}.tar.gz") 352 | end 353 | 354 | bin_dir = "bin" 355 | FileUtils.mkdir_p bin_dir 356 | Dir["#{slug_vendor_jvm}/bin/*"].each do |bin| 357 | run("ln -s ../#{bin} #{bin_dir}") 358 | end 359 | 360 | install_jce 361 | end 362 | end 363 | end 364 | 365 | def install_jce 366 | topic "Installing JCE" 367 | jce_path = File.expand_path('../../../vendor/jce', __FILE__) 368 | run("cp #{jce_path}/*.jar #{slug_vendor_jvm}/jre/lib/security/") 369 | end 370 | 371 | # find the ruby install path for its binstubs during build 372 | # @return [String] resulting path or empty string if ruby is not vendored 373 | def ruby_install_binstub_path 374 | @ruby_install_binstub_path ||= 375 | if ruby_version.build? 376 | "#{build_ruby_path}/bin" 377 | elsif ruby_version 378 | "#{slug_vendor_ruby}/bin" 379 | else 380 | "" 381 | end 382 | end 383 | 384 | # setup the environment so we can use the vendored ruby 385 | def setup_ruby_install_env 386 | instrument 'ruby.setup_ruby_install_env' do 387 | ENV["PATH"] = "#{ruby_install_binstub_path}:#{ENV["PATH"]}" 388 | 389 | if ruby_version.jruby? 390 | ENV['JAVA_OPTS'] = default_java_opts 391 | end 392 | end 393 | end 394 | 395 | # installs vendored gems into the slug 396 | def install_bundler_in_app 397 | instrument 'ruby.install_language_pack_gems' do 398 | FileUtils.mkdir_p(slug_vendor_base) 399 | Dir.chdir(slug_vendor_base) do |dir| 400 | `cp -R #{bundler.bundler_path}/. .` 401 | end 402 | end 403 | end 404 | 405 | # default set of binaries to install 406 | # @return [Array] resulting list 407 | def binaries 408 | add_node_js_binary 409 | end 410 | 411 | # vendors binaries into the slug 412 | def install_binaries 413 | instrument 'ruby.install_binaries' do 414 | binaries.each {|binary| install_binary(binary) } 415 | Dir["bin/*"].each {|path| run("chmod +x #{path}") } 416 | end 417 | end 418 | 419 | # vendors individual binary into the slug 420 | # @param [String] name of the binary package from S3. 421 | # Example: https://s3.amazonaws.com/language-pack-ruby/node-0.4.7.tgz, where name is "node-0.4.7" 422 | def install_binary(name) 423 | bin_dir = "bin" 424 | FileUtils.mkdir_p bin_dir 425 | Dir.chdir(bin_dir) do |dir| 426 | if name.match(/^node\-/) 427 | @node_installer.install 428 | else 429 | @fetchers[:buildpack].fetch_untar("#{name}.tgz") 430 | end 431 | end 432 | end 433 | 434 | # removes a binary from the slug 435 | # @param [String] relative path of the binary on the slug 436 | def uninstall_binary(path) 437 | FileUtils.rm File.join('bin', File.basename(path)), :force => true 438 | end 439 | 440 | def load_default_cache? 441 | new_app? && ruby_version.default? 442 | end 443 | 444 | # loads a default bundler cache for new apps to speed up initial bundle installs 445 | def load_default_cache 446 | instrument "ruby.load_default_cache" do 447 | if false # load_default_cache? 448 | puts "New app detected loading default bundler cache" 449 | patchlevel = run("ruby -e 'puts RUBY_PATCHLEVEL'").chomp 450 | cache_name = "#{DEFAULT_RUBY_VERSION}-p#{patchlevel}-default-cache" 451 | @fetchers[:buildpack].fetch_untar("#{cache_name}.tgz") 452 | end 453 | end 454 | end 455 | 456 | # install libyaml into the LP to be referenced for psych compilation 457 | # @param [String] tmpdir to store the libyaml files 458 | def install_libyaml(dir) 459 | instrument 'ruby.install_libyaml' do 460 | FileUtils.mkdir_p dir 461 | Dir.chdir(dir) do |dir| 462 | @fetchers[:buildpack].fetch_untar("#{LIBYAML_PATH}.tgz") 463 | end 464 | end 465 | end 466 | 467 | # remove `vendor/bundle` that comes from the git repo 468 | # in case there are native ext. 469 | # users should be using `bundle pack` instead. 470 | # https://github.com/heroku/heroku-buildpack-ruby/issues/21 471 | def remove_vendor_bundle 472 | if File.exists?("vendor/bundle") 473 | warn(< "#{pwd}/Gemfile", 531 | "BUNDLE_CONFIG" => "#{pwd}/.bundle/config", 532 | "CPATH" => noshellescape("#{yaml_include}:$CPATH"), 533 | "CPPATH" => noshellescape("#{yaml_include}:$CPPATH"), 534 | "LIBRARY_PATH" => noshellescape("#{yaml_lib}:$LIBRARY_PATH"), 535 | "RUBYOPT" => syck_hack, 536 | "NOKOGIRI_USE_SYSTEM_LIBRARIES" => "true" 537 | } 538 | env_vars["BUNDLER_LIB_PATH"] = "#{bundler_path}" if ruby_version.ruby_version == "1.8.7" 539 | puts "Running: #{bundle_command}" 540 | instrument "ruby.bundle_install" do 541 | bundle_time = Benchmark.realtime do 542 | bundler_output << pipe("#{bundle_command} --no-clean", out: "2>&1", env: env_vars, user_env: true) 543 | end 544 | end 545 | end 546 | 547 | if $?.success? 548 | puts "Bundle completed (#{"%.2f" % bundle_time}s)" 549 | log "bundle", :status => "success" 550 | puts "Cleaning up the bundler cache." 551 | instrument "ruby.bundle_clean" do 552 | # Only show bundle clean output when not using default cache 553 | if load_default_cache? 554 | run "bundle clean > /dev/null" 555 | else 556 | pipe("#{bundle_bin} clean", out: "2> /dev/null") 557 | end 558 | end 559 | cache.store ".bundle" 560 | @bundler_cache.store 561 | 562 | # Keep gem cache out of the slug 563 | FileUtils.rm_rf("#{slug_vendor_base}/cache") 564 | else 565 | log "bundle", :status => "failure" 566 | error_message = "Failed to install gems via Bundler." 567 | puts "Bundler Output: #{bundler_output}" 568 | if bundler_output.match(/An error occurred while installing sqlite3/) 569 | error_message += < 654 | 655 | <%= ENV["RAILS_ENV"] || ENV["RACK_ENV"] %>: 656 | <%= attribute "adapter", adapter %> 657 | <%= attribute "database", database %> 658 | <%= attribute "username", username %> 659 | <%= attribute "password", password, true %> 660 | <%= attribute "host", host %> 661 | <%= attribute "port", port %> 662 | 663 | <% params.each do |key, value| %> 664 | <%= key %>: <%= value.first %> 665 | <% end %> 666 | DATABASE_YML 667 | end 668 | end 669 | end 670 | end 671 | 672 | def rake 673 | @rake ||= begin 674 | LanguagePack::Helpers::RakeRunner.new( 675 | bundler.has_gem?("rake") || ruby_version.rake_is_vendored? 676 | ).load_rake_tasks!(env: rake_env) 677 | end 678 | end 679 | 680 | def rake_env 681 | if database_url 682 | { "DATABASE_URL" => database_url } 683 | else 684 | {} 685 | end.merge(user_env_hash) 686 | end 687 | 688 | def database_url 689 | env("DATABASE_URL") if env("DATABASE_URL") 690 | end 691 | 692 | # executes the block with GIT_DIR environment variable removed since it can mess with the current working directory git thinks it's in 693 | # @param [block] block to be executed in the GIT_DIR free context 694 | def allow_git(&blk) 695 | git_dir = ENV.delete("GIT_DIR") # can mess with bundler 696 | blk.call 697 | ENV["GIT_DIR"] = git_dir 698 | end 699 | 700 | # decides if we need to enable the dev database addon 701 | # @return [Array] the database addon if the pg gem is detected or an empty Array if it isn't. 702 | def add_dev_database_addon 703 | bundler.has_gem?("pg") ? ['heroku-postgresql:hobby-dev'] : [] 704 | end 705 | 706 | # decides if we need to install the node.js binary 707 | # @note execjs will blow up if no JS RUNTIME is detected and is loaded. 708 | # @return [Array] the node.js binary path if we need it or an empty Array 709 | def add_node_js_binary 710 | bundler.has_gem?('execjs') && !node_js_installed? ? [@node_installer.binary_path] : [] 711 | end 712 | 713 | def node_bp_bin_path 714 | "#{Dir.pwd}/#{NODE_BP_PATH}" 715 | end 716 | 717 | # checks if node.js is installed via the official heroku-buildpack-nodejs using multibuildpack 718 | # @return [Boolean] true if it's detected and false if it isn't 719 | def node_js_installed? 720 | @node_js_installed ||= run("#{node_bp_bin_path}/node -v") && $?.success? 721 | end 722 | 723 | def run_assets_precompile_rake_task 724 | instrument 'ruby.run_assets_precompile_rake_task' do 725 | 726 | precompile = rake.task("assets:precompile") 727 | return true unless precompile.is_defined? 728 | 729 | topic "Precompiling assets" 730 | precompile.invoke(env: rake_env) 731 | if precompile.success? 732 | puts "Asset precompilation completed (#{"%.2f" % precompile.time}s)" 733 | else 734 | precompile_fail(precompile.output) 735 | end 736 | end 737 | end 738 | 739 | def precompile_fail(output) 740 | log "assets_precompile", :status => "failure" 741 | msg = "Precompiling assets failed.\n" 742 | if output.match(/(127\.0\.0\.1)|(org\.postgresql\.util)/) 743 | msg << "Attempted to access a nonexistent database:\n" 744 | msg << "https://devcenter.heroku.com/articles/pre-provision-database\n" 745 | end 746 | error msg 747 | end 748 | 749 | def bundler_cache 750 | "vendor/bundle" 751 | end 752 | 753 | def load_bundler_cache 754 | instrument "ruby.load_bundler_cache" do 755 | cache.load "vendor" 756 | 757 | full_ruby_version = run_stdout(%q(ruby -v)).chomp 758 | rubygems_version = run_stdout(%q(gem -v)).chomp 759 | heroku_metadata = "vendor/heroku" 760 | old_rubygems_version = nil 761 | ruby_version_cache = "ruby_version" 762 | buildpack_version_cache = "buildpack_version" 763 | bundler_version_cache = "bundler_version" 764 | rubygems_version_cache = "rubygems_version" 765 | stack_cache = "stack" 766 | 767 | old_rubygems_version = @metadata.read(ruby_version_cache).chomp if @metadata.exists?(ruby_version_cache) 768 | old_stack = @metadata.read(stack_cache).chomp if @metadata.exists?(stack_cache) 769 | old_stack ||= DEFAULT_LEGACY_STACK 770 | 771 | stack_change = old_stack != @stack 772 | convert_stack = @bundler_cache.old? 773 | @bundler_cache.convert_stack(stack_change) if convert_stack 774 | if !new_app? && stack_change 775 | puts "Purging Cache. Changing stack from #{old_stack} to #{@stack}" 776 | purge_bundler_cache(old_stack) 777 | elsif !new_app? && !convert_stack 778 | @bundler_cache.load 779 | end 780 | 781 | # fix bug from v37 deploy 782 | if File.exists?("vendor/ruby_version") 783 | puts "Broken cache detected. Purging build cache." 784 | cache.clear("vendor") 785 | FileUtils.rm_rf("vendor/ruby_version") 786 | purge_bundler_cache 787 | # fix bug introduced in v38 788 | elsif !@metadata.exists?(buildpack_version_cache) && @metadata.exists?(ruby_version_cache) 789 | puts "Broken cache detected. Purging build cache." 790 | purge_bundler_cache 791 | elsif (@bundler_cache.exists? || @bundler_cache.old?) && @metadata.exists?(ruby_version_cache) && full_ruby_version != @metadata.read(ruby_version_cache).chomp 792 | puts "Ruby version change detected. Clearing bundler cache." 793 | puts "Old: #{@metadata.read(ruby_version_cache).chomp}" 794 | puts "New: #{full_ruby_version}" 795 | purge_bundler_cache 796 | end 797 | 798 | # fix git gemspec bug from Bundler 1.3.0+ upgrade 799 | if File.exists?(bundler_cache) && !@metadata.exists?(bundler_version_cache) && !run("find vendor/bundle/*/*/bundler/gems/*/ -name *.gemspec").include?("No such file or directory") 800 | puts "Old bundler cache detected. Clearing bundler cache." 801 | purge_bundler_cache 802 | end 803 | 804 | # fix for https://github.com/heroku/heroku-buildpack-ruby/issues/86 805 | if (!@metadata.exists?(rubygems_version_cache) || 806 | (old_rubygems_version == "2.0.0" && old_rubygems_version != rubygems_version)) && 807 | @metadata.exists?(ruby_version_cache) && @metadata.read(ruby_version_cache).chomp.include?("ruby 2.0.0p0") 808 | puts "Updating to rubygems #{rubygems_version}. Clearing bundler cache." 809 | purge_bundler_cache 810 | end 811 | 812 | # fix for https://github.com/sparklemotion/nokogiri/issues/923 813 | if @metadata.exists?(buildpack_version_cache) && (bv = @metadata.read(buildpack_version_cache).sub('v', '').to_i) && bv != 0 && bv <= 76 814 | puts "Fixing nokogiri install. Clearing bundler cache." 815 | puts "See https://github.com/sparklemotion/nokogiri/issues/923." 816 | purge_bundler_cache 817 | end 818 | 819 | # recompile nokogiri to use new libyaml 820 | if @metadata.exists?(buildpack_version_cache) && (bv = @metadata.read(buildpack_version_cache).sub('v', '').to_i) && bv != 0 && bv <= 99 && bundler.has_gem?("psych") 821 | puts "Need to recompile psych for CVE-2013-6393. Clearing bundler cache." 822 | puts "See http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=737076." 823 | purge_bundler_cache 824 | end 825 | 826 | FileUtils.mkdir_p(heroku_metadata) 827 | @metadata.write(ruby_version_cache, full_ruby_version, false) 828 | @metadata.write(buildpack_version_cache, BUILDPACK_VERSION, false) 829 | @metadata.write(bundler_version_cache, BUNDLER_VERSION, false) 830 | @metadata.write(rubygems_version_cache, rubygems_version, false) 831 | @metadata.write(stack_cache, @stack, false) 832 | @metadata.save 833 | end 834 | end 835 | 836 | def purge_bundler_cache(stack = nil) 837 | instrument "ruby.purge_bundler_cache" do 838 | @bundler_cache.clear(stack) 839 | # need to reinstall language pack gems 840 | install_bundler_in_app 841 | end 842 | end 843 | end 844 | --------------------------------------------------------------------------------