builder.rb 7.18 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#
# Copyright:: Copyright (c) 2012 Opscode, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
#     http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

18
19
require 'forwardable'

20
21
22
module Omnibus
  class Builder

23
24
25
26
27
28
    # Proxies method calls to either a Builder object or the Software that the
    # builder belongs to. Provides compatibility with our DSL where we never
    # yield objects to blocks and hopefully hides some of the confusion that
    # can arise from instance_eval.
    class DSLProxy
      extend Forwardable
29
      def_delegator :@builder, :patch
30
      def_delegator :@builder, :command
31
32
33
      def_delegator :@builder, :ruby
      def_delegator :@builder, :gem
      def_delegator :@builder, :bundle
34
      def_delegator :@builder, :block
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
      def_delegator :@builder, :name


      def initialize(builder, software)
        @builder, @software = builder, software
      end

      def eval_block(&block)
        instance_eval(&block)
      end

      def respond_to?(method)
        super || @software.respond_to?(method)
      end

      def methods
        super | @software.methods
      end

      def method_missing(method_name, *args, &block)
        if @software.respond_to?(method_name)
          @software.send(method_name, *args, &block)
        else
          super
        end
      end

    end

    #--
    # TODO: code duplication with Fetcher::ErrorReporter
    class ErrorReporter

      def initialize(error, fetcher)
        @error, @fetcher = error, fetcher
      end

      def e
        @error
      end

      def explain(why)
        $stderr.puts "* " * 40
        $stderr.puts why
        $stderr.puts "Exception:"
        $stderr.puts indent("#{e.class}: #{e.message.strip}", 2)
        Array(e.backtrace).each {|l| $stderr.puts indent(l, 4) }
        $stderr.puts "* " * 40
      end

      private

      def indent(string, n)
        string.split("\n").map {|l| " ".rjust(n) << l }.join("\n")
      end

    end

93
94
95
96
97
98
    BUNDLER_BUSTER = {  "RUBYOPT"         => nil,
                        "BUNDLE_BIN_PATH" => nil,
                        "BUNDLE_GEMFILE"  => nil,
                        "GEM_PATH"        => nil,
                        "GEM_HOME"        => nil }

99
100
    attr_reader :build_commands

101
102
103
104
105
106
107
108
109
110
111
112
113
114
    def initialize(software, &block)
      @software = software
      @build_commands = []
      @dsl_proxy = DSLProxy.new(self, software)
      @dsl_proxy.eval_block(&block) if block_given?
    end

    def name
      @software.name
    end

    def command(*args)
      @build_commands << args
    end
115
116
117
    
    def patch(*args) 
      args = args.dup.pop 
118
119
120
121
122
123
124
125
126
127
      source = File.expand_path("#{Omnibus.root}/config/patches/#{name}/#{args[:source]}")
      plevel = args[:plevel] || 1
      if args[:target] 
        target = File.expand_path("#{project_dir}/#{args[:target]}")
        @build_commands << 
         "cat #{source} | patch -p#{plevel} #{target}"
      else
        @build_commands << 
         "patch -d #{project_dir} -p#{plevel} -i #{source}"
      end
128
    end
129

130
131
132
133
134
135
136
137
138
139
140
141
    def ruby(*args)
      @build_commands << bundle_bust(*prepend_cmd("#{install_dir}/embedded/bin/ruby", *args))
    end

    def gem(*args)
      @build_commands << bundle_bust(*prepend_cmd("#{install_dir}/embedded/bin/gem", *args))
    end

    def bundle(*args)
      @build_commands << bundle_bust(*prepend_cmd("#{install_dir}/embedded/bin/bundle", *args))
    end

142
143
144
145
    def block(&rb_block)
      @build_commands << rb_block
    end

146
147
    def project_dir
      @software.project_dir
148
149
    end

150
151
152
153
    def install_dir
      @software.install_dir
    end

Daniel DeLeo's avatar
Daniel DeLeo committed
154
    def log(message)
155
      puts "[builder:#{name}] #{message}"
Daniel DeLeo's avatar
Daniel DeLeo committed
156
157
    end

158
    def build
159
      log "building #{name}"
160
161
162
163
      time_it("#{name} build") do
        @build_commands.each do |cmd|
          execute(cmd)
        end
164
165
166
167
      end
    end

    def execute(cmd)
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
      case cmd
      when Proc
        execute_proc(cmd)
      else
        execute_sh(cmd)
      end
    end

    private

    def execute_proc(cmd)
      cmd.call
    rescue Exception => e
      # In Ruby 1.9, Procs have a #source_location method with file/line info.
      # Too bad we can't use it :(
      ErrorReporter.new(e, self).explain("Failed to build #{name} while running ruby block build step")
      raise
    end

    def execute_sh(cmd)
188
189
190
191
192
193
      shell = nil
      cmd_args = Array(cmd)
      options = {
        :cwd => project_dir,
        :timeout => 3600
      }
194
      options[:live_stream] = STDOUT if ENV['DEBUG']
195
196
197
198
199
200
      if cmd_args.last.is_a? Hash
        cmd_options = cmd_args.last
        cmd_args[cmd_args.size - 1] = options.merge(cmd_options)
      else
        cmd_args << options
      end
201

202
      cmd_string = cmd_args[0..-2].join(' ')
203
      cmd_opts_for_display = to_kv_str(cmd_args.last)
204
205
206

      log "Executing: `#{cmd_string}` with #{cmd_opts_for_display}"

207
      shell = Mixlib::ShellOut.new(*cmd)
208
      shell.environment["HOME"] = "/tmp" unless ENV["HOME"]
209

210
      cmd_name = cmd_string.split(/\s+/).first
211
212
213
214
      time_it("#{cmd_name} command") do
        shell.run_command
        shell.error!
      end
215
216
    rescue Exception => e
      ErrorReporter.new(e, self).explain("Failed to build #{name} while running `#{cmd_string}` with #{cmd_opts_for_display}")
217
218
219
      raise
    end

220
221
222
    def prepend_cmd(str, *cmd_args)
      if cmd_args.size == 1
        # command as a string, no opts
223
        "#{str} #{cmd_args.first}"
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
      elsif cmd_args.size == 2 && cmd_args.last.is_a?(Hash)
        # command as a string w/ opts
        ["#{str} #{cmd_args.first}", cmd_args.last]
      elsif cmd_args.size == 0
        raise ArgumentError, "I don't even"
      else
        # cmd given as argv array
        cmd_args.dup.unshift(str)
      end
    end

    def bundle_bust(*cmd_args)
      if cmd_args.last.is_a?(Hash)
        cmd_args = cmd_args.dup
        cmd_opts = cmd_args.pop.dup
        cmd_opts[:env] = cmd_opts[:env] ? BUNDLER_BUSTER.merge(cmd_opts[:env]) : BUNDLER_BUSTER
        cmd_args << cmd_opts
      else
        cmd_args << {:env => BUNDLER_BUSTER}
      end
    end


247
248
249
250
251
252
253
254
255
256
257
258
    def time_it(what)
      start = Time.now
      yield
    rescue Exception
      elapsed = Time.now - start
      log "#{what} failed, #{elapsed.to_f}s"
      raise
    else
      elapsed = Time.now - start
      log "#{what} succeeded, #{elapsed.to_f}s"
    end

259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
    # Convert a hash to a string in the form `key=value`. It should work with
    # whatever input is given but is designed to make the options to ShellOut
    # look nice.
    def to_kv_str(hash, join_str=",")
      hash.inject([]) do |kv_pair_strs, (k,v)|
        val_str = case v
        when Hash
          %Q["#{to_kv_str(v, " ")}"]
        else
          v.to_s
        end
        kv_pair_strs << "#{k}=#{val_str}"
      end.join(join_str)
    end

274
  end
275
276
277
278
279
280
281
282
283

  class NullBuilder < Builder

    def build
      log "Nothing to build for #{name}"
    end

  end

284
end