cartage 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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'