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.
@@ -0,0 +1,192 @@
1
+ = Cartage by Kinetic Cafe
2
+
3
+ code :: https://github.com/KineticCafe/cartage/
4
+ issues :: https://github.com/KineticCafe/cartage/issues
5
+ docs :: http://www.rubydoc.info/github/KineticCafe/cartage/master
6
+ continuous integration :: {<img src="https://travis-ci.org/KineticCafe/cartage.svg?branch=master" alt="Build Status" />}[https://travis-ci.org/KineticCafe/cartage]
7
+
8
+ == Description
9
+
10
+ Cartage provides a plug-in based tool to reliably create a package for a
11
+ Bundler-based Ruby application that can be used in deployment with a
12
+ configuration tool like Ansible, Chef, Puppet, or Salt. The package is created
13
+ with its dependencies bundled in +vendor/bundle+, so it can be deployed in
14
+ environments with strict access control rules and without requiring development
15
+ tool access.
16
+
17
+ Cartage has learned its tricks from Heroku, Capistrano, and Hoe. From Hoe, it
18
+ learned to keep a manifest to control what is packaged (as well as its plug-in
19
+ system). From Heroku, it learned to keep a simple ignore file. From Capistrano,
20
+ it learned to mark the Git hashref as a file in its built package, and to
21
+ timestamp the packages.
22
+
23
+ Cartage follows a relatively simple set of steps when creating a package:
24
+
25
+ 1. Copy the application files to the work area. The application’s files are
26
+ specified in +Manifest.txt+ and filtered against the exclusion list
27
+ (+.cartignore+). If there is no +.cartignore+, try to use +.slugignore+. If
28
+ there is no +.slugignore+, Cartage will use a sensible default exclusion
29
+ list. To override the use of this exclusion list, an empty +.cartignore+
30
+ file must be present.
31
+
32
+ 2. The Git hashref is written to the work area (as +release_hashref+) and to
33
+ the package staging area.
34
+
35
+ 3. Files that have been modified are restored to pristine condition in the
36
+ work area. The source files are not touched. (This ensures that
37
+ +config/database.yml+, for example, will not be the version used by a
38
+ continuous integration system.)
39
+
40
+ 4. Bundler is fetched into the work area, and the bundle is installed into the
41
+ work area’s +vendor/bundle+ without the +development+ and +test+
42
+ environments. If a bundle cache is kept (by default, one is), the resulting
43
+ +vendor/bundle+ will be put into a bundle cache so that future bundle
44
+ installs are faster.
45
+
46
+ 5. A timestamped tarball is created from the contents of the work area. It can
47
+ then be copied to a more permanent or accessible location.
48
+
49
+ Cartage is extremely opinionated about its tools and environment:
50
+
51
+ * The packages are created with +tar+ and +bzip2+ using <tt>tar cfj</tt>.
52
+ * Cartage only understands +git+, which is used for creating
53
+ <tt>release_hashref</tt>s, +Manifest.txt+ creation and comparison, and even
54
+ default application name detection (from the name of the origin remote).
55
+
56
+ == Synopsis
57
+
58
+ # Build a package from the current machine, using the Manifest.txt.
59
+ cartage pack
60
+
61
+ # Create or update a Manifest.txt from the current repository.
62
+ cartage manifest
63
+ # Check the current Manifest against the files that should be there.
64
+ cartage check
65
+
66
+ # Create a .cartignore file for use.
67
+ cartage install-ignore
68
+ # Overwrite the current .cartignore with the default.
69
+ cartage install-ignore --force
70
+ # Merge the current .cartignore with the default. Merging automatically
71
+ # removes any comments.
72
+ cartage install-ignore --merge
73
+
74
+ == Install
75
+
76
+ Add cartage to your Gemfile:
77
+
78
+ gem 'cartage', '~> 1.0', groups: [ :development, :test ]
79
+
80
+ Or manually install:
81
+
82
+ % gem install cartage
83
+
84
+ == Cartage Plug-Ins
85
+
86
+ Cartage is extensible by plug-ins. This version of the plug-in system provides
87
+ one integration point, subcommands. Cartage is implemented with
88
+ {cmdparse}[http://cmdparse.gettalong.org/], and plug-ins implement a class that
89
+ performs the work (nested under the Cartage namespace) and one or more commands
90
+ that execute on the work. Plug-in files are discovered immediately relative to
91
+ the +cartage+ directory, and are registered on inheritance from
92
+ Cartage::Plugin. This plug-in would be found in <tt>'cartage/info.rb'</tt>.
93
+ Each plug-in will get a lazy-instantiation constructor added to Cartage itself
94
+ that provides a reference to the owning Cartage instance.
95
+
96
+ Below is an example of a Cartage plug-in to provide a <tt>cartage info</tt>
97
+ command. This command isn’t very useful, since most values used in Cartage
98
+ packaging are lazily resolved, but it demonstrates how simple a plug-in can be.
99
+
100
+ require 'cartage/plugin'
101
+ require 'cartage/command'
102
+
103
+ # An instance of this will be created lazily by calling Cartage#info.
104
+ class Cartage::Info < Cartage::Plugin
105
+ # It does not matter what this method is. It’s just a public instance
106
+ # method used by the command.
107
+ def run
108
+ @cartage.instance_variables.sort.each { |ivar|
109
+ puts "#{ivar}: #{@cartage.instance_variable_get(ivar)}"
110
+ }
111
+ end
112
+
113
+ # This will create a CmdParse command that responds to 'info' and takes
114
+ # no subcommands. If the plug-in provides a number of features, it is
115
+ # recommended that subcommands be used.
116
+ class InfoCommand < Cartage::Command
117
+ # The Cartage::Command objects are initialized with the cartage
118
+ # instance. Set the command name with the string here.
119
+ def initialize(cartage)
120
+ super(cartage, 'info', takes_commands: false)
121
+ short_desc('Shows configuration information about Cartage.')
122
+ end
123
+
124
+ # This is run by Cartage::Command#execute to make the command happen.
125
+ # An exception should be thrown on failure, as return values do not
126
+ # affect the status code of the application.
127
+ def perform
128
+ @cartage.info.run
129
+ end
130
+ end
131
+
132
+ # This returns the command(s) created by this plug-in.
133
+ def self.commands
134
+ [ Cartage::Info::InfoCommand ]
135
+ end
136
+ end
137
+
138
+ For a more comprehensive example, see the implementation of Manifest
139
+ (<tt>lib/cartage/manifest.rb</tt>) and its commands
140
+ (<tt>lib/cartage/manifest/commands.rb</tt>).
141
+
142
+ Future releases of Cartage will offer additional ways to extend Cartage.
143
+
144
+ == Alternate Projects
145
+
146
+ The closest project to Cartage is {pkgr}[https://github.com/crohr/pkgr]. Pkgr
147
+ will create a distribution package for Ubuntu (14.04 and 12.02, Debian 7),
148
+ CentOS 6, SLES 12, and Fedora 20.
149
+
150
+ Both Cartage and Pkgr provide isolated builds with all in-application
151
+ dependencies included.
152
+
153
+ Pkgr offers several advantages over Cartage:
154
+
155
+ * It supports more languages than just Ruby (suport for Go and Node.js are
156
+ confirmed and more are in progress). Cartage will probably have more language
157
+ support in the future, but it is not an immediate priority. For Ruby and
158
+ Node.js, the interpreters are included locally to the application.
159
+
160
+ * It creates a distribution package, meaning that you can just use +apt-get+ to
161
+ install or upgrade your package.
162
+
163
+ * It reuses Heroku buildpacks (this requires that your application behave a bit
164
+ more like a Heroku application, which may be a bit more
165
+
166
+ Cartage offers advantages over Pkgr:
167
+
168
+ * Cartage offers a plug-in based extension system. While the plug-in system is
169
+ currently limited to adding new +cartage+ commands, this is not its only
170
+ possible use. Existing plug-ins are +cartage-s3+ (upload the resulting
171
+ package to S3), +cartage-remote+ (build on a remote machine), and
172
+ +cartage-rack+ (provide a Rack application to read the +release_hashref+ over
173
+ an API call).
174
+
175
+ * Cartage makes it easier to integrate into a workflow translated from
176
+ Capistrano, as it essentially replaces the source control checkout stage.
177
+ This process makes it really easy to integrate into an Ansible playbook (as
178
+ we have done at Kinetic Cafe).
179
+
180
+ == Cartage Semantic Versioning
181
+
182
+ Cartage uses a {Semantic Versioning}[http://semver.org/] scheme with one
183
+ significant change:
184
+
185
+ * When PATCH is zero (+0+), it will be omitted from version references.
186
+
187
+ Additionally, the major version will generally be reserved for plug-in
188
+ infrastructure changes.
189
+
190
+ :include: Contributing.rdoc
191
+
192
+ :include: Licence.rdoc
@@ -0,0 +1,62 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require 'pathname'
6
+
7
+ Hoe.plugin :doofus
8
+ Hoe.plugin :email unless ENV['CI'] or ENV['TRAVIS']
9
+ Hoe.plugin :gemspec2
10
+ Hoe.plugin :git
11
+ Hoe.plugin :minitest
12
+ Hoe.plugin :rubygems
13
+ Hoe.plugin :travis
14
+
15
+ spec = Hoe.spec 'cartage' do
16
+ developer('Austin Ziegler', 'aziegler@kineticcafe.com')
17
+
18
+ self.history_file = 'History.rdoc'
19
+ self.readme_file = 'README.rdoc'
20
+ self.extra_rdoc_files = FileList['*.rdoc'].to_a
21
+
22
+ license 'MIT'
23
+
24
+ self.extra_deps << ['cmdparse', '~> 3.0']
25
+
26
+ self.extra_dev_deps << ['rake', '~> 10.0']
27
+ self.extra_dev_deps << ['hoe-doofus', '~> 1.0']
28
+ self.extra_dev_deps << ['hoe-gemspec2', '~> 1.1']
29
+ self.extra_dev_deps << ['hoe-git', '~> 1.5']
30
+ self.extra_dev_deps << ['hoe-geminabox', '~> 0.3']
31
+ self.extra_dev_deps << ['hoe-travis', '~> 1.2']
32
+ self.extra_dev_deps << ['minitest', '~> 5.4']
33
+ self.extra_dev_deps << ['minitest-autotest', '~> 1.0']
34
+ self.extra_dev_deps << ['minitest-bisect', '~> 1.2']
35
+ self.extra_dev_deps << ['minitest-focus', '~> 1.1']
36
+ self.extra_dev_deps << ['minitest-moar', '~> 0.0']
37
+ self.extra_dev_deps << ['minitest-pretty_diff', '~> 0.1']
38
+ self.extra_dev_deps << ['simplecov', '~> 0.7']
39
+ end
40
+
41
+ module Hoe::Publish
42
+ alias_method :original_make_rdoc_cmd, :make_rdoc_cmd
43
+
44
+ def make_rdoc_cmd(*extra_args) # :nodoc:
45
+ spec.extra_rdoc_files.reject! { |f| f == 'Manifest.txt' }
46
+ original_make_rdoc_cmd(*extra_args)
47
+ end
48
+ end
49
+
50
+ namespace :test do
51
+ task :coverage do
52
+ prelude = <<-EOS
53
+ require "simplecov"
54
+ SimpleCov.start("test_frameworks") { command_name "Minitest" }
55
+ gem "minitest"
56
+ EOS
57
+ spec.test_prelude = prelude.split($/).join('; ')
58
+ Rake::Task['test'].execute
59
+ end
60
+ end
61
+
62
+ # vim: syntax=ruby
@@ -0,0 +1,8 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ git_path = File.expand_path('../../.git', __FILE__)
4
+ $:.unshift(File.expand_path('../../lib', __FILE__)) if File.exist?(git_path)
5
+
6
+ require 'cartage/command'
7
+
8
+ exit Cartage.run(ARGV)
@@ -0,0 +1,172 @@
1
+ ---
2
+ # The name of the application. Optional, defaults to the basename of the origin
3
+ # Git URL. Overridden with <tt>cartage --name NAME</tt>.
4
+ # ---
5
+ # name: my-application
6
+
7
+ # The target path for the Cartage package. Optional and defaults to
8
+ # <tt>./tmp</tt>. Overridden with <tt>cartage --target PATH</tt>.
9
+ # ---
10
+ # target: tmp/cartage
11
+
12
+ # The root path of the application. Optional, defaults to the top of the Git
13
+ # repository (<tt>git rev-parse --show-cdup</tt>). Overridden with <tt>cartage
14
+ # --root-path ROOT_PATH</tt>.
15
+ # ---
16
+ # root_path: .
17
+
18
+ # The timestamp for the final package (which is
19
+ # <tt><em>name</em>-<em>timestamp</em></tt>). Optional, defaults to the current
20
+ # time in UTC. Overridden with <tt>cartage --timestamp TIMESTAMP</tt>. This
21
+ # value is *not* validated to be a time value when supplied.
22
+ # ---
23
+ # timestamp: not-a-timestamp
24
+
25
+ # The bundle cache path, where the package’s <tt>vendor-bundle.tar.bz2</tt>
26
+ # will be stored between builds. Optional, defaults to <tt>./tmp</tt>.
27
+ # Overridden with <tt>cartage --bundle-cache BUNDLE_CACHE</tt>.
28
+ # ---
29
+ # bundle_cache: tmp/cache
30
+
31
+ # The groups to exclude from <tt>bundle install</tt>. Optional, defaults to
32
+ # <tt>[ "development", "test"]</tt>. Overridden with <tt>cartage --without
33
+ # group1,group2</tt>.
34
+ # ---
35
+ # without:
36
+ # - development
37
+ # - test
38
+ # - other
39
+
40
+ # This section is for all plug-in configuration.
41
+ plugins:
42
+ # This section is for cartage-s3.
43
+ s3:
44
+ # The path to the cartage S3 bucket or directory (for another service that
45
+ # Fog::Storage supports). This has no default and is overridden with
46
+ # <tt>cartage s3 --path PATH</tt>.
47
+ # ---
48
+ # path: cartage-bucket
49
+
50
+ # The credentials dictionary passed to Fog::Storage for uploads. Each
51
+ # provider has different keys. If present, this will dictionary be used in
52
+ # preference to <tt>cartage s3</tt> flags <tt>--key-id</tt>,
53
+ # <tt>--secret-key</tt>, and <tt>--region</tt> as those work *only* with
54
+ # Amazon AWS S3.
55
+ credentials:
56
+ # The name of the provider.
57
+ # ---
58
+ # provider: AWS
59
+ # provider: Rackspace
60
+ # provider: Google
61
+
62
+ # The name of the access user. The name of this key varies per provider.
63
+ # ---
64
+ # aws_access_key_id: YOUR_AWS_ACCESS_KEY_ID
65
+ # rackspace_username: RACKSPACE_USERNAME
66
+ # google_storage_access_key_id: YOUR_SECRET_ACCESS_KEY_ID
67
+
68
+ # The authentication key. The name of this key varies per provider.
69
+ # ---
70
+ # aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY
71
+ # rackspace_api_key: RACKSPACE_API_KEY
72
+ # google_storage_secret_access_key: YOUR_SECRET_ACCESS_KEY
73
+
74
+ # Other keys can be provided as needed for the provider. AWS generally
75
+ # wants to know the +region+, and Rackspace needs to know if you are
76
+ # using its European datacentre.
77
+ # ---
78
+ # region: us-west-2
79
+ # rackspace_auth_url: lon.auth.api.rackspacecloud.com
80
+
81
+ # This section is for cartage-remote. cartage-remote will not work without a
82
+ # configuration section at this point because remote build scripts tend to be
83
+ # heavily customized. This may change in the future.
84
+ remote:
85
+ # The name of the build server. This field is required and can show up in
86
+ # two different formats.
87
+ # ---
88
+ # server: build@my-build-machine:2222
89
+ # server:
90
+ # user: build
91
+ # host: my-build-machine
92
+ # port: 2222
93
+
94
+ # The SSH key(s) used to connect to the server. Optional, and
95
+ # cartage-remote will use <tt>~/.ssh/*id_[rd]sa</tt> to find keys if this
96
+ # is not provided. Three formats are available.
97
+ #
98
+ # As a dictionary (first form), the private keys are embedded directly in
99
+ # the configuration file. As a string or an array, provides glob patterns
100
+ # to find key files on disk.
101
+ # ---
102
+ # keys:
103
+ # custom: |
104
+ # -----BEGIN RSA PRIVATE KEY-----
105
+ # ...
106
+ # -----END RSA PRIVATE KEY-----
107
+ # keys:
108
+ # - "config/secrets/*id_[rd]sa"
109
+ # - "~/.ssh/*id_[rd]sa"
110
+ # keys: "config/secrets/*id_[rd]sa"
111
+
112
+ # The build script that will be run on the remote server. This is optional
113
+ # with a reasonable default.
114
+ #
115
+ # #!/bin/bash
116
+ # set -e
117
+ # if [ -f Gemfile ]; then
118
+ # bundle install --path %<remote_bundle>s
119
+ # bundle exec cartage build \
120
+ # --config-file %<config_file>s \
121
+ # --target %<project_path>s
122
+ # else
123
+ # cartage build --config-file %<config_file>s \
124
+ # --target %<project_path>s
125
+ # fi
126
+ # ---
127
+ # build: |
128
+ # #!/bin/bash
129
+ # set -e
130
+ # if [ -f Gemfile ]; then
131
+ # bundle install --path %<remote_bundle>s
132
+ # bundle exec cartage s3 \
133
+ # --config-file %<config_file>s \
134
+ # --target %<project_path>s \
135
+ # --verbose
136
+ # else
137
+ # cartage build \
138
+ # --config-file %<config_file>s \
139
+ # --target %<project_path>s \
140
+ # --verbose
141
+ # fi
142
+
143
+ # The prebuild script that will be run on the local system. This is
144
+ # optional with a reasonable default.
145
+ #
146
+ # #!/bin/bash
147
+ # ssh-keyscan -H %<remote_host>s >> ~/.ssh/known_hosts
148
+ # ---
149
+ # prebuild: |
150
+ # #!/bin/bash
151
+ # ssh-keyscan -H %<remote_host>s >> ~/.ssh/known_hosts
152
+ # echo 'Prebuild complete'
153
+
154
+ # The postbuild script that will be run on the local system. This is
155
+ # optional with no default (no post-build actions).
156
+ # ---
157
+ # postbuild: |
158
+ # #!/bin/bash
159
+ # t="token=SLACK_TOKEN"
160
+ # c="channel=%23ci"
161
+ # d=MYDOMAIN
162
+ # u="https://${d}.slack.com/services/hooks/slackbot?${t}&${c}"
163
+ # curl --data "Build %<name>s-%<timestamp>s complete." ${u}
164
+
165
+ # This must not be indented for it to work.
166
+ <% if File.exist?('config/ansible/cartage.yml') %>
167
+ <%= File.read('config/ansible/cartage.yml') %>
168
+ <% end %>
169
+ <% if File.exist?('config/local/cartage.yml') %>
170
+ <%= File.read('config/local/cartage.yml') %>
171
+ <% end %>
172
+
@@ -0,0 +1,496 @@
1
+ require 'pathname'
2
+
3
+ # Cartage, a package builder.
4
+ class Cartage
5
+ VERSION = '1.0' #:nodoc:
6
+
7
+ # Plug-in commands that want to return a non-zero exit code should raise
8
+ # Cartage::QuietError.new(exitstatus).
9
+ class QuietError < StandardError
10
+ # Initialize the exception with +exitstatus+.
11
+ def initialize(exitstatus)
12
+ @exitstatus = exitstatus
13
+ end
14
+
15
+ # The exit status to be returned from this exception.
16
+ attr_reader :exitstatus
17
+ end
18
+
19
+ ##
20
+ # :attr_accessor: name
21
+ #
22
+ # The name of the package to create. Defaults to #default_name.
23
+
24
+ ##
25
+ # :method: default_name
26
+ #
27
+ # The default name of the package to be created, derived from the
28
+ # repository's Git URL.
29
+
30
+ ##
31
+ # :attr_accessor: root_path
32
+ #
33
+ # The root path for the application.
34
+
35
+ ##
36
+ # :method: default_root_path
37
+ #
38
+ # The default root path of the package, the top-level path of the Git
39
+ # repository.
40
+
41
+ ##
42
+ # :attr_accessor: target
43
+ #
44
+ # The target where the final Cartage package will be created.
45
+
46
+ ##
47
+ # :method: default_target
48
+ #
49
+ # The default target of the package, './tmp'.
50
+
51
+ ##
52
+ # :attr_accessor: timestamp
53
+ #
54
+ # The timestamp to be applied to the final package name.
55
+
56
+ ##
57
+ # :method: default_timestamp
58
+ #
59
+ # The default timestamp.
60
+
61
+ ##
62
+ # :attr_accessor: without_groups
63
+ #
64
+ # The environments to exclude from a bundle install.
65
+
66
+ ##
67
+ # :method: default_without_environments
68
+ #
69
+ # The default environments to exclude from a bundle install. The default is
70
+ # <tt>[ 'test', 'development' ]</tt>.
71
+
72
+ # Commands that normally output data will have that output suppressed.
73
+ attr_accessor :quiet
74
+
75
+ # Commands will be run with extra information.
76
+ attr_accessor :verbose
77
+
78
+ # The environment to be used when resolving configuration options from a
79
+ # configuration file. Cartage configuration files do not usually have
80
+ # environment partitions, but if they do, use this to select the environment
81
+ # partition.
82
+ attr_accessor :environment
83
+
84
+ # The Config object. If +with_environment+ is +true+ (the default) and
85
+ # #environment has been set, only the subset of the config matching
86
+ # #environment will be returned. If +with_environment+ is +false+, the full
87
+ # configuration will be returned.
88
+ #
89
+ # If +for_plugin+ is specified, only the subset of the config for the named
90
+ # plug-in will be returned.
91
+ #
92
+ # # Assume that #environment is 'development'.
93
+ # cartage.config
94
+ # # => base_config[:development]
95
+ # cartage.config(with_environment: false)
96
+ # # => base_config
97
+ # cartage.config(for_plugin: 's3')
98
+ # # => base_config[:development][:plugins][:s3]
99
+ # cartage.config(for_plugin: 's3', with_environment: false)
100
+ # # => base_config[:plugins][:s3]
101
+ def config(with_environment: true, for_plugin: nil)
102
+ env = environment.to_sym if with_environment && environment
103
+ plug = for_plugin.to_sym if for_plugin
104
+
105
+ cfg = if env
106
+ base_config[env]
107
+ else
108
+ base_config
109
+ end
110
+
111
+ cfg = cfg.plugins[plug] if plug && cfg.plugins
112
+ cfg
113
+ end
114
+
115
+ # The configuration file to read. This should not be used by clients.
116
+ attr_writer :load_config #:nodoc:
117
+
118
+ # The base config file. This should not be used by clients.
119
+ attr_accessor :base_config #:nodoc:
120
+
121
+ def initialize #:nodoc:
122
+ @load_config = :default
123
+ end
124
+
125
+ # Create the package.
126
+ def pack
127
+ timestamp # Force the timestamp to be set now.
128
+ prepare_work_area
129
+ save_release_hashref
130
+ fetch_bundler
131
+ install_vendor_bundle
132
+ restore_modified_files
133
+ build_final_tarball
134
+ end
135
+
136
+ # Set or return the bundle cache. The bundle cache is a compressed tarball of
137
+ # <tt>vendor/bundle</tt> in the working path.
138
+ #
139
+ # If it exists, it will be extracted into <tt>vendor/bundle</tt> before
140
+ # <tt>bundle install --deployment</tt> is run, and it will be created after
141
+ # the bundle has been installed. In this way, bundle installation works
142
+ # almost the same way as Capistrano’s shared bundle concept as long as the
143
+ # path to the bundle_cache has been set to a stable location.
144
+ #
145
+ # On Semaphore CI, this should be created relative to
146
+ # <tt>$SEMAPHORE_CACHE</tt>.
147
+ #
148
+ # cartage pack --bundle-cache $SEMAPHORE_CACHE
149
+ def bundle_cache(location = nil)
150
+ if location || !defined?(@bundle_cache)
151
+ @bundle_cache = Pathname(location || tmp_path).
152
+ join('vendor-bundle.tar.bz2').expand_path
153
+ end
154
+ @bundle_cache
155
+ end
156
+
157
+ # Return the release hashref. If the optional +save_to+ parameter is
158
+ # provided, the release hashref will be written to the specified file.
159
+ def release_hashref(save_to: nil)
160
+ @release_hashref ||= %x(git rev-parse HEAD).chomp
161
+ File.open(save_to, 'w') { |f| f.write @release_hashref } if save_to
162
+ @release_hashref
163
+ end
164
+
165
+ # The repository URL.
166
+ def repo_url
167
+ unless defined? @repo_url
168
+ origin = %x(git remote show -n origin)
169
+ match = origin.match(%r{\n\s+Fetch URL: (?<fetch>[^\n]+)})
170
+ @repo_url = match[:fetch]
171
+ end
172
+ @repo_url
173
+ end
174
+
175
+ # The path to the resulting package.
176
+ def final_tarball
177
+ @final_tarball ||= Pathname("#{final_name}.tar.bz2")
178
+ end
179
+
180
+ # The path to the resulting release_hashref.
181
+ def final_release_hashref
182
+ @final_release_hashref ||=
183
+ Pathname("#{final_name}-release-hashref.txt")
184
+ end
185
+
186
+ # A utility method for Cartage plug-ins to display a message only if verbose
187
+ # is on. Unless the command implemented by the plug-in is output only, this
188
+ # should be used.
189
+ def display(message)
190
+ __display(message)
191
+ end
192
+
193
+ private
194
+ def resolve_config!(*with_plugins)
195
+ return unless @load_config
196
+ @base_config = Cartage::Config.load(@load_config)
197
+
198
+ cfg = config
199
+ maybe_assign :target, cfg.target
200
+ maybe_assign :name, cfg.name
201
+ maybe_assign :root_path, cfg.root_path
202
+ maybe_assign :timestamp, cfg.timestamp
203
+ maybe_assign :without_groups, cfg.without
204
+
205
+ bundle_cache(cfg.bundle_cache) unless cfg.bundle_cache.nil? ||
206
+ cfg.bundle_cache.empty?
207
+
208
+ with_plugins.each do |name|
209
+ next unless respond_to? name
210
+ plugin = send(name)
211
+
212
+ next unless plugin
213
+ plugin.send(:resolve_config!, config(for_plugin: name))
214
+ end
215
+ end
216
+
217
+ def maybe_assign(name, value)
218
+ return if value.nil? || value.empty? ||
219
+ instance_variable_defined?(:"@#{name}")
220
+ send(:"#{name}=", value)
221
+ end
222
+
223
+ def __display(message, partial: false, verbose: verbose())
224
+ return unless verbose && !quiet
225
+
226
+ if partial
227
+ print message
228
+ else
229
+ puts message
230
+ end
231
+ end
232
+
233
+ def run(command)
234
+ display command.join(' ')
235
+
236
+ IO.popen(command + [ err: [ :child, :out ] ]) do |io|
237
+ __display(io.read(128), partial: true, verbose: true) until io.eof?
238
+ end
239
+
240
+ unless $?.success?
241
+ raise StandardError, "Error running '#{command.join(' ')}'"
242
+ end
243
+ end
244
+
245
+ def prepare_work_area
246
+ display "Preparing cartage work area..."
247
+
248
+ work_path.rmtree if work_path.exist?
249
+ work_path.mkpath
250
+
251
+ xf_status = cf_status = nil
252
+
253
+ manifest.resolve(root_path) do |file_list|
254
+ tar_cf_cmd = [
255
+ 'tar', 'cf', '-', '-C', parent, '-h', '-T', file_list
256
+ ].map(&:to_s)
257
+
258
+ tar_xf_cmd = [
259
+ 'tar', 'xf', '-', '-C', work_path, '--strip-components=1'
260
+ ].map(&:to_s)
261
+
262
+ IO.popen(tar_cf_cmd) do |cf|
263
+ IO.popen(tar_xf_cmd, 'w') do |xf|
264
+ xf.write cf.read
265
+ end
266
+
267
+ unless $?.success?
268
+ raise StandardError, "Error running #{tar_xf_cmd.join(' ')}"
269
+ end
270
+ end
271
+
272
+ unless $?.success?
273
+ raise StandardError, "Error running #{tar_cf_cmd.join(' ')}"
274
+ end
275
+ end
276
+ end
277
+
278
+ def save_release_hashref
279
+ display 'Saving release hashref...'
280
+ release_hashref save_to: work_path.join('release_hashref')
281
+ release_hashref save_to: final_release_hashref
282
+ end
283
+
284
+ def extract_bundle_cache
285
+ run %W(tar xfj #{bundle_cache} -C #{work_path}) if bundle_cache.exist?
286
+ end
287
+
288
+ def install_vendor_bundle
289
+ extract_bundle_cache
290
+
291
+ Bundler.with_clean_env do
292
+ Dir.chdir(work_path) do
293
+ run %w(bundle install --jobs=4 --deployment --clean --without) +
294
+ without_groups
295
+ end
296
+ end
297
+
298
+ create_bundle_cache
299
+ end
300
+
301
+ def restore_modified_files
302
+ %x(git status -s).split($/).map(&:split).map(&:last).each { |file|
303
+ restore_modified_file file
304
+ }
305
+ end
306
+
307
+ def fetch_bundler
308
+ Dir.chdir(work_path) do
309
+ run %w(gem fetch bundler)
310
+ end
311
+ end
312
+
313
+ def build_final_tarball
314
+ run %W(tar cfj #{final_tarball} -C #{tmp_path} #{name})
315
+ end
316
+
317
+ def work_path
318
+ @work_path ||= tmp_path.join(name)
319
+ end
320
+
321
+ def clean
322
+ [ work_path ] + final
323
+ end
324
+
325
+ def final
326
+ [ final_tarball, final_release_hashref ]
327
+ end
328
+
329
+ def parent
330
+ @parent ||= root_path.parent
331
+ end
332
+
333
+ def restore_modified_file(filename)
334
+ command = [
335
+ 'git', 'show', "#{release_hashref}:#{filename}"
336
+ ]
337
+
338
+ IO.popen(command) do |show|
339
+ work_path.join(filename).open('w') { |f| f.write show.read }
340
+ end
341
+ end
342
+
343
+ def tmp_path
344
+ @tmp_path ||= root_path.join('tmp')
345
+ end
346
+
347
+ def final_name
348
+ @final_name ||= tmp_path.join("#{name}-#{timestamp}")
349
+ end
350
+
351
+ class << self
352
+ private
353
+
354
+ def lazy_accessor(sym, default: nil, setter: nil, &block)
355
+ ivar = :"@#{sym}"
356
+ wsym = :"#{sym}="
357
+ dsym = :"default_#{sym}"
358
+
359
+ if default.nil? && block.nil?
360
+ raise ArgumentError, "No default provided."
361
+ end
362
+
363
+ if setter && !setter.respond_to?(:call)
364
+ raise ArgumentError, "setter must be callable"
365
+ end
366
+
367
+ setter ||= ->(v) { v }
368
+
369
+ dblk = if default.respond_to?(:call)
370
+ default
371
+ elsif default.nil?
372
+ block
373
+ else
374
+ -> { default }
375
+ end
376
+
377
+ define_method(sym) do
378
+ instance_variable_get(ivar) || send(dsym)
379
+ end
380
+
381
+ define_method(wsym) do |value|
382
+ instance_variable_set(ivar, setter.call(value || send(dsym)))
383
+ end
384
+
385
+ define_method(dsym, &dblk)
386
+ end
387
+ end
388
+
389
+ lazy_accessor :name, default: -> { File.basename(repo_url, '.git') }
390
+ lazy_accessor :root_path, setter: ->(v) { Pathname(v).expand_path },
391
+ default: -> { Pathname(%x(git rev-parse --show-cdup).chomp).expand_path }
392
+ lazy_accessor :target, setter: ->(v) { Pathname(v) },
393
+ default: -> { Pathname('tmp') }
394
+ lazy_accessor :timestamp, default: -> {
395
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
396
+ }
397
+ lazy_accessor :without_groups, setter: ->(v) { Array(v) },
398
+ default: -> { %w(development test) }
399
+ lazy_accessor :base_config, default: -> { OpenStruct.new }
400
+ end
401
+
402
+ class << Cartage
403
+ # Run the Cartage command-line program.
404
+ def run(args) #:nodoc:
405
+ require_relative 'cartage/plugin'
406
+ Cartage::Plugin.load
407
+ Cartage::Plugin.decorate(Cartage)
408
+
409
+ cartage = Cartage.new
410
+
411
+ cli = CmdParse::CommandParser.new(handle_exceptions: true)
412
+ cli.main_options.program_name = 'cartage'
413
+ cli.main_options.version = Cartage::VERSION.split(/\./)
414
+ cli.main_options.banner = 'Manage releaseable packages.'
415
+
416
+ cli.global_options do |opts|
417
+ # opts.on('--[no-]quiet', 'Silence normal command output.') { |q|
418
+ # cartage.quiet = !!q
419
+ # }
420
+ opts.on('--[no-]verbose', 'Show verbose output.') { |v|
421
+ cartage.verbose = !!v
422
+ }
423
+ opts.on(
424
+ '-E', '--environment [ENVIRONMENT]', <<-desc
425
+ Set the environment to be used when necessary. If an environment name is not
426
+ provided, it will check the values of $RAILS_ENV and RACK_ENV. If neither is
427
+ set, this option is ignored.
428
+ desc
429
+ ) { |e| cartage.environment = e || ENV['RAILS_ENV'] || ENV['RACK_ENV'] }
430
+ opts.on(
431
+ '-C', '--[no-]config-file load_config', <<-desc
432
+ Configure Cartage from a default configuration file or a specified
433
+ configuration file.
434
+ desc
435
+ ) { |c| cartage.load_config = c }
436
+ end
437
+
438
+ cli.add_command(CmdParse::HelpCommand.new)
439
+ cli.add_command(CmdParse::VersionCommand.new)
440
+ cli.add_command(Cartage::PackCommand.new(cartage))
441
+
442
+ Cartage::Plugin.registered.each do |plugin|
443
+ if plugin.respond_to?(:commands)
444
+ Array(plugin.commands).flatten.each do |command|
445
+ registered_commands << command
446
+ end
447
+ end
448
+ end
449
+
450
+ registered_commands.uniq.each { |cmd| cli.add_command(cmd.new(cartage)) }
451
+ cli.parse
452
+ 0
453
+ rescue Cartage::QuietError => qe
454
+ qe.exitstatus
455
+ rescue StandardError => e
456
+ $stderr.puts "Error:\n " + e.message
457
+ $stderr.puts e.backtrace.join("\n") if cartage.verbose
458
+ 2
459
+ end
460
+
461
+ # Set options common to anything that builds a package (that is, it calls
462
+ # Cartage#pack).
463
+ def common_build_options(opts, cartage)
464
+ opts.on(
465
+ '-t', '--target PATH',
466
+ 'The build package will be placed in PATH, which defaults to \'tmp\'.'
467
+ ) { |t| cartage.target = t }
468
+ opts.on(
469
+ '-n', '--name NAME',
470
+ "Set the package name. Defaults to '#{cartage.default_name}'."
471
+ ) { |n| cartage.name = n }
472
+ opts.on(
473
+ '-r', '--root-path PATH',
474
+ 'Set the root path. Defaults to the repository root.'
475
+ ) { |r| cartage.root_path = r }
476
+ opts.on(
477
+ '--timestamp TIMESTAMP',
478
+ 'The timestamp used for the final package.'
479
+ ) { |t| cartage.timestamp = t }
480
+ opts.on(
481
+ '--bundle-cache PATH',
482
+ 'Set the bundle cache path.'
483
+ ) { |b| cartage.bundle_cache(b) }
484
+ opts.on(
485
+ '--without GROUP1,GROUP2', Array,
486
+ 'Set the groups to be excluded from bundle installation.',
487
+ ) { |w| cartage.without_environments = w }
488
+ end
489
+
490
+ private
491
+ def registered_commands
492
+ @registered_commands ||= []
493
+ end
494
+ end
495
+
496
+ require_relative 'cartage/config'