anvil-cli 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/bin/anvil ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.expand_path("../../lib", __FILE__)
4
+
5
+ require "anvil/cli"
6
+
7
+ Anvil::CLI.start
@@ -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
@@ -0,0 +1,3 @@
1
+ module Anvil
2
+ VERSION = "0.0.2"
3
+ end
data/lib/anvil.rb ADDED
@@ -0,0 +1,2 @@
1
+ module Anvil
2
+ end
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: []