lib/nanoc/cli/commands/compile.rb in nanoc-3.4.3 vs lib/nanoc/cli/commands/compile.rb in nanoc-3.5.0b1

- old
+ new

@@ -20,106 +20,231 @@ option :a, :all, '(ignored)' option :f, :force, '(ignored)' module Nanoc::CLI::Commands - # FIXME this command is horribly long and complicated and does way too much. plz cleanup thx. class Compile < ::Nanoc::CLI::CommandRunner - def run - # Make sure we are in a nanoc site directory - puts "Loading site data…" - self.require_site + extend Nanoc::Memoization - # Check presence of --all option - if options.has_key?(:all) || options.has_key?(:force) - $stderr.puts "Warning: the --force option (and its deprecated --all alias) are, as of nanoc 3.2, no longer supported and have no effect." + # Listens to compilation events and reacts to them. This abstract class + # does not have a real implementation; subclasses should override {#start} + # and set up notifications to listen to. + # + # @abstract Subclasses must override {#start} and may override {#stop}. + class Listener + + # Starts the listener. Subclasses should override this method and set up listener notifications. + # + # @return [void] + # + # @abstract + def start + raise NotImplementedError, "Subclasses of Listener should implement #start" end - # Warn if trying to compile a single item - if arguments.size == 1 - $stderr.puts '-' * 80 - $stderr.puts 'Note: As of nanoc 3.2, it is no longer possible to compile a single item. When invoking the “compile” command, all items in the site will be compiled.' - $stderr.puts '-' * 80 + # Stops the listener. The default implementation removes self from all notification center observers. + # + # @return [void] + def stop end - # Give feedback - puts "Compiling site…" + end - # Initialize profiling stuff - time_before = Time.now - @rep_times = {} - @filter_times = {} - setup_notifications + # Generates diffs for every output file written + class DiffGenerator < Listener - # Prepare for generating diffs - setup_diffs + # @see Listener#start + def start + require 'tempfile' + self.setup_diffs + old_contents = {} + Nanoc::NotificationCenter.on(:will_write_rep) do |rep, snapshot| + path = rep.raw_path(:snapshot => snapshot) + old_contents[rep] = File.file?(path) ? File.read(path) : nil + end + Nanoc::NotificationCenter.on(:rep_written) do |rep, path, is_created, is_modified| + if !rep.binary? + new_contents = File.file?(path) ? File.read(path) : nil + if old_contents[rep] && new_contents + generate_diff_for(rep, old_contents[rep], new_contents) + end + old_contents.delete(rep) + end + end + end - # Set up GC control - @gc_lock = Mutex.new - @gc_count = 0 + # @see Listener#stop + def stop + super + self.teardown_diffs + end - # Compile - self.site.compile + protected - # Find reps - reps = self.site.items.map { |i| i.reps }.flatten + def setup_diffs + @diff_lock = Mutex.new + @diff_threads = [] + FileUtils.rm('output.diff') if File.file?('output.diff') + end - # Show skipped reps - reps.select { |r| !r.compiled? }.each do |rep| - rep.raw_paths.each do |snapshot_name, filename| - next if filename.nil? - duration = @rep_times[filename] - Nanoc::CLI::Logger.instance.file(:high, :skip, filename, duration) + def teardown_diffs + @diff_threads.each { |t| t.join } + end + + def generate_diff_for(rep, old_content, new_content) + return if old_content == new_content + + @diff_threads << Thread.new do + # Generate diff + diff = diff_strings(old_content, new_content) + diff.sub!(/^--- .*/, '--- ' + rep.raw_path) + diff.sub!(/^\+\+\+ .*/, '+++ ' + rep.raw_path) + + # Write diff + @diff_lock.synchronize do + File.open('output.diff', 'a') { |io| io.write(diff) } + end end end - # Stop diffing - teardown_diffs + def diff_strings(a, b) + require 'open3' - # Prune - if self.site.config[:prune][:auto_prune] - Nanoc::Extra::Pruner.new(self.site, :exclude => self.prune_config_exclude).run + # Create files + Tempfile.open('old') do |old_file| + Tempfile.open('new') do |new_file| + # Write files + old_file.write(a) + old_file.flush + new_file.write(b) + new_file.flush + + # Diff + cmd = [ 'diff', '-u', old_file.path, new_file.path ] + Open3.popen3(*cmd) do |stdin, stdout, stderr| + result = stdout.read + return (result == '' ? nil : result) + end + end + end + rescue Errno::ENOENT + warn 'Failed to run `diff`, so no diff with the previously compiled ' \ + 'content will be available.' + nil end - # Give general feedback - puts - puts "Site compiled in #{format('%.2f', Time.now - time_before)}s." + end - # Give detailed feedback - if options.has_key?(:verbose) - print_profiling_feedback(reps) + # Records the time spent per filter and per item representation + class TimingRecorder < Listener + + # @option params [Array<Nanoc::ItemRep>] :reps The list of item representations in the site + def initialize(params={}) + @filter_times = {} + + @reps = params.fetch(:reps) end + + # @see Listener#start + def start + Nanoc::NotificationCenter.on(:filtering_started) do |rep, filter_name| + @filter_times[filter_name] ||= [] + @filter_times[filter_name] << Time.now + end + Nanoc::NotificationCenter.on(:filtering_ended) do |rep, filter_name| + @filter_times[filter_name] << Time.now - @filter_times[filter_name].pop + end + end + + # @see Listener#stop + def stop + super + self.print_profiling_feedback + end + + protected + + def print_profiling_feedback + # Get max filter length + max_filter_name_length = @filter_times.keys.map { |k| k.to_s.size }.max + return if max_filter_name_length.nil? + + # Print warning if necessary + if @reps.any? { |r| !r.compiled? } + $stderr.puts + $stderr.puts "Warning: profiling information may not be accurate because " + + "some items were not compiled." + end + + # Print header + puts + puts ' ' * max_filter_name_length + ' | count min avg max tot' + puts '-' * max_filter_name_length + '-+-----------------------------------' + + @filter_times.to_a.sort_by { |r| r[1] }.each do |row| + self.print_row(row) + end + end + + def print_row(row) + # Extract data + filter_name, samples = *row + + # Calculate stats + count = samples.size + min = samples.min + tot = samples.inject { |memo, i| memo + i} + avg = tot/count + max = samples.max + + # Format stats + count = format('%4d', count) + min = format('%4.2f', min) + avg = format('%4.2f', avg) + max = format('%4.2f', max) + tot = format('%5.2f', tot) + + # Output stats + filter_name = format("%#{max_filter_name_length}s", filter_name) + puts "#{filter_name} | #{count} #{min}s #{avg}s #{max}s #{tot}s" + end + end - def setup_notifications - # Diff generation - require 'tempfile' - old_contents = {} - Nanoc::NotificationCenter.on(:will_write_rep) do |rep, snapshot| - path = rep.raw_path(:snapshot => snapshot) - old_contents[rep] = File.file?(path) ? File.read(path) : nil + # Controls garbage collection so that it only occurs once every 20 items + class GCController < Listener + + def initialize + @gc_count = 0 end - Nanoc::NotificationCenter.on(:rep_written) do |rep, path, is_created, is_modified| - if !rep.binary? && self.site.config[:enable_output_diff] - new_contents = File.file?(path) ? File.read(path) : nil - if old_contents[rep] && new_contents - generate_diff_for(rep, old_contents[rep], new_contents) + + # @see Listener#start + def start + Nanoc::NotificationCenter.on(:compilation_started) do |rep| + if @gc_count % 20 == 0 + GC.enable + GC.start + GC.disable end + @gc_count += 1 end end - # File notifications - Nanoc::NotificationCenter.on(:rep_written) do |rep, path, is_created, is_modified| - action = (is_created ? :create : (is_modified ? :update : :identical)) - level = (is_created ? :high : (is_modified ? :high : :low)) - duration = Time.now - @rep_times[rep.raw_path] if @rep_times[rep.raw_path] - Nanoc::CLI::Logger.instance.file(level, action, path, duration) + # @see Listener#stop + def stop + super + GC.enable end - # Debug notifications - if self.debug? + end + + # Prints debug information (compilation started/ended, filtering started/ended, …) + class DebugPrinter < Listener + + # @see Listener#start + def start Nanoc::NotificationCenter.on(:compilation_started) do |rep| puts "*** Started compilation of #{rep.inspect}" end Nanoc::NotificationCenter.on(:compilation_ended) do |rep| puts "*** Ended compilation of #{rep.inspect}" @@ -146,168 +271,119 @@ Nanoc::NotificationCenter.on(:dependency_created) do |src, dst| puts "*** Dependency created from #{src.inspect} onto #{dst.inspect}" end end - # Timing notifications - Nanoc::NotificationCenter.on(:compilation_started) do |rep| - if @gc_count % 20 == 0 && !ENV.has_key?('TRAVIS') - GC.enable - GC.start - GC.disable - end - @gc_count += 1 - @rep_times[rep.raw_path] = Time.now - end - Nanoc::NotificationCenter.on(:compilation_ended) do |rep| - @rep_times[rep.raw_path] = Time.now - @rep_times[rep.raw_path] - end - Nanoc::NotificationCenter.on(:filtering_started) do |rep, filter_name| - @filter_times[filter_name] ||= [] - @filter_times[filter_name] << Time.now - start_filter_progress(rep, filter_name) - end - Nanoc::NotificationCenter.on(:filtering_ended) do |rep, filter_name| - @filter_times[filter_name] << Time.now - @filter_times[filter_name].pop - stop_filter_progress(rep, filter_name) - end end - def setup_diffs - @diff_lock = Mutex.new - @diff_threads = [] - FileUtils.rm('output.diff') if File.file?('output.diff') - end + # Prints file actions (created, updated, deleted, identical, skipped) + class FileActionPrinter < Listener - def teardown_diffs - @diff_threads.each { |t| t.join } - end + # @option params [Array<Nanoc::ItemRep>] :reps The list of item representations in the site + def initialize(params={}) + @rep_times = {} - def generate_diff_for(rep, old_content, new_content) - return if old_content == new_content + @reps = params.fetch(:reps) + end - @diff_threads << Thread.new do - # Generate diff - diff = diff_strings(old_content, new_content) - diff.sub!(/^--- .*/, '--- ' + rep.raw_path) - diff.sub!(/^\+\+\+ .*/, '+++ ' + rep.raw_path) - - # Write diff - @diff_lock.synchronize do - File.open('output.diff', 'a') { |io| io.write(diff) } + # @see Listener#start + def start + Nanoc::NotificationCenter.on(:compilation_started) do |rep| + @rep_times[rep.raw_path] = Time.now end + Nanoc::NotificationCenter.on(:compilation_ended) do |rep| + @rep_times[rep.raw_path] = Time.now - @rep_times[rep.raw_path] + end + Nanoc::NotificationCenter.on(:rep_written) do |rep, path, is_created, is_modified| + action = (is_created ? :create : (is_modified ? :update : :identical)) + level = (is_created ? :high : (is_modified ? :high : :low)) + duration = Time.now - @rep_times[rep.raw_path] if @rep_times[rep.raw_path] + Nanoc::CLI::Logger.instance.file(level, action, path, duration) + end end - end - # TODO move this elsewhere - def diff_strings(a, b) - require 'open3' - - # Create files - Tempfile.open('old') do |old_file| - Tempfile.open('new') do |new_file| - # Write files - old_file.write(a) - old_file.flush - new_file.write(b) - new_file.flush - - # Diff - cmd = [ 'diff', '-u', old_file.path, new_file.path ] - Open3.popen3(*cmd) do |stdin, stdout, stderr| - result = stdout.read - return (result == '' ? nil : result) + # @see Listener#stop + def stop + super + @reps.select { |r| !r.compiled? }.each do |rep| + rep.raw_paths.each do |snapshot_name, filename| + next if filename.nil? + duration = @rep_times[filename] + Nanoc::CLI::Logger.instance.file(:high, :skip, filename, duration) end end end - rescue Errno::ENOENT - warn 'Failed to run `diff`, so no diff with the previously compiled ' \ - 'content will be available.' - nil + end - def start_filter_progress(rep, filter_name) - # Only show progress on terminals - return if !$stdout.tty? + def run + self.require_site + self.check_for_deprecated_usage + self.setup_listeners - @progress_thread = Thread.new do - delay = 1.0 - step = 0 + puts "Compiling site…" + time_before = Time.now + self.site.compile + self.prune + time_after = Time.now + puts + puts "Site compiled in #{format('%.2f', time_after - time_before)}s." + end - text = " running #{filter_name} filter… " + protected - loop do - if Thread.current[:stopped] - # Clear - if delay < 0.1 - $stdout.print ' ' * (text.length + 3) + "\r" - end - - break - end - - # Show progress - if delay < 0.1 - $stdout.print text + %w( | / - \\ )[step] + "\r" - step = (step + 1) % 4 - end - - sleep 0.1 - delay -= 0.1 - end - + def prune + if self.site.config[:prune][:auto_prune] + Nanoc::Extra::Pruner.new(self.site, :exclude => self.prune_config_exclude).run end end - def stop_filter_progress(rep, filter_name) - # Only show progress on terminals - return if !$stdout.tty? + def setup_listeners + @listeners = [] - @progress_thread[:stopped] = true - end + if self.site.config[:enable_output_diff] + @listeners << Nanoc::CLI::Commands::Compile::DiffGenerator.new + end - def print_profiling_feedback(reps) - # Get max filter length - max_filter_name_length = @filter_times.keys.map { |k| k.to_s.size }.max - return if max_filter_name_length.nil? + if self.debug? + @listeners << Nanoc::CLI::Commands::Compile::DebugPrinter.new + end - # Print warning if necessary - if reps.any? { |r| !r.compiled? } - $stderr.puts - $stderr.puts "Warning: profiling information may not be accurate because " + - "some items were not compiled." + if options.fetch(:verbose, false) + @listeners << Nanoc::CLI::Commands::Compile::TimingRecorder.new(:reps => self.reps) end - # Print header - puts - puts ' ' * max_filter_name_length + ' | count min avg max tot' - puts '-' * max_filter_name_length + '-+-----------------------------------' + unless ENV.has_key?('TRAVIS') + @listeners << Nanoc::CLI::Commands::Compile::GCController.new + end - @filter_times.to_a.sort_by { |r| r[1] }.each do |row| - # Extract data - filter_name, samples = *row + @listeners << Nanoc::CLI::Commands::Compile::FileActionPrinter.new(:reps => self.reps) - # Calculate stats - count = samples.size - min = samples.min - tot = samples.inject { |memo, i| memo + i} - avg = tot/count - max = samples.max + @listeners.each { |s| s.start } + end - # Format stats - count = format('%4d', count) - min = format('%4.2f', min) - avg = format('%4.2f', avg) - max = format('%4.2f', max) - tot = format('%5.2f', tot) + def teardown_listeners + @listeners.each { |s| s.stop } + end - # Output stats - filter_name = format("%#{max_filter_name_length}s", filter_name) - puts "#{filter_name} | #{count} #{min}s #{avg}s #{max}s #{tot}s" - end + def reps + self.site.items.map { |i| i.reps }.flatten end + memoize :reps - protected + def check_for_deprecated_usage + # Check presence of --all option + if options.has_key?(:all) || options.has_key?(:force) + $stderr.puts "Warning: the --force option (and its deprecated --all alias) are, as of nanoc 3.2, no longer supported and have no effect." + end + + # Warn if trying to compile a single item + if arguments.size == 1 + $stderr.puts '-' * 80 + $stderr.puts 'Note: As of nanoc 3.2, it is no longer possible to compile a single item. When invoking the “compile” command, all items in the site will be compiled.' + $stderr.puts '-' * 80 + end + end def prune_config self.site.config[:prune] || {} end