Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

executable file 673 lines (590 sloc) 17.407 kb
#!/usr/bin/env ruby
# Save your fork, there's cake!"
require 'find'
require 'open-uri'
require 'socket'
require 'fileutils'
require 'io/wait'
require 'pp'
CLOJURE = "1.2.0"
STABLE = "0.7.0"
OS_NAME = case RUBY_PLATFORM
when /darwin/ then :macosx
when /linux/ then :linux
when /(mingw|mswin)/ then :windows
end
if OS_NAME == :windows
begin
require 'rubygems' rescue nil
require 'win32/process'
rescue LoadError
puts 'cake requires win32/process. use "gem install win32-process" to install'
exit
end
TERM = 1
KILL = 'KILL'
PATH_SEP = ';'
$home = File.expand_path(ENV['HOMEDRIVE'] + ENV['HOMEPATH'])
def daemon(cmd)
Process.create(:app_name => cmd.join(' ')).process_id
end
else
TERM = 'TERM'
KILL = 'KILL'
PATH_SEP = ':'
$home = File.expand_path("~")
class Process::Error; end
def daemon(cmd)
puts cmd.join(' ') if debug?
pid = fork do
if not $stdin.tty?
$stdout.close
$stderr.close
end
Process.setsid
exec(*cmd)
end
Process.detach(pid)
pid
end
end
class IO
def gets_nonblock(delim = "\n")
line = ""
while c = read_nonblock(1)
line << c
break if c == delim
end
line
rescue Errno::EAGAIN, Errno::EWOULDBLOCK, EOFError => e
@eof_reached = true if e.is_a?(EOFError)
line
end
def eof_reached?
@eof_reached
end
def eof_nonblock?
return true if eof_reached?
ungetc(read_nonblock(1)[0])
false
rescue Errno::EAGAIN, Errno::EWOULDBLOCK, EOFError => e
e.kind_of?(EOFError)
end
def print_flush(str)
print(str)
flush
rescue Errno::EPIPE => e
end
def duplex(input, output, input_wait = 0, interval = 0.01)
until eof_nonblock?
while self.wait(interval)
if line = block_given? ? yield(gets_nonblock) : gets_nonblock
output.print_flush(line) if line.kind_of?(String)
else
finished = true
end
end
input_wait -= interval
return if finished
while input.ready?
write(input.gets_nonblock(nil))
close_write if input.eof_reached?
end unless OS_NAME == :windows or input_wait > 0
end
end
end
class Object
# Support converting simple Ruby data structures to Clojure expressions.
def to_clj(unquote = false)
if unquote
case self
when Array then return collect {|i| i.to_clj(true)}.join(' ')
when String then return self
when nil then return ''
end
end
case self
when Hash then '{' + collect {|k,v| k.to_clj + ' ' + v.to_clj}.join(' ') + '}'
when Array then '[' + collect {|i| i.to_clj}.join(' ') + ']'
when Symbol then ":#{to_s}"
else inspect.gsub(/(\\a|\\e|\\v|\\x|\\#)/) {|c| CLJ_SUB[c]}
end
end
CLJ_SUB = {
'\a' => '\007',
'\e' => '\033',
'\v' => '\013',
'\#' => '#',
'\x' => 'x', # This will mangle some strings in ruby 1.9.1, but it is better than breaking altogether.
}
define_method(:~) { Unquoted.new(self) }
end
class Unquoted
attr_reader :object
def initialize(object)
@object = object
end
def to_clj(quote = false)
object.to_clj(!quote)
end
alias to_s to_clj
define_method(:~) { @object }
end
begin
require 'readline'
$libedit = true unless Readline.respond_to?(:emacs_editing_mode)
rescue LoadError
$no_readline = true
module Readline
HISTORY = []
attr_accessor :basic_word_break_characters, :completion_proc
def readline(prompt)
$stdout.print_flush(prompt)
$stdin.gets
end
extend Readline
end
end
def add_opt!(key, *vals)
($opts[key.to_sym] ||= []).concat vals
end
def parse_opts!
ARGV.unshift('eval-file') if ARGV.any? and ARGV.first.index('/')
ARGV.unshift('default') if ARGV.empty? or ['-', '@'].include?(ARGV.first[0,1])
$command = ARGV.first.to_sym
$opts = {$command => []}
ARGV[1..-1].each do |opt|
case opt
when "--" then break # stop parsing options
when /^@([-\w]+)$/ then add_opt!(:context, $1)
when /^-(\w+)$/ then $1.split('').each {|c| add_opt!(c, '')}
when /^--?([-\w]+)=(.+)$/ then add_opt!($1, *$2.split(','))
when /^--?([-\w]+)$/ then add_opt!($1, "")
else add_opt!($command, opt)
end
end
$opts.freeze
end
def debug?
ENV['DEBUG']
end
def restart?
$opts[:R] or $opts[:restart]
end
def stable?
$opts[:S] or $opts[:stable]
end
def log(command, *messages)
messages.each do |message|
message.split("\n").each do |line|
printf("%11s %s\n", "[#{command}]", line) unless line.empty?
end
end
end
def system_try(command)
`#{command} 2>&1`
$? == 0
end
class Configuration < Hash
def initialize(*paths)
paths.each do |path|
File.open(path, 'r') do |file|
file.each do |line|
next if ['#', '!'].include?(line[0,1])
key, value = line.split('=', 2)
next unless key and value
self[key.strip] = value.strip
end
end if File.exists?(path)
end
end
def [](*keys)
if keys.first.kind_of?(Symbol)
key = keys.join('.') + '.'
clone.delete_if {|k,v| not k.index(key) == 0}
else
super
end
end
end
def project_dir(dir)
if $opts[:project] and not $opts[:global]
project = $opts[:project].last
raise "project dir #{project} does not exist" unless File.exists?(project)
return project
end
while dir != File.dirname(dir)
return dir if ["project.clj", "tasks.clj"].any? {|file| File.exists?("#{dir}/#{file}")}
dir = File.dirname(dir)
end unless $opts[:global]
"#{$home}/.cake"
end
def readlink(file)
link = File.readlink(file)
return file if link == file
link = "#{File.dirname(file)}/#{link}" unless link.index('/') == 0
readlink(link)
rescue NotImplementedError, Errno::EINVAL
file
end
GET = system_try('which wget') ? "wget -O" : "curl -fo" # prefer wget because it retries on incomplete transfers
def download(url, path, opts = {})
return path if File.exists?(path) and not opts[:force]
FileUtils.makedirs(File.dirname(path))
puts "Downloading #{url}..."
temp = "#{path}.download"
system("#{GET} #{temp} #{url}") || raise("unable to fetch #{url} with curl or wget")
FileUtils.mv(temp, path)
path
end
def get(repo, source_jar, dest_jar = source_jar)
download("#{repo}/#{source_jar}", "#{$m2}/#{dest_jar}")
end
def newer?(file1, file2)
return false unless File.exists?(file1)
not File.exists?(file2) or test(?>, file1, file2)
end
def ps
`jps -v`.split("\n").select {|l| l =~ /cake\.project/}
rescue Errno::ENOENT => e
puts "jps was not found on your PATH. Please add it."
raise e
end
def cake_pids
ps.collect {|line| line.split(' ').first.to_i}
end
def killall
cake_pids.each do |pid|
Process.kill($opts[:"9"] ? KILL : TERM, pid)
end.size > 0
end
TAIL = system_try('tail -q /dev/null') ? 'tail -q' : 'tail'
def tail_log(files, num_lines = 0)
files.collect! do |file|
file = "#{$project}/.cake/#{file}.log"
FileUtils.touch(file)
end
exec("#{TAIL} -n#{num_lines || 10} -F #{files.join(' ')}")
end
def with_logging
tail = $opts[:l] || $opts[:L]
@tail_pid = fork {tail_log(tail ? [:err, :out] : [:err])}
yield
Process.wait(@tail_pid) if $opts[:L]
ensure
Process.kill(KILL, @tail_pid)
end
class JVM
attr_reader :classpath, :pidfile, :load_time
def initialize(classpath, java_opts = "")
@classpath = make_path(classpath)
@java_opts = java_opts
@pidfile = ".cake/pid/#{pid_context.compact.join('-')}"
@load_time = File.exists?(pidfile) ? File.mtime(pidfile) : Time.now
end
def pid_context
[ENV['USER'],
Socket.gethostname,
$version.split('-').first,
$config['jvm.separate-contexts'] && $opts[:context]]
end
def pid
@pid, @port = IO.read(pidfile).split("\n")
@pid = @pid.to_i if @pid
@port = @port.to_i if @port
Process.kill(0, @pid) if @pid # make sure pid is valid
@pid
rescue Errno::ENOENT, Errno::ESRCH, Errno::EBADF, Errno::EPERM, Process::Error => e
reset! # no pidfile or invalid pid
end
def port
while pid
break if @port
sleep 0.1
end
@port
end
def java_opts
server = $config['jvm.server'] == 'true' || !system_try('java -d32 -version')
os_arch = server ? "x86_64" : "x86"
vm_opts = server ? ['-server', '-d64', '-Xmx512M', '-XX:MaxPermSize=256M'] : ['-client', '-d32', '-Xms128M', '-Xmx256M', '-XX:MaxPermSize=128M']
user_opts = "#{ENV['JAVA_OPTS']} #{$config['jvm.opts']}".split.compact
libpath = make_path([$config['jvm.library.path'], "lib/native/#{OS_NAME}/#{os_arch}"])
[vm_opts, user_opts, @java_opts, "-Dcake.pidfile=#{pidfile}", '-cp', classpath, %{-Djava.library.path=#{libpath}}]
end
def start
return if pid
log(:cake, "starting jvm") if debug?
@pid = daemon ["java", java_opts, "clojure.main", "-e", "(require 'cake.main) (cake.main/start-server)"].flatten.compact
log(:cake, "cake started with pid #{@pid}") if debug?
File.open(pidfile, 'w') {|f| f.write("#{@pid}\n") }
end
def kill(force = $opts[:"9"])
if pid
signal = force ? KILL : TERM
log(:kill, "sending #{signal} signal to jvm (#{pid})") if debug?
Process.kill(signal, pid)
reset!
else
log(:kill, "jvm not running") if $command == :kill
end
end
PROMPT = "PROMPT__#{rand}"
def repl
puts ";; cannot find readline; your repl won't be very awesome without it" if $no_readline
load_history
loop do
with_socket do |socket|
socket.write %{[repl, "#{PROMPT}"] #{$vars}}
while @ns = read_until_prompt(socket)
line = readline
return unless line
socket.write(line + "\n")
end
end
end
ensure
save_history
end
def send_command(command)
with_socket do |socket|
cmd = [command, PROMPT].to_clj
log(command, "sending: " + cmd) if debug?
socket.write("#{cmd} #{$vars}")
socket.duplex($stdin, $stdout) do |line|
if line =~ /^#{PROMPT}(.*)$/
socket.write(prompt($1))
elsif line =~ /^@#{PROMPT}(.*)$/
socket.write(prompt($1, :echo => false))
else
line
end
end
end
end
private
def make_path(paths)
paths.flatten.compact.join(PATH_SEP)
end
def reset!
File.unlink(pidfile) if File.exists?(pidfile)
@pid, @port = []
@load_time = Time.now
nil
end
def stale?(file)
File.exists?(file) and File.mtime(file) > load_time
end
def with_socket
socket = TCPSocket.new("localhost", port)
yield(socket)
rescue Errno::EADDRNOTAVAIL => e
log(:cake, "error connecting to socket")
exit
ensure
socket.close if socket
end
HISTORY_NUM = 500
HISTORY_FILE = ".cake/history"
def load_history
open(HISTORY_FILE) do |file|
file.each {|line| Readline::HISTORY << line.chomp}
end if File.exists?(HISTORY_FILE)
end
def save_history
open(HISTORY_FILE, 'w') do |file|
history = Readline::HISTORY.to_a
file.puts(history[-HISTORY_NUM..-1] || history)
end
end
def read_until_prompt(socket)
prompt = nil
socket.duplex($stdin, $stdout, 3) do |line|
if line =~ /^(.*)#{PROMPT}(.*)$/
prompt = $1.empty? ? $2 : "#{$1}\n#{$2}"
nil
else
line
end
end
prompt
end
def complete?(input)
return true if input.empty?
with_socket do |socket|
socket.write(":validate {} #{input.join("\n").strip}")
socket.close_write # send eof
socket.gets != "incomplete\n"
end
end
Readline.basic_word_break_characters = " \t\n\"'`~@;#&{}()[]"
def readline
input = []
prompt = "#{@ns}=> "
Readline.completion_proc = method(:completions)
while line = Readline.readline(prompt)
input << line
if complete?(input)
Readline::HISTORY.push(input.join(' '))
return input.join("\n")
end
if $config['repl.disable-secondary-prompt'] == 'true'
prompt = ' ' * prompt.length
else
prompt[-2] = ?*
end
end
rescue Interrupt => e
return nil if input.empty?
Readline::HISTORY.push(input)
retry
end
def completions(prefix)
return [] if prefix.empty?
with_socket do |socket|
socket.write ~[:completions, {}, ~[prefix, ~@ns, $opts[:cake]]]
completions = []
while line = socket.gets
completions << line.chomp
end
completions
end
end
def prompt(prompt, opts = {})
if opts[:echo] == false
output = `stty -echo 2>&1`
log($command, output) if debug?
echo_off = $? == 0
prompt << ' (WARNING, input will be visible on console!)' unless echo_off
prompt << ':'
end
input = Readline.readline(prompt + ' ') || ''
input + "\n"
ensure
if echo_off
system('stty echo')
puts
end
end
end
def initialize_cake_dirs
FileUtils.makedirs("#{$project}/.cake/run")
FileUtils.makedirs("#{$project}/.cake/pid")
FileUtils.makedirs("#{$home}/.cake/run")
project_clj = "#{$home}/.cake/project.clj"
File.open(project_clj, 'w') do |file|
file.write <<END
(defproject global "0.0.0"
:description "Don't rename this project, but you can change the version if you want."
:dependencies [[clojure "#{CLOJURE}"]]
:copy-deps true)
;;--------------------
;; This is the global cake project. What does that mean?
;; 1. This project is used whenever you run cake outside a project directory.
;; 2. Any dependencies specified here will be available in the global repl.
;; 3. Any dev-dependencies specified here will be available in all projects, but
;; you must run 'cake deps --global' manually when you change this file.
;; 4. Configuration options in ~/.cake/config are used in all projects.
;;--------------------
END
end unless File.exists?(project_clj)
# Enable paren matching if using readline and .inputrc doesn't exist.
inputrc = "#{$home}/.inputrc"
File.open(inputrc, 'w') do |file|
file.write "set blink-matching-paren on\n"
end unless $no_readline or $libedit or File.exists?(inputrc)
end
#==================================
parse_opts!
puts "ruby version: #{RUBY_VERSION}" if debug?
$script = File.expand_path($opts[:run].first) if $opts[:run] and $opts[:run].first
$pwd = Dir.getwd
$project = project_dir($pwd)
$file = File.expand_path(__FILE__)
$cakedir = File.dirname(File.dirname(File.expand_path(readlink($file), File.dirname($file))))
$releases = "http://ninjudd.com/cake-releases"
$m2 = "#{$home}/.m2/repository"
$config = Configuration.new("#{$home}/.cake/config", "cake.config", ".cake/config")
$vars = {:env => ENV.to_hash, :pwd => $pwd, :args => ARGV, :opts => $opts, :script => $0}.to_clj
$timeout = ($config['connect.timeout'] || 60).to_i
initialize_cake_dirs
Dir.chdir($project)
if debug?
puts "config: #{$config.inspect}"
puts "vars: #{$vars}"
end
# Bootstrap cake dependencies.
if File.exists?("#{$cakedir}/src/cake/core.clj") and File.exists?("#{$cakedir}/project.clj") and not stable?
log(:cake, "running from git checkout") if debug?
if $command == :upgrade
log(:upgrade, "pulling latest code from git")
Dir.chdir($cakedir) do
system('git pull')
system('cake deps -Sq')
end
end
if not File.exists?("#{$cakedir}/lib")
FileUtils.makedirs("#{$cakedir}/lib")
Dir.chdir($cakedir) do
system('cake deps -Sq')
end
end
$version = `cd #{$cakedir} && git describe`.chomp
cakepath = ["#{$cakedir}/src", "#{$cakedir}/lib/*"]
bakepath = "#{$cakedir}/dev"
else
log(:cake, "running from ~/.m2") if debug?
if $command == :upgrade
download("#{$releases}/cake", __FILE__, :force => true)
FileUtils.chmod 0755, __FILE__
end
$version = STABLE
clojure = get("http://build.clojure.org/releases", "org/clojure/clojure/#{CLOJURE}/clojure-#{CLOJURE}.jar")
cakejar = get($releases, "jars/cake-#{$version}.jar", "cake/cake/#{$version}/cake-#{$version}.jar")
bakejar = get($releases, "jars/bake-#{$version}.jar", "bake/bake/#{$version}/bake-#{$version}.jar")
cakepath = [cakejar, clojure, bakejar]
bakepath = bakejar
end
log(:cake, "cake version is #{$version}") if debug?
with_logging do
cake = JVM.new(
[bakepath, cakepath, "cake", "#{$home}/.cake/lib/dev/*"],
["-Dcake.project=#{$project}", "-Dbake.path=#{bakepath}"]
)
if $command == :upgrade
killall
puts "cake upgraded to #{$version}"
exit
elsif $command == :default and $opts[:version]
puts "cake #{$version}"
exit
elsif $command == :log
tail_log([:err, :out], $opts[:log].first)
elsif $command == :killall
killall || puts("No matching processes belonging to you were found")
exit
elsif $command == :kill
cake.kill
exit
elsif $command == :ps
puts ps.sort.reverse
exit
elsif $command == :pid
puts cake.pid if cake.pid
exit
elsif $command == :port
puts cake.port if cake.port
exit
end
cake.kill if restart?
cake.start
if $command == :console
num_windows = $opts[:console].first || 1
interval = $opts[:i] ? $opts[:i].first : 4
system("jconsole -interval=#{interval}" + " #{cake.pid}" * num_windows.to_i + "&")
elsif $command == :repl
cake.repl
else
cake.send_command($command)
end
end
Jump to Line
Something went wrong with that request. Please try again.