anvil-cli 0.0.2
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/bin/anvil +7 -0
- data/lib/anvil/builder.rb +70 -0
- data/lib/anvil/cli.rb +69 -0
- data/lib/anvil/helpers.rb +30 -0
- data/lib/anvil/manifest.rb +161 -0
- data/lib/anvil/version.rb +3 -0
- data/lib/anvil.rb +2 -0
- metadata +74 -0
data/bin/anvil
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require "anvil"
|
2
|
+
require "anvil/helpers"
|
3
|
+
require "json"
|
4
|
+
require "net/http"
|
5
|
+
require "net/https"
|
6
|
+
require "rest_client"
|
7
|
+
|
8
|
+
class Anvil::Builder
|
9
|
+
|
10
|
+
include Anvil::Helpers
|
11
|
+
|
12
|
+
class BuildError < StandardError; end
|
13
|
+
|
14
|
+
attr_reader :source
|
15
|
+
|
16
|
+
def initialize(source)
|
17
|
+
@source = source
|
18
|
+
end
|
19
|
+
|
20
|
+
def build(options={})
|
21
|
+
uri = URI.parse("#{anvil_host}/build")
|
22
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
23
|
+
|
24
|
+
if uri.scheme == "https"
|
25
|
+
http.use_ssl = true
|
26
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
27
|
+
end
|
28
|
+
|
29
|
+
req = Net::HTTP::Post.new uri.request_uri
|
30
|
+
|
31
|
+
req.set_form_data({
|
32
|
+
"buildpack" => options[:buildpack],
|
33
|
+
"cache" => options[:cache],
|
34
|
+
"env" => json_encode(options[:env] || {}),
|
35
|
+
"source" => source
|
36
|
+
})
|
37
|
+
|
38
|
+
slug_url = nil
|
39
|
+
|
40
|
+
http.request(req) do |res|
|
41
|
+
slug_url = res["x-slug-url"]
|
42
|
+
|
43
|
+
begin
|
44
|
+
res.read_body do |chunk|
|
45
|
+
yield chunk
|
46
|
+
end
|
47
|
+
rescue EOFError
|
48
|
+
puts
|
49
|
+
raise BuildError, "terminated unexpectedly"
|
50
|
+
end
|
51
|
+
|
52
|
+
manifest_id = [res["x-manifest-id"]].flatten.first
|
53
|
+
code = Integer(String.new(anvil["/exit/#{manifest_id}"].get.to_s))
|
54
|
+
raise BuildError, "exited #{code}" unless code.zero?
|
55
|
+
end
|
56
|
+
|
57
|
+
slug_url
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def anvil
|
63
|
+
@anvil ||= RestClient::Resource.new(anvil_host)
|
64
|
+
end
|
65
|
+
|
66
|
+
def anvil_host
|
67
|
+
ENV["ANVIL_HOST"] || "https://api.anvilworks.org"
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
data/lib/anvil/cli.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require "anvil"
|
2
|
+
require "anvil/builder"
|
3
|
+
require "anvil/manifest"
|
4
|
+
require "thor"
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
class Anvil::CLI < Thor
|
8
|
+
|
9
|
+
map ["-v", "--version"] => :version
|
10
|
+
|
11
|
+
desc "build [SOURCE]", "Build an application"
|
12
|
+
|
13
|
+
method_option :buildpack, :type => :string, :aliases => "-b", :desc => "Use a specific buildpack"
|
14
|
+
method_option :pipeline, :type => :boolean, :aliases => "-p", :desc => "Pipe compile output to stderr and put the slug url on stdout"
|
15
|
+
|
16
|
+
def build(source=nil)
|
17
|
+
if options[:pipeline]
|
18
|
+
old_stdout = $stdout.dup
|
19
|
+
$stdout = $stderr
|
20
|
+
end
|
21
|
+
|
22
|
+
source ||= "."
|
23
|
+
|
24
|
+
build_options = {
|
25
|
+
:buildpack => prepare_buildpack(options[:buildpack].to_s)
|
26
|
+
}
|
27
|
+
|
28
|
+
builder = if is_url?(source)
|
29
|
+
Anvil::Builder.new(source)
|
30
|
+
else
|
31
|
+
manifest = Anvil::Manifest.new(File.expand_path(source))
|
32
|
+
print "Uploading app... "
|
33
|
+
count = manifest.upload
|
34
|
+
puts "done, #{count} files uploaded"
|
35
|
+
manifest
|
36
|
+
end
|
37
|
+
|
38
|
+
slug_url = builder.build(build_options) do |chunk|
|
39
|
+
print chunk
|
40
|
+
end
|
41
|
+
|
42
|
+
old_stdout.puts slug_url if options[:pipeline]
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def is_url?(string)
|
48
|
+
URI.parse(string).scheme rescue nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def prepare_buildpack(buildpack)
|
52
|
+
if buildpack == ""
|
53
|
+
buildpack
|
54
|
+
elsif is_url?(buildpack)
|
55
|
+
buildpack
|
56
|
+
elsif buildpack =~ /\A\w+\/\w+\Z/
|
57
|
+
"http://buildkits-dev.s3.amazonaws.com/buildpacks/#{buildpack}.tgz"
|
58
|
+
elsif File.exists?(buildpack) && File.directory?(buildpack)
|
59
|
+
print "Uploading buildpack... "
|
60
|
+
manifest = Anvil::Manifest.new(buildpack)
|
61
|
+
manifest.upload
|
62
|
+
manifest.save
|
63
|
+
puts "done"
|
64
|
+
else
|
65
|
+
error "unrecognized buildpack specification: #{buildpack}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "anvil"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Anvil::Helpers
|
5
|
+
|
6
|
+
def json_encode(obj)
|
7
|
+
JSON.dump(obj)
|
8
|
+
end
|
9
|
+
|
10
|
+
def json_decode(str)
|
11
|
+
JSON.load(str)
|
12
|
+
end
|
13
|
+
|
14
|
+
def anvil_metadata_dir(root)
|
15
|
+
dir = File.join(root, ".anvil")
|
16
|
+
FileUtils.mkdir_p(dir)
|
17
|
+
dir
|
18
|
+
end
|
19
|
+
|
20
|
+
def read_anvil_metadata(root, name)
|
21
|
+
File.open(File.join(anvil_metadata_dir(root), name)).read.chomp rescue nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def write_anvil_metadata(root, name, data)
|
25
|
+
File.open(File.join(anvil_metadata_dir(root), name), "w") do |file|
|
26
|
+
file.puts data
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require "anvil/builder"
|
2
|
+
require "anvil/helpers"
|
3
|
+
require "json"
|
4
|
+
require "net/http"
|
5
|
+
require "net/https"
|
6
|
+
require "rest_client"
|
7
|
+
|
8
|
+
class Anvil::Manifest
|
9
|
+
|
10
|
+
include Anvil::Helpers
|
11
|
+
|
12
|
+
PUSH_THREAD_COUNT = 40
|
13
|
+
|
14
|
+
attr_reader :cache_url
|
15
|
+
attr_reader :dir
|
16
|
+
attr_reader :manifest
|
17
|
+
|
18
|
+
def initialize(dir=nil, cache_url=nil)
|
19
|
+
@dir = dir
|
20
|
+
@manifest = @dir ? directory_manifest(@dir) : {}
|
21
|
+
@cache_url = cache_url
|
22
|
+
end
|
23
|
+
|
24
|
+
def build(options={})
|
25
|
+
uri = URI.parse("#{anvil_host}/manifest/build")
|
26
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
27
|
+
|
28
|
+
if uri.scheme == "https"
|
29
|
+
http.use_ssl = true
|
30
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
31
|
+
end
|
32
|
+
|
33
|
+
req = Net::HTTP::Post.new uri.request_uri
|
34
|
+
|
35
|
+
env = options[:env] || {}
|
36
|
+
|
37
|
+
req.set_form_data({
|
38
|
+
"buildpack" => options[:buildpack],
|
39
|
+
"cache" => @cache_url,
|
40
|
+
"env" => json_encode(options[:env] || {}),
|
41
|
+
"manifest" => self.to_json
|
42
|
+
})
|
43
|
+
|
44
|
+
slug_url = nil
|
45
|
+
|
46
|
+
http.request(req) do |res|
|
47
|
+
slug_url = res["x-slug-url"]
|
48
|
+
@cache_url = res["x-cache-url"]
|
49
|
+
|
50
|
+
begin
|
51
|
+
res.read_body do |chunk|
|
52
|
+
yield chunk
|
53
|
+
end
|
54
|
+
rescue EOFError
|
55
|
+
puts
|
56
|
+
raise Heroku::Builder::BuildError, "terminated unexpectedly"
|
57
|
+
end
|
58
|
+
|
59
|
+
code = if res["x-exit-code"].nil?
|
60
|
+
manifest_id = Array(res["x-manifest-id"]).first
|
61
|
+
Integer(String.new(anvil["/exit/#{manifest_id}"].get.to_s))
|
62
|
+
else
|
63
|
+
res["x-exit-code"].first.to_i
|
64
|
+
end
|
65
|
+
|
66
|
+
raise Heroku::Builder::BuildError, "exited #{code}" unless code.zero?
|
67
|
+
end
|
68
|
+
|
69
|
+
slug_url
|
70
|
+
end
|
71
|
+
|
72
|
+
def save
|
73
|
+
res = anvil["/manifest"].post(:manifest => self.to_json)
|
74
|
+
res.headers[:location]
|
75
|
+
end
|
76
|
+
|
77
|
+
def upload
|
78
|
+
missing = json_decode(anvil["/manifest/diff"].post(:manifest => self.to_json).to_s)
|
79
|
+
upload_hashes missing
|
80
|
+
missing.length
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_json
|
84
|
+
json_encode(@manifest)
|
85
|
+
end
|
86
|
+
|
87
|
+
def add(filename)
|
88
|
+
@manifest[filename] = file_manifest(filename)
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def anvil
|
94
|
+
@anvil ||= RestClient::Resource.new(anvil_host)
|
95
|
+
end
|
96
|
+
|
97
|
+
def anvil_host
|
98
|
+
ENV["ANVIL_HOST"] || "https://api.anvilworks.org"
|
99
|
+
end
|
100
|
+
|
101
|
+
def directory_manifest(dir)
|
102
|
+
root = Pathname.new(dir)
|
103
|
+
|
104
|
+
Dir.glob(File.join(dir, "**", "*"), File::FNM_DOTMATCH).inject({}) do |hash, file|
|
105
|
+
next(hash) if %w( . .. ).include?(File.basename(file))
|
106
|
+
next(hash) if File.directory?(file)
|
107
|
+
next(hash) if File.pipe?(file)
|
108
|
+
next(hash) if file =~ /\.git/
|
109
|
+
next(hash) if file =~ /\.swp$/
|
110
|
+
hash[Pathname.new(file).relative_path_from(root).to_s] = file_manifest(file)
|
111
|
+
hash
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def file_manifest(file)
|
116
|
+
stat = File.stat(file)
|
117
|
+
manifest = {
|
118
|
+
"mtime" => stat.mtime.to_i,
|
119
|
+
"mode" => "%o" % stat.mode,
|
120
|
+
}
|
121
|
+
if File.symlink?(file)
|
122
|
+
manifest["link"] = File.readlink(file)
|
123
|
+
else
|
124
|
+
manifest["hash"] = calculate_hash(file)
|
125
|
+
end
|
126
|
+
manifest
|
127
|
+
end
|
128
|
+
|
129
|
+
def calculate_hash(filename)
|
130
|
+
Digest::SHA2.hexdigest(File.open(filename, "rb").read)
|
131
|
+
end
|
132
|
+
|
133
|
+
def upload_file(filename, hash=nil)
|
134
|
+
hash ||= calculate_hash(filename)
|
135
|
+
anvil["/file/#{hash}"].post :data => File.new(filename, "rb")
|
136
|
+
hash
|
137
|
+
rescue RestClient::Forbidden => ex
|
138
|
+
error "error uploading #{filename}: #{ex.http_body}"
|
139
|
+
end
|
140
|
+
|
141
|
+
def upload_hashes(hashes)
|
142
|
+
filenames_by_hash = @manifest.inject({}) do |ax, (name, file_manifest)|
|
143
|
+
ax.update file_manifest["hash"] => File.join(@dir.to_s, name)
|
144
|
+
end
|
145
|
+
bucket_hashes = hashes.inject({}) do |ax, hash|
|
146
|
+
index = hash.hash % PUSH_THREAD_COUNT
|
147
|
+
ax[index] ||= []
|
148
|
+
ax[index] << hash
|
149
|
+
ax
|
150
|
+
end
|
151
|
+
threads = bucket_hashes.values.map do |hashes|
|
152
|
+
Thread.new do
|
153
|
+
hashes.each do |hash|
|
154
|
+
upload_file filenames_by_hash[hash], hash
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
threads.each(&:join)
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
data/lib/anvil.rb
ADDED
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: anvil-cli
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- David Dollar
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-09 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rest-client
|
16
|
+
requirement: &70156982826160 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.6.7
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70156982826160
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: thor
|
27
|
+
requirement: &70156982825400 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.15.2
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70156982825400
|
36
|
+
description: Alternate Heroku build workflow
|
37
|
+
email: david@dollar.io
|
38
|
+
executables:
|
39
|
+
- anvil
|
40
|
+
extensions: []
|
41
|
+
extra_rdoc_files: []
|
42
|
+
files:
|
43
|
+
- bin/anvil
|
44
|
+
- lib/anvil/builder.rb
|
45
|
+
- lib/anvil/cli.rb
|
46
|
+
- lib/anvil/helpers.rb
|
47
|
+
- lib/anvil/manifest.rb
|
48
|
+
- lib/anvil/version.rb
|
49
|
+
- lib/anvil.rb
|
50
|
+
homepage: http://github.com/ddollar/anvil-cli
|
51
|
+
licenses: []
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ! '>='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubyforge_project:
|
70
|
+
rubygems_version: 1.8.11
|
71
|
+
signing_key:
|
72
|
+
specification_version: 3
|
73
|
+
summary: Alternate Heroku build workflow
|
74
|
+
test_files: []
|