inochi 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +13 -0
- data/Rakefile +6 -0
- data/bin/inochi +413 -0
- data/doc/README +6 -0
- data/doc/api/Inochi.html +2337 -0
- data/doc/api/all-methods.html +92 -0
- data/doc/api/all-namespaces.html +22 -0
- data/doc/api/app.js +18 -0
- data/doc/api/index.html +18 -0
- data/doc/api/jquery.js +11 -0
- data/doc/api/readme.html +35 -0
- data/doc/api/style.css +68 -0
- data/doc/api/syntax_highlight.css +21 -0
- data/doc/history.erb +9 -0
- data/doc/index.erb +6 -0
- data/doc/index.xhtml +1830 -0
- data/doc/inochi.png +0 -0
- data/doc/inochi.svg +405 -0
- data/doc/intro.erb +103 -0
- data/doc/setup.erb +51 -0
- data/doc/theory.erb +3 -0
- data/doc/usage.erb +367 -0
- data/lib/inochi.rb +16 -0
- data/lib/inochi/inochi.rb +1000 -0
- metadata +150 -0
data/doc/setup.erb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
<% chapter "Setup" do %>
|
2
|
+
<% section "Requirements" do %>
|
3
|
+
Your system needs the following software to run **<%= $project %>**.
|
4
|
+
|
5
|
+
| Software | Description | Notes |
|
6
|
+
| -------- | ----------- | ----- |
|
7
|
+
| [Ruby](http://ruby-lang.org) | Ruby language interpreter | Version 1.8.6 or 1.8.7 is required. |
|
8
|
+
| [RubyGems](http://rubygems.org) | Ruby packaging system | Version 1.0.0 or newer is required. |
|
9
|
+
| [Lynx](http://lynx.isc.org) | Text-mode web browser | Version 2.8.6 or newer is required to convert HTML into plain text. |
|
10
|
+
<% end %>
|
11
|
+
|
12
|
+
<% section "Installation" do %>
|
13
|
+
You can install **<%= $project %>** by running this command:
|
14
|
+
|
15
|
+
gem install <%= $program %>
|
16
|
+
|
17
|
+
To check whether the installation was sucessful, run this command:
|
18
|
+
|
19
|
+
<%= $program %> --version
|
20
|
+
|
21
|
+
If the installation was successful, you will see output like this:
|
22
|
+
|
23
|
+
<pre><%= `ruby bin/#{$program} --version` %></pre>
|
24
|
+
|
25
|
+
If you do not see such output, you may
|
26
|
+
<%= xref "License", "ask the author(s)" %> for help.
|
27
|
+
<% end %>
|
28
|
+
|
29
|
+
<% section "Manifest" do %>
|
30
|
+
You will see the following items inside **<%= $project %>**'s installation
|
31
|
+
directory, whose path you can determine by running this command:
|
32
|
+
|
33
|
+
<%= $program %> --version
|
34
|
+
|
35
|
+
* <tt>bin/</tt>
|
36
|
+
|
37
|
+
* <tt><%= $program %></tt> --- the main **<%= $project %>** executable.
|
38
|
+
|
39
|
+
* <tt>lib/</tt>
|
40
|
+
|
41
|
+
* <tt><%= $program %>.rb</tt> --- the main **<%= $project %>** library.
|
42
|
+
|
43
|
+
* <tt>doc/</tt>
|
44
|
+
|
45
|
+
* <tt>api/</tt> --- API reference documentation.
|
46
|
+
|
47
|
+
* <tt>index.erb</tt> --- source of this user manual.
|
48
|
+
|
49
|
+
* <tt>LICENSE</tt> --- copyright notice and legal conditions.
|
50
|
+
<% end %>
|
51
|
+
<% end %>
|
data/doc/theory.erb
ADDED
data/doc/usage.erb
ADDED
@@ -0,0 +1,367 @@
|
|
1
|
+
<% part "Usage" do %>
|
2
|
+
<% section "Command-line interface" do %>
|
3
|
+
When you run this command:
|
4
|
+
|
5
|
+
<%= $program %> --help
|
6
|
+
|
7
|
+
You will see this output:
|
8
|
+
|
9
|
+
<pre><%= verbatim `ruby bin/#{$program} --help` %></pre>
|
10
|
+
|
11
|
+
<% tip "Merging files with **kdiff3**" do %>
|
12
|
+
Instead of merging files by hand, you can transfer wanted changes between files semi-automatically using [kdiff3](http://kdiff3.sourceforge.net). Simply follow these instructions:
|
13
|
+
|
14
|
+
1. Create a file named <tt>merge2</tt> with the following content:
|
15
|
+
|
16
|
+
#!/bin/sh
|
17
|
+
|
18
|
+
old_file=$1
|
19
|
+
shift
|
20
|
+
|
21
|
+
new_file=$1
|
22
|
+
shift
|
23
|
+
|
24
|
+
output_file=$1
|
25
|
+
shift
|
26
|
+
|
27
|
+
kdiff3 --auto "$old_file" "$new_file" --output "$output_file"
|
28
|
+
|
29
|
+
2. Make the file executable:
|
30
|
+
|
31
|
+
chmod +x merge2
|
32
|
+
|
33
|
+
3. Place the file in a directory that is listed in your `PATH` environment variable.
|
34
|
+
|
35
|
+
4. Run **<%= $project %>** like this:
|
36
|
+
|
37
|
+
<%= $program %> -m merge2
|
38
|
+
|
39
|
+
Now **kdiff3** will be invoked to help you transfer your changes. When you are finished transferring changes, save the file and quit **kdiff3**. If you do not want to transfer any changes, simply quit **kdiff3** _without_ saving the file.
|
40
|
+
<% end %>
|
41
|
+
<% end %>
|
42
|
+
|
43
|
+
<% section "Ruby library interface" do %>
|
44
|
+
The [`Inochi` module](api/Inochi.html) has several class methods which provide a common configuration for various aspects of your project. These aspects, and their interactions with the `Inochi` module, are as follows:
|
45
|
+
* Your project's main library invokes the [`Inochi.init()` method](api/Inochi.html#init-class_method).
|
46
|
+
* Your project's main executable invokes the [`Inochi.main()` method](api/Inochi.html#main-class_method).
|
47
|
+
* Your project's <tt>Rakefile</tt> invokes the [`Inochi.rake()` method](api/Inochi.html#rake-class_method).
|
48
|
+
* Your project's user manual invokes the [`Inochi.book()` method](api/Inochi.html#book-class_method).
|
49
|
+
<% end %>
|
50
|
+
|
51
|
+
<% section "Tutorial" do %>
|
52
|
+
This tutorial shows how **<%= $project %>** is used to manage a hypothetical `WordCount` project throughout the various stages of its life.
|
53
|
+
|
54
|
+
<% section "Have a brilliant idea" do %>
|
55
|
+
It is 4am on Sunday morning. Unwilling to sleep, you have spent the past few hours programming obsessively.. Though your eyes grow heavy and your stomach churns from hunger, your mind charges forth with haste.
|
56
|
+
|
57
|
+
> Push on! Keep on!
|
58
|
+
|
59
|
+
Until at last, pushed far beyond its limit, your body overpowers your will and drags you into black unconsciousness.
|
60
|
+
|
61
|
+
*BEEP* *BEEP* *B*---
|
62
|
+
|
63
|
+
Half-asleep and violent from the sudden disturbance, you silence the bleeting alarm clock with vengeance. It is 2pm on Sunday afternoon.
|
64
|
+
|
65
|
+
Red beams of sunlight slip through the gaps in your curtains. It is a beautiful day, outside. *Outside*--- you think,
|
66
|
+
|
67
|
+
> What am I doing to myself?
|
68
|
+
>
|
69
|
+
> I've got to get *outside*.
|
70
|
+
>
|
71
|
+
> I've got to get *away*...
|
72
|
+
>
|
73
|
+
> Away from this computer... this... mental prison in which I toil night after night, like a souless machine.
|
74
|
+
|
75
|
+
Venturing into the courtyard outside your quarters, you find peace. A warm breeze graces you, sweeping your hair gently as a mother would. The bright sunlight penetrates your mind's eye as your thoughts fade...
|
76
|
+
|
77
|
+
Thoughts of tests to write, units to refactor, bugs to fix, options to document. They melt and mix and flow into nothingness.
|
78
|
+
|
79
|
+
All is clear. No thoughts. No more.
|
80
|
+
|
81
|
+
> No!
|
82
|
+
|
83
|
+
You collapse heavily onto the grassy earth beneath you. Breathing deeply, you sink into yourself and whisper
|
84
|
+
|
85
|
+
> It's okay.
|
86
|
+
>
|
87
|
+
> Just, let go.
|
88
|
+
|
89
|
+
and fall asleep.
|
90
|
+
|
91
|
+
You awaken that evening relaxed and refreshed. A brilliant idea for a new project enters your mind: the project will be a tool that counts the number of words in text file. And, the project can be accessed from Ruby via the `WordCount` module.
|
92
|
+
|
93
|
+
*However*, you must go to work the next morning, so there isn't much time. What can you do? Let's see how **<%= $project %>** can help us meet this challenge.
|
94
|
+
<% end %>
|
95
|
+
|
96
|
+
<%
|
97
|
+
require 'tempfile'
|
98
|
+
tmp = Tempfile.new($project).path
|
99
|
+
File.delete tmp
|
100
|
+
mkdir_p tmp
|
101
|
+
|
102
|
+
begin
|
103
|
+
old = Dir.pwd
|
104
|
+
cd tmp
|
105
|
+
|
106
|
+
main_executable = 'bin/word_count'
|
107
|
+
%>
|
108
|
+
<% section "Generate your project" do %>
|
109
|
+
Give life to your new project:
|
110
|
+
|
111
|
+
<pre>
|
112
|
+
# inochi WordCount
|
113
|
+
<%= verbatim `ruby #{$install}/bin/inochi WordCount` %>
|
114
|
+
</pre>
|
115
|
+
|
116
|
+
Enter the <tt>word_count</tt> directory:
|
117
|
+
|
118
|
+
<pre>
|
119
|
+
# cd word_count
|
120
|
+
<% cd "word_count" %>
|
121
|
+
</pre>
|
122
|
+
|
123
|
+
View the available Rake tasks:
|
124
|
+
|
125
|
+
<pre>
|
126
|
+
# rake -T
|
127
|
+
<%= verbatim `rake -T` %>
|
128
|
+
</pre>
|
129
|
+
|
130
|
+
Try the main project executable:
|
131
|
+
|
132
|
+
<pre>
|
133
|
+
<% command = main_executable %>
|
134
|
+
# ruby <%= command %>
|
135
|
+
<%= verbatim `ruby #{command}` %>
|
136
|
+
</pre>
|
137
|
+
|
138
|
+
See usage information:
|
139
|
+
|
140
|
+
<pre>
|
141
|
+
<% command = "#{main_executable} --help" %>
|
142
|
+
# ruby <%= command %>
|
143
|
+
<%= verbatim `ruby #{command}` %>
|
144
|
+
</pre>
|
145
|
+
|
146
|
+
See project & version information:
|
147
|
+
|
148
|
+
<pre>
|
149
|
+
<% command = "#{main_executable} --version" %>
|
150
|
+
# ruby <%= command %>
|
151
|
+
<%= verbatim `ruby #{command}` %>
|
152
|
+
</pre>
|
153
|
+
|
154
|
+
See the user manual:
|
155
|
+
|
156
|
+
<pre>
|
157
|
+
# rake doc:man 2>/dev/null
|
158
|
+
|
159
|
+
<% command = "#{main_executable} --manual" %>
|
160
|
+
# ruby <%= command %>
|
161
|
+
</pre>
|
162
|
+
|
163
|
+
The manual will now appear in your default web browser.
|
164
|
+
<% end %>
|
165
|
+
|
166
|
+
<% section "Configure your project" do %>
|
167
|
+
<%= xref "Ruby library interface" %> lists and documents the interactions between your project and **<%= $project %>**. These points of interaction are illustrated in the following sections.
|
168
|
+
|
169
|
+
<% section "Project information" do %>
|
170
|
+
<% license_file = 'LICENSE' %>
|
171
|
+
|
172
|
+
Open the <tt><%= license_file %></tt> file, which contains the open source [ISC license](http://www.opensource.org/licenses/isc-license.txt) by default, and add a copyright notice with your name and (optional) email address:
|
173
|
+
|
174
|
+
<pre>
|
175
|
+
<%= verbatim File.read(license_file) %>
|
176
|
+
</pre>
|
177
|
+
|
178
|
+
<% main_library = 'lib/word_count.rb' %>
|
179
|
+
|
180
|
+
Open the main project library file <tt><%= main_library %></tt> and fill in the blanks:
|
181
|
+
|
182
|
+
<code>
|
183
|
+
<%= verbatim File.read(main_library) %>
|
184
|
+
</code>
|
185
|
+
<% end %>
|
186
|
+
|
187
|
+
<% section "Project executable" do %>
|
188
|
+
Open the <tt><%= main_executable %></tt> file and fill in the blanks:
|
189
|
+
|
190
|
+
<code>
|
191
|
+
<%= verbatim File.read(main_executable) %>
|
192
|
+
</code>
|
193
|
+
<% end %>
|
194
|
+
|
195
|
+
<% section "Rake tasks" do %>
|
196
|
+
<% rake_file = 'Rakefile' %>
|
197
|
+
|
198
|
+
Open the <tt><%= rake_file %></tt> and fill in the blanks:
|
199
|
+
|
200
|
+
<code>
|
201
|
+
<%= verbatim File.read(rake_file) %>
|
202
|
+
</code>
|
203
|
+
<% end %>
|
204
|
+
|
205
|
+
<% section "User manual" do %>
|
206
|
+
<%
|
207
|
+
whole = 'doc/index.erb'
|
208
|
+
parts = File.read(whole).
|
209
|
+
scan(/<%#\s*include\s+(\S+)/).flatten.map {|s| "doc/#{s}" }
|
210
|
+
|
211
|
+
files = [whole, *parts]
|
212
|
+
%>
|
213
|
+
|
214
|
+
The user manual's source file <tt><%= whole %></tt> subdivides its content into several smaller files, according to topic, for easier editing and maintenance. These files are processed by the [<%= ERBook::PROJECT %>](<%= ERBook::WEBSITE %>) program's [XHTML format](<%= ERBook::DOCSITE %>#xhtml) to produce the <tt>doc/index.xhtml</tt> file.
|
215
|
+
|
216
|
+
Open these source files and fill in the blanks:
|
217
|
+
|
218
|
+
<% files.each do |f| %>
|
219
|
+
<% paragraph "<tt>#{f}</tt>" do %>
|
220
|
+
<code lang="rhtml"><%= verbatim File.read(f) %></code>
|
221
|
+
<% end %>
|
222
|
+
<% end %>
|
223
|
+
<% end %>
|
224
|
+
<% end %>
|
225
|
+
|
226
|
+
<% section "Implement your project" do %>
|
227
|
+
Add the following code to the bottom of the main project library:
|
228
|
+
|
229
|
+
<code>
|
230
|
+
module WordCount
|
231
|
+
# Returns the number of words in the given input.
|
232
|
+
def WordCount.count input
|
233
|
+
input.to_s.split(/\W+/).length
|
234
|
+
end
|
235
|
+
end
|
236
|
+
</code>
|
237
|
+
|
238
|
+
Add the following code to the bottom of the main project executable:
|
239
|
+
|
240
|
+
<code>
|
241
|
+
input = ARGF.read
|
242
|
+
total = WordCount.count(input)
|
243
|
+
puts "There are #{total} words in the input."
|
244
|
+
</code>
|
245
|
+
|
246
|
+
<% paragraph "Goodbye `$LOAD_PATH`, hello `require()`" do %>
|
247
|
+
Notice that, in the Ruby files that you modified so far, there were no `$LOAD_PATH` manipulations and no explicit `require()` statements to pull in the various parts of your project. That is because **<%= $project %>** does this for you automatically.
|
248
|
+
|
249
|
+
Furthermore, you can always `require()` a sub-library anywhere in your project using its canonical path because **<%= $project %>** puts your main project libraries on the Ruby load path.
|
250
|
+
|
251
|
+
<% sub_library = 'word_count/odf/text' %>
|
252
|
+
|
253
|
+
For example, if your project has a sub-library, say, <tt>lib/<%= sub_library %>.rb</tt> that counts the number of words in an [OpenDocument Text](http://en.wikipedia.org/wiki/OpenDocument) document, then it would be loaded into the main project executable like this:
|
254
|
+
|
255
|
+
<code>
|
256
|
+
require '<%= sub_library %>'
|
257
|
+
</code>
|
258
|
+
|
259
|
+
Regardless of whether a sub-library is used within your project itself or from within an external application, we always `require()` the sub-library using the same canonical path.
|
260
|
+
<% end %>
|
261
|
+
<% end %>
|
262
|
+
|
263
|
+
<% section "Test your project" do %>
|
264
|
+
> TODO: show how to write a unit test for the code
|
265
|
+
|
266
|
+
> TODO: integrate minitest tasks into Inochi.rake()
|
267
|
+
<% end %>
|
268
|
+
|
269
|
+
<% section "Publish your project" do %>
|
270
|
+
This command performs all of the automated steps described in the following sections:
|
271
|
+
|
272
|
+
<pre>
|
273
|
+
# rake pub
|
274
|
+
</pre>
|
275
|
+
|
276
|
+
<% section "Build a RubyGem" do %>
|
277
|
+
Build a RubyGem by running:
|
278
|
+
|
279
|
+
<pre>
|
280
|
+
# rake pak
|
281
|
+
<%= `rake pak` %>
|
282
|
+
</pre>
|
283
|
+
|
284
|
+
See the RubyGem contents:
|
285
|
+
|
286
|
+
<pre>
|
287
|
+
# gem spec pkg/*.gem
|
288
|
+
<code lang="yaml"><%= `gem spec pkg/*.gem`.rstrip %></code>
|
289
|
+
</pre>
|
290
|
+
<% end %>
|
291
|
+
|
292
|
+
<% section "Publish a RubyGem" do %>
|
293
|
+
You must first register your project on [RubyForge](http://rubyforge.org) before you can publish a RubyGem. If your RubyForge project name is different from your actual project name, then you should pass the `:rubyforge_project` and `:rubyforge_section` options to the [`Inochi.rake()` method](api/Inochi.html#rake-class_method)).
|
294
|
+
|
295
|
+
Publish a RubyGem by running:
|
296
|
+
|
297
|
+
<pre>
|
298
|
+
# rake pub:pak
|
299
|
+
</pre>
|
300
|
+
<% end %>
|
301
|
+
|
302
|
+
<% section "Announce a release" do %>
|
303
|
+
You must first provide your <%= xref "Login information" %> to **<%= $project %>**. If you do not want to do this, then see <%= xref "Manual release announcement" %>.
|
304
|
+
|
305
|
+
Announce a release by running:
|
306
|
+
|
307
|
+
<pre>
|
308
|
+
# rake pub:ann
|
309
|
+
</pre>
|
310
|
+
|
311
|
+
<% paragraph "Login information" do %>
|
312
|
+
In order to automate the announcement of releases, **<%= $project %>** needs to know your login information for the [RAA (Ruby Application Archive)](http://raa.ruby-lang.org) and [RubyForum](http://www.ruby-forum.com/forum/4), which serves as a gateway to the [ruby-talk mailing list](http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/).
|
313
|
+
|
314
|
+
<% logins_file = "~/.config/inochi/logins.yaml" %>
|
315
|
+
|
316
|
+
This information is expected to be stored in a <tt><%= logins_file %></tt> file (this location can be overridden by passing the `:logins_file` option to the [`Inochi.rake()` method](api/Inochi.html#rake-class_method)), where <tt>~</tt> denotes the path to your home directory. This file is a [YAML](http://www.yaml.org) document containing the following parameters:
|
317
|
+
|
318
|
+
<code lang="yaml">
|
319
|
+
www.ruby-forum.com:
|
320
|
+
user: YOUR_USERNAME_HERE
|
321
|
+
pass: YOUR_PASSWORD_HERE
|
322
|
+
|
323
|
+
raa.ruby-lang.org:
|
324
|
+
pass: YOUR_PASSWORD_HERE
|
325
|
+
</code>
|
326
|
+
|
327
|
+
For better security, you should ensure that this file is only readable and writable by you and is not accessible by anyone else. In a UNIX environment, this can be accomplished by running the following command:
|
328
|
+
|
329
|
+
# chmod 0600 <%= logins_file %>
|
330
|
+
|
331
|
+
<% end %>
|
332
|
+
|
333
|
+
<% section "Manual release announcement" do %>
|
334
|
+
Build release announcements by running:
|
335
|
+
|
336
|
+
<pre>
|
337
|
+
# rake ann
|
338
|
+
<%= `rake ann` %>
|
339
|
+
</pre>
|
340
|
+
|
341
|
+
This produces the following files in your project directory:
|
342
|
+
<% Dir['ANN*'].each do |f| %>
|
343
|
+
* <tt><%= f %></tt>
|
344
|
+
<% end %>
|
345
|
+
|
346
|
+
Now you can manually announce your release using these files.
|
347
|
+
<% end %>
|
348
|
+
<% end %>
|
349
|
+
|
350
|
+
<% section "Publish the documentation" do %>
|
351
|
+
Publish the user manual and API documentation by running:
|
352
|
+
|
353
|
+
<pre>
|
354
|
+
# rake pub:doc
|
355
|
+
</pre>
|
356
|
+
|
357
|
+
If your documentation website (see the `:docsite` option for the [`Inochi.init()` method](api/Inochi.html#init-class_method)) is hosted on RubyForge, then the above command will automatically upload your project's documentation to the correct place.
|
358
|
+
<% end %>
|
359
|
+
<% end %>
|
360
|
+
<%
|
361
|
+
ensure
|
362
|
+
cd old
|
363
|
+
rm_rf tmp
|
364
|
+
end
|
365
|
+
%>
|
366
|
+
<% end %>
|
367
|
+
<% end %>
|
data/lib/inochi.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'inochi', 'inochi')
|
2
|
+
|
3
|
+
Inochi.init :Inochi,
|
4
|
+
:version => '0.0.0',
|
5
|
+
:release => '2009-01-19',
|
6
|
+
:tagline => 'Gives life to RubyGems-based software',
|
7
|
+
:website => 'http://snk.tuxfamily.org/lib/inochi',
|
8
|
+
:require => {
|
9
|
+
'rubyforge' => '~> 1', # for publishing gems to RubyForge
|
10
|
+
'mechanize' => '~> 0', # for automating web browsing
|
11
|
+
'trollop' => '~> 1', # for parsing command-line options
|
12
|
+
'erbook' => '~> 6', # for processing the user manual
|
13
|
+
'launchy' => '~> 0', # for launching a web browser
|
14
|
+
'yard' => nil, # for generating API documentation
|
15
|
+
'addressable' => '~> 2', # for parsing URIs properly
|
16
|
+
}
|
@@ -0,0 +1,1000 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
module Inochi
|
4
|
+
class << self
|
5
|
+
##
|
6
|
+
# Establishes your project in Ruby's runtime environment by defining
|
7
|
+
# the project module (which serves as a namespace for all code in the
|
8
|
+
# project) and providing a common configuration for the project module:
|
9
|
+
#
|
10
|
+
# * Adds the project lib/ directory to the Ruby load path.
|
11
|
+
#
|
12
|
+
# * Defines the INOCHI constant in the project module. This constant
|
13
|
+
# contains the effective configuration parameters (@see project_config).
|
14
|
+
#
|
15
|
+
# * Defines all configuration parameters as constants in the project module.
|
16
|
+
#
|
17
|
+
# This method must be invoked from immediately within (that is, not from
|
18
|
+
# within any of its descendant directories) the project lib/ directory.
|
19
|
+
# Ideally, this method would be invoked from the main project library.
|
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] project_config
|
26
|
+
# Project configuration parameters:
|
27
|
+
#
|
28
|
+
# [String] :project =>
|
29
|
+
# Name of the project.
|
30
|
+
#
|
31
|
+
# The default value is the value of the project_symbol parameter.
|
32
|
+
#
|
33
|
+
# [String] :tagline =>
|
34
|
+
# An enticing, single line description of the project.
|
35
|
+
#
|
36
|
+
# The default value is an empty string.
|
37
|
+
#
|
38
|
+
# [String] :website =>
|
39
|
+
# URL of the published project website.
|
40
|
+
#
|
41
|
+
# The default value is an empty string.
|
42
|
+
#
|
43
|
+
# [String] :docsite =>
|
44
|
+
# URL of the published user manual.
|
45
|
+
#
|
46
|
+
# The default value is the same value as the :website parameter.
|
47
|
+
#
|
48
|
+
# [String] :program =>
|
49
|
+
# Name of the main project executable.
|
50
|
+
#
|
51
|
+
# The default value is the value of the :project parameter
|
52
|
+
# in lowercase and CamelCase converted into snake_case.
|
53
|
+
#
|
54
|
+
# [String] :version =>
|
55
|
+
# Version of the project.
|
56
|
+
#
|
57
|
+
# The default value is "0.0.0".
|
58
|
+
#
|
59
|
+
# [String] :release =>
|
60
|
+
# Date when this version was released.
|
61
|
+
#
|
62
|
+
# The default value is the current time.
|
63
|
+
#
|
64
|
+
# [String] :display =>
|
65
|
+
# How the project name should be displayed.
|
66
|
+
#
|
67
|
+
# The default value is the project name and version together.
|
68
|
+
#
|
69
|
+
# [String] :install =>
|
70
|
+
# Path to the directory which contains the project.
|
71
|
+
#
|
72
|
+
# The default value is one directory above the parent
|
73
|
+
# directory of the file from which this method was called.
|
74
|
+
#
|
75
|
+
# [Hash] :require =>
|
76
|
+
# The names and version constraints of ruby gems required by
|
77
|
+
# this project. This information must be expressed as follows:
|
78
|
+
#
|
79
|
+
# * Each hash key must be the name of a ruby gem.
|
80
|
+
#
|
81
|
+
# * Each hash value must be either +nil+, a single version number
|
82
|
+
# requirement string (see Gem::Requirement) or an Array thereof.
|
83
|
+
#
|
84
|
+
# The default value is an empty Hash.
|
85
|
+
#
|
86
|
+
# @return [Module] The newly configured project module.
|
87
|
+
#
|
88
|
+
def init project_symbol, project_config = {}
|
89
|
+
project_module = fetch_project_module(project_symbol)
|
90
|
+
|
91
|
+
# this method is not re-entrant
|
92
|
+
@already_seen ||= []
|
93
|
+
return project_module if @already_seen.include? project_module
|
94
|
+
@already_seen << project_module
|
95
|
+
|
96
|
+
# put project on Ruby load path
|
97
|
+
project_file = File.expand_path(first_caller_file)
|
98
|
+
project_libs = File.dirname(project_file)
|
99
|
+
$LOAD_PATH.unshift project_libs
|
100
|
+
|
101
|
+
# supply configuration defaults
|
102
|
+
project_config[:project] ||= project_symbol.to_s
|
103
|
+
project_config[:tagline] ||= ''
|
104
|
+
project_config[:version] ||= '0.0.0'
|
105
|
+
project_config[:release] ||= Time.now.strftime('%F')
|
106
|
+
project_config[:website] ||= ''
|
107
|
+
project_config[:docsite] ||= project_config[:website]
|
108
|
+
project_config[:display] ||= "#{project_config[:project]} #{project_config[:version]}"
|
109
|
+
project_config[:program] ||= calc_program_name(project_symbol)
|
110
|
+
project_config[:install] ||= File.dirname(project_libs)
|
111
|
+
project_config[:require] ||= {}
|
112
|
+
|
113
|
+
# establish gem version dependencies and
|
114
|
+
# sanitize the values while we're at it
|
115
|
+
src = project_config[:require].dup
|
116
|
+
dst = project_config[:require].clear
|
117
|
+
|
118
|
+
src.each_pair do |gem_name, version_reqs|
|
119
|
+
gem_name = gem_name.to_s
|
120
|
+
version_reqs = [version_reqs].flatten.compact
|
121
|
+
|
122
|
+
dst[gem_name] = version_reqs
|
123
|
+
gem gem_name, *version_reqs
|
124
|
+
end
|
125
|
+
|
126
|
+
# make configuration parameters available as constants
|
127
|
+
project_config[:inochi] = project_config
|
128
|
+
|
129
|
+
project_config.each_pair do |param, value|
|
130
|
+
project_module.const_set param.to_s.upcase, value
|
131
|
+
end
|
132
|
+
|
133
|
+
project_module
|
134
|
+
end
|
135
|
+
|
136
|
+
##
|
137
|
+
# Provides a common configuration for the main project executable:
|
138
|
+
#
|
139
|
+
# * The program description (the sequence of non-blank lines at the
|
140
|
+
# top of the file in which this method is invoked) is properly
|
141
|
+
# formatted and displayed at the top of program's help information.
|
142
|
+
#
|
143
|
+
# * The program version information is fetched from the project module
|
144
|
+
# and formatted in YAML fashion for easy consumption by other tools.
|
145
|
+
#
|
146
|
+
# * A list of command-line options is displayed at
|
147
|
+
# the bottom of the program's help information.
|
148
|
+
#
|
149
|
+
# @param [Symbol] project_symbol
|
150
|
+
# Name of the Ruby constant which serves
|
151
|
+
# as a namespace for the entire project.
|
152
|
+
#
|
153
|
+
# @param trollop_args
|
154
|
+
# Optional arguments for Trollop::options().
|
155
|
+
#
|
156
|
+
# @param trollop_config
|
157
|
+
# Optional block argument for Trollop::options().
|
158
|
+
#
|
159
|
+
# @return The result of Trollop::options().
|
160
|
+
#
|
161
|
+
def main project_symbol, *trollop_args, &trollop_config
|
162
|
+
program_file = first_caller_file
|
163
|
+
program_home = File.dirname(File.dirname(program_file))
|
164
|
+
|
165
|
+
# load the project module
|
166
|
+
program_name = File.basename(program_home)
|
167
|
+
|
168
|
+
require File.join(program_home, 'lib', program_name)
|
169
|
+
project_module = fetch_project_module(project_symbol)
|
170
|
+
|
171
|
+
# parse command-line options
|
172
|
+
require 'trollop'
|
173
|
+
|
174
|
+
options = Trollop.options(*trollop_args) do
|
175
|
+
|
176
|
+
# show project description
|
177
|
+
text "#{project_module::PROJECT} - #{project_module::TAGLINE}"
|
178
|
+
text ''
|
179
|
+
|
180
|
+
# show program description
|
181
|
+
text File.read(program_file)[/\A.*?^$\n/m]. # grab the header
|
182
|
+
gsub(/^# ?/, ''). # strip the comment markers
|
183
|
+
sub(/\A!.*?\n/, '').lstrip # omit the shebang line
|
184
|
+
text ''
|
185
|
+
|
186
|
+
instance_eval(&trollop_config) if trollop_config
|
187
|
+
|
188
|
+
# show version information
|
189
|
+
version %w[PROJECT VERSION RELEASE WEBSITE INSTALL].map {|c|
|
190
|
+
"#{c.downcase}: #{project_module.const_get c}"
|
191
|
+
}.join("\n")
|
192
|
+
|
193
|
+
opt :manual, 'Show the user manual'
|
194
|
+
end
|
195
|
+
|
196
|
+
if options[:manual]
|
197
|
+
require 'launchy'
|
198
|
+
Launchy::Browser.run "#{project_module::INSTALL}/doc/index.xhtml"
|
199
|
+
exit
|
200
|
+
end
|
201
|
+
|
202
|
+
options
|
203
|
+
end
|
204
|
+
|
205
|
+
##
|
206
|
+
# Provides Rake tasks for packaging, publishing, and announcing your project.
|
207
|
+
#
|
208
|
+
# * An AUTHORS constant (which has the form "[[name, info]]"
|
209
|
+
# where "name" is the name of a copyright holder and "info" is
|
210
|
+
# their contact information) is added to the project module.
|
211
|
+
#
|
212
|
+
# This information is extracted from copyright notices in
|
213
|
+
# the project license file. NOTE that the first copyright
|
214
|
+
# notice must correspond to the primary project maintainer.
|
215
|
+
#
|
216
|
+
# Copyright notices must be in the following form:
|
217
|
+
#
|
218
|
+
# Copyright YEAR HOLDER <EMAIL>
|
219
|
+
#
|
220
|
+
# Where HOLDER is the name of the copyright holder, YEAR is the year
|
221
|
+
# when the copyright holder first began working on the project, and
|
222
|
+
# EMAIL is (optional) the email address of the copyright holder.
|
223
|
+
#
|
224
|
+
# @param [Symbol] project_symbol
|
225
|
+
# Name of the Ruby constant which serves
|
226
|
+
# as a namespace for the entire project.
|
227
|
+
#
|
228
|
+
# @param [Hash] options
|
229
|
+
# Additional method parameters, which are all optional:
|
230
|
+
#
|
231
|
+
# [String] :license_file =>
|
232
|
+
# Path (relative to the main project directory which contains the
|
233
|
+
# project Rakefile) to the file which contains the project license.
|
234
|
+
#
|
235
|
+
# The default value is "LICENSE".
|
236
|
+
#
|
237
|
+
# [String] :logins_file =>
|
238
|
+
# Path to the YAML file which contains login
|
239
|
+
# information for publishing release announcements.
|
240
|
+
#
|
241
|
+
# The default value is "~/.config/inochi/logins.yaml"
|
242
|
+
# where "~" is the path to your home directory.
|
243
|
+
#
|
244
|
+
# [String] :rubyforge_project =>
|
245
|
+
# Name of the RubyForge project where
|
246
|
+
# release packages will be published.
|
247
|
+
#
|
248
|
+
# The default value is the value of the PROGRAM constant.
|
249
|
+
#
|
250
|
+
# [String] :rubyforge_section =>
|
251
|
+
# Name of the RubyForge project's File Release System
|
252
|
+
# section where release packages will be published.
|
253
|
+
#
|
254
|
+
# The default value is the value of the :rubyforge_project parameter.
|
255
|
+
#
|
256
|
+
# [String] :raa_project =>
|
257
|
+
# Name of the RAA (Ruby Application Archive) entry for this project.
|
258
|
+
#
|
259
|
+
# The default value is the value of the PROGRAM constant.
|
260
|
+
#
|
261
|
+
# [String] :upload_target =>
|
262
|
+
# Where to upload the project documentation.
|
263
|
+
# See "destination" in the rsync manual.
|
264
|
+
#
|
265
|
+
# The default value is nil.
|
266
|
+
#
|
267
|
+
# [String] :upload_delete =>
|
268
|
+
# Delete unknown files at the upload target location?
|
269
|
+
#
|
270
|
+
# The default value is false.
|
271
|
+
#
|
272
|
+
# [Array] :upload_options =>
|
273
|
+
# Additional command-line arguments to the rsync command.
|
274
|
+
#
|
275
|
+
# The default value is an empty array.
|
276
|
+
#
|
277
|
+
# @param gem_config
|
278
|
+
# Block that is passed to Gem::specification.new()
|
279
|
+
# for additonal gem configuration.
|
280
|
+
#
|
281
|
+
# @yieldparam [Gem::Specification] gem_spec the gem specification
|
282
|
+
#
|
283
|
+
def rake project_symbol, options = {}, &gem_config
|
284
|
+
program_file = first_caller_file
|
285
|
+
program_home = File.dirname(program_file)
|
286
|
+
|
287
|
+
# load the project module
|
288
|
+
program_name = File.basename(program_home)
|
289
|
+
|
290
|
+
require File.join('lib', program_name)
|
291
|
+
project_module = fetch_project_module(project_symbol)
|
292
|
+
|
293
|
+
# supply default options
|
294
|
+
options[:rubyforge_project] ||= program_name
|
295
|
+
options[:rubyforge_section] ||= program_name
|
296
|
+
options[:raa_project] ||= program_name
|
297
|
+
options[:license_file] ||= 'LICENSE'
|
298
|
+
options[:logins_file] ||= File.join(ENV['HOME'], '.config', 'inochi', 'logins.yaml')
|
299
|
+
options[:upload_delete] ||= false
|
300
|
+
options[:upload_options] ||= []
|
301
|
+
|
302
|
+
# add AUTHORS constant to the project module
|
303
|
+
license = File.read(options[:license_file])
|
304
|
+
|
305
|
+
copyright_holders =
|
306
|
+
license.scan(/Copyright.*?\d+\s+(.*)/).flatten.
|
307
|
+
map {|s| (s =~ /\s*<(.*?)>/) ? [$`, $1] : [s, ''] }
|
308
|
+
|
309
|
+
project_module.const_set :AUTHORS, copyright_holders
|
310
|
+
|
311
|
+
require 'rake/clean'
|
312
|
+
|
313
|
+
hide_rake_task = lambda do |name|
|
314
|
+
Rake::Task[name].instance_variable_set :@comment, nil
|
315
|
+
end
|
316
|
+
|
317
|
+
# documentation
|
318
|
+
desc 'Build all documentation.'
|
319
|
+
task :doc => %w[ doc:api doc:man ]
|
320
|
+
|
321
|
+
# user manual
|
322
|
+
doc_man_src = 'doc/index.erb'
|
323
|
+
doc_man_dst = 'doc/index.xhtml'
|
324
|
+
doc_man_deps = FileList['doc/*.erb']
|
325
|
+
|
326
|
+
doc_man_doc = nil
|
327
|
+
task :doc_man_doc => doc_man_src do
|
328
|
+
unless doc_man_doc
|
329
|
+
require 'erbook' unless defined? ERBook
|
330
|
+
doc_man_txt = File.read(doc_man_src)
|
331
|
+
doc_man_doc = ERBook::Document.new(:xhtml, doc_man_txt, doc_man_src, :unindent => true)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
desc 'Build the user manual.'
|
336
|
+
task 'doc:man' => doc_man_dst
|
337
|
+
|
338
|
+
file doc_man_dst => doc_man_deps do
|
339
|
+
Rake::Task[:doc_man_doc].invoke
|
340
|
+
File.write doc_man_dst, doc_man_doc
|
341
|
+
end
|
342
|
+
|
343
|
+
CLOBBER.include doc_man_dst
|
344
|
+
|
345
|
+
# API reference
|
346
|
+
doc_api_dst = 'doc/api'
|
347
|
+
|
348
|
+
desc 'Build API reference.'
|
349
|
+
task 'doc:api' => doc_api_dst
|
350
|
+
|
351
|
+
require 'yard'
|
352
|
+
YARD::Rake::YardocTask.new doc_api_dst do |t|
|
353
|
+
t.options.push '--protected',
|
354
|
+
'--output-dir', doc_api_dst,
|
355
|
+
'--readme', options[:license_file]
|
356
|
+
|
357
|
+
task doc_api_dst => options[:license_file]
|
358
|
+
end
|
359
|
+
|
360
|
+
hide_rake_task[doc_api_dst]
|
361
|
+
|
362
|
+
CLEAN.include '.yardoc'
|
363
|
+
CLOBBER.include doc_api_dst
|
364
|
+
|
365
|
+
# announcements
|
366
|
+
desc 'Build all release announcements.'
|
367
|
+
task :ann => %w[ ann:feed ann:html ann:text ann:mail ]
|
368
|
+
|
369
|
+
# it has long been a tradition to use an "[ANN]" prefix
|
370
|
+
# when announcing things on the ruby-talk mailing list
|
371
|
+
ann_prefix = '[ANN] '
|
372
|
+
ann_subject = ann_prefix + project_module::DISPLAY
|
373
|
+
ann_project = ann_prefix + project_module::PROJECT
|
374
|
+
|
375
|
+
# fetch the project summary from user manual
|
376
|
+
ann_nfo_doc = nil
|
377
|
+
task :ann_nfo_doc => :doc_man_doc do
|
378
|
+
ann_nfo_doc = $project_summary_node
|
379
|
+
end
|
380
|
+
|
381
|
+
# fetch release notes from user manual
|
382
|
+
ann_rel_doc = nil
|
383
|
+
task :ann_rel_doc => :doc_man_doc do
|
384
|
+
unless ann_rel_doc
|
385
|
+
if parent = $project_history_node
|
386
|
+
if child = parent.children.first
|
387
|
+
ann_rel_doc = child
|
388
|
+
else
|
389
|
+
raise 'The "project_history" node in the user manual lacks child nodes.'
|
390
|
+
end
|
391
|
+
else
|
392
|
+
raise 'The user manual lacks a "project_history" node.'
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
# build release notes in HTML and plain text
|
398
|
+
# converts the given HTML into plain text. we do this using
|
399
|
+
# lynx because (1) it outputs a list of all hyperlinks used
|
400
|
+
# in the HTML document and (2) it runs on all major platforms
|
401
|
+
convert_html_to_text = lambda do |html|
|
402
|
+
require 'tempfile'
|
403
|
+
|
404
|
+
begin
|
405
|
+
# lynx's -dump option requires a .html file
|
406
|
+
tmp_file = Tempfile.new(Inochi::PROGRAM).path + '.html'
|
407
|
+
|
408
|
+
File.write tmp_file, html
|
409
|
+
text = `lynx -dump #{tmp_file} -width 70`
|
410
|
+
ensure
|
411
|
+
File.delete tmp_file
|
412
|
+
end
|
413
|
+
|
414
|
+
# improve readability of list items that span multiple
|
415
|
+
# lines by adding a blank line between such items
|
416
|
+
text.gsub! %r{^( *[^\*\s].*)(\r?\n)( *\* \S)}, '\1\2\2\3'
|
417
|
+
|
418
|
+
text
|
419
|
+
end
|
420
|
+
|
421
|
+
# binds relative addresses in the given HTML to the project docsite
|
422
|
+
resolve_html_links = lambda do |html|
|
423
|
+
# resolve relative URLs into absolute URLs
|
424
|
+
# see http://en.wikipedia.org/wiki/URI_scheme#Generic_syntax
|
425
|
+
require 'addressable/uri'
|
426
|
+
uri = Addressable::URI.parse(project_module::DOCSITE)
|
427
|
+
doc_url = uri.to_s
|
428
|
+
dir_url = uri.path =~ %r{/$|^$} ? doc_url : File.dirname(doc_url)
|
429
|
+
|
430
|
+
html.to_s.gsub %r{(href=|src=)(.)(.*?)(\2)} do |match|
|
431
|
+
a, b = $1 + $2, $3.to_s << $4
|
432
|
+
|
433
|
+
case $3
|
434
|
+
when %r{^[[:alpha:]][[:alnum:]\+\.\-]*://} # already absolute
|
435
|
+
match
|
436
|
+
|
437
|
+
when /^#/
|
438
|
+
a << File.join(doc_url, b)
|
439
|
+
|
440
|
+
else
|
441
|
+
a << File.join(dir_url, b)
|
442
|
+
end
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
ann_html = nil
|
447
|
+
task :ann_html => [:doc_man_doc, :ann_nfo_doc, :ann_rel_doc] do
|
448
|
+
unless ann_html
|
449
|
+
ann_html = %{
|
450
|
+
<center>
|
451
|
+
<h1>#{project_module::DISPLAY}</h1>
|
452
|
+
<p>#{project_module::TAGLINE}</p>
|
453
|
+
<p>#{project_module::WEBSITE}</p>
|
454
|
+
</center>
|
455
|
+
#{ann_nfo_doc}
|
456
|
+
#{ann_rel_doc}
|
457
|
+
}
|
458
|
+
|
459
|
+
# remove heading navigation menus
|
460
|
+
ann_html.gsub! %r{<div class="nav"[^>]*>(.*?)</div>}, ''
|
461
|
+
|
462
|
+
ann_html = resolve_html_links[ann_html]
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
ann_text = nil
|
467
|
+
task :ann_text => :ann_html do
|
468
|
+
unless ann_text
|
469
|
+
ann_text = convert_html_to_text[ann_html]
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
ann_nfo_text = nil
|
474
|
+
task :ann_nfo_text => :ann_nfo_doc do
|
475
|
+
unless ann_nfo_text
|
476
|
+
ann_nfo_html = resolve_html_links[ann_nfo_doc]
|
477
|
+
ann_nfo_text = convert_html_to_text[ann_nfo_html]
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
# HTML
|
482
|
+
ann_html_dst = 'ANN.html'
|
483
|
+
|
484
|
+
desc "Build HTML announcement: #{ann_html_dst}"
|
485
|
+
task 'ann:html' => ann_html_dst
|
486
|
+
|
487
|
+
file ann_html_dst => doc_man_deps do
|
488
|
+
Rake::Task[:ann_html].invoke
|
489
|
+
File.write ann_html_dst, ann_html
|
490
|
+
end
|
491
|
+
|
492
|
+
CLEAN.include ann_html_dst
|
493
|
+
|
494
|
+
# RSS feed
|
495
|
+
ann_feed_dst = 'doc/ann.xml'
|
496
|
+
|
497
|
+
desc "Build RSS announcement: #{ann_feed_dst}"
|
498
|
+
task 'ann:feed' => ann_feed_dst
|
499
|
+
|
500
|
+
file ann_feed_dst => doc_man_deps do
|
501
|
+
require 'time'
|
502
|
+
require 'rss/maker'
|
503
|
+
|
504
|
+
feed = RSS::Maker.make('2.0') do |feed|
|
505
|
+
feed.channel.title = ann_project
|
506
|
+
feed.channel.link = project_module::WEBSITE
|
507
|
+
feed.channel.description = project_module::TAGLINE
|
508
|
+
|
509
|
+
Rake::Task[:ann_rel_doc].invoke
|
510
|
+
Rake::Task[:ann_html].invoke
|
511
|
+
|
512
|
+
item = feed.items.new_item
|
513
|
+
item.title = ann_rel_doc.title
|
514
|
+
item.link = project_module::DOCSITE + '#' + ann_rel_doc.here_frag
|
515
|
+
item.date = Time.parse(item.title)
|
516
|
+
item.description = ann_html
|
517
|
+
end
|
518
|
+
|
519
|
+
File.write ann_feed_dst, feed
|
520
|
+
end
|
521
|
+
|
522
|
+
CLOBBER.include ann_feed_dst
|
523
|
+
|
524
|
+
# plain text
|
525
|
+
ann_text_dst = 'ANN.txt'
|
526
|
+
|
527
|
+
desc "Build plain text announcement: #{ann_text_dst}"
|
528
|
+
task 'ann:text' => ann_text_dst
|
529
|
+
|
530
|
+
file ann_text_dst => doc_man_deps do
|
531
|
+
Rake::Task[:ann_text].invoke
|
532
|
+
File.write ann_text_dst, ann_text
|
533
|
+
end
|
534
|
+
|
535
|
+
CLEAN.include ann_text_dst
|
536
|
+
|
537
|
+
# e-mail
|
538
|
+
ann_mail_dst = 'ANN.eml'
|
539
|
+
|
540
|
+
desc "Build e-mail announcement: #{ann_mail_dst}"
|
541
|
+
task 'ann:mail' => ann_mail_dst
|
542
|
+
|
543
|
+
file ann_mail_dst => doc_man_deps do
|
544
|
+
File.open ann_mail_dst, 'w' do |f|
|
545
|
+
require 'time'
|
546
|
+
f.puts "Date: #{Time.now.rfc822}"
|
547
|
+
|
548
|
+
f.puts 'To: ruby-talk@ruby-lang.org'
|
549
|
+
f.puts 'From: "%s" <%s>' % project_module::AUTHORS.first
|
550
|
+
f.puts "Subject: #{ann_subject}"
|
551
|
+
|
552
|
+
Rake::Task[:ann_text].invoke
|
553
|
+
f.puts '', ann_text
|
554
|
+
end
|
555
|
+
end
|
556
|
+
|
557
|
+
CLEAN.include ann_mail_dst
|
558
|
+
|
559
|
+
# packaging
|
560
|
+
desc 'Build a release.'
|
561
|
+
task :pak => [:clobber, :doc] do
|
562
|
+
sh $0, 'package'
|
563
|
+
end
|
564
|
+
CLEAN.include 'pkg'
|
565
|
+
|
566
|
+
# ruby gem
|
567
|
+
require 'rake/gempackagetask'
|
568
|
+
|
569
|
+
gem = Gem::Specification.new do |gem|
|
570
|
+
authors = project_module::AUTHORS
|
571
|
+
|
572
|
+
if author = authors.first
|
573
|
+
gem.author, gem.email = author
|
574
|
+
end
|
575
|
+
|
576
|
+
if authors.length > 1
|
577
|
+
gem.authors = authors.map {|name, mail| name }
|
578
|
+
end
|
579
|
+
|
580
|
+
gem.rubyforge_project = options[:rubyforge_project]
|
581
|
+
|
582
|
+
# XXX: In theory, `gem.name` should be assigned to
|
583
|
+
# ::PROJECT instead of ::PROGRAM
|
584
|
+
#
|
585
|
+
# In practice, PROJECT may contain non-word
|
586
|
+
# characters and may also contain a mixture
|
587
|
+
# of lowercase and uppercase letters.
|
588
|
+
#
|
589
|
+
# This makes it difficult for people to
|
590
|
+
# install the project gem because they must
|
591
|
+
# remember the exact spelling used in
|
592
|
+
# `gem.name` when running `gem install ____`.
|
593
|
+
#
|
594
|
+
# For example, consider the "RedCloth" gem.
|
595
|
+
#
|
596
|
+
gem.name = project_module::PROGRAM
|
597
|
+
|
598
|
+
gem.version = project_module::VERSION
|
599
|
+
gem.summary = project_module::TAGLINE
|
600
|
+
gem.description = gem.summary
|
601
|
+
gem.homepage = project_module::WEBSITE
|
602
|
+
gem.files = FileList['**/*'].exclude('_darcs') - CLEAN
|
603
|
+
gem.executables = project_module::PROGRAM
|
604
|
+
gem.has_rdoc = true
|
605
|
+
|
606
|
+
unless project_module == Inochi
|
607
|
+
gem.add_dependency 'inochi', Inochi::VERSION
|
608
|
+
end
|
609
|
+
|
610
|
+
project_module::REQUIRE.each_pair do |gem_name, version_reqs|
|
611
|
+
gem.add_dependency gem_name, *version_reqs
|
612
|
+
end
|
613
|
+
|
614
|
+
# additional configuration is done by user
|
615
|
+
yield gem if gem_config
|
616
|
+
end
|
617
|
+
|
618
|
+
Rake::GemPackageTask.new(gem).define
|
619
|
+
|
620
|
+
# XXX: hide the tasks defined by the above gem packaging library
|
621
|
+
%w[gem package repackage clobber_package].each {|t| hide_rake_task[t] }
|
622
|
+
|
623
|
+
# releasing
|
624
|
+
desc 'Publish a release.'
|
625
|
+
task 'pub' => %w[ pub:pak pub:doc pub:ann ]
|
626
|
+
|
627
|
+
# connect to RubyForge services
|
628
|
+
pub_forge = nil
|
629
|
+
pub_forge_project = options[:rubyforge_project]
|
630
|
+
pub_forge_section = options[:rubyforge_section]
|
631
|
+
|
632
|
+
task :pub_forge do
|
633
|
+
require 'rubyforge'
|
634
|
+
pub_forge = RubyForge.new
|
635
|
+
pub_forge.configure('release_date' => project_module::RELEASE)
|
636
|
+
|
637
|
+
unless pub_forge.autoconfig['group_ids'].key? pub_forge_project
|
638
|
+
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."
|
639
|
+
end
|
640
|
+
|
641
|
+
pub_forge.login
|
642
|
+
end
|
643
|
+
|
644
|
+
# documentation
|
645
|
+
desc 'Publish documentation to project website.'
|
646
|
+
task 'pub:doc' => [:doc, 'ann:feed'] do
|
647
|
+
target = options[:upload_target]
|
648
|
+
|
649
|
+
unless target
|
650
|
+
require 'addressable/uri'
|
651
|
+
docsite = Addressable::URI.parse(project_module::DOCSITE)
|
652
|
+
|
653
|
+
# provide uploading capability to websites hosted on RubyForge
|
654
|
+
if docsite.host.include? '.rubyforge.org'
|
655
|
+
target = "#{pub_forge.userconfig['username']}@rubyforge.org:#{File.join '/var/www/gforge-projects', options[:rubyforge_project], docsite.path}"
|
656
|
+
end
|
657
|
+
end
|
658
|
+
|
659
|
+
if target
|
660
|
+
cmd = ['rsync', '-auvz', 'doc/', "#{target}/"]
|
661
|
+
cmd.push '--delete' if options[:upload_delete]
|
662
|
+
cmd.concat options[:upload_options]
|
663
|
+
|
664
|
+
p cmd
|
665
|
+
sh(*cmd)
|
666
|
+
end
|
667
|
+
end
|
668
|
+
|
669
|
+
# announcement
|
670
|
+
desc 'Publish all announcements.'
|
671
|
+
task 'pub:ann' => %w[ pub:ann:forge pub:ann:raa pub:ann:talk ]
|
672
|
+
|
673
|
+
# login information
|
674
|
+
ann_logins_file = options[:logins_file]
|
675
|
+
ann_logins = nil
|
676
|
+
|
677
|
+
task :ann_logins do
|
678
|
+
ann_logins = begin
|
679
|
+
require 'yaml'
|
680
|
+
YAML.load_file ann_logins_file
|
681
|
+
rescue => e
|
682
|
+
warn "Could not read login information from #{ann_logins_file.inspect}:"
|
683
|
+
warn e
|
684
|
+
warn "** You will NOT be able to publish release announcements! **"
|
685
|
+
{}
|
686
|
+
end
|
687
|
+
end
|
688
|
+
|
689
|
+
desc 'Announce to RubyForge news.'
|
690
|
+
task 'pub:ann:forge' => :pub_forge do
|
691
|
+
project = options[:rubyforge_project]
|
692
|
+
|
693
|
+
if group_id = pub_forge.autoconfig['group_ids'][project]
|
694
|
+
# check if this release was already announced
|
695
|
+
require 'mechanize'
|
696
|
+
www = WWW::Mechanize.new
|
697
|
+
page = www.get "http://rubyforge.org/news/?group_id=#{group_id}"
|
698
|
+
|
699
|
+
posts = (page/'//a[starts-with(./@href, "/forum/forum.php?forum_id=")]/text()').map {|e| e.to_s.gsub("\302\240", '').strip }
|
700
|
+
|
701
|
+
already_announced = posts.include? ann_subject
|
702
|
+
|
703
|
+
if already_announced
|
704
|
+
warn "This release was already announced to RubyForge news, so I will NOT announce it there again."
|
705
|
+
else
|
706
|
+
# make the announcement
|
707
|
+
Rake::Task[:ann_text].invoke
|
708
|
+
pub_forge.post_news project, ann_subject, ann_text
|
709
|
+
|
710
|
+
puts "Successfully announced to RubyForge news:"
|
711
|
+
puts page.uri
|
712
|
+
end
|
713
|
+
else
|
714
|
+
raise "Could not determine the group_id of the #{project.inspect} RubyForge project. Run `rubyforge config` and try again."
|
715
|
+
end
|
716
|
+
end
|
717
|
+
|
718
|
+
desc 'Announce to ruby-talk mailing list.'
|
719
|
+
task 'pub:ann:talk' => :ann_logins do
|
720
|
+
host = 'http://ruby-forum.com'
|
721
|
+
ruby_talk = 4 # ruby-talk forum ID
|
722
|
+
|
723
|
+
require 'mechanize'
|
724
|
+
www = WWW::Mechanize.new
|
725
|
+
|
726
|
+
# check if this release was already announced
|
727
|
+
already_announced =
|
728
|
+
begin
|
729
|
+
page = www.get "#{host}/forum/#{ruby_talk}", :filter => %{"#{ann_subject}"}
|
730
|
+
|
731
|
+
posts = (page/'//div[@class="forum"]//a[starts-with(./@href, "/topic/")]/text()').map {|e| e.to_s.strip }
|
732
|
+
posts.include? ann_subject
|
733
|
+
rescue
|
734
|
+
false
|
735
|
+
end
|
736
|
+
|
737
|
+
if already_announced
|
738
|
+
warn "This release was already announced to the ruby-talk mailing list, so I will NOT announce it there again."
|
739
|
+
else
|
740
|
+
# log in to RubyForum
|
741
|
+
page = www.get "#{host}/user/login"
|
742
|
+
form = page.forms.first
|
743
|
+
|
744
|
+
if login = ann_logins['www.ruby-forum.com']
|
745
|
+
form['name'] = login['user']
|
746
|
+
form['password'] = login['pass']
|
747
|
+
end
|
748
|
+
|
749
|
+
page = form.click_button # use the first submit button
|
750
|
+
|
751
|
+
if (page/'//a[@href="/user/logout"]').empty?
|
752
|
+
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."
|
753
|
+
else
|
754
|
+
# make the announcement
|
755
|
+
page = www.get "#{host}/topic/new?forum_id=#{ruby_talk}"
|
756
|
+
form = page.forms.first
|
757
|
+
|
758
|
+
Rake::Task[:ann_text].invoke
|
759
|
+
form['post[subject]'] = ann_subject
|
760
|
+
form['post[text]'] = ann_text
|
761
|
+
|
762
|
+
form.checkboxes.first.check # enable email notification
|
763
|
+
page = form.submit
|
764
|
+
|
765
|
+
errors = [page/'//div[@class="error"]/text()'].flatten
|
766
|
+
if errors.empty?
|
767
|
+
puts "Successfully announced to ruby-talk mailing list:"
|
768
|
+
puts page.uri
|
769
|
+
else
|
770
|
+
warn "Could not announce to ruby-talk mailing list:"
|
771
|
+
warn errors.join("\n")
|
772
|
+
end
|
773
|
+
end
|
774
|
+
end
|
775
|
+
end
|
776
|
+
|
777
|
+
desc 'Announce to RAA (Ruby Application Archive).'
|
778
|
+
task 'pub:ann:raa' => :ann_logins do
|
779
|
+
show_page_error = lambda do |page, message|
|
780
|
+
warn "#{message}, so I can NOT announce this release to RAA:"
|
781
|
+
warn "#{(page/'h2').text} -- #{(page/'p').first.text.strip}"
|
782
|
+
end
|
783
|
+
|
784
|
+
resource = "#{options[:raa_project].inspect} project entry on RAA"
|
785
|
+
|
786
|
+
require 'mechanize'
|
787
|
+
www = WWW::Mechanize.new
|
788
|
+
page = www.get "http://raa.ruby-lang.org/update.rhtml?name=#{options[:raa_project]}"
|
789
|
+
|
790
|
+
if form = page.forms[1]
|
791
|
+
resource << " (owned by #{form.owner.inspect})"
|
792
|
+
|
793
|
+
Rake::Task[:ann_nfo_text].invoke
|
794
|
+
form['description'] = ann_nfo_text
|
795
|
+
form['description_style'] = 'Pre-formatted'
|
796
|
+
form['short_description'] = project_module::TAGLINE
|
797
|
+
form['version'] = project_module::VERSION
|
798
|
+
form['url'] = project_module::WEBSITE
|
799
|
+
form['pass'] = ann_logins['raa.ruby-lang.org']['pass']
|
800
|
+
|
801
|
+
page = form.submit
|
802
|
+
|
803
|
+
if page.title =~ /error/i
|
804
|
+
show_page_error[page, "Could not update #{resource}"]
|
805
|
+
else
|
806
|
+
puts "Successfully announced to RAA (Ruby Application Archive)."
|
807
|
+
end
|
808
|
+
else
|
809
|
+
show_page_error[page, "Could not access #{resource}"]
|
810
|
+
end
|
811
|
+
end
|
812
|
+
|
813
|
+
# release packages
|
814
|
+
desc 'Publish release packages to RubyForge.'
|
815
|
+
task 'pub:pak' => :pub_forge do
|
816
|
+
# check if this release was already published
|
817
|
+
version = project_module::VERSION
|
818
|
+
packages = pub_forge.autoconfig['release_ids'][pub_forge_section]
|
819
|
+
|
820
|
+
if packages and packages.key? version
|
821
|
+
warn "The release packages were already published, so I will NOT publish them again."
|
822
|
+
else
|
823
|
+
# create the FRS package section
|
824
|
+
unless pub_forge.autoconfig['package_ids'].key? pub_forge_section
|
825
|
+
pub_forge.create_package pub_forge_project, pub_forge_section
|
826
|
+
end
|
827
|
+
|
828
|
+
# publish the package to the section
|
829
|
+
uploader = lambda do |command, *files|
|
830
|
+
pub_forge.__send__ command, pub_forge_project, pub_forge_section, version, *files
|
831
|
+
end
|
832
|
+
|
833
|
+
Rake::Task[:pak].invoke
|
834
|
+
packages = Dir['pkg/*.[a-z]*']
|
835
|
+
|
836
|
+
unless packages.empty?
|
837
|
+
# NOTE: use the 'add_release' command ONLY for the first
|
838
|
+
# file because it creates a new sub-section on the
|
839
|
+
# RubyForge download page; we do not want one package
|
840
|
+
# per sub-section on the RubyForge download page!
|
841
|
+
#
|
842
|
+
uploader[:add_release, packages.shift]
|
843
|
+
|
844
|
+
unless packages.empty?
|
845
|
+
uploader[:add_file, *packages]
|
846
|
+
end
|
847
|
+
|
848
|
+
puts "Successfully published release packages to RubyForge."
|
849
|
+
end
|
850
|
+
end
|
851
|
+
end
|
852
|
+
end
|
853
|
+
|
854
|
+
##
|
855
|
+
# Provides a common configuration for the project's user manual:
|
856
|
+
#
|
857
|
+
# * Assigns the title, subtitle, date, and authors for the document.
|
858
|
+
#
|
859
|
+
# You may override these assignments by reassigning these
|
860
|
+
# document parameters AFTER this method is invoked.
|
861
|
+
#
|
862
|
+
# Refer to the "document parameters" for the XHTML
|
863
|
+
# format in the "erbook" user manual for details.
|
864
|
+
#
|
865
|
+
# * Provides the project's configuration as global variables in the document.
|
866
|
+
#
|
867
|
+
# For example, <%= $version %> is the same as
|
868
|
+
# <%= project_module::VERSION %> in the document.
|
869
|
+
#
|
870
|
+
# * Defines a "project_summary" node for use in the document. The body
|
871
|
+
# of this node should contain a brief introduction to the project.
|
872
|
+
#
|
873
|
+
# * Defines a "project_history" node for use in the document. The body
|
874
|
+
# of this node should contain other nodes, each of which represent a
|
875
|
+
# single set of release notes for one of the project's releases.
|
876
|
+
#
|
877
|
+
# It is assumed that this method is called
|
878
|
+
# from within the Inochi.rake() environment.
|
879
|
+
#
|
880
|
+
# @param [Symbol] project_symbol
|
881
|
+
# Name of the Ruby constant which serves
|
882
|
+
# as a namespace for the entire project.
|
883
|
+
#
|
884
|
+
# @param [ERBook::Document::Template] book_template
|
885
|
+
# The eRuby template which serves as the documentation for the project.
|
886
|
+
#
|
887
|
+
def book project_symbol, book_template
|
888
|
+
project_module = fetch_project_module(project_symbol)
|
889
|
+
|
890
|
+
# provide project constants as global variables to the user manual
|
891
|
+
project_module::INOCHI.each_pair do |param, value|
|
892
|
+
eval "$#{param} = value", binding
|
893
|
+
end
|
894
|
+
|
895
|
+
# set document parameters for the user manual
|
896
|
+
$title = project_module::DISPLAY
|
897
|
+
$subtitle = project_module::TAGLINE
|
898
|
+
$feeds = { File.join(project_module::DOCSITE, 'ann.xml') => :rss }
|
899
|
+
$authors = Hash[
|
900
|
+
*project_module::AUTHORS.map do |name, addr|
|
901
|
+
# convert raw e-mail addresses into URLs for the erbook XHTML format
|
902
|
+
addr = "mailto:#{addr}" unless addr =~ /^\w+:/
|
903
|
+
|
904
|
+
[name, addr]
|
905
|
+
end.flatten
|
906
|
+
]
|
907
|
+
|
908
|
+
class << book_template
|
909
|
+
def project_summary
|
910
|
+
raise ArgumentError, 'block must be given' unless block_given?
|
911
|
+
node do
|
912
|
+
$project_summary_node = @nodes.last
|
913
|
+
yield
|
914
|
+
end
|
915
|
+
end
|
916
|
+
|
917
|
+
def project_history
|
918
|
+
raise ArgumentError, 'block must be given' unless block_given?
|
919
|
+
node do
|
920
|
+
$project_history_node = @nodes.last
|
921
|
+
yield
|
922
|
+
end
|
923
|
+
end
|
924
|
+
end
|
925
|
+
end
|
926
|
+
|
927
|
+
##
|
928
|
+
# Returns the name of the main program executable, which
|
929
|
+
# is the same as the project name fully in lowercase.
|
930
|
+
#
|
931
|
+
def calc_program_name project_symbol
|
932
|
+
camel_to_snake_case(project_symbol).downcase
|
933
|
+
end
|
934
|
+
|
935
|
+
##
|
936
|
+
# Calculates the name of the project module from the given project name.
|
937
|
+
#
|
938
|
+
def calc_project_symbol project_name
|
939
|
+
name = project_name.to_s.gsub(/\W+/, '_').squeeze('_').gsub(/^_|_$/, '')
|
940
|
+
(name[0,1].upcase + name[1..-1]).to_sym
|
941
|
+
end
|
942
|
+
|
943
|
+
##
|
944
|
+
# Transforms the given input from CamelCase to snake_case.
|
945
|
+
#
|
946
|
+
def camel_to_snake_case input
|
947
|
+
input = input.to_s.dup
|
948
|
+
|
949
|
+
# handle camel case like FooBar => Foo_Bar
|
950
|
+
while input.gsub!(/([a-z]+)([A-Z])(\w+)/) { $1 + '_' + $2 + $3 }
|
951
|
+
end
|
952
|
+
|
953
|
+
# handle abbreviations like XMLParser => XML_Parser
|
954
|
+
while input.gsub!(/([A-Z]+)([A-Z])([a-z]+)/) { $1 + '_' + $2 + $3 }
|
955
|
+
end
|
956
|
+
|
957
|
+
input
|
958
|
+
end
|
959
|
+
|
960
|
+
private
|
961
|
+
|
962
|
+
##
|
963
|
+
# Returns the path of the file in which this method was called. Calls
|
964
|
+
# to this method from within *THIS* file are excluded from the search.
|
965
|
+
#
|
966
|
+
def first_caller_file
|
967
|
+
File.expand_path caller.each {|s| !s.include? __FILE__ and s =~ /^(.*?):\d+/ and break $1 }
|
968
|
+
end
|
969
|
+
|
970
|
+
##
|
971
|
+
# Returns the project module corresponding to the given symbol.
|
972
|
+
# A new module is created if none already exists.
|
973
|
+
#
|
974
|
+
def fetch_project_module project_symbol
|
975
|
+
if Object.const_defined? project_symbol
|
976
|
+
project_module = Object.const_get(project_symbol)
|
977
|
+
else
|
978
|
+
project_module = Module.new
|
979
|
+
Object.const_set project_symbol, project_module
|
980
|
+
end
|
981
|
+
|
982
|
+
project_module
|
983
|
+
end
|
984
|
+
end
|
985
|
+
end
|
986
|
+
|
987
|
+
##
|
988
|
+
# utility methods
|
989
|
+
#
|
990
|
+
|
991
|
+
unless File.respond_to? :write
|
992
|
+
##
|
993
|
+
# Writes the given content to the given file.
|
994
|
+
#
|
995
|
+
# @return number of bytes written
|
996
|
+
#
|
997
|
+
def File.write path, content
|
998
|
+
File.open(path, 'wb') {|f| f.write content.to_s }
|
999
|
+
end
|
1000
|
+
end
|