William Entriken
William Entriken

Reputation: 39243

How to extract tasks and variables from a Rakefile?

I need to:

  1. Open a Rakefile
  2. Find if a certain task is defined
  3. Find if a certain variable is defined

This works to find tasks defined inside a Rakefile, but it pollutes the global namespace (i.e. if you run it twice, all tasks defined in first one will show up in the second one):

sub_rake = Rake::DefaultLoader.new
sub_rake.load("Rakefile")
puts Rake.application.tasks

In Rake, here is where it loads the Makefile:

https://github.com/ruby/rake/blob/master/lib/rake/rake_module.rb#L28

How do I get access to the variables that are loaded there?


Here is an example Rakefile I am parsing:

load '../common.rake'
@source_dir = 'source'
desc "Run all build and deployment tasks, for continuous delivery"
task :deliver => ['git:pull', 'jekyll:build', 'rsync:push']

Here's some things I tried that didn't work. Using eval on the Rakefile:

safe_object = Object.new
safe_object.instance_eval("Dir.chdir('" + f + "')\n" + File.read(folder_rakefile))
if safe_object.instance_variable_defined?("@staging_dir")
  puts "   Staging directory is " + f.yellow + safe_object.instance_variable_get("@staging_dir").yellow
else
  puts "   Staging directory is not specified".red
end

This failed when parsing desc parts of the Rakefile. I also tried things like

puts Rake.instance_variables
puts Rake.class_variables

But these are not getting the @source_dir that I am looking for.

Upvotes: 0

Views: 516

Answers (2)

William Entriken
William Entriken

Reputation: 39243

Rake runs load() on the Rakefile inside load_rakefile in the Rake module. And you can easily get the tasks with the public API.

Rake.load_rakefile("Rakefile")
puts Rake.application.tasks

Apparently that load() invocation causes the loaded variables to be captured into the main Object. This is the top-level Object of Ruby. (I expected it to be captured into Rake since the load call is made in the context of the Rake module.)

Therefore, it is possible to access instance variables from the main object using this ugly code:

main = eval 'self', TOPLEVEL_BINDING
puts main.instance_variable_get('@staging_dir')

Here is a way to encapsulate the parsing of the Rakefile so that opening two files will not have all the things from the first one show up when you are analyzing the second one:

  class RakeBrowser
    attr_reader :tasks
    attr_reader :variables

    include Rake::DSL
    def task(*args, &block)
      if args.first.respond_to?(:id2name)
        @tasks << args.first.id2name
      elsif args.first.keys.first.respond_to?(:id2name)
        @tasks << args.first.keys.first.id2name
      end
    end

    def initialize(file)
      @tasks = []
      Dir.chdir(File.dirname(file)) do
        eval(File.read(File.basename(file)))
      end
      @variables = Hash.new
      instance_variables.each do |name|
        @variables[name] = instance_variable_get(name)
      end
    end
  end
  browser = RakeBrowser.new(f + "Rakefile")
  puts browser.tasks
  puts browser.variables[:@staging_dir]

Upvotes: 0

Joshua Cheek
Joshua Cheek

Reputation: 31726

rakefile_body = <<-RUBY
load '../common.rake'
@source_dir = 'some/source/dir'
desc "Run all build and deployment tasks, for continuous delivery"
task :deliver => ['git:pull', 'jekyll:build', 'rsync:push']
RUBY

def source_dir(ast)
  return nil unless ast.kind_of? AST::Node

  if ast.type == :ivasgn && ast.children[0] == :@source_dir
    rhs = ast.children[1]
    if rhs.type != :str
      raise "@source_dir is not a string literal! #{rhs.inspect}"
    else
      return rhs.children[0]
    end
  end

  ast.children.each do |child|
    value = source_dir(child)
    return value if value
  end

  nil
end

require 'parser/ruby22'
body = Parser::Ruby22.parse(rakefile_body)
source_dir body # => "some/source/dir"

Upvotes: 1

Related Questions