rails_build 1.0.0 → 2.4.2

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.
data/bin/rails_build CHANGED
@@ -1,197 +1,154 @@
1
1
  #!/usr/bin/env ruby
2
-
3
- # file : ./bin/rails_build
4
-
5
- END{
6
-
7
- RailsBuild::CLI.build!
8
-
9
- }
10
-
2
+ # encoding: utf-8
11
3
 
12
4
  module RailsBuild
13
- class CLI
14
- def CLI.usage
15
- <<-__
16
- NAME
17
- rails_build
5
+ class CLI
6
+ def CLI.usage
7
+ <<~__
8
+ NAME
9
+ rails_build
18
10
 
19
- SYNOPSIS
20
- a small, simple, bullet proof, and very fast static site generator built on rails 5
11
+ SYNOPSIS
12
+ a small, simple, bullet proof, and fast enough static site generator built on top of the rails you already know and love
21
13
 
22
14
  USAGE
23
- rails_build [rails_root] *(options)
15
+ rails_build *(options)
24
16
 
25
17
  options:
26
18
  --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
19
+ --init, -i : initialize ./config/rails_build.rb
20
+ --parallel,--p : how many requests to make in parallel, default=n_cpus-1
29
21
  --env,--e : speciify the RAILS_ENV, default=production
30
- --server, -s : provide the url of the build server, do *not* start separate one
22
+ --url, -u : provide the url of the build server, do *not* start separate one
31
23
  --log, -l : specify the logfile, default=STDERR
32
24
  --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!
25
+ __
26
+ end
52
27
 
53
- start_server! unless server
28
+ def CLI.opts
29
+ GetoptLong.new(
30
+ [ '--help' , '-h' , GetoptLong::NO_ARGUMENT ] ,
31
+ [ '--init' , '-i' , GetoptLong::NO_ARGUMENT ] ,
32
+ [ '--parallel' , '-p' , GetoptLong::REQUIRED_ARGUMENT ] ,
33
+ [ '--env' , '-e' , GetoptLong::REQUIRED_ARGUMENT ] ,
34
+ [ '--url' , '-u' , GetoptLong::REQUIRED_ARGUMENT ] ,
35
+ [ '--server' , '-s' , GetoptLong::REQUIRED_ARGUMENT ] ,
36
+ [ '--log' , '-l' , GetoptLong::REQUIRED_ARGUMENT ] ,
37
+ [ '--verbose' , '-v' , GetoptLong::NO_ARGUMENT ] ,
38
+ )
39
+ end
54
40
 
55
- clear_cache!
41
+ def run!
42
+ @args = parse_args!
43
+ @opts = parse_opts!
56
44
 
57
- extract_urls! url_for('/rails_build/configuration.json')
45
+ case
46
+ when @args[0] == 'help' || @opts[:help]
47
+ usage!
58
48
 
59
- precompile_assets!
49
+ when @args[0] == 'init' || @opts[:init]
50
+ init!
60
51
 
61
- rsync_public!
52
+ else
53
+ if @args.empty?
54
+ build!
55
+ else
56
+ usage!
57
+ exit(42)
58
+ end
59
+ end
60
+ end
62
61
 
63
- parallel_build!
62
+ def build!
63
+ prepare!
64
64
 
65
- finalize!
66
- end
65
+ load_config!
67
66
 
68
- #
69
- def CLI.build!(*args, &block)
70
- new(*args, &block).build!
71
- end
67
+ unless url
68
+ clear_cache!
69
+ start_server!
70
+ end
72
71
 
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
72
+ extract_urls!
83
73
 
84
- #
85
- def initialize(*args, &block)
86
- setup!
74
+ precompile_assets!
87
75
 
88
- @args = parse_args!
76
+ rsync_public!
89
77
 
90
- @opts = parse_opts!
78
+ parallel_build!
91
79
 
92
- if @opts[:help]
93
- usage!
94
- exit(42)
95
- end
96
- end
80
+ finalize!
81
+ end
97
82
 
98
83
  #
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
84
+ def CLI.run!(*args, &block)
85
+ new(*args, &block).run!
114
86
  end
115
87
 
116
- #
117
- def prepare!
118
- #
119
- @rails_root = find_rails_root!(@args[0] || '.')
88
+ #
89
+ attr_accessor :rails_root
90
+ attr_accessor :url
91
+ attr_accessor :server
92
+ attr_accessor :directory
93
+ attr_accessor :uuid
94
+ attr_accessor :id
95
+ attr_accessor :env
96
+ attr_accessor :parallel
120
97
 
98
+ #
99
+ def prepare!
121
100
  #
122
- Dir.chdir(@rails_root)
101
+ @rails_root = find_rails_root!
123
102
 
124
103
  #
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
104
+ Dir.chdir(@rails_root)
140
105
 
141
106
  #
142
- begin
143
- require('pry')
144
- rescue LoadError
145
- nil
146
- end
107
+ @logger = Logger.new(@opts[:log] || STDERR)
147
108
 
148
- #
149
- @logger = Logger.new(@opts[:log] || STDERR)
109
+ @env = @opts[:env] || ENV['RAILS_BUILD_ENV'] || ENV['RAILS_ENV']
110
+ @url = @opts[:url] || ENV['RAILS_BUILD_URL']
111
+ @parallel = @opts[:parallel] || ENV['RAILS_BUILD_PARALLEL']
112
+ @verbose = @opts[:verbose] || ENV['RAILS_BUILD_VERBOSE']
150
113
 
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']
114
+ @uuid = ENV['RAILS_BUILD_UUID']
115
+ @time = ENV['RAILS_BUILD_TIME']
157
116
 
158
- @uuid ||= SecureRandom.uuid
159
- @time ||= Time.now.utc
160
- @env ||= 'production'
161
- @parallel ||= (Etc.nprocessors/2)
117
+ @env ||= 'production'
118
+ @parallel ||= (Etc.nprocessors - 1)
119
+ @uuid ||= SecureRandom.uuid
120
+ @time ||= Time.now.utc
162
121
 
163
- unless @time.is_a?(Time)
164
- @time = Time.parse(@time.to_s).utc
165
- end
166
-
167
- @parallel = @parallel.to_i
122
+ unless @time.is_a?(Time)
123
+ @time = Time.parse(@time.to_s).utc
124
+ end
168
125
 
126
+ @parallel = @parallel.to_i
169
127
 
170
- @puma = 'bundle exec puma'
171
- @pumactl = 'bundle exec pumactl'
128
+ if ENV['RAILS_BUILD_DIRECTORY']
129
+ @build_directory = File.expand_path(ENV['RAILS_BUILD_DIRECTORY'])
130
+ else
131
+ @build_directory = File.join(@rails_root, 'builds')
132
+ end
172
133
 
173
- @passenger = 'bundle exec passenger'
134
+ @directory = File.join(@build_directory, @uuid)
174
135
 
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
136
+ ENV['RAILS_ENV'] = @env
137
+ ENV['RAILS_BUILD'] = @uuid
138
+ ENV['RAILS_BUILD_ENV'] = @env
139
+ ENV['RAILS_BUILD_TIME'] = @time.httpdate
180
140
 
181
- @directory = File.join(@build_directory, @uuid)
141
+ @urls = []
182
142
 
183
- ENV['RAILS_ENV'] = @env
184
- ENV['RAILS_BUILD'] = @uuid
185
- ENV['RAILS_BUILD_ENV'] = @env
186
- ENV['RAILS_BUILD_TIME'] = @time.httpdate
143
+ @started_at = Time.now
187
144
 
188
- @urls = []
145
+ mkdir!
189
146
 
190
- @started_at = Time.now
191
- end
147
+ @server = Server.new(cli: self)
148
+ end
192
149
 
193
150
  #
194
- def find_rails_root!(path)
151
+ def find_rails_root!(path = '.')
195
152
  rails_root = File.expand_path(path.to_s)
196
153
 
197
154
  loop do
@@ -208,523 +165,755 @@ module RailsBuild
208
165
  abort("could not find a rails_root in or above #{ path }!?")
209
166
  end
210
167
 
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
168
+ #
169
+ def parse_args!
170
+ @args = ARGV.map{|arg| "#{ arg }"}
171
+ end
172
+
173
+ #
174
+ def parse_opts!
175
+ @opts = Hash.new
176
+
177
+ CLI.opts.each do |opt, arg|
178
+ key, val = opt.split('--').last, arg
179
+ @opts[key.to_s.to_sym] = (val == '' ? true : val)
180
+ end
181
+
182
+ @opts
183
+ end
184
+
185
+ #
186
+ def usage!
187
+ lines = CLI.usage.strip.split(/\n/)
188
+ n = lines[1].to_s.scan(/^\s+/).size
189
+ indent = ' ' * n
190
+ re = /^#{ Regexp.escape(indent) }/
191
+ usage = lines.map{|line| line.gsub(re, '')}.join("\n")
192
+ STDERR.puts(usage)
193
+ end
194
+
195
+ #
196
+ def init!
197
+ config = DATA.read.strip
198
+
199
+ path = './config/rails_build.rb'
200
+
201
+ FileUtils.mkdir_p(File.dirname(path))
202
+
203
+ IO.binwrite(path, config)
204
+
205
+ STDERR.puts("please review #{ path } before running `rails_build`")
206
+ end
207
+
208
+ #
209
+ def mkdir!
210
+ FileUtils.rm_rf(@directory)
211
+ FileUtils.mkdir_p(@directory)
212
+
213
+ log(:info, "build: #{ @directory }")
214
+ end
215
+
216
+ #
217
+ def start_server!
218
+ @url =
219
+ nil
220
+
221
+ @port =
222
+ nil
223
+
224
+ ports =
225
+ (2000 .. 9000).to_a
226
+
227
+ ports.each do |port|
228
+ next unless port_open?(port)
229
+
230
+ @server.start!(port:)
231
+
232
+ timeout = 11
233
+ t = Time.now.to_f
234
+ i = 0
235
+
236
+ @proto = @config.fetch('force_ssl') ? 'https' : 'http'
237
+ url = nil
238
+
239
+ loop do
240
+ i += 1
241
+ sleep(rand(0.42))
242
+
243
+ begin
244
+ raise if port_open?(port)
245
+ url = "#{ @proto }://0.0.0.0:#{ port }"
246
+ @url = url
247
+ @port = port
248
+ break
249
+ rescue Object => e
250
+ if((Time.now.to_f - t) > timeout)
251
+ abort("could not start server inside of #{ timeout } seconds")
252
+ end
253
+ end
254
+ end
255
+
256
+ break if @url
257
+ end
258
+
259
+ # barf if server could not be started
260
+ #
261
+ unless @url
262
+ abort("could not start server on any of ports #{ ports.first } .. #{ ports.last }")
263
+ else
264
+ log(:info, "url: #{ @url }")
265
+ end
266
+
267
+ #
268
+ @started_at = Time.now
269
+ @url
270
+ end
271
+
272
+ #
273
+ def load_config!
274
+ unless test(?s, RailsBuild.config_path)
275
+ log(:error, "no config found in #{ RailsBuild.config_path }")
276
+ abort
277
+ end
278
+
279
+ Tempfile.open do |tmp|
280
+ env = {RAILS_ENV:@env, RAILS_BUILD_CONFIG_DUMP:tmp.path}
281
+ spawn('rails', 'runner', 'RailsBuild.dump_config!', env:)
282
+ json = IO.binread(tmp.path)
283
+ hash = JSON.parse(json)
284
+
285
+ @config = Configuration.new(hash)
286
+ end
287
+ end
288
+
289
+ def extract_urls!
290
+ path = @config.path
291
+ urls = @config.urls
292
+
293
+ if urls.size == 0
294
+ abort("failed to find any rails_build urls in:\n#{ @config.to_json }")
295
+ end
296
+
297
+ urls.map!{|url| url_for(url)}
298
+
299
+ log(:info, "extracted #{ urls.size } url(s) to build from #{ path }")
300
+
301
+ @urls = urls
302
+ end
303
+
304
+ #
305
+ def clear_cache!
306
+ spawn "rails tmp:cache:clear", error: false
307
+ spawn "rails runner 'Rails.cache.clear'", error: false
308
+ end
309
+
310
+ #
311
+ def precompile_assets!
312
+ @asset_dir = File.join(@rails_root, "public/assets")
313
+ @asset_tmp = false
314
+
315
+ if test(?d, @asset_dir)
316
+ @asset_tmp = File.join(@rails_root, "tmp/assets-build-#{ @uuid }")
317
+ FileUtils.mv(@asset_dir, @asset_tmp)
318
+ end
319
+
320
+ spawn "RAILS_ENV=production DISABLE_SPRING=true rake assets:precompile"
321
+
322
+ assets = Dir.glob(File.join(@rails_root, 'public/assets/**/**'))
323
+
324
+ log(:info, "precompiled #{ assets.size } assets")
325
+
326
+ ensure_non_digested_assets_also_exist!(assets)
327
+ end
328
+
329
+ #
330
+ def rsync_public!
331
+ commands = [
332
+ "rsync -avz ./public/ #{ @directory }",
333
+ "cp -ru ./public/ #{ @directory }",
334
+ proc{ FileUtils.cp_r('./public', @directory) }
335
+ ]
336
+
337
+ rsynced = false
338
+
339
+ commands.each do |command|
340
+ begin
341
+ spawn(command)
342
+ rsynced = true
343
+ break
344
+ rescue
345
+ next
346
+ end
347
+ end
348
+
349
+ unless rsynced
350
+ abort "failed to rsync ./public to `#{ @directory }`"
351
+ end
352
+
353
+ count = 0
354
+ Dir.glob(File.join(@directory, '**/**')).each{ count += 1 }
263
355
 
264
- start_server, stop_server = nil
356
+ log(:info, "rsync'd #{ count } files")
265
357
 
266
- ports.each do |port|
267
- next unless port_open?(port)
358
+ if @asset_tmp
359
+ FileUtils.rm_rf(@asset_dir)
360
+ FileUtils.mv(@asset_tmp, @asset_dir)
361
+ end
362
+ end
268
363
 
269
- # TODO - get puma working...
364
+ #
270
365
  =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"
366
+ def parallel_build!
367
+ size = @parallel
368
+
369
+ stats = {
370
+ :success => [],
371
+ :missing => [],
372
+ :failure => [],
373
+ }
374
+
375
+ times = []
376
+
377
+ elapsed = timing do
378
+ ThreadPool.new(size:) do |tp|
379
+ tp.run do
380
+ @urls.each{|url| tp.process!(url:)}
381
+ end
382
+
383
+ tp.process do |url:|
384
+ uri = uri_for(url)
385
+ path = path_for(uri)
386
+
387
+ upath = uri.path
388
+ rpath = relative_path(path, :from => @directory)
389
+
390
+ code = nil
391
+ body = nil
392
+
393
+ time =
394
+ timing do
395
+ code, body = http_get(uri)
396
+ write_path(path, body) if code == 200
397
+ end
398
+
399
+ tp.success!(url:, rpath:, time:, code:)
400
+ end
401
+
402
+ tp.success do |url:, rpath:, time:, code:|
403
+ times.push(time)
404
+ rps = (times.size / times.sum).round(4)
405
+ msg = "#{ url } -> /#{ rpath } (time:#{ time }, rps:#{ rps }, code:#{ code })"
406
+
407
+ case code
408
+ when 200
409
+ log(:info, msg)
410
+ stats[:success].push(url)
411
+ when 404
412
+ log(:error, msg)
413
+ stats[:missing].push(url)
414
+ else
415
+ log(:error, msg)
416
+ stats[:failure].push(url)
417
+ end
418
+ end
419
+ end
420
+ end
421
+
422
+ borked = 0
423
+
424
+ if stats[:missing].size > 0
425
+ borked += stats[:missing].size
426
+ log(:error, "missing on #{ stats[:missing].size } url(s)")
427
+ end
428
+
429
+ if stats[:failure].size > 0
430
+ borked += stats[:failure].size
431
+ log(:error, "failure on #{ stats[:failure].size } url(s)")
432
+ end
433
+
434
+ if borked > 0
435
+ exit(borked)
436
+ end
437
+
438
+ rps = (times.size / times.sum).round(4)
439
+
440
+ log(:info, "downloaded #{ @urls.size } urls at ~#{ rps } rps")
441
+
442
+ @urls
443
+ end
275
444
  =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
445
+
446
+ #
447
+ def parallel_build!
448
+ size = @parallel
449
+
450
+ stats = {
451
+ :success => [],
452
+ :missing => [],
453
+ :failure => [],
454
+ }
455
+
456
+ times = []
457
+
458
+ Parallel.each(@urls, in_threads: 8) do |url|
459
+ uri = uri_for(url)
460
+ path = path_for(uri)
461
+
462
+ rpath = relative_path(path, :from => @directory)
463
+
464
+ code = nil
465
+ body = nil
466
+
467
+ time =
468
+ timing do
469
+ code, body = http_get(uri)
470
+ write_path(path, body) if code == 200
471
+ end
472
+
473
+ times.push(time)
474
+ ts = times.dup
475
+ rps = (ts.size / ts.sum).round(4)
476
+ msg = "#{ url } -> /#{ rpath } (time:#{ time }, rps:#{ rps }, code:#{ code })"
477
+
478
+ case code
479
+ when 200
480
+ log(:info, msg)
481
+ stats[:success].push(url)
482
+ when 404
483
+ log(:error, msg)
484
+ stats[:missing].push(url)
485
+ else
486
+ log(:error, msg)
487
+ stats[:failure].push(url)
488
+ end
489
+ end
490
+
491
+ borked = 0
492
+
493
+ if stats[:missing].size > 0
494
+ borked += stats[:missing].size
495
+ log(:error, "missing on #{ stats[:missing].size } url(s)")
496
+ end
497
+
498
+ if stats[:failure].size > 0
499
+ borked += stats[:failure].size
500
+ log(:error, "failure on #{ stats[:failure].size } url(s)")
501
+ end
502
+
503
+ if borked > 0
504
+ exit(borked)
505
+ end
506
+
507
+ rps = (times.size / times.sum).round(4)
508
+
509
+ log(:info, "downloaded #{ @urls.size } urls at ~#{ rps } rps")
510
+
511
+ @urls
512
+ end
513
+
514
+ #
515
+ def finalize!
516
+ @finished_at = Time.now
517
+
518
+ elapsed = (@finished_at.to_f - @started_at.to_f)
519
+
520
+ log(:info, "build time - #{ hms(elapsed) }")
521
+
522
+ # because netlify refuses to deploy from a symlink!
523
+ on_netlify = ENV['DEPLOY_PRIME_URL'].to_s =~ /netlify/
524
+
525
+ cp = on_netlify ? 'cp_r' : 'ln_s'
526
+
527
+ build = File.join(@rails_root, 'build')
528
+
529
+ FileUtils.rm_rf(build)
530
+ FileUtils.send(cp, @directory, build)
531
+
532
+ #log(:info, "to preview run: ruby -run -ehttpd ./build/ -p4242")
533
+ end
534
+
535
+ def timing(&block)
536
+ t = Time.now.to_f
537
+
538
+ block.call
539
+
540
+ (Time.now.to_f - t).round(2)
541
+ end
542
+
543
+ def http_get(url)
544
+ uri = URI.parse(url.to_s)
545
+
546
+ response =
547
+ begin
548
+ Net::HTTP.get_response(uri)
549
+ rescue
550
+ [code = 500, body = '']
551
+ end
552
+
553
+ if response.is_a?(Net::HTTPRedirection)
554
+ location = response['Location']
555
+
556
+ if location.to_s == url.to_s
557
+ log(:fatal, "circular redirection on #{ url }")
558
+ exit(1)
559
+ end
560
+
561
+ return http_get(location)
562
+ end
563
+
564
+ code = response.code.to_i rescue 500
565
+ body = response.body.to_s rescue ''
566
+
567
+ [code, body]
568
+ end
569
+
570
+ #
571
+ def to_s
572
+ @directory.to_s
573
+ end
574
+
575
+ #
576
+ def log(level, *args, &block)
577
+ @logger.send(level, *args, &block)
578
+ end
579
+
580
+ #
581
+ def path_for(url)
582
+ uri = uri_for(url)
583
+ path = nil
584
+
585
+ case
586
+ when uri.path=='/' || uri.path=='.'
587
+ path = File.join(@directory, 'index.html')
588
+
589
+ else
590
+ path = File.join(@directory, uri.path)
591
+
592
+ dirname, basename = File.split(path)
593
+ base, ext = basename.split('.', 2)
594
+
595
+ case
596
+ when uri.path.end_with?('/')
597
+ path =
598
+ File.join(path, 'index.html')
599
+
600
+ when ext.nil?
601
+ path =
602
+ if @config.fetch('index_html')
603
+ File.join(path, 'index.html')
604
+ else
605
+ path + '.html'
606
+ end
607
+ end
608
+ end
609
+
610
+ path
611
+ end
612
+
613
+ #
614
+ def write_path(path, body)
615
+ FileUtils.mkdir_p(File.dirname(path))
616
+ IO.binwrite(path, body)
617
+ end
618
+
619
+ #
620
+ def ensure_non_digested_assets_also_exist!(assets)
621
+ re = /(-{1}[a-z0-9]{32}*\.{1}){1}/
622
+
623
+ assets.each do |file|
624
+ next if File.directory?(file) || file !~ re
625
+ source = file.split('/')
626
+ source.push(source.pop.gsub(re, '.'))
627
+ non_digested = File.join(source)
628
+ #log(:debug, "asset: #{ file } -> #{ non_digested }")
629
+ FileUtils.ln(file, non_digested)
630
+ end
631
+ end
632
+
633
+ #
634
+ def url_for(url)
635
+ uri = URI.parse(url.to_s)
636
+
637
+ if uri.absolute?
638
+ uri.path = path
639
+ uri.to_s
640
+ else
641
+ rel = @url ? URI.parse(@url) : URI.parse('')
642
+ rel.path = absolute_path_for(uri.path)
643
+ rel.query = uri.query
644
+ rel.fragment = uri.fragment
645
+ rel.to_s
646
+ end
647
+ end
648
+
649
+ #
650
+ def uri_for(url)
651
+ uri = url.is_a?(URI) ? url : URI.parse(url.to_s)
652
+ end
653
+
654
+ #
655
+ def hms(seconds)
656
+ return unless seconds
657
+ "%02d:%02d:%02d" % hours_minutes_seconds(seconds)
658
+ end
659
+
660
+ #
661
+ def hours_minutes_seconds(seconds)
662
+ return unless seconds
663
+ seconds = Float(seconds).to_i
664
+ hours, seconds = seconds.divmod(3600)
665
+ minutes, seconds = seconds.divmod(60)
666
+ [hours.to_i, minutes.to_s, seconds]
667
+ end
668
+
669
+ #
670
+ def port_open?(port, options = {})
671
+ seconds = options[:timeout] || 1
672
+ ip = options[:ip] || '0.0.0.0'
673
+
674
+ Timeout::timeout(seconds) do
675
+ begin
676
+ TCPSocket.new(ip, port).close
677
+ false
678
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
679
+ true
680
+ rescue Object
681
+ false
682
+ end
683
+ end
684
+ rescue Timeout::Error
685
+ false
686
+ end
687
+
688
+ #
689
+ def paths_for(*args)
690
+ path = args.flatten.compact.join('/')
691
+ path.gsub!(%r|[.]+/|, '/')
692
+ path.squeeze!('/')
693
+ path.sub!(%r|^/|, '')
694
+ path.sub!(%r|/$|, '')
695
+ paths = path.split('/')
696
+ end
697
+
698
+ #
699
+ def absolute_path_for(*args)
700
+ trailing_slash = args.join.end_with?('/') ? '/' : ''
701
+ path = ('/' + paths_for(*args).join('/') + trailing_slash).squeeze('/')
702
+ path unless path.strip.empty?
703
+ end
704
+
705
+ #
706
+ def relative_path_for(*args)
707
+ path = absolute_path_for(*args).sub(%r{^/+}, '')
708
+ path unless path.strip.empty?
709
+ end
710
+
711
+ #
712
+ def relative_path(path, *args)
713
+ options = args.last.is_a?(Hash) ? args.pop : {}
714
+ path = path.to_s
715
+ relative = args.shift || options[:relative] || options[:to] || options[:from]
716
+ if relative
717
+ Pathname.new(path).relative_path_from(Pathname.new(relative.to_s)).to_s
718
+ else
719
+ relative_path_for(path)
720
+ end
721
+ end
722
+
723
+ #
724
+ def spawn(arg, *args, **kws)
725
+ command = [arg, *args]
726
+
727
+ env = kws.fetch(:env){ {} }
728
+ error = kws.fetch(:error){ true }
729
+ quiet = kws.fetch(:quiet){ false }
730
+ stdin = kws.fetch(:stdin){ '' }
731
+
732
+ env.transform_keys!(&:to_s)
733
+ env.transform_values!(&:to_s)
734
+
735
+ pid = nil
736
+ status = nil
737
+ stdout = nil
738
+ stderr = nil
739
+
740
+ Tempfile.open do |i|
741
+ i.write(stdin)
742
+ i.flush
743
+
744
+ Tempfile.open do |o|
745
+ Tempfile.open do |e|
746
+ redirects = {:in => i.path, :out => o.path, :err => e.path}
747
+
748
+ pid = Process.spawn(env, *command, redirects)
749
+
750
+ Process.wait(pid)
751
+
752
+ status = $?.exitstatus
753
+
754
+ stdout = IO.binread(o.path)
755
+ stderr = IO.binread(e.path)
756
+ end
757
+ end
758
+ end
759
+
760
+ unless status == 0
761
+ unless kws[:quiet] == true
762
+ log(:error, "#{ command.join(' ') } ###===>>> #{ status }\nSTDOUT:\n#{ stdout }\n\STDERR:\n#{ stderr }")
763
+ exit(status)
764
+ end
765
+ end
766
+
767
+ {command:, pid:, env:, status:, stdin:, stdout:, stderr:}
768
+ end
769
+ end
770
+
771
+ #
772
+ class Server
773
+ attr_reader :pid
774
+
775
+ def initialize(cli:)
776
+ @cli = cli
777
+
778
+ @env = @cli.env
779
+ @directory = @cli.directory
780
+ @rails_root = @cli.rails_root
781
+ @parallel = @cli.parallel
782
+ @uuid = @cli.uuid
783
+
784
+ @thread = nil
785
+ @pid = nil
786
+ end
787
+
788
+ def start!(port:)
789
+ system("#{ version_command } >/dev/null 2>&1") ||
790
+ abort("app fails to load via: #{ version_command }")
791
+
792
+ q = Queue.new
793
+
794
+ cmd = start_command_for(port)
795
+
796
+ log = './tmp/rails_build_server.log'
797
+
798
+ @cli.log(:info, "server: #{ cmd } > #{ log } 2>&1")
799
+
800
+ @thread = Thread.new do
801
+ Thread.current.abort_on_exception = true
802
+ pipe = IO.popen("#{ cmd } > #{ log } 2>&1")
803
+ q.push(pipe.pid)
804
+ end
805
+
806
+ @pid = q.pop
807
+
808
+ @cli.log(:info, "pid: #{ @pid }")
809
+
810
+ @assassin = Assassin.ate(@pid)
811
+
812
+ at_exit{ stop! }
813
+ end
814
+
815
+ def version_command
816
+ cmd_for(
817
+ %W[
818
+ RAILS_ENV=#{ @env }
819
+ DISABLE_SPRING=true
820
+
821
+ rails --version
822
+ ]
823
+ )
824
+ end
825
+
826
+ def start_command_for(port)
827
+ cmd_for(
828
+ %W[
829
+ RAILS_ENV=#{ @env }
830
+ DISABLE_SPRING=true
831
+
832
+ RAILS_BUILD=#{ @uuid }
833
+
834
+ RAILS_SERVE_STATIC_FILES=true
835
+ RAILS_LOG_TO_STDOUT=true
836
+ WEB_CONCURRENCY=#{ @parallel.to_s }
837
+
838
+ rails server
839
+
840
+ --environment=#{ @env }
841
+ --port=#{ port }
842
+ --binding=0.0.0.0
843
+ --log-to-stdout
844
+ ]
845
+ )
846
+ end
847
+
848
+ def cmd_for(arg, *args)
849
+ [arg, *args].flatten.compact.join(' ').squeeze(' ').strip
850
+ end
851
+
852
+ def stop!
853
+ kill!(@pid)
854
+ @thread.kill
855
+ @cli.log(:info, "stopped: #{ @pid }")
856
+ end
857
+
858
+ def kill!(pid)
859
+ 42.times do
860
+ begin
861
+ Process.kill(0, pid)
862
+ return(true)
863
+ rescue Object => e
864
+ if e.is_a?(Errno::ESRCH)
865
+ Process.kill(-15, pid) rescue nil
866
+ sleep(rand + rand)
867
+ Process.kill(-9, pid) rescue nil
868
+ end
869
+ end
870
+ sleep(0.42 + rand)
871
+ end
872
+ return(false)
873
+ end
874
+ end
875
+ end
876
+
877
+ END {
878
+ require_relative '../lib/rails_build.rb'
879
+
880
+ STDOUT.sync = true
881
+ STDERR.sync = true
882
+
883
+ RailsBuild::CLI.run!
884
+ }
885
+
886
+ __END__
887
+ <<~________
888
+
889
+ this file should to enumerate all the urls you'd like to build
890
+
891
+ the contents of your ./public directory, and any assets, are automaticaly included
892
+
893
+ therefore you need only declare which dynamic urls, that is to say, 'routes'
894
+
895
+ you would like included in your build
896
+
897
+ it is not loaded except during build time, and will not affect your normal rails app in any way
898
+
899
+ ________
900
+
901
+
902
+ RailsBuild.configure do |config|
903
+
904
+ # most of the time you are going to want your route included, which will
905
+ # translate into an ./index.html being output in the build
906
+ #
907
+
908
+ config.urls << '/'
909
+
910
+ # include any/all additional routes youd' like built thusly
911
+ #
912
+
913
+ Post.each do |post|
914
+ config.urls << "/posts/#{ post.id }"
915
+ end
916
+
917
+ # thats it! - now just run `rails_build` and you are GTG
918
+
730
919
  end