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.
@@ -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: []