inochi 0.2.0 → 0.3.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.
@@ -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
@@ -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)[^>]*>).+?(?:&nbsp;){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