Module: Inochi

Defined in:
lib/inochi/book.rb,
lib/inochi.rb,
lib/inochi/init.rb

Overview

– Copyright protects this work. See LICENSE file for details. ++

Defined Under Namespace

Modules: Manual, Version Classes: Phrases

Class Method Summary collapse

Class Method Details

.book(project_symbol, book_template) ⇒ Object

Provides a common configuration for the project’s user manual:

  • Assigns the title, subtitle, date, and authors for the document.

    You may override these assignments by reassigning these document parameters AFTER this method is invoked.

    Refer to the “document parameters” for the XHTML format in the “erbook” user manual for details.

  • Provides the project’s configuration as global variables in the document.

    For example, <%= $version %> is the same as <%= project_module::VERSION %> in the document.

  • Defines a “project_summary” node for use in the document. The body of this node should contain a brief introduction to the project.

  • Defines a “project_history” node for use in the document. The body of this node should contain other nodes, each of which represent a single set of release notes for one of the project’s releases.

It is assumed that this method is called from within the Inochi.rake() environment.

Parameters

project_symbol

Name of the Ruby constant which serves as a namespace for the entire project.

book_template

The eRuby template which serves as the documentation for the project.



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/inochi/book.rb', line 42

def Inochi.book project_symbol, book_template
  project_module = fetch_project_module(project_symbol)

  # provide project constants as global variables to the user manual
  project_module::INOCHI.each_pair do |param, value|
    eval "$#{param} = value", binding
  end

  # set document parameters for the user manual
  $title    = project_module::DISPLAY
  $subtitle = project_module::TAGLINE
  $feeds    = { File.join(project_module::DOCSITE, 'ann.xml') => :rss }
  $authors  = Hash[
    *project_module::AUTHORS.map do |name, addr|
      # convert raw e-mail addresses into URLs for the erbook XHTML format
      addr = "mailto:#{addr}" unless addr =~ /^\w+:/

      [name, addr]
    end.flatten
  ]

  book_template.extend Manual
end

.calc_program_name(project_symbol) ⇒ Object

Returns the name of the main program executable, which is the same as the project name fully in lowercase.



11
12
13
# File 'lib/inochi/util.rb', line 11

def calc_program_name project_symbol
  camel_to_snake_case(project_symbol).downcase
end

.calc_project_symbol(project_name) ⇒ Object

Calculates the name of the project module from the given project name.



18
19
20
21
# File 'lib/inochi/util.rb', line 18

def calc_project_symbol project_name
  name = project_name.to_s.gsub(/\W+/, '_').squeeze('_').gsub(/^_|_$/, '')
  (name[0,1].upcase + name[1..-1]).to_sym
end

.camel_to_snake_case(input) ⇒ Object

Transforms the given input from CamelCase to snake_case.



26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/inochi/util.rb', line 26

def camel_to_snake_case input
  input = input.to_s.dup

  # handle camel case like FooBar => Foo_Bar
  while input.gsub!(/([a-z]+)([A-Z])(\w+)/) { $1 + '_' + $2 + $3 }
  end

  # handle abbreviations like XMLParser => XML_Parser
  while input.gsub!(/([A-Z]+)([A-Z])([a-z]+)/) { $1 + '_' + $2 + $3 }
  end

  input
end

.init(project_symbol, project_config = {}) ⇒ Object

Establishes your project in Ruby’s runtime environment by defining the project module (which serves as a namespace for all code in the project) and providing a common configuration for the project module:

  • Adds the project lib/ directory to the Ruby load path.

  • Defines the INOCHI constant in the project module. This constant contains the effective configuration parameters (@see project_config).

  • Defines all configuration parameters as constants in the project module.

This method must be invoked from immediately within (that is, not from within any of its descendant directories) the project lib/ directory. Ideally, this method would be invoked from the main project library.

Parameters

project_symbol

Name of the Ruby constant which serves as a namespace for the entire project.

project_config

Optional hash of project configuration parameters:

:project

Name of the project.

The default value is the value of the project_symbol parameter.

:tagline

An enticing, single line description of the project.

The default value is an empty string.

:website

URL of the published project website.

The default value is an empty string.

:docsite

URL of the published user manual.

The default value is the same value as the :website parameter.

:program

Name of the main project executable.

The default value is the value of the :project parameter in lowercase and CamelCase converted into snake_case.

:version

Version of the project.

The default value is “0.0.0”.

:release

Date when this version was released.

The default value is the current time.

:display

How the project name should be displayed.

The default value is the project name and version together.

:install

Path to the directory which contains the project.

The default value is one directory above the parent directory of the file from which this method was called.

:require

Hash containing the names and version constraints of RubyGems required to run this project. This information must be expressed as follows:

  • Each hash key must be the name of a ruby gem.

  • Each hash value must be either nil, a single version number requirement string (see Gem::Requirement) or an Array thereof.

The default value is an empty Hash.

:develop

Hash containing the names and version constraints of RubyGems required to build this project. This information must be expressed as follows:

  • Each hash key must be the name of a ruby gem.

  • Each hash value must be either nil, a single version number requirement string (see Gem::Requirement) or an Array thereof.

The default value is an empty Hash.

Returns

The newly configured project module.



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/inochi/init.rb', line 107

def Inochi.init project_symbol, project_config = {}
  project_module = fetch_project_module(project_symbol)

  # this method is not re-entrant
    @already_seen ||= []
    return project_module if @already_seen.include? project_module
    @already_seen << project_module

  # put project on Ruby load path
    project_file = first_caller_file
    project_libs = File.dirname(project_file)
    $LOAD_PATH << project_libs unless $LOAD_PATH.include? project_libs

  # supply configuration defaults
    project_config[:project] ||= project_symbol.to_s
    project_config[:tagline] ||= ''
    project_config[:version] ||= '0.0.0'
    project_config[:release] ||= Time.now.strftime('%F')
    project_config[:website] ||= ''
    project_config[:docsite] ||= project_config[:website]
    project_config[:display] ||= "#{project_config[:project]} #{project_config[:version]}"
    project_config[:program] ||= calc_program_name(project_symbol)
    project_config[:install] ||= File.dirname(project_libs)
    project_config[:require] ||= {}
    project_config[:develop] ||= {}

  # establish gem version dependencies and
  # sanitize the values while we're at it
    src = project_config[:require].dup
    dst = project_config[:require].clear

    src.each_pair do |gem_name, version_reqs|
      dst[gem_name] = require_gem_version(gem_name, version_reqs)
    end

  # make configuration parameters available as constants
    project_config[:inochi]  = project_config
    project_config[:phrases] = Phrases.new project_config[:install]
    project_config[:version].extend Version

    project_config.each_pair do |param, value|
      project_module.const_set param.to_s.upcase, value
    end

  project_module
end

.main(project_symbol, *trollop_args, &trollop_config) ⇒ Object

Provides a common configuration for the main project executable:

  • The program description (the sequence of non-blank lines at the top of the file in which this method is invoked) is properly formatted and displayed at the top of program’s help information.

  • The program version information is fetched from the project module and formatted in YAML fashion for easy consumption by other tools.

  • A list of command-line options is displayed at the bottom of the program’s help information.

It is assumed that this method is invoked from only within the main project executable (in the project bin/ directory).

Parameters

project_symbol

Name of the Ruby constant which serves as a namespace for the entire project.

trollop_args

Optional array of arguments for Trollop::options().

trollop_config

Optional block parameter passed to Trollop::options().

Returns the result of Trollop::options().



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/inochi/main.rb', line 36

def Inochi.main project_symbol, *trollop_args, &trollop_config
  program_file = first_caller_file
  program_name = File.basename(program_file)
  program_home = File.dirname(File.dirname(program_file))

  # load the project module
    require File.join(program_home, 'lib', program_name)
    project_module = fetch_project_module(project_symbol)

  # parse command-line options
    require 'trollop'

    options = Trollop.options(*trollop_args) do

      # show project description
      text "#{project_module::PROJECT} - #{project_module::TAGLINE}"
      text ''

      # show program description
      text File.read(program_file)[/\A.*?^$\n/m]. # grab the header
           gsub(/^# ?/, ''). # strip the comment markers
           sub(/\A!.*?\n/, '').lstrip # omit the hashbang line
      text ''

      instance_eval(&trollop_config) if trollop_config

      # show version information
      version %w[PROJECT VERSION RELEASE WEBSITE INSTALL].map {|c|
        "#{c.downcase}: #{project_module.const_get c}"
      }.join("\n")

      opt :manual, 'Show the user manual'
      opt :locale, 'Set preferred language', :type => :string
    end

    if options[:manual]
      require 'launchy'

      manual = File.join(project_module::INSTALL, 'doc', 'index.html')
      Launchy::Browser.run manual

      exit
    end

    if locale = options[:locale]
      project_module::PHRASES.locale = locale
    end

    options
end

.rake(project_symbol, options = {}, &gem_config) ⇒ Object

Provides Rake tasks for packaging, publishing, and announcing your project.

  • An AUTHORS constant (which has the form “[[name, info]]” where “name” is the name of a copyright holder and “info” is their contact information) is added to the project module.

    Unless this information is supplied via the :authors option, it is automatically extracted from copyright notices in the project license file, where the first copyright notice is expected to correspond to the primary project maintainer.

    Copyright notices must be in the following form:

    Copyright YEAR HOLDER <EMAIL>
    

    Where HOLDER is the name of the copyright holder, YEAR is the year when the copyright holder first began working on the project, and EMAIL is (optional) the email address of the copyright holder.

Parameters

project_symbol

Name of the Ruby constant which serves as a namespace for the entire project.

options

Optional hash of configuration parameters:

:test_with

Names of Ruby libraries inside the “inochi/test/” namespace to load before running the test suite.

The default value is an empty Array.

:authors

A list of project authors and their contact information. This list must have the form “[[name, info]]” where “name” is the name of a project author and “info” is their contact information.

The default value is automatically extracted from your project’s license file (see description above).

:license_file

Path (relative to the main project directory which contains the project rakefile) to the file which contains the project license.

The default value is “LICENSE”.

:logins_file

Path to the YAML file which contains login information for publishing release announcements.

The default value is “~/.config/inochi/logins.yaml” where “~” is the path to your home directory.

:rubyforge_project

Name of the RubyForge project where release packages will be published.

The default value is the value of the PROGRAM constant.

:rubyforge_section

Name of the RubyForge project’s File Release System section where release packages will be published.

The default value is the value of the :rubyforge_project parameter.

:raa_project

Name of the RAA (Ruby Application Archive) entry for this project.

The default value is the value of the PROGRAM constant.

:upload_target

Where to upload the project documentation. See “destination” in the rsync manual.

The default value is nil.

:upload_delete

Delete unknown files at the upload target location?

The default value is false.

:upload_options

Array of command-line arguments to the rsync command.

The default value is an empty array.

:inochi_consumer

Add Inochi as a runtime dependency to the created gem?

The default value is true.

:inochi_producer

Add Inochi as a development dependency to the created gem?

The default value is true.

gem_config

Optional block that is passed to Gem::specification.new() for additonal gem configuration.



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
# File 'lib/inochi/rake.rb', line 110

def Inochi.rake project_symbol, options = {}, &gem_config
  program_file = first_caller_file
  program_home = File.dirname(program_file)

  # load the project module
    program_name = File.basename(program_home)
    project_libs = File.join(program_home, 'lib', program_name)

    require project_libs
    project_module = fetch_project_module(project_symbol)

  # supply default options
    options[:test_with]         ||= []

    options[:rubyforge_project] ||= program_name
    options[:rubyforge_section] ||= program_name
    options[:raa_project]       ||= program_name

    options[:license_file]      ||= 'LICENSE'
    options[:logins_file]       ||= File.join(
                                      ENV['HOME'] || ENV['USERPROFILE'] || '.',
                                      '.config', 'inochi', 'logins.yaml'
                                    )

    options[:upload_options]    ||= []

    options[:inochi_consumer]   = true unless options.key? :inochi_consumer
    options[:inochi_producer]   = true unless options.key? :inochi_producer

  # add AUTHORS constant to the project module
    copyright_holders = options[:authors] ||
      File.read(options[:license_file]).
      scan(/Copyright.*?\d+\s+(.*)/).flatten.
      map {|s| (s =~ /\s*<(.*?)>/) ? [$`, $1] : [s, ''] }

    project_module.const_set :AUTHORS, copyright_holders

  # establish development gem dependencies
    [project_module, Inochi].uniq.each do |mod|
      mod::DEVELOP.each_pair do |gem_name, version_reqs|
        require_gem_version gem_name, version_reqs
      end
    end

  require 'rake/clean'

  hide_rake_task = lambda do |name|
    Rake::Task[name].instance_variable_set :@comment, nil
  end

  # translation
    directory 'lang'

    lang_dump_deps = 'lang'
    lang_dump_file = 'lang/phrases.yaml'

    desc 'Extract language phrases for translation.'
    task 'lang:dump' => lang_dump_deps do
      ENV['dump_lang_phrases'] = '1'
      Rake::Task[:test].invoke
    end

    lang_conv_delim = "\n" * 5

    desc 'Translate extracted language phrases (from=LANGUAGE_CODE).'
    task 'lang:conv' => lang_dump_file do |t|
      require 'babelfish'

      unless
        src_lang = ENV['from'] and
        BabelFish::LANGUAGE_CODES.include? src_lang
      then
        message = ['The "from" parameter must be specified as follows:']

        BabelFish::LANGUAGE_CODES.each do |c|
          n = BabelFish::LANGUAGE_NAMES[c]
          message << "  rake #{t.name} from=#{c}  # from #{n}"
        end

        raise ArgumentError, message.join("\n")
      end

      begin
        require 'yaml'
        phrases = YAML.load_file(lang_dump_file).keys.sort
      rescue
        warn "Could not load phrases from #{lang_dump_file.inspect}"
        raise
      end

      src_lang_name = BabelFish::LANGUAGE_NAMES[src_lang]

      BabelFish::LANGUAGE_PAIRS[src_lang].each do |dst_lang|
        dst_file      = "lang/#{dst_lang}.yaml"
        dst_lang_name = BabelFish::LANGUAGE_NAMES[dst_lang]

        puts "Translating phrases from #{src_lang_name} into #{dst_lang_name} as #{dst_file.inspect}"

        translations = BabelFish.translate(
          phrases.join(lang_conv_delim), src_lang, dst_lang
        ).split(lang_conv_delim)

        File.open(dst_file, 'w') do |f|
          f.puts "# #{dst_lang} (#{dst_lang_name})"

          phrases.zip(translations).each do |a, b|
            f.puts "#{a}: #{b}"
          end
        end
      end
    end

  # testing
    test_runner = lambda do |interpreter|
      require 'tempfile'
      script = Tempfile.new($$).path # will be deleted on program exit

      libs = [program_name] + # load the project-under-test's library FIRST!
        Array(options[:test_with]).map {|lib| "inochi/test/#{lib}" }

      File.write script, %{
        # the "-I." option lets us load helper libraries inside
        # the test suite via "test/PROJECT_NAME/LIBRARY_NAME"
        $LOAD_PATH.unshift '.', 'lib'

        #{libs.inspect}.each do |lib|
          require lib
        end

        # set title of test suite
        $0 = #{project_module.to_s.inspect}

        # dump language phrases *after* exercising all code (and
        # thereby populating the phrases cache) in the project
        at_exit do
          if ENV['dump_lang_phrases'] == '1'
            file = #{File.expand_path(lang_dump_file).inspect}
            list = eval(#{project_symbol.to_s.inspect})::PHRASES.phrases
            data = list.map {|s| s + ':' }.join("\n")

            File.write file, data

            puts "Extracted \#{list.length} language phrases into \#{file.inspect}"
          end
        end

        Dir['test/**/*.rb'].sort.each do |test|
          unit = test.sub('test/', 'lib/')

          if File.exist? unit
            # strip file extension because require()
            # does not normalize its input and it
            # will think that the two paths (with &
            # without file extension) are different
            unit_path = unit.sub(/\.rb$/, '').sub('lib/', '')
            test_path = test.sub(/\.rb$/, '')

            require unit_path
            require test_path
          else
            warn "Skipped test \#{test.inspect} because it lacks a corresponding \#{unit.inspect} unit."
          end
        end
      }

      command = [interpreter.to_s]

      if interpreter == :rcov
        command.push '--output', 'cov'

        # omit internals from coverage analysis
        command.push '--exclude-only', script
        command.push '--exclude', Inochi::INSTALL

        require 'rbconfig'
        ruby_internals = File.dirname(Config::CONFIG['rubylibdir'])
        command.push '--exclude', /^#{Regexp.quote ruby_internals}/.to_s

        # show results summary after execution
        command.push '-T'
      else
        # enable Ruby warnings during execution
        command << '-w'
      end

      command << script

      require 'shellwords'
      command.concat Shellwords.shellwords(ENV['opts'].to_s)

      sh(*command)
    end

    desc 'Run tests.'
    task :test do
      test_runner.call :ruby
    end

    desc 'Run tests with code coverage analysis.'
    task 'test:cov' do
      test_runner.call :rcov
    end

    CLEAN.include 'cov'

    desc 'Run tests with multiple Ruby versions.'
    task 'test:ruby' do
      test_runner.call :multiruby
    end

    desc 'Report code quality statistics.'
    task 'lint' do
      separator = '-' * 80

      linter = lambda do |*command|
        name = command.first

        puts "\n\n", separator, name, separator
        system(*command)
      end

      ruby_files = Dir['**/*.rb']

      linter.call 'sloccount', '.'
      linter.call 'flay' # operates on all .rb & .erb files by default
      linter.call 'reek', *ruby_files
      linter.call 'roodi', *ruby_files
    end

  # documentation
    desc 'Build all documentation.'
    task :doc => %w[ doc:api doc:man ]

    # user manual
      doc_man_src = 'doc/index.erb'
      doc_man_dst = 'doc/index.html'
      doc_man_deps = FileList['doc/*.erb']

      doc_man_doc = nil
      task :doc_man_doc => doc_man_src do
        unless doc_man_doc
          require 'erbook' unless defined? ERBook

          doc_man_txt = File.read(doc_man_src)
          doc_man_doc = ERBook::Document.new(:xhtml, doc_man_txt, doc_man_src, :unindent => true)
        end
      end

      desc 'Build the user manual.'
      task 'doc:man' => doc_man_dst

      file doc_man_dst => doc_man_deps do
        Rake::Task[:doc_man_doc].invoke
        File.write doc_man_dst, doc_man_doc
      end

      CLOBBER.include doc_man_dst

    # API reference
      doc_api_dst = 'doc/api'

      desc 'Build API reference.'
      task 'doc:api' => 'doc:api:rdoc'

      namespace :doc do
        namespace :api do
          require 'rake/rdoctask'

          Rake::RDocTask.new do |t|
            t.rdoc_dir = doc_api_dst
            t.template = 'direct' # lighter template used on railsapi.com
            t.options.push '--fmt', 'shtml' # explictly set SDoc generator
            t.rdoc_files.include '[A-Z]*', '{lib,ext}/**/*.{rb,c}'

            # regenerate API reference when sources change
            task t.name => t.rdoc_files

            t.main = options[:license_file]
            task t.name => t.main
          end

          %w[rdoc clobber_rdoc rerdoc].each do |inner|
            hide_rake_task["doc:api:#{inner}"]
          end
        end
      end

      CLOBBER.include doc_api_dst

  # announcements
    desc 'Build all release announcements.'
    task :ann => %w[ ann:feed ann:html ann:text ann:mail ]

    # it has long been a tradition to use an "[ANN]" prefix
    # when announcing things on the ruby-talk mailing list
    ann_prefix = '[ANN] '
    ann_subject = ann_prefix + project_module::DISPLAY
    ann_project = ann_prefix + project_module::PROJECT

    # fetch the project summary from user manual
      ann_nfo_doc = nil
      task :ann_nfo_doc => :doc_man_doc do
        ann_nfo_doc = $project_summary_node
      end

    # fetch release notes from user manual
      ann_rel_doc = nil
      task :ann_rel_doc => :doc_man_doc do
        unless ann_rel_doc
          if parent = $project_history_node
            if child = parent.children.first
              ann_rel_doc = child
            else
              raise 'The "project_history" node in the user manual lacks child nodes.'
            end
          else
            raise 'The user manual lacks a "project_history" node.'
          end
        end
      end

    # build release notes in HTML and plain text
      # converts the given HTML into plain text.  we do this using
      # lynx because (1) it outputs a list of all hyperlinks used
      # in the HTML document and (2) it runs on all major platforms
      convert_html_to_text = lambda do |html|
        require 'tempfile'

        begin
          # lynx's -dump option requires a .html file
          tmp_file = Tempfile.new(Inochi::PROGRAM).path + '.html'

          File.write tmp_file, html
          text = `lynx -dump #{tmp_file} -width 70`
        ensure
          File.delete tmp_file
        end

        # improve readability of list items
        # by adding a blank line between them
        text.gsub! %r{(\r?\n)( +\* \S)}, '\1\1\2'

        text
      end

      # binds relative addresses in the given HTML to the project docsite
      resolve_html_links = lambda do |html|
        # resolve relative URLs into absolute URLs
        # see http://en.wikipedia.org/wiki/URI_scheme#Generic_syntax
        require 'addressable/uri'
        uri = Addressable::URI.parse(project_module::DOCSITE)
        doc_url = uri.to_s
        dir_url = uri.path =~ %r{/$|^$} ? doc_url : File.dirname(doc_url)

        html.to_s.gsub %r{(href=|src=)(.)(.*?)(\2)} do |match|
          a, b = $1 + $2, $3.to_s << $4

          case $3
          when %r{^[[:alpha:]][[:alnum:]\+\.\-]*://} # already absolute
            match

          when /^#/
            a << File.join(doc_url, b)

          else
            a << File.join(dir_url, b)
          end
        end
      end

      ann_html = nil
      task :ann_html => [:doc_man_doc, :ann_nfo_doc, :ann_rel_doc] do
        unless ann_html
          ann_html = %{
            <center>
              <h1>#{project_module::DISPLAY}</h1>
              <p>#{project_module::TAGLINE}</p>
              <p>#{project_module::WEBSITE}</p>
            </center>
            #{ann_nfo_doc}
            #{ann_rel_doc.to_s.sub ann_rel_doc.parent_tabs_begin, '<div>'}
          }

          # remove heading navigation menus
          ann_html.gsub! %r{<div class="nav"[^>]*>(.*?)</div>}, ''

          # remove latex-style heading numbers
          ann_html.gsub! %r"(<(h\d)[^>]*>).+?(?:&nbsp;){2}(.+?)(</\2>)"m, '\1\3\4'

          ann_html = resolve_html_links[ann_html]
        end
      end

      ann_text = nil
      task :ann_text => :ann_html do
        ann_text ||= convert_html_to_text[ann_html]
      end

      ann_nfo_text = nil
      task :ann_nfo_text => :ann_nfo_doc do
        unless ann_nfo_text
          ann_nfo_html = resolve_html_links[ann_nfo_doc]
          ann_nfo_text = convert_html_to_text[ann_nfo_html]
        end
      end

    # HTML
      ann_html_dst = 'ANN.html'

      desc "Build HTML announcement: #{ann_html_dst}"
      task 'ann:html' => ann_html_dst

      file ann_html_dst => doc_man_deps do
        Rake::Task[:ann_html].invoke
        File.write ann_html_dst, ann_html
      end

      CLEAN.include ann_html_dst

    # RSS feed
      ann_feed_dst = 'doc/ann.xml'

      desc "Build RSS announcement: #{ann_feed_dst}"
      task 'ann:feed' => ann_feed_dst

      file ann_feed_dst => doc_man_deps do
        require 'time'
        require 'rss/maker'

        rss = RSS::Maker.make('2.0') do |feed|
          feed.channel.title       = ann_project
          feed.channel.link        = project_module::WEBSITE
          feed.channel.description = project_module::TAGLINE

          Rake::Task[:ann_rel_doc].invoke
          Rake::Task[:ann_html].invoke

          item             = feed.items.new_item
          item.title       = ann_rel_doc.title
          item.link        = project_module::DOCSITE + '#' + ann_rel_doc.here_frag
          item.date        = Time.parse(item.title)
          item.description = ann_html
        end

        File.write ann_feed_dst, rss
      end

      CLOBBER.include ann_feed_dst

    # plain text
      ann_text_dst = 'ANN.txt'

      desc "Build plain text announcement: #{ann_text_dst}"
      task 'ann:text' => ann_text_dst

      file ann_text_dst => doc_man_deps do
        Rake::Task[:ann_text].invoke
        File.write ann_text_dst, ann_text
      end

      CLEAN.include ann_text_dst

    # e-mail
      ann_mail_dst = 'ANN.eml'

      desc "Build e-mail announcement: #{ann_mail_dst}"
      task 'ann:mail' => ann_mail_dst

      file ann_mail_dst => doc_man_deps do
        File.open ann_mail_dst, 'w' do |f|
          require 'time'
          f.puts "Date: #{Time.now.rfc822}"

          f.puts 'To: [email protected]'
          f.puts 'From: "%s" <%s>' % project_module::AUTHORS.first
          f.puts "Subject: #{ann_subject}"

          Rake::Task[:ann_text].invoke
          f.puts '', ann_text
        end
      end

      CLEAN.include ann_mail_dst

  # packaging
    directory 'pkg'
    CLOBBER.include 'pkg'

    desc 'Build a release.'
    task :gem => [:doc, :ann_text, 'pkg'] do
      gem_spec = Gem::Specification.new do |gem|
        authors = project_module::AUTHORS

        if author = authors.first
          gem.author, gem.email = author
        end

        if authors.length > 1
          gem.authors = authors.map {|name, mail| name }
        end

        gem.rubyforge_project = options[:rubyforge_project]

        # XXX: In theory, `gem.name` should be assigned to
        #      ::PROJECT instead of ::PROGRAM
        #
        #      In practice, PROJECT may contain non-word
        #      characters and may also contain a mixture
        #      of lowercase and uppercase letters.
        #
        #      This makes it difficult for people to
        #      install the project gem because they must
        #      remember the exact spelling used in
        #      `gem.name` when running `gem install ____`.
        #
        #      For example, consider the "RedCloth" gem.
        #
        gem.name        = project_module::PROGRAM
        gem.date        = project_module::RELEASE
        gem.version     = project_module::VERSION
        gem.summary     = project_module::TAGLINE
        gem.description = ann_text
        gem.homepage    = project_module::WEBSITE
        gem.files       = FileList['**/*'].exclude('_darcs') - CLEAN
        gem.has_rdoc    = true

        executable      = project_module::PROGRAM
        executable_path = File.join(gem.bindir, executable)
        gem.executables = executable if File.exist? executable_path

        project_module::DEVELOP.each_pair do |gem_name, version_reqs|
          version_reqs = Array(version_reqs).compact
          gem.add_development_dependency gem_name, *version_reqs
        end

        project_module::REQUIRE.each_pair do |gem_name, version_reqs|
          version_reqs = Array(version_reqs).compact
          gem.add_dependency gem_name, *version_reqs
        end

        unless project_module == Inochi
          if options[:inochi_producer]
            gem.add_development_dependency Inochi::PROGRAM, Inochi::VERSION.requirement
          end

          if options[:inochi_consumer]
            gem.add_dependency Inochi::PROGRAM, Inochi::VERSION.requirement
          end
        end

        # additional configuration is done by user
        gem_config.call(gem) if gem_config
      end

      mv Gem::Builder.new(gem_spec).build, 'pkg'
    end

  # releasing
    desc 'Publish a release.'
    task 'pub' => %w[ pub:gem pub:doc pub:ann ]

    # connect to RubyForge services
      pub_forge = nil
      pub_forge_project = options[:rubyforge_project]
      pub_forge_section = options[:rubyforge_section]

      task :pub_forge do
        require 'rubyforge'
        pub_forge = RubyForge.new
        pub_forge.configure('release_date' => project_module::RELEASE)

        unless pub_forge.autoconfig['group_ids'].key? pub_forge_project
          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."
        end

        pub_forge.
      end

    # documentation
      desc 'Publish documentation to project website.'
      task 'pub:doc' => [:doc, 'ann:feed'] do
        target = options[:upload_target]

        unless target
          require 'addressable/uri'
          docsite = Addressable::URI.parse(project_module::DOCSITE)

          # provide uploading capability to websites hosted on RubyForge
          if docsite.host.include? '.rubyforge.org'
            target = "#{pub_forge.userconfig['username']}@rubyforge.org:#{File.join '/var/www/gforge-projects', options[:rubyforge_project], docsite.path}"
          end
        end

        if target
          cmd = ['rsync', '-auvz', 'doc/', "#{target}/"]
          cmd.push '--delete' if options[:upload_delete]
          cmd.concat options[:upload_options]

          p cmd
          sh(*cmd)
        end
      end

    # announcement
      desc 'Publish all announcements.'
      task 'pub:ann' => %w[ pub:ann:forge pub:ann:raa pub:ann:talk ]

      # login information
        ann_logins_file = options[:logins_file]
        ann_logins = nil

        task :ann_logins do
          ann_logins = begin
            require 'yaml'
            YAML.load_file ann_logins_file
          rescue => e
            warn "Could not read login information from #{ann_logins_file.inspect}:"
            warn e
            warn "** You will NOT be able to publish release announcements! **"
            {}
          end
        end

      desc 'Announce to RubyForge news.'
      task 'pub:ann:forge' => :pub_forge do
        puts '', 'Announcing to RubyForge news...'

        project = options[:rubyforge_project]

        if group_id = pub_forge.autoconfig['group_ids'][project]
          # check if this release was already announced
            require 'mechanize'
            www = WWW::Mechanize.new
            page = www.get "http://rubyforge.org/news/?group_id=#{group_id}"

            posts = (page/'//a[starts-with(./@href, "/forum/forum.php?forum_id=")]/text()').map {|e| e.to_s.strip }

            already_announced = posts.include? ann_subject

          if already_announced
            warn 'This release was already announced to RubyForge news, so I will NOT announce it there again.'
          else
            # make the announcement
            Rake::Task[:ann_text].invoke
            pub_forge.post_news project, ann_subject, ann_text

            puts 'Successfully announced to RubyForge news:', page.uri
          end
        else
          raise "Could not determine the group_id of the #{project.inspect} RubyForge project.  Run `rubyforge config` and try again."
        end
      end

      desc 'Announce to ruby-talk mailing list.'
      task 'pub:ann:talk' => :ann_logins do
        puts '', 'Announcing to ruby-talk mailing list...'

        host = 'http://ruby-forum.com'
        ruby_talk = 4 # ruby-talk forum ID

        require 'mechanize'
        www = WWW::Mechanize.new

        # check if this release was already announced
        already_announced =
          begin
            page = www.get "#{host}/forum/#{ruby_talk}", :filter => %{"#{ann_subject}"}

            posts = (page/'//div[@class="forum"]//a[starts-with(./@href, "/topic/")]/text()').map {|e| e.to_s.strip }
            posts.include? ann_subject
          rescue
            false
          end

        if already_announced
          warn 'This release was already announced to the ruby-talk mailing list, so I will NOT announce it there again.'
        else
          # log in to RubyForum
          page = www.get "#{host}/user/login"
          form = page.forms.first

          if  = ann_logins['www.ruby-forum.com']
            form['name'] = ['user']
            form['password'] = ['pass']
          end

          page = form.click_button # use the first submit button

          if (page/'//a[@href="/user/logout"]').empty?
            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."
          else
            # make the announcement
            page = www.get "#{host}/topic/new?forum_id=#{ruby_talk}"
            form = page.forms.first

            Rake::Task[:ann_text].invoke
            form['post[subject]'] = ann_subject
            form['post[text]'] = ann_text

            # enable email notification
            form.field_with(:name => 'post[subscribed_by_author]').value = '1'

            page = form.submit
            errors = Array(page/'//div[@class="error"]/text()')

            if errors.empty?
              puts 'Successfully announced to ruby-talk mailing list:', page.uri
            else
              warn 'Could not announce to ruby-talk mailing list:'
              warn errors.join("\n")
            end
          end
        end
      end

      desc 'Announce to RAA (Ruby Application Archive).'
      task 'pub:ann:raa' => :ann_logins do
        puts '', 'Announcing to RAA (Ruby Application Archive)...'

        show_page_error = lambda do |page, message|
          warn "#{message}, so I can NOT announce this release to RAA:"
          warn "#{(page/'h2').text} -- #{(page/'p').first.text.strip}"
        end

        resource = "#{options[:raa_project].inspect} project entry on RAA"

        require 'mechanize'
        www = WWW::Mechanize.new
        page = www.get "http://raa.ruby-lang.org/update.rhtml?name=#{options[:raa_project]}"

        if form = page.forms[1]
          resource << " (owned by #{form.owner.inspect})"

          Rake::Task[:ann_nfo_text].invoke
          form['description']       = ann_nfo_text
          form['description_style'] = 'Pre-formatted'
          form['short_description'] = project_module::TAGLINE
          form['version']           = project_module::VERSION
          form['url']               = project_module::WEBSITE
          form['pass']              = ann_logins['raa.ruby-lang.org']['pass']

          page = form.submit

          if page.title =~ /error/i
            show_page_error[page, "Could not update #{resource}"]
          else
            puts 'Successfully announced to RAA (Ruby Application Archive).'
          end
        else
          show_page_error[page, "Could not access #{resource}"]
        end
      end

    # release packages
      desc 'Publish release packages to RubyForge.'
      task 'pub:gem' => :pub_forge do
        # check if this release was already published
        version = project_module::VERSION
        packages = pub_forge.autoconfig['release_ids'][pub_forge_section]

        if packages and packages.key? version
          warn "The release packages were already published, so I will NOT publish them again."
        else
          # create the FRS package section
          unless pub_forge.autoconfig['package_ids'].key? pub_forge_section
            pub_forge.create_package pub_forge_project, pub_forge_section
          end

          # publish the package to the section
          uploader = lambda do |command, *files|
            pub_forge.__send__ command, pub_forge_project, pub_forge_section, version, *files
          end

          Rake::Task[:gem].invoke
          packages = Dir['pkg/*.[a-z]*']

          unless packages.empty?
            # NOTE: use the 'add_release' command ONLY for the first
            #       file because it creates a new sub-section on the
            #       RubyForge download page; we do not want one package
            #       per sub-section on the RubyForge download page!
            #
            uploader[:add_release, packages.shift]

            unless packages.empty?
              uploader[:add_file, *packages]
            end

            puts "Successfully published release packages to RubyForge."
          end
        end
      end
end

.require_gem_version(gem_name, version_reqs) ⇒ Object

Establishes version requirements for the given gem.

Returns the sanitized Gem version requirements.



45
46
47
48
49
50
51
52
53
54
55
# File 'lib/inochi/util.rb', line 45

def require_gem_version gem_name, version_reqs
  version_reqs = Array(version_reqs).compact

  begin
    gem gem_name.to_s, *version_reqs
  rescue Gem::Exception, Gem::LoadError => e
    warn e.inspect
  end

  version_reqs
end