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