Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
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.