lib/image_optim/config.rb
# frozen_string_literal: true
require 'image_optim/option_helpers'
require 'image_optim/configuration_error'
require 'image_optim/hash_helpers'
require 'image_optim/worker'
require 'image_optim/cmd'
require 'set'
require 'yaml'
class ImageOptim
# Read, merge and parse configuration
class Config
include OptionHelpers
# Global config path at `$XDG_CONFIG_HOME/image_optim.yml` (by default
# `~/.config/image_optim.yml`)
GLOBAL_PATH = File.join(ENV.fetch('XDG_CONFIG_HOME', '~/.config'), 'image_optim.yml')
# Local config path at `./.image_optim.yml`
LOCAL_PATH = './.image_optim.yml'
class << self
# Read options at path: expand path (warn on failure), return {} if file
# does not exist or is empty, read yaml, check if it is a Hash, deep
# symbolise keys
def read_options(path)
begin
full_path = File.expand_path(path)
rescue ArgumentError => e
warn "Can't expand path #{path}: #{e}"
return {}
end
return {} unless File.size?(full_path)
config = load_yaml_file(full_path)
unless config.is_a?(Hash)
fail "expected hash, got #{config.inspect}"
end
HashHelpers.deep_symbolise_keys(config)
rescue => e
warn "exception when reading #{full_path}: #{e}"
{}
end
def load_yaml_file(path)
if YAML.respond_to?(:safe_load_file)
YAML.safe_load_file(path, permitted_classes: [Range])
else
YAML.load_file(path)
end
end
end
# Merge config from files with passed options
# Config files are checked at `GLOBAL_PATH` and `LOCAL_PATH` unless
# overriden using `:config_paths`
def initialize(options)
config_paths = options.delete(:config_paths) || [GLOBAL_PATH, LOCAL_PATH]
config_paths = Array(config_paths)
to_merge = config_paths.map{ |path| self.class.read_options(path) }
to_merge << HashHelpers.deep_symbolise_keys(options)
@options = to_merge.reduce do |memo, hash|
HashHelpers.deep_merge(memo, hash)
end
@used = Set.new
end
# Gets value for key converted to symbol and mark option as used
def get!(key)
key = key.to_sym
@used << key
@options[key]
end
# Check if key is present
def key?(key)
key = key.to_sym
@options.key?(key)
end
# Fail unless all options were marked as used (directly or indirectly
# accessed using `get!`)
def assert_no_unused_options!
unknown_options = @options.reject{ |key, _value| @used.include?(key) }
return if unknown_options.empty?
fail ConfigurationError, "unknown options #{unknown_options.inspect}"
end
# Nice level:
# * `10` by default and for `nil` or `true`
# * `0` for `false`
# * otherwise convert to integer
def nice
nice = get!(:nice)
case nice
when true, nil
10
when false
0
else
nice.to_i
end
end
# Number of parallel threads:
# * `processor_count` by default and for `nil` or `true`
# * `1` for `false`
# * otherwise convert to integer
def threads
threads = get!(:threads)
case threads
when true, nil
processor_count
when false
1
else
threads.to_i
end
end
# Timeout in seconds for each image:
# * not set by default and for `nil`
# * otherwise converted to float
def timeout
timeout = get!(:timeout)
timeout ? timeout.to_f : nil
end
# Verbose mode, converted to boolean
def verbose
!!get!(:verbose)
end
# Using image_optim_pack:
# * `false` to disable
# * `nil` to use if available
# * everything else to require
def pack
pack = get!(:pack)
return false if pack == false
require 'image_optim/pack'
true
rescue LoadError => e
raise "Cannot load image_optim_pack: #{e}" if pack
false
end
# Skip missing workers, converted to boolean
def skip_missing_workers
if key?(:skip_missing_workers)
!!get!(:skip_missing_workers)
else
pack
end
end
# Allow lossy workers and optimizations, converted to boolean
def allow_lossy
!!get!(:allow_lossy)
end
def cache_dir
dir = get!(:cache_dir)
dir unless dir.nil? || dir.empty?
end
def cache_worker_digests
!!get!(:cache_worker_digests)
end
# Options for worker class by its `bin_sym`:
# * `Hash` passed as is
# * `{}` for `true` or `nil`
# * `false` for `false`
# * otherwise fail with `ConfigurationError`
def for_worker(klass)
worker_options = get!(klass.bin_sym)
case worker_options
when Hash
worker_options
when true, nil
{}
when false
{disable: true}
else
fail ConfigurationError, "Got #{worker_options.inspect} for " \
"#{klass.name} options"
end
end
# yaml dump without document beginning prefix `---`
def to_s
YAML.dump(HashHelpers.deep_stringify_keys(@options)).sub(/\A---\n/, '')
end
private
# http://stackoverflow.com/a/6420817
def processor_count
@processor_count ||= case host_os = RbConfig::CONFIG['host_os']
when /darwin9/
Cmd.capture 'hwprefs cpu_count'
when /darwin/
if (Cmd.capture 'which hwprefs') == ''
Cmd.capture 'sysctl -n hw.ncpu'
else
Cmd.capture 'hwprefs thread_count'
end
when /linux/
Cmd.capture 'grep -c processor /proc/cpuinfo'
when /freebsd/
Cmd.capture 'sysctl -n hw.ncpu'
when /mswin|mingw/
require 'win32ole'
query = 'select NumberOfLogicalProcessors from Win32_Processor'
result = WIN32OLE.connect('winmgmts://').ExecQuery(query)
result.to_enum.first.NumberOfLogicalProcessors
else
warn "Unknown architecture (#{host_os}) assuming one processor."
1
end.to_i
end
end
end