slug-compiler 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/COPYING +20 -0
- data/Readme.md +49 -0
- data/bin/slug-compiler +27 -0
- data/lib/slug_compiler.rb +243 -0
- data/slug-compiler.gemspec +18 -0
- metadata +69 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
/slug-compiler-*.gem
|
data/COPYING
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright © 2013 Heroku
|
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 NONINFRINGEMENT.
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
18
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
19
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
20
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Readme.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Slug compiler
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
A slug is a unit of deployment which includes everything an
|
6
|
+
application needs to run. The slug compiler is a program which uses a
|
7
|
+
buildpack to transform application source into a slug, typically by
|
8
|
+
gathering all dependencies and compiling source to binaries, if
|
9
|
+
applicable.
|
10
|
+
|
11
|
+
As inputs it takes a source directory, cache directory, buildpack URL,
|
12
|
+
and output directory. It places a tarball of the slug as well as a
|
13
|
+
JSON file of process types in the output directory if compilation is
|
14
|
+
successful.
|
15
|
+
|
16
|
+
Note that this is a trimmed down version of the slug compiler that
|
17
|
+
currently runs in production on Heroku. It's intended for next-gen
|
18
|
+
services but is not currently in production use.
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
In the typical git-based Heroku deploy, the slug compiler is invoked
|
23
|
+
via a Git pre-recieve hook. However, this code has been extracted so
|
24
|
+
that the same process can be used elsewhere.
|
25
|
+
|
26
|
+
Note that it will delete .slugignore patterns and other files, so you
|
27
|
+
shouldn't run this straight out of a checkout. Create a fresh copy of
|
28
|
+
the application source before compiling it.
|
29
|
+
|
30
|
+
## Requirements
|
31
|
+
|
32
|
+
* Ruby 1.9
|
33
|
+
* tar
|
34
|
+
* du
|
35
|
+
* git (optional, used for fetching non-HTTP buildpacks)
|
36
|
+
|
37
|
+
## Responsibilities
|
38
|
+
|
39
|
+
The new slug compiler does much less than the old one. In particular,
|
40
|
+
these operations are now the responsibility of the caller:
|
41
|
+
|
42
|
+
* checking size of the slug
|
43
|
+
* stack migration
|
44
|
+
* detecting among set of default buildpacks
|
45
|
+
* git submodule handling
|
46
|
+
* special-cased bamboo operations
|
47
|
+
* calling releases API
|
48
|
+
* storing slug file in S3
|
49
|
+
* deploy hooks
|
data/bin/slug-compiler
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "slug_compiler"
|
4
|
+
|
5
|
+
build_dir, buildpack_url, cache_dir, output_dir = ARGV
|
6
|
+
abort "USAGE: #{$0} BUILD_DIR BUILDPACK_DIR CACHE_DIR OUTPUT_DIR" unless ARGV.size == 4 and
|
7
|
+
File.exists?(build_dir) and File.exists?(cache_dir) and
|
8
|
+
URI.parse(buildpack_url) rescue false and File.directory?(output_dir)
|
9
|
+
|
10
|
+
begin
|
11
|
+
start = Time.now
|
12
|
+
SlugCompiler.run(*ARGV)
|
13
|
+
# TODO: unify this with existing logging
|
14
|
+
rescue SlugCompiler::CompileFail => e
|
15
|
+
puts(" ! Heroku push rejected, #{e.message}\n")
|
16
|
+
rescue SlugCompiler::CompileError => e
|
17
|
+
puts(" ! Heroku push rejected, #{e.message}\n")
|
18
|
+
SlugCompiler.log("measure=slugc.fail elapsed=#{Time.now - start} " \
|
19
|
+
"message='#{e.message}'")
|
20
|
+
rescue => e
|
21
|
+
puts
|
22
|
+
puts(" ! Heroku push rejected due to an unrecognized error.")
|
23
|
+
puts(" ! We've been notified, see http://support.heroku.com if the problem persists.")
|
24
|
+
puts("\n")
|
25
|
+
SlugCompiler.log_error("measure=slugc.error elapsed=#{Time.now - start}", e)
|
26
|
+
raise if ENV["DEBUG"]
|
27
|
+
end
|
@@ -0,0 +1,243 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "find"
|
3
|
+
require "json"
|
4
|
+
require "open-uri"
|
5
|
+
require "shellwords"
|
6
|
+
require "syslog"
|
7
|
+
require "timeout"
|
8
|
+
require "uri"
|
9
|
+
|
10
|
+
$stdout.sync = true
|
11
|
+
Syslog.open("slug-compiler", Syslog::LOG_CONS, Syslog::LOG_USER)
|
12
|
+
|
13
|
+
module SlugCompiler
|
14
|
+
class CompileError < RuntimeError; end
|
15
|
+
class CompileFail < RuntimeError; end
|
16
|
+
|
17
|
+
module_function
|
18
|
+
|
19
|
+
def run(build_dir, buildpack_url, cache_dir, output_dir)
|
20
|
+
@compile_id = rand(2**64).to_s(36) # because UUIDs need a gem
|
21
|
+
|
22
|
+
buildpack_dir = fetch_buildpack(buildpack_url)
|
23
|
+
config = buildpack_config(buildpack_url)
|
24
|
+
buildpack_name = detect(build_dir, buildpack_dir)
|
25
|
+
compile(build_dir, buildpack_dir, cache_dir, config)
|
26
|
+
|
27
|
+
prune(build_dir)
|
28
|
+
process_types = parse_procfile(build_dir, output_dir)
|
29
|
+
slug = archive(build_dir, output_dir)
|
30
|
+
log_size(build_dir, cache_dir, slug)
|
31
|
+
|
32
|
+
[slug, process_types]
|
33
|
+
ensure
|
34
|
+
FileUtils.rm_rf(buildpack_dir) if buildpack_dir
|
35
|
+
end
|
36
|
+
|
37
|
+
def fetch_buildpack(buildpack_url)
|
38
|
+
buildpack_dir = "/tmp/buildpack_#{@compile_id}"
|
39
|
+
|
40
|
+
log("fetch_buildpack") do
|
41
|
+
Timeout.timeout((ENV["BUILDPACK_FETCH_TIMEOUT"] || 90).to_i) do
|
42
|
+
FileUtils.mkdir_p(buildpack_dir)
|
43
|
+
if buildpack_url =~ /^https?:\/\/.*\.(tgz|tar\.gz)($|\?)/
|
44
|
+
print("-----> Fetching buildpack... ")
|
45
|
+
retrying(3) do
|
46
|
+
IO.popen("tar xz -C #{buildpack_dir}", "w") do |tar|
|
47
|
+
IO.copy_stream(open(buildpack_url), tar)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
elsif File.directory?(buildpack_url)
|
51
|
+
print("-----> Copying buildpack... ")
|
52
|
+
FileUtils.cp_r(buildpack_url + "/.", buildpack_dir)
|
53
|
+
else
|
54
|
+
print("-----> Cloning buildpack... ")
|
55
|
+
url, sha = buildpack_url.split("#")
|
56
|
+
clear_var("GIT_DIR") do
|
57
|
+
system("git", "clone", Shellwords.escape(url), buildpack_dir,
|
58
|
+
[:out, :err] => "/dev/null") # or raise("Couldn't clone")
|
59
|
+
system("git", "checkout", Shellwords.escape(treeish),
|
60
|
+
[:out, :err] => "/dev/null", :chdir => buildpack_dir) if sha
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
FileUtils.chmod_R(0755, File.join(buildpack_dir, "bin"))
|
66
|
+
|
67
|
+
puts("done")
|
68
|
+
end
|
69
|
+
|
70
|
+
buildpack_dir
|
71
|
+
rescue StandardError, Timeout::Error => e
|
72
|
+
puts("failed")
|
73
|
+
log_error("error fetching buildpack", e)
|
74
|
+
raise(CompileError, "error fetching buildpack")
|
75
|
+
end
|
76
|
+
|
77
|
+
def buildpack_config(buildpack_url)
|
78
|
+
config = {}
|
79
|
+
if query = (buildpack_url && URI.parse(buildpack_url).query)
|
80
|
+
query.split("&").each do |kv|
|
81
|
+
next if kv.empty?
|
82
|
+
k, v = kv.split("=", 2)
|
83
|
+
config[k] = URI.unescape(v || "")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
config
|
87
|
+
end
|
88
|
+
|
89
|
+
def detect(build_dir, buildpack_dir)
|
90
|
+
name = `#{File.join(buildpack_dir, "bin", "detect")} #{build_dir}`.strip
|
91
|
+
puts("-----> #{name} app detected")
|
92
|
+
return name
|
93
|
+
rescue
|
94
|
+
raise(CompileFail, "no compatible app detected")
|
95
|
+
end
|
96
|
+
|
97
|
+
def compile(build_dir, buildpack_dir, cache_dir, config)
|
98
|
+
bin_compile = File.join(buildpack_dir, 'bin', 'compile')
|
99
|
+
timeout = (ENV["COMPILE_TIMEOUT"] || 900).to_i
|
100
|
+
Timeout.timeout(timeout) do
|
101
|
+
pid = spawn(config, bin_compile, build_dir, cache_dir,
|
102
|
+
unsetenv_others: true, err: :out)
|
103
|
+
Process.wait(pid)
|
104
|
+
raise(CompileFail, "build failed") unless $?.exitstatus.zero?
|
105
|
+
end
|
106
|
+
rescue Timeout::Error
|
107
|
+
raise(CompileFail, "timed out; must complete in #{timeout} seconds")
|
108
|
+
end
|
109
|
+
|
110
|
+
def prune(build_dir)
|
111
|
+
FileUtils.rm_rf(File.join(build_dir, ".git"))
|
112
|
+
FileUtils.rm_rf(File.join(build_dir, "tmp"))
|
113
|
+
|
114
|
+
Find.find(build_dir) do |path|
|
115
|
+
File.delete(path) if File.basename(path) == ".DS_Store"
|
116
|
+
end
|
117
|
+
|
118
|
+
prune_slugignore(build_dir)
|
119
|
+
end
|
120
|
+
|
121
|
+
def prune_slugignore(build_dir)
|
122
|
+
# general pattern format follows .gitignore:
|
123
|
+
# http://www.kernel.org/pub/software/scm/git/docs/gitignore.html
|
124
|
+
# blank => nothing; leading # => comment
|
125
|
+
# everything else is more or less a shell glob
|
126
|
+
slugignore_path = File.join(build_dir, ".slugignore")
|
127
|
+
return if !File.exists?(slugignore_path)
|
128
|
+
|
129
|
+
log("process_slugignore") do
|
130
|
+
lines = File.read(slugignore_path).split
|
131
|
+
total = lines.inject(0) do |total, line|
|
132
|
+
line = (line.split(/#/).first || "").strip
|
133
|
+
if line.empty?
|
134
|
+
total
|
135
|
+
else
|
136
|
+
globs = if line =~ /\//
|
137
|
+
[File.join(build_dir, line)]
|
138
|
+
else
|
139
|
+
# 1.8.7 and 1.9.2 handle expanding ** differently,
|
140
|
+
# where in 1.9.2 ** doesn't match the empty case. So
|
141
|
+
# try empty ** explicitly
|
142
|
+
["", "**"].map { |g| File.join(build_dir, g, line) }
|
143
|
+
end
|
144
|
+
|
145
|
+
to_delete = Dir[*globs].uniq.map { |p| File.expand_path(p) }
|
146
|
+
to_delete = to_delete.select { |p| p.match(/^#{build_dir}/) }
|
147
|
+
to_delete.each { |p| FileUtils.rm_rf(p) }
|
148
|
+
total + to_delete.size
|
149
|
+
end
|
150
|
+
end
|
151
|
+
puts("-----> Deleting #{total} files matching .slugignore patterns.")
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def parse_procfile(build_dir, output_dir)
|
156
|
+
path = File.join(build_dir, "Procfile")
|
157
|
+
return unless File.exists?(path)
|
158
|
+
|
159
|
+
process_types = File.read(path).split("\n").inject({}) do |ps, line|
|
160
|
+
if m = line.match(/^([a-zA-Z0-9_]+):?\s+(.*)/)
|
161
|
+
ps[m[1]] = m[2]
|
162
|
+
end
|
163
|
+
ps
|
164
|
+
end
|
165
|
+
|
166
|
+
FileUtils.mkdir_p(output_dir)
|
167
|
+
File.write(File.join(output_dir, "processes.json"),
|
168
|
+
JSON.unparse(process_types))
|
169
|
+
process_types
|
170
|
+
end
|
171
|
+
|
172
|
+
def archive(build_dir, output_dir)
|
173
|
+
FileUtils.mkdir_p(output_dir)
|
174
|
+
slug = File.join(output_dir, "slug.tgz")
|
175
|
+
log("create_tar_slug") do
|
176
|
+
system("tar", "czf", slug, "--xform", "s,^./,./app/,", "--owner=root",
|
177
|
+
"--hard-dereference", "-C", build_dir, ".",
|
178
|
+
[:out, :err] => "/dev/null") or raise("couldn't tar")
|
179
|
+
end
|
180
|
+
return slug
|
181
|
+
rescue => e
|
182
|
+
log_error("could not archive slug", e)
|
183
|
+
raise(CompileError, "could not archive slug")
|
184
|
+
end
|
185
|
+
|
186
|
+
def log_size(build_dir, cache_dir, slug)
|
187
|
+
log("check_sizes") do
|
188
|
+
raw_size = size_of_dir(build_dir)
|
189
|
+
cache_size = size_of_dir(cache_dir) if File.exists? cache_dir
|
190
|
+
slug_size = File.size(slug)
|
191
|
+
log("check_sizes raw=#{raw_size} slug=#{slug_size} cache=#{cache_size}")
|
192
|
+
puts(sprintf("-----> Compiled slug size: %1.0fK", slug_size / 1024))
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# utils
|
197
|
+
|
198
|
+
def retrying(n)
|
199
|
+
begin
|
200
|
+
yield
|
201
|
+
rescue => e
|
202
|
+
n = n - 1
|
203
|
+
log_error("retrying", e) && retry if n > 0
|
204
|
+
raise e
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def clear_var(k)
|
209
|
+
v = ENV.delete(k)
|
210
|
+
begin
|
211
|
+
yield
|
212
|
+
ensure
|
213
|
+
ENV[k] = v
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def log(msg, &block)
|
218
|
+
if block
|
219
|
+
start = Time.now
|
220
|
+
res = nil
|
221
|
+
log("slugc #{msg} at=start")
|
222
|
+
begin
|
223
|
+
res = yield
|
224
|
+
rescue => e
|
225
|
+
log_error("#{msg} elapsed=#{Time.now - start}", e)
|
226
|
+
raise(e)
|
227
|
+
end
|
228
|
+
log("slugc #{msg} at=finish elapsed=#{Time.now - start}")
|
229
|
+
res
|
230
|
+
else
|
231
|
+
Syslog.log(Syslog::LOG_INFO, msg)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def log_error(line, e)
|
236
|
+
Syslog.log(Syslog::LOG_ERR, "slugc #{line} at=error " \
|
237
|
+
"class='#{e.class}' message='#{e.message}'")
|
238
|
+
end
|
239
|
+
|
240
|
+
def size_of_dir(dir)
|
241
|
+
`du -s -x #{dir}`.split(" ").first.to_i*1024
|
242
|
+
end
|
243
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.authors = ["Noah Zoschke", "Phil Hagelberg"]
|
5
|
+
gem.email = ["phil.hagelberg@heroku.com"]
|
6
|
+
gem.description = %q{Turn application source into deployable slugs}
|
7
|
+
gem.summary = %q{Turn application source into deployable slugs}
|
8
|
+
gem.homepage = "https://github.com/heroku/slug-compiler"
|
9
|
+
|
10
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
11
|
+
gem.files = `git ls-files`.split("\n")
|
12
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
13
|
+
gem.name = "slug-compiler"
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
gem.version = "2.0.0"
|
16
|
+
|
17
|
+
gem.add_development_dependency "rake"
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: slug-compiler
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Noah Zoschke
|
9
|
+
- Phil Hagelberg
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2013-02-27 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rake
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
description: Turn application source into deployable slugs
|
32
|
+
email:
|
33
|
+
- phil.hagelberg@heroku.com
|
34
|
+
executables:
|
35
|
+
- slug-compiler
|
36
|
+
extensions: []
|
37
|
+
extra_rdoc_files: []
|
38
|
+
files:
|
39
|
+
- .gitignore
|
40
|
+
- COPYING
|
41
|
+
- Readme.md
|
42
|
+
- bin/slug-compiler
|
43
|
+
- lib/slug_compiler.rb
|
44
|
+
- slug-compiler.gemspec
|
45
|
+
homepage: https://github.com/heroku/slug-compiler
|
46
|
+
licenses: []
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ! '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
requirements: []
|
64
|
+
rubyforge_project:
|
65
|
+
rubygems_version: 1.8.23
|
66
|
+
signing_key:
|
67
|
+
specification_version: 3
|
68
|
+
summary: Turn application source into deployable slugs
|
69
|
+
test_files: []
|