diff options
Diffstat (limited to 'lib/chef/provider/package/rubygems.rb')
-rw-r--r-- | lib/chef/provider/package/rubygems.rb | 548 |
1 files changed, 548 insertions, 0 deletions
diff --git a/lib/chef/provider/package/rubygems.rb b/lib/chef/provider/package/rubygems.rb new file mode 100644 index 0000000000..e60d73ab62 --- /dev/null +++ b/lib/chef/provider/package/rubygems.rb @@ -0,0 +1,548 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2008, 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/package' +require 'chef/mixin/command' +require 'chef/resource/package' +require 'chef/mixin/get_source_from_package' + +# Class methods on Gem are defined in rubygems +require 'rubygems' +# Ruby 1.9's gem_prelude can interact poorly with loading the full rubygems +# explicitly like this. Make sure rubygems/specification is always last in this +# list +require 'rubygems/version' +require 'rubygems/dependency' +require 'rubygems/spec_fetcher' +require 'rubygems/platform' +require 'rubygems/format' +require 'rubygems/dependency_installer' +require 'rubygems/uninstaller' +require 'rubygems/specification' + +class Chef + class Provider + class Package + class Rubygems < Chef::Provider::Package + class GemEnvironment + # HACK: trigger gem config load early. Otherwise it can get lazy + # loaded during operations where we've set Gem.sources to an + # alternate value and overwrite it with the defaults. + Gem.configuration + + DEFAULT_UNINSTALLER_OPTS = {:ignore => true, :executables => true} + + ## + # The paths where rubygems should search for installed gems. + # Implemented by subclasses. + def gem_paths + raise NotImplementedError + end + + ## + # A rubygems source index containing the list of gemspecs for all + # available gems in the gem installation. + # Implemented by subclasses + # === Returns + # Gem::SourceIndex + def gem_source_index + raise NotImplementedError + end + + ## + # A rubygems specification object containing the list of gemspecs for all + # available gems in the gem installation. + # Implemented by subclasses + # For rubygems >= 1.8.0 + # === Returns + # Gem::Specification + def gem_specification + raise NotImplementedError + end + + ## + # Lists the installed versions of +gem_name+, constrained by the + # version spec in +gem_dep+ + # === Arguments + # Gem::Dependency +gem_dep+ is a Gem::Dependency object, its version + # specification constrains which gems are returned. + # === Returns + # [Gem::Specification] an array of Gem::Specification objects + def installed_versions(gem_dep) + if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.8.0') + gem_specification.find_all_by_name(gem_dep.name, gem_dep.requirement) + else + gem_source_index.search(gem_dep) + end + end + + ## + # Yields to the provided block with rubygems' source list set to the + # list provided. Always resets the list when the block returns or + # raises an exception. + def with_gem_sources(*sources) + sources.compact! + original_sources = Gem.sources + Gem.sources = sources unless sources.empty? + yield + ensure + Gem.sources = original_sources + end + + ## + # Determines the candidate version for a gem from a .gem file on disk + # and checks if it matches the version contraints in +gem_dependency+ + # === Returns + # Gem::Version a singular gem version object is returned if the gem + # is available + # nil returns nil if the gem on disk doesn't match the + # version constraints for +gem_dependency+ + def candidate_version_from_file(gem_dependency, source) + spec = Gem::Format.from_file_by_path(source).spec + if spec.satisfies_requirement?(gem_dependency) + logger.debug {"#{@new_resource} found candidate gem version #{spec.version} from local gem package #{source}"} + spec.version + else + # This is probably going to end badly... + logger.warn { "#{@new_resource} gem package #{source} does not satisfy the requirements #{gem_dependency.to_s}" } + nil + end + end + + ## + # Finds the newest version that satisfies the constraints of + # +gem_dependency+. The version is determined from the cache or a + # round-trip to the server as needed. The architecture and gem + # sources will be set before making the query. + # === Returns + # Gem::Version a singular gem version object is returned if the gem + # is available + # nil returns nil if the gem could not be found + def candidate_version_from_remote(gem_dependency, *sources) + raise NotImplementedError + end + + ## + # Find the newest gem version available from Gem.sources that satisfies + # the constraints of +gem_dependency+ + def find_newest_remote_version(gem_dependency, *sources) + # DependencyInstaller sorts the results such that the last one is + # always the one it considers best. + spec_with_source = dependency_installer.find_gems_with_sources(gem_dependency).last + + spec = spec_with_source && spec_with_source[0] + version = spec && spec_with_source[0].version + if version + logger.debug { "#{@new_resource} found gem #{spec.name} version #{version} for platform #{spec.platform} from #{spec_with_source[1]}" } + version + else + source_list = sources.compact.empty? ? "[#{Gem.sources.join(', ')}]" : "[#{sources.join(', ')}]" + logger.warn { "#{@new_resource} failed to find gem #{gem_dependency} from #{source_list}" } + nil + end + end + + ## + # Installs a gem via the rubygems ruby API. + # === Options + # :sources rubygems servers to use + # Other options are passed to Gem::DependencyInstaller.new + def install(gem_dependency, options={}) + with_gem_sources(*options.delete(:sources)) do + with_correct_verbosity do + dependency_installer(options).install(gem_dependency) + end + end + end + + ## + # Uninstall the gem +gem_name+ via the rubygems ruby API. If + # +gem_version+ is provided, only that version will be uninstalled. + # Otherwise, all versions are uninstalled. + # === Options + # Options are passed to Gem::Uninstaller.new + def uninstall(gem_name, gem_version=nil, opts={}) + gem_version ? opts[:version] = gem_version : opts[:all] = true + with_correct_verbosity do + uninstaller(gem_name, opts).uninstall + end + end + + ## + # Set rubygems' user interaction to ConsoleUI or SilentUI depending + # on our current debug level + def with_correct_verbosity + Gem::DefaultUserInteraction.ui = Chef::Log.debug? ? Gem::ConsoleUI.new : Gem::SilentUI.new + yield + end + + def dependency_installer(opts={}) + Gem::DependencyInstaller.new(opts) + end + + def uninstaller(gem_name, opts={}) + Gem::Uninstaller.new(gem_name, DEFAULT_UNINSTALLER_OPTS.merge(opts)) + end + + private + + def logger + Chef::Log.logger + end + + end + + class CurrentGemEnvironment < GemEnvironment + + def gem_paths + Gem.path + end + + def gem_source_index + Gem.source_index + end + + def gem_specification + Gem::Specification + end + + def candidate_version_from_remote(gem_dependency, *sources) + with_gem_sources(*sources) do + find_newest_remote_version(gem_dependency, *sources) + end + end + + end + + class AlternateGemEnvironment < GemEnvironment + JRUBY_PLATFORM = /(:?universal|x86_64|x86)\-java\-[0-9\.]+/ + + def self.gempath_cache + @gempath_cache ||= {} + end + + def self.platform_cache + @platform_cache ||= {} + end + + include Chef::Mixin::ShellOut + + attr_reader :gem_binary_location + + def initialize(gem_binary_location) + @gem_binary_location = gem_binary_location + end + + def gem_paths + if self.class.gempath_cache.key?(@gem_binary_location) + self.class.gempath_cache[@gem_binary_location] + else + # shellout! is a fork/exec which won't work on windows + shell_style_paths = shell_out!("#{@gem_binary_location} env gempath").stdout + # on windows, the path separator is (usually? always?) semicolon + paths = shell_style_paths.split(::File::PATH_SEPARATOR).map { |path| path.strip } + self.class.gempath_cache[@gem_binary_location] = paths + end + end + + def gem_source_index + @source_index ||= Gem::SourceIndex.from_gems_in(*gem_paths.map { |p| p + '/specifications' }) + end + + def gem_specification + # Only once, dirs calls a reset + unless @specification + Gem::Specification.dirs = gem_paths + @specification = Gem::Specification + end + @specification + end + + ## + # Attempt to detect the correct platform settings for the target gem + # environment. + # + # In practice, this only makes a difference if different versions are + # available depending on platform, and only if the target gem + # environment has a radically different platform (i.e., jruby), so we + # just try to detect jruby and fall back to the current platforms + # (Gem.platforms) if we don't detect it. + # + # === Returns + # [String|Gem::Platform] returns an array of Gem::Platform-compatible + # objects, i.e., Strings that are valid for Gem::Platform or actual + # Gem::Platform objects. + def gem_platforms + if self.class.platform_cache.key?(@gem_binary_location) + self.class.platform_cache[@gem_binary_location] + else + gem_environment = shell_out!("#{@gem_binary_location} env").stdout + if jruby = gem_environment[JRUBY_PLATFORM] + self.class.platform_cache[@gem_binary_location] = ['ruby', Gem::Platform.new(jruby)] + else + self.class.platform_cache[@gem_binary_location] = Gem.platforms + end + end + end + + def with_gem_platforms(*alt_gem_platforms) + alt_gem_platforms.flatten! + original_gem_platforms = Gem.platforms + Gem.platforms = alt_gem_platforms + yield + ensure + Gem.platforms = original_gem_platforms + end + + def candidate_version_from_remote(gem_dependency, *sources) + with_gem_sources(*sources) do + with_gem_platforms(*gem_platforms) do + find_newest_remote_version(gem_dependency, *sources) + end + end + end + + end + + include Chef::Mixin::ShellOut + + attr_reader :gem_env + attr_reader :cleanup_gem_env + + def logger + Chef::Log.logger + end + + include Chef::Mixin::GetSourceFromPackage + + def initialize(new_resource, run_context=nil) + super + @cleanup_gem_env = true + if new_resource.gem_binary + if new_resource.options && new_resource.options.kind_of?(Hash) + msg = "options cannot be given as a hash when using an explicit gem_binary\n" + msg << "in #{new_resource} from #{new_resource.source_line}" + raise ArgumentError, msg + end + @gem_env = AlternateGemEnvironment.new(new_resource.gem_binary) + Chef::Log.debug("#{@new_resource} using gem '#{new_resource.gem_binary}'") + elsif is_omnibus? && (!@new_resource.instance_of? Chef::Resource::ChefGem) + # Opscode Omnibus - The ruby that ships inside omnibus is only used for Chef + # Default to installing somewhere more functional + if new_resource.options && new_resource.options.kind_of?(Hash) + msg = "options should be a string instead of a hash\n" + msg << "in #{new_resource} from #{new_resource.source_line}" + raise ArgumentError, msg + end + gem_location = find_gem_by_path + @new_resource.gem_binary gem_location + @gem_env = AlternateGemEnvironment.new(gem_location) + Chef::Log.debug("#{@new_resource} using gem '#{gem_location}'") + else + @gem_env = CurrentGemEnvironment.new + @cleanup_gem_env = false + Chef::Log.debug("#{@new_resource} using gem from running ruby environment") + end + end + + def is_omnibus? + if RbConfig::CONFIG['bindir'] =~ %r!/opt/(opscode|chef)/embedded/bin! + Chef::Log.debug("#{@new_resource} detected omnibus installation in #{RbConfig::CONFIG['bindir']}") + # Omnibus installs to a static path because of linking on unix, find it. + true + elsif RbConfig::CONFIG['bindir'].sub(/^[\w]:/, '') == "/opscode/chef/embedded/bin" + Chef::Log.debug("#{@new_resource} detected omnibus installation in #{RbConfig::CONFIG['bindir']}") + # windows, with the drive letter removed + true + else + false + end + end + + def find_gem_by_path + Chef::Log.debug("#{@new_resource} searching for 'gem' binary in path: #{ENV['PATH']}") + separator = ::File::ALT_SEPARATOR ? ::File::ALT_SEPARATOR : ::File::SEPARATOR + path_to_first_gem = ENV['PATH'].split(::File::PATH_SEPARATOR).select { |path| ::File.exists?(path + separator + "gem") }.first + raise Chef::Exceptions::FileNotFound, "Unable to find 'gem' binary in path: #{ENV['PATH']}" if path_to_first_gem.nil? + path_to_first_gem + separator + "gem" + end + + def gem_dependency + Gem::Dependency.new(@new_resource.package_name, @new_resource.version) + end + + def source_is_remote? + return true if @new_resource.source.nil? + scheme = URI.parse(@new_resource.source).scheme + # URI.parse gets confused by MS Windows paths with forward slashes. + scheme = nil if scheme =~ /^[a-z]$/ + %w{http https}.include?(scheme) + end + + def current_version + #raise 'todo' + # If one or more matching versions are installed, the newest of them + # is the current version + if !matching_installed_versions.empty? + gemspec = matching_installed_versions.last + logger.debug { "#{@new_resource} found installed gem #{gemspec.name} version #{gemspec.version} matching #{gem_dependency}"} + gemspec + # If no version matching the requirements exists, the latest installed + # version is the current version. + elsif !all_installed_versions.empty? + gemspec = all_installed_versions.last + logger.debug { "#{@new_resource} newest installed version of gem #{gemspec.name} is #{gemspec.version}" } + gemspec + else + logger.debug { "#{@new_resource} no installed version found for #{gem_dependency.to_s}"} + nil + end + end + + def matching_installed_versions + @matching_installed_versions ||= @gem_env.installed_versions(gem_dependency) + end + + def all_installed_versions + @all_installed_versions ||= begin + @gem_env.installed_versions(Gem::Dependency.new(gem_dependency.name, '>= 0')) + end + end + + def gem_sources + @new_resource.source ? Array(@new_resource.source) : nil + end + + def load_current_resource + @current_resource = Chef::Resource::Package::GemPackage.new(@new_resource.name) + @current_resource.package_name(@new_resource.package_name) + if current_spec = current_version + @current_resource.version(current_spec.version.to_s) + end + @current_resource + end + + def cleanup_after_converge + if @cleanup_gem_env + logger.debug { "#{@new_resource} resetting gem environment to default" } + Gem.clear_paths + end + end + + def candidate_version + @candidate_version ||= begin + if target_version_already_installed? + nil + elsif source_is_remote? + @gem_env.candidate_version_from_remote(gem_dependency, *gem_sources).to_s + else + @gem_env.candidate_version_from_file(gem_dependency, @new_resource.source).to_s + end + end + end + + def target_version_already_installed? + return false unless @current_resource && @current_resource.version + return false if @current_resource.version.nil? + + Gem::Requirement.new(@new_resource.version).satisfied_by?(Gem::Version.new(@current_resource.version)) + end + + ## + # Installs the gem, using either the gems API or shelling out to `gem` + # according to the following criteria: + # 1. Use gems API (Gem::DependencyInstaller) by default + # 2. shell out to `gem install` when a String of options is given + # 3. use gems API with options if a hash of options is given + def install_package(name, version) + if source_is_remote? && @new_resource.gem_binary.nil? + if @new_resource.options.nil? + @gem_env.install(gem_dependency, :sources => gem_sources) + elsif @new_resource.options.kind_of?(Hash) + options = @new_resource.options + options[:sources] = gem_sources + @gem_env.install(gem_dependency, options) + else + install_via_gem_command(name, version) + end + elsif @new_resource.gem_binary.nil? + @gem_env.install(@new_resource.source) + else + install_via_gem_command(name,version) + end + true + end + + def gem_binary_path + @new_resource.gem_binary || 'gem' + end + + def install_via_gem_command(name, version) + if @new_resource.source =~ /\.gem$/i + name = @new_resource.source + else + src = @new_resource.source && " --source=#{@new_resource.source} --source=http://rubygems.org" + end + if version + shell_out!("#{gem_binary_path} install #{name} -q --no-rdoc --no-ri -v \"#{version}\"#{src}#{opts}", :env=>nil) + else + shell_out!("#{gem_binary_path} install #{name} -q --no-rdoc --no-ri #{src}#{opts}", :env=>nil) + end + end + + def upgrade_package(name, version) + install_package(name, version) + end + + def remove_package(name, version) + if @new_resource.gem_binary.nil? + if @new_resource.options.nil? + @gem_env.uninstall(name, version) + elsif @new_resource.options.kind_of?(Hash) + @gem_env.uninstall(name, version, @new_resource.options) + else + uninstall_via_gem_command(name, version) + end + else + uninstall_via_gem_command(name, version) + end + end + + def uninstall_via_gem_command(name, version) + if version + shell_out!("#{gem_binary_path} uninstall #{name} -q -x -I -v \"#{version}\"#{opts}", :env=>nil) + else + shell_out!("#{gem_binary_path} uninstall #{name} -q -x -I -a#{opts}", :env=>nil) + end + end + + def purge_package(name, version) + remove_package(name, version) + end + + private + + def opts + expand_options(@new_resource.options) + end + + end + end + end +end |