omnibus 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +1 -0
  4. data/.yardopts +7 -0
  5. data/CHANGELOG.md +3 -0
  6. data/Gemfile +9 -0
  7. data/LICENSE +201 -0
  8. data/NOTICE +9 -0
  9. data/README.md +186 -0
  10. data/Rakefile +7 -0
  11. data/bin/makeself-header.sh +401 -0
  12. data/bin/makeself.sh +407 -0
  13. data/bin/omnibus +11 -0
  14. data/lib/omnibus.rb +280 -0
  15. data/lib/omnibus/build_version.rb +281 -0
  16. data/lib/omnibus/builder.rb +323 -0
  17. data/lib/omnibus/clean_tasks.rb +30 -0
  18. data/lib/omnibus/cli.rb +35 -0
  19. data/lib/omnibus/cli/application.rb +136 -0
  20. data/lib/omnibus/cli/base.rb +112 -0
  21. data/lib/omnibus/cli/build.rb +66 -0
  22. data/lib/omnibus/cli/cache.rb +60 -0
  23. data/lib/omnibus/config.rb +186 -0
  24. data/lib/omnibus/exceptions.rb +54 -0
  25. data/lib/omnibus/fetcher.rb +184 -0
  26. data/lib/omnibus/fetchers.rb +22 -0
  27. data/lib/omnibus/fetchers/git_fetcher.rb +212 -0
  28. data/lib/omnibus/fetchers/net_fetcher.rb +191 -0
  29. data/lib/omnibus/fetchers/path_fetcher.rb +65 -0
  30. data/lib/omnibus/fetchers/s3_cache_fetcher.rb +42 -0
  31. data/lib/omnibus/health_check.rb +260 -0
  32. data/lib/omnibus/library.rb +70 -0
  33. data/lib/omnibus/overrides.rb +69 -0
  34. data/lib/omnibus/project.rb +566 -0
  35. data/lib/omnibus/reports.rb +99 -0
  36. data/lib/omnibus/s3_cacher.rb +136 -0
  37. data/lib/omnibus/software.rb +430 -0
  38. data/lib/omnibus/templates/Berksfile.erb +3 -0
  39. data/lib/omnibus/templates/Gemfile.erb +4 -0
  40. data/lib/omnibus/templates/README.md.erb +102 -0
  41. data/lib/omnibus/templates/Vagrantfile.erb +95 -0
  42. data/lib/omnibus/templates/gitignore.erb +8 -0
  43. data/lib/omnibus/templates/omnibus.rb.example.erb +5 -0
  44. data/lib/omnibus/templates/package_scripts/makeselfinst.erb +27 -0
  45. data/lib/omnibus/templates/package_scripts/postinst.erb +17 -0
  46. data/lib/omnibus/templates/package_scripts/postrm.erb +9 -0
  47. data/lib/omnibus/templates/project.rb.erb +21 -0
  48. data/lib/omnibus/templates/software/c-example.rb.erb +42 -0
  49. data/lib/omnibus/templates/software/erlang-example.rb.erb +38 -0
  50. data/lib/omnibus/templates/software/ruby-example.rb.erb +24 -0
  51. data/lib/omnibus/util.rb +61 -0
  52. data/lib/omnibus/version.rb +20 -0
  53. data/omnibus.gemspec +34 -0
  54. data/spec/build_version_spec.rb +228 -0
  55. data/spec/data/overrides/bad_line.overrides +3 -0
  56. data/spec/data/overrides/good.overrides +5 -0
  57. data/spec/data/overrides/with_dupes.overrides +4 -0
  58. data/spec/data/software/erchef.rb +40 -0
  59. data/spec/overrides_spec.rb +114 -0
  60. data/spec/software_spec.rb +71 -0
  61. data/spec/spec_helper.rb +28 -0
  62. metadata +239 -0
@@ -0,0 +1,566 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2012 Opscode, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+ require 'omnibus/exceptions'
18
+
19
+ module Omnibus
20
+
21
+ # Omnibus project DSL reader
22
+ #
23
+ # @todo It seems like there's a bit of a conflation between a
24
+ # "project" and a "package" in this class... perhaps the
25
+ # package-building portions should be extracted to a separate
26
+ # class.
27
+ # @todo: Reorder DSL methods to fit in the same YARD group
28
+ # @todo: Generate the DSL methods via metaprogramming... they're all so similar
29
+ class Project
30
+ include Rake::DSL
31
+
32
+ # @todo Why not just use `nil`?
33
+ NULL_ARG = Object.new
34
+
35
+ # Convenience method to initialize a Project from a DSL file.
36
+ #
37
+ # @param filename [String] the filename of the Project DSL file to load.
38
+ def self.load(filename)
39
+ new(IO.read(filename), filename)
40
+ end
41
+
42
+
43
+ # Create a new Project from the contents of a DSL file. Prefer
44
+ # calling {Omnibus::Project#load} instead of using this method
45
+ # directly.
46
+ #
47
+ # @param io [String] the contents of a Project DSL (_not_ the filename!)
48
+ # @param filename [String] unused!
49
+ #
50
+ # @see Omnibus::Project#load
51
+ #
52
+ # @todo Remove filename parameter, as it is unused.
53
+ def initialize(io, filename)
54
+ @exclusions = Array.new
55
+ @conflicts = Array.new
56
+ @dependencies = Array.new
57
+ @runtime_dependencies = Array.new
58
+ instance_eval(io)
59
+ validate
60
+ render_tasks
61
+ end
62
+
63
+ # Ensures that certain project information has been set
64
+ #
65
+ # @raise [MissingProjectConfiguration] if a required parameter has
66
+ # not been set
67
+ # @return [void]
68
+ def validate
69
+ name && install_path && maintainer && homepage
70
+ end
71
+
72
+ # @!group DSL methods
73
+ # Here is some broad documentation for the DSL methods as a whole.
74
+
75
+ # Set or retrieve the name of the project
76
+ #
77
+ # @param val [String] the name to set
78
+ # @return [String]
79
+ #
80
+ # @raise [MissingProjectConfiguration] if a value was not set
81
+ # before being subsequently retrieved (i.e., a name
82
+ # must be set in order to build a project)
83
+ def name(val=NULL_ARG)
84
+ @name = val unless val.equal?(NULL_ARG)
85
+ @name || raise(MissingProjectConfiguration.new("name", "my_project"))
86
+ end
87
+
88
+ # Set or retrieve the package name of the project. Unless
89
+ # explicitly set, the package name defaults to the project name
90
+ #
91
+ # @param val [String] the package name to set
92
+ # @return [String]
93
+ def package_name(val=NULL_ARG)
94
+ @package_name = val unless val.equal?(NULL_ARG)
95
+ @package_name.nil? ? @name : @package_name
96
+ end
97
+
98
+ # Set or retrieve the path at which the project should be
99
+ # installed by the generated package.
100
+ #
101
+ # @param val [String]
102
+ # @return [String]
103
+ #
104
+ # @raise [MissingProjectConfiguration] if a value was not set
105
+ # before being subsequently retrieved (i.e., an install_path
106
+ # must be set in order to build a project)
107
+ def install_path(val=NULL_ARG)
108
+ @install_path = val unless val.equal?(NULL_ARG)
109
+ @install_path || raise(MissingProjectConfiguration.new("install_path", "/opt/opscode"))
110
+ end
111
+
112
+ # Set or retrieve the the package maintainer.
113
+ #
114
+ # @param val [String]
115
+ # @return [String]
116
+ #
117
+ # @raise [MissingProjectConfiguration] if a value was not set
118
+ # before being subsequently retrieved (i.e., a maintainer must
119
+ # be set in order to build a project)
120
+ def maintainer(val=NULL_ARG)
121
+ @maintainer = val unless val.equal?(NULL_ARG)
122
+ @maintainer || raise(MissingProjectConfiguration.new("maintainer", "Opscode, Inc."))
123
+ end
124
+
125
+ # Set or retrive the package homepage.
126
+ #
127
+ # @param val [String]
128
+ # @return [String]
129
+ #
130
+ # @raise [MissingProjectConfiguration] if a value was not set
131
+ # before being subsequently retrieved (i.e., a homepage must be
132
+ # set in order to build a project)
133
+ def homepage(val=NULL_ARG)
134
+ @homepage = val unless val.equal?(NULL_ARG)
135
+ @homepage || raise(MissingProjectConfiguration.new("homepage", "http://www.opscode.com"))
136
+ end
137
+
138
+ # Defines the iteration for the package to be generated. Adheres
139
+ # to the conventions of the platform for which the package is
140
+ # being built.
141
+ #
142
+ # All iteration strings begin with the value set in {#build_iteration}
143
+ #
144
+ # @return [String]
145
+ def iteration
146
+ case platform_family
147
+ when 'rhel'
148
+ platform_version =~ /^(\d+)/
149
+ maj = $1
150
+ "#{build_iteration}.el#{maj}"
151
+ when 'windows'
152
+ "#{build_iteration}.windows"
153
+ else
154
+ "#{build_iteration}.#{platform}.#{platform_version}"
155
+ end
156
+ end
157
+
158
+ # Set or retrieve the project description. Defaults to `"The full
159
+ # stack of #{name}"`
160
+ #
161
+ # Corresponds to the `--description` flag of
162
+ # {https://github.com/jordansissel/fpm fpm}.
163
+ #
164
+ # @param val [String] the project description
165
+ # @return [String]
166
+ #
167
+ # @see #name
168
+ def description(val=NULL_ARG)
169
+ @description = val unless val.equal?(NULL_ARG)
170
+ @description || "The full stack of #{name}"
171
+ end
172
+
173
+ # Set or retrieve the name of the package this package will replace.
174
+ #
175
+ # Ultimately used as the value for the `--replaces` flag in
176
+ # {https://github.com/jordansissel/fpm fpm}.
177
+ #
178
+ # @param val [String] the name of the package to replace
179
+ # @return [String]
180
+ #
181
+ # @todo Consider having this default to {#package_name}; many uses of this
182
+ # method effectively do this already.
183
+ def replaces(val=NULL_ARG)
184
+ @replaces = val unless val.equal?(NULL_ARG)
185
+ @replaces
186
+ end
187
+
188
+ # Add to the list of packages this one conflicts with.
189
+ #
190
+ # Specifying conflicts is optional. See the `--conflicts` flag in
191
+ # {https://github.com/jordansissel/fpm fpm}.
192
+ #
193
+ # @param val [String]
194
+ # @return [void]
195
+ def conflict(val)
196
+ @conflicts << val
197
+ end
198
+
199
+ # Set or retrieve the version of the project.
200
+ #
201
+ # @param val [String] the version to set
202
+ # @return [String]
203
+ #
204
+ # @see Omnibus::BuildVersion
205
+ def build_version(val=NULL_ARG)
206
+ @build_version = val unless val.equal?(NULL_ARG)
207
+ @build_version
208
+ end
209
+
210
+ # Set or retrieve the build iteration of the project. Defaults to
211
+ # `1` if not otherwise set.
212
+ #
213
+ # @param val [Fixnum]
214
+ # @return [Fixnum]
215
+ #
216
+ # @todo Is there a better name for this than "build_iteration"?
217
+ # Would be nice to cut down confusiton with {#iteration}.
218
+ def build_iteration(val=NULL_ARG)
219
+ @build_iteration = val unless val.equal?(NULL_ARG)
220
+ @build_iteration || 1
221
+ end
222
+
223
+ # Add an Omnibus software dependency.
224
+ #
225
+ # Note that this is a *build time* dependency. If you need to
226
+ # specify an external dependency that is required at runtime, see
227
+ # {#runtime_dependency} instead.
228
+ #
229
+ # @param val [String] the name of a Software dependency
230
+ # @return [void]
231
+ def dependency(val)
232
+ @dependencies << val
233
+ end
234
+
235
+ # Add a package that is a runtime dependency of this
236
+ # project.
237
+ #
238
+ # This is distinct from a build-time dependency, which should
239
+ # correspond to an Omnibus software definition.
240
+ #
241
+ # Corresponds to the `--depends` flag of
242
+ # {https://github.com/jordansissel/fpm fpm}.
243
+ #
244
+ # @param val [String] the name of the runtime dependency
245
+ # @return [void]
246
+ def runtime_dependency(val)
247
+ @runtime_dependencies << val
248
+ end
249
+
250
+ # Set or retrieve the list of software dependencies for this
251
+ # project. As this is a DSL method, only pass the names of
252
+ # software components, not {Omnibus::Software} objects.
253
+ #
254
+ # These is the software that comprises your project, and is
255
+ # distinct from runtime dependencies.
256
+ #
257
+ # @note This will reinitialize the internal depdencies Array
258
+ # and overwrite any dependencies that may have been set using
259
+ # {#dependency}.
260
+ #
261
+ # @param val [Array<String>] a list of names of Software components
262
+ # @return [Array<String>]
263
+ def dependencies(val=NULL_ARG)
264
+ @dependencies = val unless val.equal?(NULL_ARG)
265
+ @dependencies
266
+ end
267
+
268
+ # Add a new exclusion pattern.
269
+ #
270
+ # Corresponds to the `--exclude` flag of {https://github.com/jordansissel/fpm fpm}.
271
+ #
272
+ # @param pattern [String]
273
+ # @return void
274
+ def exclude(pattern)
275
+ @exclusions << pattern
276
+ end
277
+
278
+ # Returns the platform version of the machine on which Omnibus is
279
+ # running, as determined by Ohai.
280
+ #
281
+ # @return [String]
282
+ def platform_version
283
+ OHAI.platform_version
284
+ end
285
+
286
+ # Returns the platform of the machine on which Omnibus is running,
287
+ # as determined by Ohai.
288
+ #
289
+ # @return [String]
290
+ def platform
291
+ OHAI.platform
292
+ end
293
+
294
+ # Returns the platform family of the machine on which Omnibus is
295
+ # running, as determined by Ohai.
296
+ #
297
+ # @return [String]
298
+ def platform_family
299
+ OHAI.platform_family
300
+ end
301
+
302
+ # Convenience method for accessing the global Omnibus configuration object.
303
+ #
304
+ # @return Omnibus::Config
305
+ #
306
+ # @see Omnibus::Config
307
+ def config
308
+ Omnibus.config
309
+ end
310
+
311
+ # The path to the package scripts directory for this project.
312
+ # These are optional scripts that can be bundled into the
313
+ # resulting package for running at various points in the package
314
+ # management lifecycle.
315
+ #
316
+ # Currently supported scripts include:
317
+ #
318
+ # * postinst
319
+ #
320
+ # A post-install script
321
+ # * prerm
322
+ #
323
+ # A pre-uninstall script
324
+ # * postrm
325
+ #
326
+ # A post-uninstall script
327
+ #
328
+ # Any scripts with these names that are present in the package
329
+ # scripts directory will be incorporated into the package that is
330
+ # built. This only applies to fpm-built packages.
331
+ #
332
+ # Additionally, there may be a `makeselfinst` script.
333
+ #
334
+ # @return [String]
335
+ #
336
+ # @todo This documentation really should be up at a higher level,
337
+ # particularly since the user has no way to change the path.
338
+ def package_scripts_path
339
+ "#{Omnibus.project_root}/package-scripts/#{name}"
340
+ end
341
+
342
+ # Determine the package type(s) to be built, based on the platform
343
+ # family for which the package is being built.
344
+ #
345
+ # If specific types cannot be determined, default to `["makeself"]`.
346
+ #
347
+ # @return [Array<(String)>]
348
+ #
349
+ # @todo Why does this only ever return a single-element array,
350
+ # instead of just a string, or symbol?
351
+ def package_types
352
+ case platform_family
353
+ when 'debian'
354
+ [ "deb" ]
355
+ when 'fedora', 'rhel'
356
+ [ "rpm" ]
357
+ when 'solaris2'
358
+ [ "solaris" ]
359
+ when 'windows'
360
+ [ "msi" ]
361
+ else
362
+ [ "makeself" ]
363
+ end
364
+ end
365
+
366
+ # Indicates whether `software` is defined as a software component
367
+ # of this project.
368
+ #
369
+ # @param software [String, Omnibus::Software, #name]
370
+ # @return [Boolean]
371
+ #
372
+ # @see #dependencies
373
+ def dependency?(software)
374
+ name = if software.respond_to?(:name)
375
+ software.send(:name)
376
+ elsif
377
+ software
378
+ end
379
+ @dependencies.include?(name)
380
+ end
381
+
382
+ # @!endgroup
383
+
384
+ private
385
+
386
+ # The command to generate an MSI package on Windows platforms.
387
+ #
388
+ # Does not execute the command, only assembles it.
389
+ #
390
+ # @return [Array<(String, Hash)>] The complete MSI command, plus a
391
+ # Hash of options to be passed on to Mixlib::ShellOut
392
+ #
393
+ # @see Mixlib::ShellOut
394
+ #
395
+ # @todo For this and all the *_command methods, just return a
396
+ # Mixlib::ShellOut object ready for execution. Using Arrays
397
+ # makes downstream processing needlessly complicated.
398
+ def msi_command
399
+ msi_command = ["light.exe",
400
+ "-nologo",
401
+ "-ext WixUIExtension",
402
+ "-cultures:en-us",
403
+ "-loc #{install_path}\\msi-tmp\\#{package_name}-en-us.wxl",
404
+ "#{install_path}\\msi-tmp\\#{package_name}-Files.wixobj",
405
+ "#{install_path}\\msi-tmp\\#{package_name}.wixobj",
406
+ "-out #{config.package_dir}\\#{package_name}-#{build_version}-#{iteration}.msi"]
407
+
408
+ # Don't care about the 204 return code from light.exe since it's
409
+ # about some expected warnings...
410
+ [msi_command.join(" "), {:returns => [0, 204]}]
411
+ end
412
+
413
+ # The {https://github.com/jordansissel/fpm fpm} command to
414
+ # generate a package for RedHat, Ubuntu, Solaris, etc. platforms.
415
+ #
416
+ # Does not execute the command, only assembles it.
417
+ #
418
+ # In contrast to {#msi_command}, command generated by
419
+ # {#fpm_command} does not require any Mixlib::Shellout options.
420
+ #
421
+ # @return [Array<String>] the components of the fpm command; need
422
+ # to be joined with " " first.
423
+ #
424
+ # @todo Just make this return a String instead of an Array
425
+ # @todo Use the long option names (i.e., the double-dash ones) in
426
+ # the fpm command for maximum clarity.
427
+ def fpm_command(pkg_type)
428
+ command_and_opts = ["fpm",
429
+ "-s dir",
430
+ "-t #{pkg_type}",
431
+ "-v #{build_version}",
432
+ "-n #{package_name}",
433
+ "--iteration #{iteration}",
434
+ install_path,
435
+ "-m '#{maintainer}'",
436
+ "--description '#{description}'",
437
+ "--url #{homepage}"]
438
+ if File.exist?("#{package_scripts_path}/postinst")
439
+ command_and_opts << "--post-install '#{package_scripts_path}/postinst'"
440
+ end
441
+ # solaris packages don't support --pre-uninstall
442
+ if File.exist?("#{package_scripts_path}/prerm") && pkg_type != "solaris"
443
+ command_and_opts << "--pre-uninstall '#{package_scripts_path}/prerm'"
444
+ end
445
+ # solaris packages don't support --post-uninstall
446
+ if File.exist?("#{package_scripts_path}/postrm") && pkg_type != "solaris"
447
+ command_and_opts << "--post-uninstall '#{package_scripts_path}/postrm'"
448
+ end
449
+
450
+ @exclusions.each do |pattern|
451
+ command_and_opts << "--exclude '#{pattern}'"
452
+ end
453
+
454
+ @runtime_dependencies.each do |runtime_dep|
455
+ command_and_opts << "--depends '#{runtime_dep}'"
456
+ end
457
+
458
+ @conflicts.each do |conflict|
459
+ command_and_opts << "--conflicts '#{conflict}'"
460
+ end
461
+
462
+ command_and_opts << " --replaces #{@replaces}" if @replaces
463
+ command_and_opts
464
+ end
465
+
466
+ # TODO: what's this do?
467
+ def makeself_command
468
+ command_and_opts = [ File.expand_path(File.join(Omnibus.source_root, "bin", "makeself.sh")),
469
+ "--gzip",
470
+ install_path,
471
+ "#{package_name}-#{build_version}_#{iteration}.sh",
472
+ "'The full stack of #{@name}'"
473
+ ]
474
+ command_and_opts << "./makeselfinst" if File.exists?("#{package_scripts_path}/makeselfinst")
475
+ command_and_opts
476
+ end
477
+
478
+ # Dynamically generate Rake tasks to build projects and all the software they depend on.
479
+ #
480
+ # @note Much Rake magic ahead!
481
+ #
482
+ # @return void
483
+ def render_tasks
484
+ directory config.package_dir
485
+ directory "pkg"
486
+
487
+ namespace :projects do
488
+
489
+ package_types.each do |pkg_type|
490
+ namespace @name do
491
+ desc "package #{@name} into a #{pkg_type}"
492
+ task pkg_type => (@dependencies.map {|dep| "software:#{dep}"}) do
493
+
494
+ package_commands = []
495
+ if pkg_type == "makeself"
496
+ # copy the makeself installer into package
497
+ if File.exists?("#{package_scripts_path}/makeselfinst")
498
+ package_commands << "cp #{package_scripts_path}/makeselfinst #{install_path}/"
499
+ end
500
+
501
+ # run the makeself program
502
+ package_commands << makeself_command.join(" ")
503
+
504
+ # rm the makeself installer (for incremental builds)
505
+ package_commands << "rm -f #{install_path}/makeselfinst"
506
+ elsif pkg_type == "msi"
507
+ package_commands << msi_command
508
+ else # pkg_type == "fpm"
509
+ package_commands << fpm_command(pkg_type).join(" ")
510
+ end
511
+
512
+ # run the commands
513
+ package_commands.each do |cmd|
514
+ cmd_options = {
515
+ :live_stream => STDOUT,
516
+ :timeout => 3600,
517
+ :cwd => config.package_dir
518
+ }
519
+
520
+ if cmd.is_a?(Array)
521
+ command = cmd[0]
522
+ cmd_options.merge!(cmd[1])
523
+ else
524
+ command = cmd
525
+ end
526
+
527
+ shell = Mixlib::ShellOut.new(command, cmd_options)
528
+ shell.run_command
529
+ shell.error!
530
+ end
531
+ end
532
+
533
+ # TODO: why aren't these dependencies just added in at the
534
+ # initial creation of the 'pkg_type' task?
535
+ task pkg_type => config.package_dir
536
+ task pkg_type => "#{@name}:health_check"
537
+ end
538
+ end
539
+
540
+ task "#{@name}:copy" => (package_types.map {|pkg_type| "#{@name}:#{pkg_type}"}) do
541
+ if OHAI.platform == "windows"
542
+ cp_cmd = "xcopy #{config.package_dir}\\*.msi pkg\\ /Y"
543
+ else
544
+ cp_cmd = "cp #{config.package_dir}/* pkg/"
545
+ end
546
+ shell = Mixlib::ShellOut.new(cp_cmd)
547
+ shell.run_command
548
+ shell.error!
549
+ end
550
+ task "#{@name}:copy" => "pkg"
551
+
552
+ desc "package #{@name}"
553
+ task @name => "#{@name}:copy"
554
+
555
+ desc "run the health check on the #{@name} install path"
556
+ task "#{@name}:health_check" do
557
+ if OHAI.platform == "windows"
558
+ puts "Skipping health check on windows..."
559
+ else
560
+ Omnibus::HealthCheck.run(install_path)
561
+ end
562
+ end
563
+ end
564
+ end
565
+ end
566
+ end