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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +65 -0
- data/.travis.yml +42 -0
- data/CHANGELOG.markdown +272 -0
- data/CONTRIBUTING.markdown +10 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +344 -0
- data/Vagrantfile +33 -0
- data/bin/image_optim +28 -0
- data/image_optim.gemspec +29 -0
- data/lib/image_optim.rb +228 -0
- data/lib/image_optim/bin_resolver.rb +144 -0
- data/lib/image_optim/bin_resolver/bin.rb +105 -0
- data/lib/image_optim/bin_resolver/comparable_condition.rb +60 -0
- data/lib/image_optim/bin_resolver/error.rb +6 -0
- data/lib/image_optim/bin_resolver/simple_version.rb +31 -0
- data/lib/image_optim/cmd.rb +49 -0
- data/lib/image_optim/config.rb +205 -0
- data/lib/image_optim/configuration_error.rb +3 -0
- data/lib/image_optim/handler.rb +57 -0
- data/lib/image_optim/hash_helpers.rb +45 -0
- data/lib/image_optim/image_meta.rb +25 -0
- data/lib/image_optim/image_path.rb +68 -0
- data/lib/image_optim/non_negative_integer_range.rb +11 -0
- data/lib/image_optim/option_definition.rb +32 -0
- data/lib/image_optim/option_helpers.rb +17 -0
- data/lib/image_optim/railtie.rb +38 -0
- data/lib/image_optim/runner.rb +139 -0
- data/lib/image_optim/runner/glob_helpers.rb +45 -0
- data/lib/image_optim/runner/option_parser.rb +227 -0
- data/lib/image_optim/space.rb +29 -0
- data/lib/image_optim/true_false_nil.rb +16 -0
- data/lib/image_optim/worker.rb +159 -0
- data/lib/image_optim/worker/advpng.rb +35 -0
- data/lib/image_optim/worker/class_methods.rb +91 -0
- data/lib/image_optim/worker/gifsicle.rb +63 -0
- data/lib/image_optim/worker/jhead.rb +43 -0
- data/lib/image_optim/worker/jpegoptim.rb +58 -0
- data/lib/image_optim/worker/jpegrecompress.rb +44 -0
- data/lib/image_optim/worker/jpegtran.rb +46 -0
- data/lib/image_optim/worker/optipng.rb +45 -0
- data/lib/image_optim/worker/pngcrush.rb +54 -0
- data/lib/image_optim/worker/pngout.rb +38 -0
- data/lib/image_optim/worker/pngquant.rb +51 -0
- data/lib/image_optim/worker/svgo.rb +32 -0
- data/script/template/jquery-2.1.3.min.js +4 -0
- data/script/template/sortable-0.6.0.min.js +2 -0
- data/script/template/worker_analysis.erb +254 -0
- data/script/update_worker_options_in_readme +60 -0
- data/script/worker_analysis +599 -0
- data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +37 -0
- data/spec/image_optim/bin_resolver/simple_version_spec.rb +57 -0
- data/spec/image_optim/bin_resolver_spec.rb +272 -0
- data/spec/image_optim/cmd_spec.rb +66 -0
- data/spec/image_optim/config_spec.rb +217 -0
- data/spec/image_optim/handler_spec.rb +95 -0
- data/spec/image_optim/hash_helpers_spec.rb +76 -0
- data/spec/image_optim/image_path_spec.rb +54 -0
- data/spec/image_optim/railtie_spec.rb +121 -0
- data/spec/image_optim/runner/glob_helpers_spec.rb +25 -0
- data/spec/image_optim/runner/option_parser_spec.rb +99 -0
- data/spec/image_optim/space_spec.rb +25 -0
- data/spec/image_optim/worker_spec.rb +192 -0
- data/spec/image_optim_spec.rb +242 -0
- data/spec/images/comparison.png +0 -0
- data/spec/images/decompressed.jpeg +0 -0
- data/spec/images/icecream.gif +0 -0
- data/spec/images/image.jpg +0 -0
- data/spec/images/invisiblepixels/generate +24 -0
- data/spec/images/invisiblepixels/image.png +0 -0
- data/spec/images/lena.jpg +0 -0
- data/spec/images/orient/0.jpg +0 -0
- data/spec/images/orient/1.jpg +0 -0
- data/spec/images/orient/2.jpg +0 -0
- data/spec/images/orient/3.jpg +0 -0
- data/spec/images/orient/4.jpg +0 -0
- data/spec/images/orient/5.jpg +0 -0
- data/spec/images/orient/6.jpg +0 -0
- data/spec/images/orient/7.jpg +0 -0
- data/spec/images/orient/8.jpg +0 -0
- data/spec/images/orient/generate +23 -0
- data/spec/images/orient/original.jpg +0 -0
- data/spec/images/quant/64.png +0 -0
- data/spec/images/quant/generate +25 -0
- data/spec/images/rails.png +0 -0
- data/spec/images/test.svg +3 -0
- data/spec/images/transparency1.png +0 -0
- data/spec/images/transparency2.png +0 -0
- data/spec/images/vergroessert.jpg +0 -0
- data/spec/spec_helper.rb +64 -0
- data/vendor/jpegrescan +143 -0
- 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,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
|