diff --git a/middleman-core/lib/middleman-core/cli/build.rb b/middleman-core/lib/middleman-core/cli/build.rb index 4b884863..7719bac7 100644 --- a/middleman-core/lib/middleman-core/cli/build.rb +++ b/middleman-core/lib/middleman-core/cli/build.rb @@ -1,14 +1,18 @@ require "middleman-core" require "fileutils" +require 'set' # CLI Module module Middleman::Cli + # Alias "b" to "build" + Base.map({ "b" => "build" }) # The CLI Build class class Build < Thor include Thor::Actions attr_reader :debugging + attr_accessor :had_errors check_unknown_options! @@ -51,21 +55,19 @@ module Middleman::Cli require 'find' @debugging = Middleman::Cli::Base.respond_to?(:debugging) && Middleman::Cli::Base.debugging - @had_errors = false + self.had_errors = false self.class.shared_instance(options["verbose"], options["instrument"]) - self.class.shared_rack - opts = {} - opts[:glob] = options["glob"] if options.has_key?("glob") - opts[:clean] = options["clean"] if options.has_key?("clean") + opts[:glob] = options["glob"] if options.has_key?("glob") + opts[:clean] = options["clean"] - action GlobAction.new(self, opts) + action BuildAction.new(self, opts) self.class.shared_instance.run_hook :after_build, self - if @had_errors && !@debugging + if self.had_errors && !self.debugging msg = "There were errors during this build" unless options["verbose"] msg << ", re-run with `middleman build --verbose` to see the full exception." @@ -73,7 +75,7 @@ module Middleman::Cli self.shell.say msg, :red end - exit(1) if @had_errors + exit(1) if self.had_errors end # Static methods @@ -91,88 +93,11 @@ module Middleman::Cli logger(verbose ? 0 : 1, instrument) end end - - # Middleman::Application class singleton - # - # @return [Middleman::Application] - def shared_server - @_shared_server ||= shared_instance.class - end - - # Rack::Test::Session singleton - # - # @return [Rack::Test::Session] - def shared_rack - @_shared_rack ||= ::Rack::Test::Session.new(shared_server.to_rack_app) - end - - # Set the root path to the Middleman::Application's root - def source_root - shared_instance.root - end end - - no_tasks { - # Render a resource to a file. - # - # @param [Middleman::Sitemap::Resource] resource - # @return [String] The full path of the file that was written - def render_to_file(resource) - build_dir = self.class.shared_instance.config[:build_dir] - output_file = File.join(build_dir, resource.destination_path.gsub('%20', ' ')) - - if resource.binary? - if !File.exists?(output_file) - say_status :create, output_file, :green - elsif FileUtils.compare_file(resource.source_file, output_file) - say_status :identical, output_file, :blue - return output_file - else - say_status :update, output_file, :yellow - end - - FileUtils.mkdir_p(File.dirname(output_file)) - FileUtils.cp(resource.source_file, output_file) - else - begin - response = self.class.shared_rack.get(URI.escape(resource.request_path)) - - if response.status == 200 - create_file(output_file, binary_encode(response.body)) - else - handle_error(output_file, response.body) - end - rescue => e - handle_error(output_file, "#{e}\n#{e.backtrace.join("\n")}", e) - end - end - - output_file - end - - def handle_error(file_name, response, e=Thor::Error.new(response)) - @had_errors = true - - say_status :error, file_name, :red - if self.debugging - raise e - exit(1) - elsif options["verbose"] - self.shell.say response, :red - end - end - - def binary_encode(string) - if string.respond_to?(:force_encoding) - string.force_encoding("ascii-8bit") - end - string - end - } end # A Thor Action, modular code, which does the majority of the work. - class GlobAction < ::Thor::Actions::EmptyDirectory + class BuildAction < ::Thor::Actions::EmptyDirectory attr_reader :source attr_reader :logger @@ -181,23 +106,23 @@ module Middleman::Cli # @param [Middleman::Cli::Build] base # @param [Hash] config def initialize(base, config={}) - @app = base.class.shared_instance - source = @app.source - @destination = @app.build_dir + @app = base.class.shared_instance + @source_dir = Pathname(@app.source_dir) + @build_dir = Pathname(@app.build_dir) + @to_clean = Set.new - @source = File.expand_path(base.find_in_source_paths(source.to_s)) + @logger = @app.logger + @rack = ::Rack::Test::Session.new(@app.class.to_rack_app) - @logger = Middleman::Cli::Build.shared_instance.logger - - super(base, @destination, config) + super(base, @build_dir, config) end # Execute the action # @return [void] def invoke! - queue_current_paths if cleaning? + queue_current_paths if should_clean? execute! - clean! if cleaning? + clean! if should_clean? end protected @@ -205,39 +130,36 @@ module Middleman::Cli # Remove files which were not built in this cycle # @return [void] def clean! - @cleaning_queue.select { |q| q.file? }.each do |f| + @to_clean.each do |f| base.remove_file f, :force => true end - Dir[File.join(@destination, "**", "*")].select { |d| - File.directory?(d) - }.each do |d| - base.remove_file d, :force => true if directory_empty? Pathname(d) + Dir[@build_dir.join("**", "*")].select {|d| File.directory?(d) }.each do |d| + base.remove_file d, :force => true if directory_empty? d end end # Whether we should clean the build # @return [Boolean] - def cleaning? - @config.has_key?(:clean) && @config[:clean] + def should_clean? + @config[:clean] end # Whether the given directory is empty - # @param [String] directory + # @param [String, Pathname] directory # @return [Boolean] def directory_empty?(directory) - directory.children.empty? + Pathname(directory).children.empty? end - # Get a list of all the paths in the destination folder and save them + # Get a list of all the file paths in the destination folder and save them # for comparison against the files we build in this cycle # @return [void] def queue_current_paths - @cleaning_queue = [] - return unless File.exist?(@destination) + return unless File.exist?(@build_dir) - paths = ::Middleman::Util.all_files_under(@destination).map(&:realpath) - @cleaning_queue += paths.select do |path| + paths = ::Middleman::Util.all_files_under(@build_dir).map(&:realpath).select(&:file?) + @to_clean += paths.select do |path| path.to_s !~ /\/\./ || path.to_s =~ /\.(htaccess|htpasswd)/ end end @@ -254,13 +176,13 @@ module Middleman::Cli @app.sitemap.resources.select do |resource| resource.ext == ".css" end.each do |resource| - Middleman::Cli::Build.shared_rack.get(URI.escape(resource.destination_path)) + @rack.get(URI.escape(resource.destination_path)) end logger.debug "== Checking for Compass sprites" # Double-check for compass sprites - @app.files.find_new_files((Pathname(@app.source_dir) + @app.images_dir).relative_path_from(@app.root_path)) + @app.files.find_new_files((@source_dir + @app.images_dir).relative_path_from(@app.root_path)) @app.sitemap.ensure_resource_list_updated! # Sort paths to be built by the above order. This is primarily so Compass can @@ -273,24 +195,79 @@ module Middleman::Cli sort_order.index(r.ext) || 100 end + if @build_dir.expand_path.relative_path_from(@source_dir).to_s =~ /\A[.\/]+\Z/ + raise ":build_dir (#{@build_dir}) cannot be a parent of :source_dir (#{@source_dir})" + end + # Loop over all the paths and build them. resources.each do |resource| next if @config[:glob] && !File.fnmatch(@config[:glob], resource.destination_path) - output_path = base.render_to_file(resource) + output_path = render_to_file(resource) - if cleaning? - pn = Pathname(output_path) - @cleaning_queue.delete(pn.realpath) if pn.exist? + if should_clean? + @to_clean.delete(output_path.realpath) if output_path.exist? end end ::Middleman::Profiling.report("build") end - end - # Alias "b" to "build" - Base.map({ "b" => "build" }) + # Render a resource to a file. + # + # @param [Middleman::Sitemap::Resource] resource + # @return [Pathname] The full path of the file that was written + def render_to_file(resource) + output_file = @build_dir + resource.destination_path.gsub('%20', ' ') + + if resource.binary? + if !output_file.exist? + base.say_status :create, output_file, :green + elsif FileUtils.compare_file(resource.source_file, output_file) + base.say_status :identical, output_file, :blue + return output_file + else + base.say_status :update, output_file, :yellow + end + + output_file.dirname.mkpath + FileUtils.cp(resource.source_file, output_file) + else + begin + response = @rack.get(URI.escape(resource.request_path)) + + if response.status == 200 + base.create_file(output_file, binary_encode(response.body)) + else + handle_error(output_file, response.body) + end + rescue => e + handle_error(output_file, "#{e}\n#{e.backtrace.join("\n")}", e) + end + end + + output_file + end + + def handle_error(file_name, response, e=Thor::Error.new(response)) + base.had_errors = true + + base.say_status :error, file_name, :red + if base.debugging + raise e + exit(1) + elsif base.options["verbose"] + base.shell.say response, :red + end + end + + def binary_encode(string) + if string.respond_to?(:force_encoding) + string.force_encoding("ascii-8bit") + end + string + end + end end # Quiet down create file