Skip to content
This repository
tree: ac05ea7ea5
Fetching contributors…

Cannot retrieve contributors at this time

file 214 lines (173 sloc) 6.103 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
require "digest/sha1"
require "heroku/auth"
require "heroku/command"
require "heroku/command/base"
require "heroku/command/help"
require "heroku/plugin"
require "net/http/post/multipart"
require "rest_client"
require "thor"
require "tmpdir"
require "uri"
require "vulcan"
require "yaml"

class Vulcan::CLI < Thor

  desc "build", <<-DESC
build a piece of software for the heroku cloud using COMMAND as a build command
if no COMMAND is specified, a sensible default will be chosen for you

DESC

  method_option :command, :aliases => "-c", :desc => "the command to run for compilation"
  method_option :name, :aliases => "-n", :desc => "the name of the library (defaults to the directory name)"
  method_option :output, :aliases => "-o", :desc => "output build artifacts to this file"
  method_option :prefix, :aliases => "-p", :desc => "vulcan will look in this path for the compiled artifacts"
  method_option :source, :aliases => "-s", :desc => "the source directory to build from or the tgz-ed source archive"
  method_option :deps, :aliases => "-d", :desc => "urls of vulcan compiled libraries to build with", :type=>:array
  method_option :verbose, :aliases => "-v", :desc => "show the full build output", :type => :boolean

  def build
    app = read_config[:app] || "need a server first, use vulcan create"

    source = options[:source] || Dir.pwd
    name = options[:name] || File.basename(source)
    output = options[:output] || "/tmp/#{name}.tgz"
    prefix = options[:prefix] || "/app/vendor/#{name}"
    command = options[:command] || "./configure --prefix #{prefix} && make install"
    deps = options[:deps] || []
    server = URI.parse(ENV["VULCAN_HOST"] || "http://#{app}.herokuapp.com")

    source_is_url = URI.parse(source).scheme

    Dir.mktmpdir do |dir|
      unless source_is_url
        input_tgz = "#{dir}/input.tgz"
        if source.match(/.tgz$/)
          input_tgz = source
        else
          action "Packaging local directory" do
            %x{ cd #{source} && tar czvf #{input_tgz} . 2>&1 }
          end
        end
        input = File.open(input_tgz, "r")
      end

      make_options = {
        "command" => command,
        "prefix" => prefix,
        "secret" => config[:secret],
        "deps" => deps
      }

      if source_is_url
        make_options["code_url"] = source
      else
        make_options["code"] = UploadIO.new(input, "application/octet-stream", "input.tgz")
      end

      request = Net::HTTP::Post::Multipart.new "/make", make_options

      if source_is_url
        print "Initializing build... "
      else
        print "Uploading source package... "
      end

      response = Net::HTTP.start(server.host, server.port) do |http|
        http.request(request) do |response|
          response.read_body do |chunk|
            unless chunk == 0.chr + "\n"
              print chunk if options[:verbose]
            end
          end
        end
      end

      error "Unknown error, no build output given" unless response["X-Make-Id"]

      puts ">> Downloading build artifacts to: #{output}"

      output_url = "#{server}/output/#{response["X-Make-Id"]}"
      puts " (available at #{output_url})"

      File.open(output, "w") do |output|
        begin
          output.print RestClient.get(output_url)
        rescue Exception => ex
          puts ex.inspect
        end
      end
    end
  rescue Interrupt
    error "Aborted by user"
  rescue Errno::EPIPE
    error "Could not connect to build server: #{server}"
  end

  desc "create APP_NAME", <<-DESC
create a build server on Heroku

DESC

  def create(name)
    secret = Digest::SHA1.hexdigest("--#{rand(10000)}--#{Time.now}--")

    Dir.mktmpdir do |dir|
      Dir.chdir(dir) do
        system "env BUNDLE_GEMFILE= heroku create #{name} -s cedar"
      end
    end
    write_config :app => name, :host => "#{name}.herokuapp.com", :secret => secret
    update
  end


  desc "update", <<-DESC
update the build server

DESC

  def update
    error "no app yet, create first" unless config[:app]

    # clean up old plugin, can use auth:token now
    FileUtils.rm_rf(File.expand_path("~/.heroku/plugins/heroku-credentials"))

    Dir.mktmpdir do |dir|
      Dir.chdir(dir) do
        Heroku::Plugin.load!
        api_key = Heroku::Auth.api_key
        error "invalid api key detected, try running `heroku auth:token`" if api_key =~ / /

        system "git init"
        system "git remote add heroku git@#{heroku_git_domain}:#{config[:app]}.git"
        FileUtils.cp_r "#{server_path}/.", "."
        File.open(".gitignore", "w") do |file|
          file.puts ".env"
        end

        system "git add . >/dev/null"
        system "git commit -m commit >/dev/null"
        system "git push heroku -f master"

        heroku "config:add SECRET=#{config[:secret]} SPAWN_ENV=heroku HEROKU_APP=#{config[:app]} HEROKU_API_KEY=#{api_key} NODE_PATH=lib NODE_ENV=production"
        heroku "addons:add cloudant:oxygen"
      end
    end
  end

private

  def action(message)
    print "#{message}... "
    yield
    puts "done"
  end

  def heroku(command)
    %x{ env BUNDLE_GEMFILE= heroku #{command} 2>&1 }
  end

  def config_file
    File.expand_path("~/.vulcan")
  end

  def config
    read_config
  end

  def read_config
    return {} unless File.exists?(config_file)
    config = YAML.load_file(config_file)
    config.is_a?(Hash) ? config : {}
  end

  def write_config(config)
    full_config = read_config.merge(config)
    File.open(config_file, "w") do |file|
      file.puts YAML.dump(full_config)
    end
  end

  def error(message)
    puts "!! #{message}"
    exit 1
  end

  def server_path
    File.expand_path("../../../server", __FILE__)
  end

  #
  # heroku_git_domain checks to see if the heroku-accounts plugin is present,
  # and if so, it will set the domain to the one that matches the credentials
  # for the currently set account
  #
  def heroku_git_domain
    suffix = %x{ git config heroku.account }
    suffix = "com" if suffix.nil? or suffix.strip == ""
    "heroku.#{suffix.strip}"
  end

end
Something went wrong with that request. Please try again.