openstreetmap-image_optim 0.21.0.1

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 (94) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rubocop.yml +65 -0
  4. data/.travis.yml +42 -0
  5. data/CHANGELOG.markdown +272 -0
  6. data/CONTRIBUTING.markdown +10 -0
  7. data/Gemfile +9 -0
  8. data/LICENSE.txt +20 -0
  9. data/README.markdown +344 -0
  10. data/Vagrantfile +33 -0
  11. data/bin/image_optim +28 -0
  12. data/image_optim.gemspec +29 -0
  13. data/lib/image_optim.rb +228 -0
  14. data/lib/image_optim/bin_resolver.rb +144 -0
  15. data/lib/image_optim/bin_resolver/bin.rb +105 -0
  16. data/lib/image_optim/bin_resolver/comparable_condition.rb +60 -0
  17. data/lib/image_optim/bin_resolver/error.rb +6 -0
  18. data/lib/image_optim/bin_resolver/simple_version.rb +31 -0
  19. data/lib/image_optim/cmd.rb +49 -0
  20. data/lib/image_optim/config.rb +205 -0
  21. data/lib/image_optim/configuration_error.rb +3 -0
  22. data/lib/image_optim/handler.rb +57 -0
  23. data/lib/image_optim/hash_helpers.rb +45 -0
  24. data/lib/image_optim/image_meta.rb +25 -0
  25. data/lib/image_optim/image_path.rb +68 -0
  26. data/lib/image_optim/non_negative_integer_range.rb +11 -0
  27. data/lib/image_optim/option_definition.rb +32 -0
  28. data/lib/image_optim/option_helpers.rb +17 -0
  29. data/lib/image_optim/railtie.rb +38 -0
  30. data/lib/image_optim/runner.rb +139 -0
  31. data/lib/image_optim/runner/glob_helpers.rb +45 -0
  32. data/lib/image_optim/runner/option_parser.rb +227 -0
  33. data/lib/image_optim/space.rb +29 -0
  34. data/lib/image_optim/true_false_nil.rb +16 -0
  35. data/lib/image_optim/worker.rb +159 -0
  36. data/lib/image_optim/worker/advpng.rb +35 -0
  37. data/lib/image_optim/worker/class_methods.rb +91 -0
  38. data/lib/image_optim/worker/gifsicle.rb +63 -0
  39. data/lib/image_optim/worker/jhead.rb +43 -0
  40. data/lib/image_optim/worker/jpegoptim.rb +58 -0
  41. data/lib/image_optim/worker/jpegrecompress.rb +44 -0
  42. data/lib/image_optim/worker/jpegtran.rb +46 -0
  43. data/lib/image_optim/worker/optipng.rb +45 -0
  44. data/lib/image_optim/worker/pngcrush.rb +54 -0
  45. data/lib/image_optim/worker/pngout.rb +38 -0
  46. data/lib/image_optim/worker/pngquant.rb +51 -0
  47. data/lib/image_optim/worker/svgo.rb +32 -0
  48. data/script/template/jquery-2.1.3.min.js +4 -0
  49. data/script/template/sortable-0.6.0.min.js +2 -0
  50. data/script/template/worker_analysis.erb +254 -0
  51. data/script/update_worker_options_in_readme +60 -0
  52. data/script/worker_analysis +599 -0
  53. data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +37 -0
  54. data/spec/image_optim/bin_resolver/simple_version_spec.rb +57 -0
  55. data/spec/image_optim/bin_resolver_spec.rb +272 -0
  56. data/spec/image_optim/cmd_spec.rb +66 -0
  57. data/spec/image_optim/config_spec.rb +217 -0
  58. data/spec/image_optim/handler_spec.rb +95 -0
  59. data/spec/image_optim/hash_helpers_spec.rb +76 -0
  60. data/spec/image_optim/image_path_spec.rb +54 -0
  61. data/spec/image_optim/railtie_spec.rb +121 -0
  62. data/spec/image_optim/runner/glob_helpers_spec.rb +25 -0
  63. data/spec/image_optim/runner/option_parser_spec.rb +99 -0
  64. data/spec/image_optim/space_spec.rb +25 -0
  65. data/spec/image_optim/worker_spec.rb +192 -0
  66. data/spec/image_optim_spec.rb +242 -0
  67. data/spec/images/comparison.png +0 -0
  68. data/spec/images/decompressed.jpeg +0 -0
  69. data/spec/images/icecream.gif +0 -0
  70. data/spec/images/image.jpg +0 -0
  71. data/spec/images/invisiblepixels/generate +24 -0
  72. data/spec/images/invisiblepixels/image.png +0 -0
  73. data/spec/images/lena.jpg +0 -0
  74. data/spec/images/orient/0.jpg +0 -0
  75. data/spec/images/orient/1.jpg +0 -0
  76. data/spec/images/orient/2.jpg +0 -0
  77. data/spec/images/orient/3.jpg +0 -0
  78. data/spec/images/orient/4.jpg +0 -0
  79. data/spec/images/orient/5.jpg +0 -0
  80. data/spec/images/orient/6.jpg +0 -0
  81. data/spec/images/orient/7.jpg +0 -0
  82. data/spec/images/orient/8.jpg +0 -0
  83. data/spec/images/orient/generate +23 -0
  84. data/spec/images/orient/original.jpg +0 -0
  85. data/spec/images/quant/64.png +0 -0
  86. data/spec/images/quant/generate +25 -0
  87. data/spec/images/rails.png +0 -0
  88. data/spec/images/test.svg +3 -0
  89. data/spec/images/transparency1.png +0 -0
  90. data/spec/images/transparency2.png +0 -0
  91. data/spec/images/vergroessert.jpg +0 -0
  92. data/spec/spec_helper.rb +64 -0
  93. data/vendor/jpegrescan +143 -0
  94. metadata +308 -0
@@ -0,0 +1,144 @@
1
+ require 'thread'
2
+ require 'fspath'
3
+ require 'image_optim/bin_resolver/error'
4
+ require 'image_optim/bin_resolver/bin'
5
+
6
+ class ImageOptim
7
+ # Handles resolving binaries and checking versions
8
+ #
9
+ # If there is an environment variable XXX_BIN when resolving xxx, then a
10
+ # symlink to binary will be created in a temporary directory which will be
11
+ # added to PATH
12
+ class BinResolver
13
+ class BinNotFound < Error; end
14
+
15
+ # Directory for symlinks to bins if XXX_BIN was used
16
+ attr_reader :dir
17
+
18
+ # Path to pack from image_optim_pack if used
19
+ attr_reader :pack_path
20
+
21
+ def initialize(image_optim)
22
+ @image_optim = image_optim
23
+ @bins = {}
24
+ @lock = Mutex.new
25
+ init_pack
26
+ end
27
+
28
+ # Binary resolving: create symlink if there is XXX_BIN environment variable,
29
+ # build Bin with full path, check binary version
30
+ # Return Bin instance
31
+ def resolve!(name)
32
+ name = name.to_sym
33
+
34
+ resolving(name) do
35
+ path = symlink_custom_bin!(name) || full_path(name)
36
+ bin = Bin.new(name, path) if path
37
+
38
+ if bin && @image_optim.verbose
39
+ $stderr << "Resolved #{bin}\n"
40
+ end
41
+
42
+ @bins[name] = bin
43
+
44
+ bin.check! if bin
45
+ end
46
+
47
+ if @bins[name]
48
+ @bins[name].check_fail!
49
+ else
50
+ fail BinNotFound, "`#{name}` not found"
51
+ end
52
+
53
+ @bins[name]
54
+ end
55
+
56
+ # Path to vendor at root of image_optim
57
+ VENDOR_PATH = File.expand_path('../../../vendor', __FILE__)
58
+
59
+ # Prepand `dir` and append `VENDOR_PATH` to `PATH` from environment
60
+ def env_path
61
+ [
62
+ dir,
63
+ pack_path,
64
+ ENV['PATH'],
65
+ VENDOR_PATH,
66
+ ].compact.join(File::PATH_SEPARATOR)
67
+ end
68
+
69
+ # Collect resolving errors when running block over items of enumerable
70
+ def self.collect_errors(enumerable)
71
+ errors = []
72
+ enumerable.each do |item|
73
+ begin
74
+ yield item
75
+ rescue Error => e
76
+ errors << e
77
+ end
78
+ end
79
+ errors
80
+ end
81
+
82
+ private
83
+
84
+ def init_pack
85
+ return unless @image_optim.pack
86
+
87
+ @pack_path = if @image_optim.verbose
88
+ Pack.path do |message|
89
+ $stderr << "#{message}\n"
90
+ end
91
+ else
92
+ Pack.path
93
+ end
94
+ return if @pack_path
95
+
96
+ warn 'No pack for this OS and/or ARCH, check verbose output'
97
+ end
98
+
99
+ # Double-checked locking
100
+ def resolving(name)
101
+ return if @bins.include?(name)
102
+ @lock.synchronize do
103
+ yield unless @bins.include?(name)
104
+ end
105
+ end
106
+
107
+ # Check path in XXX_BIN to exist, be a file and be executable and symlink to
108
+ # dir as name
109
+ def symlink_custom_bin!(name)
110
+ env_name = "#{name}_bin".upcase
111
+ path = ENV[env_name]
112
+ return unless path
113
+ path = File.expand_path(path)
114
+ desc = "`#{path}` specified in #{env_name}"
115
+ fail "#{desc} doesn\'t exist" unless File.exist?(path)
116
+ fail "#{desc} is not a file" unless File.file?(path)
117
+ fail "#{desc} is not executable" unless File.executable?(path)
118
+ if @image_optim.verbose
119
+ $stderr << "Custom path for #{name} specified in #{env_name}: #{path}\n"
120
+ end
121
+ unless @dir
122
+ @dir = FSPath.temp_dir
123
+ at_exit{ FileUtils.remove_entry_secure @dir }
124
+ end
125
+ symlink = @dir / name
126
+ symlink.make_symlink(path)
127
+ path
128
+ end
129
+
130
+ # Return full path to bin or null
131
+ # based on http://stackoverflow.com/a/5471032/96823
132
+ def full_path(name)
133
+ # PATHEXT is needed only for windows
134
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
135
+ env_path.split(File::PATH_SEPARATOR).each do |dir|
136
+ exts.each do |ext|
137
+ path = File.expand_path("#{name}#{ext}", dir)
138
+ return path if File.file?(path) && File.executable?(path)
139
+ end
140
+ end
141
+ nil
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,105 @@
1
+ require 'image_optim/bin_resolver/error'
2
+ require 'image_optim/bin_resolver/simple_version'
3
+ require 'image_optim/bin_resolver/comparable_condition'
4
+ require 'image_optim/cmd'
5
+ require 'shellwords'
6
+
7
+ class ImageOptim
8
+ class BinResolver
9
+ # Holds bin name and path, gets version
10
+ class Bin
11
+ class UnknownVersion < Error; end
12
+ class BadVersion < Error; end
13
+
14
+ attr_reader :name, :path, :version
15
+ def initialize(name, path)
16
+ @name = name.to_sym
17
+ @path = path.to_s
18
+ @version = detect_version
19
+ end
20
+
21
+ def to_s
22
+ "#{name} #{version || '?'} at #{path}"
23
+ end
24
+
25
+ is = ComparableCondition.is
26
+
27
+ FAIL_CHECKS = [
28
+ [:pngcrush, is.between?('1.7.60', '1.7.65'), 'is known to produce '\
29
+ 'broken pngs'],
30
+ [:pngcrush, is == '1.7.80', 'loses one color in indexed images'],
31
+ [:pngquant, is < '2.0', 'is not supported'],
32
+ ]
33
+
34
+ WARN_CHECKS = [
35
+ [:advpng, is < '1.17', 'does not use zopfli'],
36
+ [:gifsicle, is < '1.85', 'does not support removing extension blocks'],
37
+ [:pngcrush, is < '1.7.38', 'does not have blacken flag'],
38
+ [:pngquant, is < '2.1', 'may be lossy even with quality `100-`'],
39
+ ]
40
+
41
+ # Fail if version will not work properly
42
+ def check_fail!
43
+ fail UnknownVersion, "didn't get version of #{self}" unless version
44
+
45
+ FAIL_CHECKS.each do |bin_name, matcher, message|
46
+ next unless bin_name == name
47
+ next unless matcher.match(version)
48
+ fail BadVersion, "#{self} (#{matcher}) #{message}"
49
+ end
50
+ end
51
+
52
+ # Run check_fail!, otherwise warn if version is known to misbehave
53
+ def check!
54
+ check_fail!
55
+
56
+ WARN_CHECKS.each do |bin_name, matcher, message|
57
+ next unless bin_name == name
58
+ next unless matcher.match(version)
59
+ warn "WARN: #{self} (#{matcher}) #{message}"
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ # Wrap version_string with SimpleVersion
66
+ def detect_version
67
+ str = version_string
68
+ str && SimpleVersion.new(str)
69
+ end
70
+
71
+ # Getting version of bin, will fail for an unknown name
72
+ def version_string
73
+ case name
74
+ when :advpng, :gifsicle, :jpegoptim, :optipng, :pngquant
75
+ capture("#{escaped_path} --version 2> /dev/null")[/\d+(\.\d+){1,}/]
76
+ when :svgo
77
+ capture("#{escaped_path} --version 2>&1")[/\d+(\.\d+){1,}/]
78
+ when :jhead, :'jpeg-recompress'
79
+ capture("#{escaped_path} -V 2> /dev/null")[/\d+(\.\d+){1,}/]
80
+ when :jpegtran
81
+ capture("#{escaped_path} -v - 2>&1")[/version (\d+\S*)/, 1]
82
+ when :pngcrush
83
+ capture("#{escaped_path} -version 2>&1")[/\d+(\.\d+){1,}/]
84
+ when :pngout
85
+ date_regexp = /[A-Z][a-z]{2} (?: |\d)\d \d{4}/
86
+ date_str = capture("#{escaped_path} 2>&1")[date_regexp]
87
+ Date.parse(date_str).strftime('%Y%m%d') if date_str
88
+ when :jpegrescan
89
+ # jpegrescan has no version so just check presence
90
+ path && '-'
91
+ else
92
+ fail "getting `#{name}` version is not defined"
93
+ end
94
+ end
95
+
96
+ def capture(cmd)
97
+ Cmd.capture(cmd)
98
+ end
99
+
100
+ def escaped_path
101
+ path.shellescape
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,60 @@
1
+ class ImageOptim
2
+ class BinResolver
3
+ # Allows to externalize conditions for an instance of Comparable to use in
4
+ # case statemens
5
+ #
6
+ # is = ComparableCondition.is
7
+ # case rand(100)
8
+ # when is < 10 then # ...
9
+ # when is.between?(13, 23) then # ...
10
+ # when is >= 90 then # ...
11
+ # end
12
+ class ComparableCondition
13
+ # Helper class for creating conditions using ComparableCondition.is
14
+ class Builder
15
+ Comparable.instance_methods.each do |method|
16
+ define_method method do |*args|
17
+ ComparableCondition.new(method, *args)
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.is
23
+ Builder.new
24
+ end
25
+
26
+ attr_reader :method, :args
27
+ def initialize(method, *args)
28
+ @method, @args = method.to_sym, args
29
+
30
+ case @method
31
+ when :between?
32
+ @args.length == 2 || argument_error!("`between?' expects 2 arguments")
33
+ when :<, :<=, :==, :>, :>=
34
+ @args.length == 1 || argument_error!("`#{method}' expects 1 argument")
35
+ else
36
+ argument_error! "Unknown method `#{method}'"
37
+ end
38
+ end
39
+
40
+ def ===(other)
41
+ other.send(@method, *@args)
42
+ end
43
+ alias_method :match, :===
44
+
45
+ def to_s
46
+ if @method == :between?
47
+ @args.join('..')
48
+ else
49
+ "#{@method} #{@args.first}"
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def argument_error!(message)
56
+ fail ArgumentError, message
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,6 @@
1
+ class ImageOptim
2
+ class BinResolver
3
+ # Base error during bin resolving
4
+ class Error < StandardError; end
5
+ end
6
+ end
@@ -0,0 +1,31 @@
1
+ class ImageOptim
2
+ class BinResolver
3
+ # Allows comparision of simple versions, only numbers separated by dots are
4
+ # taken into account
5
+ class SimpleVersion
6
+ include Comparable
7
+
8
+ # Numbers extracted from version string
9
+ attr_reader :parts
10
+
11
+ # Initialize with a string or an object convertible to string
12
+ #
13
+ # SimpleVersion.new('2.0.1') <=> SimpleVersion.new(2)
14
+ def initialize(str)
15
+ @str = String(str)
16
+ @parts = @str.split('.').map(&:to_i).reverse.drop_while(&:zero?).reverse
17
+ end
18
+
19
+ # Returns original version string
20
+ def to_s
21
+ @str
22
+ end
23
+
24
+ # Compare version parts of self with other
25
+ def <=>(other)
26
+ other = self.class.new(other) unless other.is_a?(self.class)
27
+ parts <=> other.parts
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,49 @@
1
+ require 'English'
2
+
3
+ class ImageOptim
4
+ # Helper for running commands
5
+ module Cmd
6
+ class << self
7
+ # Run using `system`
8
+ # Return success status
9
+ # Will raise SignalException if process was interrupted
10
+ def run(*args)
11
+ success = system(*args)
12
+
13
+ check_status!
14
+
15
+ success
16
+ end
17
+
18
+ # Run using backtick
19
+ # Return captured output
20
+ # Will raise SignalException if process was interrupted
21
+ def capture(cmd)
22
+ output = `#{cmd}`
23
+
24
+ check_status!
25
+
26
+ output
27
+ end
28
+
29
+ private
30
+
31
+ def check_status!
32
+ status = $CHILD_STATUS
33
+
34
+ return unless status.signaled?
35
+
36
+ # jruby incorrectly returns true for `signaled?` if process exits with
37
+ # non zero status. For following code
38
+ #
39
+ # `sh -c 'exit 66'`
40
+ # p [$?.signaled?, $?.exitstatus, $?.termsig]
41
+ #
42
+ # jruby outputs `[true, 66, 66]` instead of expected `[false, 66, nil]`
43
+ return if defined?(JRUBY_VERSION) && status.exitstatus == status.termsig
44
+
45
+ fail SignalException, status.termsig
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,205 @@
1
+ require 'image_optim/option_helpers'
2
+ require 'image_optim/configuration_error'
3
+ require 'image_optim/hash_helpers'
4
+ require 'image_optim/worker'
5
+ require 'image_optim/cmd'
6
+ require 'set'
7
+ require 'yaml'
8
+
9
+ class ImageOptim
10
+ # Read, merge and parse configuration
11
+ class Config
12
+ include OptionHelpers
13
+
14
+ # Global config path at `$XDG_CONFIG_HOME/image_optim.yml` (by default
15
+ # `~/.config/image_optim.yml`)
16
+ GLOBAL_PATH = begin
17
+ File.join(ENV['XDG_CONFIG_HOME'] || '~/.config', 'image_optim.yml')
18
+ end
19
+
20
+ # Local config path at `./.image_optim.yml`
21
+ LOCAL_PATH = './.image_optim.yml'
22
+
23
+ class << self
24
+ # Read options at path: expand path (warn on failure), return {} if file
25
+ # does not exist, read yaml, check if it is a Hash, deep symbolise keys
26
+ def read_options(path)
27
+ begin
28
+ full_path = File.expand_path(path)
29
+ rescue ArgumentError => e
30
+ warn "Can't expand path #{path}: #{e}"
31
+ return {}
32
+ end
33
+ return {} unless File.file?(full_path)
34
+ config = YAML.load_file(full_path)
35
+ unless config.is_a?(Hash)
36
+ fail "expected hash, got #{config.inspect}"
37
+ end
38
+ HashHelpers.deep_symbolise_keys(config)
39
+ rescue => e
40
+ warn "exception when reading #{full_path}: #{e}"
41
+ {}
42
+ end
43
+ end
44
+
45
+ # Merge config from files with passed options
46
+ # Config files are checked at `GLOBAL_PATH` and `LOCAL_PATH` unless
47
+ # overriden using `:config_paths`
48
+ def initialize(options)
49
+ config_paths = options.delete(:config_paths) || [GLOBAL_PATH, LOCAL_PATH]
50
+ config_paths = Array(config_paths)
51
+
52
+ to_merge = config_paths.map{ |path| self.class.read_options(path) }
53
+ to_merge << HashHelpers.deep_symbolise_keys(options)
54
+
55
+ @options = to_merge.reduce do |memo, hash|
56
+ HashHelpers.deep_merge(memo, hash)
57
+ end
58
+ @used = Set.new
59
+ end
60
+
61
+ # Gets value for key converted to symbol and mark option as used
62
+ def get!(key)
63
+ key = key.to_sym
64
+ @used << key
65
+ @options[key]
66
+ end
67
+
68
+ # Check if key is present
69
+ def key?(key)
70
+ key = key.to_sym
71
+ @options.key?(key)
72
+ end
73
+
74
+ # Fail unless all options were marked as used (directly or indirectly
75
+ # accessed using `get!`)
76
+ def assert_no_unused_options!
77
+ unknown_options = @options.reject{ |key, _value| @used.include?(key) }
78
+ return if unknown_options.empty?
79
+ fail ConfigurationError, "unknown options #{unknown_options.inspect}"
80
+ end
81
+
82
+ # Nice level:
83
+ # * `10` by default and for `nil` or `true`
84
+ # * `0` for `false`
85
+ # * otherwise convert to integer
86
+ def nice
87
+ nice = get!(:nice)
88
+
89
+ case nice
90
+ when true, nil
91
+ 10
92
+ when false
93
+ 0
94
+ else
95
+ nice.to_i
96
+ end
97
+ end
98
+
99
+ # Number of parallel threads:
100
+ # * `processor_count` by default and for `nil` or `true`
101
+ # * `1` for `false`
102
+ # * otherwise convert to integer
103
+ def threads
104
+ threads = get!(:threads)
105
+
106
+ case threads
107
+ when true, nil
108
+ processor_count
109
+ when false
110
+ 1
111
+ else
112
+ threads.to_i
113
+ end
114
+ end
115
+
116
+ # Verbose mode, converted to boolean
117
+ def verbose
118
+ !!get!(:verbose)
119
+ end
120
+
121
+ # Using image_optim_pack:
122
+ # * `false` to disable
123
+ # * `nil` to use if available
124
+ # * everything else to require
125
+ def pack
126
+ pack = get!(:pack)
127
+ return false if pack == false
128
+
129
+ require 'image_optim/pack'
130
+ true
131
+ rescue LoadError => e
132
+ raise "Cannot load image_optim_pack: #{e}" if pack
133
+ false
134
+ end
135
+
136
+ # Skip missing workers, converted to boolean
137
+ def skip_missing_workers
138
+ if key?(:skip_missing_workers)
139
+ !!get!(:skip_missing_workers)
140
+ else
141
+ pack
142
+ end
143
+ end
144
+
145
+ # Allow lossy workers and optimizations, converted to boolean
146
+ def allow_lossy
147
+ !!get!(:allow_lossy)
148
+ end
149
+
150
+ # Options for worker class by its `bin_sym`:
151
+ # * `Hash` passed as is
152
+ # * `{}` for `true` or `nil`
153
+ # * `false` for `false`
154
+ # * otherwise fail with `ConfigurationError`
155
+ def for_worker(klass)
156
+ worker_options = get!(klass.bin_sym)
157
+
158
+ case worker_options
159
+ when Hash
160
+ worker_options
161
+ when true, nil
162
+ {}
163
+ when false
164
+ {:disable => true}
165
+ else
166
+ fail ConfigurationError, "Got #{worker_options.inspect} for "\
167
+ "#{klass.name} options"
168
+ end
169
+ end
170
+
171
+ # yaml dump without document beginning prefix `---`
172
+ def to_s
173
+ YAML.dump(HashHelpers.deep_stringify_keys(@options)).sub(/\A---\n/, '')
174
+ end
175
+
176
+ private
177
+
178
+ # http://stackoverflow.com/a/6420817
179
+ def processor_count
180
+ @processor_count ||= case host_os = RbConfig::CONFIG['host_os']
181
+ when /darwin9/
182
+ Cmd.capture 'hwprefs cpu_count'
183
+ when /darwin/
184
+ if (Cmd.capture 'which hwprefs') != ''
185
+ Cmd.capture 'hwprefs thread_count'
186
+ else
187
+ Cmd.capture 'sysctl -n hw.ncpu'
188
+ end
189
+ when /linux/
190
+ Cmd.capture 'grep -c processor /proc/cpuinfo'
191
+ when /freebsd/
192
+ Cmd.capture 'sysctl -n hw.ncpu'
193
+ when /mswin|mingw/
194
+ require 'win32ole'
195
+ WIN32OLE.
196
+ connect('winmgmts://').
197
+ ExecQuery('select NumberOfLogicalProcessors from Win32_Processor').
198
+ to_enum.first.NumberOfLogicalProcessors
199
+ else
200
+ warn "Unknown architecture (#{host_os}) assuming one processor."
201
+ 1
202
+ end.to_i
203
+ end
204
+ end
205
+ end