ruby / rake

A make-like build utility for Ruby.
https://ruby.github.io/rake
MIT License
2.35k stars 613 forks source link

import [filename] with task [filename] causes both to execute TWICE? #228

Open rlue opened 7 years ago

rlue commented 7 years ago

I picked up Andrey Koleshko's Rake Task Management Essentials to start learning rake. The book covers the import statement in Chapter 1. After some tinkering, I think I figured out what he was trying to say, but noticed some differences between the behavior he describes in the book and what happens on my machine.

I've copied the relevant section below for reference, but I think the upshot is:

If you import a file, and also define a task with the same name as that file, then that task will be run before the file is imported. This feature is provided so that dependency files may be generated on-the-fly.

...The import statement may be used in any line of the Rakefile, and this doesn't apply to the loading process at all. The imported files will be loaded after the whole Rakefile is loaded. Its usage looks similar to the require statement and is shown in the following line of code:

import(filename)

Here, you are able to pass more than one file.

There is one more feature of the import method. If you pass the filenames to the import task, they are evaluated first, and this allows us to generate the dependent files on the fly. Look at the following Rakefile:

task 'dep.rb' do
  sh %Q{echo "puts 'Hello, from the dep.rb'" > dep.rb}
end

task :hello => 'dep.rb'

import 'dep.rb'

This example generates the dep.rb file on the file due to the import 'dep.rb' call that evaluates the 'dep.rb' task. The result of the hello task execution is shown as follows:

$ rake hello
echo "puts 'Hello, from the dep.rb'" > dep.rb
Hello, from the dep.rb

The problem is that on my machine, the aforementioned pre-import task runs twice:

# Rakefile
import 'foo.rb'

task 'foo.rb' do
  puts "I'm a little teacup,"
end
# foo.rb
puts "short and stout!"
$ rake foo.rb
I'm a little teacup,
short and stout!
I'm a little teacup,
short and stout!

At first, I thought this was happening because I was explicitly calling the foo.rb task, so maybe that was running once because I called it and again in response to the import statement — but then I tried running a different task altogether:

# Rakefile
import 'foo.rb'

task 'foo.rb' do
  puts "I'm a little teacup,"
end

task :bar

and I get the same duplicated output as before:

$ rake bar
I'm a little teacup,
short and stout!
I'm a little teacup,
short and stout!

In fact, it doesn't just spit out the same input twice; it actually runs twice, from start to finish:

# Rakefile
import 'foo.rb'

task 'foo.rb' do
  if !File.exist?('foo.rb')
    puts 'Where is foo.rb?'
    `echo "puts 'I\'m right here.'" > foo.rb`
  else
    puts 'Oh, I didn\'t see you there.'
    `echo "puts 'Sorry, I got held up in traffic.'" > foo.rb`
  end
end
$ rm foo.rb
$ rake foo.rb
Where is foo.rb?
I'm right here.
Oh, I didn't see you there.
Sorry, I got held up in traffic.

Is this the expected behavior, or does this constitute a bug in rake?


On a related note, the command-line flag -T is supposed to display a list of all the rake tasks that have descriptions, but when running rake -T against the above Rakefile, the import directive causes the corresponding task to run anyway:

$ rake -T
I'm a little teacup,
short and stout!
I'm a little teacup,
short and stout!

Is this the expected behavior, or does this constitute a bug in rake?


FWIW I'm on Ruby 2.4.0 / Rake 12.0.0

drbrain commented 7 years ago

Hi, we can’t work on examples that are sexually explicit. It’s not appropriate for users of rake looking for big fixes to be force to read such things

rlue commented 7 years ago

I have removed the offending Saturday Night Live reference from my description of the issue. Thank you for clarifying your standard of propriety; I sincerely apologize for any harm the wording of my issue may have caused.

Now that it has been amended, can you comment on whether the behavior described above is expected/intended or not?

wolftatsu commented 7 years ago

Hi there. I have a same problem. I want to fix it and read a code. I think it is reason below but I don’t understand why Task needs have a actions “Array”? https://github.com/ruby/rake/blob/master/lib/rake/task.rb

def enhance(deps=nil, &block)
  @prerequisites |= deps if deps
  @actions << block if block_given?
  self
end

My plan is a same name task has to be only one. thx.

@wolftatsu

rlue commented 7 years ago

@wolftatsu, I'm not sure we're talking about the same thing. Can you explain more?

fcce commented 6 years ago

@wolftatsu The same problem. load makes the task's actions duplicate

[40] pry(main)> Rake.application.tasks.first.actions
=> [#<Proc:0x007ff287d796b0@/.../lib/tasks/test_task.rake:11>]
[41] pry(main)> load('./lib/tasks/test_task.rake')
=> true
[42] pry(main)> Rake.application.tasks.first.actions
=> [#<Proc:0x007ff287d796b0@/.../lib/tasks/test_task.rake:11>,
 #<Proc:0x007ff287af32b0@/.../lib/tasks/test_task.rake:11>]
[43] pry(main)> load('./lib/tasks/test_task.rake')
=> true
[44] pry(main)> Rake.application.tasks.first.actions
=> [#<Proc:0x007ff287d796b0@/.../lib/tasks/test_task.rake:11>,
 #<Proc:0x007ff287af32b0@/.../lib/tasks/test_task.rake:11>,
 #<Proc:0x007ff285d65450@/.../lib/tasks/test_task.rake:11>]

I'm not sure if the following method has any other impacts

require 'rake/task'
module Rake
  class Task
    def enhance(deps=nil, &block)
      @prerequisites |= deps if deps
      if block_given?
        @actions.clear
        @actions << block
      end
      self
    end
  end
end
stanhu commented 4 years ago

We ran into this as well, and it caused test failures because a test that ran Rails.application.load_tasks and then Rake.application.rake_require 'some/task' would run twice.

Alternative, should @actions be defined as a Set?

fatkodima commented 3 years ago

@fcce @stanhu This is an intended behavior https://github.com/ruby/rake/blob/c2eeae2fe2b67170472a1441ebf84d3a238c3361/lib/rake/task.rb#L413-L415

You should use require to load files only once in this case.

============================================================

@rlue You should use file instead of task in your example. Studying the code for loading imports (and tests for it) https://github.com/ruby/rake/blob/c2eeae2fe2b67170472a1441ebf84d3a238c3361/lib/rake/application.rb#L775-L789

I would say, that this is an intended behavior.

# Rakefile
task 'dep.rb' do
  puts "From Rakefile"
end

task :hello => 'dep.rb'

import 'dep.rb'
# dep.rb
task 'dep.rb' do
  puts "From dep.rb"
end

Output:

$ bundle exec rake hello
From Rakefile
From Rakefile
From dep.rb

You enhanced the task named dep.rb in the file named dep.rb (by "redefining" it), so after you imported this file you want to run that part too.

So, this issue should be closed.