date-performance 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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