middleman/middleman-core/lib/middleman-core/builder.rb

254 lines
7.5 KiB
Ruby
Raw Normal View History

2014-07-08 04:43:22 +02:00
require 'pathname'
require 'fileutils'
require 'tempfile'
require 'middleman-core/rack'
require 'middleman-core/callback_manager'
2014-07-08 04:43:22 +02:00
require 'middleman-core/contracts'
module Middleman
class Builder
extend Forwardable
include Contracts
# Make app & events available to `after_build` callbacks.
attr_reader :app, :events
2015-02-27 02:08:40 +01:00
# Reference to the Thor class.
attr_accessor :thor
2014-07-08 04:43:22 +02:00
# Logger comes from App.
def_delegator :@app, :logger
# Sort order, images, fonts, js/css and finally everything else.
2016-01-14 20:21:42 +01:00
SORT_ORDER = %w(.png .jpeg .jpg .gif .bmp .svg .svgz .webp .ico .woff .woff2 .otf .ttf .eot .js .css).freeze
2014-07-08 04:43:22 +02:00
# Create a new Builder instance.
# @param [Middleman::Application] app The app to build.
# @param [Hash] opts The builder options
def initialize(app, opts={})
@app = app
2014-07-16 03:01:45 +02:00
@source_dir = Pathname(File.join(@app.root, @app.config[:source]))
2014-07-08 04:43:22 +02:00
@build_dir = Pathname(@app.config[:build_dir])
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
@glob = opts.fetch(:glob)
@cleaning = opts.fetch(:clean)
rack_app = ::Middleman::Rack.new(@app).to_app
@rack = ::Rack::MockRequest.new(rack_app)
@callbacks = ::Middleman::CallbackManager.new
@callbacks.install_methods!(self, [:on_build_event])
2014-07-08 04:43:22 +02:00
end
# Run the build phase.
# @return [Boolean] Whether the build was successful.
2015-04-24 19:26:42 +02:00
Contract Bool
2014-07-08 04:43:22 +02:00
def run!
@has_error = false
@events = {}
@app.execute_callbacks(:before_build, [self])
2014-07-08 04:43:22 +02:00
queue_current_paths if @cleaning
2015-12-13 23:30:10 +01:00
prerender_css
2014-07-08 04:43:22 +02:00
output_files
2016-01-11 02:14:41 +01:00
clean! if @cleaning
2014-07-08 04:43:22 +02:00
::Middleman::Profiling.report('build')
@app.execute_callbacks(:after_build, [self])
2014-07-08 04:43:22 +02:00
!@has_error
end
2015-12-13 23:30:10 +01:00
# Pre-request CSS to give Compass a chance to build sprites
# @return [Array<Resource>] List of css resources that were output.
Contract ResourceList
def prerender_css
logger.debug '== Prerendering CSS'
2016-01-14 02:16:36 +01:00
css_files = @app.sitemap.resources
2016-01-14 20:21:42 +01:00
.select { |resource| resource.ext == '.css' }
.each(&method(:output_resource))
2016-01-14 02:16:36 +01:00
2015-12-13 23:30:10 +01:00
# Double-check for compass sprites
2016-01-14 02:16:36 +01:00
if @app.files.find_new_files!.length > 0
logger.debug '== Checking for Compass sprites'
@app.sitemap.ensure_resource_list_updated!
end
2015-12-13 23:30:10 +01:00
css_files
end
2014-07-08 04:43:22 +02:00
# Find all the files we need to output and do so.
# @return [Array<Resource>] List of resources that were output.
2015-04-24 19:26:42 +02:00
Contract ResourceList
2014-07-08 04:43:22 +02:00
def output_files
logger.debug '== Building files'
@app.sitemap.resources
2016-01-14 20:21:42 +01:00
.sort_by { |resource| SORT_ORDER.index(resource.ext) || 100 }
.reject { |resource| resource.ext == '.css' }
.select { |resource| !@glob || File.fnmatch(@glob, resource.destination_path) }
.each(&method(:output_resource))
2014-07-08 04:43:22 +02:00
end
# Figure out the correct event mode.
# @param [Pathname] output_file The output file path.
# @param [String] source The source file path.
# @return [Symbol]
Contract Pathname, String => Symbol
def which_mode(output_file, source)
if !output_file.exist?
:created
else
FileUtils.compare_file(source.to_s, output_file.to_s) ? :identical : :updated
end
end
# Create a tempfile for a given output with contents.
# @param [Pathname] output_file The output path.
# @param [String] contents The file contents.
# @return [Tempfile]
Contract Pathname, String => Tempfile
def write_tempfile(output_file, contents)
file = Tempfile.new([
2016-01-14 20:21:42 +01:00
File.basename(output_file),
File.extname(output_file)
])
2014-07-08 04:43:22 +02:00
file.binmode
file.write(contents)
file.close
File.chmod(0644, file)
2014-07-08 04:43:22 +02:00
file
end
# Actually export the file.
# @param [Pathname] output_file The path to output to.
# @param [String|Pathname] source The source path or contents.
# @return [void]
Contract Pathname, Or[String, Pathname] => Any
def export_file!(output_file, source)
2016-01-14 02:16:36 +01:00
# ::Middleman::Util.instrument "write_file", output_file: output_file do
2016-01-14 20:21:42 +01:00
source = write_tempfile(output_file, source.to_s) if source.is_a? String
2014-07-08 04:43:22 +02:00
2016-01-14 20:21:42 +01:00
method, source_path = if source.is_a? Tempfile
[::FileUtils.method(:mv), source.path]
else
[::FileUtils.method(:cp), source.to_s]
end
2014-07-08 04:43:22 +02:00
2016-01-14 20:21:42 +01:00
mode = which_mode(output_file, source_path)
2014-07-08 04:43:22 +02:00
2016-01-14 20:21:42 +01:00
if mode == :created || mode == :updated
::FileUtils.mkdir_p(output_file.dirname)
method.call(source_path, output_file.to_s)
end
2014-07-08 04:43:22 +02:00
2016-01-14 20:21:42 +01:00
source.unlink if source.is_a? Tempfile
2014-07-08 04:43:22 +02:00
2016-01-14 20:21:42 +01:00
trigger(mode, output_file)
2016-01-14 02:16:36 +01:00
# end
2014-07-08 04:43:22 +02:00
end
# Try to output a resource and capture errors.
# @param [Middleman::Sitemap::Resource] resource The resource.
# @return [void]
Contract IsA['Middleman::Sitemap::Resource'] => Any
def output_resource(resource)
output_file = @build_dir + resource.destination_path.gsub('%20', ' ')
begin
if resource.binary?
export_file!(output_file, resource.file_descriptor[:full_path])
2014-07-08 04:43:22 +02:00
else
2016-01-14 02:16:36 +01:00
response = @rack.get(::URI.escape(resource.request_path))
2014-07-08 04:43:22 +02:00
# If we get a response, save it to a tempfile.
if response.status == 200
export_file!(output_file, binary_encode(response.body))
else
@has_error = true
trigger(:error, output_file, response.body)
end
end
rescue => e
@has_error = true
trigger(:error, output_file, "#{e}\n#{e.backtrace.join("\n")}")
end
return unless @cleaning
return unless output_file.exist?
# handle UTF-8-MAC filename on MacOS
cleaned_name = if RUBY_PLATFORM =~ /darwin/
output_file.to_s.encode('UTF-8', 'UTF-8-MAC')
else
output_file
end
@to_clean.delete(Pathname(cleaned_name))
end
# Get a list of all the paths in the destination folder and save them
# for comparison against the files we build in this cycle
# @return [void]
2015-04-24 19:26:42 +02:00
Contract Any
2014-07-08 04:43:22 +02:00
def queue_current_paths
@to_clean = []
return unless File.exist?(@app.config[:build_dir])
paths = ::Middleman::Util.all_files_under(@app.config[:build_dir]).map do |path|
Pathname(path)
end
@to_clean = paths.select do |path|
path.realpath.relative_path_from(@build_dir.realpath).to_s !~ /\/\./ || path.to_s =~ /\.(htaccess|htpasswd)/
2014-07-08 04:43:22 +02:00
end
# handle UTF-8-MAC filename on MacOS
@to_clean = @to_clean.map do |path|
if RUBY_PLATFORM =~ /darwin/
Pathname(path.to_s.encode('UTF-8', 'UTF-8-MAC'))
else
Pathname(path)
end
end
end
# Remove files which were not built in this cycle
2015-04-24 19:26:42 +02:00
Contract ArrayOf[Pathname]
2016-01-11 02:14:41 +01:00
def clean!
to_remove = @to_clean.reject do |f|
app.config[:skip_build_clean].call(f.to_s)
end
to_remove.each do |f|
2014-07-08 04:43:22 +02:00
FileUtils.rm(f)
trigger(:deleted, f)
end
end
Contract String => String
def binary_encode(string)
string.force_encoding('ascii-8bit') if string.respond_to?(:force_encoding)
string
end
Contract Symbol, Or[String, Pathname], Maybe[String] => Any
def trigger(event_type, target, extra=nil)
@events[event_type] ||= []
@events[event_type] << target
execute_callbacks(:on_build_event, [event_type, target, extra])
2014-07-08 04:43:22 +02:00
end
end
end