rails_build 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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