rails_build 1.0.0 → 2.4.2

Sign up to get free protection for your applications and to get access to all the features.
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