date-performance 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/AUTHORS +1 -0
  2. data/BENCHMARKS +30 -0
  3. data/COPYING +22 -0
  4. data/README.txt +134 -0
  5. data/Rakefile +90 -0
  6. data/doc/asciidoc.conf +21 -0
  7. data/doc/changes.html +32 -0
  8. data/doc/changes.txt +7 -0
  9. data/doc/images/icons/callouts/1.png +0 -0
  10. data/doc/images/icons/callouts/10.png +0 -0
  11. data/doc/images/icons/callouts/11.png +0 -0
  12. data/doc/images/icons/callouts/12.png +0 -0
  13. data/doc/images/icons/callouts/13.png +0 -0
  14. data/doc/images/icons/callouts/14.png +0 -0
  15. data/doc/images/icons/callouts/15.png +0 -0
  16. data/doc/images/icons/callouts/2.png +0 -0
  17. data/doc/images/icons/callouts/3.png +0 -0
  18. data/doc/images/icons/callouts/4.png +0 -0
  19. data/doc/images/icons/callouts/5.png +0 -0
  20. data/doc/images/icons/callouts/6.png +0 -0
  21. data/doc/images/icons/callouts/7.png +0 -0
  22. data/doc/images/icons/callouts/8.png +0 -0
  23. data/doc/images/icons/callouts/9.png +0 -0
  24. data/doc/images/icons/caution.png +0 -0
  25. data/doc/images/icons/example.png +0 -0
  26. data/doc/images/icons/home.png +0 -0
  27. data/doc/images/icons/important.png +0 -0
  28. data/doc/images/icons/next.png +0 -0
  29. data/doc/images/icons/note.png +0 -0
  30. data/doc/images/icons/prev.png +0 -0
  31. data/doc/images/icons/tip.png +0 -0
  32. data/doc/images/icons/up.png +0 -0
  33. data/doc/images/icons/warning.png +0 -0
  34. data/doc/license.html +42 -0
  35. data/doc/license.txt +1 -0
  36. data/doc/stylesheets/handbookish-manpage.css +36 -0
  37. data/doc/stylesheets/handbookish-quirks.css +2 -0
  38. data/doc/stylesheets/handbookish.css +119 -0
  39. data/ext/date_performance.c +401 -0
  40. data/ext/extconf.rb +9 -0
  41. data/lib/date/memoize.rb +86 -0
  42. data/lib/date/performance.rb +44 -0
  43. data/misc/asciidoc.rake +121 -0
  44. data/misc/project.rake +922 -0
  45. data/test/date_memoize_test.rb +70 -0
  46. data/test/extension_test.rb +120 -0
  47. metadata +110 -0
@@ -0,0 +1,9 @@
1
+ require 'mkmf'
2
+
3
+ # Disable warnings from ld
4
+ $LDFLAGS = "-w"
5
+ # turn on warnings from gcc
6
+ $CFLAGS = "-pedantic -Wall -Wno-long-long -Winline"
7
+
8
+ dir_config 'date_performance'
9
+ create_makefile 'date_performance'
@@ -0,0 +1,86 @@
1
+ # See Date::Memoize.
2
+
3
+ require 'date'
4
+ require 'date/performance'
5
+
6
+ class Date #:nodoc:
7
+
8
+ # Adds memoization to Date. This can speed things up significantly in cases where a lot
9
+ # of the same Date objects are created.
10
+ module Memoize
11
+
12
+ # Memoized version of Date::strptime.
13
+ def strptime(str='-4712-01-01', fmt='%F', sg=ITALY)
14
+ @__memoized_strptime_dates[ [ str, fmt, sg ] ]
15
+ end
16
+
17
+ # Memoized version Date::parse.
18
+ def parse(str='-4712-01-01', comp=false, sg=ITALY)
19
+ @__memoized_parse_dates[ [ str, comp, sg ] ]
20
+ end
21
+
22
+ # Memoized version of Date::civil.
23
+ def civil(y=-4712, m=1, d=1, sg=ITALY)
24
+ @__memoized_civil_dates[ [ y, m, d, sg ] ]
25
+ end
26
+
27
+ alias_method :new, :civil
28
+
29
+ public
30
+
31
+ # The methods we'll be replacing on the Date singleton.
32
+ def self.methods_replaced
33
+ [ :new, :civil, :strptime, :parse ]
34
+ end
35
+
36
+ # Overridden to move the existing methods out of the way before copying this module's
37
+ # methods.
38
+ def self.extend_object(base)
39
+ singleton = (class<<base;self;end)
40
+ methods_replaced.each do |method|
41
+ singleton.send :alias_method, "#{method}_without_memoization", method
42
+ singleton.send :remove_method, method
43
+ end
44
+ base.send :instance_variable_set, :@__memoized_civil_dates,
45
+ Hash.new{|h,key| h[key]=Date.new_without_memoization(*key)}
46
+ base.send :instance_variable_set, :@__memoized_strptime_dates,
47
+ Hash.new{|h,key| h[key]=Date.strptime_without_memoization(*key)}
48
+ base.send :instance_variable_set, :@__memoized_parse_dates,
49
+ Hash.new{|h,key| h[key]=Date.parse_without_memoization(*key)}
50
+ super
51
+ end
52
+
53
+ # Removes memoization methods from singleton of the class provided.
54
+ def self.unextend_object(base)
55
+ singleton = (class<<base;self;end)
56
+ methods_replaced.each do |method|
57
+ singleton.send :alias_method, method, "#{method}_without_memoization"
58
+ singleton.send :remove_method, "#{method}_without_memoization"
59
+ end
60
+ base.send :remove_instance_variable, :@__memoized_civil_dates
61
+ base.send :remove_instance_variable, :@__memoized_strptime_dates
62
+ base.send :remove_instance_variable, :@__memoized_parse_dates
63
+ base
64
+ end
65
+
66
+ # Is Date memoization currently installed and active?
67
+ def self.installed?
68
+ Date.respond_to? :civil_without_memoization
69
+ end
70
+
71
+ # Extend the Date class with memoized versions of +new+ and +civil+ but only if
72
+ # memoization has not yet been installed.
73
+ def self.install!
74
+ Date.extend self unless installed?
75
+ end
76
+
77
+ # Remove memoized methods and free up memo cache. This method is idempotent.
78
+ def self.uninstall!
79
+ unextend_object Date if installed?
80
+ end
81
+
82
+ end
83
+
84
+ Memoize.install!
85
+
86
+ end
@@ -0,0 +1,44 @@
1
+ require 'date'
2
+
3
+ # Loading this file is not idemponent and can cause damage when loaded twice.
4
+ # Fail hard and fast.
5
+ fail "Date::Performance already loaded." if defined? Date::Performance
6
+
7
+ class Date
8
+
9
+ # The Date::Performance module is present when the performance enhacing extension
10
+ # has been loaded. It serves no other purpose.
11
+ module Performance
12
+ VERSION = "0.4.6"
13
+ end
14
+
15
+ # The extension replaces Date#strftime but falls back on the stock version when
16
+ # strftime(3) cannot handle the format.
17
+ alias_method :strftime_without_performance, :strftime
18
+
19
+ class << self
20
+ # Ruby 1.8.6 introduced Date.new! and the extension uses it. The method was
21
+ # called new0 in <= 1.8.5.
22
+ alias_method :new!, :new0 unless Date.respond_to?(:new!)
23
+
24
+ # The extension replaces Date.strptime but falls back on the stock version when
25
+ # strptime(3) can't handle the format.
26
+ alias_method :strptime_without_performance, :strptime
27
+ end
28
+
29
+ end
30
+
31
+ # Load up the extension but bring the Date class back to its original state
32
+ # if the extension fails to load properly.
33
+ begin
34
+ require 'date_performance.so'
35
+ rescue
36
+ class Date
37
+ remove_const :Performance
38
+ remove_method :strftime_without_performance
39
+ class << self
40
+ remove_method :strptime_without_performance
41
+ end
42
+ end
43
+ raise
44
+ end
@@ -0,0 +1,121 @@
1
+
2
+ # Run asciidoc.
3
+ def asciidoc(source, dest, *args)
4
+ options = args.last.is_a?(Hash) ? args.pop : {}
5
+ options[:verbose] = verbose if options[:verbose].nil?
6
+ attributes = options[:attributes] || {}
7
+ config_file = options[:config_file]
8
+ if source_dir = options[:source_dir]
9
+ source = source.sub(/^#{source_dir}/, '.')
10
+ dest = dest.sub(/^#{source_dir}/, '.')
11
+ config_file = config_file.sub(/^#{source_dir}/, '.') if config_file
12
+ end
13
+ command = [
14
+ 'asciidoc',
15
+ ('--unsafe' unless options[:safe]),
16
+ ('--verbose' if options[:verbose]),
17
+ ('--no-header-footer' if options[:suppress_header]),
18
+ ("-a theme=#{options[:theme]}" if options[:theme]),
19
+ ("-a stylesdir='#{options[:styles]}'" if options[:styles]),
20
+ "-a linkcss -a quirks\!",
21
+ ("-f '#{config_file}'" if config_file),
22
+ attributes.map{|k,v| "-a #{k}=#{v.inspect}" },
23
+ ("-d #{options[:doc_type]}" if options[:doc_type]),
24
+ "-o", dest,
25
+ args,
26
+ source
27
+ ].flatten.compact
28
+ chdir(options[:source_dir] || Dir.getwd) { sh command.join(' ') }
29
+ end
30
+
31
+
32
+ class AsciiDocTasks
33
+
34
+ attr_reader :task_name
35
+
36
+ # The directory where asciidoc sources are stored. Defaults to +doc+.
37
+ attr_accessor :source_dir
38
+
39
+ # A list of source files to build.
40
+ attr_accessor :source_files
41
+
42
+ # Set true to disable potentially unsafe document instructions. For example,
43
+ # the asciidoc sys:: macro is disabled when safe mode is enabled.
44
+ attr_accessor :safe
45
+
46
+ # Override the default verbosity.
47
+ attr_accessor :verbose
48
+
49
+ # Do not output header and footer.
50
+ attr_accessor :suppress_header
51
+
52
+ # An asciidoc configuration file.
53
+ attr_accessor :config_file
54
+
55
+ # One of :article, :manpage, or :book
56
+ attr_accessor :doc_type
57
+
58
+ # Hash of asciidoc attributes passed in via the -a argument.
59
+ attr_accessor :attributes
60
+
61
+ def initialize(task_name)
62
+ @task_name = task_name
63
+ @source_dir = 'doc'
64
+ @source_files = FileList.new
65
+ @safe = false
66
+ @verbose = nil
67
+ @suppress_header = false
68
+ @doc_type = :article
69
+ @config_file = nil
70
+ yield self if block_given?
71
+ define!
72
+ end
73
+
74
+ def define!
75
+ task task_name => "#{task_name}:build"
76
+ task "#{task_name}:clean"
77
+ task "#{task_name}:build"
78
+ task "#{task_name}:rebuild"
79
+ task :clean => "#{task_name}:clean"
80
+ source_and_destination_files.each do |source,dest|
81
+ file dest => [ source, config_file ].compact do |f|
82
+ asciidoc source, dest, options
83
+ end
84
+ task("#{task_name}:build" => dest)
85
+ task("#{task_name}:clean") { rm_f dest }
86
+ task("#{task_name}:rebuild" => [ "#{task_name}:clean", "#{task_name}:build" ])
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ def destination_files
93
+ source_files.collect { |path| path.sub(/\.\w+$/, ".html") }
94
+ end
95
+
96
+ def source_and_destination_files
97
+ source_files.zip(destination_files)
98
+ end
99
+
100
+ undef :config_file
101
+
102
+ def config_file
103
+ @config_file ||
104
+ if File.exist?("#{source_dir}/asciidoc.conf")
105
+ "#{source_dir}/asciidoc.conf"
106
+ else
107
+ nil
108
+ end
109
+ end
110
+
111
+ def options
112
+ { :safe => safe,
113
+ :suppress_header => suppress_header,
114
+ :verbose => self.verbose,
115
+ :source_dir => source_dir,
116
+ :config_file => config_file,
117
+ :doc_type => doc_type,
118
+ :attributes => attributes }
119
+ end
120
+
121
+ end
@@ -0,0 +1,922 @@
1
+ # An Intelligent Ruby Project Template
2
+ #
3
+ # === Synopsis
4
+ #
5
+ # This file should be loaded at the top of your project Rakefile as follows:
6
+ #
7
+ # load "misc/project.rake"
8
+ #
9
+ # Project.new "Test Project", "1.0" do |p|
10
+ # p.package_name = 'test-project'
11
+ # p.author = 'John Doe <jdoe@example.com>'
12
+ # p.summary = 'A Project That Does Nothing'
13
+ # p.description = <<-end
14
+ # This project does nothing other than serve as an example of
15
+ # how to use this default project thingy. By the way, any leading
16
+ # space included in this description is automatically stripped.
17
+ #
18
+ # Even when text spans multiple paragraphs.
19
+ # end
20
+ #
21
+ # p.depends_on 'some-package', '~> 1.0'
22
+ # p.remote_dist_location = "example.com:/dist/#{p.package_name}"
23
+ # p.remote_doc_location = "example.com:/doc/#{p.package_name}"
24
+ # end
25
+ #
26
+ # A default set of Rake tasks are created based on the attributes specified.
27
+ # See the documentation for the Project class for more information.
28
+ #
29
+ # === MIT License
30
+ #
31
+ # Copyright (C) 2007 by Ryan Tomayko
32
+ #
33
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
34
+ # of this software and associated documentation files (the "Software"), to deal
35
+ # in the Software without restriction, including without limitation the rights
36
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
37
+ # copies of the Software, and to permit persons to whom the Software is
38
+ # furnished to do so, subject to the following conditions:
39
+ #
40
+ # The above copyright notice and this permission notice shall be included in all
41
+ # copies or substantial portions of the Software.
42
+ #
43
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
44
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
45
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
46
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
47
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
48
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
49
+ # SOFTWARE.
50
+
51
+ require 'rake'
52
+ require 'rake/clean'
53
+ require 'rake/gempackagetask'
54
+ require 'rake/rdoctask'
55
+ require 'rake/testtask'
56
+
57
+ # The Project class stores various aspects of project configuration information
58
+ # and attempts to make best-guess assumptions about the current environment.
59
+ class Project
60
+
61
+ # Array of project attribute names.
62
+ @project_attributes = []
63
+
64
+ class << self #:nodoc:
65
+
66
+ # The first Project instance that's created is stored here. Messages sent
67
+ # to the Project class are delegated here.
68
+ attr_accessor :current
69
+
70
+ # An array of attribute names available on project objects.
71
+ attr_reader :project_attributes
72
+
73
+ # Project class delegates missing messages to the current instance so let
74
+ # the world know.
75
+ def respond_to?(name, include_private=false) #:nodoc:
76
+ super || (current && current.respond_to?(name, include_private))
77
+ end
78
+
79
+ private
80
+
81
+ # Pass messages over to the current Project instance if we don't have an
82
+ # implementation.
83
+ def method_missing(name, *args, &b)
84
+ if current && current.respond_to?(name)
85
+ current.send(name, *args, &b)
86
+ else
87
+ super
88
+ end
89
+ end
90
+
91
+ # An attr_writer that, when given a String, creates a FileList with the
92
+ # given value.
93
+ def file_list_attr_writer(*names)
94
+ names.each do |name|
95
+ class_eval <<-end_ruby
96
+ undef #{name}=
97
+ def #{name}=(value)
98
+ value = FileList[value.to_str] if value.respond_to?(:to_str)
99
+ @#{name} = value
100
+ end
101
+ end_ruby
102
+ end
103
+ end
104
+
105
+ # Track attributes as they're declares with attr_accessor.
106
+ def attr_accessor_with_tracking(*names) #:nodoc:
107
+ project_attributes.concat names
108
+ attr_accessor_without_tracking(*names)
109
+ end
110
+
111
+ public
112
+
113
+ send :alias_method, :attr_accessor_without_tracking, :attr_accessor
114
+ send :alias_method, :attr_accessor, :attr_accessor_with_tracking
115
+
116
+ end
117
+
118
+
119
+ # The project's name. This should be short but may include spaces and
120
+ # puncuation.
121
+ attr_accessor :name
122
+
123
+ alias :project_name :name
124
+
125
+ # The package name that should be used when building distributables or when a
126
+ # UNIX friendly name is otherwise needed. If this variable is not set, an
127
+ # attempt is made to derive it from the +name+ attribute.
128
+ attr_accessor :package_name
129
+
130
+ # The project version as a string. This is typically read from a source
131
+ # file.
132
+ attr_accessor :version
133
+
134
+ # The source file that's used to keep the project version. The file will
135
+ # typically include a +VERSION+ constant. With no explicit #version set,
136
+ # this file will be inspected.
137
+ #
138
+ # The #version_pattern attribute can be used to describe how to locate the
139
+ # version.
140
+ attr_accessor :version_file
141
+
142
+ # A string pattern that describes how the #version can be established from
143
+ # the #version_file. The default pattern is
144
+ # <code>/^\s*VERSION\s*=\s*['"]([\.\d]+)['"]/</code>
145
+ attr_accessor :version_pattern
146
+
147
+ # A short, one/two sentence, description of the project.
148
+ attr_accessor :summary
149
+
150
+ # A longer -- single paragraph -- description of the project.
151
+ attr_accessor :description
152
+
153
+ # The directory where programs live. This is detected automatically if a
154
+ # +bin+ or +scripts+ directory exists off the project root.
155
+ attr_accessor :bin_dir
156
+
157
+ # A list of programs (under bin_dir) to install. This is detected
158
+ # automatically.
159
+ attr_accessor :programs
160
+
161
+ # The directory where tests live. Default is +tests+.
162
+ attr_accessor :test_dir
163
+
164
+ # By default, this attribute returns a FileList of all test files living
165
+ # under +test_dir+ that match +test_pattern+ but can be set explicitly if
166
+ # needed.
167
+ attr_accessor :tests
168
+
169
+ # A glob string used to find test cases. Default: '**/*_test.rb'
170
+ attr_accessor :test_pattern
171
+
172
+ # Directory where library files. Default: +lib+
173
+ attr_accessor :lib_dir
174
+
175
+ # A FileList of library files. Defaults to all .rb files under +lib_dir+
176
+ attr_accessor :lib_files
177
+
178
+ # The top-level documentation directory. Default: +doc+.
179
+ attr_accessor :doc_dir
180
+
181
+ # All documentation files. Default is to include everything under +doc_dir+
182
+ # except for API documentation.
183
+ attr_accessor :doc_files
184
+
185
+ # Additional files to include in generated API documentation
186
+ attr_accessor :rdoc_files
187
+
188
+ # The author's name and email address: "Mr Foo <foo@example.com>"
189
+ attr_accessor :author
190
+
191
+ # The author's name. If this is not set, an attempt is made to establish it
192
+ # from the +author+ attribute.
193
+ attr_accessor :author_name
194
+
195
+ # The author's email. If this is not set, an attempt is made to establish it
196
+ # from the +author+ attribute.
197
+ attr_accessor :author_email
198
+
199
+ # The project's homepage URL.
200
+ attr_accessor :project_url
201
+
202
+ # Directory where distributables are built.
203
+ attr_accessor :package_dir
204
+
205
+ # Any additional files to include in the package.
206
+ attr_accessor :extra_files
207
+
208
+ # Native extension files. Default: <tt>FileList['ext/**/*.{c,h,rb}']</tt>.
209
+ attr_accessor :extension_files
210
+
211
+ # An Array of gem dependencies. The #depends_on is the simplest way of getting
212
+ # new dependencies defined.
213
+ attr_accessor :dependencies
214
+
215
+ # The rubyforge project
216
+ attr_accessor :rubyforge_project
217
+
218
+ # A remote location where packages should published. This should be of the
219
+ # form host.domain:/path/to/dir
220
+ attr_accessor :remote_dist_location
221
+
222
+ # The shell command to run on the remote dist host that reindexes the gem
223
+ # repository. This is typically something like
224
+ # <code>'cd ~/www/gems && index_gem_repository.rb -v'</code>. If you don't
225
+ # maintain a gem repository, this is safe to ignore.
226
+ attr_accessor :index_gem_repository_command #:nodoc:
227
+
228
+ # The remote location where built documentation should be published. This
229
+ # should be of the form host.domain:/path/to/dir
230
+ attr_accessor :remote_doc_location
231
+
232
+ # The remote location where the git repo should be published. This should
233
+ # be of the form host.domain:/path/to/dir.git The local .git directory is copied
234
+ # to the destination directly.
235
+ attr_accessor :remote_branch_location
236
+
237
+ # Generate README file based on project information.
238
+ attr_accessor :generate_readme
239
+
240
+ file_list_attr_writer :tests, :lib_files, :doc_files, :extra_files, :rdoc_files,
241
+ :extension_files
242
+
243
+ def initialize(project_name, options={}, &b)
244
+ self.class.current ||= self
245
+ @name = project_name
246
+ @version = options.delete(:version)
247
+ @version_file = nil
248
+ @version_pattern = /^\s*VERSION\s*=\s*['"]([\.\d]+)['"]/
249
+ @summary, @description, @package_name = nil
250
+ @author, @author_email, @author_name = nil
251
+ @bin_dir, @programs = nil
252
+ @test_dir, @tests = nil
253
+ @test_pattern = 'test/**/*_test.rb'
254
+ @lib_dir = 'lib'
255
+ @lib_files = nil
256
+ @doc_dir = 'doc'
257
+ @doc_files = nil
258
+ @rdoc_dir = nil
259
+ @rdoc_files = Rake::FileList['{README,LICENSE,COPYING}*']
260
+ @package_dir = 'dist'
261
+ @extra_files = Rake::FileList['Rakefile', 'Change{Log,s}*', 'NEWS*', 'misc/*.rake']
262
+ @extension_files = nil
263
+ @project_url = nil
264
+ @dependencies = []
265
+ @remote_dist_location, @remote_doc_location, @remote_branch_location = nil
266
+ @index_gem_repository_command = nil
267
+ @generate_readme = false
268
+ @package_configurator = nil
269
+ @rdoc_configurator = nil
270
+ @test_configurator = nil
271
+ @rubyforge_project = nil
272
+ enhance(options, &b)
273
+ define_tasks
274
+ end
275
+
276
+ def enhance(options={})
277
+ options.each { |k,v| send("#{k}=", v) }
278
+ yield self if block_given?
279
+ end
280
+
281
+ undef :package_name, :description=, :bin_dir, :test_dir, :programs, :tests,
282
+ :lib_files, :doc_files
283
+
284
+ def package_name #:nodoc:
285
+ read_attr(:package_name) { name.downcase.gsub(/[:\s]+/, '-') }
286
+ end
287
+
288
+ undef version
289
+
290
+ def version #:nodoc:
291
+ read_attr(:version, true) { read_version_from_version_file }
292
+ end
293
+
294
+ # Read the version from version_file using the version_pattern.
295
+ def read_version_from_version_file #:nodoc:
296
+ if version_file
297
+ if match = File.read(version_file).match(version_pattern)
298
+ match[1]
299
+ else
300
+ fail "No version match %p in %p." % [ version_file, version_pattern ]
301
+ end
302
+ else
303
+ fail "No version or version_file specified."
304
+ end
305
+ end
306
+
307
+ # Writes the currently set version to the version file.
308
+ def update_version_and_write_to_version_file(version) #:nodoc:
309
+ contents = File.read(version_file)
310
+ if match = contents.match(version_pattern)
311
+ old_version_line = match[0]
312
+ new_version_line = old_version_line.sub(match[1], version)
313
+ contents.sub! old_version_line, new_version_line
314
+ File.open(version_file, 'wb') { |io| io.write(contents) }
315
+ else
316
+ fail "Project version not found in #{version_file} (pattern: %p)." % version_pattern
317
+ end
318
+ end
319
+
320
+ # The next version, calculated by incrementing the last version component by
321
+ # 1. For instance, the version after +0.2.9+ is +0.2.10+.
322
+ def next_version #:nodoc:
323
+ parts = version.split('.')
324
+ parts[-1] = (parts[-1].to_i + 1).to_s
325
+ parts.join('.')
326
+ end
327
+
328
+ def description=(value) #:nodoc:
329
+ @description =
330
+ if value.respond_to? :to_str
331
+ value.to_str.strip_leading_indentation.strip
332
+ else
333
+ value
334
+ end
335
+ end
336
+
337
+ def bin_dir #:nodoc:
338
+ read_attr(:bin_dir) { FileList['bin', 'scripts'].existing.first }
339
+ end
340
+
341
+ def test_dir #:nodoc:
342
+ read_attr(:test_dir) { FileList['test'].existing.first }
343
+ end
344
+
345
+ def programs #:nodoc:
346
+ read_attr(:programs, true) { bin_dir ? FileList["#{bin_dir}/*"] : [] }
347
+ end
348
+
349
+ def tests #:nodoc:
350
+ read_attr(:tests, true) { test_dir ? FileList[test_pattern] : [] }
351
+ end
352
+
353
+ def lib_files #:nodoc:
354
+ read_attr(:lib_files, true) { FileList["#{lib_dir}/**/*.rb"] }
355
+ end
356
+
357
+ def doc_files #:nodoc:
358
+ read_attr(:doc_files) {
359
+ FileList["doc/**/*"] - FileList["#{rdoc_dir}/**/*", rdoc_dir] }
360
+ end
361
+
362
+ def rdoc_dir #:nodoc:
363
+ read_attr(:rdoc_dir) { File.join(doc_dir, 'api') }
364
+ end
365
+
366
+ def extensions_dir
367
+ 'ext'
368
+ end
369
+
370
+ undef extension_files
371
+
372
+ def extension_files
373
+ read_attr(:extension_files) { FileList["#{extensions_dir}/**/*.{h,c,rb}"] }
374
+ end
375
+
376
+ undef author
377
+
378
+ def author #:nodoc:
379
+ read_attr(:author, true) {
380
+ if @author_name && @author_email
381
+ "#{@author_name} <#{@author_email}>"
382
+ elsif @author_name
383
+ @author_name
384
+ elsif @author_email
385
+ @author_email
386
+ end
387
+ }
388
+ end
389
+
390
+ undef :author_name
391
+
392
+ def author_name #:nodoc:
393
+ read_attr(:author_name) { author && author[/[^<]*/].strip }
394
+ end
395
+
396
+ undef :author_email
397
+
398
+ def author_email #:nodoc:
399
+ read_attr(:author_email) {
400
+ if author && author =~ /<(.*)>$/
401
+ $1
402
+ else
403
+ nil
404
+ end
405
+ }
406
+ end
407
+
408
+ # All files to be included in built packages. This includes +lib_files+,
409
+ # +tests+, +rdoc_files+, +programs+, and +extra_files+.
410
+ def package_files
411
+ (rdoc_files + lib_files + tests + doc_files +
412
+ programs + extra_files + extension_files).uniq
413
+ end
414
+
415
+ # The basename of the current distributable package.
416
+ def package_basename(extension='.gem')
417
+ [ package_name, version ].join('-') + extension
418
+ end
419
+
420
+ # The path from the project root to a package distributable.
421
+ def package_path(extension='.gem')
422
+ File.join(package_dir, package_basename(extension))
423
+ end
424
+
425
+ # A list of built package distributables for the current project version.
426
+ def packages
427
+ FileList[package_path('.*')]
428
+ end
429
+
430
+ # Declare that this project depends on the specified package.
431
+ def depends_on(package, *version)
432
+ dependencies << [ package, *version ]
433
+ end
434
+
435
+ def configure_package(&block)
436
+ @package_configurator = block
437
+ end
438
+
439
+ def configure_rdoc(&block)
440
+ @rdoc_configurator = block
441
+ end
442
+
443
+ def configure_tests(&block)
444
+ @test_configurator = block
445
+ end
446
+
447
+ public
448
+
449
+ # An Array of attribute names available for the project.
450
+ def project_attribute_array
451
+ self.class.project_attributes.collect do |name|
452
+ [ name.to_sym, send(name) ]
453
+ end
454
+ end
455
+
456
+ alias :to_a :project_attribute_array
457
+
458
+ # A Hash of attribute name to attribute values -- one for each project
459
+ # attribute.
460
+ def project_attribute_hash
461
+ to_a.inject({}) { |hash,(k,v)| hash[k] = v }
462
+ end
463
+
464
+ alias :to_hash :project_attribute_hash
465
+
466
+ public
467
+
468
+ # A Gem::Specification instance with values based on the attributes defined on
469
+ # the project
470
+ def gem_spec
471
+ Gem::Specification.new do |s|
472
+ s.name = package_name
473
+ s.version = version
474
+ s.platform = Gem::Platform::RUBY
475
+ s.summary = summary
476
+ s.description = description || summary
477
+ s.author = author_name
478
+ s.email = author_email
479
+ s.homepage = project_url
480
+ s.require_path = lib_dir
481
+ s.files = package_files
482
+ s.test_files = tests
483
+ s.bindir = bin_dir
484
+ s.executables = programs.map{|p| File.basename(p)}
485
+ s.extensions = FileList['ext/**/extconf.rb']
486
+ s.has_rdoc = true
487
+ s.extra_rdoc_files = rdoc_files
488
+ s.rdoc_options.concat(rdoc_options)
489
+ s.test_files = tests
490
+ s.rubyforge_project = rubyforge_project
491
+ dependencies.each { |args| s.add_dependency(*args) }
492
+ @package_configurator.call(s) if @package_configurator
493
+ end
494
+ end
495
+
496
+ private
497
+
498
+ # Read and return the instance variable with name if it is defined and is non
499
+ # nil. When the variable's value is a Proc, invoke it and return the
500
+ # result. When the variable's value is nil, yield to the block and return the
501
+ # result.
502
+ def read_attr(name, record=false)
503
+ result =
504
+ case value = instance_variable_get("@#{name}")
505
+ when nil
506
+ yield if block_given?
507
+ when Proc
508
+ value.to_proc.call(self)
509
+ else
510
+ value
511
+ end
512
+ instance_variable_set("@#{name}", result) if record
513
+ result
514
+ end
515
+
516
+ # Define Rake tasks for this project.
517
+ def define_tasks
518
+ private_methods.grep(/^define_(\w+)_tasks$/).each do |meth|
519
+ namespace_name = meth.match(/^define_(\w+)_tasks$/)[1]
520
+ send(meth)
521
+ end
522
+ end
523
+
524
+ # Project Tasks ===========================================================
525
+
526
+ def define_project_tasks
527
+ namespace :project do
528
+ desc "Show high level project information"
529
+ task :info do |t|
530
+ puts [ project_name, version ].compact.join('/')
531
+ puts summary if summary
532
+ puts "\n%s" % [ description ] if description
533
+ end
534
+
535
+ desc "Write project version to STDOUT"
536
+ task :version do |t|
537
+ puts version
538
+ end
539
+
540
+ if version_file
541
+ bump_version_to = ENV['VERSION'] || next_version
542
+ desc "Bump project version (to %s) in %s" % [ bump_version_to, version_file ]
543
+ task :revise => [ version_file ] do |t|
544
+ message = "bumping version to %s in %s" % [ bump_version_to, version_file ]
545
+ STDERR.puts message if verbose
546
+ update_version_and_write_to_version_file(bump_version_to)
547
+ end
548
+ else
549
+ task :bump
550
+ end
551
+
552
+ desc "Write project / package attributes to STDOUT"
553
+ task :attributes do |t|
554
+ puts project_attribute_array.collect { |k,v| "%s: %p" % [ k, v ] }.join("\n")
555
+ end
556
+ end
557
+ end
558
+
559
+ # Test Tasks ===============================================================
560
+
561
+ def define_test_tasks
562
+ desc "Run all tests under #{test_dir}"
563
+ Rake::TestTask.new :test do |t|
564
+ t.libs = [ lib_dir, test_dir ]
565
+ t.ruby_opts = [ '-rubygems' ]
566
+ t.warning = true
567
+ t.test_files = tests
568
+ t.verbose = false
569
+ @test_configurator.call(t) if @test_configurator
570
+ end
571
+ end
572
+
573
+ # Package Tasks ===========================================================
574
+
575
+ def define_package_tasks
576
+ @package_config =
577
+ Rake::GemPackageTask.new(gem_spec) do |p|
578
+ p.package_dir = package_dir
579
+ p.need_tar_gz = true
580
+ p.need_zip = true
581
+ end
582
+
583
+ Rake::Task[:package].comment = nil
584
+ Rake::Task[:repackage].comment = nil
585
+ Rake::Task[:clobber_package].comment = nil
586
+ Rake::Task[:gem].comment = nil
587
+
588
+ namespace :package do
589
+ desc "Build distributable packages under #{package_dir}"
590
+ task :build => :package
591
+
592
+ desc "Rebuild distributable packages..."
593
+ task :rebuild => :repackage
594
+
595
+ desc "Remove most recent package files"
596
+ task :clean => :clobber_package
597
+
598
+ desc "Dump package manifest to STDOUT"
599
+ task :manifest do |t|
600
+ puts package_files.sort.join("\n")
601
+ end
602
+
603
+ desc "Install #{package_basename} (requires sudo)"
604
+ task :install => package_path do |t|
605
+ sh "sudo gem install #{package_path}"
606
+ end
607
+
608
+ desc "Uninstall #{package_basename} (requires sudo)"
609
+ task :uninstall do |t|
610
+ sh "sudo gem uninstall #{package_name} --version #{version}"
611
+ end
612
+ end
613
+ end
614
+
615
+ # Doc Tasks ===============================================================
616
+
617
+ def define_doc_tasks
618
+ desc "Remove all generated documentation"
619
+ task 'doc:clean'
620
+
621
+ desc "Build all documentation under #{doc_dir}"
622
+ task 'doc:build'
623
+
624
+ desc "Rebuild all documentation ..."
625
+ task 'doc:rebuild'
626
+
627
+ task :clean => 'doc:clean'
628
+ task :build => 'doc:build'
629
+ task :rebuild => 'doc:rebuild'
630
+ task :doc => 'doc:build'
631
+ end
632
+
633
+ def rdoc_options
634
+ [
635
+ "--title", "#{project_name} API Documentation",
636
+ '--extension', 'rake=rb',
637
+ '--line-numbers',
638
+ '--inline-source',
639
+ '--tab-width=4'
640
+ ]
641
+ end
642
+
643
+ def define_rdoc_tasks
644
+ namespace :doc do
645
+ Rake::RDocTask.new do |r|
646
+ r.rdoc_dir = rdoc_dir
647
+ r.main = project_name if project_name =~ /::/
648
+ r.title = "#{project_name} API Documentation"
649
+ r.rdoc_files.add lib_files
650
+ r.options = rdoc_options
651
+ end
652
+ end
653
+
654
+ Rake::Task['doc:rdoc'].comment = nil
655
+ Rake::Task['doc:rerdoc'].comment = nil
656
+ Rake::Task['doc:clobber_rdoc'].comment = nil
657
+
658
+ task 'doc:api:rebuild' => 'doc:rerdoc'
659
+ task 'doc:api:clean' => 'doc:clobber_rdoc'
660
+
661
+ desc "Build API / RDoc under #{rdoc_dir}"
662
+ task 'doc:api' => 'doc:rdoc'
663
+
664
+ task 'doc:clean' => 'doc:api:clean'
665
+ task 'doc:build' => 'doc:api'
666
+ task 'doc:rebuild' => 'doc:api:rebuild'
667
+ end
668
+
669
+ # AsciiDoc Tasks ==========================================================
670
+
671
+ def asciidoc_available?
672
+ @asciidoc_available ||=
673
+ system 'asciidoc --version > /dev/null 2>&1'
674
+ end
675
+
676
+ # Attributes passed to asciidoc for use in documents.
677
+ def asciidoc_attributes
678
+ { 'author' => author_name,
679
+ 'email' => author_email,
680
+ 'project-name' => project_name,
681
+ 'package-name' => package_name,
682
+ 'package-version' => version,
683
+ 'package-description' => description,
684
+ 'package-summary' => summary,
685
+ 'project-url' => project_url
686
+ }.reject{|k,v| v.nil? }
687
+ end
688
+
689
+ # Defines tasks for building HTML documentation with AsciiDoc.
690
+ def define_asciidoc_tasks
691
+ if defined?(AsciiDocTasks) && File.exist?("#{doc_dir}/asciidoc.conf") && asciidoc_available?
692
+ man_pages = FileList["#{doc_dir}/*.[0-9].txt"]
693
+ articles = FileList["#{doc_dir}/*.txt"] - man_pages
694
+ desc "Build AsciiDoc under #{doc_dir}"
695
+ AsciiDocTasks.new('doc:asciidoc') do |t|
696
+ t.source_dir = doc_dir
697
+ t.source_files = articles
698
+ t.doc_type = :article
699
+ t.config_file = "#{doc_dir}/asciidoc.conf"
700
+ t.attributes = asciidoc_attributes
701
+ end
702
+ AsciiDocTasks.new('doc:asciidoc') do |t|
703
+ t.source_dir = doc_dir
704
+ t.source_files = man_pages
705
+ t.doc_type = :manpage
706
+ t.config_file = "#{doc_dir}/asciidoc.conf"
707
+ t.attributes = asciidoc_attributes
708
+ end
709
+ else
710
+ desc "Build AsciiDoc (disabled)"
711
+ task 'asciidoc'
712
+ task 'asciidoc:build'
713
+ task 'asciidoc:clean'
714
+ task 'asciidoc:rebuild'
715
+ end
716
+ task 'doc:build' => 'doc:asciidoc:build'
717
+ task 'doc:clean' => 'doc:asciidoc:clean'
718
+ task 'doc:rebuild' => 'doc:asciidoc:rebuild'
719
+ end
720
+
721
+
722
+ # Publishing ==============================================================
723
+
724
+ class RemoteLocation #:nodoc:
725
+ attr_reader :user, :host, :path
726
+ def initialize(location)
727
+ if location =~ /^([\w\.]+):(.+)$/
728
+ @user, @host, @path = ENV['USER'], $1, $2
729
+ elsif location =~ /^(\w+)@([\w\.]+):(.+)$/
730
+ @user, @host, @path = $1, $2, $3
731
+ else
732
+ raise ArgumentError, "Invalid remote location: %p" % location
733
+ end
734
+ end
735
+ def to_s
736
+ "#{user}@#{host}:#{path}"
737
+ end
738
+ def inspect
739
+ to_s.inspect
740
+ end
741
+ def self.[](location)
742
+ if location.respond_to?(:to_str)
743
+ new(location.to_str)
744
+ else
745
+ location
746
+ end
747
+ end
748
+ end
749
+
750
+ public
751
+
752
+ undef :remote_dist_location=
753
+
754
+ def remote_dist_location=(value) #:nodoc:
755
+ @remote_dist_location = RemoteLocation[value]
756
+ end
757
+
758
+ undef :remote_doc_location=
759
+
760
+ def remote_doc_location=(value) #:nodoc:
761
+ @remote_doc_location = RemoteLocation[value]
762
+ end
763
+
764
+ undef :remote_branch_location=
765
+
766
+ def remote_branch_location=(value) #:nodoc:
767
+ @remote_branch_location = RemoteLocation[value]
768
+ end
769
+
770
+ private
771
+
772
+ def remote_index_gem_repository_command #:nodoc:
773
+ if index_gem_repository_command
774
+ "ssh #{remote_dist_location.user}@#{remote_dist_location.host}" +
775
+ "'#{index_gem_repository_command}'"
776
+ end
777
+ end
778
+
779
+ def rsync_packages_command #:nodoc:
780
+ "rsync -aP #{packages.join(' ')} #{remote_dist_location}"
781
+ end
782
+
783
+ def publish_packages_command #:nodoc:
784
+ [ rsync_packages_command, remote_index_gem_repository_command ].compact.join(' && ')
785
+ end
786
+
787
+ def publish_doc_command
788
+ "rsync -azP #{doc_dir}/ #{remote_doc_location}"
789
+ end
790
+
791
+ def temporary_git_repo_dir
792
+ ".git-#{Process.pid}"
793
+ end
794
+
795
+ def publish_branch_command
796
+ [
797
+ "git repack -d",
798
+ "git clone --bare -l . #{temporary_git_repo_dir}",
799
+ "git --bare --git-dir=#{temporary_git_repo_dir} update-server-info",
800
+ "rsync -azP --delete --hard-links #{temporary_git_repo_dir}/ #{remote_branch_location}"
801
+ ].join(' && ')
802
+ end
803
+
804
+ def define_publish_tasks
805
+ desc 'Publish'
806
+ task 'publish'
807
+
808
+ if remote_dist_location
809
+ desc "Publish packages to #{remote_dist_location}"
810
+ task 'publish:packages' => 'package:build' do |t|
811
+ sh publish_packages_command, :verbose => true
812
+ end
813
+ desc 'packages'
814
+ task 'publish' => 'publish:packages'
815
+ end
816
+
817
+ if remote_branch_location && File.exist?('.git')
818
+ desc "Publish branch to #{remote_branch_location}"
819
+ task 'publish:branch' => '.git' do |t|
820
+ sh publish_branch_command, :verbose => true do |res,ok|
821
+ rm_rf temporary_git_repo_dir
822
+ fail "publish git repository failed." if ! ok
823
+ end
824
+ end
825
+ desc 'branch'
826
+ task 'publish' => 'publish:branch'
827
+ end
828
+
829
+ if remote_doc_location
830
+ desc "Publish doc to #{remote_doc_location}"
831
+ task 'publish:doc' => 'doc:build' do |t|
832
+ sh publish_doc_command, :verbose => true
833
+ end
834
+ desc 'doc'
835
+ task 'publish' => 'publish:doc'
836
+ end
837
+
838
+ end
839
+
840
+ # Tags ====================================================================
841
+
842
+ def define_tags_tasks
843
+
844
+ task :ctags => (lib_files + tests) do |t|
845
+ args = [
846
+ "--recurse",
847
+ "-f tags",
848
+ "--extra=+f",
849
+ "--links=yes",
850
+ "--tag-relative=yes",
851
+ "--totals=yes",
852
+ "--regex-ruby='/.*alias(_method)?[[:space:]]+:([[:alnum:]_=!?]+),?[[:space:]]+:([[:alnum:]_=!]+)/\\2/f/'"
853
+ ].join(' ')
854
+ sh 'ctags', *args
855
+ end
856
+
857
+ CLEAN.include [ 'tags' ]
858
+
859
+ task :ftags => FileList['Rakefile', '**/*.{r*,conf,c,h,ya?ml}'] do |t|
860
+ pattern = '.*(Rakefile|\.(rb|rhtml|rtxt|conf|c|h|ya?ml))'
861
+ command = [
862
+ "find",
863
+ "-regex '#{pattern.gsub(/[()|]/){|m| '\\' + m}}'",
864
+ "-type f",
865
+ "-printf '%f\t%p\t1\n'",
866
+ "| sort -f > #{t.name}"
867
+ ].join(' ')
868
+ sh command
869
+ end
870
+
871
+ CLEAN.include [ '.ftags' ]
872
+
873
+ desc "Generate tags file with ctags"
874
+ task :tags => [ :ctags, :ftags ]
875
+ end
876
+
877
+ end
878
+
879
+ class String
880
+
881
+ # The number of characters of leading indent. The line with the least amount
882
+ # of indent wins:
883
+ #
884
+ # text = <<-end
885
+ # this is line 1
886
+ # this is line 2
887
+ # this is line 3
888
+ # this is line 4
889
+ # end
890
+ # assert 2 == text.leading_indent
891
+ #
892
+ # This is used by the #strip_leading_indent method.
893
+ def leading_indentation_length
894
+ infinity = 1.0 / 0.0
895
+ inject infinity do |indent,line|
896
+ case line
897
+ when /^[^\s]/
898
+ # return immediately on first non-indented line
899
+ return 0
900
+ when /^\s+$/
901
+ # ignore lines that are all whitespace
902
+ next indent
903
+ else
904
+ this = line[/^[\t ]+/].length
905
+ this < indent ? this : indent
906
+ end
907
+ end
908
+ end
909
+
910
+ # Removes the same amount of leading space from each line in the string. The
911
+ # number of spaces removed is based on the #leading_indent_length.
912
+ #
913
+ # This is most useful for reformatting inline strings created with here docs.
914
+ def strip_leading_indentation
915
+ if (indent = leading_indentation_length) > 0
916
+ collect { |line| line =~ /^[\s]+[^\s]/ ? line[indent..-1] : line }.join
917
+ else
918
+ self
919
+ end
920
+ end
921
+
922
+ end