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
Optional: The name of the executables directory. [default: bin]
Optional: A description of the release’s latest changes. Auto-populates to the top entry of History.txt.
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
.
Optional: What sections from the readme to use for auto-description. Defaults to %w(description).
MANDATORY: The author’s email address(es). (can be array)
Use the developer
method to fill in both author and email cleanly.
Optional: An array of rubygem dependencies.
extra_deps << ['blah', '~> 1.0']
Optional: An array of rubygem developer dependencies.
Optional: Extra files you want to add to RDoc.
.txt files are automatically included (excluding the obvious).
Optional: The name of the group authoring the project. [default: name.downcase]
Optional: The filename for the project history. [default: History.txt]
Optional: The homepage of the project. Auto-populates to the home key of the urls read from the README.txt
Optional: An array containing the license(s) under which this gem is released.
Warns and defaults to “MIT” if not set.
MANDATORY: The name of the release.
Set via Hoe.spec
.
Optional: A post-install message to be displayed when gem is installed.
Optional: The filename for the project readme. [default: README.txt]
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)
Optional: A short summary of the project. Auto-populates from the first sentence of the description.
See also: Hoe#description
and Hoe.paragraphs_of
.
Optional: Number of sentences from description for summary. Defaults to 1.
Optional: An array of test file patterns [default: test/*/test_.rb]
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
MANDATORY: The version. Don’t hardcode! use a constant in the project.
Public Class Methods
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
Returns plugins that could not be loaded by Hoe.load_plugins
.
# File lib/hoe.rb, line 307 def self.bad_plugins @bad_plugins end
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
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
The list of active plugins.
# File lib/hoe.rb, line 387 def self.plugins @@plugins end
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
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 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 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 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
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
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
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
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
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 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
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 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
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 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
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 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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