omnibus 3.2.0.rc.1 → 3.2.0.rc.2

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -2
  3. data/Rakefile +2 -2
  4. data/lib/omnibus.rb +13 -4
  5. data/lib/omnibus/builder.rb +596 -248
  6. data/lib/omnibus/cleanroom.rb +7 -7
  7. data/lib/omnibus/cli/deprecated.rb +1 -1
  8. data/lib/omnibus/exceptions.rb +36 -5
  9. data/lib/omnibus/fetchers/net_fetcher.rb +4 -0
  10. data/lib/omnibus/git_cache.rb +3 -3
  11. data/lib/omnibus/instrumentation.rb +29 -0
  12. data/lib/omnibus/package.rb +98 -5
  13. data/lib/omnibus/packagers/base.rb +1 -0
  14. data/lib/omnibus/project.rb +5 -64
  15. data/lib/omnibus/publishers/artifactory_publisher.rb +4 -1
  16. data/lib/omnibus/software.rb +52 -29
  17. data/lib/omnibus/util.rb +11 -1
  18. data/lib/omnibus/version.rb +1 -1
  19. data/omnibus.gemspec +2 -3
  20. data/spec/data/complicated/config/software/libyaml-windows.rb +1 -1
  21. data/spec/functional/builder_spec.rb +305 -0
  22. data/spec/{functional → integration}/packagers/mac_spec.rb +0 -0
  23. data/spec/{functional → integration}/packagers/windows_spec.rb +0 -0
  24. data/spec/spec_helper.rb +8 -7
  25. data/spec/unit/build_version_dsl_spec.rb +77 -75
  26. data/spec/unit/build_version_spec.rb +4 -4
  27. data/spec/unit/builder_spec.rb +49 -0
  28. data/spec/unit/config_spec.rb +1 -1
  29. data/spec/unit/fetchers/git_fetcher_spec.rb +11 -11
  30. data/spec/unit/fetchers/net_fetcher_spec.rb +3 -5
  31. data/spec/unit/git_cache_spec.rb +2 -2
  32. data/spec/unit/package_spec.rb +118 -11
  33. data/spec/unit/packagers/base_spec.rb +17 -17
  34. data/spec/unit/packagers/mac_pkg_spec.rb +1 -1
  35. data/spec/unit/project_spec.rb +27 -101
  36. data/spec/unit/publisher_spec.rb +2 -2
  37. data/spec/unit/publishers/artifactory_publisher_spec.rb +8 -7
  38. data/spec/unit/publishers/s3_publisher_spec.rb +3 -3
  39. data/spec/unit/s3_cacher_spec.rb +2 -2
  40. data/spec/unit/software_spec.rb +26 -18
  41. metadata +27 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4812c396c2ec933378d32542df7c8245700ecf52
4
- data.tar.gz: 51791434059dcada2197bf2bb8693e6c080669f2
3
+ metadata.gz: 713b0a1bd3b7ac7ea05f5d7b40eb80824c24477e
4
+ data.tar.gz: b0d7e0927ed88cd196966be1891268e62a0ce5f7
5
5
  SHA512:
6
- metadata.gz: 805e85738934c39e9d849fa6dd93d0877b17c60aab484e788616e1e143bd72df59971a8fce9c02cc15f2c3402717a565883425058a6fba20c0e3231c4508a6c8
7
- data.tar.gz: 9351cf29fb353525d4e705bd0fdde1884ccaac3d393c3c78b96ed88f6963467a92ed32b40f5aad9e8ef7ccda0e460df0a8ed0954b39818e2d0a715d96261aca6
6
+ metadata.gz: 8289bda18635035acc08431683e4444c24bf2759bdcbe15aa3c69537458c4c1d0718562e45faa789832b0710a5b96e75eb55be4910b4f17ecb3108287ed9ce9c
7
+ data.tar.gz: f524d6d38c6f328a408b2531c6eabd9853462c3b020a5e10de9b84bf3c5e3bb8d50318c04763bfcf8e69056e66ff768e7882a06c3896439f17e4dbd7560fdeff
data/CHANGELOG.md CHANGED
@@ -1,8 +1,8 @@
1
1
  Omnibus Ruby CHANGELOG
2
2
  ======================
3
3
 
4
- v3.2.0.rc.1 (July 14, 2014)
5
- -----------==--------------
4
+ v3.2.0.rc.2 (July 17, 2014)
5
+ ---------------------------
6
6
  - Make build commands output during `log.info` instead of `log.debug`
7
7
  - Refactor Chef Sugar into an includable module, permitting DSL methods in both Software and Project definitions
8
8
  - Refactor `omnibus release` into a non-S3-specific backend "publisher"
@@ -21,6 +21,8 @@ v3.2.0.rc.1 (July 14, 2014)
21
21
  - Add publish APIs for dirtying the git cache
22
22
  - Add test coverage for the "public" API
23
23
  - Add validation to `source` in software DSL
24
+ - Add logging to the Packager class
25
+ - Add functional tests for builders
24
26
  - Update generator templates to use the new APIs
25
27
  - Upgrade to Ohai 7.2
26
28
  - Improve YARDoc
@@ -40,11 +42,13 @@ v3.2.0.rc.1 (July 14, 2014)
40
42
  - Deprecate `Project#install_path` in favor of `Project.install_dir`
41
43
  - Deprecate [DSL] `install_path` in favor of `install_dir`
42
44
  - Rename `Config.install_path_cache_dir` to `git_cache_dir`
45
+ - Fix a bug in the deprecations where a hardcoded output was used instead of a dynamic variable
43
46
 
44
47
  ### DSL Changes
45
48
  - Add `with_embedded_path` to software
46
49
  - Add `with_standard_compiler_flags` to software
47
50
  - Add `package_scripts_path` to project
51
+ - Add builder DSL methods for `mkdir`, `touch`, `delete`, `copy`, `move`, and `link`
48
52
 
49
53
  ### Bug fixes
50
54
  - Fix a small typo in the project generator (come -> some)
@@ -63,6 +67,9 @@ v3.2.0.rc.1 (July 14, 2014)
63
67
  - Intelligently parse the project's homepage because Ruby's native URI implementation is buggy
64
68
  - Fetch all software at the start of the build - this fixes a bug where a build would fail halfway through because of a tiny typo of GitHub outage. Now, all required software is downloaded **before** the build starts, lowering the feedback time for a failure due to networking issues
65
69
  - Use the fetcher's `version_for_cache` method directly, falling back to `0.0.0` (and a warining) if no version is given
70
+ - Require `net/http`, `net/https`, and `net/ftp` in the base fetcher module
71
+ - Use -R, not -W1 on FreeBSD's compile flags
72
+ - Expand all paths relative to the project_root
66
73
 
67
74
  ### Potentially breaking changes
68
75
  - Merged `Package` and `Artifact` into the same class and updated API - this was considered an **internal** API so it is not a violation of semver
@@ -70,6 +77,8 @@ v3.2.0.rc.1 (July 14, 2014)
70
77
  - Use a cleanroom object when evaluating the DSL - prior to this release, Omnibus did not declare a public API. Project and software definitions had unrestricted access to the entire project.rb and software.rb methods respectively. This poses two problems - first, it makes it impossible to guarantee a public DSL API over a public (code) API. Second, it permits a developer to change the behavior of project.rb or software.rb accidentially, simply by defining a new method. The introducing of a cleanroom fixes both these bugs, however, it was impossible to know what was formerly considered a public API. Thus, it is possible that a previously-relied-on method is now unavaiable using the cleanroom. Please open an issue if you encounter such a case.
71
78
  - Remove mixlib-config - if you were relying on mixlib-config as a transitive dependency, it is no longer available
72
79
  - Remove the ability to use an overrides file - this was for internal use only and was never exposed as a public API. However, if you dug into the code and found it, it has now been removed. For BC purposes, the value still exists in the configuration object, but is essentially a no-op
80
+ - Move project loading from INFO to DEBUG
81
+ - Truncate platforms to short versions
73
82
 
74
83
 
75
84
  v3.1.1 (May 20, 2014)
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'bundler/gem_tasks'
2
2
 
3
3
  require 'rspec/core/rake_task'
4
- [:unit, :functional].each do |type|
4
+ [:unit, :functional, :integration].each do |type|
5
5
  RSpec::Core::RakeTask.new(type) do |t|
6
6
  t.pattern = "spec/#{type}/**/*_spec.rb"
7
7
  t.rspec_opts = [].tap do |a|
@@ -22,7 +22,7 @@ end
22
22
 
23
23
  namespace :travis do
24
24
  desc 'Run tests on Travis'
25
- task ci: %w(unit acceptance)
25
+ task ci: %w(unit functional acceptance)
26
26
  end
27
27
 
28
28
  task default: %w(travis:ci)
data/lib/omnibus.rb CHANGED
@@ -40,6 +40,7 @@ module Omnibus
40
40
  autoload :Generator, 'omnibus/generator'
41
41
  autoload :GitCache, 'omnibus/git_cache'
42
42
  autoload :HealthCheck, 'omnibus/health_check'
43
+ autoload :Instrumentation, 'omnibus/instrumentation'
43
44
  autoload :Library, 'omnibus/library'
44
45
  autoload :Logger, 'omnibus/logger'
45
46
  autoload :Logging, 'omnibus/logging'
@@ -249,13 +250,15 @@ module Omnibus
249
250
  # @return [Array<String>]
250
251
  #
251
252
  def software_dirs
253
+ return @software_dirs if @software_dirs
254
+
252
255
  directories = [
253
256
  paths_from_project_root,
254
257
  paths_from_local_software_dirs,
255
258
  paths_from_software_gems,
256
259
  ].flatten
257
260
 
258
- directories.inject([]) do |array, directory|
261
+ @software_dirs = directories.inject([]) do |array, directory|
259
262
  softwares_path = File.join(directory, Config.software_dir)
260
263
 
261
264
  if File.directory?(softwares_path)
@@ -269,6 +272,8 @@ module Omnibus
269
272
 
270
273
  array
271
274
  end
275
+
276
+ @software_dirs
272
277
  end
273
278
 
274
279
  #
@@ -283,12 +288,14 @@ module Omnibus
283
288
  # @return [Hash<String, String>]
284
289
  #
285
290
  def software_map
286
- software_dirs.inject({}) do |hash, directory|
291
+ return @software_map if @software_map
292
+
293
+ @software_map = software_dirs.inject({}) do |hash, directory|
287
294
  Dir.glob("#{directory}/*.rb").each do |path|
288
295
  name = File.basename(path, '.rb')
289
296
 
290
297
  if hash[name].nil?
291
- Omnibus.logger.info('Omnibus#software_map') do
298
+ Omnibus.logger.debug('Omnibus#software_map') do
292
299
  "Using software `#{name}' from `#{path}'."
293
300
  end
294
301
 
@@ -303,6 +310,8 @@ module Omnibus
303
310
 
304
311
  hash
305
312
  end
313
+
314
+ @software_map
306
315
  end
307
316
 
308
317
  private
@@ -323,7 +332,7 @@ module Omnibus
323
332
  name = File.basename(path, '.rb')
324
333
 
325
334
  if hash[name].nil?
326
- Omnibus.logger.info('Omnibus#projects') do
335
+ Omnibus.logger.debug('Omnibus#projects') do
327
336
  "Using project `#{name}' from `#{path}'."
328
337
  end
329
338
 
@@ -14,178 +14,453 @@
14
14
  # limitations under the License.
15
15
  #
16
16
 
17
- require 'forwardable'
17
+ require 'bundler'
18
+ require 'fileutils'
18
19
  require 'ostruct'
20
+ require 'mixlib/shellout'
19
21
 
20
22
  module Omnibus
21
23
  class Builder
24
+ include Cleanroom
25
+ include Digestable
26
+ include Instrumentation
22
27
  include Logging
28
+ include Util
23
29
 
24
- # Proxies method calls to either a Builder object or the Software that the
25
- # builder belongs to. Provides compatibility with our DSL where we never
26
- # yield objects to blocks and hopefully hides some of the confusion that
27
- # can arise from instance_eval.
28
- class DSLProxy
29
- extend Forwardable
30
-
31
- def_delegator :@builder, :patch
32
- def_delegator :@builder, :erb
33
- def_delegator :@builder, :command
34
- def_delegator :@builder, :ruby
35
- def_delegator :@builder, :gem
36
- def_delegator :@builder, :bundle
37
- def_delegator :@builder, :rake
38
- def_delegator :@builder, :block
39
- def_delegator :@builder, :name
40
- def_delegator :@builder, :max_build_jobs
41
-
42
- def initialize(builder, software)
43
- @builder, @software = builder, software
30
+ #
31
+ # Since builder is also a proxy object to software, we dynamically re-define
32
+ # all the methods that exist on {Software} as proxy methhods here. This
33
+ # permits developers to use {Software} methods as if they were directly
34
+ # part of this DSL.
35
+ #
36
+ Software.exposed_methods.each do |name, _|
37
+ define_method(name) do |*args, &block|
38
+ software.send(name, *args, &block)
44
39
  end
40
+ expose(name)
41
+ end
45
42
 
46
- def eval_block(&block)
47
- instance_eval(&block)
48
- end
43
+ #
44
+ # @return [Software]
45
+ # the software definition that created this builder
46
+ #
47
+ attr_reader :software
49
48
 
50
- def respond_to?(method)
51
- super || @software.respond_to?(method)
52
- end
49
+ #
50
+ # Create a new builder object for evaluation.
51
+ #
52
+ # @param [Software] software
53
+ # the software definition that created this builder
54
+ #
55
+ def initialize(software)
56
+ @software = software
57
+ end
53
58
 
54
- def methods
55
- super | @software.methods
56
- end
59
+ #
60
+ # @!group System DSL methods
61
+ #
62
+ # The following DSL methods are available from within build blocks.
63
+ # --------------------------------------------------
57
64
 
58
- def method_missing(method_name, *args, &block)
59
- if @software.respond_to?(method_name)
60
- @software.send(method_name, *args, &block)
61
- else
62
- super
63
- end
65
+ #
66
+ # Execute the given command string or command arguments.
67
+ #
68
+ # @example
69
+ # command 'make install', env: { 'PATH' => '/my/custom/path' }
70
+ #
71
+ # @param [String] command
72
+ # the command to execute
73
+ # @param [Hash] options
74
+ # a list of options to pass to the +Mixlib::ShellOut+ instance when it is
75
+ # executed
76
+ #
77
+ # @return [void]
78
+ #
79
+ def command(command, options = {})
80
+ build_commands << BuildCommand.new("Execute: `#{command}'") do
81
+ _shellout!(command, options)
64
82
  end
65
83
  end
84
+ expose :command
66
85
 
67
- # @todo code duplication with {Fetcher::ErrorReporter}
68
- # @todo make this use the logger
69
- class ErrorReporter
70
- # @todo fetcher isn't even used
71
- def initialize(error, fetcher)
72
- @error, @fetcher = error, fetcher
73
- end
86
+ #
87
+ # Apply the patch by the given name. This method will search all possible
88
+ # locations for a patch (such as {Config#software_gems}).
89
+ #
90
+ # @example
91
+ # patch source: 'ncurses-clang.patch'
92
+ #
93
+ # @example
94
+ # patch source: 'patch-ad', plevel: 0
95
+ #
96
+ # @param [Hash] options
97
+ # the list of options
98
+ #
99
+ # @option options [String] :source
100
+ # the name of the patch to apply
101
+ # @option options [Fixnum] :plevel
102
+ # the level to apply the patch
103
+ # @option options [String] :target
104
+ # the destination to apply the patch
105
+ #
106
+ # @return (see #command)
107
+ #
108
+ def patch(options = {})
109
+ source = options.delete(:source)
110
+ plevel = options.delete(:plevel) || 1
111
+ target = options.delete(:target)
74
112
 
75
- # @todo this isn't necessary
76
- def e
77
- @error
113
+ locations, patch_path = find_file('config/patches', source)
114
+
115
+ unless patch_path
116
+ raise MissingPatch.new(source, locations)
78
117
  end
79
118
 
80
- def explain(why)
81
- $stderr.puts '* ' * 40
82
- $stderr.puts why
83
- $stderr.puts 'Exception:'
84
- $stderr.puts indent("#{e.class}: #{e.message.strip}", 2)
85
- Array(e.backtrace).each { |l| $stderr.puts indent(l, 4) }
86
- $stderr.puts '* ' * 40
119
+ # Apply patches nicely on Windows
120
+ patch_path = windows_safe_path(patch_path)
121
+
122
+ if target
123
+ command = "cat #{patch_path} | patch -p#{plevel} #{target}"
124
+ else
125
+ command = "patch -d #{software.project_dir} -p#{plevel} -i #{patch_path}"
87
126
  end
88
127
 
89
- private
128
+ patches << patch_path
90
129
 
91
- def indent(string, n)
92
- string.split("\n").map { |l| ' '.rjust(n) << l }.join("\n")
130
+ build_commands << BuildCommand.new("Apply patch `#{source}'") do
131
+ _shellout!(command, options)
93
132
  end
94
133
  end
134
+ expose :patch
95
135
 
96
- # @todo Look at using Bundler.with_clean_env{ ... } instead
97
- BUNDLER_BUSTER = {
98
- 'RUBYOPT' => nil,
99
- 'BUNDLE_BIN_PATH' => nil,
100
- 'BUNDLE_GEMFILE' => nil,
101
- 'GEM_PATH' => nil,
102
- 'GEM_HOME' => nil,
103
- }
136
+ #
137
+ # The maximum number of build jobs, as computed from Ohai data. If the Ohai
138
+ # data is unavailable, +3+ is used.
139
+ #
140
+ # @todo Should this be moved to {Software}?
141
+ #
142
+ # @example
143
+ # command "make install -j#{max_build_jobs}"
144
+ #
145
+ # @return [Fixnum]
146
+ #
147
+ def max_build_jobs
148
+ if Ohai['cpu'] && Ohai['cpu']['total'] && Ohai['cpu']['total'].to_s =~ /^\d+$/
149
+ Ohai['cpu']['total'].to_i + 1
150
+ else
151
+ 3
152
+ end
153
+ end
154
+ expose :max_build_jobs
104
155
 
105
- attr_reader :build_commands
156
+ #
157
+ # @!endgroup
158
+ # --------------------------------------------------
106
159
 
107
- def initialize(software, &block)
108
- @software = software
109
- @build_commands = []
110
- @dsl_proxy = DSLProxy.new(self, software)
111
- @dsl_proxy.eval_block(&block) if block_given?
112
- end
160
+ #
161
+ # @!group Ruby DSL methods
162
+ #
163
+ # The following DSL methods are available from within build blocks and
164
+ # expose Ruby DSL methods.
165
+ # --------------------------------------------------
113
166
 
114
- def name
115
- @software.name
167
+ #
168
+ # Execute the given Ruby command or script against the embedded Ruby.
169
+ #
170
+ # @example
171
+ # ruby 'setup.rb'
172
+ #
173
+ # @param (see #command)
174
+ # @return (see #command)
175
+ #
176
+ def ruby(command, options = {})
177
+ build_commands << BuildCommand.new("ruby `#{command}'") do
178
+ bin = windows_safe_path("#{install_dir}/embedded/bin/ruby")
179
+ _shellout!("#{bin} #{command}", options)
180
+ end
116
181
  end
182
+ expose :ruby
117
183
 
118
- def command(*args)
119
- @build_commands << args
184
+ #
185
+ # Execute the given Rubygem command against the embedded Rubygems.
186
+ #
187
+ # @example
188
+ # gem 'install chef'
189
+ #
190
+ # @param (see #command)
191
+ # @return (see #command)
192
+ #
193
+ def gem(command, options = {})
194
+ build_commands << BuildCommand.new("gem `#{command}'") do
195
+ bin = windows_safe_path("#{install_dir}/embedded/bin/gem")
196
+ _shellout!("#{bin} #{command}", options)
197
+ end
120
198
  end
199
+ expose :gem
121
200
 
122
- def patch(*args)
123
- args = args.dup.pop
201
+ #
202
+ # Execute the given bundle command against the embedded Ruby's bundler. This
203
+ # command assumes the +bundler+ gem is installed and in the embedded Ruby.
204
+ # You should add a dependency on the +bundler+ software definition if you
205
+ # want to use this command.
206
+ #
207
+ # @example
208
+ # bundle 'install'
209
+ #
210
+ # @param (see #command)
211
+ # @return (see #command)
212
+ #
213
+ def bundle(command, options = {})
214
+ build_commands << BuildCommand.new("bundle `#{command}'") do
215
+ bin = windows_safe_path("#{install_dir}/embedded/bin/bundle")
216
+ _shellout!("#{bin} #{command}", options)
217
+ end
218
+ end
219
+ expose :bundle
124
220
 
125
- # Search for patches just like we search for software
126
- candidate_paths = Omnibus.software_dirs.map do |directory|
127
- patches = directory.sub(Config.software_dir, 'config/patches')
128
- "#{patches}/#{name}/#{args[:source]}"
221
+ #
222
+ # Execute the given Rake command against the embedded Ruby's rake. This
223
+ # command assumes the +rake+ gem has been installed.
224
+ #
225
+ # @example
226
+ # rake 'test'
227
+ #
228
+ # @param (see #command)
229
+ # @return (see #command)
230
+ #
231
+ def rake(command, options = {})
232
+ build_commands << BuildCommand.new("rake `#{command}'") do
233
+ bin = windows_safe_path("#{install_dir}/embedded/bin/rake")
234
+ _shellout!("#{bin} #{command}", options)
129
235
  end
236
+ end
237
+ expose :rake
238
+
239
+ #
240
+ # Execute the given Ruby block at runtime. The block is captured as-is and
241
+ # no validation is performed. As a general rule, you should avoid this
242
+ # method unless you know what you are doing.
243
+ #
244
+ # TODO: the "name" does nothing right now
245
+ #
246
+ # @example
247
+ # block do
248
+ # # Some complex operation
249
+ # end
250
+ #
251
+ # @example
252
+ # block 'Named operation' do
253
+ # # The above name can be used in log output to identify the operation
254
+ # end
255
+ #
256
+ # @param (see #command)
257
+ # @return (see #command)
258
+ #
259
+ def block(name = '<Dynamic Ruby block>', &proc)
260
+ build_commands << BuildCommand.new(name, &proc)
261
+ end
262
+ expose :block
263
+
264
+ #
265
+ # Render the erb template by the given name. This method will search all
266
+ # possible locations for an erb template (such as {Config#software_gems}).
267
+ #
268
+ # @example
269
+ # erb source: 'example.erb',
270
+ # dest: '/path/on/disk/to/render'
271
+ #
272
+ # @example
273
+ # erb source: 'example.erb',
274
+ # dest: '/path/on/disk/to/render',
275
+ # vars: { foo: 'bar' },
276
+ # mode: '0755'
277
+ #
278
+ # @param [Hash] options
279
+ # the list of options
280
+ #
281
+ # @option options [String] :source
282
+ # the name of the patch to apply
283
+ # @option options [String] :dest
284
+ # the path on disk where the erb should be rendered
285
+ # @option options [Hash] :vars
286
+ # the list of variables to pass to the ERB rendering
287
+ # @option options [String] :mode
288
+ # the file mode for the rendered template (default varies by system)
289
+ #
290
+ # @return (see #command)
291
+ #
292
+ def erb(options = {})
293
+ source = options.delete(:source)
294
+ dest = options.delete(:dest)
295
+ mode = options.delete(:mode) || 0644
296
+ vars = options.delete(:vars) || {}
297
+
298
+ raise "Missing required option `:source'!" unless source
299
+ raise "Missing required option `:dest'!" unless dest
130
300
 
131
- source = candidate_paths.find { |path| File.exist?(path) }
301
+ locations, source_path = find_file('config/templates', source)
132
302
 
133
- unless source
134
- raise MissingPatch.new(args[:source], candidate_paths)
303
+ unless source_path
304
+ raise MissingTemplate.new(source, locations)
135
305
  end
136
306
 
137
- plevel = args[:plevel] || 1
138
- if args[:target]
139
- target = File.expand_path("#{project_dir}/#{args[:target]}")
140
- @build_commands <<
141
- "cat #{source} | patch -p#{plevel} #{target}"
142
- else
143
- @build_commands <<
144
- "patch -d #{project_dir} -p#{plevel} -i #{source}"
307
+ erbs << source_path
308
+
309
+ block "Render erb `#{source}'" do
310
+ template = ERB.new(File.read(source_path), nil, '%')
311
+ struct = OpenStruct.new(vars)
312
+ result = template.result(struct.instance_eval { binding })
313
+
314
+ File.open(dest, 'w') do |file|
315
+ file.write(result)
316
+ end
317
+
318
+ File.chmod(mode, dest)
145
319
  end
146
320
  end
321
+ expose :erb
147
322
 
148
- def erb(*args)
149
- args = args.dup.pop
323
+ #
324
+ # @!endgroup
325
+ # --------------------------------------------------
150
326
 
151
- source_path = File.expand_path("#{Config.project_root}/config/templates/#{name}/#{args[:source]}")
327
+ #
328
+ # @!group File system DSL methods
329
+ #
330
+ # The following DSL methods are available from within build blocks that
331
+ # mutate the file system.
332
+ #
333
+ # **These commands are run from inside {Software#install_dir}, so exercise
334
+ # good judgement when using relative paths!**
335
+ # --------------------------------------------------
152
336
 
153
- unless File.exist?(source_path)
154
- raise MissingTemplate.new(args[:source], "#{Config.project_root}/config/templates/#{name}")
337
+ #
338
+ # Make a directory at runtime. This method uses the equivalent of +mkdir -p+
339
+ # under the covers.
340
+ #
341
+ # @param [String] directory
342
+ # the name or path of the directory to create
343
+ # @param [Hash] options
344
+ # the list of options to pass to the underlying +FileUtils+ call
345
+ #
346
+ # @return (see #command)
347
+ #
348
+ def mkdir(directory, options = {})
349
+ build_commands << BuildCommand.new("mkdir `#{directory}'") do
350
+ Dir.chdir(software.install_dir) do
351
+ FileUtils.mkdir_p(directory, options)
352
+ end
155
353
  end
354
+ end
355
+ expose :mkdir
156
356
 
157
- block do
158
- template = ERB.new(File.new(source_path).read, nil, '%')
159
- File.open(args[:dest], 'w') do |file|
160
- file.write(template.result(OpenStruct.new(args[:vars]).instance_eval { binding }))
357
+ #
358
+ # Touch the given filepath at runtime.
359
+ #
360
+ # @param [String] file
361
+ # the path of the file to touch
362
+ # @param (see #mkdir)
363
+ #
364
+ # @return (see #command)
365
+ #
366
+ def touch(file, options = {})
367
+ build_commands << BuildCommand.new("touch `#{file}'") do
368
+ Dir.chdir(software.install_dir) do
369
+ FileUtils.touch(file, options)
161
370
  end
162
-
163
- File.chmod(args[:mode], args[:dest])
164
371
  end
165
372
  end
373
+ expose :touch
166
374
 
167
- # @todo all these ruby commands (ruby, gem, bundle, rake) could
168
- # all be collapsed into a single underlying implementation, since
169
- # they all just differ on the executable being called
170
- def ruby(*args)
171
- @build_commands << bundle_bust(*prepend_cmd("#{install_dir}/embedded/bin/ruby", *args))
375
+ #
376
+ # Delete the given file or directory on the system. This method uses the
377
+ # equivalent of +rm -rf+.
378
+ #
379
+ # @param [String] path
380
+ # the path of the file to delete
381
+ # @param (see #mkdir)
382
+ #
383
+ # @return (see #command)
384
+ #
385
+ def delete(path, options = {})
386
+ build_commands << BuildCommand.new("delete `#{path}'") do
387
+ Dir.chdir(software.install_dir) do
388
+ FileUtils.rm_rf(path, options)
389
+ end
390
+ end
172
391
  end
392
+ expose :delete
173
393
 
174
- def gem(*args)
175
- @build_commands << bundle_bust(*prepend_cmd("#{install_dir}/embedded/bin/gem", *args))
394
+ #
395
+ # Copy the given source to the destination.
396
+ #
397
+ # @param [String] source
398
+ # the path on disk to copy from
399
+ # @param [String] destination
400
+ # the path on disk to copy to
401
+ # @param (see #mkdir)
402
+ #
403
+ # @return (see #command)
404
+ #
405
+ def copy(source, destination, options = {})
406
+ build_commands << BuildCommand.new("copy `#{source}' to `#{destination}'") do
407
+ Dir.chdir(software.install_dir) do
408
+ FileUtils.cp_r(source, destination, options)
409
+ end
410
+ end
176
411
  end
412
+ expose :copy
177
413
 
178
- def bundle(*args)
179
- @build_commands << bundle_bust(*prepend_cmd("#{install_dir}/embedded/bin/bundle", *args))
414
+ #
415
+ # Copy the given source to the destination.
416
+ #
417
+ # @param [String] source
418
+ # the path on disk to move from
419
+ # @param [String] destination
420
+ # the path on disk to move to
421
+ # @param (see #mkdir)
422
+ #
423
+ # @return (see #command)
424
+ #
425
+ def move(source, destination, options = {})
426
+ build_commands << BuildCommand.new("move `#{source}' to `#{destination}'") do
427
+ Dir.chdir(software.install_dir) do
428
+ FileUtils.mv(source, destination, options)
429
+ end
430
+ end
180
431
  end
432
+ expose :move
181
433
 
182
- def rake(*args)
183
- @build_commands << bundle_bust(*prepend_cmd("#{install_dir}/embedded/bin/rake", *args))
434
+ #
435
+ # Link the given source to the destination.
436
+ #
437
+ # @param [String] source
438
+ # the path on disk to link from
439
+ # @param [String] destination
440
+ # the path on disk to link to
441
+ # @param (see #mkdir)
442
+ #
443
+ # @return (see #command)
444
+ #
445
+ def link(source, destination, options = {})
446
+ build_commands << BuildCommand.new("link `#{source}' to `#{destination}'") do
447
+ Dir.chdir(software.install_dir) do
448
+ FileUtils.ln_s(source, destination, options)
449
+ end
450
+ end
184
451
  end
452
+ expose :link
185
453
 
186
- def block(&rb_block)
187
- @build_commands << rb_block
188
- end
454
+ #
455
+ # @!endgroup
456
+ # --------------------------------------------------
457
+
458
+ #
459
+ # @!group Deprecated DSL methods
460
+ #
461
+ # The following DSL methods are available from within build blocks, but are
462
+ # deprecated and will be removed in the next major release.
463
+ # --------------------------------------------------
189
464
 
190
465
  #
191
466
  # @deprecated Use {Config.project_root} instead
@@ -197,177 +472,250 @@ module Omnibus
197
472
 
198
473
  Config.project_root
199
474
  end
475
+ expose :project_root
200
476
 
201
- def project_dir
202
- @software.project_dir
203
- end
477
+ #
478
+ # @!endgroup
479
+ # --------------------------------------------------
204
480
 
205
481
  #
206
- # @deprecated Use {install_path} instead
482
+ # @!group Public API
207
483
  #
208
- def install_path
209
- log.deprecated(log_key) do
210
- 'install_path (DSL). Please use install_dir instead.'
211
- end
212
-
213
- install_dir
214
- end
215
-
216
- def install_dir
217
- @software.install_dir
218
- end
484
+ # The following methods are considered part of the public API for a
485
+ # builder. All DSL methods are also considered part of the public API.
486
+ # --------------------------------------------------
219
487
 
488
+ #
489
+ # Execute all the {BuildCommand} instances, in order, for this builder.
490
+ #
491
+ # @return [void]
492
+ #
220
493
  def build
221
- log.info(log_key) { 'Starting...' }
494
+ log.info(log_key) { 'Starting build' }
222
495
 
223
- if @software.overridden?
496
+ if software.overridden?
224
497
  log.info(log_key) do
225
- "Version overridden from #{@software.default_version} to "\
226
- "#{@software.version}"
498
+ "Version overridden from #{software.default_version} to "\
499
+ "#{software.version}"
227
500
  end
228
501
  end
229
502
 
230
- time_it('Build') do
231
- @build_commands.each do |cmd|
232
- execute(cmd)
503
+ measure("Build #{software.name}") do
504
+ build_commands.each do |command|
505
+ execute(command)
233
506
  end
234
507
  end
235
- end
236
508
 
237
- def execute(cmd)
238
- case cmd
239
- when Proc
240
- execute_proc(cmd)
241
- else
242
- execute_sh(cmd)
243
- end
509
+ log.info(log_key) { 'Finished build' }
244
510
  end
245
511
 
246
- def max_build_jobs
247
- if Ohai['cpu'] && Ohai['cpu']['total'] && Ohai['cpu']['total'].to_s =~ /^\d+$/
248
- Ohai['cpu']['total'].to_i + 1
249
- else
250
- 3
512
+ #
513
+ # The shasum for this builder object. The shasum is calculated using the
514
+ # following:
515
+ #
516
+ # - The descriptions of all {BuildCommand} objects
517
+ # - The digest of all patch files on disk
518
+ # - The digest of all erb files on disk
519
+ #
520
+ # @return [String]
521
+ #
522
+ def shasum
523
+ @shasum ||= begin
524
+ digest = Digest::SHA256.new
525
+
526
+ build_commands.each do |build_command|
527
+ update_with_string(digest, build_command.description)
528
+ end
529
+
530
+ patches.each do |patch_path|
531
+ update_with_file_contents(digest, patch_path)
532
+ end
533
+
534
+ erbs.each do |erb_path|
535
+ update_with_file_contents(digest, erb_path)
536
+ end
537
+
538
+ digest.hexdigest
251
539
  end
252
540
  end
253
541
 
542
+ #
543
+ # @!endgroup
544
+ # --------------------------------------------------
545
+
254
546
  private
255
547
 
256
- def execute_proc(cmd)
257
- cmd.call
258
- rescue Exception => e
259
- # In Ruby 1.9, Procs have a #source_location method with file/line info.
260
- # Too bad we can't use it :(
261
- ErrorReporter.new(e, self).explain("Failed to build #{name} while running ruby block build step")
262
- raise
548
+ #
549
+ # The **in-order** list of {BuildCommand} for this builder.
550
+ #
551
+ # @return [Array<BuildCommand>]
552
+ #
553
+ def build_commands
554
+ @build_commands ||= []
263
555
  end
264
556
 
265
- def execute_sh(cmd)
266
- retries ||= 0
267
- shell = nil
268
- cmd_args = Array(cmd)
269
- options = {
270
- cwd: project_dir,
271
- timeout: 5400,
272
- }
273
- options[:live_stream] = log.live_stream(:debug)
274
- if cmd_args.last.is_a? Hash
275
- cmd_options = cmd_args.last
276
- cmd_args[cmd_args.size - 1] = options.merge(cmd_options)
277
- else
278
- cmd_args << options
279
- end
557
+ #
558
+ # The list of paths to patch files on disk. This is used in the calculation
559
+ # of the shasum.
560
+ #
561
+ # @return [Array<String>]
562
+ #
563
+ def patches
564
+ @patches ||= []
565
+ end
280
566
 
281
- cmd_string = cmd_args[0..-2].join(' ')
282
- cmd_opts_for_display = to_kv_str(cmd_args.last)
567
+ #
568
+ # The list of paths to erb files on disk. This is used in the calculation
569
+ # of the shasum.
570
+ #
571
+ # @return [Array<String>]
572
+ #
573
+ def erbs
574
+ @erbs ||= []
575
+ end
283
576
 
284
- log.info(log_key) do
285
- "Executing: `#{cmd_string}` with #{cmd_opts_for_display}"
286
- end
577
+ #
578
+ # This is a helper method that wraps {Util#shellout!} for the purposes of
579
+ # setting the +:cwd+ value.
580
+ #
581
+ # @see (Util#shellout!)
582
+ #
583
+ def _shellout!(command_string, options = {})
584
+ # Make sure the PWD is set to the correct directory
585
+ options = { cwd: software.project_dir }.merge(options)
287
586
 
288
- shell = Mixlib::ShellOut.new(*cmd)
289
- shell.environment['HOME'] = '/tmp' unless ENV['HOME']
587
+ # Use Util's shellout
588
+ shellout!(command_string, options)
589
+ end
290
590
 
291
- cmd_name = cmd_string.split(/\s+/).first
292
- time_it("#{cmd_name} command") do
293
- shell.run_command
294
- shell.error!
295
- end
296
- rescue Exception => e
297
- raise if Config.build_retries.nil? || Config.build_retries == 0
298
- # Getting lots of errors from github, particularly with erlang/rebar
299
- # projects fetching tons of deps via git all the time. This isn't a
300
- # particularly elegant way to solve that problem. But it should work.
301
- if retries >= Config.build_retries
302
- ErrorReporter.new(e, self).explain("Failed to build #{name} while running `#{cmd_string}` with #{cmd_opts_for_display}")
303
- raise
304
- else
305
- time_to_sleep = 5 * (2**retries)
306
- retries += 1
307
- log.info(log_key) do
308
- "Failed to execute cmd `#{cmd}` #{retries} time(s). " \
309
- "Retrying in #{time_to_sleep}s."
591
+ #
592
+ # Execute the given command object. This method also wraps the following
593
+ # operations:
594
+ #
595
+ # - Reset bundler's environment using +Bundler.with_clean_env+
596
+ # - Instrument (time/measure) the individual command's execution
597
+ # - Retry failed commands in accordance with {Config#build_retries}
598
+ #
599
+ # @param [BuildCommand] command
600
+ # the command object to build
601
+ #
602
+ def execute(command)
603
+ Bundler.with_clean_env do
604
+ measure(command.description) do
605
+ with_retries do
606
+ command.run(self)
607
+ end
310
608
  end
311
- sleep(time_to_sleep)
312
- retry
313
609
  end
314
610
  end
315
611
 
316
- def prepend_cmd(str, *cmd_args)
317
- if cmd_args.size == 1
318
- # command as a string, no opts
319
- "#{str} #{cmd_args.first}"
320
- elsif cmd_args.size == 2 && cmd_args.last.is_a?(Hash)
321
- # command as a string w/ opts
322
- ["#{str} #{cmd_args.first}", cmd_args.last]
323
- elsif cmd_args.size == 0
324
- raise ArgumentError, "I don't even"
325
- else
326
- # cmd given as argv array
327
- cmd_args.dup.unshift(str)
612
+ #
613
+ # Execute the given block with (n) reties defined by {Config#build_retries}.
614
+ # This method will only retry for the following exceptions:
615
+ #
616
+ # - +Mixlib::ShellOut::ShellCommandFailed+
617
+ # - +Mixlib::ShellOut::CommandTimeout+
618
+ #
619
+ # @param [Proc] block
620
+ # the block to execute
621
+ #
622
+ def with_retries(&block)
623
+ tries = Config.build_retries
624
+ delay = 5
625
+ exceptions = [
626
+ Mixlib::ShellOut::ShellCommandFailed,
627
+ Mixlib::ShellOut::CommandTimeout,
628
+ ]
629
+
630
+ begin
631
+ block.call
632
+ rescue *exceptions => e
633
+ if tries <= 0
634
+ raise e
635
+ else
636
+ delay = delay * 2
637
+
638
+ log.warn(log_key) do
639
+ label = "#{(Config.build_retries - tries) + 1}/#{Config.build_retries}"
640
+ "[#{label}] Failed to execute command. Retrying in #{delay} seconds..."
641
+ end
642
+
643
+ sleep(delay)
644
+ tries -= 1
645
+ retry
646
+ end
328
647
  end
329
648
  end
330
649
 
331
- def bundle_bust(*cmd_args)
332
- if cmd_args.last.is_a?(Hash)
333
- cmd_args = cmd_args.dup
334
- cmd_opts = cmd_args.pop.dup
335
- cmd_opts[:env] = cmd_opts[:env] ? BUNDLER_BUSTER.merge(cmd_opts[:env]) : BUNDLER_BUSTER
336
- cmd_args << cmd_opts
337
- else
338
- cmd_args << { env: BUNDLER_BUSTER }
650
+ #
651
+ # Find a file amonst all local files, "remote" local files, and
652
+ # {Config#software_gems}.
653
+ #
654
+ # @param [String] path
655
+ # the path to find the file
656
+ # @param [String] source
657
+ # the source name of the file to find
658
+ #
659
+ # @return [Array<Array<String>, String, nil>]
660
+ # an array where the first entry is the list of candidate paths searched,
661
+ # and the second entry is the first occurence of the matched file (or
662
+ # +nil+) if one does not exist.
663
+ #
664
+ def find_file(path, source)
665
+ # Search for patches just like we search for software
666
+ candidate_paths = Omnibus.software_dirs.map do |directory|
667
+ full_path = directory.sub(Config.software_dir, path)
668
+ "#{full_path}/#{software.name}/#{source}"
339
669
  end
340
- end
341
670
 
342
- def time_it(what)
343
- start = Time.now
344
- yield
345
- rescue Exception
346
- elapsed = Time.now - start
347
- log.warn(log_key) { "#{what} failed! (#{elapsed.to_f}s)" }
348
- raise
349
- else
350
- elapsed = Time.now - start
351
- log.info(log_key) { "#{what} succeeded! (#{elapsed.to_f}s)" }
352
- end
671
+ file = candidate_paths.find { |path| File.exist?(path) }
353
672
 
354
- # Convert a hash to a string in the form `key=value`. It should work with
355
- # whatever input is given but is designed to make the options to ShellOut
356
- # look nice.
357
- def to_kv_str(hash, join_str = ',')
358
- hash.reduce([]) do |kv_pair_strs, (k, v)|
359
- val_str = case v
360
- when Hash
361
- %Q("#{to_kv_str(v, " ") }")
362
- else
363
- v.to_s
364
- end
365
- kv_pair_strs << "#{k}=#{val_str}"
366
- end.join(join_str)
673
+ [candidate_paths, file]
367
674
  end
368
675
 
676
+ #
677
+ # The log key for this class, overriden to incorporate the software name.
678
+ #
679
+ # @return [String]
680
+ #
369
681
  def log_key
370
- @log_key ||= "#{super}: #{name}"
682
+ @log_key ||= "#{super}: #{software.name}"
683
+ end
684
+
685
+ #
686
+ # This is an internal wrapper around a command executed on the system. The
687
+ # block could contain a Ruby command (such as +FileUtils.rm_rf('/')+), or it
688
+ # could contain a call to shell out to the system.
689
+ #
690
+ class BuildCommand
691
+ attr_reader :description
692
+
693
+ #
694
+ # Create a new BuildCommand object.
695
+ #
696
+ # @param [String] description
697
+ # a unique identifier for this build command - it will be used for
698
+ # logging and timing labels
699
+ # @param [Proc] block
700
+ # the block to capture
701
+ #
702
+ def initialize(description, &block)
703
+ @description, @block = description, block
704
+ end
705
+
706
+ #
707
+ # Execute the build command against the given object. Because BuildCommand
708
+ # objects could reference internal DSL methods, this method requires you
709
+ # pass in an object against which to +instance_eval+ the block. Otherwise,
710
+ # you would be severly restricted in the commands avaiable to you via the
711
+ # DSL.
712
+ #
713
+ # @param [Builder] builder
714
+ # the builder to +instance_eval+ against
715
+ #
716
+ def run(builder)
717
+ builder.instance_eval(&@block)
718
+ end
371
719
  end
372
720
  end
373
721
  end