inochi 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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