slug-compiler 2.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.
- 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: []
|