├── .gitignore ├── bin └── sake ├── Manifest ├── lib ├── pastie.rb ├── server.rb ├── help.rb └── sake.rb ├── LICENSE ├── Rakefile └── README /.gitignore: -------------------------------------------------------------------------------- 1 | sake.gemspec 2 | -------------------------------------------------------------------------------- /bin/sake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | require 'sake' 4 | 5 | Sake.new(ARGV).run 6 | -------------------------------------------------------------------------------- /Manifest: -------------------------------------------------------------------------------- 1 | bin/sake 2 | lib/help.rb 3 | lib/pastie.rb 4 | lib/sake.rb 5 | lib/server.rb 6 | Manifest 7 | Rakefile 8 | README 9 | LICENSE 10 | -------------------------------------------------------------------------------- /lib/pastie.rb: -------------------------------------------------------------------------------- 1 | require 'tempfile' 2 | 3 | class Pastie #:nodoc: all 4 | PASTE_URL = ENV['SAKE_PASTIE_URL'] || ENV['PASTIE_URL'] || 'http://pastie.caboo.se/pastes/create' 5 | 6 | def self.paste(text) 7 | text_file = Tempfile.open('w+') 8 | text_file << text 9 | text_file.flush 10 | 11 | cmd = <<-EOS 12 | curl #{PASTE_URL} \ 13 | -s -L -o /dev/null -w "%{url_effective}" \ 14 | -H "Expect:" \ 15 | -F "paste[parser]=ruby" \ 16 | -F "paste[restricted]=0" \ 17 | -F "paste[authorization]=burger" \ 18 | -F "paste[body]=<#{text_file.path}" \ 19 | -F "key=" \ 20 | -F "x=27" \ 21 | -F "y=27" 22 | EOS 23 | 24 | out = %x{ 25 | #{cmd} 26 | } 27 | 28 | text_file.close(true) 29 | out 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007 Chris Wanstrath 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | require 'lib/sake' unless defined? Sake 5 | 6 | begin 7 | require 'echoe' 8 | 9 | Echoe.new('sake', Sake::Version::String) do |p| 10 | p.rubyforge_name = 'err' 11 | p.summary = "Sake tastes great and helps maintain system-level Rake files." 12 | p.description = "Sake tastes great and helps maintain system-level Rake files." 13 | p.url = "http://errtheblog.com/" 14 | p.author = 'Chris Wanstrath' 15 | p.email = "chris@ozmm.org" 16 | p.dependencies = ['ParseTree >=2.1.1', 'ruby2ruby >=1.1.8'] 17 | end 18 | 19 | rescue LoadError => boom 20 | puts "You are missing a dependency required for meta-operations on this gem." 21 | puts "#{boom.to_s.capitalize}." 22 | end 23 | 24 | desc 'Generate RDoc documentation for Sake.' 25 | Rake::RDocTask.new(:rdoc) do |rdoc| 26 | files = ['README', 'LICENSE', 'lib/**/*.rb'] 27 | rdoc.rdoc_files.add(files) 28 | rdoc.main = "README" # page to start on 29 | rdoc.title = "sake" 30 | rdoc.template = File.exists?(t="/Users/chris/ruby/projects/err/rock/template.rb") ? t : "/var/www/rock/template.rb" 31 | rdoc.rdoc_dir = 'doc' # rdoc output folder 32 | rdoc.options << '--inline-source' 33 | end 34 | -------------------------------------------------------------------------------- /lib/server.rb: -------------------------------------------------------------------------------- 1 | require 'sake' unless defined? Sake 2 | require 'mongrel' 3 | 4 | class Sake 5 | module Server #:nodoc:all 6 | extend self 7 | 8 | def start(args) 9 | if index = args.index('-p') 10 | port = args[index+1].to_i 11 | else 12 | port = 4567 13 | end 14 | 15 | daemoned = args.include? '-d' 16 | 17 | config = Mongrel::Configurator.new :host => "127.0.0.1" do 18 | daemonize(:cwd => '.', :log_file => 'sake.log') if daemoned 19 | listener(:port => port) { uri "/", :handler => Handler.new } 20 | end 21 | 22 | trap("INT") { config.stop } 23 | 24 | config.run 25 | puts "# Serving warm sake tasks on port #{port}..." unless daemoned 26 | config.join 27 | end 28 | 29 | class Handler < Mongrel::HttpHandler 30 | def process(request, response) 31 | uri = request.params['PATH_INFO'].sub(/^\//, '') 32 | status = uri.empty? ? 200 : 404 33 | body = status == 200 ? Store.to_ruby : 'Not Found' 34 | 35 | response.start(status) do |headers, output| 36 | headers['Content-Type'] = 'text/plain' 37 | output.write body 38 | end 39 | end 40 | end 41 | end 42 | end 43 | 44 | Sake::Server.start(ARGV) if $0 == __FILE__ 45 | -------------------------------------------------------------------------------- /lib/help.rb: -------------------------------------------------------------------------------- 1 | class Sake 2 | module Help #:nodoc: 3 | extend self 4 | 5 | def display 6 | die <<-end_help 7 | Usage: sake [options] 8 | 9 | Any can be either a local file or a remote URL (such as a web page). 10 | 11 | -T Show installed Sake tasks. 12 | -T pattern Show installed Sake tasks matching . 13 | -T file Show tasks in . 14 | -T file pattern Show tasks in . 15 | -Tv Show all installed Sake tasks. 16 | -Tv pattern Show all installed Sake tasks matching . 17 | -Tv file Show all tasks in . 18 | -Tv file pattern Show all tasks in . 19 | 20 | -i file Install all tasks from . 21 | -i file tasks Install tasks from . Can be one or more tasks. 22 | 23 | -u tasks Uninstall one or more tasks. 24 | 25 | -e task Show the source for task. 26 | -e file Show the source for all tasks from . 27 | -e file tasks Show the source for as defined in . 28 | -P [file/tasks] Send the source for tasks to Pastie (see -e for options). 29 | 30 | -S Start a Mongrel handler and serve your installed Sake tasks 31 | over port 4567. 32 | -p Set the port to serve Sake tasks on. Defaults to 4567. Only 33 | works with -S. 34 | -d Start and daemonize a Sake server. Only works with -S. 35 | end_help 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | = Sake. Best served warm. 2 | 3 | Sick of copy & pasting your badass custom Rakefiles into every new Rails app 4 | you start? Fed up with writing one-off admistrative scripts and leaving them 5 | everything? 6 | 7 | No longer. Sake is a tool which helps you maintain a set of system level Rake tasks. 8 | 9 | Get started: 10 | 11 | $ sudo gem install sake 12 | $ sake -h 13 | 14 | Show all Sake tasks (but no local Rake tasks), optionally only those matching a pattern. 15 | $ sake -T 16 | $ sake -T db 17 | 18 | Show tasks in a Rakefile, optionally only those matching a pattern. 19 | $ sake -T file.rake 20 | $ sake -T file.rake db 21 | 22 | Install tasks from a Rakefile, optionally specifying specific tasks. 23 | $ sake -i Rakefile 24 | $ sake -i Rakefile db:remigrate 25 | $ sake -i Rakefile db:remigrate routes 26 | 27 | Examine the source of a Rake task. 28 | $ sake -e routes 29 | 30 | You can also examine the source of a task not yet installed. 31 | $ sake -e Rakefile db:remigrate 32 | 33 | Uninstall an installed task. (Can be passed one or more tasks.) 34 | $ sake -u db:remigrate 35 | 36 | Post a task to Pastie! 37 | $ sake -p routes 38 | 39 | Invoke a Sake task. 40 | $ sake 41 | 42 | Some Sake tasks may depend on tasks which exist only locally. 43 | 44 | For instance, you may have a db:version sake task which depends 45 | on the 'environment' Rake task. The 'environment' Rake task is one 46 | defined by Rails to load its environment. This db:version task will 47 | work when your current directory is within a Rails app because 48 | Sake knows how to find Rake tasks. This task will not work, 49 | however, in any other directory (unless a task named 'environment' 50 | indeed exists). 51 | 52 | Sake can also serve its tasks over a network by launching a Mongrel handler. 53 | Pass the -S switch to start Sake in server mode. 54 | 55 | $ sake -S 56 | 57 | You can, of course, specify a port. 58 | $ sake -S -p 1111 59 | 60 | You can also daemonize your server for long term serving fun. 61 | $ sake -S -d 62 | 63 | == Special Thanks 64 | 65 | * Ryan Davis 66 | * Eric Hodel 67 | * Josh Susser 68 | * Brian Donovan 69 | * Zack Chandler 70 | * Dr Nic Williams 71 | 72 | == Author 73 | 74 | >> Chris Wanstrath 75 | => chris@ozmm.org 76 | -------------------------------------------------------------------------------- /lib/sake.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Sake. Best served warm. 3 | # 4 | # >> Chris Wanstrath 5 | # => chris@ozmm.org 6 | 7 | require 'rubygems' 8 | require 'rake' 9 | require 'fileutils' 10 | require 'open-uri' 11 | begin 12 | gem 'ParseTree', '>=2.1.1' 13 | require 'parse_tree' 14 | gem 'ruby2ruby', '>=1.1.8' 15 | require 'ruby2ruby' 16 | rescue LoadError 17 | puts "# Sake requires the ParseTree and ruby2ruby gems and Ruby >=1.8.6." 18 | exit 19 | end 20 | require File.dirname(__FILE__) + '/help' 21 | require File.dirname(__FILE__) + '/pastie' 22 | 23 | ## 24 | # Show all Sake tasks (but no local Rake tasks), optionally only those matching a pattern. 25 | # $ sake -T 26 | # $ sake -T db 27 | # 28 | # Show tasks in a Rakefile, optionally only those matching a pattern. 29 | # $ sake -T file.rake 30 | # $ sake -T file.rake db 31 | # 32 | # Install tasks from a Rakefile, optionally specifying specific tasks. 33 | # $ sake -i Rakefile 34 | # $ sake -i Rakefile db:remigrate 35 | # $ sake -i Rakefile db:remigrate routes 36 | # 37 | # Examine the source of a Rake task. 38 | # $ sake -e routes 39 | # 40 | # You can also examine the source of a task not yet installed. 41 | # $ sake -e Rakefile db:remigrate 42 | # 43 | # Uninstall an installed task. 44 | # $ sake -u db:remigrate 45 | # 46 | # Stores the source of a task into a pastie (http://pastie.caboo.se). 47 | # Returns the url of the pastie to stdout. 48 | # $ sake -P routes 49 | # 50 | # Can be passed one or more tasks. 51 | # 52 | # Invoke a Sake task. 53 | # $ sake 54 | # 55 | # Some Sake tasks may depend on tasks which exist only locally. 56 | # 57 | # For instance, you may have a db:version sake task which depends 58 | # on the 'environment' Rake task. The 'environment' Rake task is one 59 | # defined by Rails to load its environment. This db:version task will 60 | # work when your current directory is within a Rails app because 61 | # Sake knows how to find Rake tasks. This task will not work, 62 | # however, in any other directory (unless a task named 'environment' 63 | # indeed exists). 64 | # 65 | # Sake can also serve its tasks over a network by launching a Mongrel handler. 66 | # Pass the -S switch to start Sake in server mode. 67 | # 68 | # $ sake -S 69 | # 70 | # You can, of course, specify a port. 71 | # $ sake -S -p 1111 72 | # 73 | # You can also daemonize your server for long term serving fun. 74 | # $ sake -S -d 75 | # 76 | class Sake 77 | module Version #:nodoc: 78 | Major = '1' 79 | Minor = '0' 80 | Tweak = '16' 81 | String = [ Major, Minor, Tweak ].join('.') 82 | end 83 | 84 | ## 85 | # The `application' class, this is basically the controller 86 | # which decides what to do then executes. 87 | def initialize(args) 88 | @args = args 89 | Rake.application 90 | Rake.application.options.silent = true 91 | end 92 | 93 | ## 94 | # This method figures out what to do and does it. 95 | # Basically a big switch. Note the seemingly random 96 | # return statements: return if you don't want run_rake invoked. 97 | # Some actions do want it invoked, however, so they don't return 98 | # (like version, which prints a Sake version then trusts Rake to do 99 | # likewise). 100 | def run 101 | if index = @args.index("--force") 102 | @args.delete("--force") 103 | @force = true 104 | end 105 | 106 | ## 107 | # Show Sake tasks in the store or in a file, optionally searching for a pattern. 108 | # $ sake -T 109 | # $ sake -T db 110 | # $ sake -T file.rake 111 | # $ sake -T file.rake db 112 | # Show all Sake tasks in the store or in a file, optionally searching for a pattern. 113 | # $ sake -Tv 114 | # $ sake -Tv db 115 | # $ sake -Tv file.rake 116 | # $ sake -Tv file.rake db 117 | if (index = @args.index('-T') || @args.index('-Tv')) || @args.empty? 118 | display_hidden = true if @args.index('-Tv') 119 | begin 120 | tasks = TasksFile.parse(@args[index + 1]).tasks 121 | pattern = @args[index + 2] 122 | rescue => parse_error 123 | tasks = Store.tasks.sort 124 | pattern = index ? @args[index + 1] : nil 125 | end 126 | output = show_tasks(tasks, pattern, display_hidden) 127 | if output.empty? and @args.size > 1 # show_tasks didn't show any tasks 128 | case parse_error 129 | when Errno::ENOENT, OpenURI::HTTPError 130 | die "# Can't find file (or task) `#{@args[index + 1]}'" 131 | when SecurityError 132 | die "# SecurityError parsing `#{@args[index + 1]}'" 133 | else 134 | die "# No matching tasks for `#{pattern}'" if pattern 135 | end 136 | end 137 | return output 138 | 139 | ## 140 | # Install a Rakefile or a single Rake task 141 | # $ sake -i Rakefile 142 | # $ sake -i Rakefile db:migrate 143 | elsif index = @args.index('-i') 144 | return install(index) 145 | 146 | ## 147 | # Uninstall one or more Rake tasks from the Sake store. 148 | elsif index = @args.index('-u') 149 | return uninstall(index) 150 | 151 | ## 152 | # Examine a Rake task 153 | # $ sake -e routes 154 | # $ sake -e Rakefile db:remigrate 155 | elsif index = @args.index('-e') 156 | die examine(index) 157 | 158 | ## 159 | # Save one or more tasks to Pastie (http://pastie.caboos.se) 160 | # then return the new Pastie's url 161 | # $ sake -P routes 162 | # $ sake -P Rakefile db:remigrate 163 | elsif index = @args.index('-P') 164 | die Pastie.paste(examine(index)) 165 | 166 | ## 167 | # Start a Mongrel handler which will serve local Rake tasks 168 | # to anyone who wants them. 169 | # 170 | # $ sake -S 171 | # 172 | # Set a port 173 | # $ sake -S -p 1111 174 | # 175 | # Daemonize 176 | # $ sake -S -d 177 | elsif @args.include? '-S' 178 | return serve_tasks 179 | 180 | ## 181 | # Prints Sake and Rake versions. 182 | elsif @args.include? '--version' 183 | version 184 | 185 | ## 186 | # Prints out the help screen. 187 | elsif @args.include? '-h' or @args.include? '--help' 188 | return Help.display 189 | end 190 | 191 | ## 192 | # Runs Rake proper, including our ~/.sake tasks. 193 | run_rake 194 | end 195 | 196 | private 197 | 198 | def show_tasks(tasks = [], pattern = nil, display_hidden = nil) 199 | Rake.application.show(tasks, pattern, display_hidden) 200 | end 201 | 202 | def install(index) 203 | die "# I need a Rakefile." unless file = @args[index+1] 204 | 205 | tasks = TasksFile.parse(file).tasks 206 | 207 | # We may want to install a specific task 208 | unless (target_tasks = @args[index + 2..-1]).empty? 209 | tasks = tasks.select { |task| target_tasks.include? task.name } 210 | end 211 | 212 | # No duplicates. 213 | tasks.each do |task| 214 | if Store.has_task?(task) && !@force 215 | puts "# Task `#{task}' already exists in #{Store.path}" 216 | next 217 | elsif Store.has_task?(task) 218 | puts "# Task `#{task}' already exists. Updating it." 219 | Store.remove_task(task) 220 | else 221 | puts "# Installing task `#{task}'" 222 | end 223 | 224 | Store.add_task task 225 | end 226 | 227 | # Commit. 228 | Store.save! 229 | end 230 | 231 | def uninstall(index) 232 | die "# -u option needs one or more installed tasks" if (tasks = @args[index+1..-1]).empty? 233 | 234 | tasks.each do |name| 235 | if task = Store.tasks[name] 236 | puts "# Uninstalling `#{task}'. Here it is, for reference:", task.to_ruby, '' 237 | Store.remove_task(task) 238 | else 239 | puts "# You don't have task `#{name}' installed.", '' 240 | end 241 | end 242 | 243 | Store.save! 244 | end 245 | 246 | ## 247 | # There is a lot of guesswork inside this method. Sorry. 248 | def examine(index) 249 | # Can be -e file task or -e task, which defaults to Store.path 250 | if @args[index + 2] 251 | file = @args[index + 1] 252 | task = @args[index + 2] 253 | else 254 | task = @args[index + 1] 255 | end 256 | 257 | # They didn't pass any args in, so just show the ~/.sake file 258 | unless task 259 | return Store.tasks.to_ruby 260 | end 261 | 262 | # Try to find the task we think they asked for. 263 | tasks = file ? TasksFile.parse(file).tasks : Store.tasks 264 | 265 | if tasks[task] 266 | return tasks[task].to_ruby 267 | end 268 | 269 | # Didn't find the task. See if it's a file and, if so, spit 270 | # it out. 271 | unless (tasks = TasksFile.parse(task).tasks).empty? 272 | return tasks.to_ruby 273 | end 274 | 275 | # Failure. On all counts. 276 | error = "# Can't find task (or file) `#{task}'" 277 | error << " in #{file}" if file 278 | die error 279 | end 280 | 281 | def serve_tasks 282 | require File.dirname(__FILE__) + '/server' 283 | Server.start(@args) 284 | end 285 | 286 | def version 287 | puts "sake, version #{Version::String}" 288 | end 289 | 290 | def run_rake 291 | import Sake::Store.path 292 | Rake.application.run 293 | end 294 | 295 | ## 296 | # Lets us do: 297 | # tasks = TasksFile.parse('Rakefile').tasks 298 | # task = tasks['db:remigrate'] 299 | class TasksArray < Array 300 | ## 301 | # Accepts a task name or index. 302 | def [](name_or_index) 303 | if name_or_index.is_a? String 304 | detect { |task| task.name == name_or_index } 305 | else 306 | super 307 | end 308 | end 309 | 310 | ## 311 | # The source of all these tasks. 312 | def to_ruby 313 | map { |task| task.to_ruby }.join("\n") 314 | end 315 | end 316 | 317 | ## 318 | # This class represents a Rake task file, in the traditional sense. 319 | # It takes on parameter: the path to a Rakefile. When instantiated, 320 | # it will read the file and parse out the rake tasks, storing them in 321 | # a 'tasks' array. This array can be accessed directly: 322 | # 323 | # file = Sake::TasksFile.parse('Rakefile') 324 | # puts file.tasks.inspect 325 | # 326 | # The parse method also works with remote files, as its implementation 327 | # uses open-uri's open(). 328 | # 329 | # Sake::TasksFile.parse('Rakefile') 330 | # Sake::TasksFile.parse('http://errtheblog.com/code/errake') 331 | class TasksFile 332 | include Rake::TaskManager 333 | attr_reader :tasks 334 | 335 | ## 336 | # The idea here is that we may be sucking in Rakefiles from an untrusted 337 | # source. While we're happy to let the user audit the code of any Rake 338 | # task before running it, we'd rather not be responsible for executing a 339 | # `rm -rf` in the Rakefile itself. To ensure this, we need to set a 340 | # safelevel before parsing the Rakefile in question. 341 | def self.parse(file) 342 | body = (file == "-" ? $stdin : open(file)).read 343 | 344 | instance = new 345 | Thread.new { instance.instance_eval "$SAFE = 3\n#{body}" }.join 346 | instance 347 | end 348 | 349 | def initialize 350 | @namespace = [] 351 | @tasks = TasksArray.new 352 | @comment = nil 353 | end 354 | 355 | ## 356 | # We fake out an approximation of the Rake DSL in order to build 357 | # our tasks array. 358 | private 359 | 360 | ## 361 | # Set a namespace for the duration of the block. Namespaces can be 362 | # nested. 363 | def namespace(name) 364 | @namespace << name 365 | yield 366 | @namespace.delete name 367 | end 368 | 369 | ## 370 | # Describe the following task. 371 | def desc(comment) 372 | @comment = comment 373 | end 374 | 375 | ## 376 | # Define a task and any dependencies it may have. 377 | def task(*args, &block) 378 | # Use Rake::TaskManager method to get task details 379 | task_name, arg_names, deps = resolve_args(args) 380 | 381 | # Our namespace is really just a convenience method. Essentially, 382 | # a namespace is just part of the task name. 383 | task_name = [ @namespace, task_name ].flatten * ':' 384 | 385 | # Sake's version of a rake task 386 | task = Task.new(task_name, arg_names, deps, @comment, &block) 387 | 388 | @tasks << task 389 | 390 | # We sucked up the last 'desc' declaration if it existed, so now clear 391 | # it -- we don't want tasks without a description given one. 392 | @comment = nil 393 | end 394 | 395 | public 396 | 397 | ## 398 | # Call to_ruby on all our tasks and return a concat'd string of them. 399 | def to_ruby 400 | @tasks.to_ruby 401 | end 402 | 403 | ## 404 | # Add tasks to this TasksFile. Can accept another TasksFile object or 405 | # an array of Task objects. 406 | def add_tasks(tasks) 407 | Array(tasks.is_a?(TasksFile) ? tasks.tasks : tasks).each do |task| 408 | add_task task 409 | end 410 | end 411 | 412 | ## 413 | # Single task version of add_tasks 414 | def add_task(task) 415 | @tasks << task 416 | end 417 | 418 | ## 419 | # Does this task exist? 420 | def has_task?(task) 421 | @tasks.map { |t| t.to_s }.include? task.to_s 422 | end 423 | 424 | ## 425 | # Hunt for and remove a particular task. 426 | def remove_task(target_task) 427 | @tasks.reject! { |task| task.name == target_task.name } 428 | end 429 | end 430 | 431 | ## 432 | # This is Sake's version of a Rake task. Please handle with care. 433 | class Task 434 | attr_reader :name, :comment 435 | 436 | def initialize(name, args = nil, deps = nil, comment = nil, &block) 437 | @name = name 438 | @comment = comment 439 | @args = Array(args) 440 | @deps = Array(deps) 441 | @body = block 442 | end 443 | 444 | ## 445 | # Turn ourselves back into Rake task plaintext. 446 | def to_ruby 447 | out = '' 448 | out << "desc '#{@comment.gsub("'", "\\\\'")}'\n" if @comment 449 | out << "task '#{@name}'" 450 | 451 | if @args.any? 452 | args = @args.map { |arg| ":#{arg}" }.join(', ') 453 | out << ", #{args} " 454 | end 455 | 456 | if @deps.any? 457 | deps = @deps.map { |dep| "'#{dep}'" }.join(', ') 458 | out << ", :needs => [ #{deps} ]" 459 | end 460 | 461 | if @args.any? 462 | out << " do |t, args|\n" 463 | else 464 | out << " do\n" 465 | end 466 | 467 | # get rid of the proc { / } lines 468 | out << @body.to_ruby.split("\n")[1...-1].join("\n") rescue nil 469 | 470 | out << "\nend\n" 471 | end 472 | 473 | ## 474 | # String-ish duck typing, sorting based on Task names 475 | def <=>(other) 476 | to_s <=> other.to_s 477 | end 478 | 479 | ## 480 | # The task name 481 | def to_s; @name end 482 | 483 | ## 484 | # Basically to_s.inspect 485 | def inspect; @name.inspect end 486 | end 487 | 488 | ## 489 | # The store is, as of writing, a single Rakefile: ~/.sake 490 | # When we add new tasks, we just re-build this file. Over 491 | # and over. 492 | module Store 493 | extend self 494 | 495 | ## 496 | # Everything we can't catch gets sent to our tasks_file. 497 | # Common examples are #tasks or #add_task. 498 | def method_missing(*args, &block) 499 | tasks_file.send(*args, &block) 500 | end 501 | 502 | ## 503 | # A TaskFile object of our Store 504 | def tasks_file 505 | @tasks_file ||= TasksFile.parse(path) 506 | end 507 | 508 | ## 509 | # The platform-aware path to the Store 510 | def path 511 | path = if PLATFORM =~ /win32/ 512 | win32_path 513 | else 514 | File.join(File.expand_path('~'), '.sake') 515 | end 516 | FileUtils.touch(path) unless path.is_file? 517 | path 518 | end 519 | 520 | def win32_path #:nodoc: 521 | unless File.exists?(win32home = ENV['HOMEDRIVE'] + ENV['HOMEPATH']) 522 | puts "# No HOMEDRIVE or HOMEPATH environment variable.", 523 | "# Sake needs to know where it should save Rake tasks!" 524 | else 525 | File.join(win32home, 'Sakefile') 526 | end 527 | end 528 | 529 | ## 530 | # Wrote our current tasks_file to disk, overwriting the current Store. 531 | def save! 532 | tasks_file # ensure the tasks_file is loaded before overwriting 533 | File.open(path, 'w') do |file| 534 | file.puts tasks_file.to_ruby 535 | end 536 | end 537 | end 538 | end 539 | 540 | module Rake # :nodoc: all 541 | class Application 542 | ## 543 | # Show the tasks as 'sake' tasks. 544 | def printf(*args) 545 | args[0].sub!('rake', 'sake') if args[0].is_a? String 546 | super 547 | end 548 | 549 | ## 550 | # Show tasks that don't have comments' 551 | def display_tasks_and_comments(tasks = nil, pattern = nil, display_hidden = nil) 552 | tasks ||= self.tasks 553 | 554 | if pattern ||= options.show_task_pattern 555 | tasks = tasks.select { |t| t.name[pattern] || t.comment.to_s[pattern] } 556 | end 557 | 558 | width = tasks.collect { |t| t.name.length }.max 559 | 560 | tasks.each do |t| 561 | comment = " # #{t.comment}" if t.comment 562 | if display_hidden 563 | printf "sake %-#{width}s#{comment}\n", t.name 564 | else 565 | printf "sake %-#{width}s#{comment}\n", t.name if t.name && t.comment 566 | end 567 | end 568 | end 569 | alias_method :show, :display_tasks_and_comments 570 | 571 | ## 572 | # Run Sake even if no Rakefile exists in the current directory. 573 | alias_method :sake_original_have_rakefile, :have_rakefile 574 | def have_rakefile(*args) 575 | @rakefile ||= '' 576 | sake_original_have_rakefile(*args) || true 577 | end 578 | 579 | ## 580 | # Accept only one task, unlike Rake, to make passing arguments cleaner. 581 | alias_method :sake_original_collect_tasks, :collect_tasks 582 | def collect_tasks 583 | sake_original_collect_tasks 584 | @top_level_tasks = [@top_level_tasks.first] 585 | end 586 | end 587 | end 588 | 589 | ## 590 | # Hacks which give us "Rakefile".is_file? 591 | class String # :nodoc: 592 | def is_file? 593 | File.exists? self 594 | end 595 | end 596 | 597 | class Nil # :nodoc: 598 | def is_file? 599 | false 600 | end 601 | 602 | # under the evil 603 | def method_missing(*args, &block) 604 | super 605 | end 606 | end 607 | 608 | def die(*message) # :nodoc: 609 | puts message 610 | exit 611 | end 612 | 613 | Sake.new(ARGV).run if $0 == __FILE__ 614 | --------------------------------------------------------------------------------