cartage 1.2 → 2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ Cartage::CLI.extend do
4
+ desc 'Create a package with Cartage based on the Manifest.'
5
+ command %w(pack build) do |pack|
6
+ pack.desc 'Do not check the status of the Manifest file before packaging.'
7
+ pack.switch 'skip-check', negatable: false
8
+
9
+ pack.action do |_global, options, _args|
10
+ unless options['skip-check'] || cartage.manifest.check
11
+ puts
12
+ fail Cartage::CLI::CustomExit.new 'Manifest.txt is not up-to-date.',
13
+ $?.exitstatus
14
+ end
15
+ cartage.build_package
16
+ end
17
+ end
18
+ end
@@ -1,36 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'psych'
5
+ rescue LoadError
6
+ nil # This has been intentionally suppressed.
7
+ end
1
8
  require 'ostruct'
2
9
  require 'pathname'
10
+ require 'erb'
11
+ require 'yaml'
3
12
 
4
13
  class Cartage
5
14
  # The Cartage configuration structure. The supported Cartage-wide
6
15
  # configuration fields are:
7
16
  #
8
- # +target+:: The target where the final Cartage package will be created. Sets
9
- # Cartage#target. Equivalent to <tt>--target</tt>.
10
- # +name+:: The name of the package to create. Sets Cartage#name. Equivalent
11
- # to <tt>--name</tt>.
12
- # +root_path+:: The root path of the application. Sets Cartage#root_path.
13
- # Equivalent to <tt>--root-path</tt>.
14
- # +timestamp+:: The timestamp for the final package. Sets Cartage#timestamp.
15
- # Equivalent to <tt>--timestamp</tt>.
16
- # +bundle_cache+:: The bundle cache. Sets Cartage#bundle_cache. Equivalent to
17
- # <tt>--bundle-cache</tt>.
18
- # +without+:: Groups to exclude from bundle installation. Sets
19
- # Cartage#without_groups. Equivalent to <tt>--without</tt>.
20
- # This value should be provided as an array.
21
- # +plugins+:: A dictionary for plug-in configuration groups. See below for
22
- # more information.
23
- #
24
- # Cartage configuration is not typically partitioned by an environment label,
25
- # but can be. See the examples below for details.
26
- #
27
- # == Plug-Ins
28
- #
29
- # Plug-ins also keep configuration in the Cartage configuration structure,
30
- # but as dictionary (hash) structures under the +plugins+ field. Each plug-in
31
- # has its own key based on its name, so that the Manifest plug-in (if it had
32
- # storable configuration values) would keep its configuration in a +manifest+
33
- # key. See the examples below for details.
17
+ # +name+:: The name of the application. Sets Cartage#name. Equivalent to
18
+ # <tt>--name</tt>. Optional, defaults to the basename of the origin
19
+ # repo URL. Overridden with <tt>cartage --name NAME</tt>.
20
+ # +target+:: The target path for the Cartage package. Optional, defaults to
21
+ # <tt>./tmp</tt>. Overridden with <tt>cartage --target PATH</tt>.
22
+ # +root_path+:: The root path of the application. Optional, defaults to the
23
+ # top of the repository (<tt>git rev-parse --show-cdup</tt>).
24
+ # Overridden with <tt>cartage --root-path ROOT_PATH</tt>.
25
+ # +timestamp+:: Equivalent to <tt>--timestamp</tt>. The timestamp for the
26
+ # final package (which is
27
+ # <tt><em>name</em>-<em>timestamp</em></tt>). Optional,
28
+ # defaults to the current time in UTC. Overridden with
29
+ # <tt>cartage --timestamp TIMESTAMP</tt>. This value is *not*
30
+ # validated to be a time value when supplied.
31
+ # +compression+:: The type of compression to be used. Optional, defaults to
32
+ # 'bzip2'. Must be one of 'bzip2', 'gzip', or 'none'.
33
+ # Overridden with <tt>cartage --compression TYPE</tt>.
34
+ #
35
+ # This affects the compression and filenames of both the
36
+ # final package and the dependency cache.
37
+ # +quiet+:: Silence normal output. Optional, defaults false. Overridden with
38
+ # <tt>cartage --quiet</tt>.
39
+ # +verbose+:: Show verbose output. Optional, defaults false. Overridden with
40
+ # <tt>cartage --verbose</tt>.
41
+ # +disable_dependency_cache+:: Disable dependency caching. Optional, defaults
42
+ # false.
43
+ # +dependency_cache_path+:: The path where the dependency cache will be
44
+ # written (<tt>dependency-cache.tar.*</tt>) for use
45
+ # in successive builds. Optional, defaults to
46
+ # <tt>./tmp</tt>. Overridden with <tt>cartage
47
+ # --dependency-cache-path PATH</tt>.
48
+ #
49
+ # On a CI system, this should be written somewhere
50
+ # that the CI system uses for build caching.
51
+ #
52
+ # == Commands and Plug-Ins
53
+ #
54
+ # Commands and plug-ins have access to configuration dictinoaries in the main
55
+ # Cartage configuration structure. Command configuration dictionaries are
56
+ # found under +commands+ and plug-in configuration dictionaries are found
57
+ # under +plugins+.
58
+ #
59
+ # +commands+:: This dictionary is for command-specific configuration. The
60
+ # keys are freeform and should be based on the *primary* name of
61
+ # the command (so the <tt>cartage pack</tt> command should use
62
+ # the key <tt>pack</tt>.)
63
+ # +plugins+:: This dictionary is for plug-in-specific configuration. See each
64
+ # plug-in for configuration options. The keys to the plug-ins are
65
+ # based on the plug-in name. cartage-bundler is available as
66
+ # Cartage::Bundler; the transformed plug-in name will be
67
+ # <tt>bundler</tt>.
34
68
  #
35
69
  # == Loading Configuration
36
70
  #
@@ -39,77 +73,35 @@ class Cartage
39
73
  # filename is not given, Cartage will look for the configuration in the
40
74
  # following locations:
41
75
  #
42
- # * config/cartage.yml
76
+ # * ./config/cartage.yml
77
+ # * ./.cartage.yml
43
78
  # * ./cartage.yml
44
- # * $HOME/.config/cartage.yml
45
- # * $HOME/.cartage.yml
46
- # * /etc/cartage.yml
47
79
  #
48
80
  # The contents of the configuration file are evaluated through ERB and then
49
- # parsed from YAML and converted to nested OpenStruct objects. The basic
50
- # environment example below would look like:
51
- #
52
- # #<OpenStruct development=
53
- # #<OpenStruct without=["test", "development", "assets"]>
54
- # >
55
- #
56
- # == Examples
57
- #
58
- # Basic Cartage configuration:
59
- #
60
- # ---
61
- # without:
62
- # - test
63
- # - development
64
- # - assets
65
- #
66
- # With an environment set:
67
- #
68
- # ---
69
- # development:
70
- # without:
71
- # - test
72
- # - development
73
- # - assets
74
- #
75
- # With the Manifest plug-in (note: the Manifest plug-in does *not* have
76
- # configurable options; this is for example purposes only).
77
- #
78
- # ---
79
- # without:
80
- # - test
81
- # - development
82
- # - assets
83
- # manifest:
84
- # format: json
85
- #
86
- # With the Manifest plug-in and an environment:
87
- #
88
- # ---
89
- # development:
90
- # without:
91
- # - test
92
- # - development
93
- # - assets
94
- # manifest:
95
- # format: json
81
+ # parsed from YAML and converted to nested OpenStruct objects.
96
82
  class Config < OpenStruct
97
83
  #:stopdoc:
98
84
  DEFAULT_CONFIG_FILES = %w(
99
- config/cartage.yml
100
- ./cartage.yml
101
- ~/.config/cartage.yml
102
- ~/.cartage.yml
103
- /etc/cartage.yml
104
- )
85
+ ./config/cartage.yml
86
+ ./cartage.yml
87
+ ./.cartage.yml
88
+ ).each(&:freeze).freeze
105
89
  #:startdoc:
106
90
 
107
91
  class << self
108
- # Load a Cartage configuration file.
92
+ # Load a Cartage configuration file as specified by +filename+. If
93
+ # +filename+ is the special value <tt>:default</tt>, project-specific
94
+ # configuration files will be located.
109
95
  def load(filename)
110
96
  config_file = resolve_config_file(filename)
111
- config = YAML.load(ERB.new(config_file.read, nil, '%<>-').result)
112
- new(ostructify(config))
97
+ config = ::YAML.load(ERB.new(config_file.read, nil, '%<>-').result)
98
+ new(config)
99
+ end
100
+
101
+ # Read the contents of +filename+ if and only if it exists. For use in
102
+ # ERB configuration of Cartage to read local or Ansible-created files.
103
+ def import(filename)
104
+ File.read(filename) if File.exist?(filename)
113
105
  end
114
106
 
115
107
  private
@@ -117,46 +109,36 @@ class Cartage
117
109
  def resolve_config_file(filename)
118
110
  return unless filename
119
111
 
120
- files = if filename == :default
121
- DEFAULT_CONFIG_FILES
122
- else
112
+ filename = nil if filename == :default
113
+
114
+ files = if filename
123
115
  [ filename ]
116
+ else
117
+ DEFAULT_CONFIG_FILES
124
118
  end
125
119
 
126
- file = files.find { |f| Pathname(f).expand_path.exist? }
120
+ file = files.find { |f| Pathname(f).expand_path.exist? }
127
121
 
128
122
  if file
129
123
  Pathname(file).expand_path
124
+ elsif filename
125
+ fail ArgumentError, "Configuration file #{filename} does not exist."
130
126
  else
131
- message = if filename
132
- "Configuration file #{filename} does not exist."
133
- else
134
- "No default configuration file found."
135
- end
136
-
137
- raise ArgumentError, message
127
+ StringIO.new('{}')
138
128
  end
139
129
  end
140
130
 
141
- def ostructify(hash)
142
- hash = hash.dup
143
- hash.keys.each do |k|
144
- hash[k.to_sym] = ostructify_recursively(hash.delete(k))
145
- end
146
- OpenStruct.new(hash)
147
- end
148
-
149
- def ostructify_recursively(object)
131
+ def ostructify(object)
150
132
  case object
151
133
  when ::Array
152
- object.map! { |i| ostructify_recursively(i) }
153
- when ::OpenStruct
154
- object = ostructify(object.to_h)
155
- when ::Hash
156
- object = ostructify(object)
134
+ object.map { |e| ostructify(e) }
135
+ when ::OpenStruct, ::Hash
136
+ OpenStruct.new({}.tap { |h|
137
+ object.each_pair { |k, v| h[k] = ostructify(v) }
138
+ })
139
+ else
140
+ object
157
141
  end
158
-
159
- object
160
142
  end
161
143
  end
162
144
 
@@ -170,24 +152,35 @@ class Cartage
170
152
  to_h.to_yaml
171
153
  end
172
154
 
173
- private
174
- def hashify(ostruct)
175
- {}.tap { |hash|
176
- ostruct.each_pair do |k, v|
177
- hash[k.to_s] = hashify_recursively(v)
178
- end
179
- }
155
+ def initialize(hash = nil) # :nodoc:
156
+ super(hash && self.class.send(:ostructify, hash))
157
+ self.plugins ||= OpenStruct.new
158
+ self.commands ||= OpenStruct.new
180
159
  end
181
160
 
182
- def hashify_recursively(object)
161
+ private
162
+
163
+ def hashify(object, convert_keys: :to_s)
183
164
  case object
184
165
  when ::Array
185
- object.map! { |i| hashify_recursively(i) }
166
+ object.map { |e| hashify(e) }
186
167
  when ::OpenStruct, ::Hash
187
- object = hashify(object)
168
+ {}.tap { |h|
169
+ object.each_pair { |k, v|
170
+ k = case convert_keys
171
+ when :to_s
172
+ k.to_s
173
+ when :to_sym
174
+ k.to_sym
175
+ else
176
+ k
177
+ end
178
+ h[k.to_s] = hashify(v)
179
+ }
180
+ }
181
+ else
182
+ object
188
183
  end
189
-
190
- object
191
184
  end
192
185
  end
193
186
  end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ class Cartage
5
+ # Extensions for us to use to help define Cartage with its attr_readers with
6
+ # defaults and attr_writers with transforms.
7
+ module Core #:nodoc:
8
+ private
9
+
10
+ # Define an attr_reader with a memoized default value. The +default+ is
11
+ # required and can be provided either as a callable (such as a proc), a
12
+ # value, or as a block.
13
+ #
14
+ # Note that the default will be called if and only if the instance variable
15
+ # of the same name has not been set in any other way.
16
+ #
17
+ # ==== Example
18
+ #
19
+ # # Create :answer and :default_answer.
20
+ # attr_reader_with_default :answer, -> { 42 }
21
+ # # Does the same thing
22
+ # attr_reader_with_default :answer do
23
+ # 42
24
+ # end
25
+ # # Does the same thing
26
+ # attr_reader_with_default :answer, 42
27
+ def attr_reader_with_default(name, default = nil, &block)
28
+ fail ArgumentError, 'No default provided.' unless default || block
29
+ fail ArgumentError, 'Too many defaults provided.' if default && block
30
+
31
+ default_ivar = :"@default_#{name}"
32
+ default_name = :"default_#{name}"
33
+
34
+ ivar = :"@#{name}"
35
+ name = name.to_sym
36
+
37
+ define_method(name) do
38
+ if instance_variable_defined?(ivar)
39
+ instance_variable_get(ivar)
40
+ else
41
+ send(default_name)
42
+ end
43
+ end
44
+
45
+ dblk = if default.respond_to?(:call)
46
+ default
47
+ else
48
+ block || -> { default }
49
+ end
50
+
51
+ define_method(default_name) do
52
+ if instance_variable_defined?(default_ivar)
53
+ instance_variable_get(default_ivar)
54
+ else
55
+ instance_variable_set(default_ivar, instance_exec(&dblk))
56
+ end
57
+ end
58
+ end
59
+
60
+ # Define an attr_writer with a transform that transforms the provided
61
+ # value. Conceptually, this is the same as defining an assignment method
62
+ # that performs the transform on the provided value.
63
+ #
64
+ # The +transform+ may be provided as a callable (such as a proc), an object
65
+ # that responds to #to_proc (such as a Symbol), or a block.
66
+ def attr_writer_with_transform(name, transform = nil, &block)
67
+ fail ArgumentError, 'No transform provided.' unless transform || block
68
+ fail ArgumentError, 'Too many transforms provided.' if transform && block
69
+
70
+ tblk = if transform.respond_to?(:call)
71
+ transform
72
+ elsif transform.respond_to?(:to_proc)
73
+ transform.to_proc
74
+ elsif block
75
+ block
76
+ else
77
+ fail ArgumentError, 'Transform is not callable.'
78
+ end
79
+
80
+ define_method(:"#{name}=") do |v|
81
+ instance_variable_set(:"@#{name}", tblk.call(v))
82
+ end
83
+ end
84
+
85
+ # Define an attr_accessor with a default attr_reader. The default is
86
+ # required and must be provided either using the +default+ parameter or a
87
+ # block.
88
+ #
89
+ # Optionally, a +transform+ block can be used to provide a transformation
90
+ # executed on assignment. (This will call #attr_writer_with_transform if
91
+ # provided, #attr_writer if not.)
92
+ #
93
+ # ==== Example
94
+ #
95
+ # # Creates :name, :name=, :default_name methods.
96
+ # attr_accessor_with_default :name do
97
+ # File.basename(repo_url, '.git')
98
+ # end
99
+ def attr_accessor_with_default(name, default: nil, transform: nil, &block)
100
+ attr_reader_with_default name, default || block
101
+ if transform
102
+ attr_writer_with_transform name, transform
103
+ else
104
+ attr_writer name
105
+ end
106
+ end
107
+ end
108
+
109
+ extend Core
110
+ end
111
+
112
+ require_relative 'backport'
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gli'
4
+
5
+ ##
6
+ module Cartage::CLIOptionsSupport #:nodoc:
7
+ # Clears defaults from a flag-set. By default, only clears symbolic defaults
8
+ # (e.g., <tt>:'default-value'</tt>.)
9
+ def clear_defaults_from(opts, flag_set: flags, symbol_defaults_only: true)
10
+ flag_set.each do |name, flag|
11
+ next unless flag.default_value
12
+ next if symbol_defaults_only && !flag.default_value.kind_of?(Symbol)
13
+ next unless opts[name] == flag.default_value
14
+
15
+ aliases = [ name, *flag.aliases ].compact
16
+ aliases += aliases.map(&:to_s)
17
+ aliases.each { |aka| opts[aka] = nil }
18
+ end
19
+ end
20
+ end
21
+
22
+ # Work around a bug with the RdocDocumentListener
23
+ module RdocDocumentListenerAppFix #:nodoc:
24
+ def initialize(_global_options, _options, _arguments, app)
25
+ super
26
+ @app = app
27
+ end
28
+ end
29
+
30
+ ##
31
+ class GLI::Commands::RdocDocumentListener #:nodoc:
32
+ prepend RdocDocumentListenerAppFix
33
+ end
34
+
35
+ ##
36
+ module GLI::App #:nodoc:
37
+ include Cartage::CLIOptionsSupport
38
+
39
+ # Indicate the parent GLI application.
40
+ def app
41
+ self
42
+ end
43
+ end
44
+
45
+ ##
46
+ class GLI::Command #:nodoc:
47
+ include Cartage::CLIOptionsSupport
48
+
49
+ # Indicate the parent GLI application.
50
+ def app
51
+ parent.app
52
+ end
53
+
54
+ # Mark the provided command as hidden from documentation.
55
+ def hide!
56
+ singleton_class.send(:define_method, :nodoc) { true }
57
+ end
58
+ end