diff --git a/lib/rake.rb b/lib/rake.rb index 0dfd05315..7c1aef6e9 100644 --- a/lib/rake.rb +++ b/lib/rake.rb @@ -58,6 +58,7 @@ module Rake; end require "rake/late_time" require "rake/name_space" require "rake/task_manager" +require "rake/lazy_task_definition" require "rake/application" require "rake/backtrace" diff --git a/lib/rake/application.rb b/lib/rake/application.rb index ac5714b11..600f2d9c8 100644 --- a/lib/rake/application.rb +++ b/lib/rake/application.rb @@ -2,6 +2,7 @@ require "optparse" require "rake/task_manager" +require "rake/lazy_task_definition" require "rake/file_list" require "rake/thread_pool" require "rake/thread_history_display" @@ -18,6 +19,7 @@ module Rake class Application include TaskManager + include LazyTaskDefinition include TraceOutput # The name of the application (typically 'rake') @@ -129,16 +131,20 @@ def load_rakefile # Run the top level tasks of a Rake application. def top_level - run_with_threads do - if options.show_tasks - display_tasks_and_comments - elsif options.show_prereqs - display_prerequisites - else - top_level_tasks.each { |task_name| invoke_task(task_name) } - end + run_with_threads { top_level_intern } + end + + def top_level_intern + execute_all_lazy_definitions if options.loadlazydefinitions + if options.show_tasks + display_tasks_and_comments + elsif options.show_prereqs + display_prerequisites + else + top_level_tasks.each { |task_name| invoke_task(task_name) } end end + private :top_level_intern # Run the given block with the thread startup and shutdown. def run_with_threads @@ -530,6 +536,10 @@ def standard_rake_options # :nodoc: "-N", "Do not search parent directories for the Rakefile.", lambda { |value| options.nosearch = true } ], + ["--load-lazy-definitions", "--loadlazydefinitions", + "-L", "Include all lazy definitions when listing tasks with --tasks", + lambda { |value| options.loadlazydefinitions = true} + ], ["--prereqs", "-P", "Display the tasks and dependencies, then exit.", lambda { |value| options.show_prereqs = true } @@ -838,6 +848,7 @@ def set_default_options # :nodoc: options.job_stats = false options.load_system = false options.nosearch = false + options.loadlazydefinitions = false options.rakelib = %w[rakelib] options.show_all_tasks = false options.show_prereqs = false diff --git a/lib/rake/dsl_definition.rb b/lib/rake/dsl_definition.rb index 27969d69e..b3154b120 100644 --- a/lib/rake/dsl_definition.rb +++ b/lib/rake/dsl_definition.rb @@ -185,6 +185,12 @@ def import(*fns) # :doc: Rake.application.add_import(fn) end end + + # Associate a block to the current scope and + # execute it only when a lookup for a task is performed in the same scope. + def lazy(&block) + Rake.application.register_lazy_definition(&block) + end end extend FileUtilsExt end diff --git a/lib/rake/lazy_task_definition.rb b/lib/rake/lazy_task_definition.rb new file mode 100644 index 000000000..3792ddf87 --- /dev/null +++ b/lib/rake/lazy_task_definition.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true +module Rake + + # The LazyTaskDefinition module is a mixin for managing lazy defined tasks. + module LazyTaskDefinition + + # Execute all definitions: usefull for rake -T for instance + def execute_all_lazy_definitions + lazy_definitions.each do |scope_path, _definitions| + execute_lazy_definitions(scope_path) + end + end + + # Execute all definitions linked to specified +scope_path+ + # and its parent scopes. + def execute_lazy_definitions(scope_path) + scope_path_elements = scope_path.split(':') + sub_scope_elements = [] + scope_path_elements.each do |e| + sub_scope_elements << e + sub_scope_path = sub_scope_elements.join(':') + definitions = lazy_definitions[sub_scope_path] + next unless definitions + definitions.each do |definition| + definition.call + end + definitions.clear + end + end + + # Evaluate the block in specified +scope+. + def in_scope(scope) + cur_scope = @scope + @scope = scope + yield + ensure + @scope = cur_scope + end + + # Register a block which will be called only when necessary during the lookup + # of tasks + def register_lazy_definition(&block) + cur_scope = @scope + lazy_definitions[cur_scope.path] ||= [] + lazy_definitions[cur_scope.path] << ->() { in_scope(cur_scope, &block) } + end + + def lazy_definitions + @lazy_definitions ||= {} + end + private :lazy_definitions + end +end \ No newline at end of file diff --git a/lib/rake/task_manager.rb b/lib/rake/task_manager.rb index 0db5c241e..63243b7cd 100644 --- a/lib/rake/task_manager.rb +++ b/lib/rake/task_manager.rb @@ -208,6 +208,8 @@ def lookup(task_name, initial_scope=nil) def lookup_in_scope(name, scope) loop do tn = scope.path_with_task_name(name) + # use lazy definitions if mixin LazyTaskDefinition is enabled + execute_lazy_definitions(tn) if respond_to?(:execute_lazy_definitions) task = @tasks[tn] return task if task break if scope.empty? diff --git a/test/test_rake_dsl.rb b/test/test_rake_dsl.rb index 6d0e7344d..4fc4d4383 100644 --- a/test/test_rake_dsl.rb +++ b/test/test_rake_dsl.rb @@ -38,4 +38,37 @@ def test_no_commands_constant assert ! defined?(Commands), "should not define Commands" end + def test_lazy + call_count_t1 = 0 + call_count_t3 = 0 + namespace "a" do + lazy do + task "t1" + call_count_t1 += 1 + end + end + namespace "b" do + task "t2" + namespace "c" do + lazy do + namespace "d" do + lazy do + task "t3" + call_count_t3 += 1 + end + end + end + end + end + refute_nil Rake::Task["b:t2"] + assert_equal 0, call_count_t1 + assert_equal 0, call_count_t3 + refute_nil Rake::Task["a:t1"] + assert_equal 1, call_count_t1 + assert_equal 0, call_count_t3 + refute_nil Rake::Task["b:c:d:t3"] + assert_equal 1, call_count_t1 + assert_equal 1, call_count_t3 + end + end diff --git a/test/test_rake_lazy_task_definition.rb b/test/test_rake_lazy_task_definition.rb new file mode 100644 index 000000000..6bd3ccaaa --- /dev/null +++ b/test/test_rake_lazy_task_definition.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true +require File.expand_path("../helper", __FILE__) + +class TestRakeLazyTaskDefinition < Rake::TestCase # :nodoc: + + def setup + super + + @tm = Rake::TestCase::TaskManager.new + @tm.extend Rake::LazyTaskDefinition + end + + def test_lazy_definition + t1, t2, t3 = nil, nil, nil + lazy_definition_call_count = 0 + @tm.in_namespace("a1") do + @tm.register_lazy_definition do + t1 = @tm.define_task(Rake::Task, :t1) + lazy_definition_call_count += 1 + @tm.in_namespace("a2") do + t2 = @tm.define_task(Rake::Task, :t2) + end + end + end + @tm.in_namespace("b") do + t3 = @tm.define_task(Rake::Task, :t3) + end + # task t3 is not lazy. It can be found + assert_equal t3, @tm[:t3, Rake::Scope.make("b")] + # lazy definition is not called until we look for task in namespace a + assert_equal lazy_definition_call_count, 0 + + # task t2 can be found + found_task_t2 = @tm[:t2, Rake::Scope.make("a1:a2")] + assert_equal t2, found_task_t2 + # lazy definition is expected to be called + assert_equal lazy_definition_call_count, 1 + + # task t1 can also be found + found_task_t1 = @tm[:t1, Rake::Scope.make("a1")] + assert_equal t1, found_task_t1 + # lazy definition is called at most once + assert_equal lazy_definition_call_count, 1 + end + + def test_execute_all_lazy_definitions + lazy_definition_call_count = 0 + @tm.in_namespace("a") do + @tm.register_lazy_definition do + lazy_definition_call_count += 1 + end + end + assert_equal lazy_definition_call_count, 0 + @tm.execute_all_lazy_definitions + assert_equal lazy_definition_call_count, 1 + @tm.execute_all_lazy_definitions + assert_equal lazy_definition_call_count, 1 + end + +end