-
-
Save matt-morris/6d4c130c3a3d44a2739b to your computer and use it in GitHub Desktop.
a script that boils a rails' app into static in a fucking hurry
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env ruby | |
# TODO - use a server other than passenger? | |
# this script uses wget and passenger to crawl a rails' application in order | |
# to create a static build from virtually any project. | |
# | |
# it has three simple requirements, and one optional one: | |
# | |
# 1) you have the wget program installed | |
# | |
# 2) you have | |
# | |
# gem 'passenger' | |
# | |
# in your Gemfile | |
# | |
# 3) you have configured your rails application such that urls end with | |
# trailing slashes. this is *** ÜBER IMPORTANT ***. putting the following in | |
# config/initializers/trailing_slashes.rb will do the trick. | |
# | |
# Rails.configuration.before_initialize do | |
# | |
# ActionController::Base.module_eval do | |
# before_filter :enforce_trailing_slash | |
# | |
# protected | |
# | |
# def enforce_trailing_slash | |
# if request.get? | |
# ext = request.fullpath.split('.', 2)[1] | |
# | |
# if ext.nil? and request.format.to_s == 'text/html' | |
# url = request.original_url | |
# url, qs = url.split('?') | |
# | |
# if !url.ends_with?('/') | |
# flash.keep | |
# | |
# url = url + '/' | |
# | |
# if qs | |
# url = url + '?' + qs | |
# end | |
# | |
# redirect_to(url) | |
# | |
# return | |
# end | |
# end | |
# end | |
# end | |
# | |
# end | |
# | |
# 4) although only required for development and test envs, if you want your | |
# site to build in development mode you'll need something like such that | |
# assets build correctly when rails is in development mode | |
# | |
# if ENV['RAILS_BUILD'] | |
# config.serve_static_assets = true | |
# config.assets.compress = true | |
# config.assets.compile = true | |
# config.assets.digest = true | |
# config.assets.initialize_on_precompile = false | |
# config.assets.debug = false | |
# end | |
# | |
# in config/environments/development.rb, etc. the last line is ***VERY | |
# IMPORTANT*** | |
# | |
# | |
# | |
# | |
# afterwards all you need to do is run | |
# | |
# ~> ./script/build | |
# | |
# and you'll have a preview-able static cache of all reachable pages in | |
# | |
# public/builds/$UUID | |
# | |
# check | |
# | |
# public/builds/$UUID/wget.oe | |
# | |
# if the build fails for any reasons. examine the detailed wget output for | |
# '404' or '500', etc. | |
# | |
# the build will be suitable for deployment to s3, bitballoon, etc. and will | |
# include all images, compiled assets, etc. | |
# | |
# make a build | |
@build = Build.new | |
@build.build! | |
puts @build.directory | |
BEGIN { | |
# | |
require 'fileutils' | |
require 'thread' | |
require 'socket' | |
require 'timeout' | |
require 'uri' | |
require 'open-uri' | |
require 'securerandom' | |
require 'rubygems' | |
require 'logger' | |
# awesome sauce static build support | |
# | |
class Build | |
# | |
attr_accessor :script_dir | |
attr_accessor :rails_root | |
attr_accessor :lib_dir | |
attr_accessor :directory | |
attr_accessor :uuid | |
attr_accessor :env | |
attr_accessor :passenger | |
attr_accessor :url | |
# | |
def initialize(*args, &block) | |
@options = args.last.is_a?(Hash) ? args.pop : {} | |
@logger = @options[:logger] || Logger.new(STDERR) | |
@script_dir = File.expand_path(File.dirname(__FILE__)) | |
@rails_root = File.expand_path(File.dirname(@script_dir)) | |
@lib_dir = File.join(@rails_root, 'lib') | |
@uuid = ENV['RAILS_BUILD'] || SecureRandom.uuid | |
@url = ENV['RAILS_BUILD_SERVER'] || ENV['RAILS_URL'] | |
@env = ENV['RAILS_BUILD_ENV'] || ENV['RAILS_ENV'] || 'development' | |
@passenger = 'bundle exec passenger' | |
Dir.chdir(@rails_root) | |
if File.exists?('./Gemfile') | |
require 'bundler/setup' | |
Bundler.setup(:require => false) | |
end | |
$LOAD_PATH.unshift(@lib_dir) | |
@directory = File.join(@rails_root, 'public', 'system', 'builds', @uuid) | |
ENV['RAILS_BUILD'] ||= @uuid | |
ENV['RAILS_BUILD_ENV'] ||= @env | |
end | |
# | |
def build! | |
log(:debug, "building #{ @directory }...") | |
log(:debug, "locking...") | |
lock! | |
log(:debug, "locked.") | |
start_passenger! unless @url | |
wget_mirror! | |
normalize_files! | |
end | |
# | |
def lock!(max = 300) | |
started_at = Time.now.to_f | |
loop do | |
if DATA.flock(File::LOCK_EX | File::LOCK_NB) | |
break | |
else | |
warn "could not obtain lock for #{ max } seconds..." | |
if((Time.now.to_f - started_at) > max) | |
warn "ignoring failed lock!" | |
break | |
else | |
sleep(rand) | |
end | |
end | |
end | |
end | |
# | |
def start_passenger! | |
validate_passenger_version! | |
@url = | |
nil | |
passenger_port = | |
nil | |
ports = | |
(3001 .. 4001).to_a | |
ports.each do |port| | |
next unless port_open?(port) | |
start_passenger = | |
"#{ @passenger } start --daemonize --environment #{ @env } --port #{ port } --max-pool-size 16 --min-instances 16" | |
passenger_output = | |
`#{ start_passenger } 2>&1`.strip | |
t = Time.now.to_f | |
timeout = 10 | |
i = 0 | |
loop do | |
i += 1 | |
begin | |
url = "http://0.0.0.0:#{ port }" | |
open(url){|socket| socket.read} | |
@url = url | |
passenger_port = port | |
break | |
rescue Object => e | |
if i > 2 | |
log :error, "#{ e.message }(#{ e.class })\n" | |
log :error, "#{ passenger_output }\n\n" | |
end | |
if((Time.now.to_f - t) > timeout) | |
abort("could not start passenger inside of #{ timeout } ;-/") | |
else | |
sleep(rand(0.42)) | |
end | |
end | |
end | |
break if @url | |
end | |
# barf if passenger could not be started | |
# | |
unless @url | |
abort("could not start passenger on any of ports #{ ports.first } .. #{ ports.last }") | |
end | |
log(:info, "started passenger on #{ @url }") | |
# set assassins to ensure the passenger daemon never outlives the build script | |
# no matter how it is killed (even -9) | |
# | |
stop_passenger = | |
"#{ @passenger } stop --port #{ passenger_port }" | |
at_exit{ | |
`#{ stop_passenger } >/dev/null 2>&1` | |
log(:info, "stopped passenger on #{ @url }") | |
} | |
pppid = Process.pid | |
unless fork | |
at_exit{ exit! } | |
Process.setsid | |
if fork | |
exit | |
else | |
loop do | |
begin | |
Process.kill(0, pppid) | |
rescue Object => e | |
if e.is_a?(Errno::ESRCH) | |
`#{ stop_passenger } >/dev/null 2>&1` | |
end | |
exit | |
end | |
sleep(1 + rand) | |
end | |
end | |
end | |
end | |
# | |
def validate_passenger_version! | |
abort("could not find passenger") unless `#{ @passenger }`.to_s =~ /phusion\s+passenger/i | |
passenger_version = `#{ passenger } --version 2>/dev/null`.to_s.match(/version\s+([0-9.]+)/).to_a.last | |
abort("could not find passenger") unless passenger_version | |
major = passenger_version.to_s.split('.').first.to_i | |
abort("could not find passenger >= 4") unless(major && major >= 4) | |
end | |
# | |
def port_open?(port, options = {}) | |
seconds = options[:timeout] || 1 | |
ip = options[:ip] || '0.0.0.0' | |
Timeout::timeout(seconds) do | |
begin | |
TCPSocket.new(ip, port).close | |
false | |
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH | |
true | |
rescue Object | |
false | |
end | |
end | |
rescue Timeout::Error | |
false | |
end | |
# | |
def wget_mirror! | |
FileUtils.rm_rf(@directory) | |
FileUtils.mkdir_p(@directory) | |
Dir.chdir(@directory) do | |
mirrored = false | |
a = Time.now | |
wget = "wget -m -r -E -nH -kK -l 42 --trust-server-names -P . #{ @url } > wget.oe 2>&1" | |
mirrored = system(wget) | |
b = Time.now | |
if mirrored | |
log(:info, "built site in #{ (b.to_f - a.to_f) }s") | |
else | |
log(:error, "failed to build site!") | |
end | |
end | |
end | |
# | |
def normalize_files! | |
glob = File.join(@directory, '**/**') | |
Dir.glob(glob) do |entry| | |
next unless test(?f, entry) | |
dirname = File.dirname(entry) | |
basename = File.basename(entry) | |
base, query = basename.split('?', 2) | |
if query | |
path = File.join(dirname, base) | |
FileUtils.cp(entry, path) | |
FileUtils.rm(entry) | |
end | |
end | |
end | |
# | |
def to_s | |
@directory.to_s | |
end | |
# | |
def log(level, *args, &block) | |
@logger.send(level, *args, &block) | |
end | |
end | |
} | |
__END__ | |
__LOCK__ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment