inochi 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +2 -0
- data/bin/inochi +30 -29
- data/doc/api/Inochi.html +439 -323
- data/doc/api/Inochi/Manual.html +230 -0
- data/doc/api/Inochi/Phrases.html +409 -0
- data/doc/api/Inochi/Version.html +222 -0
- data/doc/api/all-methods.html +97 -9
- data/doc/api/all-namespaces.html +7 -1
- data/doc/api/readme.html +3 -0
- data/doc/history.erb +50 -20
- data/doc/index.xhtml +355 -141
- data/doc/intro.erb +14 -14
- data/doc/setup.erb +8 -7
- data/doc/usage.erb +110 -5
- data/lib/inochi.rb +14 -3
- data/lib/inochi/book.rb +84 -0
- data/lib/inochi/init.rb +239 -0
- data/lib/inochi/main.rb +75 -0
- data/lib/inochi/rake.rb +777 -0
- data/lib/inochi/util.rb +77 -0
- data/test/{inochi/inochi.rb → inochi.rb} +0 -0
- metadata +32 -5
- data/lib/inochi/inochi.rb +0 -1064
data/lib/inochi/main.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
##
|
2
|
+
# Provides a common configuration for the main project executable:
|
3
|
+
#
|
4
|
+
# * The program description (the sequence of non-blank lines at the
|
5
|
+
# top of the file in which this method is invoked) is properly
|
6
|
+
# formatted and displayed at the top of program's help information.
|
7
|
+
#
|
8
|
+
# * The program version information is fetched from the project module
|
9
|
+
# and formatted in YAML fashion for easy consumption by other tools.
|
10
|
+
#
|
11
|
+
# * A list of command-line options is displayed at
|
12
|
+
# the bottom of the program's help information.
|
13
|
+
#
|
14
|
+
# It is assumed that this method is invoked from only within
|
15
|
+
# the main project executable (in the project bin/ directory).
|
16
|
+
#
|
17
|
+
# @param [Symbol] project_symbol
|
18
|
+
# Name of the Ruby constant which serves
|
19
|
+
# as a namespace for the entire project.
|
20
|
+
#
|
21
|
+
# @param trollop_args
|
22
|
+
# Optional arguments for Trollop::options().
|
23
|
+
#
|
24
|
+
# @param trollop_config
|
25
|
+
# Optional block argument for Trollop::options().
|
26
|
+
#
|
27
|
+
# @return The result of Trollop::options().
|
28
|
+
#
|
29
|
+
def Inochi.main project_symbol, *trollop_args, &trollop_config
|
30
|
+
program_file = first_caller_file
|
31
|
+
program_name = File.basename(program_file)
|
32
|
+
program_home = File.dirname(File.dirname(program_file))
|
33
|
+
|
34
|
+
# load the project module
|
35
|
+
require File.join(program_home, 'lib', program_name)
|
36
|
+
project_module = fetch_project_module(project_symbol)
|
37
|
+
|
38
|
+
# parse command-line options
|
39
|
+
require 'trollop'
|
40
|
+
|
41
|
+
options = Trollop.options(*trollop_args) do
|
42
|
+
|
43
|
+
# show project description
|
44
|
+
text "#{project_module::PROJECT} - #{project_module::TAGLINE}"
|
45
|
+
text ''
|
46
|
+
|
47
|
+
# show program description
|
48
|
+
text File.read(program_file)[/\A.*?^$\n/m]. # grab the header
|
49
|
+
gsub(/^# ?/, ''). # strip the comment markers
|
50
|
+
sub(/\A!.*?\n/, '').lstrip # omit the shebang line
|
51
|
+
text ''
|
52
|
+
|
53
|
+
instance_eval(&trollop_config) if trollop_config
|
54
|
+
|
55
|
+
# show version information
|
56
|
+
version %w[PROJECT VERSION RELEASE WEBSITE INSTALL].map {|c|
|
57
|
+
"#{c.downcase}: #{project_module.const_get c}"
|
58
|
+
}.join("\n")
|
59
|
+
|
60
|
+
opt :manual, 'Show the user manual'
|
61
|
+
opt :locale, 'Set preferred language', :type => :string
|
62
|
+
end
|
63
|
+
|
64
|
+
if options[:manual]
|
65
|
+
require 'launchy'
|
66
|
+
Launchy::Browser.run "#{project_module::INSTALL}/doc/index.xhtml"
|
67
|
+
exit
|
68
|
+
end
|
69
|
+
|
70
|
+
if locale = options[:locale]
|
71
|
+
project_module::PHRASES.locale = locale
|
72
|
+
end
|
73
|
+
|
74
|
+
options
|
75
|
+
end
|
data/lib/inochi/rake.rb
ADDED
@@ -0,0 +1,777 @@
|
|
1
|
+
##
|
2
|
+
# Provides Rake tasks for packaging, publishing, and announcing your project.
|
3
|
+
#
|
4
|
+
# * An AUTHORS constant (which has the form "[[name, info]]"
|
5
|
+
# where "name" is the name of a copyright holder and "info" is
|
6
|
+
# their contact information) is added to the project module.
|
7
|
+
#
|
8
|
+
# Unless this information is supplied via the :authors option,
|
9
|
+
# it is automatically extracted from copyright notices in the
|
10
|
+
# project license file, where the first copyright notice is
|
11
|
+
# expected to correspond to the primary project maintainer.
|
12
|
+
#
|
13
|
+
# Copyright notices must be in the following form:
|
14
|
+
#
|
15
|
+
# Copyright YEAR HOLDER <EMAIL>
|
16
|
+
#
|
17
|
+
# Where HOLDER is the name of the copyright holder, YEAR is the year
|
18
|
+
# when the copyright holder first began working on the project, and
|
19
|
+
# EMAIL is (optional) the email address of the copyright holder.
|
20
|
+
#
|
21
|
+
# @param [Symbol] project_symbol
|
22
|
+
# Name of the Ruby constant which serves
|
23
|
+
# as a namespace for the entire project.
|
24
|
+
#
|
25
|
+
# @param [Hash] options
|
26
|
+
# Additional method parameters, which are all optional:
|
27
|
+
#
|
28
|
+
# [Array] :authors =>
|
29
|
+
# A list of project authors and their contact information. This
|
30
|
+
# list must have the form "[[name, info]]" where "name" is the name
|
31
|
+
# of a project author and "info" is their contact information.
|
32
|
+
#
|
33
|
+
# [String] :license_file =>
|
34
|
+
# Path (relative to the main project directory which contains the
|
35
|
+
# project Rakefile) to the file which contains the project license.
|
36
|
+
#
|
37
|
+
# The default value is "LICENSE".
|
38
|
+
#
|
39
|
+
# [String] :logins_file =>
|
40
|
+
# Path to the YAML file which contains login
|
41
|
+
# information for publishing release announcements.
|
42
|
+
#
|
43
|
+
# The default value is "~/.config/inochi/logins.yaml"
|
44
|
+
# where "~" is the path to your home directory.
|
45
|
+
#
|
46
|
+
# [String] :rubyforge_project =>
|
47
|
+
# Name of the RubyForge project where
|
48
|
+
# release packages will be published.
|
49
|
+
#
|
50
|
+
# The default value is the value of the PROGRAM constant.
|
51
|
+
#
|
52
|
+
# [String] :rubyforge_section =>
|
53
|
+
# Name of the RubyForge project's File Release System
|
54
|
+
# section where release packages will be published.
|
55
|
+
#
|
56
|
+
# The default value is the value of the :rubyforge_project parameter.
|
57
|
+
#
|
58
|
+
# [String] :raa_project =>
|
59
|
+
# Name of the RAA (Ruby Application Archive) entry for this project.
|
60
|
+
#
|
61
|
+
# The default value is the value of the PROGRAM constant.
|
62
|
+
#
|
63
|
+
# [String] :upload_target =>
|
64
|
+
# Where to upload the project documentation.
|
65
|
+
# See "destination" in the rsync manual.
|
66
|
+
#
|
67
|
+
# The default value is nil.
|
68
|
+
#
|
69
|
+
# [String] :upload_delete =>
|
70
|
+
# Delete unknown files at the upload target location?
|
71
|
+
#
|
72
|
+
# The default value is false.
|
73
|
+
#
|
74
|
+
# [Array] :upload_options =>
|
75
|
+
# Additional command-line arguments to the rsync command.
|
76
|
+
#
|
77
|
+
# The default value is an empty array.
|
78
|
+
#
|
79
|
+
# @param gem_config
|
80
|
+
# Block that is passed to Gem::specification.new()
|
81
|
+
# for additonal gem configuration.
|
82
|
+
#
|
83
|
+
# @yieldparam [Gem::Specification] gem_spec the gem specification
|
84
|
+
#
|
85
|
+
def Inochi.rake project_symbol, options = {}, &gem_config
|
86
|
+
program_file = first_caller_file
|
87
|
+
program_home = File.dirname(program_file)
|
88
|
+
|
89
|
+
# load the project module
|
90
|
+
program_name = File.basename(program_home)
|
91
|
+
project_libs = File.join('lib', program_name)
|
92
|
+
|
93
|
+
require project_libs
|
94
|
+
project_module = fetch_project_module(project_symbol)
|
95
|
+
|
96
|
+
# supply default options
|
97
|
+
options[:rubyforge_project] ||= program_name
|
98
|
+
options[:rubyforge_section] ||= program_name
|
99
|
+
options[:raa_project] ||= program_name
|
100
|
+
|
101
|
+
options[:license_file] ||= 'LICENSE'
|
102
|
+
options[:logins_file] ||= File.join(
|
103
|
+
ENV['HOME'] || ENV['USERPROFILE'] || '.',
|
104
|
+
'.config', 'inochi', 'logins.yaml'
|
105
|
+
)
|
106
|
+
|
107
|
+
options[:upload_delete] ||= false
|
108
|
+
options[:upload_options] ||= []
|
109
|
+
|
110
|
+
# add AUTHORS constant to the project module
|
111
|
+
copyright_holders = options[:authors] ||
|
112
|
+
File.read(options[:license_file]).
|
113
|
+
scan(/Copyright.*?\d+\s+(.*)/).flatten.
|
114
|
+
map {|s| (s =~ /\s*<(.*?)>/) ? [$`, $1] : [s, ''] }
|
115
|
+
|
116
|
+
project_module.const_set :AUTHORS, copyright_holders
|
117
|
+
|
118
|
+
require 'rake/clean'
|
119
|
+
|
120
|
+
hide_rake_task = lambda do |name|
|
121
|
+
Rake::Task[name].instance_variable_set :@comment, nil
|
122
|
+
end
|
123
|
+
|
124
|
+
# translation
|
125
|
+
directory 'lang'
|
126
|
+
|
127
|
+
lang_dump_deps = 'lang'
|
128
|
+
lang_dump_file = 'lang/phrases.yaml'
|
129
|
+
|
130
|
+
desc 'Extract language phrases for translation.'
|
131
|
+
task 'lang:dump' => lang_dump_file
|
132
|
+
|
133
|
+
file lang_dump_file => lang_dump_deps do
|
134
|
+
ENV['dump_lang_phrases'] = '1'
|
135
|
+
Rake::Task[:test].invoke
|
136
|
+
end
|
137
|
+
|
138
|
+
lang_conv_delim = "\n" * 5
|
139
|
+
|
140
|
+
desc 'Translate extracted language phrases (from=LANGUAGE_CODE).'
|
141
|
+
task 'lang:conv' => lang_dump_file do |t|
|
142
|
+
require 'babelfish'
|
143
|
+
|
144
|
+
unless
|
145
|
+
src_lang = ENV['from'] and
|
146
|
+
BabelFish::LANGUAGE_CODES.include? src_lang
|
147
|
+
then
|
148
|
+
message = ['The "from" parameter must be specified as follows:']
|
149
|
+
|
150
|
+
BabelFish::LANGUAGE_CODES.each do |c|
|
151
|
+
n = BabelFish::LANGUAGE_NAMES[c]
|
152
|
+
message << " rake #{t.name} from=#{c} # from #{n}"
|
153
|
+
end
|
154
|
+
|
155
|
+
raise ArgumentError, message.join("\n")
|
156
|
+
end
|
157
|
+
|
158
|
+
begin
|
159
|
+
require 'yaml'
|
160
|
+
phrases = YAML.load_file(lang_dump_file).keys.sort
|
161
|
+
rescue
|
162
|
+
warn "Could not load phrases from #{lang_dump_file.inspect}"
|
163
|
+
raise
|
164
|
+
end
|
165
|
+
|
166
|
+
src_lang_name = BabelFish::LANGUAGE_NAMES[src_lang]
|
167
|
+
|
168
|
+
BabelFish::LANGUAGE_PAIRS[src_lang].each do |dst_lang|
|
169
|
+
dst_file = "lang/#{dst_lang}.yaml"
|
170
|
+
dst_lang_name = BabelFish::LANGUAGE_NAMES[dst_lang]
|
171
|
+
|
172
|
+
puts "Translating phrases from #{src_lang_name} into #{dst_lang_name} as #{dst_file.inspect}"
|
173
|
+
|
174
|
+
translations = BabelFish.translate(
|
175
|
+
phrases.join(lang_conv_delim), src_lang, dst_lang
|
176
|
+
).split(lang_conv_delim)
|
177
|
+
|
178
|
+
File.open(dst_file, 'w') do |f|
|
179
|
+
f.puts "# #{dst_lang} (#{dst_lang_name})"
|
180
|
+
|
181
|
+
phrases.zip(translations).each do |a, b|
|
182
|
+
f.puts "#{a}: #{b}"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# testing
|
189
|
+
desc 'Run all unit tests.'
|
190
|
+
task :test do
|
191
|
+
ruby '-w', '-I.', '-Ilib', '-r', program_name, '-e', %q{
|
192
|
+
# dump language phrases *after* exercising all code (and
|
193
|
+
# thereby populating the phrases cache) in the project
|
194
|
+
at_exit do
|
195
|
+
if ENV['dump_lang_phrases'] == '1'
|
196
|
+
file = %s
|
197
|
+
list = %s::PHRASES.phrases
|
198
|
+
data = list.map {|s| s + ':' }.join("\n")
|
199
|
+
|
200
|
+
File.write file, data
|
201
|
+
|
202
|
+
puts "Extracted #{list.length} language phrases into #{file.inspect}"
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# set title of test suite
|
207
|
+
$0 = File.basename(Dir.pwd)
|
208
|
+
|
209
|
+
require 'minitest/unit'
|
210
|
+
require 'minitest/spec'
|
211
|
+
require 'minitest/mock'
|
212
|
+
MiniTest::Unit.autorun
|
213
|
+
|
214
|
+
Dir['test/**/*.rb'].sort.each do |test|
|
215
|
+
unit = test.sub('test/', 'lib/')
|
216
|
+
|
217
|
+
if File.exist? unit
|
218
|
+
# strip file extension because require()
|
219
|
+
# does not normalize its input and it
|
220
|
+
# will think that the two paths (with &
|
221
|
+
# without file extension) are different
|
222
|
+
unit_path = unit.sub(/\.rb$/, '').sub('lib/', '')
|
223
|
+
test_path = test.sub(/\.rb$/, '')
|
224
|
+
|
225
|
+
require unit_path
|
226
|
+
require test_path
|
227
|
+
else
|
228
|
+
warn "Skipped test #{test.inspect} because it lacks a corresponding #{unit.inspect} unit."
|
229
|
+
end
|
230
|
+
end
|
231
|
+
} % [lang_dump_file.inspect, project_symbol]
|
232
|
+
end
|
233
|
+
|
234
|
+
# documentation
|
235
|
+
desc 'Build all documentation.'
|
236
|
+
task :doc => %w[ doc:api doc:man ]
|
237
|
+
|
238
|
+
# user manual
|
239
|
+
doc_man_src = 'doc/index.erb'
|
240
|
+
doc_man_dst = 'doc/index.xhtml'
|
241
|
+
doc_man_deps = FileList['doc/*.erb']
|
242
|
+
|
243
|
+
doc_man_doc = nil
|
244
|
+
task :doc_man_doc => doc_man_src do
|
245
|
+
unless doc_man_doc
|
246
|
+
require 'erbook' unless defined? ERBook
|
247
|
+
|
248
|
+
doc_man_txt = File.read(doc_man_src)
|
249
|
+
doc_man_doc = ERBook::Document.new(:xhtml, doc_man_txt, doc_man_src, :unindent => true)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
desc 'Build the user manual.'
|
254
|
+
task 'doc:man' => doc_man_dst
|
255
|
+
|
256
|
+
file doc_man_dst => doc_man_deps do
|
257
|
+
Rake::Task[:doc_man_doc].invoke
|
258
|
+
File.write doc_man_dst, doc_man_doc
|
259
|
+
end
|
260
|
+
|
261
|
+
CLOBBER.include doc_man_dst
|
262
|
+
|
263
|
+
# API reference
|
264
|
+
doc_api_dst = 'doc/api'
|
265
|
+
|
266
|
+
desc 'Build API reference.'
|
267
|
+
task 'doc:api' => doc_api_dst
|
268
|
+
|
269
|
+
require 'yard'
|
270
|
+
YARD::Rake::YardocTask.new doc_api_dst do |t|
|
271
|
+
t.options.push '--protected',
|
272
|
+
'--output-dir', doc_api_dst,
|
273
|
+
'--readme', options[:license_file]
|
274
|
+
|
275
|
+
task doc_api_dst => options[:license_file]
|
276
|
+
end
|
277
|
+
|
278
|
+
hide_rake_task[doc_api_dst]
|
279
|
+
|
280
|
+
CLEAN.include '.yardoc'
|
281
|
+
CLOBBER.include doc_api_dst
|
282
|
+
|
283
|
+
# announcements
|
284
|
+
desc 'Build all release announcements.'
|
285
|
+
task :ann => %w[ ann:feed ann:html ann:text ann:mail ]
|
286
|
+
|
287
|
+
# it has long been a tradition to use an "[ANN]" prefix
|
288
|
+
# when announcing things on the ruby-talk mailing list
|
289
|
+
ann_prefix = '[ANN] '
|
290
|
+
ann_subject = ann_prefix + project_module::DISPLAY
|
291
|
+
ann_project = ann_prefix + project_module::PROJECT
|
292
|
+
|
293
|
+
# fetch the project summary from user manual
|
294
|
+
ann_nfo_doc = nil
|
295
|
+
task :ann_nfo_doc => :doc_man_doc do
|
296
|
+
ann_nfo_doc = $project_summary_node
|
297
|
+
end
|
298
|
+
|
299
|
+
# fetch release notes from user manual
|
300
|
+
ann_rel_doc = nil
|
301
|
+
task :ann_rel_doc => :doc_man_doc do
|
302
|
+
unless ann_rel_doc
|
303
|
+
if parent = $project_history_node
|
304
|
+
if child = parent.children.first
|
305
|
+
ann_rel_doc = child
|
306
|
+
else
|
307
|
+
raise 'The "project_history" node in the user manual lacks child nodes.'
|
308
|
+
end
|
309
|
+
else
|
310
|
+
raise 'The user manual lacks a "project_history" node.'
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# build release notes in HTML and plain text
|
316
|
+
# converts the given HTML into plain text. we do this using
|
317
|
+
# lynx because (1) it outputs a list of all hyperlinks used
|
318
|
+
# in the HTML document and (2) it runs on all major platforms
|
319
|
+
convert_html_to_text = lambda do |html|
|
320
|
+
require 'tempfile'
|
321
|
+
|
322
|
+
begin
|
323
|
+
# lynx's -dump option requires a .html file
|
324
|
+
tmp_file = Tempfile.new(Inochi::PROGRAM).path + '.html'
|
325
|
+
|
326
|
+
File.write tmp_file, html
|
327
|
+
text = `lynx -dump #{tmp_file} -width 70`
|
328
|
+
ensure
|
329
|
+
File.delete tmp_file
|
330
|
+
end
|
331
|
+
|
332
|
+
# improve readability of list items
|
333
|
+
# by adding a blank line between them
|
334
|
+
text.gsub! %r{(\r?\n)( +\* \S)}, '\1\1\2'
|
335
|
+
|
336
|
+
text
|
337
|
+
end
|
338
|
+
|
339
|
+
# binds relative addresses in the given HTML to the project docsite
|
340
|
+
resolve_html_links = lambda do |html|
|
341
|
+
# resolve relative URLs into absolute URLs
|
342
|
+
# see http://en.wikipedia.org/wiki/URI_scheme#Generic_syntax
|
343
|
+
require 'addressable/uri'
|
344
|
+
uri = Addressable::URI.parse(project_module::DOCSITE)
|
345
|
+
doc_url = uri.to_s
|
346
|
+
dir_url = uri.path =~ %r{/$|^$} ? doc_url : File.dirname(doc_url)
|
347
|
+
|
348
|
+
html.to_s.gsub %r{(href=|src=)(.)(.*?)(\2)} do |match|
|
349
|
+
a, b = $1 + $2, $3.to_s << $4
|
350
|
+
|
351
|
+
case $3
|
352
|
+
when %r{^[[:alpha:]][[:alnum:]\+\.\-]*://} # already absolute
|
353
|
+
match
|
354
|
+
|
355
|
+
when /^#/
|
356
|
+
a << File.join(doc_url, b)
|
357
|
+
|
358
|
+
else
|
359
|
+
a << File.join(dir_url, b)
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
ann_html = nil
|
365
|
+
task :ann_html => [:doc_man_doc, :ann_nfo_doc, :ann_rel_doc] do
|
366
|
+
unless ann_html
|
367
|
+
ann_html = %{
|
368
|
+
<center>
|
369
|
+
<h1>#{project_module::DISPLAY}</h1>
|
370
|
+
<p>#{project_module::TAGLINE}</p>
|
371
|
+
<p>#{project_module::WEBSITE}</p>
|
372
|
+
</center>
|
373
|
+
#{ann_nfo_doc}
|
374
|
+
#{ann_rel_doc}
|
375
|
+
}
|
376
|
+
|
377
|
+
# remove heading navigation menus
|
378
|
+
ann_html.gsub! %r{<div class="nav"[^>]*>(.*?)</div>}, ''
|
379
|
+
|
380
|
+
# remove latex-style heading numbers
|
381
|
+
ann_html.gsub! %r"(<(h\d)[^>]*>).+?(?: ){2}(.+?)(</\2>)"m, '\1\3\4'
|
382
|
+
|
383
|
+
ann_html = resolve_html_links[ann_html]
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
ann_text = nil
|
388
|
+
task :ann_text => :ann_html do
|
389
|
+
unless ann_text
|
390
|
+
ann_text = convert_html_to_text[ann_html]
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
ann_nfo_text = nil
|
395
|
+
task :ann_nfo_text => :ann_nfo_doc do
|
396
|
+
unless ann_nfo_text
|
397
|
+
ann_nfo_html = resolve_html_links[ann_nfo_doc]
|
398
|
+
ann_nfo_text = convert_html_to_text[ann_nfo_html]
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# HTML
|
403
|
+
ann_html_dst = 'ANN.html'
|
404
|
+
|
405
|
+
desc "Build HTML announcement: #{ann_html_dst}"
|
406
|
+
task 'ann:html' => ann_html_dst
|
407
|
+
|
408
|
+
file ann_html_dst => doc_man_deps do
|
409
|
+
Rake::Task[:ann_html].invoke
|
410
|
+
File.write ann_html_dst, ann_html
|
411
|
+
end
|
412
|
+
|
413
|
+
CLEAN.include ann_html_dst
|
414
|
+
|
415
|
+
# RSS feed
|
416
|
+
ann_feed_dst = 'doc/ann.xml'
|
417
|
+
|
418
|
+
desc "Build RSS announcement: #{ann_feed_dst}"
|
419
|
+
task 'ann:feed' => ann_feed_dst
|
420
|
+
|
421
|
+
file ann_feed_dst => doc_man_deps do
|
422
|
+
require 'time'
|
423
|
+
require 'rss/maker'
|
424
|
+
|
425
|
+
feed = RSS::Maker.make('2.0') do |feed|
|
426
|
+
feed.channel.title = ann_project
|
427
|
+
feed.channel.link = project_module::WEBSITE
|
428
|
+
feed.channel.description = project_module::TAGLINE
|
429
|
+
|
430
|
+
Rake::Task[:ann_rel_doc].invoke
|
431
|
+
Rake::Task[:ann_html].invoke
|
432
|
+
|
433
|
+
item = feed.items.new_item
|
434
|
+
item.title = ann_rel_doc.title
|
435
|
+
item.link = project_module::DOCSITE + '#' + ann_rel_doc.here_frag
|
436
|
+
item.date = Time.parse(item.title)
|
437
|
+
item.description = ann_html
|
438
|
+
end
|
439
|
+
|
440
|
+
File.write ann_feed_dst, feed
|
441
|
+
end
|
442
|
+
|
443
|
+
CLOBBER.include ann_feed_dst
|
444
|
+
|
445
|
+
# plain text
|
446
|
+
ann_text_dst = 'ANN.txt'
|
447
|
+
|
448
|
+
desc "Build plain text announcement: #{ann_text_dst}"
|
449
|
+
task 'ann:text' => ann_text_dst
|
450
|
+
|
451
|
+
file ann_text_dst => doc_man_deps do
|
452
|
+
Rake::Task[:ann_text].invoke
|
453
|
+
File.write ann_text_dst, ann_text
|
454
|
+
end
|
455
|
+
|
456
|
+
CLEAN.include ann_text_dst
|
457
|
+
|
458
|
+
# e-mail
|
459
|
+
ann_mail_dst = 'ANN.eml'
|
460
|
+
|
461
|
+
desc "Build e-mail announcement: #{ann_mail_dst}"
|
462
|
+
task 'ann:mail' => ann_mail_dst
|
463
|
+
|
464
|
+
file ann_mail_dst => doc_man_deps do
|
465
|
+
File.open ann_mail_dst, 'w' do |f|
|
466
|
+
require 'time'
|
467
|
+
f.puts "Date: #{Time.now.rfc822}"
|
468
|
+
|
469
|
+
f.puts 'To: ruby-talk@ruby-lang.org'
|
470
|
+
f.puts 'From: "%s" <%s>' % project_module::AUTHORS.first
|
471
|
+
f.puts "Subject: #{ann_subject}"
|
472
|
+
|
473
|
+
Rake::Task[:ann_text].invoke
|
474
|
+
f.puts '', ann_text
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
CLEAN.include ann_mail_dst
|
479
|
+
|
480
|
+
# packaging
|
481
|
+
desc 'Build a release.'
|
482
|
+
task :pak => [:clobber, :doc] do
|
483
|
+
sh $0, 'package'
|
484
|
+
end
|
485
|
+
CLEAN.include 'pkg'
|
486
|
+
|
487
|
+
# ruby gem
|
488
|
+
require 'rake/gempackagetask'
|
489
|
+
|
490
|
+
gem = Gem::Specification.new do |gem|
|
491
|
+
authors = project_module::AUTHORS
|
492
|
+
|
493
|
+
if author = authors.first
|
494
|
+
gem.author, gem.email = author
|
495
|
+
end
|
496
|
+
|
497
|
+
if authors.length > 1
|
498
|
+
gem.authors = authors.map {|name, mail| name }
|
499
|
+
end
|
500
|
+
|
501
|
+
gem.rubyforge_project = options[:rubyforge_project]
|
502
|
+
|
503
|
+
# XXX: In theory, `gem.name` should be assigned to
|
504
|
+
# ::PROJECT instead of ::PROGRAM
|
505
|
+
#
|
506
|
+
# In practice, PROJECT may contain non-word
|
507
|
+
# characters and may also contain a mixture
|
508
|
+
# of lowercase and uppercase letters.
|
509
|
+
#
|
510
|
+
# This makes it difficult for people to
|
511
|
+
# install the project gem because they must
|
512
|
+
# remember the exact spelling used in
|
513
|
+
# `gem.name` when running `gem install ____`.
|
514
|
+
#
|
515
|
+
# For example, consider the "RedCloth" gem.
|
516
|
+
#
|
517
|
+
gem.name = project_module::PROGRAM
|
518
|
+
|
519
|
+
gem.version = project_module::VERSION
|
520
|
+
gem.summary = project_module::TAGLINE
|
521
|
+
gem.description = gem.summary
|
522
|
+
gem.homepage = project_module::WEBSITE
|
523
|
+
gem.files = FileList['**/*'].exclude('_darcs') - CLEAN
|
524
|
+
gem.executables = project_module::PROGRAM
|
525
|
+
gem.has_rdoc = true
|
526
|
+
|
527
|
+
unless project_module == Inochi
|
528
|
+
gem.add_dependency 'inochi', Inochi::VERSION.requirement
|
529
|
+
end
|
530
|
+
|
531
|
+
project_module::REQUIRE.each_pair do |gem_name, version_reqs|
|
532
|
+
gem.add_dependency gem_name, *version_reqs
|
533
|
+
end
|
534
|
+
|
535
|
+
# additional configuration is done by user
|
536
|
+
yield gem if gem_config
|
537
|
+
end
|
538
|
+
|
539
|
+
Rake::GemPackageTask.new(gem).define
|
540
|
+
|
541
|
+
# XXX: hide the tasks defined by the above gem packaging library
|
542
|
+
%w[gem package repackage clobber_package].each {|t| hide_rake_task[t] }
|
543
|
+
|
544
|
+
# releasing
|
545
|
+
desc 'Publish a release.'
|
546
|
+
task 'pub' => %w[ pub:pak pub:doc pub:ann ]
|
547
|
+
|
548
|
+
# connect to RubyForge services
|
549
|
+
pub_forge = nil
|
550
|
+
pub_forge_project = options[:rubyforge_project]
|
551
|
+
pub_forge_section = options[:rubyforge_section]
|
552
|
+
|
553
|
+
task :pub_forge do
|
554
|
+
require 'rubyforge'
|
555
|
+
pub_forge = RubyForge.new
|
556
|
+
pub_forge.configure('release_date' => project_module::RELEASE)
|
557
|
+
|
558
|
+
unless pub_forge.autoconfig['group_ids'].key? pub_forge_project
|
559
|
+
raise "The #{pub_forge_project.inspect} project was not recognized by the RubyForge client. Either specify a different RubyForge project by passing the :rubyforge_project option to Inochi.rake(), or ensure that the client is configured correctly (see `rubyforge --help` for help) and try again."
|
560
|
+
end
|
561
|
+
|
562
|
+
pub_forge.login
|
563
|
+
end
|
564
|
+
|
565
|
+
# documentation
|
566
|
+
desc 'Publish documentation to project website.'
|
567
|
+
task 'pub:doc' => [:doc, 'ann:feed'] do
|
568
|
+
target = options[:upload_target]
|
569
|
+
|
570
|
+
unless target
|
571
|
+
require 'addressable/uri'
|
572
|
+
docsite = Addressable::URI.parse(project_module::DOCSITE)
|
573
|
+
|
574
|
+
# provide uploading capability to websites hosted on RubyForge
|
575
|
+
if docsite.host.include? '.rubyforge.org'
|
576
|
+
target = "#{pub_forge.userconfig['username']}@rubyforge.org:#{File.join '/var/www/gforge-projects', options[:rubyforge_project], docsite.path}"
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
if target
|
581
|
+
cmd = ['rsync', '-auvz', 'doc/', "#{target}/"]
|
582
|
+
cmd.push '--delete' if options[:upload_delete]
|
583
|
+
cmd.concat options[:upload_options]
|
584
|
+
|
585
|
+
p cmd
|
586
|
+
sh(*cmd)
|
587
|
+
end
|
588
|
+
end
|
589
|
+
|
590
|
+
# announcement
|
591
|
+
desc 'Publish all announcements.'
|
592
|
+
task 'pub:ann' => %w[ pub:ann:forge pub:ann:raa pub:ann:talk ]
|
593
|
+
|
594
|
+
# login information
|
595
|
+
ann_logins_file = options[:logins_file]
|
596
|
+
ann_logins = nil
|
597
|
+
|
598
|
+
task :ann_logins do
|
599
|
+
ann_logins = begin
|
600
|
+
require 'yaml'
|
601
|
+
YAML.load_file ann_logins_file
|
602
|
+
rescue => e
|
603
|
+
warn "Could not read login information from #{ann_logins_file.inspect}:"
|
604
|
+
warn e
|
605
|
+
warn "** You will NOT be able to publish release announcements! **"
|
606
|
+
{}
|
607
|
+
end
|
608
|
+
end
|
609
|
+
|
610
|
+
desc 'Announce to RubyForge news.'
|
611
|
+
task 'pub:ann:forge' => :pub_forge do
|
612
|
+
puts 'Announcing to RubyForge news...'
|
613
|
+
|
614
|
+
project = options[:rubyforge_project]
|
615
|
+
|
616
|
+
if group_id = pub_forge.autoconfig['group_ids'][project]
|
617
|
+
# check if this release was already announced
|
618
|
+
require 'mechanize'
|
619
|
+
www = WWW::Mechanize.new
|
620
|
+
page = www.get "http://rubyforge.org/news/?group_id=#{group_id}"
|
621
|
+
|
622
|
+
posts = (page/'//a[starts-with(./@href, "/forum/forum.php?forum_id=")]/text()').map {|e| e.to_s.gsub("\302\240", '').strip }
|
623
|
+
|
624
|
+
already_announced = posts.include? ann_subject
|
625
|
+
|
626
|
+
if already_announced
|
627
|
+
warn 'This release was already announced to RubyForge news, so I will NOT announce it there again.'
|
628
|
+
else
|
629
|
+
# make the announcement
|
630
|
+
Rake::Task[:ann_text].invoke
|
631
|
+
pub_forge.post_news project, ann_subject, ann_text
|
632
|
+
|
633
|
+
puts 'Successfully announced to RubyForge news:', page.uri
|
634
|
+
end
|
635
|
+
else
|
636
|
+
raise "Could not determine the group_id of the #{project.inspect} RubyForge project. Run `rubyforge config` and try again."
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
desc 'Announce to ruby-talk mailing list.'
|
641
|
+
task 'pub:ann:talk' => :ann_logins do
|
642
|
+
puts 'Announcing to ruby-talk mailing list...'
|
643
|
+
|
644
|
+
host = 'http://ruby-forum.com'
|
645
|
+
ruby_talk = 4 # ruby-talk forum ID
|
646
|
+
|
647
|
+
require 'mechanize'
|
648
|
+
www = WWW::Mechanize.new
|
649
|
+
|
650
|
+
# check if this release was already announced
|
651
|
+
already_announced =
|
652
|
+
begin
|
653
|
+
page = www.get "#{host}/forum/#{ruby_talk}", :filter => %{"#{ann_subject}"}
|
654
|
+
|
655
|
+
posts = (page/'//div[@class="forum"]//a[starts-with(./@href, "/topic/")]/text()').map {|e| e.to_s.strip }
|
656
|
+
posts.include? ann_subject
|
657
|
+
rescue
|
658
|
+
false
|
659
|
+
end
|
660
|
+
|
661
|
+
if already_announced
|
662
|
+
warn 'This release was already announced to the ruby-talk mailing list, so I will NOT announce it there again.'
|
663
|
+
else
|
664
|
+
# log in to RubyForum
|
665
|
+
page = www.get "#{host}/user/login"
|
666
|
+
form = page.forms.first
|
667
|
+
|
668
|
+
if login = ann_logins['www.ruby-forum.com']
|
669
|
+
form['name'] = login['user']
|
670
|
+
form['password'] = login['pass']
|
671
|
+
end
|
672
|
+
|
673
|
+
page = form.click_button # use the first submit button
|
674
|
+
|
675
|
+
if (page/'//a[@href="/user/logout"]').empty?
|
676
|
+
warn "Could not log in to RubyForum using the login information in #{ann_logins_file.inspect}, so I can NOT announce this release to the ruby-talk mailing list."
|
677
|
+
else
|
678
|
+
# make the announcement
|
679
|
+
page = www.get "#{host}/topic/new?forum_id=#{ruby_talk}"
|
680
|
+
form = page.forms.first
|
681
|
+
|
682
|
+
Rake::Task[:ann_text].invoke
|
683
|
+
form['post[subject]'] = ann_subject
|
684
|
+
form['post[text]'] = ann_text
|
685
|
+
|
686
|
+
form.checkboxes.first.check # enable email notification
|
687
|
+
page = form.submit
|
688
|
+
|
689
|
+
errors = [page/'//div[@class="error"]/text()'].flatten
|
690
|
+
if errors.empty?
|
691
|
+
puts 'Successfully announced to ruby-talk mailing list:', page.uri
|
692
|
+
else
|
693
|
+
warn 'Could not announce to ruby-talk mailing list:'
|
694
|
+
warn errors.join("\n")
|
695
|
+
end
|
696
|
+
end
|
697
|
+
end
|
698
|
+
end
|
699
|
+
|
700
|
+
desc 'Announce to RAA (Ruby Application Archive).'
|
701
|
+
task 'pub:ann:raa' => :ann_logins do
|
702
|
+
puts 'Announcing to RAA (Ruby Application Archive)...'
|
703
|
+
|
704
|
+
show_page_error = lambda do |page, message|
|
705
|
+
warn "#{message}, so I can NOT announce this release to RAA:"
|
706
|
+
warn "#{(page/'h2').text} -- #{(page/'p').first.text.strip}"
|
707
|
+
end
|
708
|
+
|
709
|
+
resource = "#{options[:raa_project].inspect} project entry on RAA"
|
710
|
+
|
711
|
+
require 'mechanize'
|
712
|
+
www = WWW::Mechanize.new
|
713
|
+
page = www.get "http://raa.ruby-lang.org/update.rhtml?name=#{options[:raa_project]}"
|
714
|
+
|
715
|
+
if form = page.forms[1]
|
716
|
+
resource << " (owned by #{form.owner.inspect})"
|
717
|
+
|
718
|
+
Rake::Task[:ann_nfo_text].invoke
|
719
|
+
form['description'] = ann_nfo_text
|
720
|
+
form['description_style'] = 'Pre-formatted'
|
721
|
+
form['short_description'] = project_module::TAGLINE
|
722
|
+
form['version'] = project_module::VERSION
|
723
|
+
form['url'] = project_module::WEBSITE
|
724
|
+
form['pass'] = ann_logins['raa.ruby-lang.org']['pass']
|
725
|
+
|
726
|
+
page = form.submit
|
727
|
+
|
728
|
+
if page.title =~ /error/i
|
729
|
+
show_page_error[page, "Could not update #{resource}"]
|
730
|
+
else
|
731
|
+
puts 'Successfully announced to RAA (Ruby Application Archive).'
|
732
|
+
end
|
733
|
+
else
|
734
|
+
show_page_error[page, "Could not access #{resource}"]
|
735
|
+
end
|
736
|
+
end
|
737
|
+
|
738
|
+
# release packages
|
739
|
+
desc 'Publish release packages to RubyForge.'
|
740
|
+
task 'pub:pak' => :pub_forge do
|
741
|
+
# check if this release was already published
|
742
|
+
version = project_module::VERSION
|
743
|
+
packages = pub_forge.autoconfig['release_ids'][pub_forge_section]
|
744
|
+
|
745
|
+
if packages and packages.key? version
|
746
|
+
warn "The release packages were already published, so I will NOT publish them again."
|
747
|
+
else
|
748
|
+
# create the FRS package section
|
749
|
+
unless pub_forge.autoconfig['package_ids'].key? pub_forge_section
|
750
|
+
pub_forge.create_package pub_forge_project, pub_forge_section
|
751
|
+
end
|
752
|
+
|
753
|
+
# publish the package to the section
|
754
|
+
uploader = lambda do |command, *files|
|
755
|
+
pub_forge.__send__ command, pub_forge_project, pub_forge_section, version, *files
|
756
|
+
end
|
757
|
+
|
758
|
+
Rake::Task[:pak].invoke
|
759
|
+
packages = Dir['pkg/*.[a-z]*']
|
760
|
+
|
761
|
+
unless packages.empty?
|
762
|
+
# NOTE: use the 'add_release' command ONLY for the first
|
763
|
+
# file because it creates a new sub-section on the
|
764
|
+
# RubyForge download page; we do not want one package
|
765
|
+
# per sub-section on the RubyForge download page!
|
766
|
+
#
|
767
|
+
uploader[:add_release, packages.shift]
|
768
|
+
|
769
|
+
unless packages.empty?
|
770
|
+
uploader[:add_file, *packages]
|
771
|
+
end
|
772
|
+
|
773
|
+
puts "Successfully published release packages to RubyForge."
|
774
|
+
end
|
775
|
+
end
|
776
|
+
end
|
777
|
+
end
|