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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +179 -0
- data/Rakefile +37 -0
- data/app/assets/config/rails_build_manifest.js +2 -0
- data/app/assets/javascripts/rails_build/application.js +13 -0
- data/app/assets/stylesheets/rails_build/application.css +32 -0
- data/app/controllers/rails_build/application_controller.rb +12 -0
- data/app/helpers/rails_build/application_helper.rb +4 -0
- data/app/jobs/rails_build/application_job.rb +4 -0
- data/app/mailers/rails_build/application_mailer.rb +6 -0
- data/app/models/rails_build/application_record.rb +5 -0
- data/app/views/layouts/rails_build/application.html.erb +14 -0
- data/app/views/rails_build/application/index.html.erb +20 -0
- data/bin/rails +13 -0
- data/bin/rails_build +730 -0
- data/config/routes.rb +10 -0
- data/lib/rails_build.rb +37 -0
- data/lib/rails_build/engine.rb +50 -0
- data/lib/rails_build/version.rb +3 -0
- data/lib/tasks/rails_build_tasks.rake +31 -0
- metadata +130 -0
checksums.yaml
ADDED
@@ -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
|
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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).
|
data/Rakefile
ADDED
@@ -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,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,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>
|
data/bin/rails
ADDED
@@ -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'
|
data/bin/rails_build
ADDED
@@ -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
|