diff options
Diffstat (limited to 'knife/lib/chef/knife/core/gem_glob_loader.rb')
-rw-r--r-- | knife/lib/chef/knife/core/gem_glob_loader.rb | 134 |
1 files changed, 134 insertions, 0 deletions
diff --git a/knife/lib/chef/knife/core/gem_glob_loader.rb b/knife/lib/chef/knife/core/gem_glob_loader.rb new file mode 100644 index 0000000000..d365602cb4 --- /dev/null +++ b/knife/lib/chef/knife/core/gem_glob_loader.rb @@ -0,0 +1,134 @@ +# Author:: Christopher Brown (<cb@chef.io>) +# Author:: Daniel DeLeo (<dan@chef.io>) +# Copyright:: Copyright (c) Chef Software 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_relative "../version" +require "chef-config/path_helper" unless defined?(ChefConfig::PathHelper) +class Chef + class Knife + class SubcommandLoader + class GemGlobLoader < Chef::Knife::SubcommandLoader + MATCHES_CHEF_GEM ||= %r{/chef-\d+\.\d+\.\d+}.freeze + MATCHES_THIS_CHEF_GEM ||= %r{/chef-#{Chef::VERSION}(-\w+)?(-\w+)?/}.freeze + + def subcommand_files + @subcommand_files ||= (gem_and_builtin_subcommands.values + site_subcommands).flatten.uniq + end + + # Returns a Hash of paths to knife commands built-in to chef, or installed via gem. + # If rubygems is not installed, falls back to globbing the knife directory. + # The Hash is of the form {"relative/path" => "/absolute/path"} + #-- + # Note: the "right" way to load the plugins is to require the relative path, i.e., + # require 'chef/knife/command' + # but we're getting frustrated by bugs at every turn, and it's slow besides. So + # subcommand loader has been modified to load the plugins by using Kernel.load + # with the absolute path. + def gem_and_builtin_subcommands + require "rubygems" unless defined?(Gem) + find_subcommands_via_rubygems + rescue LoadError + find_subcommands_via_dirglob + end + + def find_subcommands_via_rubygems + files = find_files_latest_gems "chef/knife/*.rb" + version_file_match = /#{Regexp.escape(File.join('chef', 'knife', 'version'))}$/ + subcommand_files = {} + files.each do |file| + + rel_path = file[/(.*)(#{Regexp.escape File.join('chef', 'knife', '')}.*)\.rb/, 2] + + # When not installed as a gem (ChefDK/appbundler in particular), AND + # a different version of Chef is installed via gems, `files` will + # include some files from the 'other' Chef install. If this contains + # a knife command that doesn't exist in this version of Chef, we will + # get a LoadError later when we try to require it. + next if from_different_chef_version?(file) + + # Exclude knife/chef/version. It's not a knife command, and force-loading + # when we load all of these files will emit constant-already-defined warnings + next if rel_path =~ version_file_match + + subcommand_files[rel_path] = file + end + + subcommand_files.merge(find_subcommands_via_dirglob) + end + + private + + def find_files_latest_gems(glob, check_load_path = true) + files = [] + + if check_load_path + files = $LOAD_PATH.map do |load_path| + Dir["#{File.expand_path glob, ChefConfig::PathHelper.escape_glob_dir(load_path)}#{Gem.suffix_pattern}"] + end.flatten.select { |file| File.file? file.untaint } + + end + + gem_files = latest_gem_specs.map do |spec| + # Gem::Specification#matches_for_glob wasn't added until RubyGems 1.8 + if spec.respond_to? :matches_for_glob + spec.matches_for_glob("#{glob}#{Gem.suffix_pattern}") + else + check_spec_for_glob(spec, glob) + end + end.flatten + + files.concat gem_files + files.uniq! if check_load_path + + files + end + + def latest_gem_specs + @latest_gem_specs ||= if Gem::Specification.respond_to? :latest_specs + Gem::Specification.latest_specs(true) # find prerelease gems + else + Gem.source_index.latest_specs(true) + end + end + + def check_spec_for_glob(spec, glob) + dirs = if spec.require_paths.size > 1 + "{#{spec.require_paths.join(",")}}" + else + spec.require_paths.first + end + + glob = File.join(ChefConfig::PathHelper.escape_glob_dir(spec.full_gem_path, dirs), glob) + + Dir[glob].map(&:untaint) + end + + def from_different_chef_version?(path) + matches_any_chef_gem?(path) && !matches_this_chef_gem?(path) + end + + def matches_any_chef_gem?(path) + path =~ MATCHES_CHEF_GEM + end + + def matches_this_chef_gem?(path) + path =~ MATCHES_THIS_CHEF_GEM + end + end + end + end +end |