class Hoe

Hoe is a simple rake/rubygems helper for project Rakefiles. It helps generate rubygems and includes a dynamic plug-in system allowing for easy extensibility. Hoe ships with plug-ins for all your usual project tasks including rdoc generation, testing, packaging, and deployment.

Using Hoe

Basics

Sow generates a new project from scratch. Sow uses a simple ERB templating system allowing you to capture patterns common to your projects. Run ‘sow` and then see ~/.hoe_template for more info:

% sow project_name
...
% cd project_name

and have at it.

Extra Configuration Options:

Hoe maintains a config file for cross-project values. The file is located at ~/.hoerc. The file is a YAML formatted config file with the following settings (extended by plugins):

exclude

A regular expression of files to exclude from check_manifest.

Run `rake config_hoe` and see ~/.hoerc for examples.

Extending Hoe

Hoe can be extended via its plugin system. Hoe searches out all installed files matching 'hoe/*.rb' and loads them. Those files are expected to define a module matching the file name. The module must define a define task method and can optionally define an initialize method. Both methods must be named to match the file. eg

module Hoe::Blah
  def initialize_blah # optional
    # ...
  end

  def define_blah_tasks
    # ...
  end
end

Hoe Plugin Loading Sequence

Hoe.spec
  Hoe.load_plugins
    require
  activate_plugins
    extend plugin_module
    initialize_plugins
      initialize_XXX
    activate_plugin_deps
      activate_XXX_deps
  yield spec
  post_initialize
    define_spec # gemspec, not hoespec
    load_plugin_tasks
    add_dependencies

Constants

DEFAULT_CONFIG

Default configuration values for .hoerc. Plugins should populate this on load.

RUBY_DEBUG

Used to add extra flags to RUBY_FLAGS.

RUBY_FLAGS

Used to specify flags to ruby [has smart default].

URLS_TO_META_MAP

Map from the commonly used url names to gemspec’s metadata keys See guides.rubygems.org/specification-reference/#metadata

VERSION

duh

WINDOZE

True if you’re a masochistic developer. Used for building commands.

Attributes

author[RW]

MANDATORY: The author(s) of the package. (can be array)

Use the developer method to fill in both author and email cleanly.

bindir[RW]

Optional: The name of the executables directory. [default: bin]

changes[RW]

Optional: A description of the release’s latest changes. Auto-populates to the top entry of History.txt.

description[RW]

Optional: A description of the project. Auto-populates from the first paragraph of the DESCRIPTION section of README.txt.

See also: Hoe#summary and Hoe.paragraphs_of.

description_sections[RW]

Optional: What sections from the readme to use for auto-description. Defaults to %w(description).

email[RW]

MANDATORY: The author’s email address(es). (can be array)

Use the developer method to fill in both author and email cleanly.

extra_deps[RW]

Optional: An array of rubygem dependencies.

extra_deps << ['blah', '~> 1.0']
extra_dev_deps[RW]

Optional: An array of rubygem developer dependencies.

extra_rdoc_files[RW]

Optional: Extra files you want to add to RDoc.

.txt files are automatically included (excluding the obvious).

group_name[RW]

Optional: The name of the group authoring the project. [default: name.downcase]

history_file[RW]

Optional: The filename for the project history. [default: History.txt]

homepage[RW]

Optional: The homepage of the project. Auto-populates to the home key of the urls read from the README.txt

licenses[RW]

Optional: An array containing the license(s) under which this gem is released.

Warns and defaults to “MIT” if not set.

name[RW]

MANDATORY: The name of the release.

Set via Hoe.spec.

post_install_message[RW]

Optional: A post-install message to be displayed when gem is installed.

readme_file[RW]

Optional: The filename for the project readme. [default: README.txt]

spec_extras[RW]

Optional: A hash of extra values to set in the gemspec. Value may be a proc.

spec_extras[:required_rubygems_version] = '>= 1.3.2'

(tho, see pluggable! if that’s all you want to do)

summary[RW]

Optional: A short summary of the project. Auto-populates from the first sentence of the description.

See also: Hoe#description and Hoe.paragraphs_of.

summary_sentences[RW]

Optional: Number of sentences from description for summary. Defaults to 1.

test_globs[RW]

Optional: An array of test file patterns [default: test/*/test_.rb]

urls[RW]

Optional: The urls of the project. This can be an array or (preferably) a hash. Auto-populates to the urls read from the beginning of README.txt.

See parse_urls for more details

version[RW]

MANDATORY: The version. Don’t hardcode! use a constant in the project.

Public Class Methods

add_include_dirs(*dirs) click to toggle source

Add extra dirs to both $: and RUBY_FLAGS (for test runs and rakefile deps)

# File lib/hoe.rb, line 296
def self.add_include_dirs(*dirs)
  dirs = dirs.flatten
  include_dirs.concat dirs
  $:.unshift(*dirs)
  s = File::PATH_SEPARATOR
  RUBY_FLAGS.sub!(/-I/, "-I#{dirs.join(s)}#{s}")
end
bad_plugins() click to toggle source

Returns plugins that could not be loaded by Hoe.load_plugins.

# File lib/hoe.rb, line 307
def self.bad_plugins
  @bad_plugins
end
load_plugins(plugins = Hoe.plugins) click to toggle source

Find and load all plugin files.

It is called at the end of hoe.rb

# File lib/hoe.rb, line 316
def self.load_plugins plugins = Hoe.plugins
  @found  ||= {}
  @loaded ||= {}
  @files  ||= Gem.find_files "hoe/*.rb"

  @files.reverse.each do |path| # reverse so first one wins
    @found[File.basename(path, ".rb").intern] = path
  end

  :keep_doing_this while @found.map { |name, plugin|
    next unless plugins.include? name
    next if @loaded[name]
    begin
      warn "loading #{plugin}" if $DEBUG
      @loaded[name] = require plugin
    rescue LoadError => e
      warn "error loading #{plugin.inspect}: #{e.message}. skipping..."
    end
  }.any?

  bad_plugins = plugins - @loaded.keys
  bad_plugins.each do |bad_plugin|
    plugins.delete bad_plugin
  end

  @bad_plugins.concat bad_plugins
  @bad_plugins.uniq!

  return @loaded, @found
end
plugin(*plugins) click to toggle source

Activates plugins. If a plugin cannot be loaded it will be ignored.

Plugins may also be activated through a plugins array in ~/.hoerc. This should only be used for plugins that aren’t critical to your project and plugins that you want to use on other projects.

# File lib/hoe.rb, line 379
def self.plugin *plugins
  self.plugins.concat plugins
  self.plugins.uniq!
end
plugins() click to toggle source

The list of active plugins.

# File lib/hoe.rb, line 387
def self.plugins
  @@plugins
end
spec(name, &block) click to toggle source

Execute the Hoe DSL to define your project’s Hoe specification (which interally creates a gem specification). All hoe attributes and methods are available within block. Eg:

Hoe.spec name do
  # ... project specific data ...
end
# File lib/hoe.rb, line 400
def self.spec name, &block
  Hoe.load_plugins

  spec = self.new name
  spec.activate_plugins
  spec.instance_eval(&block)
  spec.post_initialize
  spec # TODO: remove?
end

Public Instance Methods

activate_plugin_deps() click to toggle source

Run all activate_*_deps methods for plugins

# File lib/hoe.rb, line 448
def activate_plugin_deps
  Hoe.plugins.each do |plugin|
    msg = "activate_#{plugin}_deps"
    warn msg if $DEBUG
    send msg if self.respond_to? msg
  end
end
activate_plugins() click to toggle source

Activate plugin modules and add them to the current instance.

# File lib/hoe.rb, line 413
def activate_plugins
  with_config do |config, _|
    config_plugins = config["plugins"]
    break unless config_plugins
    Hoe.plugins.concat config_plugins.map(&:intern)
  end

  Hoe.load_plugins Hoe.plugins

  names = Hoe.constants.map(&:to_s)
  names.reject! { |n| n =~ /^[A-Z_]+$/ }

  names.each do |name|
    next unless Hoe.plugins.include? name.downcase.intern
    warn "extend #{name}" if $DEBUG
    self.extend Hoe.const_get(name)
  end

  initialize_plugins
end
add_dependencies() click to toggle source

Add standard and user defined dependencies to the spec.

# File lib/hoe.rb, line 484
def add_dependencies
  self.extra_deps     = normalize_deps extra_deps
  self.extra_dev_deps = normalize_deps extra_dev_deps

  case name
  when "hoe" then
    # do nothing? these deps are already in the hoe spec in the Rakefile
  else
    version = VERSION.split(/\./).first(2).join(".")
    dependency "hoe", "~> #{version}", :development
  end

  seen = {}

  extra_deps.each do |dep|
    next if seen[dep.first]
    seen[dep.first] = true

    spec.add_dependency(*dep)
  end

  extra_dev_deps.each do |dep|
    next if seen[dep.first]
    seen[dep.first] = true

    spec.add_development_dependency(*dep)
  end
end
define_spec() click to toggle source

Define the Gem::Specification.

# File lib/hoe.rb, line 523
def define_spec
  self.spec = Gem::Specification.new do |s|
    dirs = Dir["lib"]

    manifest = read_manifest

    abort [
           "Manifest is missing or couldn't be read.",
           "The Manifest is kind of a big deal.",
           "Maybe you're using a gem packaged by a linux project.",
           "It seems like they enjoy breaking other people's code.",
           ].join "\n" unless manifest

    s.name                 = name
    s.version              = version if version
    s.summary              = summary
    s.email                = email
    s.homepage             = homepage || urls["home"] || urls.values.first

    s.description          = description
    s.files                = manifest
    s.bindir               = bindir || "bin"
    s.executables          = s.files.grep(/^#{s.bindir}/) { |f| File.basename(f) }
    s.require_paths        = dirs unless dirs.empty?
    s.rdoc_options         = ["--main", readme_file]
    s.post_install_message = post_install_message
    s.metadata             = (urls.keys & URLS_TO_META_MAP.keys).map { |name|
      [URLS_TO_META_MAP[name], urls[name]]
    }.to_h if urls

    missing "Manifest.txt" if s.files.empty?

    case author
    when Array
      s.authors = author
    else
      s.author  = author
    end

    s.extra_rdoc_files += s.files.grep(/\.(txt|rdoc|md)$/)
    s.extra_rdoc_files.reject! { |f| f =~ %r%^(test|spec|vendor|template|data|tmp)/% }
    s.extra_rdoc_files += @extra_rdoc_files
  end

  check_for_version

  if licenses.empty?
    warn "Defaulting gemspec to MIT license."
    warn "Call license in hoe spec to change."
    license "MIT"
  end

  spec.licenses = licenses

  run_spec_extras
end
dependency(name, version, type = :runtime) click to toggle source

Add a dependency declaration to your spec. Pass :dev to type for developer dependencies.

# File lib/hoe.rb, line 468
def dependency name, version, type = :runtime
  raise "Unknown dependency type: #{type}" unless
    [:runtime, :dev, :development, :developer].include? type

  ary = if type == :runtime then
          extra_deps
        else
          extra_dev_deps
        end

  ary << [name, version]
end
dependency_target() click to toggle source

Returns the proper dependency list for the thingy.

# File lib/hoe.rb, line 516
def dependency_target
  self.name == "hoe" ? extra_deps : extra_dev_deps
end
developer(name, email) click to toggle source

Convenience method to set add to both the author and email fields.

# File lib/hoe.rb, line 617
def developer name, email
  self.author << name
  self.email  << email
end
have_gem?(name) click to toggle source

Returns true if the gem name is installed.

# File lib/hoe.rb, line 625
def have_gem? name
  Gem::Specification.find_by_name name.to_s
rescue Gem::LoadError
  false
end
initialize_plugins() click to toggle source

Run all initialize_* methods for plugins

# File lib/hoe.rb, line 437
def initialize_plugins
  Hoe.plugins.each do |plugin|
    msg = "initialize_#{plugin}"
    warn msg if $DEBUG
    send msg if self.respond_to? msg
  end
end
intuit_values(input) click to toggle source

Intuit values from the readme and history files.

# File lib/hoe.rb, line 672
def intuit_values input
  readme = input
             .lines
             .chunk { |l| l[/^(?:=+|#+)/] || "" }
             .map(&:last)
             .each_slice(2)
             .to_h { |k, v|
               raise "No body for %p section" % [k[0].strip] \
                 unless v
               kp = k.map { |s|
                 s.strip.chomp(":").sub(/(?:=+|#+)\s*/, '').downcase
               }.join("\n")

               [kp, v.join.strip]
             }

  unless readme.empty? then
    desc     = readme.values_at(*description_sections).join("\n\n")
    summ     = desc.split(/\.\s+/).first(summary_sentences).join(". ")

    self.urls        ||= parse_urls(readme.values.first)
    self.description ||= desc
    self.summary     ||= summ
  else
    missing readme_file
  end

  self.changes ||= begin
                     h = File.read_utf(history_file)
                     h.split(/^(={2,}|\#{2,})/)[1..2].join.strip
                   rescue
                     missing history_file
                     ""
                   end
end
license(name) click to toggle source

Specify a license for your gem. Call it multiple times if you are releasing under multiple licenses.

# File lib/hoe.rb, line 460
def license name
  self.licenses << name.to_s
end
load_plugin_tasks() click to toggle source

Load activated plugins by calling their define tasks method.

# File lib/hoe.rb, line 739
def load_plugin_tasks
  bad = []

  $plugin_max = self.class.plugins.map { |s| s.to_s.size }.max

  self.class.plugins.each do |plugin|
    warn "define: #{plugin}" if $DEBUG

    old_tasks = Rake::Task.tasks.dup

    begin
      send "define_#{plugin}_tasks"
    rescue NoMethodError
      warn "warning: couldn't activate the #{plugin} plugin, skipping"

      bad << plugin
      next
    end

    (Rake::Task.tasks - old_tasks).each do |task|
      task.plugin = plugin
    end
  end
  @@plugins -= bad
end
missing(name) click to toggle source

Bitch about a file that is missing data or unparsable for intuiting values.

# File lib/hoe.rb, line 768
def missing name
  warn "** #{name} is missing or in the wrong format for auto-intuiting."
  warn "   run `sow blah` and look at its text files"
end
normalize_deps(deps) click to toggle source

Normalize the dependencies.

# File lib/hoe.rb, line 776
def normalize_deps deps
  deps = Array(deps)

  deps.each do |o|
    abort "ERROR: Add '~> x.y' to the '#{o}' dependency." if String === o
  end

  deps
end
paragraphs_of(path, *paragraphs) click to toggle source

Reads a file at path and spits out an array of the paragraphs specified.

changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
summary, *description = p.paragraphs_of('README.txt', 3, 3..8)
# File lib/hoe.rb, line 792
def paragraphs_of path, *paragraphs
  File.read_utf(path).delete("\r").split(/\n\n+/).values_at(*paragraphs)
end
parse_urls(text) click to toggle source

Parse the urls section of the readme file. Returns a hash or an array depending on the format of the section.

label1 :: url1
label2 :: url2
label3 :: url3

vs:

* url1
* url2
* url3

The hash format is preferred as it will be used to populate gem metadata. The array format will work, but will warn that you should update the readme.

# File lib/hoe.rb, line 726
def parse_urls text
  lines = text.gsub(/^\* /, "").delete("<>").split(/\n/).grep(/\S+/)

  if lines.first =~ /::/ then
    Hash[lines.map { |line| line.split(/\s*::\s*/) }]
  else
    raise "Please switch readme to hash format for urls."
  end
end
pluggable!() click to toggle source

Tell the world you’re a pluggable package (ie you require rubygems 1.3.1+)

This uses require_rubygems_version. Last one wins. Make sure you account for that.

# File lib/hoe.rb, line 802
def pluggable!
  abort "update rubygems to >= 1.3.1" unless  Gem.respond_to? :find_files
  require_rubygems_version ">= 1.3.1"
end
plugin?(name) click to toggle source

Is a plugin activated? Used for guarding missing plugins in your hoe spec:

Hoe.spec "blah" do
  if plugin? :enhancement then
    self.enhancement = true # or whatever...
  end
end
# File lib/hoe.rb, line 817
def plugin? name
  self.class.plugins.include? name
end
post_initialize() click to toggle source

Finalize configuration

# File lib/hoe.rb, line 824
def post_initialize
  activate_plugin_deps
  unless skip_intuit_values?
    intuit_values File.read_utf readme_file if readme_file
  end
  validate_fields
  define_spec
  load_plugin_tasks
  add_dependencies
end
read_manifest() click to toggle source

Reads Manifest.txt and returns an Array of lines in the manifest.

Returns nil if no manifest was found.

# File lib/hoe.rb, line 840
def read_manifest
  File.read_utf("Manifest.txt").split(/\r?\n\r?/) rescue nil
end
require_ruby_version(*versions) click to toggle source

Declare that your gem requires a specific ruby version. Last one wins.

# File lib/hoe.rb, line 854
def require_ruby_version *versions
  spec_extras[:required_ruby_version] = versions
end
require_rubygems_version(*versions) click to toggle source

Declare that your gem requires a specific rubygems version. Last one wins.

# File lib/hoe.rb, line 847
def require_rubygems_version *versions
  spec_extras[:required_rubygems_version] = versions
end
ruby20!() click to toggle source

Declare that this gem requires ruby to be in the 2.0+ family.

# File lib/hoe.rb, line 861
def ruby20!
  require_ruby_version "~> 2.0"
end
ruby21!() click to toggle source

Declare that this gem requires ruby to be in the 2.1+ family.

# File lib/hoe.rb, line 868
def ruby21!
  require_ruby_version "~> 2.1"
end
ruby22!() click to toggle source

Declare that this gem requires ruby to be in the 2.2+ family.

# File lib/hoe.rb, line 875
def ruby22!
  require_ruby_version "~> 2.2"
end
ruby23!() click to toggle source

Declare that this gem requires ruby to be in the 2.3+ family.

# File lib/hoe.rb, line 882
def ruby23!
  require_ruby_version "~> 2.3"
end
timebomb(n, m, finis = nil, start = nil) click to toggle source

Provide a linear degrading value from n to m over start to finis dates. If not provided, start and finis will default to 1/1 and 12/31 of the current year.

# File lib/hoe.rb, line 894
def timebomb n, m, finis = nil, start = nil
  require "time"
  finis = Time.parse(finis || "#{Time.now.year}-12-31")
  start = Time.parse(start || "#{Time.now.year}-01-01")
  rest  = (finis - Time.now)
  full  = (finis - start)

  [((n - m) * rest / full).to_i + m, m].max
end
unknown() click to toggle source

Optional: Extra directories to use (eg for test runs). See Hoe.add_include_dirs.

# File lib/hoe.rb, line 291
mc.send :attr_accessor, :include_dirs
validate_fields() click to toggle source

Verify that mandatory fields are set.

# File lib/hoe.rb, line 907
def validate_fields
  %w[email author].each do |field|
    value = self.send(field)
    abort "Hoe #{field} value not set. aborting" if value.nil? or value.empty?
  end
end
with_config() { |config, rc| ... } click to toggle source

Loads ~/.hoerc, merges it with a .hoerc in the current pwd (if any) and yields the configuration and its path

# File lib/hoe.rb, line 922
def with_config
  config = Hoe::DEFAULT_CONFIG

  rc = File.expand_path("~/.hoerc")
  homeconfig = maybe_load_yaml rc

  config = config.merge homeconfig

  localrc = File.join Dir.pwd, ".hoerc"
  localconfig = maybe_load_yaml(localrc)

  config = config.merge localconfig

  yield config, rc
end