# frozen_string_literal: true
begin
require "psych"
rescue LoadError
nil # This has been intentionally suppressed.
end
require "ostruct"
require "pathname"
require "erb"
require "yaml"
class Cartage
# The Cartage configuration structure. The supported Cartage-wide
# configuration fields are:
#
# +name+:: The name of the application. Sets Cartage#name. Equivalent to
# --name. Optional, defaults to the basename of the origin
# repo URL. Overridden with cartage --name NAME.
# +target+:: The target path for the Cartage package. Optional, defaults to
# ./tmp. Overridden with cartage --target PATH.
# +root_path+:: The root path of the application. Optional, defaults to the
# top of the repository (git rev-parse --show-cdup).
# Overridden with cartage --root-path ROOT_PATH.
# +timestamp+:: Equivalent to --timestamp. The timestamp for the
# final package (which is
# name-timestamp). Optional,
# defaults to the current time in UTC. Overridden with
# cartage --timestamp TIMESTAMP. This value is *not*
# validated to be a time value when supplied.
# +compression+:: The type of compression to be used. Optional, defaults to
# 'bzip2'. Must be one of 'bzip2', 'gzip', or 'none'.
# Overridden with cartage --compression TYPE.
#
# This affects the compression and filenames of both the
# final package and the dependency cache.
# +quiet+:: Silence normal output. Optional, defaults false. Overridden with
# cartage --quiet.
# +verbose+:: Show verbose output. Optional, defaults false. Overridden with
# cartage --verbose.
# +disable_dependency_cache+:: Disable dependency caching. Optional, defaults
# false.
# +dependency_cache_path+:: The path where the dependency cache will be
# written (dependency-cache.tar.*) for use
# in successive builds. Optional, defaults to
# ./tmp. Overridden with cartage
# --dependency-cache-path PATH.
#
# On a CI system, this should be written somewhere
# that the CI system uses for build caching.
#
# == Commands and Plug-Ins
#
# Commands and plug-ins have access to configuration dictinoaries in the main
# Cartage configuration structure. Command configuration dictionaries are
# found under +commands+ and plug-in configuration dictionaries are found
# under +plugins+.
#
# +commands+:: This dictionary is for command-specific configuration. The
# keys are freeform and should be based on the *primary* name of
# the command (so the cartage pack command should use
# the key pack.)
# +plugins+:: This dictionary is for plug-in-specific configuration. See each
# plug-in for configuration options. The keys to the plug-ins are
# based on the plug-in name. cartage-bundler is available as
# Cartage::Bundler; the transformed plug-in name will be
# bundler.
#
# == Loading Configuration
#
# When --config-file is specified, the configuration file will be
# loaded and parsed. If a filename is given, that file will be loaded. If a
# filename is not given, Cartage will look for the configuration in the
# following locations:
#
# * ./config/cartage.yml
# * ./.cartage.yml
# * ./cartage.yml
#
# The contents of the configuration file are evaluated through ERB and then
# parsed from YAML and converted to nested OpenStruct objects.
class Config < OpenStruct
# :stopdoc:
unless defined?(DEFAULT_CONFIG_FILES)
DEFAULT_CONFIG_FILES = %w[
./config/cartage.yml
./cartage.yml
./.cartage.yml
].each(&:freeze).freeze
end
# :startdoc:
class << self
# Load a Cartage configuration file as specified by +filename+. If
# +filename+ is the special value :default, project-specific
# configuration files will be located.
def load(filename)
config_file = resolve_config_file(filename)
config = ::YAML.unsafe_load(ERB.new(config_file.read, trim_mode: "%-").result)
new(config)
end
# Read the contents of +filename+ if and only if it exists. For use in
# ERB configuration of Cartage to read local or Ansible-created files.
def import(filename)
File.read(filename) if File.exist?(filename)
end
private
def resolve_config_file(filename)
return unless filename
filename = nil if filename == :default
files = if filename
[filename]
else
DEFAULT_CONFIG_FILES
end
file = files.find { |f| Pathname(f).expand_path.exist? }
if file
Pathname(file).expand_path
elsif filename
fail ArgumentError, "Configuration file #{filename} does not exist."
else
StringIO.new("{}")
end
end
def ostructify(object)
case object
when ::Array
object.map { |e| ostructify(e) }
when ::OpenStruct, ::Hash
OpenStruct.new({}.tap { |h|
object.each_pair { |k, v| h[k] = ostructify(v) }
})
else
object
end
end
end
# Convert the entire Config structure to a hash recursively.
def to_h
hashify(self)
end
# Override the default #to_yaml implementation.
def to_yaml
to_h.to_yaml
end
def initialize(hash = nil) # :nodoc:
super(hash && self.class.send(:ostructify, hash))
self.plugins ||= OpenStruct.new
self.commands ||= OpenStruct.new
end
private
def hashify(object, convert_keys: :to_s)
case object
when ::Array
object.map { |e| hashify(e) }
when ::OpenStruct, ::Hash
{}.tap { |h|
object.each_pair { |k, v|
k = case convert_keys
when :to_s
k.to_s
when :to_sym
k.to_sym
else
k
end
h[k.to_s] = hashify(v)
}
}
else
object
end
end
end
end