cartage 1.2 → 2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,6 +3,6 @@
3
3
  git_path = File.expand_path('../../.git', __FILE__)
4
4
  $:.unshift(File.expand_path('../../lib', __FILE__)) if File.exist?(git_path)
5
5
 
6
- require 'cartage/command'
6
+ require 'cartage/cli'
7
7
 
8
- exit Cartage.run(ARGV)
8
+ exit Cartage::CLI.run(ARGV)
@@ -0,0 +1,137 @@
1
+ # cartage - Manage releaseable packages
2
+
3
+ Cartage provides a repeatable means to create a package for a server-side
4
+ application that can be used in deployment with a configuration tool like
5
+ Ansible, Chef, Puppet, or Salt.
6
+
7
+ ## Global Options
8
+
9
+ __`-C`, `--config-file FILE`__
10
+
11
+ Use the specified Cartage configuration file.
12
+
13
+ Use the specified configuration file. If not specified, the configuration is
14
+ found relative to the current working directory (assumed to be the project
15
+ root) at one of the following locations:
16
+
17
+ 1. config/cartage.yml
18
+ 2. cartage.yml
19
+ 3. .cartage.yml
20
+
21
+ __`--dependency-cache-path PATH`__
22
+
23
+ The path where vendored dependencies will be cached between builds.
24
+
25
+ Dependencies for deployable packages are vendored. To reduce network calls and
26
+ build time, Cartage will cache the vendored dependencies in a tarball
27
+ (dependency-cache.tar.bz2) in this path.
28
+
29
+ __`-n`, `--name NAME`__
30
+
31
+ The name of the package.
32
+
33
+ The name of the package is used with the timestamp to create the final package
34
+ name. Defaults to the last part of the repo URL.
35
+
36
+ __`-r`, `--root-path PATH`__
37
+
38
+ The package source root path.
39
+
40
+ Where the package is built from. Defaults to the root of the repository.
41
+
42
+ __`-t`, `--target PATH`__
43
+
44
+ The destination of the created package.
45
+
46
+ Where the final package will be written. Defaults to 'tmp'.
47
+
48
+ __`--timestamp TIMESTAMP`__
49
+
50
+ The timestamp used for building the package.
51
+
52
+ The timestamp is used with the name of the package is used to create the final
53
+ package name.
54
+
55
+ __`-T`, `--trace`__
56
+
57
+ Show the backtrace when an error occurs.
58
+
59
+ __`--disable-dependency-cache`__
60
+
61
+ Disable the use of vendor dependency caching.
62
+
63
+ __`-q`, `--[no-]quiet`__
64
+
65
+ Silence normal output.
66
+
67
+ __`-v`, `--[no-]verbose`__
68
+
69
+ Show verbose output.
70
+
71
+ __`--version`__
72
+
73
+ Display the program version.
74
+
75
+ ## Commands
76
+
77
+ ### `help COMMAND`
78
+
79
+ Shows a list of commands or help for one command.
80
+
81
+ Gets help for the application or its commands. Can also list the commands in a
82
+ way helpful to creating a bash-style completion function.
83
+
84
+ *__Options__*
85
+
86
+ __`-c`__
87
+
88
+ List commands one per line, to assist with shell completion.
89
+
90
+ ### `manifest`
91
+
92
+ Work with the Manifest.txt file
93
+
94
+ #### `manifest` Subcommands
95
+
96
+ ##### `manifest cartignore`
97
+
98
+ Install or update the .cartignore file
99
+
100
+ *__Options__*
101
+
102
+ __`--mode MODE`__
103
+
104
+ Overwrite or merge an existing .cartignore. If specified, must be one of
105
+ `overwrite` or `merge`.
106
+
107
+ __`-f`, `--force`, `--overwrite`__
108
+
109
+ Overwrite an existing .cartignore (the same as `--mode overwrite`).
110
+
111
+ __`-m`, `--merge`__
112
+
113
+ Merge an existing .cartignore (the same as `--mode merge`).
114
+
115
+ ##### `manifest check`
116
+
117
+ Check the Manifest.txt file against the current repository. If no command is
118
+ provided, this will run.
119
+
120
+ ##### `manifest generate`, `manifest update`
121
+
122
+ Generate or update the Manifest.txt file.
123
+
124
+ ##### `manifest show`
125
+
126
+ Show the files that will be included in the package.
127
+
128
+ ### `pack`, `build`
129
+
130
+ Create a package with Cartage based on the Manifest.
131
+
132
+ *__Options__*
133
+
134
+ __`--skip-check`__
135
+
136
+ Skip checking the status of the Manifest before attempting to build the
137
+ package.
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  # The name of the application. Optional, defaults to the basename of the origin
3
- # Git URL. Overridden with <tt>cartage --name NAME</tt>.
3
+ # repo URL. Overridden with <tt>cartage --name NAME</tt>.
4
4
  # ---
5
5
  # name: my-application
6
6
 
7
- # The target path for the Cartage package. Optional and defaults to
7
+ # The target path for the Cartage package. Optional, defaults to
8
8
  # <tt>./tmp</tt>. Overridden with <tt>cartage --target PATH</tt>.
9
9
  # ---
10
10
  # target: tmp/cartage
11
11
 
12
- # The root path of the application. Optional, defaults to the top of the Git
12
+ # The root path of the application. Optional, defaults to the top of the
13
13
  # repository (<tt>git rev-parse --show-cdup</tt>). Overridden with <tt>cartage
14
14
  # --root-path ROOT_PATH</tt>.
15
15
  # ---
@@ -22,162 +22,52 @@
22
22
  # ---
23
23
  # timestamp: not-a-timestamp
24
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>.
25
+ # The type of compression to be used. Optional, defaults to 'bzip2'. Must be
26
+ # one of 'bzip2', 'gzip', or 'none'. Overridden with <tt>cartage --compression
27
+ # TYPE</tt>.
28
+ #
29
+ # This affects the compression of both the final package and the dependency
30
+ # cache.
28
31
  # ---
29
- # bundle_cache: tmp/cache
32
+ # compression: gzip
30
33
 
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
+ # Silence normal output. Optional, defaults false. Overridden with <tt>cartage
35
+ # --quiet</tt>.
34
36
  # ---
35
- # without:
36
- # - development
37
- # - test
38
- # - other
37
+ # quiet: true
39
38
 
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"
39
+ # Show verbose output. Optional, defaults false. Overridden with <tt>cartage
40
+ # --verbose</tt>.
41
+ # ---
42
+ # verbose: true
111
43
 
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
44
+ # Disable dependency caching. Optional, defaults false.
45
+ # ---
46
+ # disable_dependency_cache: true
47
+
48
+ # The path where the dependency cache will be written
49
+ # (<tt>dependency-cache.tar.*</tt>) for use in successive builds. Optional,
50
+ # defaults to <tt>./tmp</tt>. Overridden with <tt>cartage
51
+ # --dependency-cache-path PATH</tt>.
52
+ #
53
+ # On a CI system, this should be written somewhere that the CI system uses for
54
+ # build caching.
55
+ # ---
56
+ # dependency_cache_path: <%= ENV['SEMAPHORE_CACHE'] %>
142
57
 
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'
58
+ # This dictionary is for command-specific configuration. As of this writing,
59
+ # none of the commands provided by default or in existing plug-ins have
60
+ # command-specific configuration. The keys are freeform and should be based on
61
+ # the *primary* name of the command (so the <tt>cartage pack</tt> command
62
+ # should use the key <tt>pack</tt>.)
63
+ commands: {}
153
64
 
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
- # This script will be passed the stage identifier as $1 (valid values are
158
- # config, ssh_config, prebuild, remote_clone, remote_build, cleanup, or
159
- # finished). If the build was interrupted with an error, the error
160
- # message will be passed as $2.
161
- # ---
162
- # postbuild: |
163
- # #!/bin/bash
164
- # case ${1:-undefined} in
165
- # finished)
166
- # t="token=SLACK_TOKEN"
167
- # c="channel=%23ci"
168
- # d=MYDOMAIN
169
- # u="https://${d}.slack.com/services/hooks/slackbot?${t}&${c}"
170
- # curl --data "Build %<name>s-%<timestamp>s complete." ${u}
171
- # ;;
172
- # *)
173
- # : # ${1} is the stage, ${2} is the error message
174
- # ;;
175
- # esac
65
+ # This dictionary is for plug-in-specific configuration. See each plug-in for
66
+ # configuration options. The keys to the plug-ins are based on the plug-in
67
+ # name. cartage-bundler is available as Cartage::Bundler; the transformed
68
+ # plug-in name will be <tt>bundler</tt>.
69
+ plugins: {}
176
70
 
177
71
  # This must not be indented for it to work.
178
- <% if File.exist?('config/ansible/cartage.yml') %>
179
- <%= File.read('config/ansible/cartage.yml') %>
180
- <% end %>
181
- <% if File.exist?('config/local/cartage.yml') %>
182
- <%= File.read('config/local/cartage.yml') %>
183
- <% end %>
72
+ <%= Cartage::Config.import 'config/ansible/cartage.yml' %>
73
+ <%= Cartage::Config.import 'config/local/cartage.yml' %>
@@ -1,31 +1,22 @@
1
- require 'pathname'
1
+ # frozen_string_literal: true
2
2
 
3
- # Cartage, a package builder.
4
- class Cartage
5
- VERSION = '1.2' #:nodoc:
6
-
7
- # Plug-in commands that want to return a specific exit code should use
8
- # Cartage::StatusError to wrap the error.
9
- class StatusError < StandardError
10
- # Initialize the exception with +exitstatus+ and the exception to wrap.
11
- def initialize(exitstatus, exception_or_message)
12
- super(exception_or_message) if exception_or_message
13
- @exitstatus = exitstatus
14
- end
3
+ require 'pathname'
4
+ require 'json'
15
5
 
16
- # The exit status to be returned from this exception.
17
- attr_reader :exitstatus
18
- end
6
+ require 'cartage/core'
7
+ require 'cartage/plugin'
8
+ require 'cartage/config'
19
9
 
20
- # Plug-in commands that want to return a non-zero exit code without a message
21
- # should raise Cartage::QuietError.new(exitstatus).
22
- class QuietError < StatusError
23
- # Initialize the exception with +exitstatus+.
24
- def initialize(exitstatus)
25
- super(exitstatus, nil)
26
- end
10
+ ##
11
+ # Cartage, a reliable package builder.
12
+ class Cartage
13
+ VERSION = '2.0.rc1' #:nodoc:
27
14
 
28
- attr_reader :exitstatus
15
+ # Creates a new Cartage instance. If provided a Cartage::Config object in
16
+ # +config+, sets the configuration and resolves it. If +config+ is not
17
+ # provided, the default configuration will be loaded.
18
+ def initialize(config = nil)
19
+ self.config = config || Cartage::Config.load(:default)
29
20
  end
30
21
 
31
22
  ##
@@ -39,6 +30,8 @@ class Cartage
39
30
  # The default name of the package to be created, derived from the
40
31
  # repository's Git URL.
41
32
 
33
+ attr_accessor_with_default :name, default: -> { File.basename(repo_url, '.git') }
34
+
42
35
  ##
43
36
  # :attr_accessor: root_path
44
37
  #
@@ -50,6 +43,16 @@ class Cartage
50
43
  # The default root path of the package, the top-level path of the Git
51
44
  # repository.
52
45
 
46
+ attr_reader_with_default :root_path do
47
+ Pathname(%x(git rev-parse --show-cdup).chomp).expand_path
48
+ end
49
+
50
+ ##
51
+ def root_path=(v) #:nodoc:
52
+ reset_computed_values
53
+ @root_path = Pathname(v).expand_path
54
+ end
55
+
53
56
  ##
54
57
  # :attr_accessor: target
55
58
  #
@@ -60,6 +63,10 @@ class Cartage
60
63
  #
61
64
  # The default target of the package, './tmp'.
62
65
 
66
+ attr_accessor_with_default :target,
67
+ transform: ->(v) { Pathname(v) },
68
+ default: -> { Pathname('tmp') }
69
+
63
70
  ##
64
71
  # :attr_accessor: timestamp
65
72
  #
@@ -70,169 +77,265 @@ class Cartage
70
77
  #
71
78
  # The default timestamp.
72
79
 
73
- ##
74
- # :attr_accessor: without_groups
75
- #
76
- # The environments to exclude from a bundle install.
80
+ attr_accessor_with_default :timestamp, default: -> {
81
+ Time.now.utc.strftime('%Y%m%d%H%M%S')
82
+ }
77
83
 
78
84
  ##
79
- # :method: default_without_environments
85
+ # :attr_accessor: compression
80
86
  #
81
- # The default environments to exclude from a bundle install. The default is
82
- # <tt>[ 'test', 'development' ]</tt>.
87
+ # The compression to be applied to any tarballs created (either the final
88
+ # tarball or the dependency cache tarball).
83
89
 
84
- # Commands that normally output data will have that output suppressed.
85
- attr_accessor :quiet
90
+ ##
91
+ def compression
92
+ unless defined?(@compression)
93
+ @compression = :bzip2
94
+ reset_computed_values
95
+ end
96
+ @compression
97
+ end
86
98
 
87
- # Commands will be run with extra information.
88
- attr_accessor :verbose
99
+ ##
100
+ def compression=(value) #:nodoc:
101
+ case value
102
+ when :bzip2, :none, :gzip, 'bzip2', 'none', 'gzip'
103
+ @compression = value
104
+ reset_computed_values
105
+ else
106
+ fail ArgumentError, "Invalid compression type #{value.inspect}"
107
+ end
108
+ end
89
109
 
90
- # The environment to be used when resolving configuration options from a
91
- # configuration file. Cartage configuration files do not usually have
92
- # environment partitions, but if they do, use this to select the environment
93
- # partition.
94
- attr_accessor :environment
110
+ # If +true+, dependencies will not be cached.
111
+ attr_accessor :disable_dependency_cache
95
112
 
96
- # The Config object. If +with_environment+ is +true+ (the default) and
97
- # #environment has been set, only the subset of the config matching
98
- # #environment will be returned. If +with_environment+ is +false+, the full
99
- # configuration will be returned.
113
+ ##
114
+ # :attr_reader: dependency_cache
100
115
  #
101
- # If +for_plugin+ is specified, only the subset of the config for the named
102
- # plug-in will be returned.
116
+ # The path to the tarball of vendored dependencies in the working path.
103
117
  #
104
- # # Assume that #environment is 'development'.
105
- # cartage.config
106
- # # => base_config[:development]
107
- # cartage.config(with_environment: false)
108
- # # => base_config
109
- # cartage.config(for_plugin: 's3')
110
- # # => base_config[:development][:plugins][:s3]
111
- # cartage.config(for_plugin: 's3', with_environment: false)
112
- # # => base_config[:plugins][:s3]
113
- def config(with_environment: true, for_plugin: nil)
114
- env = environment.to_sym if with_environment && environment
115
- plug = for_plugin.to_sym if for_plugin
116
-
117
- cfg = if env
118
- base_config[env]
119
- else
120
- base_config
121
- end
122
-
123
- cfg = cfg.plugins[plug] if plug && cfg.plugins
124
- cfg
125
- end
126
-
127
- # The configuration file to read. This should not be used by clients.
128
- attr_writer :load_config #:nodoc:
129
-
130
- # The base config file. This should not be used by clients.
131
- attr_accessor :base_config #:nodoc:
132
-
133
- def initialize #:nodoc:
134
- @load_config = :default
135
- end
136
-
137
- # Create the package.
138
- def pack
139
- timestamp # Force the timestamp to be set now.
140
- prepare_work_area
141
- save_release_hashref
142
- fetch_bundler
143
- install_vendor_bundle
144
- restore_modified_files
145
- build_final_tarball
118
+ # Vendored dependencies vary by build system. With Ruby and Bundler, this
119
+ # would be the <tt>vendor/bundle</tt> path; with npm, this would be the
120
+ # <tt>node_modules</tt> path.
121
+
122
+ ##
123
+ def dependency_cache
124
+ self.dependency_cache_path = tmp_path unless defined?(@dependency_cache)
125
+ @dependency_cache
146
126
  end
147
127
 
148
- # Set or return the bundle cache. The bundle cache is a compressed tarball of
149
- # <tt>vendor/bundle</tt> in the working path.
128
+ ##
129
+ # :attr_accessor: dependency_cache_path
150
130
  #
151
- # If it exists, it will be extracted into <tt>vendor/bundle</tt> before
152
- # <tt>bundle install --deployment</tt> is run, and it will be created after
153
- # the bundle has been installed. In this way, bundle installation works
154
- # almost the same way as Capistrano’s shared bundle concept as long as the
155
- # path to the bundle_cache has been set to a stable location.
131
+ # Reads or sets the vendored dependency cache path. This is where the tarball
132
+ # of vendored dependencies in the working path will reside.
156
133
  #
157
- # On Semaphore CI, this should be created relative to
134
+ # On a CI system, this should be written somewhere that the CI system uses
135
+ # for build caching. On Semaphore CI, this would be
158
136
  # <tt>$SEMAPHORE_CACHE</tt>.
159
- #
160
- # cartage pack --bundle-cache $SEMAPHORE_CACHE
161
- def bundle_cache(location = nil)
162
- if location || !defined?(@bundle_cache)
163
- @bundle_cache = Pathname(location || tmp_path).
164
- join('vendor-bundle.tar.bz2').expand_path
137
+
138
+ ##
139
+ def dependency_cache_path
140
+ self.dependency_cache_path = tmp_path unless defined?(@dependency_cache_path)
141
+ @dependency_cache_path
142
+ end
143
+
144
+ ##
145
+ def dependency_cache_path=(path) #:nodoc:
146
+ @dependency_cache_path = Pathname(path || tmp_path).expand_path
147
+ @dependency_cache = @dependency_cache_path.
148
+ join("dependency-cache.tar#{tar_compression_extension}")
149
+ end
150
+
151
+ # Commands that normally output data will have that output suppressed.
152
+ attr_accessor :quiet
153
+
154
+ # Commands will be run with extra information.
155
+ attr_accessor :verbose
156
+
157
+ # The cartage configuration object, implemented as a recursive OpenStruct.
158
+ # This can return just the subset of configuration for a command or plug-in
159
+ # by providing the +for_plugin+ or +for_command+ parameters.
160
+ def config(for_plugin: nil, for_command: nil)
161
+ if for_plugin && for_command
162
+ fail ArgumentError, 'Cannot get config for plug-in and command together'
163
+ elsif for_plugin
164
+ @config.dig(:plugins, for_plugin.to_sym) || OpenStruct.new
165
+ elsif for_command
166
+ @config.dig(:commands, for_command.to_sym) || OpenStruct.new
167
+ else
168
+ @config
165
169
  end
166
- @bundle_cache
167
170
  end
168
171
 
169
- # Return the release hashref. If the optional +save_to+ parameter is
170
- # provided, the release hashref will be written to the specified file.
171
- def release_hashref(save_to: nil)
172
+ # The config file. This should not be used by clients.
173
+ def config=(cfg) # :nodoc:
174
+ fail ArgumentError, 'No config provided' unless cfg
175
+ @plugins = Plugins.new
176
+ @config = cfg
177
+ resolve_config!
178
+ end
179
+
180
+ # The release metadata that will be written for the package.
181
+ def release_metadata
182
+ @release_metadata ||= {
183
+ package: {
184
+ name: name,
185
+ repo: {
186
+ type: 'git', # Hardcoded until we have other support
187
+ url: repo_url
188
+ },
189
+ hashref: release_hashref,
190
+ timestamp: timestamp
191
+ }
192
+ }
193
+ end
194
+
195
+ # Return the release hashref.
196
+ def release_hashref
172
197
  @release_hashref ||= %x(git rev-parse HEAD).chomp
173
- File.open(save_to, 'w') { |f| f.write @release_hashref } if save_to
174
- @release_hashref
175
198
  end
176
199
 
177
200
  # The repository URL.
178
201
  def repo_url
179
202
  unless defined? @repo_url
180
- origin = %x(git remote show -n origin)
181
- match = origin.match(%r{\n\s+Fetch URL: (?<fetch>[^\n]+)})
182
- @repo_url = match[:fetch]
203
+ @repo_url = %x(git remote show -n origin).
204
+ match(/\n\s+Fetch URL: (?<fetch>[^\n]+)/)[:fetch]
183
205
  end
184
206
  @repo_url
185
207
  end
186
208
 
187
- # The path to the resulting package.
188
- def final_tarball
189
- @final_tarball ||= Pathname("#{final_name}.tar.bz2")
209
+ # The temporary path.
210
+ def tmp_path
211
+ @tmp_path ||= root_path.join('tmp')
212
+ end
213
+
214
+ # The working path for the job, in #tmp_path.
215
+ def work_path
216
+ @work_path ||= tmp_path.join(name)
190
217
  end
191
218
 
192
- # The path to the resulting release_hashref.
193
- def final_release_hashref
194
- @final_release_hashref ||=
195
- Pathname("#{final_name}-release-hashref.txt")
219
+ # The final name
220
+ def final_name
221
+ @final_name ||= tmp_path.join("#{name}-#{timestamp}")
196
222
  end
197
223
 
198
- # A utility method for Cartage plug-ins to display a message only if verbose
199
- # is on. Unless the command implemented by the plug-in is output only, this
200
- # should be used.
224
+ # The path to the resulting release-metadata.json file.
225
+ def final_release_metadata_json
226
+ @final_release_metadata_json ||= Pathname("#{final_name}-release-metadata.json")
227
+ end
228
+
229
+ # A utility method for Cartage plug-ins to display a +message+ only if
230
+ # verbose is on. Unless the command implemented by the plug-in is output
231
+ # only, this should be used.
201
232
  def display(message)
202
233
  __display(message)
203
234
  end
204
235
 
236
+ # A utility method for Cartage plug-ins to run a +command+ in the shell. Uses
237
+ # IO.popen.
238
+ def run(command)
239
+ display command.join(' ')
240
+
241
+ IO.popen(command + [ err: %i(child out) ]) do |io|
242
+ __display(io.read(128), partial: true, verbose: true) until io.eof?
243
+ end
244
+
245
+ fail StandardError, "Error running '#{command.join(' ')}'" unless $?.success?
246
+ end
247
+
248
+ # Returns the registered plug-ins, once configuration has been resolved.
249
+ def plugins
250
+ @plugins ||= Plugins.new
251
+ end
252
+
253
+ # Create the release package(s).
254
+ #
255
+ # Requests:
256
+ # * +:vendor_dependencies+ (#vendor_dependencies, #path)
257
+ # * +:pre_build_package+
258
+ # * +:build_package+
259
+ # * +:post_build_package+
260
+ def build_package
261
+ # Force timestamp to be initialized before anything else. This gives us a
262
+ # stable timestamp for the process.
263
+ timestamp
264
+ # Prepare the work area: copy files from root_path to work_path based on
265
+ # the resolved Manifest.txt.
266
+ prepare_work_area
267
+ # Anything that has been modified locally needs to be reset.
268
+ restore_modified_files
269
+ # Save both the final release metadata and the in-package release metadata.
270
+ save_release_metadata
271
+ # Vendor the dependencies for the package.
272
+ vendor_dependencies
273
+ # Request that supporting plug-ins build the package.
274
+ request_build_package
275
+ end
276
+
277
+ # Returns the flag to use with +tar+ given the value of +compression+.
278
+ def tar_compression_flag
279
+ case compression
280
+ when :bzip2, 'bzip2', nil
281
+ 'j'
282
+ when :gzip, 'gzip'
283
+ 'z'
284
+ when :none, 'none'
285
+ ''
286
+ end
287
+ end
288
+
289
+ # Returns the extension to use with +tar+ given the value of +compression+.
290
+ def tar_compression_extension
291
+ case compression
292
+ when :bzip2, 'bzip2', nil
293
+ '.bz2'
294
+ when :gzip, 'gzip'
295
+ '.gz'
296
+ when :none, 'none'
297
+ ''
298
+ end
299
+ end
300
+
205
301
  private
206
- def resolve_config!(*with_plugins)
207
- return unless @load_config
208
- @base_config = Cartage::Config.load(@load_config)
209
302
 
210
- cfg = config
211
- maybe_assign :target, cfg.target
212
- maybe_assign :name, cfg.name
213
- maybe_assign :root_path, cfg.root_path
214
- maybe_assign :timestamp, cfg.timestamp
215
- maybe_assign :without_groups, cfg.without
303
+ attr_writer :release_hashref
304
+
305
+ def resolve_config!
306
+ fail 'No configuration' unless config
307
+
308
+ Cartage::Plugin.load_for(singleton_class)
216
309
 
217
- bundle_cache(cfg.bundle_cache) unless cfg.bundle_cache.nil? ||
218
- cfg.bundle_cache.empty?
310
+ self.disable_dependency_cache = config.disable_dependency_cache
311
+ self.quiet = config.quiet
312
+ self.verbose = config.verbose
219
313
 
220
- with_plugins.each do |name|
221
- next unless respond_to? name
222
- plugin = send(name)
314
+ maybe_assign :name, config.name
315
+ maybe_assign :target, config.target
316
+ maybe_assign :root_path, config.root_path
317
+ maybe_assign :timestamp, config.timestamp
318
+ maybe_assign :dependency_cache_path, config.dependency_cache_path
319
+ maybe_assign :release_hashref, config.release_hashref
223
320
 
224
- next unless plugin
321
+ Cartage::Plugin.each do |name|
322
+ next unless respond_to?(name)
323
+ plugin = send(name) or next
225
324
  plugin.send(:resolve_config!, config(for_plugin: name))
325
+
326
+ plugins.add plugin
226
327
  end
328
+
329
+ plugins.freeze
227
330
  end
228
331
 
229
332
  def maybe_assign(name, value)
230
- return if value.nil? || value.empty? ||
231
- instance_variable_defined?(:"@#{name}")
333
+ return if value.nil? || (value.respond_to?(:empty?) && value.empty?) ||
334
+ instance_variable_defined?(:"@#{name}")
232
335
  send(:"#{name}=", value)
233
336
  end
234
337
 
235
- def __display(message, partial: false, verbose: verbose())
338
+ def __display(message, partial: false, verbose: self.verbose)
236
339
  return unless verbose && !quiet
237
340
 
238
341
  if partial
@@ -242,26 +345,21 @@ class Cartage
242
345
  end
243
346
  end
244
347
 
245
- def run(command)
246
- display command.join(' ')
247
-
248
- IO.popen(command + [ err: [ :child, :out ] ]) do |io|
249
- __display(io.read(128), partial: true, verbose: true) until io.eof?
250
- end
251
-
252
- unless $?.success?
253
- raise StandardError, "Error running '#{command.join(' ')}'"
254
- end
348
+ def reset_computed_values
349
+ instance_variable_set(:@tmp_path, nil)
350
+ instance_variable_set(:@final_name, nil)
351
+ instance_variable_set(:@final_release_metadata_json, nil)
352
+ instance_variable_set(:@release_metadata, nil)
353
+ instance_variable_set(:@work_path, nil)
354
+ self.dependency_cache_path = dependency_cache_path
255
355
  end
256
356
 
257
357
  def prepare_work_area
258
- display "Preparing cartage work area..."
358
+ display 'Preparing cartage work area...'
259
359
 
260
360
  work_path.rmtree if work_path.exist?
261
361
  work_path.mkpath
262
362
 
263
- xf_status = cf_status = nil
264
-
265
363
  manifest.resolve(root_path) do |file_list|
266
364
  tar_cf_cmd = [
267
365
  'tar', 'cf', '-', '-C', parent, '-h', '-T', file_list
@@ -276,74 +374,29 @@ class Cartage
276
374
  xf.write cf.read
277
375
  end
278
376
 
279
- unless $?.success?
280
- raise StandardError, "Error running #{tar_xf_cmd.join(' ')}"
281
- end
377
+ fail StandardError, "Error running #{tar_xf_cmd.join(' ')}" unless $?.success?
282
378
  end
283
379
 
284
- unless $?.success?
285
- raise StandardError, "Error running #{tar_cf_cmd.join(' ')}"
286
- end
380
+ fail StandardError, "Error running #{tar_cf_cmd.join(' ')}" unless $?.success?
287
381
  end
288
382
  end
289
383
 
290
- def save_release_hashref
291
- display 'Saving release hashref...'
292
- release_hashref save_to: work_path.join('release_hashref')
293
- release_hashref save_to: final_release_hashref
294
- end
295
-
296
- def extract_bundle_cache
297
- run %W(tar xfj #{bundle_cache} -C #{work_path}) if bundle_cache.exist?
298
- end
299
-
300
- def create_bundle_cache
301
- run %W(tar cfj #{bundle_cache} -C #{work_path} vendor/bundle)
302
- end
303
-
304
- def install_vendor_bundle
305
- extract_bundle_cache
306
-
307
- Bundler.with_clean_env do
308
- Dir.chdir(work_path) do
309
- run %w(bundle install --jobs=4 --deployment --clean --without) +
310
- without_groups
311
- end
312
- end
313
-
314
- create_bundle_cache
384
+ def save_release_metadata
385
+ display 'Saving release metadata...'
386
+ json = JSON.generate(release_metadata)
387
+ work_path.join('release-metadata.json').write(json)
388
+ final_release_metadata_json.write(json)
315
389
  end
316
390
 
317
391
  def restore_modified_files
318
- %x(git status -s).split($/).map(&:split).map(&:last).each { |file|
319
- restore_modified_file file
320
- }
321
- end
322
-
323
- def fetch_bundler
324
- Dir.chdir(work_path) do
325
- run %w(gem fetch bundler)
326
- end
327
- end
328
-
329
- def build_final_tarball
330
- run %W(tar cfj #{final_tarball} -C #{tmp_path} #{name})
331
- end
332
-
333
- def work_path
334
- @work_path ||= tmp_path.join(name)
335
- end
336
-
337
- def clean
338
- [ work_path ] + final
339
- end
340
-
341
- def final
342
- [ final_tarball, final_release_hashref ]
343
- end
344
-
345
- def parent
346
- @parent ||= root_path.parent
392
+ %x(git status -s).
393
+ split($/).
394
+ map(&:split).
395
+ select { |s, _f| s !~ /\?/ }.
396
+ map(&:last).
397
+ each { |file|
398
+ restore_modified_file file
399
+ }
347
400
  end
348
401
 
349
402
  def restore_modified_file(filename)
@@ -354,175 +407,52 @@ class Cartage
354
407
  IO.popen(command) do |show|
355
408
  work_path.join(filename).open('w') { |f|
356
409
  f.puts show.read
357
- f.puts timestamp
358
410
  }
359
411
  end
360
412
  end
361
413
 
362
- def tmp_path
363
- @tmp_path ||= root_path.join('tmp')
364
- end
365
-
366
- def final_name
367
- @final_name ||= tmp_path.join("#{name}-#{timestamp}")
368
- end
369
-
370
- class << self
371
- private
414
+ def vendor_dependencies
415
+ extract_dependency_cache
372
416
 
373
- def lazy_accessor(sym, default: nil, setter: nil, &block)
374
- ivar = :"@#{sym}"
375
- wsym = :"#{sym}="
376
- dsym = :"default_#{sym}"
377
-
378
- if default.nil? && block.nil?
379
- raise ArgumentError, "No default provided."
380
- end
381
-
382
- if setter && !setter.respond_to?(:call)
383
- raise ArgumentError, "setter must be callable"
384
- end
385
-
386
- setter ||= ->(v) { v }
387
-
388
- dblk = if default.respond_to?(:call)
389
- default
390
- elsif default.nil?
391
- block
392
- else
393
- -> { default }
394
- end
395
-
396
- define_method(sym) do
397
- instance_variable_get(ivar) || send(dsym)
398
- end
399
-
400
- define_method(wsym) do |value|
401
- instance_variable_set(ivar, setter.call(value || send(dsym)))
402
- end
417
+ plugins.request(:vendor_dependencies)
403
418
 
404
- define_method(dsym, &dblk)
405
- end
419
+ create_dependency_cache(
420
+ plugins.request_map(:vendor_dependencies, :path).compact.flatten
421
+ )
406
422
  end
407
423
 
408
- lazy_accessor :name, default: -> { File.basename(repo_url, '.git') }
409
- lazy_accessor :root_path, setter: ->(v) { Pathname(v).expand_path },
410
- default: -> { Pathname(%x(git rev-parse --show-cdup).chomp).expand_path }
411
- lazy_accessor :target, setter: ->(v) { Pathname(v) },
412
- default: -> { Pathname('tmp') }
413
- lazy_accessor :timestamp, default: -> {
414
- Time.now.utc.strftime("%Y%m%d%H%M%S")
415
- }
416
- lazy_accessor :without_groups, setter: ->(v) { Array(v) },
417
- default: -> { %w(development test) }
418
- lazy_accessor :base_config, default: -> { OpenStruct.new }
419
- end
420
-
421
- class << Cartage
422
- # Run the Cartage command-line program.
423
- def run(args) #:nodoc:
424
- require_relative 'cartage/plugin'
425
- Cartage::Plugin.load
426
- Cartage::Plugin.decorate(Cartage)
427
-
428
- cartage = Cartage.new
429
-
430
- cli = CmdParse::CommandParser.new(handle_exceptions: true)
431
- cli.main_options.program_name = 'cartage'
432
- cli.main_options.version = Cartage::VERSION.split(/\./)
433
- cli.main_options.banner = 'Manage releaseable packages.'
434
-
435
- cli.global_options do |opts|
436
- # opts.on('--[no-]quiet', 'Silence normal command output.') { |q|
437
- # cartage.quiet = !!q
438
- # }
439
- opts.on('--[no-]verbose', 'Show verbose output.') { |v|
440
- cartage.verbose = !!v
441
- }
442
- opts.on(
443
- '-E', '--environment [ENVIRONMENT]', <<-desc
444
- Set the environment to be used when necessary. If an environment name is not
445
- provided, it will check the values of $RAILS_ENV and RACK_ENV. If neither is
446
- set, this option is ignored.
447
- desc
448
- ) { |e| cartage.environment = e || ENV['RAILS_ENV'] || ENV['RACK_ENV'] }
449
- opts.on(
450
- '-C', '--[no-]config-file load_config', <<-desc
451
- Configure Cartage from a default configuration file or a specified
452
- configuration file.
453
- desc
454
- ) { |c| cartage.load_config = c }
455
- end
456
-
457
- cli.add_command(CmdParse::HelpCommand.new)
458
- cli.add_command(CmdParse::VersionCommand.new)
459
- cli.add_command(Cartage::PackCommand.new(cartage))
460
-
461
- Cartage::Plugin.registered.each do |plugin|
462
- if plugin.respond_to?(:commands)
463
- Array(plugin.commands).flatten.each do |command|
464
- registered_commands << command
465
- end
466
- end
467
- end
424
+ def extract_dependency_cache
425
+ return if disable_dependency_cache || !dependency_cache.exist?
426
+ run %W(tar xf#{tar_compression_flag} #{dependency_cache} -C #{work_path})
427
+ end
468
428
 
469
- registered_commands.uniq.each { |cmd| cli.add_command(cmd.new(cartage)) }
470
- cli.parse
471
- return 0
472
- rescue Exception => exception
473
- show_message_for exception, for_cartage: cartage
474
- return exitstatus_for(exception)
475
- end
476
-
477
- # Set options common to anything that builds a package (that is, it calls
478
- # Cartage#pack).
479
- def common_build_options(opts, cartage)
480
- opts.on(
481
- '-t', '--target PATH',
482
- 'The build package will be placed in PATH, which defaults to \'tmp\'.'
483
- ) { |t| cartage.target = t }
484
- opts.on(
485
- '-n', '--name NAME',
486
- "Set the package name. Defaults to '#{cartage.default_name}'."
487
- ) { |n| cartage.name = n }
488
- opts.on(
489
- '-r', '--root-path PATH',
490
- 'Set the root path. Defaults to the repository root.'
491
- ) { |r| cartage.root_path = r }
492
- opts.on(
493
- '--timestamp TIMESTAMP',
494
- 'The timestamp used for the final package.'
495
- ) { |t| cartage.timestamp = t }
496
- opts.on(
497
- '--bundle-cache PATH',
498
- 'Set the bundle cache path.'
499
- ) { |b| cartage.bundle_cache(b) }
500
- opts.on(
501
- '--without GROUP1,GROUP2', Array,
502
- 'Set the groups to be excluded from bundle installation.',
503
- ) { |w| cartage.without_environments = w }
429
+ def create_dependency_cache(paths = [])
430
+ return if disable_dependency_cache || paths.empty?
431
+ run [
432
+ 'tar',
433
+ "cf#{tar_compression_flag}",
434
+ dependency_cache,
435
+ '-C',
436
+ work_path,
437
+ *paths
438
+ ].map(&:to_s)
504
439
  end
505
440
 
506
- private
507
- def registered_commands
508
- @registered_commands ||= []
441
+ def parent
442
+ @parent ||= root_path.parent
509
443
  end
510
444
 
511
- def exitstatus_for(exception)
512
- if exception.respond_to? :exitstatus
513
- exception.exitstatus
514
- else
515
- 2
516
- end
445
+ def realize!
446
+ repo_url
447
+ root_path
448
+ release_hashref
449
+ timestamp
517
450
  end
518
451
 
519
- def show_message_for(exception, for_cartage: nil)
520
- unless exception.kind_of?(Cartage::QuietError)
521
- $stderr.puts "Error:\n " + exception.message
522
- if for_cartage && for_cartage.verbose
523
- $stderr.puts exception.backtrace.join("\n")
524
- end
525
- end
452
+ def request_build_package
453
+ plugins.request(:pre_build_package)
454
+ plugins.request(:build_package)
455
+ plugins.request(:post_build_package)
526
456
  end
527
457
  end
528
458