configurability 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/ChangeLog +15 -0
- data/LICENSE +27 -0
- data/README.md +133 -0
- data/Rakefile +348 -0
- data/lib/configurability.rb +150 -0
- data/lib/configurability/logformatter.rb +60 -0
- data/rake/191_compat.rb +26 -0
- data/rake/dependencies.rb +76 -0
- data/rake/documentation.rb +89 -0
- data/rake/helpers.rb +502 -0
- data/rake/hg.rb +275 -0
- data/rake/manual.rb +787 -0
- data/rake/packaging.rb +129 -0
- data/rake/publishing.rb +278 -0
- data/rake/style.rb +62 -0
- data/rake/svn.rb +668 -0
- data/rake/testing.rb +187 -0
- data/rake/verifytask.rb +64 -0
- data/spec/configurability_spec.rb +211 -0
- data/spec/lib/helpers.rb +137 -0
- metadata +90 -0
@@ -0,0 +1,150 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
# A configuration mixin for Ruby classes.
|
6
|
+
#
|
7
|
+
# @author Michael Granger <ged@FaerieMUD.org>
|
8
|
+
#
|
9
|
+
module Configurability
|
10
|
+
|
11
|
+
# Library version constant
|
12
|
+
VERSION = '1.0.0'
|
13
|
+
|
14
|
+
# Version-control revision constant
|
15
|
+
REVISION = %q$Revision: e0fef8dabba4 $
|
16
|
+
|
17
|
+
require 'configurability/logformatter.rb'
|
18
|
+
|
19
|
+
|
20
|
+
### The objects that have had Configurability added to them
|
21
|
+
@configurable_objects = []
|
22
|
+
|
23
|
+
### Logging
|
24
|
+
@default_logger = Logger.new( $stderr )
|
25
|
+
@default_logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
|
26
|
+
|
27
|
+
@default_log_formatter = Configurability::LogFormatter.new( @default_logger )
|
28
|
+
@default_logger.formatter = @default_log_formatter
|
29
|
+
|
30
|
+
@logger = @default_logger
|
31
|
+
|
32
|
+
|
33
|
+
class << self
|
34
|
+
|
35
|
+
# @return [Array] the Array of objects that have had Configurability
|
36
|
+
# added to them
|
37
|
+
attr_accessor :configurable_objects
|
38
|
+
|
39
|
+
# @return [Logger::Formatter] the log formatter that will be used when the logging
|
40
|
+
# subsystem is reset
|
41
|
+
attr_accessor :default_log_formatter
|
42
|
+
|
43
|
+
# @return [Logger] the logger that will be used when the logging subsystem is reset
|
44
|
+
attr_accessor :default_logger
|
45
|
+
|
46
|
+
# @return [Logger] the logger that's currently in effect
|
47
|
+
attr_accessor :logger
|
48
|
+
alias_method :log, :logger
|
49
|
+
alias_method :log=, :logger=
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
### Add configurability to the given +object+.
|
54
|
+
def self::extend_object( object )
|
55
|
+
self.log.debug "Adding Configurability to %p" % [ object ]
|
56
|
+
super
|
57
|
+
self.configurable_objects << object
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
### Mixin hook: extend including classes instead
|
62
|
+
def self::included( mod )
|
63
|
+
mod.extend( self )
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
### Try to generate a config key from the given object. If it responds_to #name,
|
68
|
+
### the result will be stringified and stripped of non-word characters. If the
|
69
|
+
### object itself doesn't have a name, the name of its class will be used instead.
|
70
|
+
def self::make_key_from_object( object )
|
71
|
+
if object.respond_to?( :name )
|
72
|
+
return object.name.sub( /.*::/, '' ).gsub( /\W+/, '_' ).downcase.to_sym
|
73
|
+
elsif object.class.name && !object.class.name.empty?
|
74
|
+
return object.class.name.sub( /.*::/, '' ).gsub( /\W+/, '_' ).downcase.to_sym
|
75
|
+
else
|
76
|
+
return :anonymous
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
### Configure objects that have had Configurability added to them with
|
82
|
+
### the sections of the specified +config+ that correspond to their
|
83
|
+
### +config_key+. If the +config+ doesn't #respond_to the object's
|
84
|
+
### +config_key+, the object's #configure method is called with +nil+
|
85
|
+
### instead.
|
86
|
+
def self::configure_objects( config )
|
87
|
+
self.configurable_objects.each do |obj|
|
88
|
+
section = obj.config_key.to_sym
|
89
|
+
self.log.debug "Configuring %p with the %p section of the config." %
|
90
|
+
[ obj, section ]
|
91
|
+
|
92
|
+
if config.respond_to?( section )
|
93
|
+
self.log.debug " config has a %p method; using that" % [ section ]
|
94
|
+
obj.configure( config.send(section) )
|
95
|
+
elsif config.respond_to?( :[] )
|
96
|
+
self.log.debug " config has a an index operator method; using that"
|
97
|
+
obj.configure( config[section] || config[section.to_s] )
|
98
|
+
else
|
99
|
+
self.log.warn " don't know how to get the %p section of the config from %p" %
|
100
|
+
[ section, config ]
|
101
|
+
obj.configure( nil )
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
### Reset the global logger object to the default
|
108
|
+
### @return [void]
|
109
|
+
def self::reset_logger
|
110
|
+
self.logger = self.default_logger
|
111
|
+
self.logger.level = Logger::WARN
|
112
|
+
self.logger.formatter = self.default_log_formatter
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
|
117
|
+
#############################################################
|
118
|
+
### A P P E N D E D M E T H O D S
|
119
|
+
#############################################################
|
120
|
+
|
121
|
+
### The symbol which corresponds to the section of the configuration
|
122
|
+
### used to configure the object.
|
123
|
+
attr_writer :config_key
|
124
|
+
|
125
|
+
### Get (and optionally set) the +config_key+.
|
126
|
+
### @param [Symbol] sym the config key
|
127
|
+
### @return [Symbol] the config key
|
128
|
+
def config_key( sym=nil )
|
129
|
+
@config_key = sym unless sym.nil?
|
130
|
+
@config_key ||= Configurability.make_key_from_object( self )
|
131
|
+
@config_key
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
### Set the config key of the object.
|
136
|
+
### @param [Symbol] sym the config key
|
137
|
+
def config_key=( sym )
|
138
|
+
@config_key = sym
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
### Default configuration method.
|
143
|
+
### @param [Object] configuration section object
|
144
|
+
def configure( config )
|
145
|
+
@config = config
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
end # module Configurability
|
150
|
+
|
@@ -0,0 +1,60 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
require 'configurability'
|
6
|
+
|
7
|
+
# A custom log-formatter class for
|
8
|
+
class Configurability::LogFormatter < Logger::Formatter
|
9
|
+
|
10
|
+
# The format to output unless debugging is turned on
|
11
|
+
DEFAULT_FORMAT = "[%1$s.%2$06d %3$d/%4$s] %5$5s -- %7$s\n"
|
12
|
+
|
13
|
+
# The format to output if debugging is turned on
|
14
|
+
DEFAULT_DEBUG_FORMAT = "[%1$s.%2$06d %3$d/%4$s] %5$5s {%6$s} -- %7$s\n"
|
15
|
+
|
16
|
+
|
17
|
+
### Initialize the formatter with a reference to the logger so it can check for log level.
|
18
|
+
def initialize( logger, format=DEFAULT_FORMAT, debug=DEFAULT_DEBUG_FORMAT ) # :notnew:
|
19
|
+
@logger = logger
|
20
|
+
@format = format
|
21
|
+
@debug_format = debug
|
22
|
+
|
23
|
+
super()
|
24
|
+
end
|
25
|
+
|
26
|
+
######
|
27
|
+
public
|
28
|
+
######
|
29
|
+
|
30
|
+
# The Logger object associated with the formatter
|
31
|
+
attr_accessor :logger
|
32
|
+
|
33
|
+
# The logging format string
|
34
|
+
attr_accessor :format
|
35
|
+
|
36
|
+
# The logging format string that's used when outputting in debug mode
|
37
|
+
attr_accessor :debug_format
|
38
|
+
|
39
|
+
|
40
|
+
### Log using either the DEBUG_FORMAT if the associated logger is at ::DEBUG level or
|
41
|
+
### using FORMAT if it's anything less verbose.
|
42
|
+
def call( severity, time, progname, msg )
|
43
|
+
args = [
|
44
|
+
time.strftime( '%Y-%m-%d %H:%M:%S' ), # %1$s
|
45
|
+
time.usec, # %2$d
|
46
|
+
Process.pid, # %3$d
|
47
|
+
Thread.current == Thread.main ? 'main' : Thread.object_id, # %4$s
|
48
|
+
severity, # %5$s
|
49
|
+
progname, # %6$s
|
50
|
+
msg # %7$s
|
51
|
+
]
|
52
|
+
|
53
|
+
if @logger.level == Logger::DEBUG
|
54
|
+
return self.debug_format % args
|
55
|
+
else
|
56
|
+
return self.format % args
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end # class Configurability::LogFormatter
|
data/rake/191_compat.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# 1.9.1 fixes
|
2
|
+
|
3
|
+
|
4
|
+
# Make Pathname compatible with 1.8.7 Pathname.
|
5
|
+
unless Pathname.instance_methods.include?( :=~ )
|
6
|
+
class Pathname
|
7
|
+
def self::glob( *args ) # :yield: p
|
8
|
+
args = args.collect {|p| p.to_s }
|
9
|
+
if block_given?
|
10
|
+
Dir.glob(*args) {|f| yield self.new(f) }
|
11
|
+
else
|
12
|
+
Dir.glob(*args).map {|f| self.new(f) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def =~( other )
|
17
|
+
self.to_s =~ other
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_str
|
21
|
+
self.to_s
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
#
|
2
|
+
# Dependency-checking and Installation Rake Tasks
|
3
|
+
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'rubygems/dependency_installer'
|
7
|
+
require 'rubygems/source_index'
|
8
|
+
require 'rubygems/requirement'
|
9
|
+
require 'rubygems/doc_manager'
|
10
|
+
|
11
|
+
### Install the specified +gems+ if they aren't already installed.
|
12
|
+
def install_gems( gems )
|
13
|
+
|
14
|
+
defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({
|
15
|
+
:generate_rdoc => true,
|
16
|
+
:generate_ri => true,
|
17
|
+
:install_dir => Gem.dir,
|
18
|
+
:format_executable => false,
|
19
|
+
:test => false,
|
20
|
+
:version => Gem::Requirement.default,
|
21
|
+
})
|
22
|
+
|
23
|
+
# Check for root
|
24
|
+
if Process.euid != 0
|
25
|
+
$stderr.puts "This probably won't work, as you aren't root, but I'll try anyway"
|
26
|
+
end
|
27
|
+
|
28
|
+
gemindex = Gem::SourceIndex.from_installed_gems
|
29
|
+
|
30
|
+
gems.each do |gemname, reqstring|
|
31
|
+
requirement = Gem::Requirement.new( reqstring )
|
32
|
+
trace "requirement is: %p" % [ requirement ]
|
33
|
+
|
34
|
+
trace "Searching for an installed #{gemname}..."
|
35
|
+
specs = gemindex.find_name( gemname )
|
36
|
+
trace "...found %d specs: %s" %
|
37
|
+
[ specs.length, specs.collect {|s| "%s %s" % [s.name, s.version] }.join(', ') ]
|
38
|
+
|
39
|
+
if spec = specs.find {|spec| requirement.satisfied_by?(spec.version) }
|
40
|
+
log "Version %s of %s is already installed (needs %s); skipping..." %
|
41
|
+
[ spec.version, spec.name, requirement ]
|
42
|
+
next
|
43
|
+
end
|
44
|
+
|
45
|
+
rgv = Gem::Version.new( Gem::RubyGemsVersion )
|
46
|
+
installer = nil
|
47
|
+
|
48
|
+
log "Trying to install #{gemname.inspect} #{requirement}..."
|
49
|
+
if rgv >= Gem::Version.new( '1.1.1' )
|
50
|
+
installer = Gem::DependencyInstaller.new
|
51
|
+
installer.install( gemname, requirement )
|
52
|
+
else
|
53
|
+
installer = Gem::DependencyInstaller.new( gemname )
|
54
|
+
installer.install
|
55
|
+
end
|
56
|
+
|
57
|
+
installer.installed_gems.each do |spec|
|
58
|
+
log "Installed: %s" % [ spec.full_name ]
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
### Task: install runtime dependencies
|
66
|
+
desc "Install runtime dependencies as gems"
|
67
|
+
task :install_dependencies do
|
68
|
+
install_gems( DEPENDENCIES )
|
69
|
+
end
|
70
|
+
|
71
|
+
### Task: install gems for development tasks
|
72
|
+
desc "Install development dependencies as gems"
|
73
|
+
task :install_dev_dependencies do
|
74
|
+
install_gems( DEVELOPMENT_DEPENDENCIES )
|
75
|
+
end
|
76
|
+
|
@@ -0,0 +1,89 @@
|
|
1
|
+
#
|
2
|
+
# Documentation Rake tasks
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'rake/clean'
|
6
|
+
|
7
|
+
|
8
|
+
# Append docs/lib to the load path if it exists for documentation
|
9
|
+
# helpers.
|
10
|
+
DOCSLIB = DOCSDIR + 'lib'
|
11
|
+
$LOAD_PATH.unshift( DOCSLIB.to_s ) if DOCSLIB.exist?
|
12
|
+
|
13
|
+
# Make relative string paths of all the stuff we need to generate docs for
|
14
|
+
DOCFILES = Rake::FileList[ LIB_FILES + EXT_FILES + GEMSPEC.extra_rdoc_files ]
|
15
|
+
|
16
|
+
# Documentation coverage constants
|
17
|
+
COVERAGE_DIR = BASEDIR + 'coverage'
|
18
|
+
COVERAGE_REPORT = COVERAGE_DIR + 'documentation.txt'
|
19
|
+
|
20
|
+
|
21
|
+
# Prefer YARD, fallback to RDoc
|
22
|
+
begin
|
23
|
+
require 'yard'
|
24
|
+
require 'yard/rake/yardoc_task'
|
25
|
+
|
26
|
+
# Undo the lazy-assed monkeypatch yard/globals.rb installs and
|
27
|
+
# re-install them as mixins as they should have been from the
|
28
|
+
# start
|
29
|
+
# <metamonkeypatch>
|
30
|
+
class Object
|
31
|
+
remove_method :log
|
32
|
+
remove_method :P
|
33
|
+
end
|
34
|
+
|
35
|
+
module YardGlobals
|
36
|
+
def P(namespace, name = nil)
|
37
|
+
namespace, name = nil, namespace if name.nil?
|
38
|
+
YARD::Registry.resolve(namespace, name, false, true)
|
39
|
+
end
|
40
|
+
|
41
|
+
def log
|
42
|
+
YARD::Logger.instance
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class YARD::CLI::Base; include YardGlobals; end
|
47
|
+
class YARD::Parser::SourceParser; extend YardGlobals; include YardGlobals; end
|
48
|
+
class YARD::Parser::CParser; include YardGlobals; end
|
49
|
+
class YARD::CodeObjects::Base; include YardGlobals; end
|
50
|
+
class YARD::Handlers::Base; include YardGlobals; end
|
51
|
+
class YARD::Handlers::Processor; include YardGlobals; end
|
52
|
+
class YARD::Serializers::Base; include YardGlobals; end
|
53
|
+
class YARD::RegistryStore; include YardGlobals; end
|
54
|
+
class YARD::Docstring; include YardGlobals; end
|
55
|
+
module YARD::Templates::Helpers::ModuleHelper; include YardGlobals; end
|
56
|
+
# </metamonkeypatch>
|
57
|
+
|
58
|
+
YARD_OPTIONS = [] unless defined?( YARD_OPTIONS )
|
59
|
+
|
60
|
+
yardoctask = YARD::Rake::YardocTask.new( :apidocs ) do |task|
|
61
|
+
task.files = DOCFILES
|
62
|
+
task.options = YARD_OPTIONS
|
63
|
+
task.options << '--debug' << '--verbose' if $trace
|
64
|
+
end
|
65
|
+
yardoctask.before = lambda {
|
66
|
+
trace "Calling yardoc like:",
|
67
|
+
" yardoc %s" % [ quotelist(yardoctask.options + yardoctask.files).join(' ') ]
|
68
|
+
}
|
69
|
+
|
70
|
+
YARDOC_CACHE = BASEDIR + '.yardoc'
|
71
|
+
CLOBBER.include( YARDOC_CACHE.to_s )
|
72
|
+
|
73
|
+
rescue LoadError
|
74
|
+
require 'rdoc/task'
|
75
|
+
|
76
|
+
desc "Build API documentation in #{API_DOCSDIR}"
|
77
|
+
RDoc::Task.new( :apidocs ) do |task|
|
78
|
+
task.main = "README"
|
79
|
+
task.rdoc_files.include( DOCFILES )
|
80
|
+
task.rdoc_dir = API_DOCSDIR.to_s
|
81
|
+
task.options = RDOC_OPTIONS
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Need the DOCFILES to exist to build the API docs
|
86
|
+
task :apidocs => DOCFILES
|
87
|
+
|
88
|
+
CLEAN.include( API_DOCSDIR.to_s )
|
89
|
+
|
data/rake/helpers.rb
ADDED
@@ -0,0 +1,502 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
#####################################################################
|
4
|
+
### G L O B A L H E L P E R F U N C T I O N S
|
5
|
+
#####################################################################
|
6
|
+
|
7
|
+
|
8
|
+
require 'pathname'
|
9
|
+
require 'uri'
|
10
|
+
|
11
|
+
begin
|
12
|
+
require 'readline'
|
13
|
+
include Readline
|
14
|
+
rescue LoadError
|
15
|
+
# Fall back to a plain prompt
|
16
|
+
def readline( text )
|
17
|
+
$stderr.print( text.chomp )
|
18
|
+
return $stdin.gets
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module RakefileHelpers
|
23
|
+
# Set some ANSI escape code constants (Shamelessly stolen from Perl's
|
24
|
+
# Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com>
|
25
|
+
ANSI_ATTRIBUTES = {
|
26
|
+
'clear' => 0,
|
27
|
+
'reset' => 0,
|
28
|
+
'bold' => 1,
|
29
|
+
'dark' => 2,
|
30
|
+
'underline' => 4,
|
31
|
+
'underscore' => 4,
|
32
|
+
'blink' => 5,
|
33
|
+
'reverse' => 7,
|
34
|
+
'concealed' => 8,
|
35
|
+
|
36
|
+
'black' => 30, 'on_black' => 40,
|
37
|
+
'red' => 31, 'on_red' => 41,
|
38
|
+
'green' => 32, 'on_green' => 42,
|
39
|
+
'yellow' => 33, 'on_yellow' => 43,
|
40
|
+
'blue' => 34, 'on_blue' => 44,
|
41
|
+
'magenta' => 35, 'on_magenta' => 45,
|
42
|
+
'cyan' => 36, 'on_cyan' => 46,
|
43
|
+
'white' => 37, 'on_white' => 47
|
44
|
+
}
|
45
|
+
|
46
|
+
|
47
|
+
MULTILINE_PROMPT = <<-'EOF'
|
48
|
+
Enter one or more values for '%s'.
|
49
|
+
A blank line finishes input.
|
50
|
+
EOF
|
51
|
+
|
52
|
+
|
53
|
+
CLEAR_TO_EOL = "\e[K"
|
54
|
+
CLEAR_CURRENT_LINE = "\e[2K"
|
55
|
+
|
56
|
+
|
57
|
+
TAR_OPTS = { :compression => :gzip }
|
58
|
+
|
59
|
+
|
60
|
+
###############
|
61
|
+
module_function
|
62
|
+
###############
|
63
|
+
|
64
|
+
### Output a logging message
|
65
|
+
def log( *msg )
|
66
|
+
output = colorize( msg.flatten.join(' '), 'cyan' )
|
67
|
+
$stderr.puts( output )
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
### Output a logging message if tracing is on
|
72
|
+
def trace( *msg )
|
73
|
+
return unless $trace
|
74
|
+
output = colorize( msg.flatten.join(' '), 'yellow' )
|
75
|
+
$stderr.puts( output )
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
### Return the specified args as a string, quoting any that have a space.
|
80
|
+
def quotelist( *args )
|
81
|
+
return args.flatten.collect {|part| part =~ /\s/ ? part.inspect : part}
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
### Run the specified command +cmd+ with system(), failing if the execution
|
86
|
+
### fails.
|
87
|
+
def run( *cmd )
|
88
|
+
cmd.flatten!
|
89
|
+
|
90
|
+
if cmd.length > 1
|
91
|
+
trace( quotelist(*cmd) )
|
92
|
+
else
|
93
|
+
trace( cmd )
|
94
|
+
end
|
95
|
+
|
96
|
+
if $dryrun
|
97
|
+
$stderr.puts "(dry run mode)"
|
98
|
+
else
|
99
|
+
system( *cmd )
|
100
|
+
unless $?.success?
|
101
|
+
fail "Command failed: [%s]" % [cmd.join(' ')]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
### Run the given +cmd+ with the specified +args+ without interpolation by the shell and
|
108
|
+
### return anything written to its STDOUT.
|
109
|
+
def read_command_output( cmd, *args )
|
110
|
+
trace "Reading output from: %s" % [ cmd, quotelist(cmd, *args) ]
|
111
|
+
output = IO.read( '|-' ) or exec cmd, *args
|
112
|
+
return output
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
### Run a subordinate Rake process with the same options and the specified +targets+.
|
117
|
+
def rake( *targets )
|
118
|
+
opts = ARGV.select {|arg| arg[0,1] == '-' }
|
119
|
+
args = opts + targets.map {|t| t.to_s }
|
120
|
+
run 'rake', '-N', *args
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
### Open a pipe to a process running the given +cmd+ and call the given block with it.
|
125
|
+
def pipeto( *cmd )
|
126
|
+
$DEBUG = true
|
127
|
+
|
128
|
+
cmd.flatten!
|
129
|
+
log( "Opening a pipe to: ", cmd.collect {|part| part =~ /\s/ ? part.inspect : part} )
|
130
|
+
if $dryrun
|
131
|
+
$stderr.puts "(dry run mode)"
|
132
|
+
else
|
133
|
+
open( '|-', 'w+' ) do |io|
|
134
|
+
|
135
|
+
# Parent
|
136
|
+
if io
|
137
|
+
yield( io )
|
138
|
+
|
139
|
+
# Child
|
140
|
+
else
|
141
|
+
exec( *cmd )
|
142
|
+
fail "Command failed: [%s]" % [cmd.join(' ')]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
### Download the file at +sourceuri+ via HTTP and write it to +targetfile+.
|
150
|
+
def download( sourceuri, targetfile=nil )
|
151
|
+
oldsync = $stdout.sync
|
152
|
+
$stdout.sync = true
|
153
|
+
require 'open-uri'
|
154
|
+
|
155
|
+
targetpath = Pathname.new( targetfile )
|
156
|
+
|
157
|
+
log "Downloading %s to %s" % [sourceuri, targetfile]
|
158
|
+
trace " connecting..."
|
159
|
+
ifh = open( sourceuri ) do |ifh|
|
160
|
+
trace " connected..."
|
161
|
+
targetpath.open( File::WRONLY|File::TRUNC|File::CREAT, 0644 ) do |ofh|
|
162
|
+
log "Downloading..."
|
163
|
+
buf = ''
|
164
|
+
|
165
|
+
while ifh.read( 16384, buf )
|
166
|
+
until buf.empty?
|
167
|
+
bytes = ofh.write( buf )
|
168
|
+
buf.slice!( 0, bytes )
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
log "Done."
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
|
177
|
+
return targetpath
|
178
|
+
ensure
|
179
|
+
$stdout.sync = oldsync
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
### Return the fully-qualified path to the specified +program+ in the PATH.
|
184
|
+
def which( program )
|
185
|
+
return nil unless ENV['PATH']
|
186
|
+
ENV['PATH'].split(/:/).
|
187
|
+
collect {|dir| Pathname.new(dir) + program }.
|
188
|
+
find {|path| path.exist? && path.executable? }
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
### Create a string that contains the ANSI codes specified and return it
|
193
|
+
def ansi_code( *attributes )
|
194
|
+
attributes.flatten!
|
195
|
+
attributes.collect! {|at| at.to_s }
|
196
|
+
# $stderr.puts "Returning ansicode for TERM = %p: %p" %
|
197
|
+
# [ ENV['TERM'], attributes ]
|
198
|
+
return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
|
199
|
+
attributes = ANSI_ATTRIBUTES.values_at( *attributes ).compact.join(';')
|
200
|
+
|
201
|
+
# $stderr.puts " attr is: %p" % [attributes]
|
202
|
+
if attributes.empty?
|
203
|
+
return ''
|
204
|
+
else
|
205
|
+
return "\e[%sm" % attributes
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
### Colorize the given +string+ with the specified +attributes+ and return it, handling
|
211
|
+
### line-endings, color reset, etc.
|
212
|
+
def colorize( *args )
|
213
|
+
string = ''
|
214
|
+
|
215
|
+
if block_given?
|
216
|
+
string = yield
|
217
|
+
else
|
218
|
+
string = args.shift
|
219
|
+
end
|
220
|
+
|
221
|
+
ending = string[/(\s)$/] || ''
|
222
|
+
string = string.rstrip
|
223
|
+
|
224
|
+
return ansi_code( args.flatten ) + string + ansi_code( 'reset' ) + ending
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
### Output the specified <tt>msg</tt> as an ANSI-colored error message
|
229
|
+
### (white on red).
|
230
|
+
def error_message( msg, details='' )
|
231
|
+
$stderr.puts colorize( 'bold', 'white', 'on_red' ) { msg } + details
|
232
|
+
end
|
233
|
+
alias :error :error_message
|
234
|
+
|
235
|
+
|
236
|
+
### Highlight and embed a prompt control character in the given +string+ and return it.
|
237
|
+
def make_prompt_string( string )
|
238
|
+
return CLEAR_CURRENT_LINE + colorize( 'bold', 'green' ) { string + ' ' }
|
239
|
+
end
|
240
|
+
|
241
|
+
|
242
|
+
### Output the specified <tt>prompt_string</tt> as a prompt (in green) and
|
243
|
+
### return the user's input with leading and trailing spaces removed. If a
|
244
|
+
### test is provided, the prompt will repeat until the test returns true.
|
245
|
+
### An optional failure message can also be passed in.
|
246
|
+
def prompt( prompt_string, failure_msg="Try again." ) # :yields: response
|
247
|
+
prompt_string.chomp!
|
248
|
+
prompt_string << ":" unless /\W$/.match( prompt_string )
|
249
|
+
response = nil
|
250
|
+
|
251
|
+
begin
|
252
|
+
prompt = make_prompt_string( prompt_string )
|
253
|
+
response = readline( prompt ) || ''
|
254
|
+
response.strip!
|
255
|
+
if block_given? && ! yield( response )
|
256
|
+
error_message( failure_msg + "\n\n" )
|
257
|
+
response = nil
|
258
|
+
end
|
259
|
+
end while response.nil?
|
260
|
+
|
261
|
+
return response
|
262
|
+
end
|
263
|
+
|
264
|
+
|
265
|
+
### Prompt the user with the given <tt>prompt_string</tt> via #prompt,
|
266
|
+
### substituting the given <tt>default</tt> if the user doesn't input
|
267
|
+
### anything. If a test is provided, the prompt will repeat until the test
|
268
|
+
### returns true. An optional failure message can also be passed in.
|
269
|
+
def prompt_with_default( prompt_string, default, failure_msg="Try again." )
|
270
|
+
response = nil
|
271
|
+
|
272
|
+
begin
|
273
|
+
default ||= '~'
|
274
|
+
response = prompt( "%s [%s]" % [ prompt_string, default ] )
|
275
|
+
response = default.to_s if !response.nil? && response.empty?
|
276
|
+
|
277
|
+
trace "Validating response %p" % [ response ]
|
278
|
+
|
279
|
+
# the block is a validator. We need to make sure that the user didn't
|
280
|
+
# enter '~', because if they did, it's nil and we should move on. If
|
281
|
+
# they didn't, then call the block.
|
282
|
+
if block_given? && response != '~' && ! yield( response )
|
283
|
+
error_message( failure_msg + "\n\n" )
|
284
|
+
response = nil
|
285
|
+
end
|
286
|
+
end while response.nil?
|
287
|
+
|
288
|
+
return nil if response == '~'
|
289
|
+
return response
|
290
|
+
end
|
291
|
+
|
292
|
+
|
293
|
+
### Prompt for an array of values
|
294
|
+
def prompt_for_multiple_values( label, default=nil )
|
295
|
+
$stderr.puts( MULTILINE_PROMPT % [label] )
|
296
|
+
if default
|
297
|
+
$stderr.puts "Enter a single blank line to keep the default:\n %p" % [ default ]
|
298
|
+
end
|
299
|
+
|
300
|
+
results = []
|
301
|
+
result = nil
|
302
|
+
|
303
|
+
begin
|
304
|
+
result = readline( make_prompt_string("> ") )
|
305
|
+
if result.nil? || result.empty?
|
306
|
+
results << default if default && results.empty?
|
307
|
+
else
|
308
|
+
results << result
|
309
|
+
end
|
310
|
+
end until result.nil? || result.empty?
|
311
|
+
|
312
|
+
return results.flatten
|
313
|
+
end
|
314
|
+
|
315
|
+
|
316
|
+
### Turn echo and masking of input on/off.
|
317
|
+
def noecho( masked=false )
|
318
|
+
require 'termios'
|
319
|
+
|
320
|
+
rval = nil
|
321
|
+
term = Termios.getattr( $stdin )
|
322
|
+
|
323
|
+
begin
|
324
|
+
newt = term.dup
|
325
|
+
newt.c_lflag &= ~Termios::ECHO
|
326
|
+
newt.c_lflag &= ~Termios::ICANON if masked
|
327
|
+
|
328
|
+
Termios.tcsetattr( $stdin, Termios::TCSANOW, newt )
|
329
|
+
|
330
|
+
rval = yield
|
331
|
+
ensure
|
332
|
+
Termios.tcsetattr( $stdin, Termios::TCSANOW, term )
|
333
|
+
end
|
334
|
+
|
335
|
+
return rval
|
336
|
+
end
|
337
|
+
|
338
|
+
|
339
|
+
### Prompt the user for her password, turning off echo if the 'termios' module is
|
340
|
+
### available.
|
341
|
+
def prompt_for_password( prompt="Password: " )
|
342
|
+
return noecho( true ) do
|
343
|
+
$stderr.print( prompt )
|
344
|
+
($stdin.gets || '').chomp
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
|
349
|
+
### Display a description of a potentially-dangerous task, and prompt
|
350
|
+
### for confirmation. If the user answers with anything that begins
|
351
|
+
### with 'y', yield to the block. If +abort_on_decline+ is +true+,
|
352
|
+
### any non-'y' answer will fail with an error message.
|
353
|
+
def ask_for_confirmation( description, abort_on_decline=true )
|
354
|
+
puts description
|
355
|
+
|
356
|
+
answer = prompt_with_default( "Continue?", 'n' ) do |input|
|
357
|
+
input =~ /^[yn]/i
|
358
|
+
end
|
359
|
+
|
360
|
+
if answer =~ /^y/i
|
361
|
+
return yield
|
362
|
+
elsif abort_on_decline
|
363
|
+
error "Aborted."
|
364
|
+
fail
|
365
|
+
end
|
366
|
+
|
367
|
+
return false
|
368
|
+
end
|
369
|
+
alias :prompt_for_confirmation :ask_for_confirmation
|
370
|
+
|
371
|
+
|
372
|
+
### Search line-by-line in the specified +file+ for the given +regexp+, returning the
|
373
|
+
### first match, or nil if no match was found. If the +regexp+ has any capture groups,
|
374
|
+
### those will be returned in an Array, else the whole matching line is returned.
|
375
|
+
def find_pattern_in_file( regexp, file )
|
376
|
+
rval = nil
|
377
|
+
|
378
|
+
File.open( file, 'r' ).each do |line|
|
379
|
+
if (( match = regexp.match(line) ))
|
380
|
+
rval = match.captures.empty? ? match[0] : match.captures
|
381
|
+
break
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
return rval
|
386
|
+
end
|
387
|
+
|
388
|
+
|
389
|
+
### Search line-by-line in the output of the specified +cmd+ for the given +regexp+,
|
390
|
+
### returning the first match, or nil if no match was found. If the +regexp+ has any
|
391
|
+
### capture groups, those will be returned in an Array, else the whole matching line
|
392
|
+
### is returned.
|
393
|
+
def find_pattern_in_pipe( regexp, *cmd )
|
394
|
+
require 'open3'
|
395
|
+
output = []
|
396
|
+
|
397
|
+
log( cmd.collect {|part| part =~ /\s/ ? part.inspect : part} )
|
398
|
+
Open3.popen3( *cmd ) do |stdin, stdout, stderr|
|
399
|
+
stdin.close
|
400
|
+
|
401
|
+
output << stdout.gets until stdout.eof?
|
402
|
+
output << stderr.gets until stderr.eof?
|
403
|
+
end
|
404
|
+
|
405
|
+
result = output.find { |line| regexp.match(line) }
|
406
|
+
return $1 || result
|
407
|
+
end
|
408
|
+
|
409
|
+
|
410
|
+
### Invoke the user's editor on the given +filename+ and return the exit code
|
411
|
+
### from doing so.
|
412
|
+
def edit( filename )
|
413
|
+
editor = ENV['EDITOR'] || ENV['VISUAL'] || DEFAULT_EDITOR
|
414
|
+
system editor, filename
|
415
|
+
unless $?.success? || editor =~ /vim/i
|
416
|
+
fail "Editor exited uncleanly."
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
|
421
|
+
### Extract all the non Rake-target arguments from ARGV and return them.
|
422
|
+
def get_target_args
|
423
|
+
args = ARGV.reject {|arg| arg =~ /^-/ || Rake::Task.task_defined?(arg) }
|
424
|
+
return args
|
425
|
+
end
|
426
|
+
|
427
|
+
|
428
|
+
### Log a subdirectory change, execute a block, and exit the subdirectory
|
429
|
+
def in_subdirectory( subdir )
|
430
|
+
block = Proc.new
|
431
|
+
|
432
|
+
log "Entering #{subdir}"
|
433
|
+
Dir.chdir( subdir, &block )
|
434
|
+
log "Leaving #{subdir}"
|
435
|
+
end
|
436
|
+
|
437
|
+
|
438
|
+
### Make an easily-comparable version vector out of +ver+ and return it.
|
439
|
+
def vvec( ver )
|
440
|
+
return ver.split('.').collect {|char| char.to_i }.pack('N*')
|
441
|
+
end
|
442
|
+
|
443
|
+
|
444
|
+
### Archive::Tar::Reader#extract (as of 0.9.0) is broken w.r.t.
|
445
|
+
### permissions, so we have to do this ourselves.
|
446
|
+
def untar( tarfile, targetdir )
|
447
|
+
require 'archive/tar'
|
448
|
+
targetdir = Pathname( targetdir )
|
449
|
+
raise "No such directory: #{targetdir}" unless targetdir.directory?
|
450
|
+
|
451
|
+
reader = Archive::Tar::Reader.new( tarfile.to_s, TAR_OPTS )
|
452
|
+
|
453
|
+
mkdir_p( targetdir )
|
454
|
+
reader.each( true ) do |header, body|
|
455
|
+
path = targetdir + header[:path]
|
456
|
+
# trace "Header is: %p" % [ header ]
|
457
|
+
|
458
|
+
case header[:type]
|
459
|
+
when :file
|
460
|
+
trace " #{path}"
|
461
|
+
path.open( File::WRONLY|File::EXCL|File::CREAT|File::TRUNC, header[:mode] ) do |fio|
|
462
|
+
bytesize = header[:size]
|
463
|
+
fio.write( body[0,bytesize] )
|
464
|
+
end
|
465
|
+
|
466
|
+
when :directory
|
467
|
+
trace " #{path}"
|
468
|
+
path.mkpath
|
469
|
+
|
470
|
+
when :link
|
471
|
+
linktarget = targetdir + header[:dest]
|
472
|
+
trace " #{path} => #{linktarget}"
|
473
|
+
path.make_link( linktarget.to_s )
|
474
|
+
|
475
|
+
when :symlink
|
476
|
+
linktarget = targetdir + header[:dest]
|
477
|
+
trace " #{path} -> #{linktarget}"
|
478
|
+
path.make_symlink( linktarget )
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
end
|
483
|
+
|
484
|
+
|
485
|
+
### Extract the contents of the specified +zipfile+ into the given +targetdir+.
|
486
|
+
def unzip( zipfile, targetdir, *files )
|
487
|
+
require 'zip/zip'
|
488
|
+
targetdir = Pathname( targetdir )
|
489
|
+
raise "No such directory: #{targetdir}" unless targetdir.directory?
|
490
|
+
|
491
|
+
Zip::ZipFile.foreach( zipfile ) do |entry|
|
492
|
+
# trace " entry is: %p" % [ entry ]
|
493
|
+
next unless files.empty? || files.include?( entry.name )
|
494
|
+
target_path = targetdir + entry.name
|
495
|
+
# trace " would extract to: %s" % [ target_path ]
|
496
|
+
entry.extract( target_path ) { true }
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
end # module Rakefile::Helpers
|
501
|
+
|
502
|
+
|