rails_build 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cd82bac7610eed4089c5be9667a685dea9ebd327
4
+ data.tar.gz: 99110d59847285fad099e0769f333f49aded632f
5
+ SHA512:
6
+ metadata.gz: 4c4ddd3929c28ada377db4c57811e3934aaeeaae6e30e6fa920f158e5cd7c9bce2276111fbda6f15b23685658fe88cb107593d981e8bf4d0dd9788036eb9f8be
7
+ data.tar.gz: e1203289ae526130f34645705cefc37edf2409065671bd591991084755eabe7e0aeb1a37838dc9d94bde6a9793d370163f24cce9ccac9843cf443322c2a55642
@@ -0,0 +1,20 @@
1
+ Copyright 2016 ahoward
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,179 @@
1
+ # RailsBuild
2
+
3
+ A very small, very simple, very fast, and bullet proof static site generator
4
+ built as a Rails 5 engine.
5
+
6
+
7
+
8
+ ## How It Works
9
+
10
+ RailsBuild bundles all your assets, public directory, and configured urls into
11
+ a static site suitable for deployment to Netlify, Amazon S3, or your favorite
12
+ static website hosting solution. It does this by:
13
+
14
+ - Booting your application in *production* mode
15
+ - Precompiling all assets
16
+ - Including every static resource in ./public/
17
+ - GET'ing every configured url via super fast parallel downloading
18
+
19
+ RailsBuild let's you leverage the entire Rails' ecosystem for your static
20
+ sites and requires learning practically no new techniques to make super fast
21
+ building static websites.
22
+
23
+
24
+
25
+ ## Configuration
26
+
27
+ Configuration is quite simple, typically it will be much less than comparable
28
+ static site generators. You need to drop a *./config/rails_build.rb* looking
29
+ something like this into your app:
30
+
31
+ ```ruby
32
+
33
+ RailsBuild.configure do |rails_build|
34
+
35
+ urls = rails_build.urls
36
+
37
+ urls << "/"
38
+
39
+ urls << "/about/"
40
+
41
+ urls << "/contact/"
42
+
43
+
44
+ Post.each do |post|
45
+ urls << blog_path(post)
46
+ end
47
+
48
+ end
49
+
50
+
51
+ ```
52
+
53
+ That's it: simply enumerate the urls - anything additional to your assets and
54
+ ./public/ directory - that you want to include in your build.
55
+
56
+ ## On Trailing Slashes
57
+
58
+ Most static hosting solutions support Apache style directory indexing will be
59
+ better behaved with urls that look like
60
+
61
+ ```markdown
62
+
63
+ http://my.site.com/blog/
64
+
65
+ ```
66
+
67
+ vs.
68
+
69
+ ```markdown
70
+
71
+ http://my.site.com/blog
72
+
73
+ ```
74
+
75
+ RailsBuild tries to help you do this with a little bit of Rails' config that
76
+ is turned on by default but which can be turned off via
77
+
78
+ ```ruby
79
+
80
+ RailsBuild.configure do |rails_build|
81
+
82
+ rails_build.trailing_slash false # the default is 'true'
83
+
84
+ end
85
+
86
+ ```
87
+
88
+ The only real impact will be felt if you are using relative urls in your site
89
+ like './about' vs. '../about'
90
+
91
+
92
+
93
+ ## Optimization and Notes
94
+
95
+
96
+ RailsBuild is fast. Very fast. [DOJO4](http://dojo4.com) has seen optimized [Middleman](https://middlemanapp.com/) builds of > 30 minutes dropped to *60 seconds* by simply making basic use of Rails' built-in caching facilites.
97
+
98
+ When trying to squeeze out performance just remember that RailsBuild runs in
99
+ production mode and, therefore, making a build go fast follows the *exact same
100
+ rules* as making anything other Rails' application fast. The first place to
101
+ reach is typically fragment caching of partials used in your app.
102
+
103
+ Finally, don't forget about *./config/initializers/assets.rb* - RailsBuild
104
+ doesn't do anything special to the asset pipeline and only those assets
105
+ normally built when
106
+
107
+ ```bash
108
+
109
+ ~> rake assets:precompile
110
+
111
+ ```
112
+
113
+ is run will be included in the build.
114
+
115
+
116
+
117
+ ## Installation
118
+
119
+ Add this line to your application's Gemfile:
120
+
121
+ ```ruby
122
+
123
+ gem 'rails_build'
124
+
125
+
126
+ ```
127
+
128
+ And then execute:
129
+
130
+ ```bash
131
+
132
+ $ bundle
133
+
134
+ $ bundle binstubs rails_build
135
+
136
+
137
+ ```
138
+
139
+
140
+
141
+ ## Building Your Site
142
+
143
+
144
+ After installation and configuration simply run
145
+
146
+ ```bash
147
+
148
+ ~> ./bin/rails_build
149
+
150
+
151
+ # or, if you prefer, simply
152
+
153
+ ~> rails build
154
+
155
+
156
+ ```
157
+
158
+
159
+
160
+ ## Netlfiy
161
+
162
+ We love Netlify at [DOJO4](http://dojo4.com). RailsBuild works with netlify
163
+ out of the box and simply requires
164
+
165
+ ```yaml
166
+
167
+ build_command : rails_build
168
+
169
+ build_directory: build
170
+
171
+ ```
172
+
173
+ to be configured as the build command and directory respectively.
174
+
175
+
176
+
177
+ ## License
178
+
179
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,37 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'RailsBuild'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+
21
+ load 'rails/tasks/statistics.rake'
22
+
23
+
24
+
25
+ require 'bundler/gem_tasks'
26
+
27
+ require 'rake/testtask'
28
+
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.libs << 'lib'
31
+ t.libs << 'test'
32
+ t.pattern = 'test/**/*_test.rb'
33
+ t.verbose = false
34
+ end
35
+
36
+
37
+ task default: :test
@@ -0,0 +1,2 @@
1
+ //= link_directory ../javascripts/rails_build .js
2
+ //= link_directory ../stylesheets/rails_build .css
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file. JavaScript code in this file should be added after the last require_* statement.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,32 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
16
+
17
+ body{
18
+ font-size: 150%;
19
+ }
20
+
21
+ a:link,
22
+ a:visited,
23
+ a:active
24
+ {
25
+ color: black;
26
+ }
27
+ a:hover
28
+ {
29
+ color: #FF69B4;
30
+ }
31
+
32
+
@@ -0,0 +1,12 @@
1
+ module RailsBuild
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+
5
+ def index
6
+ end
7
+
8
+ def configuration
9
+ render :json => RailsBuild.configuration, :layout => false
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ module RailsBuild
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module RailsBuild
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module RailsBuild
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module RailsBuild
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Rails build</title>
5
+ <%= stylesheet_link_tag "rails_build/application", media: "all" %>
6
+ <%= javascript_include_tag "rails_build/application" %>
7
+ <%= csrf_meta_tags %>
8
+ </head>
9
+ <body>
10
+
11
+ <%= yield %>
12
+
13
+ </body>
14
+ </html>
@@ -0,0 +1,20 @@
1
+ <%
2
+ @links = [
3
+
4
+ rails_build.url_for(:action => :index, :only_path => true),
5
+
6
+ rails_build.url_for(:action => :configuration, :only_path => true)
7
+
8
+ ].map{|href| link_to(href, href)}
9
+ %>
10
+
11
+ <br>
12
+ <hr>
13
+
14
+ <ul>
15
+ <% @links.each do |link| %>
16
+ <li>
17
+ <%= link %>
18
+ </li>
19
+ <% end %>
20
+ </ul>
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path('../..', __FILE__)
6
+ ENGINE_PATH = File.expand_path('../../lib/rails_build/engine', __FILE__)
7
+
8
+ # Set up gems listed in the Gemfile.
9
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
10
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
11
+
12
+ require 'rails/all'
13
+ require 'rails/engine/commands'
@@ -0,0 +1,730 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # file : ./bin/rails_build
4
+
5
+ END{
6
+
7
+ RailsBuild::CLI.build!
8
+
9
+ }
10
+
11
+
12
+ module RailsBuild
13
+ class CLI
14
+ def CLI.usage
15
+ <<-__
16
+ NAME
17
+ rails_build
18
+
19
+ SYNOPSIS
20
+ a small, simple, bullet proof, and very fast static site generator built on rails 5
21
+
22
+ USAGE
23
+ rails_build [rails_root] *(options)
24
+
25
+ options:
26
+ --help, -h : this message
27
+ --rails_root,--r : specifiy the RAILS_ROOT, default=./
28
+ --parallel,--p : how many requests to make in parallel, default=n_cpus/2
29
+ --env,--e : speciify the RAILS_ENV, default=production
30
+ --server, -s : provide the url of the build server, do *not* start separate one
31
+ --log, -l : specify the logfile, default=STDERR
32
+ --verbose, -v : turn on verbose logging
33
+ __
34
+ end
35
+
36
+ def CLI.opts
37
+ GetoptLong.new(
38
+ [ '--help' , '-h' , GetoptLong::NO_ARGUMENT ] ,
39
+ [ '--server' , '-s' , GetoptLong::REQUIRED_ARGUMENT ] ,
40
+ [ '--parallel' , '-p' , GetoptLong::REQUIRED_ARGUMENT ] ,
41
+ [ '--rails_root' , '-r' , GetoptLong::REQUIRED_ARGUMENT ] ,
42
+ [ '--env' , '-e' , GetoptLong::REQUIRED_ARGUMENT ] ,
43
+ [ '--log' , '-l' , GetoptLong::REQUIRED_ARGUMENT ] ,
44
+ [ '--verbose' , '-v' , GetoptLong::NO_ARGUMENT ] ,
45
+ )
46
+ end
47
+
48
+ def build!
49
+ prepare!
50
+
51
+ mkdir!
52
+
53
+ start_server! unless server
54
+
55
+ clear_cache!
56
+
57
+ extract_urls! url_for('/rails_build/configuration.json')
58
+
59
+ precompile_assets!
60
+
61
+ rsync_public!
62
+
63
+ parallel_build!
64
+
65
+ finalize!
66
+ end
67
+
68
+ #
69
+ def CLI.build!(*args, &block)
70
+ new(*args, &block).build!
71
+ end
72
+
73
+ #
74
+ attr_accessor :rails_root
75
+ attr_accessor :server
76
+ attr_accessor :directory
77
+ attr_accessor :uuid
78
+ attr_accessor :id
79
+ attr_accessor :env
80
+ attr_accessor :puma
81
+ attr_accessor :passenger
82
+ attr_accessor :url
83
+
84
+ #
85
+ def initialize(*args, &block)
86
+ setup!
87
+
88
+ @args = parse_args!
89
+
90
+ @opts = parse_opts!
91
+
92
+ if @opts[:help]
93
+ usage!
94
+ exit(42)
95
+ end
96
+ end
97
+
98
+ #
99
+ def setup!
100
+ #
101
+ STDOUT.sync = true
102
+ STDERR.sync = true
103
+
104
+ #
105
+ ENV['SPRING_DISABLE'] = 'true'
106
+ ENV['DISABLE_SPRING'] = 'true'
107
+
108
+ #
109
+ %w[
110
+ fileutils pathname thread socket timeout time uri etc open-uri securerandom logger getoptlong rubygems json
111
+ ].each do |stdlib|
112
+ require(stdlib)
113
+ end
114
+ end
115
+
116
+ #
117
+ def prepare!
118
+ #
119
+ @rails_root = find_rails_root!(@args[0] || '.')
120
+
121
+ #
122
+ Dir.chdir(@rails_root)
123
+
124
+ #
125
+ if File.exists?('./Gemfile')
126
+ require 'bundler/setup'
127
+ Bundler.setup(:require => false)
128
+ end
129
+
130
+ #
131
+ %w[
132
+ threadify persistent_http phusion_passenger
133
+ ].each do |gem|
134
+ begin
135
+ require(gem)
136
+ rescue LoadError
137
+ abort "add gem 'rails_build' to your Gemfile"
138
+ end
139
+ end
140
+
141
+ #
142
+ begin
143
+ require('pry')
144
+ rescue LoadError
145
+ nil
146
+ end
147
+
148
+ #
149
+ @logger = Logger.new(@opts[:log] || STDERR)
150
+
151
+ @uuid = ENV['RAILS_BUILD']
152
+ @time = ENV['RAILS_BUILD_TIME']
153
+ @server = @opts[:server] || ENV['RAILS_BUILD_SERVER']
154
+ @env = @opts[:env] || ENV['RAILS_BUILD_ENV'] || ENV['RAILS_ENV']
155
+ @parallel = @opts[:parallel] || ENV['RAILS_BUILD_PARALLEL']
156
+ @verbose = @opts[:verbose] || ENV['RAILS_BUILD_VERBOSE']
157
+
158
+ @uuid ||= SecureRandom.uuid
159
+ @time ||= Time.now.utc
160
+ @env ||= 'production'
161
+ @parallel ||= (Etc.nprocessors/2)
162
+
163
+ unless @time.is_a?(Time)
164
+ @time = Time.parse(@time.to_s).utc
165
+ end
166
+
167
+ @parallel = @parallel.to_i
168
+
169
+
170
+ @puma = 'bundle exec puma'
171
+ @pumactl = 'bundle exec pumactl'
172
+
173
+ @passenger = 'bundle exec passenger'
174
+
175
+ if ENV['RAILS_BUILD_DIRECTORY']
176
+ @build_directory = File.expand_path(ENV['RAILS_BUILD_DIRECTORY'])
177
+ else
178
+ @build_directory = File.join(@rails_root, 'builds')
179
+ end
180
+
181
+ @directory = File.join(@build_directory, @uuid)
182
+
183
+ ENV['RAILS_ENV'] = @env
184
+ ENV['RAILS_BUILD'] = @uuid
185
+ ENV['RAILS_BUILD_ENV'] = @env
186
+ ENV['RAILS_BUILD_TIME'] = @time.httpdate
187
+
188
+ @urls = []
189
+
190
+ @started_at = Time.now
191
+ end
192
+
193
+ #
194
+ def find_rails_root!(path)
195
+ rails_root = File.expand_path(path.to_s)
196
+
197
+ loop do
198
+ is_rails_root = %w[ app lib config public ].all?{|dir| test(?d, File.join(rails_root, dir))}
199
+
200
+ if is_rails_root
201
+ return(rails_root)
202
+ else
203
+ rails_root = File.dirname(rails_root)
204
+ break if rails_root == '/'
205
+ end
206
+ end
207
+
208
+ abort("could not find a rails_root in or above #{ path }!?")
209
+ end
210
+
211
+ #
212
+ def parse_args!
213
+ @args = ARGV.map{|arg| "#{ arg }"}
214
+ end
215
+
216
+ #
217
+ def parse_opts!
218
+ @opts = Hash.new
219
+
220
+ CLI.opts.each do |opt, arg|
221
+ key, val = opt.split('--').last, arg
222
+ @opts[key] = (val == '' ? true : val)
223
+ end
224
+
225
+ @opts
226
+ end
227
+
228
+ #
229
+ def usage!
230
+ lines = CLI.usage.strip.split(/\n/)
231
+ n = lines[1].to_s.scan(/^\s+/).size
232
+ indent = ' ' * n
233
+ re = /^#{ Regexp.escape(indent) }/
234
+ usage = lines.map{|line| line.gsub(re, '')}.join("\n")
235
+ STDERR.puts(usage)
236
+ end
237
+
238
+ #
239
+ def mkdir!
240
+ FileUtils.rm_rf(@directory)
241
+ FileUtils.mkdir_p(@directory)
242
+ log(:info, "directory: #{ @directory }")
243
+ end
244
+
245
+ #
246
+ def start_server!
247
+ @server =
248
+ nil
249
+
250
+ @port =
251
+ nil
252
+
253
+ ports =
254
+ (2000 .. 9000).to_a
255
+
256
+ # TODO - get puma working...
257
+ =begin
258
+ pidfile = File.join(@directory, "puma.pid")
259
+ statefile = File.join(@directory, "puma-state.txt")
260
+ configfile = File.join(@directory, "puma-config.rb")
261
+ FileUtils.touch(configfile)
262
+ =end
263
+
264
+ start_server, stop_server = nil
265
+
266
+ ports.each do |port|
267
+ next unless port_open?(port)
268
+
269
+ # TODO - get puma working...
270
+ =begin
271
+ start_server =
272
+ "puma --port=#{ port } --environment=#{ @env } --pidfile=#{ pidfile } --state=#{ statefile } --config=#{ configfile } --daemon --preload config.ru"
273
+ stop_server =
274
+ "#{ @pumactl } --pidfile=#{ pidfile } --state=#{ statefile } stop"
275
+ =end
276
+ start_server =
277
+ "#{ @passenger } start --daemonize --environment #{ @env } --port #{ port } --max-pool-size 16"
278
+ stop_server =
279
+ "#{ @passenger } stop --port #{ port }"
280
+
281
+ `#{ stop_server } 2>&1`.strip
282
+
283
+ log(:info, "cmd: #{ start_server }")
284
+ server_output = `#{ start_server } 2>&1`.strip
285
+
286
+ log(:info, "status: #{ $?.exitstatus }")
287
+
288
+ t = Time.now.to_f
289
+ timeout = 10
290
+ i = 0
291
+
292
+ loop do
293
+ i += 1
294
+
295
+ begin
296
+ url = "http://localhost:#{ port }"
297
+ open(url){|socket| socket.read}
298
+ @server = url
299
+ @port = port
300
+ break
301
+ rescue Object => e
302
+ if i > 2
303
+ log :error, "#{ e.message }(#{ e.class })\n"
304
+ log :error, "#{ server_output }\n\n"
305
+ end
306
+
307
+ if((Time.now.to_f - t) > timeout)
308
+ abort("could not start server inside of #{ timeout } seconds ;-/")
309
+ else
310
+ sleep(rand(0.42))
311
+ end
312
+ end
313
+ end
314
+
315
+ break if @server
316
+ end
317
+
318
+ # barf if server could not be started
319
+ #
320
+ unless @server
321
+ abort("could not start server on any of ports #{ ports.first } .. #{ ports.last }")
322
+ else
323
+ log(:info, "started server on #{ @server }")
324
+ end
325
+
326
+ # set assassins to ensure the server daemon never outlives the build script
327
+ # no matter how it is killed (even -9)
328
+ #
329
+ at_exit{
330
+ log(:info, "cmd: #{ stop_server }")
331
+ `#{ stop_server } >/dev/null 2>&1`
332
+ log(:info, "status: #{ $?.exitstatus }")
333
+ log(:info, "stopped server #{ @server }")
334
+ }
335
+
336
+ assassin = <<-__
337
+ pid = #{ Process.pid }
338
+
339
+ 4242.times do
340
+ begin
341
+ Process.kill(0, pid)
342
+ rescue Object => e
343
+ if e.is_a?(Errno::ESRCH)
344
+ `#{ stop_server } >/dev/null 2>&1`
345
+ Process.kill(-15, pid) rescue nil
346
+ sleep(rand + rand)
347
+ Process.kill(-9, pid) rescue nil
348
+ end
349
+ exit
350
+ end
351
+ sleep(1 + rand)
352
+ end
353
+ __
354
+ IO.binwrite('tmp/build-assassin.rb', assassin)
355
+ cmd = "nohup ruby tmp/build-assassin.rb >/dev/null 2>&1 &"
356
+ system cmd
357
+
358
+ #
359
+ @started_at = Time.now
360
+ @server
361
+ end
362
+
363
+ #
364
+ def extract_urls!(build_url)
365
+ urls = []
366
+
367
+ code, body = http_get(build_url)
368
+
369
+ unless code == 200
370
+ raise "failed to get build urls from #{ build_url }"
371
+ end
372
+
373
+ @_build = JSON.parse(body)
374
+
375
+ unless @_build['urls'].is_a?(Array)
376
+ raise "failed to find any build urls at #{ build_url }"
377
+ end
378
+
379
+ urls = @_build['urls']
380
+
381
+ urls.map!{|url| url_for(url)}
382
+
383
+ log(:info, "extracted #{ urls.size } urls to build from #{ build_url }")
384
+
385
+ @urls = urls
386
+ end
387
+
388
+ #
389
+ def clear_cache!
390
+ #spawn "rake tmp:cache:clear"
391
+ spawn "rails runner 'Rails.cache.clear'"
392
+ end
393
+
394
+ #
395
+ def precompile_assets!
396
+ @asset_dir = File.join(@rails_root, "public/assets")
397
+ @asset_tmp = false
398
+
399
+ if test(?d, @asset_dir)
400
+ @asset_tmp = File.join(@rails_root, "tmp/assets-build-#{ @uuid }")
401
+ FileUtils.mv(@asset_dir, @asset_tmp)
402
+ end
403
+
404
+ spawn "RAILS_ENV=production rake assets:precompile"
405
+
406
+ assets = Dir.glob(File.join(@rails_root, 'public/assets/**/**'))
407
+
408
+ log(:info, "precompiled #{ assets.size } assets")
409
+
410
+ ensure_non_digested_assets_also_exist!(assets)
411
+ end
412
+
413
+ #
414
+ def rsync_public!
415
+ spawn "rsync -avz ./public/ #{ @directory }"
416
+
417
+ count = 0
418
+
419
+ Dir.glob(File.join(@directory, '**/**')).each{ count += 1 }
420
+
421
+ log(:info, "rsync'd #{ count } files")
422
+
423
+ if @asset_tmp
424
+ FileUtils.rm_rf(@asset_dir)
425
+ FileUtils.mv(@asset_tmp, @asset_dir)
426
+ end
427
+ end
428
+
429
+ #
430
+ def parallel_build!(n = nil)
431
+ n ||= @parallel
432
+
433
+ stats = {
434
+ :success => [], :missing => [], :failure => [],
435
+ }
436
+
437
+ times = Queue.new
438
+
439
+ avg = nil
440
+
441
+ Thread.new do
442
+ Thread.current.abort_on_exception = true
443
+ total = 0.0
444
+ n = 0
445
+ loop do
446
+ while(time = times.pop)
447
+ total += time
448
+ n += 1
449
+ avg = (total / n).round(2)
450
+ end
451
+ end
452
+ end
453
+
454
+ a = Time.now.to_f
455
+
456
+ _stats =
457
+ @urls.threadify(n) do |url|
458
+ uri = uri_for(url)
459
+ path = path_for(uri)
460
+ upath = uri.path
461
+ rpath = relative_path(path, :from => @directory)
462
+
463
+ _a = Time.now.to_f
464
+
465
+ code, body = http_get(uri)
466
+
467
+ _b = Time.now.to_f
468
+ _e = (_b - _a).round(2)
469
+
470
+ times.push(_e)
471
+
472
+ label = "#{ code } @ (t̄:#{ avg }s)"
473
+
474
+ case code
475
+ when 200
476
+ write_path(path, body)
477
+ log(:info, "#{ label }: #{ upath } -> #{ rpath } (t:#{ _e }s)")
478
+ [:success, url]
479
+ when 404
480
+ log(:error, "#{ label }: #{ upath }")
481
+ [:missing, url]
482
+ when 500
483
+ log(:error, "#{ label }: #{ upath }")
484
+ [:failure, url]
485
+ else
486
+ log(:error, "#{ label }: #{ upath }")
487
+ [:failure, url]
488
+ end
489
+ end
490
+
491
+ _stats.each{|stat, url| stats[stat].push(url)}
492
+
493
+ b = Time.now.to_f
494
+
495
+ borked = false
496
+
497
+ if stats[:missing].size > 0
498
+ borked = true
499
+ log(:error, "missing on #{ stats[:missing].size } urls")
500
+ end
501
+
502
+ if stats[:failure].size > 0
503
+ borked = true
504
+ log(:error, "failure on #{ stats[:failure].size } urls")
505
+ end
506
+
507
+ if borked
508
+ exit(1)
509
+ end
510
+
511
+ elapsed = b - a
512
+ n = @urls.size
513
+ rps = (n / elapsed).round(2)
514
+
515
+ log(:info, "downloaded #{ n } urls at ~#{ rps }/s")
516
+
517
+ @urls
518
+ end
519
+
520
+ #
521
+ def finalize!
522
+ @finished_at = Time.now
523
+ elapsed = (@finished_at.to_f - @started_at.to_f)
524
+ log(:info, "build time - #{ hms(elapsed) }")
525
+ on_netlify = ENV['DEPLOY_PRIME_URL'].to_s =~ /netlify/
526
+ strategy = on_netlify ? 'cp_r' : 'ln_s' # netlify refuses to deploy from a symlink ;-/ # FIXME
527
+ build = File.join(@rails_root, 'build')
528
+ FileUtils.rm_rf(build)
529
+ FileUtils.send(strategy, @directory, build)
530
+ log(:info, "preview with `static ./build/` # brew install node-static")
531
+ end
532
+
533
+ #
534
+ def http_get(url)
535
+ response = HTTP.get(url.to_s, :follow => true)
536
+ body = response.body.to_s rescue ''
537
+ code = response.status.code rescue 500
538
+ [code, body]
539
+ end
540
+
541
+ def http_connection
542
+ @http_connection ||= (
543
+ PersistentHTTP.new({
544
+ :url => @server,
545
+ :pool_size => (@parallel + 1),
546
+ :logger => (@verbose ? @logger : nil),
547
+ :pool_timeout => 10,
548
+ :warn_timeout => 1,
549
+ :force_retry => true,
550
+ })
551
+ )
552
+ end
553
+
554
+ def http_get(url)
555
+ request = Net::HTTP::Get.new(url)
556
+ response = http_connection.request(request)
557
+
558
+ if response.is_a?(Net::HTTPRedirection)
559
+ location = response['Location']
560
+ if location.to_s == url.to_s
561
+ log(:fatal, "circular redirection on #{ url }")
562
+ exit(1)
563
+ end
564
+ return http_get(location)
565
+ end
566
+
567
+ code = response.code.to_i rescue 500
568
+ body = response.body.to_s rescue ''
569
+
570
+ [code, body]
571
+ end
572
+
573
+ #
574
+ def path_for(url)
575
+ uri = uri_for(url)
576
+
577
+ case
578
+ when uri.path=='/' || uri.path=='.'
579
+ path = File.join(@directory, 'index.html')
580
+ else
581
+ path = File.join(@directory, uri.path)
582
+ dirname, basename = File.split(path)
583
+ base, ext = basename.split('.', 2)
584
+ if ext.nil?
585
+ path = File.join(path, 'index.html')
586
+ end
587
+ end
588
+ path
589
+ end
590
+
591
+ #
592
+ def write_path(path, body)
593
+ FileUtils.mkdir_p(File.dirname(path))
594
+ IO.binwrite(path, body)
595
+ end
596
+
597
+ #
598
+ def ensure_non_digested_assets_also_exist!(assets)
599
+ re = /(-{1}[a-z0-9]{32}*\.{1}){1}/
600
+
601
+ assets.each do |file|
602
+ next if File.directory?(file) || file !~ re
603
+ source = file.split('/')
604
+ source.push(source.pop.gsub(re, '.'))
605
+ non_digested = File.join(source)
606
+ #log(:debug, "asset: #{ file } -> #{ non_digested }")
607
+ FileUtils.ln(file, non_digested)
608
+ end
609
+ end
610
+
611
+ #
612
+ def url_for(url)
613
+ uri = URI.parse(url.to_s)
614
+
615
+ if uri.absolute?
616
+ uri.to_s
617
+ else
618
+ relative_uri = URI.parse(@server)
619
+ relative_uri.path = absolute_path_for(uri.path)
620
+ relative_uri.query = uri.query
621
+ relative_uri.fragment = uri.fragment
622
+ relative_uri.to_s
623
+ end
624
+ end
625
+
626
+ #
627
+ def uri_for(url)
628
+ uri = url.is_a?(URI) ? url : URI.parse(url.to_s)
629
+ end
630
+
631
+ #
632
+ def to_s
633
+ @directory.to_s
634
+ end
635
+
636
+ #
637
+ def log(level, *args, &block)
638
+ @logger.send(level, *args, &block)
639
+ end
640
+
641
+ #
642
+ def hms(seconds)
643
+ return unless seconds
644
+ "%02d:%02d:%02d" % hours_minutes_seconds(seconds)
645
+ end
646
+
647
+ #
648
+ def hours_minutes_seconds(seconds)
649
+ return unless seconds
650
+ seconds = Float(seconds).to_i
651
+ hours, seconds = seconds.divmod(3600)
652
+ minutes, seconds = seconds.divmod(60)
653
+ [hours.to_i, minutes.to_s, seconds]
654
+ end
655
+
656
+ #
657
+ def stopwatch(&block)
658
+ a = Time.now
659
+ result = block.call
660
+ b = Time.now
661
+ [result, b.to_f - a.to_f]
662
+ end
663
+
664
+ #
665
+ def port_open?(port, options = {})
666
+ seconds = options[:timeout] || 1
667
+ ip = options[:ip] || '0.0.0.0'
668
+
669
+ Timeout::timeout(seconds) do
670
+ begin
671
+ TCPSocket.new(ip, port).close
672
+ false
673
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
674
+ true
675
+ rescue Object
676
+ false
677
+ end
678
+ end
679
+ rescue Timeout::Error
680
+ false
681
+ end
682
+
683
+ #
684
+ def paths_for(*args)
685
+ path = args.flatten.compact.join('/')
686
+ path.gsub!(%r|[.]+/|, '/')
687
+ path.squeeze!('/')
688
+ path.sub!(%r|^/|, '')
689
+ path.sub!(%r|/$|, '')
690
+ paths = path.split('/')
691
+ end
692
+
693
+ #
694
+ def absolute_path_for(*args)
695
+ path = ('/' + paths_for(*args).join('/')).squeeze('/')
696
+ path unless path.strip.empty?
697
+ end
698
+
699
+ #
700
+ def relative_path_for(*args)
701
+ path = absolute_path_for(*args).sub(%r{^/+}, '')
702
+ path unless path.strip.empty?
703
+ end
704
+
705
+ #
706
+ def relative_path(path, *args)
707
+ options = args.last.is_a?(Hash) ? args.pop : {}
708
+ path = path.to_s
709
+ relative = args.shift || options[:relative] || options[:to] || options[:from]
710
+ if relative
711
+ Pathname.new(path).relative_path_from(Pathname.new(relative.to_s)).to_s
712
+ else
713
+ relative_path_for(path)
714
+ end
715
+ end
716
+
717
+ #
718
+ def spawn(command)
719
+ oe = `#{ command } 2>&1`
720
+
721
+ unless $? == 0
722
+ msg = "command(#{ command }) failed with (#{ $? })"
723
+ log(:error, msg)
724
+ raise(msg)
725
+ end
726
+
727
+ oe
728
+ end
729
+ end
730
+ end