cartage 1.0
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/.autotest +8 -0
- data/.gemtest +1 -0
- data/.minitest.rb +2 -0
- data/.travis.yml +36 -0
- data/Cartage.yml.rdoc +271 -0
- data/Contributing.rdoc +66 -0
- data/Gemfile +9 -0
- data/History.rdoc +5 -0
- data/Licence.rdoc +27 -0
- data/Manifest.txt +24 -0
- data/README.rdoc +192 -0
- data/Rakefile +62 -0
- data/bin/cartage +8 -0
- data/cartage.yml.sample +172 -0
- data/lib/cartage.rb +496 -0
- data/lib/cartage/command.rb +59 -0
- data/lib/cartage/config.rb +193 -0
- data/lib/cartage/manifest.rb +218 -0
- data/lib/cartage/manifest/commands.rb +106 -0
- data/lib/cartage/pack_command.rb +14 -0
- data/lib/cartage/plugin.rb +80 -0
- data/test/minitest_config.rb +72 -0
- data/test/test_cartage.rb +261 -0
- data/test/test_cartage_config.rb +47 -0
- metadata +348 -0
@@ -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'
|