slug-compiler 2.0.0

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