class Halite::Gem
A model for a gem/cookbook within Halite
.
@since 1.0.0 @example
g = Halite::Gem.new('chef-mycookbook', '1.1.0') puts(g.cookbook_name) #=> mycookbook
Attributes
Public Class Methods
name can be either a string name, Gem::Dependency, or Gem::Specification @param name [String, Gem::Dependency, Gem::Specification]
# File lib/halite/gem.rb, line 46 def initialize(name, version=nil) # Allow passing a Dependency by just grabbing its spec. name = dependency_to_spec(name) if name.is_a?(::Gem::Dependency) # Stubs don't load enough data for us, grab the real spec. RIP IOPS. name = name.to_spec if name.is_a?(::Gem::StubSpecification) || (defined?(Bundler::StubSpecification) && name.is_a?(Bundler::StubSpecification)) if name.is_a?(::Gem::Specification) raise Error.new("Cannot pass version when using an explicit specficiation") if version @spec = name @name = spec.name else @name = name @version = version raise Error.new("Gem #{name}#{version ? " v#{version}" : ''} not found") unless spec end end
Public Instance Methods
Create a Chef::CookbookVersion object that represents this gem. This can be injected in to Chef to simulate the cookbook being available.
@return [Chef::CookbookVersion] @example
run_context.cookbook_collection[gem.cookbook_name] = gem.as_cookbook_version
# File lib/halite/gem.rb, line 196 def as_cookbook_version # Put this in a local variable for a closure below. path = spec.full_gem_path Chef::CookbookVersion.new(cookbook_name, File.join(path, 'chef')).tap do |c| # Use CookbookVersion#files_for as a feature test for ManifestV2. This # can be changed to ::Gem::Requirement.create('>= 13').satisfied_by?(::Gem::Version.create(Chef::VERSION)) # once https://github.com/chef/chef/pull/5929 is merged. if defined?(c.files_for) c.all_files = each_file('chef').map(&:first) else c.attribute_filenames = each_file('chef/attributes').map(&:first) c.file_filenames = each_file('chef/files').map(&:first) c.recipe_filenames = each_file('chef/recipes').map(&:first) c.template_filenames = each_file('chef/templates').map(&:first) end # Haxx, rewire the filevendor for this cookbook to look up in our folder. # This is touching two different internal interfaces, but ¯\_(ツ)_/¯ c.send(:file_vendor).define_singleton_method(:get_filename) do |filename| File.join(path, 'chef', filename) end # Store the true root for use in other tools. c.define_singleton_method(:halite_root) { path } end end
Figure out which version of Chef this cookbook requires. Returns an array of Gem::Requirement-style string like `~> 12.0`.
@return [Array<String>]
# File lib/halite/gem.rb, line 242 def chef_version_requirement if spec.metadata['halite_chef_version'] # Manually overridden by gem metadata, use that. [spec.metadata['halite_chef_version']] elsif dep = spec.dependencies.find {|inner_dep| inner_dep.name == 'chef' && !inner_dep.requirement.none? } # Parse through the dependencies looking for something like `spec.add_dependency 'chef', '>= 12.1''`. dep.requirement.as_list else ['>= 12'] end end
# File lib/halite/gem.rb, line 181 def cookbook_dependencies(development: false) Dependencies.extract(spec, development: development) end
# File lib/halite/gem.rb, line 70 def cookbook_name if spec.metadata.include?('halite_name') spec.metadata['halite_name'] else spec.name.gsub(/(^(chef|cookbook)[_-])|([_-](chef|cookbook))$/, '') end end
Version of the gem sanitized for Chef. This means no non-numeric tags and only three numeric components.
@return [String]
# File lib/halite/gem.rb, line 82 def cookbook_version if match = version.match(/^(\d+\.\d+\.(\d+)?)/) match[1] else raise Halite::Error.new("Unable to parse #{version.inspect} as a Chef cookbook version") end end
Iterate over all the files in the gem, with an optional prefix. Each element in the iterable will be [full_path, relative_path], where relative_path is relative to the prefix or gem path.
@param prefix_paths [String, Array<String>, nil] Option prefix paths. @param block [Proc] Callable for iteration. @return [Array<Array<String>>] @example
gem_data.each_file do |full_path, rel_path| # ... end
# File lib/halite/gem.rb, line 152 def each_file(prefix_paths=nil, &block) globs = if prefix_paths Array(prefix_paths).map {|path| File.join(spec.full_gem_path, path) } else [spec.full_gem_path] end [].tap do |files| globs.each do |glob| Dir[File.join(glob, '**', '*')].each do |path| next unless File.file?(path) val = [path, path[glob.length+1..-1]] block.call(*val) if block files << val end end # Make sure the order is stable for my tests. Probably overkill, I think # Dir#[] sorts already. files.sort! end end
Special case of the {#each_file} the gem's require paths.
@param block [Proc] Callable for iteration. @return [Array<Array<String>>]
# File lib/halite/gem.rb, line 177 def each_library_file(&block) each_file(spec.require_paths, &block) end
Search for a file like README.md or LICENSE.txt in the gem.
@param name [String] Basename to search for. @return [String, Array<String>] @example
gem.misc_file('Readme') => /path/to/readme.txt
# File lib/halite/gem.rb, line 227 def find_misc_path(name) [name, name.upcase, name.downcase].each do |base| ['.md', '', '.txt', '.html'].each do |suffix| path = File.join(spec.full_gem_path, base+suffix) return path if File.exist?(path) && Dir.entries(File.dirname(path)).include?(File.basename(path)) end end # Didn't find anything nil end
Is this gem really a cookbook? (anything that depends directly on halite and doesn't have the ignore flag)
# File lib/halite/gem.rb, line 186 def is_halite_cookbook? spec.dependencies.any? {|subdep| subdep.name == 'halite'} && !spec.metadata.include?('halite_ignore') end
URL to the issue tracker for this project.
@return [String, nil]
# File lib/halite/gem.rb, line 111 def issues_url if spec.metadata['issues_url'] spec.metadata['issues_url'] elsif spec.homepage =~ /^http(s)?:\/\/(www\.)?github\.com/ spec.homepage.chomp('/') + '/issues' end end
License header extacted from the gemspec. Suitable for inclusion in other Ruby source files.
@return [String]
# File lib/halite/gem.rb, line 104 def license_header IO.readlines(spec_file).take_while { |line| line.strip.empty? || line.strip.start_with?('#') }.join('') end
Platform support to be used in the Chef metadata.
@return [Array<Array<String>>]
# File lib/halite/gem.rb, line 122 def platforms raw_platforms = spec.metadata.fetch('platforms', '').strip case raw_platforms when '' [] when 'any', 'all', '*' # Based on `ls lib/fauxhai/platforms | xargs echo`. %w{aix amazon arch centos chefspec debian dragonfly4 fedora freebsd gentoo ios_xr mac_os_x nexus omnios openbsd opensuse oracle raspbian redhat slackware smartos solaris2 suse ubuntu windows}.map {|p| [p] } when /,/ # Comma split mode. String looks like "name, name constraint, name constraint" raw_platforms.split(/\s*,\s*/).map {|p| p.split(/\s+/, 2) } else # Whitepace split mode, assume no constraints. raw_platforms.split(/\s+/).map {|p| [p] } end end
# File lib/halite/gem.rb, line 62 def spec @spec ||= dependency_to_spec(::Gem::Dependency.new(@name, ::Gem::Requirement.new(@version))) end
Path to the .gemspec for this gem. This is different from Gem::Specification#spec_file because the Rubygems API is shit and just assumes the file layout matches normal, which is not the case with Bundler and path or git sources.
@return [String]
# File lib/halite/gem.rb, line 96 def spec_file File.join(spec.full_gem_path, spec.name + '.gemspec') end
# File lib/halite/gem.rb, line 66 def version spec.version.to_s end
Private Instance Methods
Find a spec given a dependency.
@since 1.0.1 @param dep [Gem::Dependency] Dependency to solve. @return [Gem::Specificiation]
# File lib/halite/gem.rb, line 261 def dependency_to_spec(dep) # #to_spec doesn't allow prereleases unless the requirement is # for a prerelease. Just use the last valid spec if possible. spec = dep.to_spec || dep.to_specs.last raise Error.new("Cannot find a gem to satisfy #{dep}") unless spec spec rescue ::Gem::LoadError => ex raise Error.new("Cannot find a gem to satisfy #{dep}: #{ex}") end