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