lib/buildr/core/application.rb in vic-buildr-1.3.1 vs lib/buildr/core/application.rb in vic-buildr-1.3.3

- old
+ new

@@ -33,27 +33,28 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -require 'benchmark' +require 'highline/import' require 'rake' require 'rubygems/source_info_cache' require 'buildr/core/application_cli' require 'buildr/core/util' # Gem::user_home is nice, but ENV['HOME'] lets you override from the environment. ENV["HOME"] ||= File.expand_path(Gem::user_home) ENV['BUILDR_ENV'] ||= 'development' + module Buildr # Provide settings that come from three sources. # # User settings are placed in the .buildr/settings.yaml file located in the user's home directory. - # The should only be used for settings that are specific to the user and applied the same way + # They should only be used for settings that are specific to the user and applied the same way # across all builds. Example for user settings are preferred repositories, path to local repository, # user/name password for uploading to remote repository. # # Build settings are placed in the build.yaml file located in the build directory. They help keep # the buildfile and build.yaml file simple and readable, working to the advantages of each one. @@ -90,15 +91,16 @@ end private def load_from(base_name, dir = nil) - file_name = ['yaml', 'yml'].map { |ext| File.expand_path("#{base_name}.#{ext}", dir) }.find { |fn| File.exist?(fn) } + base_name = File.expand_path(base_name, dir) if dir + file_name = ['yaml', 'yml'].map { |ext| "#{base_name}.#{ext}" }.find { |fn| File.exist?(fn) } return {} unless file_name yaml = YAML.load(File.read(file_name)) || {} fail "Expecting #{file_name} to be a map (name: value)!" unless Hash === yaml - @application.build_files << file_name + @application.buildfile.enhance [file_name] yaml end end @@ -118,14 +120,15 @@ @name = 'Buildr' @requires = [] @top_level_tasks = [] parse_options collect_tasks - top_level_tasks.unshift 'buildr:initialize' @home_dir = File.expand_path('.buildr', ENV['HOME']) - mkpath @home_dir unless File.exist?(@home_dir) + mkpath @home_dir, :verbose=>false unless File.exist?(@home_dir) @environment = ENV['BUILDR_ENV'] ||= 'development' + @on_completion = [] + @on_failure = [] end # Returns list of Gems associated with this buildfile, as listed in build.yaml. # Each entry is of type Gem::Specification. attr_reader :gems @@ -136,27 +139,84 @@ # Copied from BUILD_ENV. attr_reader :environment # Returns the Settings associated with this build. def settings + fail "Internal error: Called Buildr.settings before buildfile located" unless rakefile @settings ||= Settings.new(self) end # :call-seq: # buildfile + # Returns the buildfile as a task that you can use as a dependency. def buildfile - rakefile + @buildfile_task ||= BuildfileTask.define_task(File.expand_path(rakefile)) end + + # Files that complement the buildfile itself + def build_files #:nodoc: + buildfile.prerequisites + end + def run + standard_exception_handling do + find_buildfile + load_gems + load_artifacts + load_tasks + load_requires + load_buildfile + load_imports + task('buildr:initialize').invoke + top_level + end + title, message = 'Your build has completed', "#{Dir.pwd}\nbuildr #{@top_level_tasks.join(' ')}" + @on_completion.each { |block| block.call(title, message) rescue nil } + end + + # Yields to block on successful completion. Primarily used for notifications. + def on_completion(&block) + @on_completion << block + end + + # Yields to block on failure with exception. Primarily used for notifications. + def on_failure(&block) + @on_failure << block + end + + # Not for external consumption. + def switch_to_namespace(names) #:nodoc: + current, @scope = @scope, names + begin + yield + ensure + @scope = current + end + end + # :call-seq: - # build_files => files + # deprecated(message) # - # Returns a list of build files. These are files used by the build, - def build_files - [buildfile].compact + Array(@build_files) + # Use with deprecated methods and classes. This method automatically adds the file name and line number, + # and the text 'Deprecated' before the message, and eliminated duplicate warnings. It only warns when + # running in verbose mode. + # + # For example: + # deprecated 'Please use new_foo instead of foo.' + def deprecated(message) #:nodoc: + return unless verbose + "#{caller[1]}: Deprecated: #{message}".tap do |message| + @deprecated ||= {} + unless @deprecated[message] + @deprecated[message] = true + warn message + end + end end + private + # Returns Gem::Specification for every listed and installed Gem, Gem::Dependency # for listed and uninstalled Gem, which is the installed before loading the buildfile. def listed_gems #:nodoc: Array(settings.build['gems']).map do |dep| name, trail = dep.scan(/^\s*(\S*)\s*(.*)\s*$/).first @@ -164,32 +224,11 @@ versions = ['>= 0'] if versions.empty? dep = Gem::Dependency.new(name, versions) Gem::SourceIndex.from_installed_gems.search(dep).last || dep end end - private :listed_gems - def run - times = Benchmark.measure do - standard_exception_handling do - find_buildfile - load_gems - load_artifacts - load_tasks - load_buildfile - top_level - end - end - if verbose - real = [] - real << ("%ih" % (times.real / 3600)) if times.real >= 3600 - real << ("%im" % ((times.real / 60) % 60)) if times.real >= 60 - real << ("%.3fs" % (times.real % 60)) - puts "Completed in #{real.join}" - end - end - # Load artifact specs from the build.yaml file, making them available # by name ( ruby symbols ). def load_artifacts #:nodoc: hash = settings.build['artifacts'] return unless hash @@ -200,40 +239,31 @@ # Load/install all Gems specified in build.yaml file. def load_gems #:nodoc: missing_deps, installed = listed_gems.partition { |gem| gem.is_a?(Gem::Dependency) } unless missing_deps.empty? - remote = missing_deps.map { |dep| Gem::SourceInfoCache.search(dep).last || dep } - not_found_deps, install = remote.partition { |gem| gem.is_a?(Gem::Dependency) } - fail Gem::LoadError, "Build requires the gems #{not_found_deps.join(', ')}, which cannot be found in local or remote repository." unless not_found_deps.empty? - uses = "This build requires the gems #{install.map(&:full_name).join(', ')}:" - fail Gem::LoadError, "#{uses} to install, run Buildr interactively." unless $stdout.isatty - unless agree("#{uses} do you want me to install them? [Y/n]", true) - fail Gem::LoadError, 'Cannot build without these gems.' - end - install.each do |spec| - say "Installing #{spec.full_name} ... " if verbose - Util.ruby 'install', spec.name, '-v', spec.version.to_s, :command => 'gem', :sudo => true, :verbose => false - Gem.source_index.load_gems_in Gem::SourceIndex.installed_spec_directories - end - installed += install + newly_installed = Util::Gems.install(*missing_deps) + installed += newly_installed end - installed.each do |spec| if gem(spec.name, spec.version.to_s) - # FileList[spec.require_paths.map { |path| File.expand_path("#{path}/*.rb", spec.full_gem_path) }]. - # map { |path| File.basename(path) }.each { |file| require file } - # FileList[File.expand_path('tasks/*.rake', spec.full_gem_path)].each do |file| - # Buildr.application.add_import file - # end + # TODO: is this intended to load rake tasks from the installed gems? + # We should use a convention like .. if the gem has a _buildr.rb file, load it. + + #FileList[spec.require_paths.map { |path| File.expand_path("#{path}/*.rb", spec.full_gem_path) }]. + # map { |path| File.basename(path) }.each { |file| require file } + #FileList[File.expand_path('tasks/*.rake', spec.full_gem_path)].each do |file| + # Buildr.application.add_import file + #end end end @gems = installed end - def find_buildfile - here = Dir.pwd + def find_buildfile #:nodoc: + here = original_dir + Dir.chdir(here) unless Dir.pwd == here while ! have_rakefile Dir.chdir('..') if Dir.pwd == here || options.nosearch error = "No Buildfile found (looking for: #{@rakefiles.join(', ')})" if STDIN.isatty @@ -245,66 +275,86 @@ end here = Dir.pwd end end - def load_buildfile - @requires.each { |name| require name } - puts "(in #{Dir.pwd}, #{environment})" + def load_buildfile #:nodoc: + info "(in #{Dir.pwd}, #{environment})" load File.expand_path(@rakefile) if @rakefile != '' - load_imports + buildfile.enhance @requires.select { |f| File.file?(f) }.map{ |f| File.expand_path(f) } end - # Loads buildr.rake files from users home directory and project directory. + def load_requires #:nodoc: + @requires.each { |name| require name } + end + + # Loads buildr.rb files from users home directory and project directory. # Loads custom tasks from .rake files in tasks directory. def load_tasks #:nodoc: - @build_files = [ File.expand_path('buildr.rb', ENV['HOME']), 'buildr.rb' ].select { |file| File.exist?(file) } - @build_files += [ File.expand_path('buildr.rake', ENV['HOME']), File.expand_path('buildr.rake') ]. + files = [ File.expand_path('buildr.rb', ENV['HOME']), 'buildr.rb' ].select { |file| File.exist?(file) } + files += [ File.expand_path('buildr.rake', ENV['HOME']), File.expand_path('buildr.rake') ]. select { |file| File.exist?(file) }.each { |file| warn "Please use '#{file.ext('rb')}' instead of '#{file}'" } #Load local tasks that can be used in the Buildfile. - @build_files += Dir["#{Dir.pwd}/tasks/*.rake"] - @build_files.each do |file| + files += Dir[File.expand_path('tasks/*.rake', original_dir)] + files.each do |file| unless $LOADED_FEATURES.include?(file) load file $LOADED_FEATURES << file end end + buildfile.enhance files true end - private :load_tasks - # :call-seq: - # deprecated(message) - # - # Use with deprecated methods and classes. This method automatically adds the file name and line number, - # and the text 'Deprecated' before the message, and eliminated duplicate warnings. It only warns when - # running in verbose mode. - # - # For example: - # deprecated 'Please use new_foo instead of foo.' - def deprecated(message) #:nodoc: - return unless verbose - "#{caller[1]}: Deprecated: #{message}".tap do |message| - @deprecated ||= {} - unless @deprecated[message] - @deprecated[message] = true - warn message + def display_prerequisites + invoke_task('buildr:initialize') + tasks.each do |task| + if task.name =~ options.show_task_pattern + puts "buildr #{task.name}" + task.prerequisites.each { |prereq| puts " #{prereq}" } end end end - - # Not for external consumption. - def switch_to_namespace(names) #:nodoc: - current, @scope = @scope, names + + # Provide standard execption handling for the given block. + def standard_exception_handling begin yield - ensure - @scope = current + rescue SystemExit => ex + # Exit silently with current status + exit(ex.status) + rescue SystemExit, GetoptLong::InvalidOption => ex + # Exit silently + exit(1) + rescue Exception => ex + title, message = 'Your build failed with an error', "#{Dir.pwd}:\n#{ex.message}" + @on_failure.each { |block| block.call(title, message, ex) rescue nil } + # Exit with error message + $stderr.puts "buildr aborted!" + $stderr.puts $terminal.color(ex.message, :red) + if options.trace + $stderr.puts ex.backtrace.join("\n") + else + $stderr.puts ex.backtrace.select { |str| str =~ /#{buildfile}/ }.map { |line| $terminal.color(line, :red) }.join("\n") + $stderr.puts "(See full trace by running task with --trace)" + end + exit(1) end end - + end + + + # This task stands for the buildfile and all its associated helper files (e.g., buildr.rb, build.yaml). + # By using this task as a prerequisite for other tasks, you can ensure these tasks will be needed + # whenever the buildfile changes. + class BuildfileTask < Rake::FileTask + + def timestamp + ([name] + prerequisites).map { |f| File.stat(f).mtime }.max rescue Time.now + end + end class << self task 'buildr:initialize' do @@ -335,29 +385,79 @@ Buildr.application = Buildr::Application.new end -# Add a touch of colors (red) to warnings. +# Add a touch of color when available and running in terminal. if $stdout.isatty begin require 'Win32/Console/ANSI' if Config::CONFIG['host_os'] =~ /mswin/ HighLine.use_color = true rescue LoadError end +else + HighLine.use_color = false end -if HighLine.use_color? - module Kernel #:nodoc: - alias :warn_without_color :warn - def warn(message) - warn_without_color $terminal.color(message.to_s, :red) + +# Let's see if we can use Growl. We do this at the very end, loading Ruby Cocoa +# could slow the build down, so later is better. We only do this when running +# from the console in verbose mode. +if $stdout.isatty && verbose && RUBY_PLATFORM =~ /darwin/ + begin + require 'osx/cocoa' + icon = OSX::NSApplication.sharedApplication.applicationIconImage + icon = OSX::NSImage.alloc.initWithContentsOfFile(File.join(File.dirname(__FILE__), '../resources/buildr.icns')) + + # Register with Growl, that way you can turn notifications on/off from system preferences. + OSX::NSDistributedNotificationCenter.defaultCenter. + postNotificationName_object_userInfo_deliverImmediately(:GrowlApplicationRegistrationNotification, nil, + { :ApplicationName=>'Buildr', :AllNotifications=>['Completed', 'Failed'], + :ApplicationIcon=>icon.TIFFRepresentation }, true) + + notify = lambda do |type, title, message| + OSX::NSDistributedNotificationCenter.defaultCenter. + postNotificationName_object_userInfo_deliverImmediately(:GrowlNotification, nil, + { :ApplicationName=>'Buildr', :NotificationName=>type, + :NotificationTitle=>title, :NotificationDescription=>message }, true) end + Buildr.application.on_completion { |title, message| notify['Completed', title, message] } + Buildr.application.on_failure { |title, message, ex| notify['Failed', title, message] } + rescue Exception # No growl end +elsif $stdout.isatty && verbose + notify = lambda { |type, title, message| $stdout.puts "[#{type}] #{title}: #{message}" } + Buildr.application.on_completion { |title, message| notify['Completed', title, message] } + Buildr.application.on_failure { |title, message, ex| notify['Failed', title, message] } end +alias :warn_without_color :warn + +# Show warning message. +def warn(message) + warn_without_color $terminal.color(message.to_s, :blue) if verbose +end + +# Show error message. Use this when you need to show an error message and not throwing +# an exception that will stop the build. +def error(message) + puts $terminal.color(message.to_s, :red) +end + +# Show optional information. The message is printed only when running in verbose +# mode (the default). +def info(message) + puts message if verbose +end + +# Show message. The message is printed out only when running in trace mode. +def trace(message) + puts message if Buildr.application.options.trace +end + + module Rake #:nodoc class Task #:nodoc: def invoke(*args) task_args = TaskArguments.new(arg_names, args) invoke_with_call_chain(task_args, Thread.current[:rake_chain] || InvocationChain::EMPTY) @@ -369,16 +469,21 @@ if application.options.trace puts "** Invoke #{name} #{format_trace_flags}" end return if @already_invoked @already_invoked = true - invoke_prerequisites(task_args, new_chain) begin + invoke_prerequisites(task_args, new_chain) + rescue + trace "Exception while invoking prerequisites of task #{self.inspect}" + raise + end + begin old_chain, Thread.current[:rake_chain] = Thread.current[:rake_chain], new_chain execute(task_args) if needed? ensure Thread.current[:rake_chain] = nil end end end end -end +end \ No newline at end of file