# Copyright (C) 2011-2013 RightScale, Inc, All Rights Reserved Worldwide. # # THIS PROGRAM IS CONFIDENTIAL AND PROPRIETARY TO RIGHTSCALE # AND CONSTITUTES A VALUABLE TRADE SECRET. Any unauthorized use, # reproduction, modification, or disclosure of this program is # strictly prohibited. Any use of this program by an authorized # licensee is strictly subject to the terms and conditions, # including confidentiality obligations, set forth in the applicable # License Agreement between RightScale.com, Inc. and # the licensee require 'fileutils' require 'tmpdir' module RightConf # Specify rbenv and ruby-build git revision for installation RBENV_GIT_REPO = 'git://github.com/sstephenson/rbenv.git' RBENV_GIT_REVISION = "605e691bff4863e5014ba5ad750ad65e3278aea3" RUBYBUILD_GIT_REPO = 'git://github.com/sstephenson/ruby-build.git' RUBY_BUILD_GIT_REVISION = "57cb1e98c6ec695c130e369629d02417ccc1a51a" # Local installation target folders RBENV_INSTALL_TARGET = "#{ENV['HOME']}/.rbenv" RUBYBUILD_TARGET = "#{RBENV_INSTALL_TARGET}/plugins/ruby-build" RUBYBUILD_DEFINITIONS_PATH = ::File.join(RUBYBUILD_TARGET, 'share', 'ruby-build') # rubygems RUBYGEMS_URI_PREFIX = 'http://production.cf.rubygems.org/rubygems' class RubyConfigurator include Configurator register :ruby description "Installs ruby interpreter and rubygems.\n" + 'Installs and uses rbenv on supported (i.e. non-Windows) platforms' setting 'version', 'Ruby version using rbenv notation (see "rbenv versions")', :required => true setting 'rubygems', 'RubyGems version using semantic versioning' updater lambda { update_rbenv_installation } # Message to display to the user when rbenv isn't in the path. PATH_ADVICE = "You should add rbenv to your path and 'eval \"$(rbenv init -)\"' to your .bash_profile / .bashrc etc. " + "so rbenv is properly initialized in new shells.\n\n" + "If you are a bash user (e.g. Mac OS X), you can add the following to ~/.bash_profile:\n\n" + " echo 'export PATH=\"$HOME/.rbenv/bin:$PATH\"' >> ~/.bash_profile\n" + " echo 'eval \"$(rbenv init -)\"' >> ~/.bash_profile\n\n" # Let configurator run, it is idempotent # # === Return # false:: Always return false def check_linux false end alias :check_darwin :check_linux alias :check_windows :check_linux # Switch to ruby version defined in settings # Use rbenv and install it if needed # # === Return # true:: Always return true def run_linux if Command.execute('rvm').success? report_fatal "rconf detected an installation of RVM, rconf now uses rbenv to configure ruby versions.\n" + "Unfortunately rbenv and RVM cannot be installed together on the same machine (see https://github.com/sstephenson/rbenv/).\n" + "Please uninstall RVM and try again ('rvm implode' will delete RVM and all installed rubies and gems).\n" + "Once RVM is uninstalled please start a new shell (you may also need to remove references to RVM from your ~/.bash_profile)" return true end check_rbenv return true if aborting check_ruby Command.set_ruby(ruby_with_rubygems_version) check_rubygems true end alias :run_darwin :run_linux # Switch to ruby version defined in settings # TBD # # === Return # true:: Always return true def run_windows end protected # Make our version specifier compatible with RVM-style specifiers (override base class accessor) def ruby_version @ruby_version ||= version.start_with?('ruby-') ? version[5..-1] : version end # appends a rubygems version specifier only when necessary. def ruby_with_rubygems_version @ruby_with_rubygems_version ||= (rubygems ? "#{ruby_version}_rubygems-#{rubygems}" : ruby_version) end def ruby_bin_path_for(version) ::File.join(RBENV_INSTALL_TARGET, 'versions', version, 'bin', 'ruby') end def gem_bin_path_for(version) ::File.join(RBENV_INSTALL_TARGET, 'versions', version, 'bin', 'gem') end # Check whether rbenv and ruby-build are installed and installs it/them if not # # === Return # true:: Always return true def check_rbenv rbenv_present = File.exist?(File.join(RBENV_INSTALL_TARGET, 'bin', 'rbenv')) res = Command.execute('rbenv') rbenv_in_path = res.success? if rbenv_present && !rbenv_in_path # Bail if rbenv isn't correctly initialized (we can't use it in this state) post_note "rconf detected rbenv is installed in #{rbenv_path} but it is not in the PATH.\n" + PATH_ADVICE aborting(true) elsif rbenv_in_path # rbenv is installed and initialized; make sure we have a suitable version res.output =~ /^rbenv ([0-9]+)\.([0-9]+)\.([0-9]+)(\-.*)?$/ maj, min, build, subhash = [$1.to_i, $2.to_i, $3.to_i, $4] if maj == 0 && min < 4 report_fatal("rconf requires rbenv version 0.4.0 or greater, you have #{maj}.#{min}.#{build}#{subhash} installed, "+ "please upgrade (e.g. via 'brew upgrade rbenv') and try again") end else # No sign of rbenv; try to install it install_rbenv_from_git end true end # Overwrite ruby-build definition file to use our own, patched version of REE and to set certain compile options # so that it REE works with newer versions of GCC as well. # # === Return # true:: Always return true def self.add_custom_ree_definition puts "Adding custom ruby enterprise edition definition to ruby-build ..." src_patched_ree = ::File.expand_path('../../../patches/ree-1.8.7-2012.02-rs-custom', __FILE__) dst_patched_ree = ::File.join(RUBYBUILD_DEFINITIONS_PATH, 'ree-1.8.7-2012.02') ::FileUtils.cp(src_patched_ree, dst_patched_ree) true end # Update rbenv and ruby-build to the version specified through the git commit version constant # # === Return # true:: Always return true def self.update_rbenv_installation puts "Updating rbenv ..." if File.directory?("#{RBENV_INSTALL_TARGET}/.git") # Perform the pull only if rbenv has been installed by using a git pull (and not through a package manager) system("cd #{RBENV_INSTALL_TARGET}; git fetch -q; git checkout -f -q #{RBENV_GIT_REVISION}") end puts "Updating ruby-build ..." if File.directory?("#{RUBYBUILD_TARGET}/.git") # Perform the pull only if ruby-build has been installed by using a git pull (and not through a package manager) # Before checking out the new branch, reset the repository to avoid conflicts system("cd #{RUBYBUILD_TARGET}; git reset -q --hard; git fetch -q; git checkout -f -q #{RUBY_BUILD_GIT_REVISION}") # Add our ree definition again because the repository has been reset RubyConfigurator.add_custom_ree_definition end true end # Install rbenv # 1. Fetch rbenv from repository and install it # 2. Fetch ruby-build from repository and install it # 3. Install custom ree installation definition into ruby-build so that our patched REE gets installed # # === Return # true:: Always return true def install_rbenv_from_git # Require: git res = Command.execute('git', '--version').success? if !res # Install git opts = { :report => true, :post_install => update_msg }.merge(abort_option('Failed to install git, required for rbenv')) PackageInstaller.install('git', opts) end # Fetch and install rbenv # *********************** # Fetch rbenv from git repo Command.execute('git', 'clone', RBENV_GIT_REPO, RBENV_INSTALL_TARGET, {:abort_on_failure => 'Git clone of rbnev failed - server down?'}) Kernel.system("cd #{RBENV_INSTALL_TARGET}; git checkout #{RBENV_GIT_REVISION}") # Fetch, install and patch ruby-build # *********************************** # Create plugins directory (target for ruby-build) ret = FileUtils.mkdir_p(RUBYBUILD_TARGET) if !ret report_fatal("Could not create directory: #{RUBYBUILD_TARGET}"); end Command.execute('git', 'clone', RUBYBUILD_GIT_REPO, RUBYBUILD_TARGET, {:abort_on_failure => 'Git clone of ruby-build failed - server down?'}) Kernel.system("cd #{RUBYBUILD_TARGET}; git checkout #{RUBY_BUILD_GIT_REVISION}") RubyConfigurator.add_custom_ree_definition end # Check .ruby-version and its content # # === Return # true:: Always return true def check_ruby if Command.execute('rbenv', 'local', ruby_with_rubygems_version).success? # always (re)install latest rconf gem because switched-to ruby may exist # but not have rconf. install_rconf(ruby_with_rubygems_version) else report_check("Installing ruby #{ruby_with_rubygems_version} (this will take a while, please be patient)") # create a ruby+rubygems definition file in order to isolate any # differences in rubygems versions between projects. ideally only a # single rubygems version will be specified for use with a given ruby # platform version. # # note that copying/renaming an existing installed ruby directory does # not work as expected because the installed files contain shebangs that # refer back to the original installed directory. if ruby_version != ruby_with_rubygems_version # ruby+rubygems definition may already exist because it was previously # created here, checked into source control, etc. dst_def_path = ::File.join(RUBYBUILD_DEFINITIONS_PATH, ruby_with_rubygems_version) unless ::File.file?(dst_def_path) src_def_path = ::File.join(RUBYBUILD_DEFINITIONS_PATH, ruby_version) if ::File.file?(src_def_path) # existing ruby platform definition may include a specifier for # the requested rubygems version or some other version but not all # definitions will have this specifier. in any case, we need to # run and then restart rconf and maintain state so we need to # create a custom ruby platform definition. # # remove any existing rubygems specifier. src_def_text = ::File.read(src_def_path) any_rubygems_regex = /^install_package\s+"rubygems\-.*$/ dst_def_text = src_def_text.gsub(any_rubygems_regex, '').gsub("\n\n", "\n").strip # append full rubygems specifier to definition to make # rbenv install do the work of installing correct rubygems version. rubygems_basename = "rubygems-#{rubygems}" rubygems_specifier_prefix = "install_package #{rubygems_basename.inspect}" rubygems_filename = "#{rubygems_basename}.tgz" rubygems_tarball_url = ::File.join(RUBYGEMS_URI_PREFIX, rubygems_filename) rubygems_specifier_full = "#{rubygems_specifier_prefix} #{rubygems_tarball_url.inspect} ruby" dst_def_text += "\n#{rubygems_specifier_full}" # write custom ruby platform definition. ::File.open(dst_def_path, 'w') { |f| f.write(dst_def_text) } else post_note "Requested ruby version definition not found: #{ruby_version}" aborting(true) end end end Platform.dispatch(ruby_with_rubygems_version) { :install_ruby } end which = Command.execute('which', 'ruby').output.strip if (which =~ %r(^(/usr)?/bin.*ruby$)) post_note "Your PATH is not setup correctly for rbenv; ('which ruby' => #{which}).\n" + PATH_ADVICE aborting(true) end # if currenly executing ruby is not our selected ruby then we must abort # and run rconf again or else bundler will install gems to wrong directory. which = Command.execute('rbenv', 'which', 'ruby').output.strip if which != ruby_bin_path_for(ruby_with_rubygems_version) post_note "rconf selected ruby #{ruby_with_rubygems_version} and needs to be restarted so the right ruby tools get activated\nPlease run 'rconf' again." aborting(true) end true end # Install given ruby version using rbenv # Install any prerequesites first # # === Parameters # ruby(String):: Ruby version compatible with rbenv # # === Return # true:: Always return true def install_ruby(ruby) Platform.dispatch(ruby) { :install_ruby_prerequisites } # Can't abort on failure rbenv install seems to exist with a non zero error code even when successful :( Command.execute('rbenv', 'install', ruby) report_success abort_message = < abort_message.chomp) install_rconf(ruby) post_note "rconf installed ruby #{ruby} and needs to be restarted so the right ruby tools get activated\nPlease run 'rconf' again." aborting(true) true end # gem install the latest greatest rconf before leaving to avoid confusion # when user tries and fails to restart rconf as indicated. assumes some # version of rubygems is provided by every ruby install. def install_rconf(ruby_selector) report_check("Installing latest rconf gem into selected ruby '#{ruby_selector}'") Command.execute(gem_bin_path_for(ruby_selector), 'install', 'rconf', '--no-rdoc', '--no-ri', :abort_on_failure => "Failed to install rconf gem in selected ruby") report_success end # Install given ruby version using rbenv # On Lion, need to setup CC env var before running rbenv install # # === Parameters # ruby(String):: Ruby version compatible with rbenv # # === Return # true:: Always return true def install_ruby_darwin(ruby) Platform.dispatch(ruby) { :install_ruby_prerequisites } c_version = `system_profiler SPDeveloperToolsDataType -xml | xpath "//*[text()='_items']/following-sibling::array/dict/child::key[text()='spdevtools_version']/following-sibling::string/text()" 2> /dev/null` env = {} # Ensure we're using a sane GCC 4.2, not Apple's LLVM-based lookalike. gcc42 = ['/usr/local/bin/gcc-4.2', '/usr/bin/gcc-4.2'].detect { |p| File.executable?(p) } if c_version =~ /^4\.2\.[0-9]+/ if gcc42.nil? report_fatal("The C compiler included with Xcode #{c_version} produces buggy ruby interpreters, please install the C compilers from https://github.com/downloads/kennethreitz/osx-gcc-installer/GCC-10.7-v2.pkg or update your version of Xcode and re-run rconf") else env['CC'] = gcc42 end end # Ruby 1.8 force-installs the Tk stdlib extension, no way to opt out. # Ensure XQuartz is installed and its headers are on the path. if ruby =~ /1\.8/ if File.directory?('/opt/X11/include') env['CC'] = gcc42 env['CPPFLAGS'] = '-I/opt/X11/include' else report_fatal("Installing ruby 1.8 (or ree 1.8) on Mac OS X requires that XQuartz be installed on the machine first, please go to http://xquartz.macosforge.org/landing/, install XQuartz and try again") end end Command.execute('rbenv', 'install', ruby, :abort_on_failure => 'Failed to install ruby', :env => env) report_success end # Make sure to install all required linux packages first # # === Return # true:: Always return true def install_ruby_prerequisites_linux_ubuntu(ruby) report_check("Installing required packages, this could take a while") packages = [] packages = %w(build-essential bison openssl libreadline6 libreadline6-dev curl git-core zlib1g zlib1g-dev libssl-dev libyaml-dev libsqlite3-0 libsqlite3-dev sqlite3 libxml2-dev libxslt-dev autoconf libc6-dev) Command.sudo('apt-get', 'install', '-y', *packages) report_success end alias install_ruby_prerequisites_linux_debian install_ruby_prerequisites_linux_ubuntu # Make sure to install all required CentOS / RedHhat packages first # NOTE: For centos 5.4 final iconv-devel might not be available :( # # === Return # true:: Always return true def install_ruby_prerequisites_linux_centos(ruby) report_check("Installing required packages, this could take a while") packages = [] if ruby =~ /^ree-|^ruby-/ packages = %w(gcc-c++ patch readline readline-devel zlib zlib-devel libyaml-devel libffi-devel openssl-devel iconv-devel) end # TBD Define packages needed for other rubies Command.sudo('yum', 'install', '-y', *packages) report_success end alias :install_ruby_prerequisites_linux_redhat :install_ruby_prerequisites_linux_centos # No pre-requesites to install ree or Matz ruby on Mac (TBD others) # # === Return # true:: Always return true def install_ruby_prerequisites_darwin(ruby) version_output = `sw_vers` if m = /ProductVersion:\s*([0-9.]+)/m.match(version_output) osx_version = m[1].split(".").map { |c| c.to_i } else raise RuntimeError, "Can't determine OS X version from sw_vers output (#{version_output})" end if (osx_version[0] > 10 || osx_version[1] > 6) report_check("installing non-LLVM GCC 4.2") PackageInstaller.install('https://raw.github.com/Homebrew/homebrew-dupes/master/apple-gcc42.rb', :abort_on_failure => "Could not install gcc 4.2") report_success end true end # check if rubygems is installed and has the expected version or else kick # off installation when required. def check_rubygems return true unless rubygems report_check("Checking for rubygems #{rubygems}") gem_bin_path = gem_bin_path_for(ruby_with_rubygems_version) result = Command.execute(gem_bin_path, '-v', :abort_on_failure => nil) current_rubygems = result.success? ? result.output.strip : '' if current_rubygems == rubygems report_success else install_rubygems(rubygems) result = Command.execute(gem_bin_path, '-v', :abort_on_failure => nil) current_rubygems = result.success? ? result.output.strip : '' if current_rubygems == rubygems report_success else post_note "Failed to update RubyGems to v#{rubygems}:\n#{result.output.strip}" report_failure end end true end # installs, upgrades, downgrades, or reinstalls rubygems to specified # version by downloading source tarball and executing 'setup.rb'. def install_rubygems(version) report_check("Installing rubygems #{version}") basename = "rubygems-#{version}" filename = "#{basename}.tgz" tarball_url = ::File.join(RUBYGEMS_URI_PREFIX, filename) ::Dir.mktmpdir do |tmpdir| ::Dir.chdir(tmpdir) do Command.execute('curl', '-O', '-f', tarball_url, :abort_on_failure => "Failed to curl #{tarball_url}") Command.execute('tar', '-xzvf', filename, :abort_on_failure => "Failed to explode #{filename}") # rubygems 'setup.rb' needs it's root dir to be the working dir. ::Dir.chdir(basename) do Command.execute(ruby_bin_path_for(ruby_with_rubygems_version), 'setup.rb', :abort_on_failure => "Failed to setup rubygems") end end end end # Produce abort on failure option # # === Parameters # message(String):: Abort message to be used in case abort option should be set # # === Return # {}:: Empty hash if 'abort_on_failure' is notset # opts(Hash):: Abort option with give message otherwise def abort_option(message) opts = abort_on_failure && { :abort_on_failure => message } || {} end end end