cartage 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,59 @@
1
+ require 'cartage'
2
+ require 'cmdparse'
3
+
4
+ class Cartage
5
+ # The base Cartage command object. This is used to provide commands with a
6
+ # reference to the Cartage instance (<tt>@cartage</tt>) that is running under
7
+ # the main command-line program.
8
+ #
9
+ # class Cartage::InfoCommand < Cartage::Command
10
+ # def initialize(cartage)
11
+ # super(cartage, 'info')
12
+ # end
13
+ #
14
+ # def perform
15
+ # puts 'info'
16
+ # end
17
+ # end
18
+ #
19
+ # Cartage::Command changes the class-based command protocol from CmdParse so
20
+ # that you must define #perform instead of #execute. This is so that certain
21
+ # common behaviours (such as common config resolution) can be performed for
22
+ # all commands.
23
+ class Command < ::CmdParse::Command
24
+ # Create a ::CmdParse::Command with the given name. Save a reference to the
25
+ # provided cartage object.
26
+ def initialize(cartage, name, takes_commands: false)
27
+ if self.instance_of?(Cartage::Command)
28
+ raise ArgumentError, 'Cartage::Command cannot be instantiated.'
29
+ end
30
+
31
+ super(name, takes_commands: takes_commands)
32
+
33
+ @cartage = cartage
34
+ end
35
+
36
+ # Run the command. Children must *not* implement this method.
37
+ def execute(*args)
38
+ @cartage.send(:resolve_config!, *Array(with_plugins))
39
+ perform(*args)
40
+ end
41
+
42
+ ##
43
+ # :method: perform
44
+ #
45
+ # Perform the action of the command. Children *must* implement this method.
46
+
47
+ # This optional method is implemented by a plug-in to instruct Cartage to
48
+ # resolve the plug-in configuration for the specified plug-ins. Specify the
49
+ # plug-ins to be resolved as an array of Symbols.
50
+ #
51
+ # Plug-ins resolve by overriding the private method
52
+ # Cartage::Plugin#resolve_config!.
53
+ def with_plugins
54
+ []
55
+ end
56
+ end
57
+ end
58
+
59
+ require_relative 'pack_command'
@@ -0,0 +1,193 @@
1
+ require 'ostruct'
2
+ require 'pathname'
3
+
4
+ class Cartage
5
+ # The Cartage configuration structure. The supported Cartage-wide
6
+ # configuration fields are:
7
+ #
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.
34
+ #
35
+ # == Loading Configuration
36
+ #
37
+ # When <tt>--config-file</tt> is specified, the configuration file will be
38
+ # loaded and parsed. If a filename is given, that file will be loaded. If a
39
+ # filename is not given, Cartage will look for the configuration in the
40
+ # following locations:
41
+ #
42
+ # * config/cartage.yml
43
+ # * ./cartage.yml
44
+ # * $HOME/.config/cartage.yml
45
+ # * $HOME/.cartage.yml
46
+ # * /etc/cartage.yml
47
+ #
48
+ # 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
96
+ class Config < OpenStruct
97
+ #:stopdoc:
98
+ DEFAULT_CONFIG_FILES = %w(
99
+ config/cartage.yml
100
+ ./cartage.yml
101
+ ~/.config/cartage.yml
102
+ ~/.cartage.yml
103
+ /etc/cartage.yml
104
+ )
105
+ #:startdoc:
106
+
107
+ class << self
108
+ # Load a Cartage configuration file.
109
+ def load(filename)
110
+ config_file = resolve_config_file(filename)
111
+ config = YAML.load(ERB.new(config_file.read, nil, '%<>-').result)
112
+ new(ostructify(config))
113
+ end
114
+
115
+ private
116
+
117
+ def resolve_config_file(filename)
118
+ return unless filename
119
+
120
+ files = if filename == :default
121
+ DEFAULT_CONFIG_FILES
122
+ else
123
+ [ filename ]
124
+ end
125
+
126
+ file = files.find { |f| Pathname(f).expand_path.exist? }
127
+
128
+ if file
129
+ Pathname(file).expand_path
130
+ 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
138
+ end
139
+ end
140
+
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)
150
+ case object
151
+ 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)
157
+ end
158
+
159
+ object
160
+ end
161
+ end
162
+
163
+ # Convert the entire Config structure to a hash recursively.
164
+ def to_h
165
+ hashify(self)
166
+ end
167
+
168
+ # Override the default #to_yaml implementation.
169
+ def to_yaml
170
+ to_h.to_yaml
171
+ end
172
+
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
+ }
180
+ end
181
+
182
+ def hashify_recursively(object)
183
+ case object
184
+ when ::Array
185
+ object.map! { |i| hashify_recursively(i) }
186
+ when ::OpenStruct, ::Hash
187
+ object = hashify(object)
188
+ end
189
+
190
+ object
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,218 @@
1
+ require 'tempfile'
2
+ # Manage and use the package manifest ('Manifest.txt') and the ignore file
3
+ # ('.cartignore').
4
+ class Cartage::Manifest < Cartage::Plugin
5
+ # This exception is raised if the package manifest is missing.
6
+ MissingError = Class.new(StandardError) do
7
+ def message
8
+ <<-exception
9
+ Cartage cannot create a package without a Manifest.txt file. You may generate
10
+ or update the Manifest.txt file with the following command:
11
+
12
+ exception
13
+ end
14
+ end
15
+
16
+ DIFF = if system('gdiff', __FILE__, __FILE__) #:nodoc:
17
+ 'gdiff'
18
+ else
19
+ 'diff'
20
+ end
21
+
22
+ def initialize(cartage) #:nodoc:
23
+ @cartage = cartage
24
+ end
25
+
26
+ # Resolve the Manifest to something that can be used by <tt>tar -T</tt>. The
27
+ # manifest should be relative to the files in the repository, as reported
28
+ # from <tt>git ls-files</tt>. +tar+ requires either full paths, or files
29
+ # relative to the directory you are packaging from (and we want relative
30
+ # files).
31
+ #
32
+ # This reads the manifest, prunes it with package ignores, and then writes it
33
+ # in a GNU tar compatible format as a tempfile. It does this by taking the
34
+ # expanded version of +path+, grabbing the basename, and inserting that in
35
+ # front of every line in the Manifest.
36
+ #
37
+ # If +path+ is not provided, Dir.pwd is used.
38
+ def resolve(path = nil, command: nil) # :yields: resolved manifest filename
39
+ raise MissingError unless manifest_file.exist?
40
+
41
+ data = strip_comments_and_empty_lines(manifest_file.readlines)
42
+ raise "Manifest.txt is empty." if data.empty?
43
+
44
+ path = Pathname(path || Dir.pwd).expand_path.basename
45
+ tmpfile = Tempfile.new('Manifest')
46
+
47
+ tmpfile.puts prune(data, with_slugignore: true).map { |line|
48
+ path.join(line).to_s
49
+ }.join("\n")
50
+
51
+ tmpfile.close
52
+
53
+ yield tmpfile.path
54
+ ensure
55
+ if tmpfile
56
+ tmpfile.close
57
+ tmpfile.unlink
58
+ end
59
+ end
60
+
61
+ # Generates Manifest.txt.
62
+ def generate
63
+ create_file_list(manifest_file)
64
+ end
65
+
66
+ # Checks Manifest.txt
67
+ def check
68
+ raise MissingError unless manifest_file.exist?
69
+ tmp = create_file_list('Manifest.tmp')
70
+ system(DIFF, '-du', manifest_file.basename.to_s, tmp.to_s)
71
+ $?.success?
72
+ ensure
73
+ tmp.unlink if tmp
74
+ end
75
+
76
+ # Installs the default .cartignore file.
77
+ def install_default_ignore(mode: nil)
78
+ mode = mode.to_s.downcase.strip
79
+ save = mode || !ignore_file.exist?
80
+
81
+ if mode == :merge
82
+ data = strip_comments_and_empty_lines(ignore_file.readlines)
83
+
84
+ if data.empty?
85
+ data = DEFAULT_IGNORE
86
+ else
87
+ data += strip_comments_and_empty_lines(DEFAULT_IGNORE.split($/))
88
+ data = data.uniq.join("\n")
89
+ end
90
+ else
91
+ data = DEFAULT_IGNORE
92
+ end
93
+
94
+ ignore_file.open('w') { |f| f.puts data } if save
95
+ end
96
+
97
+ private
98
+
99
+ def ignore_file
100
+ @ignore_file ||= @cartage.root_path.join('.cartignore')
101
+ end
102
+
103
+ def slugignore_file
104
+ @slugignore_file ||= @cartage.root_path.join('.slugignore')
105
+ end
106
+
107
+ def manifest_file
108
+ @manifest_file ||= @cartage.root_path.join('Manifest.txt')
109
+ end
110
+
111
+ def create_file_list(filename)
112
+ Pathname(filename).tap { |file|
113
+ file.open('w') { |f|
114
+ f.puts prune(%x(git ls-files).split.map(&:chomp)).sort.join("\n")
115
+ }
116
+ }
117
+ end
118
+
119
+ def ignore_patterns(with_slugignore: false)
120
+ pats = if ignore_file.exist?
121
+ ignore_file.readlines
122
+ elsif with_slugignore && slugignore_file.exist?
123
+ slugignore_file.readlines
124
+ else
125
+ DEFAULT_IGNORE.split($/)
126
+ end
127
+
128
+ pats = strip_comments_and_empty_lines(pats)
129
+
130
+ pats.map { |pat|
131
+ if pat =~ %r{/\z}
132
+ Regexp.new(%r{\A#{pat}})
133
+ elsif pat =~ %r{\A/[^*?]+\z}
134
+ Regexp.new(%r{\A#{pat.sub(%r{\A/}, '')}/})
135
+ else
136
+ pat
137
+ end
138
+ }.compact
139
+ end
140
+
141
+ def strip_comments_and_empty_lines(list)
142
+ list.map { |item|
143
+ item = item.chomp.gsub(/(?:^|[^\\])#.*\z/, '').strip
144
+ if item.empty?
145
+ nil
146
+ else
147
+ item
148
+ end
149
+ }.compact
150
+ end
151
+
152
+ def prune(files, with_slugignore: false)
153
+ exclusions = ignore_patterns(with_slugignore: with_slugignore)
154
+ files.reject { |file| prune?(file, exclusions) }
155
+ end
156
+
157
+ def prune?(file, exclusions = ignore_patterns())
158
+ exclusions.any? do |pat|
159
+ case pat
160
+ when /[*?]/
161
+ File.fnmatch?(pat, file, File::FNM_PATHNAME | File::FNM_EXTGLOB |
162
+ File::FNM_DOTMATCH)
163
+ when Regexp
164
+ file =~ pat
165
+ else
166
+ file == pat
167
+ end
168
+ end
169
+ end
170
+
171
+ DEFAULT_IGNORE = <<-'EOM' #:nodoc:
172
+ # Some of these are in .gitignore, but let’s remove these just in case they got
173
+ # checked in.
174
+
175
+ # Exact files to remove. Matches with ==.
176
+ .DS_Store
177
+ .autotest
178
+ .editorconfig
179
+ .env
180
+ .git-wtfrc
181
+ .gitignore
182
+ .local.vimrc
183
+ .lvimrc
184
+ .cartignore
185
+ .powenv
186
+ .rake_tasks~
187
+ .rspec
188
+ .rubocop.yml
189
+ .rvmrc
190
+ .semaphore-cache
191
+ .workenv
192
+ Guardfile
193
+ README.md
194
+ bin/build
195
+ bin/notify-project-board
196
+ bin/osx-bootstrap
197
+ bin/setup
198
+
199
+ # Patterns to remove. These have a *, **, or ? in them. Uses File.fnmatch with
200
+ # File::FNM_DOTMATCH and File::FNM_EXTGLOB.
201
+ *.rbc
202
+ .*.swp
203
+ **/.DS_Store
204
+
205
+ # Directories to remove. These should end with a slash. Matches as the regular
206
+ # expression %r{\A#{pattern}}.
207
+ db/seeds/development/
208
+ db/seeds/test/
209
+ # db/seeds/dit/
210
+ # db/seeds/staging/
211
+ log/
212
+ test/
213
+ tmp/
214
+ vendor/bundle/
215
+ EOM
216
+ end
217
+
218
+ require_relative 'manifest/commands'