module Glim::Commands

Public Class Methods

build(config) click to toggle source
# File lib/commands.rb, line 3
def self.build(config)
  output_dir = File.expand_path(config['destination'])
  files      = config.site.files_and_documents.select { |file| file.write? }
  symlinks   = (config.site.symlinks || []).map { |link| [ File.expand_path(File.join(link[:data]['domain'] || '.', link[:name]), output_dir), link[:realpath] ] }.to_h

  output_paths = files.map { |file| file.output_path(output_dir) }
  output_paths.concat(symlinks.keys)

  delete_files, delete_dirs = items_in_directory(output_dir, skip: config['keep_files'])
  deleted = delete_items(delete_files, delete_dirs, keep: output_paths)
  created, updated, warnings, errors = *generate(output_dir, config['jobs'] || 7, files, backtrace: config['show_backtrace'])

  symlinks.each do |dest, path|
    FileUtils.mkdir_p(File.dirname(dest))
    begin
      File.symlink(path, dest)
      created << dest
    rescue Errno::EEXIST
      if File.readlink(dest) != path
        File.unlink(dest)
        File.symlink(path, dest)
        updated << dest
      end
    end
  end

  [ [ 'Created', created ], [ 'Deleted', deleted ], [ 'Updated', updated ] ].each do |label, files|
    unless files.empty?
      STDERR.puts "==> #{label} #{files.size} #{files.size == 1 ? 'File' : 'Files'}"
      STDERR.puts files.map { |path| Util.relative_path(path, output_dir) }.sort.join(', ')
    end
  end

  unless warnings.empty?
    STDERR.puts "==> #{warnings.size} #{warnings.size == 1 ? 'Warning' : 'Warnings'}"
    warnings.each do |message|
      STDERR.puts message
    end
  end

  unless errors.empty?
    STDERR.puts "==> Stopped After #{errors.size} #{errors.size == 1 ? 'Error' : 'Errors'}"
    errors.each do |arr|
      arr.each_with_index do |err, i|
        STDERR.puts err.gsub(/^/, '  '*i)
      end
    end
  end
end
clean(config) click to toggle source
# File lib/commands.rb, line 53
def self.clean(config)
  files, dirs = items_in_directory(File.expand_path(config['destination']), skip: config['keep_files'])

  if config['dry_run']
    if files.empty?
      STDOUT.puts "No files to delete"
    else
      files.each do |file|
        STDOUT.puts "Delete #{Util.relative_path(file, File.expand_path(config['source']))}"
      end
    end
  else
    deleted = delete_items(files, dirs)
    STDOUT.puts "Deleted #{deleted.size} #{deleted.size == 1 ? 'File' : 'Files'}."
  end
end
profile(config) click to toggle source
# File lib/commands.rb, line 70
def self.profile(config)
  Profiler.enabled = true

  site = Profiler.run("Setting up site") do
    config.site
  end

  Profiler.run("Loading cache") do
    Glim::Cache.load
  end

  files = []

  Profiler.run("Loading pages") do
    files.concat(site.files)
  end

  Profiler.run("Loading collections") do
    files.concat(site.documents)
  end

  Profiler.run("Generating virtual pages") do
    files.concat(site.generated_files)
  end

  files = files.select { |file| file.frontmatter? }

  Profiler.run("Expanding liquid tags") do
    files.each { |file| file.content('post-liquid') }
  end

  Profiler.run("Transforming pages") do
    files.each { |file| file.content('pre-output') }
  end

  Profiler.run("Creating final output (layout)") do
    files.each { |file| file.output }
  end

  Profiler.enabled = false
end

Private Class Methods

delete_items(files, dirs, keep: []) click to toggle source
# File lib/commands.rb, line 140
def self.delete_items(files, dirs, keep: [])
  res = []

  keep_files = Set.new(keep)
  files.each do |path|
    unless keep_files.include?(path)
      begin
        File.unlink(path)
        res << path
      rescue => e
        $log.error("Error unlinking ‘#{path}’: #{e}\n")
      end
    end
  end

  dirs.sort.reverse_each do |path|
    begin
      Dir.rmdir(path)
    rescue Errno::ENOTEMPTY => e
      # Ignore
    rescue => e
      $log.error("Error removing directory ‘#{path}’: #{e}\n")
    end
  end

  res
end
generate(output_dir, number_of_jobs, files, backtrace: false) click to toggle source
# File lib/commands.rb, line 170
def self.generate(output_dir, number_of_jobs, files, backtrace: false)
  Profiler.run("Creating pages") do
    if number_of_jobs == 1
      generate_subset(output_dir, files, backtrace: backtrace)
    else
      generate_async(output_dir, files.shuffle, number_of_jobs, backtrace: backtrace)
    end
  end
end
generate_async(output_dir, files, number_of_jobs, backtrace: false) click to toggle source
# File lib/commands.rb, line 182
def self.generate_async(output_dir, files, number_of_jobs, backtrace: false)
  total  = files.size
  slices = number_of_jobs.times.map do |i|
    first = (total *    i  / number_of_jobs).ceil
    last  = (total * (i+1) / number_of_jobs).ceil
    files.shift(last-first)
  end

  Glim::Cache.track_updates = true
  semaphore = Mutex.new
  created, updated, warnings, errors = [], [], [], []

  threads = slices.each_with_index.map do |files_slice, i|
    pipe_rd, pipe_wr = IO.pipe
    pid = fork do
      start = Time.now
      pipe_rd.close
      created, updated, warnings, errors = *generate_subset(output_dir, files_slice, backtrace: backtrace)
      pipe_wr << Marshal.dump({
        'cache_updates' => Glim::Cache.updates,
        'created'       => created,
        'updated'       => updated,
        'warnings'      => warnings,
        'errors'        => errors,
        'duration'      => Time.now - start,
        'id'            => i,
      })
      pipe_wr.close
    end

    Process.detach(pid)

    Thread.new do
      pipe_wr.close
      res = Marshal.load(pipe_rd)
      semaphore.synchronize do
        Glim::Cache.merge!(res['cache_updates'])
        created  += res['created']
        updated  += res['updated']
        warnings += res['warnings']
        errors   += res['errors']
        $log.debug("Wrote #{files_slice.size} pages in #{res['duration']} seconds (thread #{res['id']})") if Profiler.enabled
      end
    end
  end

  threads.each { |thread| thread.join }

  [ created, updated, warnings, errors ]
end
generate_subset(output_dir, files, backtrace: false) click to toggle source
# File lib/commands.rb, line 235
def self.generate_subset(output_dir, files, backtrace: false)
  created, updated, warnings, errors = [], [], [], []

  files.each do |file|
    dest = file.output_path(output_dir)
    file_exists = File.exists?(dest)

    FileUtils.mkdir_p(File.dirname(dest))
    if file.frontmatter?
      begin
        if !file_exists || File.read(dest) != file.output
          File.unlink(dest) if file_exists
          File.write(dest, file.output)
          (file_exists ? updated : created) << dest
        end
        warnings.concat(file.warnings.map { |warning| "#{file}: #{warning}" }) unless file.warnings.nil?
      rescue Glim::Error => e
        errors << [ "Unable to create output for: #{file}", *e.messages ]
        break
      rescue => e
        error = [ "Unable to create output for: #{file}", e.to_s ]
        error << e.backtrace.join("\n") if backtrace
        errors << error
        break
      end
    else
      unless File.file?(dest) && File.file?(file.path) && File.stat(dest).ino == File.stat(file.path).ino
        File.unlink(dest) if file_exists
        File.link(file.path, dest)
      end
    end
  end

  [ created, updated, warnings, errors ]
end
items_in_directory(dir, skip: []) click to toggle source

Private =

# File lib/commands.rb, line 116
def self.items_in_directory(dir, skip: [])
  files, dirs = [], []

  begin
    Find.find(dir) do |path|
      next if path == dir
      Find.prune if skip.include?(File.basename(path))

      if File.file?(path) || File.symlink?(path)
        files << path
      elsif File.directory?(path)
        dirs << path
      else
        $log.warn("Unknown entry: #{path}")
      end
    end
  rescue Errno::ENOENT
  end

  [ files, dirs ]
end