omnibus 1.0.0

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