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

name[R]

Public Class Methods

new(name, version=nil) click to toggle source

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

as_cookbook_version() click to toggle source

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
chef_version_requirement() click to toggle source

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
cookbook_dependencies(development: false) click to toggle source
# File lib/halite/gem.rb, line 181
def cookbook_dependencies(development: false)
  Dependencies.extract(spec, development: development)
end
cookbook_name() click to toggle source
# 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
cookbook_version() click to toggle source

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
each_file(prefix_paths=nil, &block) click to toggle source

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
each_library_file(&block) click to toggle source

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
find_misc_path(name) click to toggle source

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_halite_cookbook?() click to toggle source

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
issues_url() click to toggle source

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() click to toggle source

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
platforms() click to toggle source

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
spec() click to toggle source
# File lib/halite/gem.rb, line 62
def spec
  @spec ||= dependency_to_spec(::Gem::Dependency.new(@name, ::Gem::Requirement.new(@version)))
end
spec_file() click to toggle source

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
version() click to toggle source
# File lib/halite/gem.rb, line 66
def version
  spec.version.to_s
end

Private Instance Methods

dependency_to_spec(dep) click to toggle source

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