├── config └── cdn.yml ├── .gitignore ├── lib ├── language_pack │ ├── version.rb │ ├── no_lockfile.rb │ ├── disable_deploys.rb │ ├── metadata.rb │ ├── helpers │ │ ├── stale_file_cleaner.rb │ │ ├── plugin_installer.rb │ │ ├── rake_runner.rb │ │ └── bundler_wrapper.rb │ ├── fetcher.rb │ ├── rack.rb │ ├── cache.rb │ ├── instrument.rb │ ├── rails2.rb │ ├── shell_helpers.rb │ ├── ruby_version.rb │ ├── rails4.rb │ ├── rails3.rb │ ├── base.rb │ └── ruby.rb └── language_pack.rb ├── bin ├── release ├── detect └── compile ├── vendor ├── dotenv.rb ├── syck_hack.rb └── lpxc.rb ├── spec ├── default_cache_spec.rb ├── rails23_spec.rb ├── no_lockfile_spec.rb ├── helpers │ ├── fetcher_spec.rb │ ├── bundler_wrapper_spec.rb │ ├── stale_file_cleaner_spec.rb │ ├── rake_runner_spec.rb │ └── ruby_version_spec.rb ├── user_env_compile_edge_case_spec.rb ├── gem_detect_errors_spec.rb ├── bugs_spec.rb ├── ruby_spec.rb ├── spec_helper.rb ├── rails4_spec.rb ├── rubies_spec.rb └── rails3_spec.rb ├── Gemfile ├── .travis.yml ├── LICENSE ├── hatchet.json ├── Gemfile.lock ├── support └── s3 │ ├── hmac │ └── s3 ├── README.md ├── Rakefile └── CHANGELOG.md /config/cdn.yml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | repos/* 2 | .DS_Store 3 | vendor/bundler/* 4 | vendor/bundle/* 5 | .env 6 | .ruby-version 7 | buildpacks/* 8 | -------------------------------------------------------------------------------- /lib/language_pack/version.rb: -------------------------------------------------------------------------------- 1 | require "language_pack/base" 2 | 3 | module LanguagePack 4 | class LanguagePack::Base 5 | BUILDPACK_VERSION = "v91" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/default_cache_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Default Cache" do 4 | it "gets loaded successfully" do 5 | Hatchet::Runner.new("default_ruby").deploy do |app| 6 | expect(app.output).to match("loading default bundler cache") 7 | end 8 | end 9 | end 10 | 11 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | 9 | LanguagePack::Instrument.trace 'compile', 'app.compile' do 10 | if pack = LanguagePack.detect(ARGV[0], ARGV[1]) 11 | pack.topic("Compiling #{pack.name}") 12 | pack.log("compile") do 13 | pack.compile 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /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/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", labs: "user-env-compile") 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 | end 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /spec/helpers/bundler_wrapper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "BundlerWrapper" do 4 | 5 | after(:each) do 6 | FileUtils.remove_entry_secure("tmp") if Dir.exist?("tmp") 7 | end 8 | 9 | it "detects windows gemfiles" do 10 | Hatchet::App.new("rails4_windows_mri193").in_directory do |dir| 11 | @bundler = LanguagePack::Helpers::BundlerWrapper.new(gemfile_path: "./Gemfile") 12 | expect(@bundler.windows_gemfile_lock?).to be_true 13 | end 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /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 ~/.ssh/id_rsa 13 | 14 | env: 15 | global: 16 | - HATCHET_RETRIES=3 17 | - IS_RUNNING_ON_TRAVIS=true 18 | - HATCHET_DEPLOY_STRATEGY=git 19 | # sets the HEROKU_API_KEY to a dummy user for Heroku deploys 20 | - secure: |- 21 | ccqb7fKumq2+VEkSrkCqmLjALj5R/ZDgAQGZULSFYeD0O0man3hezUEbMB53 22 | U+7gLkbqz5saFX9S5Sz26f99vmhWz1bMHkC2UtlyFSlgaqSvXMZNHJAweqRY 23 | ptqugqtvT0VVl1g3DInVgjDZhjINICv/CIorNuHijOFanP1Zmxg= 24 | -------------------------------------------------------------------------------- /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 | end 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/ruby_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | describe "Ruby apps" do 4 | describe "Rake detection" do 5 | context "Ruby 1.8.7" do 6 | it "doesn't run rake tasks if no rake gem" do 7 | Hatchet::Runner.new('mri_187_no_rake').deploy do |app, heroku| 8 | expect(app.output).not_to include("foo") 9 | end 10 | end 11 | 12 | it "runs a rake task if the gem exists" do 13 | Hatchet::Runner.new('mri_187_rake').deploy do |app, heroku| 14 | expect(app.output).to include("foo") 15 | end 16 | end 17 | end 18 | 19 | context "Ruby 1.9+" do 20 | it "runs rake tasks if no rake gem" do 21 | Hatchet::Runner.new('mri_200_no_rake').deploy do |app, heroku| 22 | expect(app.output).to include("foo") 23 | end 24 | end 25 | 26 | it "runs a rake task if the gem exists" do 27 | Hatchet::Runner.new('mri_200_rake').deploy do |app, heroku| 28 | expect(app.output).to include("foo") 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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) 10 | @config = load_config 11 | @host_url = fetch_cdn(host_url) 12 | end 13 | 14 | def fetch(path) 15 | curl = curl_command("-O #{@host_url.join(path)}") 16 | run!(curl) 17 | end 18 | 19 | def fetch_untar(path) 20 | curl = curl_command("#{@host_url.join(path)} -s -o") 21 | run!("#{curl} - | tar zxf -") 22 | end 23 | 24 | def fetch_bunzip2(path) 25 | curl = curl_command("#{@host_url.join(path)} -s -o") 26 | run!("#{curl} - | tar jxf -") 27 | end 28 | 29 | private 30 | def curl_command(command) 31 | "set -o pipefail; curl --fail --retry 3 --retry-delay 1 --connect-timeout 3 --max-time #{curl_timeout_in_seconds} #{command}" 32 | end 33 | 34 | def curl_timeout_in_seconds 35 | ENV['CURL_TIMEOUT'] || 30 36 | end 37 | 38 | def load_config 39 | YAML.load_file(CDN_YAML_FILE) || {} 40 | end 41 | 42 | def fetch_cdn(url) 43 | url = @config[url] || url 44 | Pathname.new(url) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /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" => "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 | -------------------------------------------------------------------------------- /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, 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/no_lockfile" 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /hatchet.json: -------------------------------------------------------------------------------- 1 | { 2 | "rake": [ 3 | "sharpstone/asset_precompile_fail", 4 | "sharpstone/asset_precompile_pass", 5 | "sharpstone/asset_precompile_not_found", 6 | "sharpstone/no_rakefile", 7 | "sharpstone/bad_rakefile", 8 | "sharpstone/mri_187_no_rake", 9 | "sharpstone/mri_187_rake", 10 | "sharpstone/mri_200_no_rake", 11 | "sharpstone/mri_200_rake" 12 | ], 13 | "bundler": [ 14 | "sharpstone/bad_gemfile_on_platform", 15 | "sharpstone/git_gemspec", 16 | "sharpstone/no_lockfile", 17 | "sharpstone/sqlite3_gemfile", 18 | "sharpstone/nokogiri_160" 19 | ], 20 | "ruby": [ 21 | "sharpstone/mri_187", 22 | "sharpstone/ruby_193_jruby_173", 23 | "sharpstone/ruby_193_jruby_176" 24 | ], 25 | "rack": [ 26 | "sharpstone/default_ruby", 27 | "sharpstone/mri_187_nokogiri", 28 | "sharpstone/mri_192", 29 | "sharpstone/mri_193", 30 | "sharpstone/mri_200", 31 | "sharpstone/mri_210" 32 | ], 33 | "rails2": [ 34 | "sharpstone/rails23_mri_187" 35 | ], 36 | "rails3": [ 37 | "sharpstone/rails3_mri_193", 38 | "sharpstone/railties3_mri_193", 39 | "sharpstone/rails3_12factor", 40 | "sharpstone/rails3_one_plugin", 41 | "sharpstone/rails3_runtime_assets", 42 | "sharpstone/rails3-fail-assets-compile" 43 | ], 44 | "rails4": [ 45 | "sharpstone/rails4-manifest", 46 | "sharpstone/rails3-to-4-no-bin", 47 | "sharpstone/rails4_windows_mri193", 48 | "sharpstone/rails4-fail-assets-compile" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /lib/language_pack/cache.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | require "language_pack" 3 | 4 | class LanguagePack::Cache 5 | def initialize(cache_path) 6 | @cache_base = Pathname.new(cache_path) 7 | end 8 | 9 | # removes the the specified 10 | # @param [String] relative path from the cache_base 11 | def clear(path) 12 | target = (@cache_base + path) 13 | target.exist? && target.rmtree 14 | end 15 | 16 | # write cache contents 17 | # @param [String] path of contents to store. it will be stored using this a relative path from the cache_base. 18 | # @param [Boolean] defaults to true. if set to true, the cache store directory will be cleared before writing to it. 19 | def store(path, clear_first=true) 20 | clear(path) if clear_first 21 | copy path, (@cache_base + path) 22 | end 23 | 24 | # load cache contents 25 | # @param [String] relative path of the cache contents 26 | def load(path) 27 | copy (@cache_base + path), path 28 | end 29 | 30 | # copy cache contents 31 | # @param [String] source directory 32 | # @param [String] destination directory 33 | def copy(from, to) 34 | return false unless File.exist?(from) 35 | FileUtils.mkdir_p File.dirname(to) 36 | system("cp -a #{from}/. #{to}") 37 | end 38 | 39 | # check if the cache content exists 40 | # @param [String] relative path of the cache contents 41 | # @param [Boolean] true if the path exists in the cache and false if otherwise 42 | def exists?(path) 43 | File.exists?(@cache_base + path) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.0.2) 12 | i18n (~> 0.6, >= 0.6.4) 13 | minitest (~> 4.2) 14 | multi_json (~> 1.3) 15 | thread_safe (~> 0.1) 16 | tzinfo (~> 0.3.37) 17 | anvil-cli (0.16.1) 18 | progress (~> 2.4.0) 19 | rest-client (~> 1.6.7) 20 | thor (~> 0.15.2) 21 | atomic (1.1.14) 22 | diff-lcs (1.1.3) 23 | excon (0.31.0) 24 | heroku-api (0.3.16) 25 | excon (~> 0.27) 26 | multi_json (~> 1.8.2) 27 | heroku_hatchet (1.1.7) 28 | activesupport 29 | anvil-cli 30 | excon 31 | heroku-api 32 | repl_runner 33 | rrrretry 34 | thor 35 | i18n (0.6.9) 36 | mime-types (2.0) 37 | minitest (4.7.5) 38 | multi_json (1.8.4) 39 | netrc (0.7.7) 40 | parallel (0.6.5) 41 | parallel_tests (0.13.1) 42 | parallel 43 | progress (2.4.0) 44 | rake (10.0.4) 45 | repl_runner (0.0.2) 46 | activesupport 47 | rest-client (1.6.7) 48 | mime-types (>= 1.16) 49 | rrrretry (1.0.0) 50 | rspec (2.2.0) 51 | rspec-core (~> 2.2) 52 | rspec-expectations (~> 2.2) 53 | rspec-mocks (~> 2.2) 54 | rspec-core (2.13.1) 55 | rspec-expectations (2.12.1) 56 | diff-lcs (~> 1.1.3) 57 | rspec-mocks (2.13.1) 58 | rspec-retry (0.2.1) 59 | rspec 60 | thor (0.15.4) 61 | thread_safe (0.1.3) 62 | atomic 63 | tzinfo (0.3.38) 64 | 65 | PLATFORMS 66 | ruby 67 | 68 | DEPENDENCIES 69 | excon 70 | git! 71 | heroku_hatchet 72 | netrc 73 | parallel_tests 74 | rake 75 | rspec-core 76 | rspec-expectations 77 | rspec-retry 78 | -------------------------------------------------------------------------------- /spec/rails4_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | describe "Rails 4.x" do 4 | it "should deploy on ruby 2.0.0" do 5 | Hatchet::Runner.new("rails4-manifest").deploy do |app, heroku| 6 | add_database(app, heroku) 7 | expect(app.output).to include("Detected manifest file, assuming assets were compiled locally") 8 | expect(app.output).not_to match("Include 'rails_12factor' gem to enable all platform features") 9 | end 10 | end 11 | 12 | it "upgraded from 3 to 4 missing ./bin still works" do 13 | Hatchet::Runner.new("rails3-to-4-no-bin").deploy do |app, heroku| 14 | expect(app.output).to include("Asset precompilation completed") 15 | add_database(app, heroku) 16 | 17 | expect(app.output).to match("WARNINGS") 18 | expect(app.output).to match("Include 'rails_12factor' gem to enable all platform features") 19 | 20 | app.run("rails console") do |console| 21 | console.run("'hello' + 'world'") {|result| expect(result).to match('helloworld')} 22 | end 23 | end 24 | end 25 | 26 | it "works with windows" do 27 | Hatchet::Runner.new("rails4_windows_mri193").deploy do |app, heroku| 28 | result = app.run("rails -v") 29 | expect(result).to match("4.0.0") 30 | 31 | result = app.run("rake -T") 32 | expect(result).to match("assets:precompile") 33 | 34 | result = app.run("bundle show rails") 35 | expect(result).to match("rails-4.0.0") 36 | 37 | before_warnings = app.output.split("WARNINGS:").first 38 | expect(before_warnings).to match("Removing `Gemfile.lock`") 39 | end 40 | end 41 | 42 | it "fails compile if assets:precompile fails" do 43 | Hatchet::Runner.new("rails4-fail-assets-compile", allow_failure: true).deploy do |app, heroku| 44 | expect(app.output).to include("raising on assets:precompile on purpose") 45 | expect(app).not_to be_deployed 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /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" => "production", 27 | "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/rubies_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | describe "Ruby Versions" do 4 | it "should deploy ruby 1.8.7 properly" do 5 | Hatchet::Runner.new("mri_187").deploy do |app| 6 | version = '1.8.7' 7 | expect(app.output).to match(version) 8 | expect(app.run('ruby -v')).to match(version) 9 | end 10 | end 11 | 12 | it "should deploy ruby 1.9.2 properly" do 13 | Hatchet::Runner.new("mri_192").deploy do |app| 14 | version = '1.9.2' 15 | expect(app.output).to match(version) 16 | expect(app.run('ruby -v')).to match(version) 17 | end 18 | end 19 | 20 | it "should deploy ruby 1.9.2 properly (git)" do 21 | Hatchet::GitApp.new("mri_192", buildpack: git_repo).deploy do |app| 22 | version = '1.9.2' 23 | expect(app.output).to match(version) 24 | expect(app.run('ruby -v')).to match(version) 25 | end 26 | end 27 | 28 | it "should deploy ruby 1.9.3 properly" do 29 | Hatchet::Runner.new("mri_193").deploy do |app| 30 | version = '1.9.3' 31 | expect(app.output).to match(version) 32 | expect(app.run('ruby -v')).to match(version) 33 | end 34 | end 35 | 36 | it "should deploy ruby 2.0.0 properly" do 37 | Hatchet::Runner.new("mri_200").deploy do |app| 38 | version = '2.0.0' 39 | expect(app.output).to match(version) 40 | expect(app.run('ruby -v')).to match(version) 41 | end 42 | end 43 | 44 | it "should deploy jruby 1.7.3 (legacy jdk) properly" do 45 | Hatchet::AnvilApp.new("ruby_193_jruby_173").deploy do |app| 46 | expect(app.output).to match("Installing JVM: openjdk1.7.0_25") 47 | expect(app.output).to match("ruby-1.9.3-jruby-1.7.3") 48 | expect(app.output).not_to include("OpenJDK 64-Bit Server VM warning") 49 | expect(app.run('ruby -v')).to match("jruby 1.7.3") 50 | end 51 | end 52 | 53 | it "should deploy jruby 1.7.6 (latest jdk) properly" do 54 | Hatchet::AnvilApp.new("ruby_193_jruby_176").deploy do |app| 55 | expect(app.output).to match("Installing JVM: openjdk7-latest") 56 | expect(app.output).to match("ruby-1.9.3-jruby-1.7.6") 57 | expect(app.output).not_to include("OpenJDK 64-Bit Server VM warning") 58 | expect(app.run('ruby -v')).to match("jruby 1.7.6") 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /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("WARNINGS") 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/language_pack/shell_helpers.rb: -------------------------------------------------------------------------------- 1 | require "shellwords" 2 | 3 | module LanguagePack 4 | module ShellHelpers 5 | # display error message and stop the build process 6 | # @param [String] error message 7 | def error(message) 8 | Kernel.puts " !" 9 | message.split("\n").each do |line| 10 | Kernel.puts " ! #{line.strip}" 11 | end 12 | Kernel.puts " !" 13 | log "exit", :error => message if respond_to?(:log) 14 | exit 1 15 | end 16 | 17 | # run a shell comannd and pipe stderr to stdout 18 | # @param [String] command to be run 19 | # @return [String] output of stdout and stderr 20 | def run(command) 21 | %x{ bash -c #{command.shellescape} 2>&1 } 22 | end 23 | 24 | def run!(command) 25 | result = run(command) 26 | error("Command: '#{command}' failed unexpectedly:\n#{result}") unless $?.success? 27 | return result 28 | end 29 | 30 | # doesn't do any special piping. stderr won't be redirected. 31 | # @param [String] command to be run 32 | # @return [String] output of stdout 33 | def run_no_pipe(command) 34 | %x{ bash -c #{command.shellescape} } 35 | end 36 | 37 | # run a shell command and pipe stderr to /dev/null 38 | # @param [String] command to be run 39 | # @return [String] output of stdout 40 | def run_stdout(command) 41 | %x{ bash -c #{command.shellescape} 2>/dev/null } 42 | end 43 | 44 | # run a shell command and stream the output 45 | # @param [String] command to be run 46 | def pipe(command) 47 | output = "" 48 | IO.popen(command) do |io| 49 | until io.eof? 50 | buffer = io.gets 51 | output << buffer 52 | puts buffer 53 | end 54 | end 55 | 56 | output 57 | end 58 | 59 | # display a topic message 60 | # (denoted by ----->) 61 | # @param [String] topic message to be displayed 62 | def topic(message) 63 | Kernel.puts "-----> #{message}" 64 | $stdout.flush 65 | end 66 | 67 | # display a message in line 68 | # (indented by 6 spaces) 69 | # @param [String] message to be displayed 70 | def puts(message) 71 | message.split("\n").each do |line| 72 | super " #{line.strip}" 73 | end 74 | $stdout.flush 75 | end 76 | 77 | def warn(message, options = {}) 78 | if options.key?(:inline) ? options[:inline] : false 79 | topic "Warning:" 80 | puts message 81 | end 82 | @warnings ||= [] 83 | @warnings << message 84 | end 85 | 86 | def deprecate(message) 87 | @deprecations ||= [] 88 | @deprecations << message 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /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, app = {}) 31 | @set = nil 32 | @bundler = bundler 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 | def gemfile 78 | ruby_version = @bundler.ruby_version 79 | return ruby_version.to_s 80 | end 81 | 82 | def none 83 | if @app[:is_new] 84 | DEFAULT_VERSION 85 | elsif @app[:last_version] 86 | @app[:last_version] 87 | else 88 | LEGACY_VERSION 89 | end 90 | end 91 | 92 | def set_version 93 | bundler_output = gemfile 94 | if bundler_output.empty? 95 | @set = false 96 | @version = none 97 | else 98 | @set = :gemfile 99 | @version = bundler_output.sub('(', '').sub(')', '').split.join('-') 100 | end 101 | end 102 | 103 | def parse_version 104 | md = RUBY_VERSION_REGEX.match(version) 105 | raise BadVersionError.new("'#{version}' is not valid") unless md 106 | @ruby_version = md[:ruby_version] 107 | @patchlevel = md[:patchlevel] 108 | @engine_version = md[:engine_version] || @ruby_version 109 | @engine = (md[:engine] || :ruby).to_sym 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /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('5.0.0') 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 | setup_database_url_env 73 | 74 | if Dir.glob('public/assets/manifest-*.json').any? 75 | puts "Detected manifest file, assuming assets were compiled locally" 76 | return true 77 | end 78 | 79 | precompile = rake.task("assets:precompile") 80 | return true unless precompile.is_defined? 81 | 82 | topic("Preparing app for Rails asset pipeline") 83 | ENV["RAILS_GROUPS"] ||= "assets" 84 | ENV["RAILS_ENV"] ||= "production" 85 | 86 | @cache.load public_assets_folder 87 | @cache.load default_assets_cache 88 | 89 | precompile.invoke 90 | 91 | if precompile.success? 92 | log "assets_precompile", :status => "success" 93 | puts "Asset precompilation completed (#{"%.2f" % precompile.time}s)" 94 | 95 | puts "Cleaning assets" 96 | pipe "env PATH=$PATH:bin bundle exec rake assets:clean 2>& 1" 97 | 98 | cleanup_assets_cache 99 | @cache.store public_assets_folder 100 | @cache.store default_assets_cache 101 | else 102 | log "assets_precompile", :status => "failure" 103 | error "Precompiling assets failed." 104 | end 105 | end 106 | end 107 | end 108 | 109 | def cleanup_assets_cache 110 | instrument "rails4.cleanup_assets_cache" do 111 | LanguagePack::Helpers::StaleFileCleaner.new(default_assets_cache).clean_over(ASSETS_CACHE_LIMIT) 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /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, :command, :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, command = nil) 15 | @task = task 16 | command = "env PATH=$PATH:bin bundle exec rake #{task} 2>&1" if command.nil? 17 | raise "expect #{command} to contain #{task}" unless command.include?(task) 18 | 19 | @command = command 20 | @status = :nil 21 | @output = "" 22 | end 23 | 24 | def success? 25 | status == :pass 26 | end 27 | 28 | def status? 29 | @status && @status != :nil 30 | end 31 | 32 | def status 33 | raise "Status not set for #{self.inspect}" if @status == :nil 34 | raise "Not allowed status: #{@status} for #{self.inspect}" unless ALLOWED.include?(@status) 35 | @status 36 | end 37 | 38 | def invoke(cmd = nil) 39 | cmd = cmd || @command 40 | puts "Running: rake #{@task}" 41 | time = Benchmark.realtime do 42 | self.output = pipe(cmd) 43 | end 44 | self.time = time 45 | 46 | if $?.success? 47 | self.status = :pass 48 | else 49 | self.status = :fail 50 | end 51 | return self 52 | end 53 | end 54 | 55 | def initialize(has_rake_gem = true) 56 | @has_rake = has_rake_gem && has_rakefile? 57 | if @has_rake 58 | load_rake_tasks 59 | else 60 | @rake_tasks = "" 61 | @rakefile_can_load = false 62 | end 63 | end 64 | 65 | def cannot_load_rakefile? 66 | !rakefile_can_load? 67 | end 68 | 69 | def rakefile_can_load? 70 | @rakefile_can_load 71 | end 72 | 73 | def instrument(*args, &block) 74 | LanguagePack::Instrument.instrument(*args, &block) 75 | end 76 | 77 | def load_rake_tasks 78 | instrument "ruby.rake_task_defined" do 79 | @rake_tasks ||= run("env PATH=$PATH bundle exec rake -P --trace") 80 | @rakefile_can_load ||= $?.success? 81 | @rake_tasks 82 | end 83 | end 84 | 85 | def load_rake_tasks! 86 | out = load_rake_tasks 87 | msg = "Could not detect rake tasks\n" 88 | msg << "ensure you can run `$ bundle exec rake -P` against your app with no environment variables present\n" 89 | msg << "and using the production group of your Gemfile.\n" 90 | msg << "This may be intentional, if you expected rake tasks to be run\n" 91 | msg << "cancel the build (CTRL+C) and fix the error then commit the fix:\n" 92 | msg << out 93 | puts msg if cannot_load_rakefile? 94 | return self 95 | end 96 | 97 | def task_defined?(task) 98 | return false if cannot_load_rakefile? 99 | @task_available ||= Hash.new {|hash, key| hash[key] = @rake_tasks.match(/\s#{key}\s/) } 100 | @task_available[task] 101 | end 102 | 103 | def not_found?(task) 104 | !task_defined?(task) 105 | end 106 | 107 | def task(rake_task, command = nil) 108 | t = RakeTask.new(rake_task, command) 109 | t.task_defined = task_defined?(rake_task) 110 | t.rakefile_can_load = rakefile_can_load? 111 | t 112 | end 113 | 114 | def invoke(task, command = nil) 115 | self.task(task, command).invoke 116 | end 117 | 118 | private 119 | 120 | def has_rakefile? 121 | %W{ Rakefile rakefile rakefile.rb Rakefile.rb}.detect {|file| File.exist?(file) } 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/language_pack/helpers/bundler_wrapper.rb: -------------------------------------------------------------------------------- 1 | class LanguagePack::Helpers::BundlerWrapper 2 | class GemfileParseError < StandardError 3 | def initialize(error) 4 | msg = "There was an error parsing your Gemfile, we cannot continue\n" 5 | msg << error.message 6 | self.set_backtrace(error.backtrace) 7 | super msg 8 | end 9 | end 10 | 11 | VENDOR_URL = LanguagePack::Base::VENDOR_URL # coupling 12 | DEFAULT_FETCHER = LanguagePack::Fetcher.new(VENDOR_URL) # coupling 13 | BUNDLER_DIR_NAME = LanguagePack::Ruby::BUNDLER_GEM_PATH # coupling 14 | BUNDLER_PATH = File.expand_path("../../../../tmp/#{BUNDLER_DIR_NAME}", __FILE__) 15 | GEMFILE_PATH = Pathname.new "./Gemfile" 16 | 17 | attr_reader :bundler_path 18 | 19 | def initialize(options = {}) 20 | @fetcher = options[:fetcher] || DEFAULT_FETCHER 21 | @bundler_path = options[:bundler_path] || BUNDLER_PATH 22 | @gemfile_path = options[:gemfile_path] || GEMFILE_PATH 23 | @bundler_tar = options[:bundler_tar] || "#{BUNDLER_DIR_NAME}.tgz" 24 | @gemfile_lock_path = "#{@gemfile_path}.lock" 25 | ENV['BUNDLE_GEMFILE'] = @gemfile_path.to_s 26 | @unlock = false 27 | @path = Pathname.new "#{@bundler_path}/gems/#{BUNDLER_DIR_NAME}/lib" 28 | fetch_bundler 29 | $LOAD_PATH << @path 30 | without_warnings do 31 | load @path.join("bundler.rb") 32 | end 33 | end 34 | 35 | def without_warnings(&block) 36 | orig_verb = $VERBOSE 37 | $VERBOSE = nil 38 | yield 39 | ensure 40 | $VERBOSE = orig_verb 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 clean 80 | FileUtils.remove_entry_secure(bundler_path) 81 | end 82 | 83 | def ui 84 | Bundler.ui = Bundler::UI::Shell.new({}) 85 | end 86 | 87 | def definition 88 | Bundler.definition(@unlock) 89 | rescue => e 90 | raise GemfileParseError.new(e) 91 | end 92 | 93 | def unlock 94 | @unlock = true 95 | yield 96 | ensure 97 | @unlock = false 98 | end 99 | 100 | def ruby_version 101 | unlock do 102 | definition.ruby_version 103 | end 104 | end 105 | 106 | def lockfile_parser 107 | @lockfile_parser ||= parse_gemfile_lock 108 | end 109 | 110 | private 111 | def fetch_bundler 112 | instrument 'fetch_bundler' do 113 | return true if Dir.exists?(bundler_path) 114 | FileUtils.mkdir_p(bundler_path) 115 | Dir.chdir(bundler_path) do 116 | @fetcher.fetch_untar(@bundler_tar) 117 | end 118 | end 119 | end 120 | 121 | def parse_gemfile_lock 122 | instrument 'parse_bundle' do 123 | gemfile_contents = File.read(@gemfile_lock_path) 124 | Bundler::LockfileParser.new(gemfile_contents) 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /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 | setup_database_url_env 63 | 64 | if File.exists?("public/assets/manifest.yml") 65 | puts "Detected manifest.yml, assuming assets were compiled locally" 66 | return true 67 | end 68 | 69 | precompile = rake.task("assets:precompile") 70 | return true unless precompile.is_defined? 71 | 72 | topic("Preparing app for Rails asset pipeline") 73 | 74 | ENV["RAILS_GROUPS"] ||= "assets" 75 | ENV["RAILS_ENV"] ||= "production" 76 | 77 | puts "Running: rake assets:precompile" 78 | require 'benchmark' 79 | 80 | precompile.invoke 81 | if precompile.success? 82 | log "assets_precompile", :status => "success" 83 | puts "Asset precompilation completed (#{"%.2f" % precompile.time}s)" 84 | else 85 | log "assets_precompile", :status => "failure" 86 | error "Precompiling assets failed." 87 | end 88 | end 89 | end 90 | end 91 | 92 | # setup the database url as an environment variable 93 | def setup_database_url_env 94 | instrument "rails3.setup_database_url_env" do 95 | ENV["DATABASE_URL"] ||= begin 96 | # need to use a dummy DATABASE_URL here, so rails can load the environment 97 | scheme = 98 | if bundler.has_gem?("pg") || bundler.has_gem?("jdbc-postgres") 99 | "postgres" 100 | elsif bundler.has_gem?("mysql") 101 | "mysql" 102 | elsif bundler.has_gem?("mysql2") 103 | "mysql2" 104 | elsif bundler.has_gem?("sqlite3") || bundler.has_gem?("sqlite3-ruby") 105 | "sqlite3" 106 | end 107 | "#{scheme}://user:pass@127.0.0.1/dbname" 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/helpers/ruby_version_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "RubyVersion" do 4 | before(:each) do 5 | # bundler wrapper must be initialized relative to the target Gemfile 6 | end 7 | 8 | after(:each) do 9 | FileUtils.remove_entry_secure("tmp") if Dir.exist?("tmp") 10 | end 11 | 12 | it "correctly sets ruby version for bundler specified versions" do 13 | Hatchet::App.new("mri_193").in_directory do |dir| 14 | @bundler = LanguagePack::Helpers::BundlerWrapper.new(gemfile_path: "./Gemfile") 15 | ruby_version = LanguagePack::RubyVersion.new(@bundler, {is_new: true}) 16 | version_number = "1.9.3" 17 | version = "ruby-#{version_number}" 18 | expect(ruby_version.version_without_patchlevel).to eq(version) 19 | expect(ruby_version.engine_version).to eq(version_number) 20 | expect(ruby_version.to_gemfile).to eq("ruby '#{version_number}'") 21 | expect(ruby_version.engine).to eq(:ruby) 22 | end 23 | end 24 | 25 | it "correctly sets default ruby versions" do 26 | Hatchet::App.new("default_ruby").in_directory do |dir| 27 | @bundler = LanguagePack::Helpers::BundlerWrapper.new(gemfile_path: "./Gemfile") 28 | 29 | ruby_version = LanguagePack::RubyVersion.new(@bundler, {is_new: true}) 30 | version_number = LanguagePack::RubyVersion::DEFAULT_VERSION_NUMBER 31 | version = LanguagePack::RubyVersion::DEFAULT_VERSION 32 | expect(ruby_version.version_without_patchlevel).to eq(version) 33 | expect(ruby_version.engine_version).to eq(version_number) 34 | expect(ruby_version.to_gemfile).to eq("ruby '#{version_number}'") 35 | expect(ruby_version.engine).to eq(:ruby) 36 | expect(ruby_version.default?).to eq(true) 37 | end 38 | end 39 | 40 | it "correctly sets default legacy version" do 41 | Hatchet::App.new("default_ruby").in_directory do |dir| 42 | @bundler = LanguagePack::Helpers::BundlerWrapper.new(gemfile_path: "./Gemfile") 43 | ruby_version = LanguagePack::RubyVersion.new(@bundler, {is_new: false}) 44 | version_number = LanguagePack::RubyVersion::LEGACY_VERSION_NUMBER 45 | version = LanguagePack::RubyVersion::LEGACY_VERSION 46 | expect(ruby_version.version_without_patchlevel).to eq(version) 47 | expect(ruby_version.engine_version).to eq(version_number) 48 | expect(ruby_version.to_gemfile).to eq("ruby '#{version_number}'") 49 | expect(ruby_version.engine).to eq(:ruby) 50 | end 51 | end 52 | 53 | it "detects Ruby 2.0.0" do 54 | Hatchet::App.new("mri_200").in_directory do |dir| 55 | @bundler = LanguagePack::Helpers::BundlerWrapper.new(gemfile_path: "./Gemfile") 56 | ruby_version = LanguagePack::RubyVersion.new(@bundler, {is_new: true}) 57 | version_number = "2.0.0" 58 | version = "ruby-#{version_number}" 59 | expect(ruby_version.version_without_patchlevel).to eq(version) 60 | expect(ruby_version.engine_version).to eq(version_number) 61 | expect(ruby_version.to_gemfile).to eq("ruby '#{version_number}'") 62 | expect(ruby_version.engine).to eq(:ruby) 63 | end 64 | end 65 | 66 | 67 | it "detects non mri engines" do 68 | Hatchet::App.new("ruby_193_jruby_173").in_directory do |dir| 69 | @bundler = LanguagePack::Helpers::BundlerWrapper.new(gemfile_path: "./Gemfile") 70 | ruby_version = LanguagePack::RubyVersion.new(@bundler, {is_new: true}) 71 | version_number = "1.9.3" 72 | engine_version = "1.7.3" 73 | engine = :jruby 74 | version = "ruby-#{version_number}-#{engine}-#{engine_version}" 75 | to_gemfile = "ruby '#{version_number}', :engine => '#{engine}', :engine_version => '#{engine_version}'" 76 | expect(ruby_version.version_without_patchlevel).to eq(version) 77 | expect(ruby_version.engine_version).to eq(engine_version) 78 | expect(ruby_version.to_gemfile).to eq(to_gemfile) 79 | expect(ruby_version.engine).to eq(engine) 80 | end 81 | end 82 | 83 | it "surfaces error message from bundler" do 84 | bundle_error_msg = "Zomg der was a problem in da gemfile" 85 | error_klass = LanguagePack::Helpers::BundlerWrapper::GemfileParseError 86 | Hatchet::App.new("bad_gemfile_on_platform").in_directory do |dir| 87 | @bundler = LanguagePack::Helpers::BundlerWrapper.new(gemfile_path: "./Gemfile") 88 | expect {LanguagePack::RubyVersion.new(@bundler)}.to raise_error(error_klass, /#{Regexp.escape(bundle_error_msg)}/) 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /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/metadata" 8 | require "language_pack/fetcher" 9 | require "language_pack/instrument" 10 | 11 | Encoding.default_external = Encoding::UTF_8 if defined?(Encoding) 12 | 13 | # abstract class that all the Ruby based Language Packs inherit from 14 | class LanguagePack::Base 15 | include LanguagePack::ShellHelpers 16 | 17 | VENDOR_URL = "https://s3-external-1.amazonaws.com/heroku-buildpack-ruby" 18 | 19 | attr_reader :build_path, :cache 20 | 21 | # changes directory to the build_path 22 | # @param [String] the path of the build dir 23 | # @param [String] the path of the cache dir this is nil during detect and release 24 | def initialize(build_path, cache_path=nil) 25 | self.class.instrument "base.initialize" do 26 | @build_path = build_path 27 | @cache = LanguagePack::Cache.new(cache_path) if cache_path 28 | @metadata = LanguagePack::Metadata.new(@cache) 29 | @id = Digest::SHA1.hexdigest("#{Time.now.to_f}-#{rand(1000000)}")[0..10] 30 | @warnings = [] 31 | @deprecations = [] 32 | @fetchers = {:buildpack => LanguagePack::Fetcher.new(VENDOR_URL) } 33 | 34 | Dir.chdir build_path 35 | end 36 | end 37 | 38 | def instrument(*args, &block) 39 | self.class.instrument(*args, &block) 40 | end 41 | 42 | def self.instrument(*args, &block) 43 | LanguagePack::Instrument.instrument(*args, &block) 44 | end 45 | 46 | def self.===(build_path) 47 | raise "must subclass" 48 | end 49 | 50 | # name of the Language Pack 51 | # @return [String] the result 52 | def name 53 | raise "must subclass" 54 | end 55 | 56 | # list of default addons to install 57 | def default_addons 58 | raise "must subclass" 59 | end 60 | 61 | # config vars to be set on first push. 62 | # @return [Hash] the result 63 | # @not: this is only set the first time an app is pushed to. 64 | def default_config_vars 65 | raise "must subclass" 66 | end 67 | 68 | # process types to provide for the app 69 | # Ex. for rails we provide a web process 70 | # @return [Hash] the result 71 | def default_process_types 72 | raise "must subclass" 73 | end 74 | 75 | # this is called to build the slug 76 | def compile 77 | write_release_yaml 78 | instrument 'base.compile' do 79 | if @warnings.any? 80 | topic "WARNINGS:" 81 | puts @warnings.join("\n") 82 | end 83 | if @deprecations.any? 84 | topic "DEPRECATIONS:" 85 | puts @deprecations.join("\n") 86 | end 87 | end 88 | end 89 | 90 | def write_release_yaml 91 | release = {} 92 | release["addons"] = default_addons 93 | release["default_process_types"] = default_process_types 94 | FileUtils.mkdir("tmp") unless File.exists?("tmp") 95 | File.open("tmp/heroku-buildpack-release-step.yml", 'w') do |f| 96 | f.write(release.to_yaml) 97 | end 98 | end 99 | 100 | # log output 101 | # Ex. log "some_message", "here", :someattr="value" 102 | def log(*args) 103 | args.concat [:id => @id] 104 | args.concat [:framework => self.class.to_s.split("::").last.downcase] 105 | 106 | start = Time.now.to_f 107 | log_internal args, :start => start 108 | 109 | if block_given? 110 | begin 111 | ret = yield 112 | finish = Time.now.to_f 113 | log_internal args, :status => "complete", :finish => finish, :elapsed => (finish - start) 114 | return ret 115 | rescue StandardError => ex 116 | finish = Time.now.to_f 117 | log_internal args, :status => "error", :finish => finish, :elapsed => (finish - start), :message => ex.message 118 | raise ex 119 | end 120 | end 121 | end 122 | 123 | private ################################## 124 | 125 | # sets up the environment variables for the build process 126 | def setup_language_pack_environment 127 | end 128 | 129 | def add_to_profiled(string) 130 | FileUtils.mkdir_p "#{build_path}/.profile.d" 131 | File.open("#{build_path}/.profile.d/ruby.sh", "a") do |file| 132 | file.puts string 133 | end 134 | end 135 | 136 | def set_env_default(key, val) 137 | add_to_profiled "export #{key}=${#{key}:-#{val}}" 138 | end 139 | 140 | def set_env_override(key, val) 141 | add_to_profiled %{export #{key}="#{val.gsub('"','\"')}"} 142 | end 143 | 144 | def log_internal(*args) 145 | message = build_log_message(args) 146 | %x{ logger -p user.notice -t "slugc[$$]" "buildpack-ruby #{message}" } 147 | end 148 | 149 | def build_log_message(args) 150 | args.map do |arg| 151 | case arg 152 | when Float then "%0.2f" % arg 153 | when Array then build_log_message(arg) 154 | when Hash then arg.map { |k,v| "#{k}=#{build_log_message([v])}" }.join(" ") 155 | else arg 156 | end 157 | end.join(" ") 158 | end 159 | end 160 | 161 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Heroku buildpack: Ruby GSL 2 | ====================== 3 | 4 | This is a buildpack that enables using the [gsl gem](http://rb-gsl.rubyforge.org/) on Heroku Cedar. 5 | 6 | heroku create --stack cedar --buildpack https://github.com/tomwolfe/heroku-buildpack-gsl-ruby.git 7 | git push heroku master 8 | ... 9 | -----> Heroku receiving push 10 | -----> Fetching custom buildpack... done 11 | -----> Ruby/Rails app detected 12 | -----> Installing gsl 13 | -----> Installing dependencies using Bundler version 1.1.2 14 | Running: bundle install --without development:test --path vendor/bundle --binstubs bin/ --deployment 15 | .... 16 | 17 | 18 | Heroku buildpack: Ruby 19 | ====================== 20 | 21 | This is a [Heroku buildpack](http://devcenter.heroku.com/articles/buildpacks) for Ruby, Rack, and Rails apps. It uses [Bundler](http://gembundler.com) for dependency management. 22 | 23 | Usage 24 | ----- 25 | 26 | ### Ruby 27 | 28 | Example Usage: 29 | 30 | $ ls 31 | Gemfile Gemfile.lock 32 | 33 | $ heroku create --stack cedar --buildpack https://github.com/heroku/heroku-buildpack-ruby.git 34 | 35 | $ git push heroku master 36 | ... 37 | -----> Heroku receiving push 38 | -----> Fetching custom buildpack 39 | -----> Ruby app detected 40 | -----> Installing dependencies using Bundler version 1.1.rc 41 | Running: bundle install --without development:test --path vendor/bundle --deployment 42 | Fetching gem metadata from http://rubygems.org/.. 43 | Installing rack (1.3.5) 44 | Using bundler (1.1.rc) 45 | Your bundle is complete! It was installed into ./vendor/bundle 46 | Cleaning up the bundler cache. 47 | -----> Discovering process types 48 | Procfile declares types -> (none) 49 | Default types for Ruby -> console, rake 50 | 51 | The buildpack will detect your app as Ruby if it has a `Gemfile` and `Gemfile.lock` files in the root directory. It will then proceed to run `bundle install` after setting up the appropriate environment for [ruby](http://ruby-lang.org) and [Bundler](http://gembundler.com). 52 | 53 | #### Run the Tests 54 | 55 | The tests on this buildpack are written in Rspec to allow the use of 56 | `focused: true`. Parallelization of testing is provided by 57 | https://github.com/grosser/parallel_tests this lib spins up an arbitrary 58 | number of processes and running a different test file in each process, 59 | it does not parallelize tests within a test file. To run the tests: clone the repo, then `bundle install` then clone the test fixtures by running: 60 | 61 | ```sh 62 | $ hatchet install 63 | ``` 64 | 65 | Now run the tests: 66 | 67 | ```sh 68 | $ bundle exec parallel_rspec -n 6 spec/ 69 | ``` 70 | 71 | If you don't want to run them in parallel you can still: 72 | 73 | ```sh 74 | $ bundle exec rake spec 75 | ``` 76 | 77 | Now go take a nap or something for a really long time. 78 | 79 | #### Bundler 80 | 81 | For non-windows `Gemfile.lock` files, the `--deployment` flag will be used. In the case of windows, the Gemfile.lock will be deleted and Bundler will do a full resolve so native gems are handled properly. The `vendor/bundle` directory is cached between builds to allow for faster `bundle install` times. `bundle clean` is used to ensure no stale gems are stored between builds. 82 | 83 | ### Rails 2 84 | 85 | Example Usage: 86 | 87 | $ ls 88 | app config db doc Gemfile Gemfile.lock lib log public Rakefile README script test tmp vendor 89 | 90 | $ ls config/environment.rb 91 | config/environment.rb 92 | 93 | $ heroku create --stack cedar --buildpack https://github.com/heroku/heroku-buildpack-ruby.git 94 | 95 | $ git push heroku master 96 | ... 97 | -----> Heroku receiving push 98 | -----> Ruby/Rails app detected 99 | -----> Installing dependencies using Bundler version 1.1.rc 100 | ... 101 | -----> Writing config/database.yml to read from DATABASE_URL 102 | -----> Rails plugin injection 103 | Injecting rails_log_stdout 104 | -----> Discovering process types 105 | Procfile declares types -> (none) 106 | Default types for Ruby/Rails -> console, rake, web, worker 107 | 108 | The buildpack will detect your app as a Rails 2 app if it has a `environment.rb` file in the `config` directory. 109 | 110 | #### Rails Log STDOUT 111 | A [rails_log_stdout](http://github.com/ddollar/rails_log_stdout) is installed by default so Rails' logger will log to STDOUT and picked up by Heroku's [logplex](http://github.com/heroku/logplex). 112 | 113 | #### Auto Injecting Plugins 114 | 115 | Any vendored plugin can be stopped from being installed by creating the directory it's installed to in the slug. For instance, to prevent rails_log_stdout plugin from being injected, add `vendor/plugins/rails_log_stdout/.gitkeep` to your git repo. 116 | 117 | ### Rails 3 118 | 119 | Example Usage: 120 | 121 | $ ls 122 | app config config.ru db doc Gemfile Gemfile.lock lib log Procfile public Rakefile README script tmp vendor 123 | 124 | $ ls config/application.rb 125 | config/application.rb 126 | 127 | $ heroku create --stack cedar --buildpack https://github.com/heroku/heroku-buildpack-ruby.git 128 | 129 | $ git push heroku master 130 | -----> Heroku receiving push 131 | -----> Ruby/Rails app detected 132 | -----> Installing dependencies using Bundler version 1.1.rc 133 | Running: bundle install --without development:test --path vendor/bundle --deployment 134 | ... 135 | -----> Writing config/database.yml to read from DATABASE_URL 136 | -----> Preparing app for Rails asset pipeline 137 | Running: rake assets:precompile 138 | -----> Rails plugin injection 139 | Injecting rails_log_stdout 140 | Injecting rails3_serve_static_assets 141 | -----> Discovering process types 142 | Procfile declares types -> web 143 | Default types for Ruby/Rails -> console, rake, worker 144 | 145 | The buildpack will detect your apps as a Rails 3 app if it has an `application.rb` file in the `config` directory. 146 | 147 | #### Assets 148 | 149 | To enable static assets being served on the dyno, [rails3_serve_static_assets](http://github.com/pedro/rails3_serve_static_assets) is installed by default. If the [execjs gem](http://github.com/sstephenson/execjs) is detected then [node.js](http://github.com/joyent/node) will be vendored. The `assets:precompile` rake task will get run if no `public/manifest.yml` is detected. See [this article](http://devcenter.heroku.com/articles/rails31_heroku_cedar) on how rails 3.1 works on cedar. 150 | 151 | Hacking 152 | ------- 153 | 154 | To use this buildpack, fork it on Github. Push up changes to your fork, then create a test app with `--buildpack ` and push to it. 155 | 156 | To change the vendored binaries for Bundler, [Node.js](http://github.com/joyent/node), and rails plugins, use the rake tasks provided by the `Rakefile`. You'll need an S3-enabled AWS account and a bucket to store your binaries in as well as the [vulcan](http://github.com/heroku/vulcan) gem to build the binaries on heroku. 157 | 158 | For example, you can change the vendored version of Bundler to 1.1.rc. 159 | 160 | First you'll need to build a Heroku-compatible version of Node.js: 161 | 162 | $ export AWS_ID=xxx AWS_SECRET=yyy S3_BUCKET=zzz 163 | $ s3 create $S3_BUCKET 164 | $ rake gem:install[bundler,1.1.rc] 165 | 166 | Open `lib/language_pack/ruby.rb` in your editor, and change the following line: 167 | 168 | BUNDLER_VERSION = "1.1.rc" 169 | 170 | Open `lib/language_pack/base.rb` in your editor, and change the following line: 171 | 172 | VENDOR_URL = "https://s3.amazonaws.com/zzz" 173 | 174 | Commit and push the changes to your buildpack to your Github fork, then push your sample app to Heroku to test. You should see: 175 | 176 | -----> Installing dependencies using Bundler version 1.1.rc 177 | 178 | NOTE: You'll need to vendor the plugins, node, Bundler, and libyaml by running the rake tasks for the buildpack to work properly. 179 | 180 | Flow 181 | ---- 182 | 183 | Here's the basic flow of how the buildpack works: 184 | 185 | Ruby (Gemfile and Gemfile.lock is detected) 186 | 187 | * runs Bundler 188 | * installs binaries 189 | * installs node if the gem execjs is detected 190 | * runs `rake assets:precompile` if the rake task is detected 191 | 192 | Rack (config.ru is detected) 193 | 194 | * everything from Ruby 195 | * sets RACK_ENV=production 196 | 197 | Rails 2 (config/environment.rb is detected) 198 | 199 | * everything from Rack 200 | * sets RAILS_ENV=production 201 | * install rails 2 plugins 202 | * [rails_log_stdout](http://github.com/ddollar/rails_log_stdout) 203 | 204 | Rails 3 (config/application.rb is detected) 205 | 206 | * everything from Rails 2 207 | * install rails 3 plugins 208 | * [rails3_server_static_assets](https://github.com/pedro/rails3_serve_static_assets) 209 | 210 | -------------------------------------------------------------------------------- /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 | GSL_VENDOR_URL = "https://s3.amazonaws.com/gsl_bin/gsl-1.15.tgz" 8 | 9 | def s3_tools_dir 10 | File.expand_path("../support/s3", __FILE__) 11 | end 12 | 13 | def s3_upload(tmpdir, name) 14 | sh("#{s3_tools_dir}/s3 put #{S3_BUCKET_NAME} #{name}.tgz #{tmpdir}/#{name}.tgz") 15 | end 16 | 17 | def vendor_plugin(git_url, branch = nil) 18 | name = File.basename(git_url, File.extname(git_url)) 19 | Dir.mktmpdir("#{name}-") do |tmpdir| 20 | FileUtils.rm_rf("#{tmpdir}/*") 21 | 22 | Dir.chdir(tmpdir) do 23 | sh "git clone #{git_url} ." 24 | sh "git checkout origin/#{branch}" if branch 25 | FileUtils.rm_rf("#{name}/.git") 26 | sh("tar czvf #{tmpdir}/#{name}.tgz *") 27 | s3_upload(tmpdir, name) 28 | end 29 | end 30 | end 31 | 32 | def in_gem_env(gem_home, &block) 33 | old_gem_home = ENV['GEM_HOME'] 34 | old_gem_path = ENV['GEM_PATH'] 35 | ENV['GEM_HOME'] = ENV['GEM_PATH'] = gem_home.to_s 36 | 37 | yield 38 | 39 | ENV['GEM_HOME'] = old_gem_home 40 | ENV['GEM_PATH'] = old_gem_path 41 | end 42 | 43 | def install_gem(gem, version) 44 | name = "#{gem}-#{version}" 45 | Dir.mktmpdir("#{gem}-#{version}") do |tmpdir| 46 | Dir.chdir(tmpdir) do |dir| 47 | FileUtils.rm_rf("#{tmpdir}/*") 48 | 49 | in_gem_env(tmpdir) do 50 | sh("unset RUBYOPT; gem install #{gem} --version #{version} --no-ri --no-rdoc --env-shebang") 51 | sh("rm #{gem}-#{version}.gem") 52 | sh("rm -rf cache/#{gem}-#{version}.gem") 53 | sh("tar czvf #{tmpdir}/#{name}.tgz *") 54 | s3_upload(tmpdir, name) 55 | end 56 | end 57 | end 58 | end 59 | 60 | desc "update plugins" 61 | task "plugins:update" do 62 | vendor_plugin "http://github.com/heroku/rails_log_stdout.git", "legacy" 63 | vendor_plugin "http://github.com/pedro/rails3_serve_static_assets.git" 64 | vendor_plugin "http://github.com/hone/rails31_enable_runtime_asset_compilation.git" 65 | end 66 | 67 | desc "install vendored gem" 68 | task "gem:install", :gem, :version do |t, args| 69 | gem = args[:gem] 70 | version = args[:version] 71 | 72 | install_gem(gem, version) 73 | end 74 | 75 | desc "generate ruby versions manifest" 76 | task "ruby:manifest" do 77 | require 'rexml/document' 78 | require 'yaml' 79 | 80 | document = REXML::Document.new(`curl https://#{S3_BUCKET_NAME}.s3.amazonaws.com`) 81 | rubies = document.elements.to_a("//Contents/Key").map {|node| node.text }.select {|text| text.match(/^(ruby|rbx|jruby)-\\\\d+\\\\.\\\\d+\\\\.\\\\d+(-p\\\\d+)?/) } 82 | 83 | Dir.mktmpdir("ruby_versions-") do |tmpdir| 84 | name = 'ruby_versions.yml' 85 | File.open(name, 'w') {|file| file.puts(rubies.to_yaml) } 86 | sh("#{s3_tools_dir}/s3 put #{S3_BUCKET_NAME} #{name} #{name}") 87 | end 88 | end 89 | 90 | namespace :buildpack do 91 | require 'netrc' 92 | require 'excon' 93 | require 'json' 94 | require 'time' 95 | require 'cgi' 96 | require 'git' 97 | require 'fileutils' 98 | require 'digest/md5' 99 | require 'securerandom' 100 | 101 | def connection 102 | @connection ||= begin 103 | user, password = Netrc.read["api.heroku.com"] 104 | Excon.new("https://#{CGI.escape(user)}:#{password}@buildkits.herokuapp.com") 105 | end 106 | end 107 | 108 | def latest_release 109 | @latest_release ||= begin 110 | buildpack_name = "heroku/ruby" 111 | response = connection.get(path: "buildpacks/#{buildpack_name}/revisions") 112 | releases = JSON.parse(response.body) 113 | 114 | # { 115 | # "tar_link": "https://codon-buildpacks.s3.amazonaws.com/buildpacks/heroku/ruby-v84.tgz", 116 | # "created_at": "2013-11-06T18:55:04Z", 117 | # "published_by": "richard@heroku.com", 118 | # "id": 84 119 | # } 120 | releases.map! do |a| 121 | a["created_at"] = Time.parse(a["created_at"]) 122 | a 123 | end.sort! { |a,b| b["created_at"] <=> a["created_at"] } 124 | releases.first 125 | end 126 | end 127 | 128 | def new_version 129 | @new_version ||= "v#{latest_release["id"] + 1}" 130 | end 131 | 132 | desc "increment buildpack version" 133 | task :increment do 134 | version_file = './lib/language_pack/version' 135 | require './lib/language_pack' 136 | require version_file 137 | 138 | if LanguagePack::Base::BUILDPACK_VERSION != new_version 139 | git = Git.open(".") 140 | stashes = nil 141 | 142 | if git.status.changed.any? 143 | stashes = Git::Stashes.new(git) 144 | stashes.save("WIP") 145 | end 146 | 147 | File.open("#{version_file}.rb", 'w') do |file| 148 | file.puts <<-FILE 149 | require "language_pack/base" 150 | 151 | # This file is automatically generated by rake 152 | module LanguagePack 153 | class LanguagePack::Base 154 | BUILDPACK_VERSION = "#{new_version}" 155 | end 156 | end 157 | FILE 158 | end 159 | 160 | git.add "#{version_file}.rb" 161 | git.commit "bump to #{new_version}" 162 | 163 | stashes.pop if stashes 164 | 165 | puts "Bumped to #{new_version}" 166 | else 167 | puts "Already on #{new_version}" 168 | end 169 | end 170 | 171 | def changelog_entry? 172 | File.read("./CHANGELOG.md").split("\n").any? {|line| line.match(/^## #{new_version}/) } 173 | end 174 | 175 | desc "check if there's a changelog for the new version" 176 | task :changelog do 177 | if changelog_entry? 178 | puts "Changelog for #{new_version} exists" 179 | else 180 | puts "Please add a changelog entry for #{new_version}" 181 | end 182 | end 183 | 184 | desc "stage a tarball of the buildpack" 185 | task :stage do 186 | Dir.mktmpdir("heroku-buildpack-ruby") do |tmpdir| 187 | Git.clone(File.expand_path("."), 'heroku-buildpack-ruby', path: tmpdir) 188 | Dir.chdir(tmpdir) do |dir| 189 | streamer = lambda do |chunk, remaining_bytes, total_bytes| 190 | File.open("ruby.tgz", "w") {|file| file.print(chunk) } 191 | end 192 | Excon.get(latest_release["tar_link"], :response_block => streamer) 193 | Dir.chdir("heroku-buildpack-ruby") do |dir| 194 | sh "tar xzf ../ruby.tgz .env" 195 | sh "tar czf ../buildpack.tgz * .env" 196 | end 197 | 198 | @digest = Digest::MD5.hexdigest(File.read("buildpack.tgz")) 199 | end 200 | 201 | filename = "buildpacks/#{@digest}.tgz" 202 | puts "Writing to #{filename}" 203 | FileUtils.mkdir_p("buildpacks/") 204 | FileUtils.cp("#{tmpdir}/buildpack.tgz", filename) 205 | FileUtils.cp("#{tmpdir}/buildpack.tgz", "buildpacks/buildpack.tgz") 206 | end 207 | end 208 | 209 | def multipart_form_data(buildpack_file_path) 210 | body = '' 211 | boundary = SecureRandom.hex(4) 212 | data = File.open(buildpack_file_path) 213 | 214 | data.binmode if data.respond_to?(:binmode) 215 | data.pos = 0 if data.respond_to?(:pos=) 216 | 217 | body << "--#{boundary}" << Excon::CR_NL 218 | body << %{Content-Disposition: form-data; name="buildpack"; filename="#{File.basename(buildpack_file_path)}"} << Excon::CR_NL 219 | body << 'Content-Type: application/x-gtar' << Excon::CR_NL 220 | body << Excon::CR_NL 221 | body << File.read(buildpack_file_path) 222 | body << Excon::CR_NL 223 | body << "--#{boundary}--" << Excon::CR_NL 224 | 225 | { 226 | :headers => { 'Content-Type' => %{multipart/form-data; boundary="#{boundary}"} }, 227 | :body => body 228 | } 229 | end 230 | 231 | desc "publish buildpack" 232 | task :publish do 233 | buildpack_name = "heroku/ruby" 234 | puts "Publishing #{buildpack_name} buildpack" 235 | resp = connection.post(multipart_form_data("buildpacks/buildpack.tgz").merge(path: "/buildpacks/#{buildpack_name}")) 236 | puts resp.status 237 | puts resp.body 238 | end 239 | 240 | desc "tag a release" 241 | task :tag do 242 | git = Git.open(".") 243 | git.add_tag(new_version) 244 | puts "Created tag #{new_version}" 245 | 246 | remote = git.remotes.detect {|remote| remote.url.match(%r{heroku/heroku-buildpack-ruby.git$}) } 247 | puts "Pushing tag to remote #{remote}" 248 | git.push(remote, nil, true) 249 | end 250 | 251 | desc "release a new version of the buildpack" 252 | task :release do 253 | Rake::Task["buildpack:increment"].invoke 254 | raise "Please add a changelog entry for #{new_version}" unless changelog_entry? 255 | Rake::Task["buildpack:stage"].invoke 256 | Rake::Task["buildpack:publish"].invoke 257 | Rake::Task["buildpack:tag"].invoke 258 | end 259 | end 260 | 261 | begin 262 | require 'rspec/core/rake_task' 263 | 264 | desc "Run specs" 265 | RSpec::Core::RakeTask.new(:spec) do |t| 266 | t.rspec_opts = %w(-fs --color) 267 | #t.ruby_opts = %w(-w) 268 | end 269 | task :default => :spec 270 | rescue LoadError => e 271 | end 272 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Master 2 | 3 | Features: 4 | 5 | Bugfixes: 6 | 7 | ## v91 (01/16/2013) 8 | 9 | Features: 10 | 11 | * Parallel gem installation with bundler 1.5.2 12 | 13 | Bugfixes: 14 | 15 | 16 | ## v90 (01/09/2013) 17 | 18 | Features: 19 | 20 | * Rollback v89 due to bug in bundler 1.5.1 21 | 22 | Bugfixes: 23 | 24 | ## v89 (01/09/2013) 25 | 26 | Features: 27 | 28 | * Use most recent version of bundler with support for parallel Gem installation 29 | 30 | Bugfixes: 31 | 32 | ## v86 (12/11/2013) 33 | 34 | Features: 35 | 36 | Bugfixes: 37 | 38 | * 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. 39 | * Checks for `public/assets/manifest*.json` and `public/assets/manifest.yml` will now come before Rake task detection introduced in v85. 40 | 41 | ## v85 (12/05/2013) 42 | 43 | Features: 44 | 45 | 46 | Bugfixes: 47 | 48 | * Any errors in a Rakefile will now be explicitly shown as such instead of hidden in a `assets:precompile` task detection failure (#171) 49 | * Now using correct default "hobby" database #179 50 | 51 | ## v84 (11/06/2013) 52 | 53 | Features: 54 | 55 | * 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. 56 | 57 | 58 | Bugfixes: 59 | 60 | * Fix default gem cache 61 | 62 | ## v83 (10/29/2013) 63 | 64 | Features: 65 | 66 | * RubyVersion extracted into its own class 67 | * Release no longer requires language_pack 68 | * Detect no longer requires language_pack 69 | * Downloads with curl now retry on failed connections, pass exit status appropriately 70 | 71 | Bugfixes: 72 | 73 | * Errors in Gemfiles will no longer show up as bad ruby versions #36 74 | * Fix warning warning libjffi-1.2.so on < JRuby 1.7.3 75 | 76 | ## v82 (10/28/2013) 77 | 78 | Bugfixes: 79 | 80 | * Rails 3 deploys that do not successfully run `assets:precompile` will now fail. 81 | 82 | ## v81 (10/15/2013) 83 | 84 | Features: 85 | 86 | * add Default Bundler Cache for new Ruby 2.0.0 apps 87 | * use Virginia S3 bucket instead of Cloudfront 88 | 89 | ## v80 (9/23/2013) 90 | 91 | Features: 92 | 93 | * Cache 50mb of Rails 4 intermediate cache 94 | * Support for Ruby 2.1.0 95 | 96 | Bugfixes: 97 | 98 | * Disable invoke dynamic on JRuby by default until JDK stabalizes it 99 | 100 | ## v79 (9/3/2013) 101 | 102 | Bugfixes: 103 | 104 | * Remove LPXC debug output when `DEBUG` env var is set (#141) 105 | * Symlink ruby.exe, so Rails 4 bins work for Windows (#139) 106 | 107 | ## v78 (8/28/2013) 108 | 109 | Features: 110 | 111 | * Don't add plugins if already gems 112 | 113 | Bugfixes: 114 | 115 | * Fix issue #127 Race condition with LPXC 116 | 117 | ## v77 (8/5/2013) 118 | 119 | Features: 120 | 121 | * Force nokogiri to compile with system libs 122 | 123 | ## v76 (7/29/2013) 124 | 125 | Bugfixes: 126 | 127 | * fix request_id for instrumentation to follow standard 128 | 129 | ## v75 (7/29/2013) 130 | 131 | Features: 132 | 133 | * add request_id to instrumentation 134 | * switchover to rubinius hosted rbx binaries 135 | 136 | Bugfixes: 137 | 138 | * OpenJDK version was rolled back, stop special casing JRuby 1.7.3. 139 | 140 | ## v74 (7/24/2013) 141 | 142 | Bugfixes: 143 | 144 | * Lock JRuby 1.7.3 and lower to older version of JDK due to 145 | 146 | ## v73 (7/23/2013) 147 | 148 | * Revert to v69 due to asset:precompile bugs 149 | 150 | ## v72 (7/23/2013) 151 | 152 | Bugfixes: 153 | 154 | * Fix rake task detection for Rails 3 (@hynkle, #118) 155 | 156 | ## v71 (7/18/2013) 157 | 158 | * Revert to v69 due to asset:precompile bugs 159 | 160 | ## v70 (7/18/2013) 161 | 162 | Bugfixes: 163 | 164 | * Don't silently fail rake task checks (@gabrielg, #34) 165 | 166 | ## v69 (7/16/2013) 167 | 168 | Bugfixes: 169 | 170 | * Add spacing to end of instrumentation 171 | 172 | ## v68 (7/16/2013) 173 | 174 | Features: 175 | 176 | * Log buildpack name and entering rails3/4 compile 177 | 178 | ## v67 (7/10/2013) 179 | 180 | Features: 181 | 182 | * Fetcher uses CDN if available 183 | * Add buildpack_version to the instrumentation output 184 | 185 | Bugfixes: 186 | 187 | * Don't print DEBUG messages for lxpc when env var is present 188 | * Fix ruby gemfile warning line for JRuby 189 | 190 | ## v66 (7/9/2013) 191 | 192 | Bugfixes: 193 | 194 | * Include logtoken properly 195 | 196 | ## v65 (7/9/2013) 197 | 198 | Features: 199 | 200 | * Instrument timing infrastructure for the buildpack 201 | 202 | Bugfixes: 203 | 204 | * Fix DATABASE_URL to use jdbc-postgres for JRuby (@jkrall, #116) 205 | 206 | ## v64 (6/19/2013) 207 | 208 | Features: 209 | 210 | * only download one copy of bundler per process (@dpiddy, #69) 211 | * roll up all warnings for end of push output 212 | * write database.yml for Rails 4 213 | 214 | Bugfixes: 215 | 216 | * fix sqlite3 error messaging detection 217 | 218 | ## v63 (6/17/2013) 219 | 220 | Features: 221 | 222 | * Lock default ruby if default ruby is used 223 | * Change default ruby to 2.0.0 224 | * Stop using the stack image ruby and always vendor ruby 225 | 226 | ## v62 (5/21/2013) 227 | 228 | Bugfixes: 229 | 230 | * Correctly detect asset manifest files in Rails 4 231 | * Fix jruby 1.8.7 bundler/psych require bug 232 | 233 | ## v61 (4/18/2013) 234 | 235 | Features: 236 | 237 | * Start caching the rubygems version used. 238 | 239 | Bugfixes: 240 | 241 | * Rebuild bundler cache if rubygems 2 is detected. Bugfixes in later rubygems. 242 | 243 | ## v60 (4/17/2013) 244 | 245 | Security: 246 | 247 | * Disable Java RMI Remote Classloading for CVE-2013-1537, 248 | 249 | ## v59 (4/4/2013) 250 | 251 | Bugfixes: 252 | 253 | * Change JVM S3 bucket 254 | 255 | ## v58 (3/19/2013) 256 | 257 | Bugfixes: 258 | 259 | * Fix ruby 1.8.7 not being able to compile native extensions 260 | 261 | ## v57 (3/18/2013) 262 | 263 | Bugfixes: 264 | 265 | * Fix git gemspec bug in bundler 266 | 267 | ## v56 (3/11/2013) 268 | 269 | Bugfixes: 270 | 271 | * Upgrade bundler to 1.3.2 to fix --dry-clean/Would have removed bug in bundle clean, part 2. 272 | 273 | ## v55 (3/7/2013) 274 | 275 | Bugfixes: 276 | 277 | * Revert back to Bundler 1.3.0.pre.5, see https://gist.github.com/mattonrails/e063caf86962995e7ba0 278 | 279 | ## v54 (3/7/2013) 280 | 281 | Bugfixes: 282 | 283 | * Upgrade bundler to 1.3.2 to fix --dry-clean/Would have removed bug in bundle clean 284 | 285 | ## v53 (3/6/2013) 286 | 287 | Bugfixes: 288 | 289 | * bin/detect for Rails 3 and 4 will use railties for detection vs the rails gem 290 | * bin/detect does not error out when Gemfile + Gemfile.lock are missing 291 | 292 | ## v52 (2/25/2013) 293 | 294 | Bugfixes: 295 | 296 | * Revert back to 1.3.0.pre.5 due to bundler warnings 297 | 298 | ## v51 (2/25/2013) 299 | 300 | Features: 301 | 302 | * Initial Rails 4 beta support 303 | * Upgrade bundler to 1.3.0 304 | 305 | Bugfixes: 306 | 307 | * Better buildpack detection through Gemfile.lock gems 308 | 309 | ## v50 (1/31/2013) 310 | 311 | Features: 312 | 313 | * Restore ruby deploys back to normal 314 | 315 | ## v49 (1/30/2013) 316 | 317 | Features: 318 | 319 | * Re-enable ruby deploys for apps just using the heroku cache 320 | * Display ruby version change when busting the cache 321 | 322 | ## v48 (1/30/2013) 323 | 324 | Features: 325 | 326 | * Update deploy error message copy to link to status incident. 327 | 328 | ## v47 (1/30/2013) 329 | 330 | Features: 331 | 332 | * Disable ruby deploys due to rubygems.org compromise 333 | 334 | ## v46 (1/10/2013) 335 | 336 | Features: 337 | 338 | * Upgrade Bundler to 1.3.0.pre.5 339 | * bundler binstubs now go in vendor/bundle/bin 340 | 341 | ## v45 (12/14/2012) 342 | 343 | Features: 344 | 345 | * Stop setting env vars in bin/release now that login-shell is released 346 | * Enable Invoke Dynamic on JRuby by default 347 | * GEM_PATH is now updated on each push 348 | 349 | ## v44 (12/14/2012) 350 | 351 | Faulty Release 352 | 353 | ## v43 (12/13/2012) 354 | 355 | Features: 356 | 357 | * Upgrade Bundler to 1.3.0.pre.2 358 | 359 | ## v42 (11/26/2012) 360 | 361 | Features: 362 | 363 | * Upgrade Bundler to 1.2.2 to fix Ruby 2.0.0/YAML issues 364 | 365 | ## v41 (11/1/2012) 366 | 367 | Features: 368 | 369 | * Enable ruby 2.0.0 support for testing 370 | 371 | ## v40 (10/14/2012) 372 | 373 | Features: 374 | 375 | * Cache version of the buildpack we used to deploy 376 | * Purge cache when v38 is detected 377 | 378 | ## v39 (10/14/2012) 379 | 380 | Bugfixes: 381 | 382 | * Don't display cache clearing message for new apps 383 | * Actually clear bundler cache on ruby version change 384 | 385 | ## v38 (10/14/2012) 386 | 387 | Bugfixes: 388 | 389 | * Stop bundle cache from continually growing 390 | 391 | ## v37 (10/12/2012) 392 | 393 | Bugfixes: 394 | 395 | * Remove temporary workaround from v36. 396 | * Clear bundler cache upon Ruby version change 397 | 398 | ## v36 (10/12/2012) 399 | 400 | Bugfixes: 401 | 402 | * Always clear the cache for ruby 1.9.3 as a temporary workaround due to the security upgrade 403 | 404 | ## v35 (9/19/2012) 405 | 406 | Features: 407 | 408 | * Upgrade to Bundler 1.2.1 409 | * Display bundle clean output 410 | * More resilent to rubygems.org API outages 411 | 412 | Bugfixes: 413 | 414 | * `bundle clean` works again 415 | 416 | ## v34 (8/30/2012) 417 | 418 | Features: 419 | 420 | * Upgrade to Bundler 1.2.0 421 | 422 | ## v33 (8/9/2012) 423 | 424 | Features: 425 | 426 | * Upgrade to Bundler 1.2.0.rc.2 427 | * vendor JDK7 for JRuby, but disable invoke dynamic 428 | 429 | ## v29 (7/19/2012) 430 | 431 | Features: 432 | 433 | * support .profile.d/ruby.sh 434 | * sync stdout so that the buildpack streams even in non-interactive shells 435 | * Upgrade to Bundler 1.2.0.rc 436 | 437 | ## v28 (7/16/2012) 438 | 439 | Features: 440 | 441 | * Vendor OpenJDK6 into slug when using JRuby 442 | * ruby version support for ruby 1.8.7 via bundler's ruby DSL 443 | 444 | Bugfixes: 445 | 446 | * sqlite3 error gets displayed again 447 | 448 | ## v27 (6/14/2012) 449 | 450 | Bugfixes: 451 | 452 | * Remove `vendor/bundle` message only appears when dir actually exists 453 | 454 | ## v26 (6/14/2012) 455 | 456 | Features: 457 | 458 | * print message when assets:precompile finishes successfully 459 | * Remove `vendor/bundle` if user commits it to their git repo. 460 | 461 | ## v25 (6/12/2012) 462 | 463 | Features: 464 | 465 | * support "ruby-xxx-jruby-yyy" for jruby detection packages 466 | 467 | ## v24 (6/7/2012) 468 | 469 | Features: 470 | 471 | * removes bundler cache in the slug, to minimize slug size (@stevenh512, #16) 472 | * optimize push time with caching 473 | 474 | ## v23 (5/8/2012) 475 | 476 | Bugfixes: 477 | 478 | * fix ruby version bug with "fatal:-Not-a-git-repository" 479 | 480 | ## v22 (5/7/2012) 481 | 482 | Features: 483 | 484 | * bundler 1.2.0.pre 485 | * ruby version support for ruby 1.9.2/1.9.3 via bundler's ruby DSL 486 | 487 | Deprecation: 488 | 489 | * ENV['RUBY_VERSION'] in favor of bundler's ruby DSL 490 | 491 | ## v21 (3/21/2012) 492 | 493 | Features: 494 | 495 | * bundler 1.1.2 496 | 497 | ## v20 (3/12/2012) 498 | 499 | Features: 500 | 501 | * bundler 1.1.0 \o/ 502 | 503 | ## v19 (1/25/2012) 504 | 505 | Bugfixes: 506 | 507 | * fix native extension building for rbx 2.0.0dev 508 | 509 | ## v18 (1/18/2012) 510 | 511 | Features: 512 | 513 | * JRuby support 514 | * rbx 2.0.0dev support 515 | 516 | Bugfixes: 517 | 518 | * force db password to be a string in the yaml file 519 | 520 | ## v17 (12/29/2011) 521 | 522 | Features: 523 | 524 | * bundler 1.1.rc.7 525 | 526 | ## v16 (12/29/2011) 527 | 528 | Features: 529 | 530 | * pass DATABASE_URL to rails 3.1 assets:precompile rake task detection 531 | 532 | ## v15 (12/27/2011) 533 | 534 | Features: 535 | 536 | * bundler 1.1.rc.6 537 | 538 | ## v14 (12/22/2011) 539 | 540 | Bugfixes: 541 | 542 | * stop freedom patching syck in ruby 1.9.3+ 543 | 544 | ## v13 (12/15/2011) 545 | 546 | Features: 547 | 548 | * bundler 1.1.rc.5 549 | 550 | ## v12 (12/13/2011) 551 | 552 | Bugfixes: 553 | 554 | * syck workaround for yaml/psych issues 555 | 556 | ## v11 (12/12/2011) 557 | 558 | Features: 559 | 560 | * bundler 1.1.rc.3 561 | 562 | ## v10 (11/23/2011) 563 | 564 | Features: 565 | 566 | * bundler binstubs 567 | * dynamic slug_vendor_base detection 568 | 569 | Bugfixes: 570 | 571 | * don't show sqlite3 error if it's in a bundle without group on failed bundle install 572 | 573 | ## v9 (11/14/2011) 574 | 575 | Features: 576 | 577 | * rbx 1.2.4 support 578 | * print out RUBY_VERSION being used 579 | 580 | Bugfixes: 581 | 582 | * don't leave behind ruby_versions.yml 583 | 584 | ## v8 (11/8/2011) 585 | 586 | Features: 587 | 588 | * use vm as part of RUBY_VERSION 589 | 590 | ## v7 (11/8/2011) 591 | 592 | Features: 593 | 594 | * ruby 1.9.3 support 595 | * specify ruby versions using RUBY_VERSION build var 596 | 597 | Bugfixes: 598 | 599 | * move "bin/" to the front of the PATH, so apps can override existing bins 600 | 601 | ## v6 (11/2/2011) 602 | 603 | Features: 604 | 605 | * add sqlite3 warning when detected on bundle install error 606 | 607 | Bugfixes: 608 | 609 | * Change gem detection to use lockfile parser 610 | * use `$RACK_ENV` when thin is detected for rack apps 611 | -------------------------------------------------------------------------------- /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/version" 9 | 10 | # base Ruby Language Pack. This is for any base ruby app. 11 | class LanguagePack::Ruby < LanguagePack::Base 12 | GSL_VENDOR_URL = "https://s3.amazonaws.com/gsl_bin/gsl-1.15.tgz" 13 | NAME = "ruby" 14 | LIBYAML_VERSION = "0.1.4" 15 | LIBYAML_PATH = "libyaml-#{LIBYAML_VERSION}" 16 | BUNDLER_VERSION = "1.5.2" 17 | BUNDLER_GEM_PATH = "bundler-#{BUNDLER_VERSION}" 18 | NODE_VERSION = "0.4.7" 19 | NODE_JS_BINARY_PATH = "node-#{NODE_VERSION}" 20 | JVM_BASE_URL = "http://heroku-jdk.s3.amazonaws.com" 21 | LATEST_JVM_VERSION = "openjdk7-latest" 22 | LEGACY_JVM_VERSION = "openjdk1.7.0_25" 23 | DEFAULT_RUBY_VERSION = "ruby-2.0.0" 24 | RBX_BASE_URL = "http://binaries.rubini.us/heroku" 25 | 26 | # detects if this is a valid Ruby app 27 | # @return [Boolean] true if it's a Ruby app 28 | def self.use? 29 | instrument "ruby.use" do 30 | File.exist?("Gemfile") 31 | end 32 | end 33 | 34 | def self.bundler 35 | @bundler ||= LanguagePack::Helpers::BundlerWrapper.new 36 | end 37 | 38 | def bundler 39 | self.class.bundler 40 | end 41 | 42 | def self.bundle 43 | bundler.lockfile_parser 44 | end 45 | 46 | def bundle 47 | self.class.bundle 48 | end 49 | 50 | def bundler_path 51 | bundler.bundler_path 52 | end 53 | 54 | def initialize(build_path, cache_path=nil) 55 | super(build_path, cache_path) 56 | @fetchers[:jvm] = LanguagePack::Fetcher.new(JVM_BASE_URL) 57 | @fetchers[:rbx] = LanguagePack::Fetcher.new(RBX_BASE_URL) 58 | end 59 | 60 | def name 61 | "Ruby" 62 | end 63 | 64 | def default_addons 65 | instrument "ruby.default_addons" do 66 | add_dev_database_addon 67 | end 68 | end 69 | 70 | def default_config_vars 71 | instrument "ruby.default_config_vars" do 72 | vars = { 73 | "LANG" => "en_US.UTF-8", 74 | "PATH" => default_path, 75 | "GEM_PATH" => slug_vendor_base, 76 | "LD_LIBRARY_PATH" => ld_path, 77 | } 78 | 79 | ruby_version.jruby? ? vars.merge({ 80 | "JAVA_OPTS" => default_java_opts, 81 | "JRUBY_OPTS" => default_jruby_opts, 82 | "JAVA_TOOL_OPTIONS" => default_java_tool_options 83 | }) : vars 84 | end 85 | end 86 | 87 | def default_process_types 88 | instrument "ruby.default_process_types" do 89 | { 90 | "rake" => "bundle exec rake", 91 | "console" => "bundle exec irb" 92 | } 93 | end 94 | end 95 | 96 | def compile 97 | instrument 'ruby.compile' do 98 | # check for new app at the beginning of the compile 99 | new_app? 100 | Dir.chdir(build_path) 101 | remove_vendor_bundle 102 | install_ruby 103 | install_jvm 104 | setup_language_pack_environment 105 | setup_profiled 106 | allow_git do 107 | install_gsl 108 | run("cp -R vendor/gsl-1 /app/vendor/gsl") 109 | run("cp -R vendor/gsl-1 /app/vendor/gsl-1") 110 | install_language_pack_gems 111 | build_bundler 112 | create_database_yml 113 | install_binaries 114 | run_assets_precompile_rake_task 115 | end 116 | super 117 | end 118 | end 119 | 120 | private 121 | 122 | # the base PATH environment variable to be used 123 | # @return [String] the resulting PATH 124 | def default_path 125 | "bin:#{bundler_binstubs_path}:/usr/local/bin:/usr/bin:/bin:/app/vendor/gsl-1/bin" 126 | end 127 | 128 | def ld_path 129 | "/app/vendor/gsl-1/lib" 130 | end 131 | 132 | # the relative path to the bundler directory of gems 133 | # @return [String] resulting path 134 | def slug_vendor_base 135 | instrument 'ruby.slug_vendor_base' do 136 | if @slug_vendor_base 137 | @slug_vendor_base 138 | elsif ruby_version.ruby_version == "1.8.7" 139 | @slug_vendor_base = "vendor/bundle/1.8" 140 | else 141 | @slug_vendor_base = run_no_pipe(%q(ruby -e "require 'rbconfig';puts \"vendor/bundle/#{RUBY_ENGINE}/#{RbConfig::CONFIG['ruby_version']}\"")).chomp 142 | error "Problem detecting bundler vendor directory: #{@slug_vendor_base}" unless $?.success? 143 | end 144 | end 145 | end 146 | 147 | # the relative path to the vendored ruby directory 148 | # @return [String] resulting path 149 | def slug_vendor_ruby 150 | "vendor/#{ruby_version.version_without_patchlevel}" 151 | end 152 | 153 | # the relative path to the vendored jvm 154 | # @return [String] resulting path 155 | def slug_vendor_jvm 156 | "vendor/jvm" 157 | end 158 | 159 | # the absolute path of the build ruby to use during the buildpack 160 | # @return [String] resulting path 161 | def build_ruby_path 162 | "/tmp/#{ruby_version.version_without_patchlevel}" 163 | end 164 | 165 | # fetch the ruby version from bundler 166 | # @return [String, nil] returns the ruby version if detected or nil if none is detected 167 | def ruby_version 168 | instrument 'ruby.ruby_version' do 169 | return @ruby_version if @ruby_version 170 | new_app = !File.exist?("vendor/heroku") 171 | last_version_file = "buildpack_ruby_version" 172 | last_version = nil 173 | last_version = @metadata.read(last_version_file).chomp if @metadata.exists?(last_version_file) 174 | 175 | @ruby_version = LanguagePack::RubyVersion.new(bundler, 176 | is_new: new_app, 177 | last_version: last_version) 178 | return @ruby_version 179 | end 180 | end 181 | 182 | # default JAVA_OPTS 183 | # return [String] string of JAVA_OPTS 184 | def default_java_opts 185 | "-Xmx384m -Xss512k -XX:+UseCompressedOops -Dfile.encoding=UTF-8" 186 | end 187 | 188 | # default JRUBY_OPTS 189 | # return [String] string of JRUBY_OPTS 190 | def default_jruby_opts 191 | "-Xcompile.invokedynamic=false" 192 | end 193 | 194 | # default JAVA_TOOL_OPTIONS 195 | # return [String] string of JAVA_TOOL_OPTIONS 196 | def default_java_tool_options 197 | "-Djava.rmi.server.useCodebaseOnly=true" 198 | end 199 | 200 | # list the available valid ruby versions 201 | # @note the value is memoized 202 | # @return [Array] list of Strings of the ruby versions available 203 | def ruby_versions 204 | return @ruby_versions if @ruby_versions 205 | 206 | Dir.mktmpdir("ruby_versions-") do |tmpdir| 207 | Dir.chdir(tmpdir) do 208 | @fetchers[:buildpack].fetch("ruby_versions.yml") 209 | @ruby_versions = YAML::load_file("ruby_versions.yml") 210 | end 211 | end 212 | 213 | @ruby_versions 214 | end 215 | 216 | # sets up the environment variables for the build process 217 | def setup_language_pack_environment 218 | instrument 'ruby.setup_language_pack_environment' do 219 | setup_ruby_install_env 220 | 221 | config_vars = default_config_vars.each do |key, value| 222 | ENV[key] ||= value 223 | end 224 | ENV["GEM_HOME"] = slug_vendor_base 225 | ENV["GEM_PATH"] = slug_vendor_base 226 | ENV["PATH"] = "#{ruby_install_binstub_path}:#{slug_vendor_base}/bin:#{config_vars["PATH"]}" 227 | end 228 | end 229 | 230 | # sets up the profile.d script for this buildpack 231 | def setup_profiled 232 | instrument 'setup_profiled' do 233 | set_env_override "GEM_PATH", "$HOME/#{slug_vendor_base}:$GEM_PATH" 234 | set_env_default "LANG", "en_US.UTF-8" 235 | set_env_override "PATH", "$HOME/bin:$HOME/#{slug_vendor_base}/bin:$HOME/#{bundler_binstubs_path}:$PATH" 236 | 237 | if ruby_version.jruby? 238 | set_env_default "JAVA_OPTS", default_java_opts 239 | set_env_default "JRUBY_OPTS", default_jruby_opts 240 | set_env_default "JAVA_TOOL_OPTIONS", default_java_tool_options 241 | end 242 | end 243 | end 244 | 245 | # install the vendored ruby 246 | # @return [Boolean] true if it installs the vendored ruby and false otherwise 247 | def install_ruby 248 | instrument 'ruby.install_ruby' do 249 | return false unless ruby_version 250 | 251 | invalid_ruby_version_message = <= Gem::Version.new("1.7.4") 332 | LATEST_JVM_VERSION 333 | else 334 | LEGACY_JVM_VERSION 335 | end 336 | 337 | topic "Installing JVM: #{jvm_version}" 338 | 339 | FileUtils.mkdir_p(slug_vendor_jvm) 340 | Dir.chdir(slug_vendor_jvm) do 341 | @fetchers[:jvm].fetch_untar("#{jvm_version}.tar.gz") 342 | end 343 | 344 | bin_dir = "bin" 345 | FileUtils.mkdir_p bin_dir 346 | Dir["#{slug_vendor_jvm}/bin/*"].each do |bin| 347 | run("ln -s ../#{bin} #{bin_dir}") 348 | end 349 | end 350 | end 351 | end 352 | 353 | # find the ruby install path for its binstubs during build 354 | # @return [String] resulting path or empty string if ruby is not vendored 355 | def ruby_install_binstub_path 356 | @ruby_install_binstub_path ||= 357 | if ruby_version.build? 358 | "#{build_ruby_path}/bin" 359 | elsif ruby_version 360 | "#{slug_vendor_ruby}/bin" 361 | else 362 | "" 363 | end 364 | end 365 | 366 | # setup the environment so we can use the vendored ruby 367 | def setup_ruby_install_env 368 | instrument 'ruby.setup_ruby_install_env' do 369 | ENV["PATH"] = "#{ruby_install_binstub_path}:#{ENV["PATH"]}" 370 | 371 | if ruby_version.jruby? 372 | ENV['JAVA_OPTS'] = default_java_opts 373 | end 374 | end 375 | end 376 | 377 | # list of default gems to vendor into the slug 378 | # @return [Array] resulting list of gems 379 | def gems 380 | [BUNDLER_GEM_PATH] 381 | end 382 | 383 | # installs vendored gems into the slug 384 | def install_language_pack_gems 385 | instrument 'ruby.install_language_pack_gems' do 386 | FileUtils.mkdir_p(slug_vendor_base) 387 | Dir.chdir(slug_vendor_base) do |dir| 388 | gems.each do |g| 389 | @fetchers[:buildpack].fetch_untar("#{g}.tgz") 390 | end 391 | Dir["bin/*"].each {|path| run("chmod 755 #{path}") } 392 | end 393 | end 394 | end 395 | 396 | # default set of binaries to install 397 | # @return [Array] resulting list 398 | def binaries 399 | add_node_js_binary 400 | end 401 | 402 | # vendors binaries into the slug 403 | def install_binaries 404 | instrument 'ruby.install_binaries' do 405 | binaries.each {|binary| install_binary(binary) } 406 | Dir["bin/*"].each {|path| run("chmod +x #{path}") } 407 | end 408 | end 409 | 410 | # vendors individual binary into the slug 411 | # @param [String] name of the binary package from S3. 412 | # Example: https://s3.amazonaws.com/language-pack-ruby/node-0.4.7.tgz, where name is "node-0.4.7" 413 | def install_binary(name) 414 | bin_dir = "bin" 415 | FileUtils.mkdir_p bin_dir 416 | Dir.chdir(bin_dir) do |dir| 417 | @fetchers[:buildpack].fetch_untar("#{name}.tgz") 418 | end 419 | end 420 | 421 | # removes a binary from the slug 422 | # @param [String] relative path of the binary on the slug 423 | def uninstall_binary(path) 424 | FileUtils.rm File.join('bin', File.basename(path)), :force => true 425 | end 426 | 427 | def install_gsl 428 | topic("Installing gsl") 429 | bin_dir = "vendor/gsl-1" 430 | FileUtils.mkdir_p bin_dir 431 | Dir.chdir(bin_dir) do |dir| 432 | run("curl #{GSL_VENDOR_URL} -s -o - | tar xzf -") 433 | end 434 | end 435 | 436 | def load_default_cache? 437 | new_app? && ruby_version.default? 438 | end 439 | 440 | # loads a default bundler cache for new apps to speed up initial bundle installs 441 | def load_default_cache 442 | instrument "ruby.load_default_cache" do 443 | if load_default_cache? 444 | puts "New app detected loading default bundler cache" 445 | patchlevel = run("ruby -e 'puts RUBY_PATCHLEVEL'").chomp 446 | cache_name = "#{DEFAULT_RUBY_VERSION}-p#{patchlevel}-default-cache" 447 | @fetchers[:buildpack].fetch_untar("#{cache_name}.tgz") 448 | end 449 | end 450 | end 451 | 452 | # install libyaml into the LP to be referenced for psych compilation 453 | # @param [String] tmpdir to store the libyaml files 454 | def install_libyaml(dir) 455 | instrument 'ruby.install_libyaml' do 456 | FileUtils.mkdir_p dir 457 | Dir.chdir(dir) do |dir| 458 | @fetchers[:buildpack].fetch_untar("#{LIBYAML_PATH}.tgz") 459 | end 460 | end 461 | end 462 | 463 | # remove `vendor/bundle` that comes from the git repo 464 | # in case there are native ext. 465 | # users should be using `bundle pack` instead. 466 | # https://github.com/heroku/heroku-buildpack-ruby/issues/21 467 | def remove_vendor_bundle 468 | if File.exists?("vendor/bundle") 469 | warn(<&1") 532 | end 533 | end 534 | end 535 | 536 | if $?.success? 537 | puts "Bundle completed (#{"%.2f" % bundle_time}s)" 538 | log "bundle", :status => "success" 539 | puts "Cleaning up the bundler cache." 540 | instrument "ruby.bundle_clean" do 541 | # Only show bundle clean output when not using default cache 542 | if load_default_cache? 543 | run "bundle clean > /dev/null" 544 | else 545 | pipe "#{bundle_bin} clean 2> /dev/null" 546 | end 547 | end 548 | cache.store ".bundle" 549 | cache.store "vendor/bundle" 550 | 551 | # Keep gem cache out of the slug 552 | FileUtils.rm_rf("#{slug_vendor_base}/cache") 553 | else 554 | log "bundle", :status => "failure" 555 | error_message = "Failed to install gems via Bundler." 556 | puts "Bundler Output: #{bundler_output}" 557 | if bundler_output.match(/An error occurred while installing sqlite3/) 558 | error_message += < 636 | 637 | <%= ENV["RAILS_ENV"] || ENV["RACK_ENV"] %>: 638 | <%= attribute "adapter", adapter %> 639 | <%= attribute "database", database %> 640 | <%= attribute "username", username %> 641 | <%= attribute "password", password, true %> 642 | <%= attribute "host", host %> 643 | <%= attribute "port", port %> 644 | 645 | <% params.each do |key, value| %> 646 | <%= key %>: <%= value.first %> 647 | <% end %> 648 | DATABASE_YML 649 | end 650 | end 651 | end 652 | end 653 | 654 | def rake 655 | @rake ||= LanguagePack::Helpers::RakeRunner.new( 656 | bundler.has_gem?("rake") || ruby_version.rake_is_vendored? 657 | ).load_rake_tasks! 658 | end 659 | 660 | # executes the block with GIT_DIR environment variable removed since it can mess with the current working directory git thinks it's in 661 | # @param [block] block to be executed in the GIT_DIR free context 662 | def allow_git(&blk) 663 | git_dir = ENV.delete("GIT_DIR") # can mess with bundler 664 | blk.call 665 | ENV["GIT_DIR"] = git_dir 666 | end 667 | 668 | # decides if we need to enable the dev database addon 669 | # @return [Array] the database addon if the pg gem is detected or an empty Array if it isn't. 670 | def add_dev_database_addon 671 | bundler.has_gem?("pg") ? ['heroku-postgresql:hobby-dev'] : [] 672 | end 673 | 674 | # decides if we need to install the node.js binary 675 | # @note execjs will blow up if no JS RUNTIME is detected and is loaded. 676 | # @return [Array] the node.js binary path if we need it or an empty Array 677 | def add_node_js_binary 678 | bundler.has_gem?('execjs') ? [NODE_JS_BINARY_PATH] : [] 679 | end 680 | 681 | def run_assets_precompile_rake_task 682 | instrument 'ruby.run_assets_precompile_rake_task' do 683 | 684 | precompile = rake.task("assets:precompile") 685 | return true unless precompile.is_defined? 686 | 687 | topic "Running: rake assets:precompile" 688 | precompile.invoke 689 | if precompile.success? 690 | puts "Asset precompilation completed (#{"%.2f" % precompile.time}s)" 691 | else 692 | log "assets_precompile", :status => "failure" 693 | error "Precompiling assets failed." 694 | end 695 | end 696 | end 697 | 698 | def bundler_cache 699 | "vendor/bundle" 700 | end 701 | 702 | def load_bundler_cache 703 | instrument "ruby.load_bundler_cache" do 704 | cache.load "vendor" 705 | 706 | full_ruby_version = run_stdout(%q(ruby -v)).chomp 707 | rubygems_version = run_stdout(%q(gem -v)).chomp 708 | heroku_metadata = "vendor/heroku" 709 | old_rubygems_version = nil 710 | ruby_version_cache = "ruby_version" 711 | buildpack_version_cache = "buildpack_version" 712 | bundler_version_cache = "bundler_version" 713 | rubygems_version_cache = "rubygems_version" 714 | 715 | old_rubygems_version = @metadata.read(ruby_version_cache).chomp if @metadata.exists?(ruby_version_cache) 716 | 717 | load_default_cache 718 | 719 | # fix bug from v37 deploy 720 | if File.exists?("vendor/ruby_version") 721 | puts "Broken cache detected. Purging build cache." 722 | cache.clear("vendor") 723 | FileUtils.rm_rf("vendor/ruby_version") 724 | purge_bundler_cache 725 | # fix bug introduced in v38 726 | elsif !@metadata.exists?(buildpack_version_cache) && @metadata.exists?(ruby_version_cache) 727 | puts "Broken cache detected. Purging build cache." 728 | purge_bundler_cache 729 | elsif cache.exists?(bundler_cache) && @metadata.exists?(ruby_version_cache) && full_ruby_version != @metadata.read(ruby_version_cache).chomp 730 | puts "Ruby version change detected. Clearing bundler cache." 731 | puts "Old: #{@metadata.read(ruby_version_cache).chomp}" 732 | puts "New: #{full_ruby_version}" 733 | purge_bundler_cache 734 | end 735 | 736 | # fix git gemspec bug from Bundler 1.3.0+ upgrade 737 | if File.exists?(bundler_cache) && !@metadata.exists?(bundler_version_cache) && !run("find vendor/bundle/*/*/bundler/gems/*/ -name *.gemspec").include?("No such file or directory") 738 | puts "Old bundler cache detected. Clearing bundler cache." 739 | purge_bundler_cache 740 | end 741 | 742 | # fix for https://github.com/heroku/heroku-buildpack-ruby/issues/86 743 | if (!@metadata.exists?(rubygems_version_cache) || 744 | (old_rubygems_version == "2.0.0" && old_rubygems_version != rubygems_version)) && 745 | @metadata.exists?(ruby_version_cache) && @metadata.read(ruby_version_cache).chomp.include?("ruby 2.0.0p0") 746 | puts "Updating to rubygems #{rubygems_version}. Clearing bundler cache." 747 | purge_bundler_cache 748 | end 749 | 750 | # fix for https://github.com/sparklemotion/nokogiri/issues/923 751 | if @metadata.exists?(buildpack_version_cache) && (bv = @metadata.read(buildpack_version_cache).sub('v', '').to_i) && bv != 0 && bv <= 76 752 | puts "Fixing nokogiri install. Clearing bundler cache." 753 | puts "See https://github.com/sparklemotion/nokogiri/issues/923." 754 | purge_bundler_cache 755 | end 756 | 757 | FileUtils.mkdir_p(heroku_metadata) 758 | @metadata.write(ruby_version_cache, full_ruby_version, false) 759 | @metadata.write(buildpack_version_cache, BUILDPACK_VERSION, false) 760 | @metadata.write(bundler_version_cache, BUNDLER_VERSION, false) 761 | @metadata.write(rubygems_version_cache, rubygems_version, false) 762 | @metadata.save 763 | end 764 | end 765 | 766 | def purge_bundler_cache 767 | instrument "ruby.purge_bundler_cache" do 768 | FileUtils.rm_rf(bundler_cache) 769 | cache.clear bundler_cache 770 | # need to reinstall language pack gems 771 | install_language_pack_gems 772 | end 773 | end 774 | end 775 | --------------------------------------------------------------------------------