├── bin ├── setup ├── console └── rubocop ├── doc ├── example │ ├── a.c │ ├── b.c │ ├── main.c │ ├── Rakefile2 │ └── Rakefile1 ├── glossary.rdoc ├── proto_rake.rdoc ├── rake.1 └── rational.rdoc ├── .gitignore ├── Gemfile ├── lib ├── rake │ ├── task_argument_error.rb │ ├── tasklib.rb │ ├── version.rb │ ├── default_loader.rb │ ├── late_time.rb │ ├── multi_task.rb │ ├── phony.rb │ ├── early_time.rb │ ├── private_reader.rb │ ├── rule_recursion_overflow_error.rb │ ├── cloneable.rb │ ├── pseudo_status.rb │ ├── invocation_exception_mixin.rb │ ├── rake_test_loader.rb │ ├── trace_output.rb │ ├── ext │ │ ├── core.rb │ │ └── string.rb │ ├── file_creation_task.rb │ ├── name_space.rb │ ├── backtrace.rb │ ├── scope.rb │ ├── thread_history_display.rb │ ├── invocation_chain.rb │ ├── loaders │ │ └── makefile.rb │ ├── win32.rb │ ├── rake_module.rb │ ├── file_task.rb │ ├── clean.rb │ ├── promise.rb │ ├── task_arguments.rb │ ├── linked_list.rb │ ├── cpu_counter.rb │ ├── file_utils.rb │ ├── file_utils_ext.rb │ ├── thread_pool.rb │ ├── testtask.rb │ └── dsl_definition.rb └── rake.rb ├── .github ├── dependabot.yml └── workflows │ ├── lint.yml │ ├── coverage.yml │ ├── push_gem.yml │ ├── dependabot_automerge.yml │ ├── gh-pages.yml │ └── test.yml ├── test ├── test_rake_file_list_path_map.rb ├── test_rake_late_time.rb ├── test_rake_path_map_partial.rb ├── test_rake_pseudo_status.rb ├── test_rake_reduce_compat.rb ├── test_rake_early_time.rb ├── test_private_reader.rb ├── test_rake_dsl.rb ├── support │ ├── file_creation.rb │ └── ruby_runner.rb ├── test_rake_require.rb ├── test_rake.rb ├── test_rake_path_map_explode.rb ├── test_rake_task_manager_argument_resolution.rb ├── test_rake_scope.rb ├── test_rake_makefile_loader.rb ├── test_rake_name_space.rb ├── test_rake_extension.rb ├── test_trace_output.rb ├── test_rake_top_level_functions.rb ├── test_rake_cpu_counter.rb ├── test_rake_invocation_chain.rb ├── test_rake_file_creation_task.rb ├── test_rake_rake_test_loader.rb ├── test_rake_win32.rb ├── test_rake_definitions.rb ├── test_rake_directory_task.rb ├── test_rake_linked_list.rb ├── test_rake_package_task.rb ├── test_thread_history_display.rb ├── test_rake_multi_task.rb ├── helper.rb ├── test_rake_backtrace.rb ├── test_rake_task_argument_parsing.rb ├── test_rake_thread_pool.rb ├── test_rake_clean.rb ├── test_rake_task_arguments.rb ├── test_rake_file_task.rb ├── test_rake_test_task.rb ├── test_rake_task_with_arguments.rb ├── test_rake_task_manager.rb └── test_rake_path_map.rb ├── Rakefile ├── MIT-LICENSE ├── .rubocop.yml ├── exe └── rake ├── CONTRIBUTING.rdoc ├── rake.gemspec └── README.rdoc /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | -------------------------------------------------------------------------------- /doc/example/a.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void a() 4 | { 5 | printf ("In function a\n"); 6 | } 7 | -------------------------------------------------------------------------------- /doc/example/b.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void b() 4 | { 5 | printf ("In function b\n"); 6 | } 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "rake" 5 | 6 | require "irb" 7 | IRB.start 8 | -------------------------------------------------------------------------------- /doc/example/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | extern void a(); 4 | extern void b(); 5 | 6 | int main () 7 | { 8 | a(); 9 | b(); 10 | return 0; 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | *.rbc 3 | *.swp 4 | *~ 5 | .#* 6 | .DS_Store 7 | .idea 8 | .rbx 9 | /.rdoc 10 | /TAGS 11 | /coverage 12 | /html 13 | /_site 14 | /pkg 15 | Gemfile.lock 16 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem "test-unit" 7 | gem "coveralls" 8 | gem "rubocop" 9 | gem "rdoc" 10 | end 11 | -------------------------------------------------------------------------------- /lib/rake/task_argument_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | 4 | # Error indicating an ill-formed task declaration. 5 | class TaskArgumentError < ArgumentError 6 | end 7 | 8 | end 9 | -------------------------------------------------------------------------------- /lib/rake/tasklib.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative "../rake" 3 | 4 | module Rake 5 | 6 | # Base class for Task Libraries. 7 | class TaskLib 8 | include Cloneable 9 | include Rake::DSL 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /lib/rake/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | VERSION = "13.3.1" 4 | 5 | module Version # :nodoc: all 6 | MAJOR, MINOR, BUILD, *OTHER = Rake::VERSION.split "." 7 | 8 | NUMBERS = [MAJOR, MINOR, BUILD, *OTHER] 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # dependencies for GitHub Actions 4 | - package-ecosystem: 'github-actions' 5 | directory: '/' 6 | schedule: 7 | interval: 'weekly' 8 | # dependencies for bundler 9 | - package-ecosystem: 'bundler' 10 | directory: '/' 11 | schedule: 12 | interval: 'weekly' 13 | -------------------------------------------------------------------------------- /lib/rake/default_loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | 4 | # Default Rakefile loader used by +import+. 5 | class DefaultLoader 6 | 7 | ## 8 | # Loads a rakefile into the current application from +fn+ 9 | 10 | def load(fn) 11 | Rake.load_rakefile(File.expand_path(fn)) 12 | end 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /lib/rake/late_time.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | # LateTime is a fake timestamp that occurs _after_ any other time value. 4 | class LateTime 5 | include Comparable 6 | include Singleton 7 | 8 | def <=>(other) 9 | 1 10 | end 11 | 12 | def to_s 13 | "" 14 | end 15 | end 16 | 17 | LATE = LateTime.instance 18 | end 19 | -------------------------------------------------------------------------------- /lib/rake/multi_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | 4 | # Same as a regular task, but the immediate prerequisites are done in 5 | # parallel using Ruby threads. 6 | # 7 | class MultiTask < Task 8 | private 9 | 10 | def invoke_prerequisites(task_args, invocation_chain) # :nodoc: 11 | invoke_prerequisites_concurrently(task_args, invocation_chain) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/rake/phony.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Defines a :phony task that you can use as a dependency. This allows 3 | # file-based tasks to use non-file-based tasks as prerequisites 4 | # without forcing them to rebuild. 5 | # 6 | # See FileTask#out_of_date? and Task#timestamp for more info. 7 | 8 | require_relative "../rake" 9 | 10 | task :phony 11 | 12 | Rake::Task[:phony].tap do |task| 13 | def task.timestamp # :nodoc: 14 | Time.at 0 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/rake/early_time.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | 4 | # EarlyTime is a fake timestamp that occurs _before_ any other time value. 5 | class EarlyTime 6 | include Comparable 7 | include Singleton 8 | 9 | ## 10 | # The EarlyTime always comes before +other+! 11 | 12 | def <=>(other) 13 | -1 14 | end 15 | 16 | def to_s # :nodoc: 17 | "" 18 | end 19 | end 20 | 21 | EARLY = EarlyTime.instance 22 | end 23 | -------------------------------------------------------------------------------- /lib/rake/private_reader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | 4 | # Include PrivateReader to use +private_reader+. 5 | module PrivateReader # :nodoc: all 6 | 7 | def self.included(base) 8 | base.extend(ClassMethods) 9 | end 10 | 11 | module ClassMethods 12 | 13 | # Declare a list of private accessors 14 | def private_reader(*names) 15 | attr_reader(*names) 16 | private(*names) 17 | end 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/rake/rule_recursion_overflow_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | 4 | # Error indicating a recursion overflow error in task selection. 5 | class RuleRecursionOverflowError < StandardError 6 | def initialize(*args) 7 | super 8 | @targets = [] 9 | end 10 | 11 | def add_target(target) 12 | @targets << target 13 | end 14 | 15 | def message 16 | super + ": [" + @targets.reverse.join(" => ") + "]" 17 | end 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /lib/rake/cloneable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | ## 4 | # Mixin for creating easily cloned objects. 5 | 6 | module Cloneable # :nodoc: 7 | # The hook that is invoked by 'clone' and 'dup' methods. 8 | def initialize_copy(source) 9 | super 10 | source.instance_variables.each do |var| 11 | src_value = source.instance_variable_get(var) 12 | value = src_value.clone rescue src_value 13 | instance_variable_set(var, value) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/test_rake_file_list_path_map.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeFileListPathMap < Rake::TestCase # :nodoc: 5 | def test_file_list_supports_pathmap 6 | assert_equal ["a", "b"], FileList["dir/a.rb", "dir/b.rb"].pathmap("%n") 7 | end 8 | 9 | def test_file_list_supports_pathmap_with_a_block 10 | mapped = FileList["dir/a.rb", "dir/b.rb"].pathmap("%{.*,*}n") do |name| 11 | name.upcase 12 | end 13 | assert_equal ["A", "B"], mapped 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/rake/pseudo_status.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | 4 | ## 5 | # Exit status class for times the system just gives us a nil. 6 | class PseudoStatus # :nodoc: all 7 | attr_reader :exitstatus 8 | 9 | def initialize(code=0) 10 | @exitstatus = code 11 | end 12 | 13 | def to_i 14 | @exitstatus << 8 15 | end 16 | 17 | def >>(n) 18 | to_i >> n 19 | end 20 | 21 | def stopped? 22 | false 23 | end 24 | 25 | def exited? 26 | true 27 | end 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /test/test_rake_late_time.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeLateTime < Rake::TestCase # :nodoc: 5 | def test_late_time_comparisons 6 | late = Rake::LATE 7 | assert_equal late, late 8 | assert late >= Time.now 9 | assert late > Time.now 10 | assert late != Time.now 11 | assert Time.now < late 12 | assert Time.now <= late 13 | assert Time.now != late 14 | end 15 | 16 | def test_to_s 17 | assert_equal "", Rake::LATE.to_s 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/rake/invocation_exception_mixin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | module InvocationExceptionMixin 4 | # Return the invocation chain (list of Rake tasks) that were in 5 | # effect when this exception was detected by rake. May be null if 6 | # no tasks were active. 7 | def chain 8 | @rake_invocation_chain ||= nil 9 | end 10 | 11 | # Set the invocation chain in effect when this exception was 12 | # detected. 13 | def chain=(value) 14 | @rake_invocation_chain = value 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: # added using https://github.com/step-security/secure-workflows 6 | contents: read 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | continue-on-error: true 12 | steps: 13 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 14 | - uses: ruby/setup-ruby@d697be2f83c6234b20877c3b5eac7a7f342f0d0c # v1.269.0 15 | with: 16 | ruby-version: '3.0' 17 | bundler-cache: true 18 | - name: Run rubocop 19 | run: bundle exec rubocop 20 | -------------------------------------------------------------------------------- /lib/rake/rake_test_loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "file_list" 4 | 5 | # Load the test files from the command line. 6 | argv = ARGV.select do |argument| 7 | case argument 8 | when /^-/ then 9 | argument 10 | when /\*/ then 11 | Rake::FileList[argument].to_a.each do |file| 12 | require File.expand_path file 13 | end 14 | 15 | false 16 | else 17 | path = File.expand_path argument 18 | 19 | abort "\nFile does not exist: #{path}\n\n" unless File.exist?(path) 20 | 21 | require path 22 | 23 | false 24 | end 25 | end 26 | 27 | ARGV.replace argv 28 | -------------------------------------------------------------------------------- /test/test_rake_path_map_partial.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakePathMapPartial < Rake::TestCase # :nodoc: 5 | def test_pathmap_partial 6 | @path = "1/2/file".dup 7 | def @path.call(n) 8 | pathmap_partial(n) 9 | end 10 | assert_equal("1", @path.call(1)) 11 | assert_equal("1/2", @path.call(2)) 12 | assert_equal("1/2", @path.call(3)) 13 | assert_equal(".", @path.call(0)) 14 | assert_equal("2", @path.call(-1)) 15 | assert_equal("1/2", @path.call(-2)) 16 | assert_equal("1/2", @path.call(-3)) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/test_rake_pseudo_status.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakePseudoStatus < Rake::TestCase # :nodoc: 5 | def test_with_zero_exit_status 6 | s = Rake::PseudoStatus.new 7 | assert_equal 0, s.exitstatus 8 | assert_equal 0, s.to_i 9 | assert_equal 0, s >> 8 10 | refute s.stopped? 11 | assert s.exited? 12 | end 13 | 14 | def test_with_99_exit_status 15 | s = Rake::PseudoStatus.new(99) 16 | assert_equal 99, s.exitstatus 17 | assert_equal 25344, s.to_i 18 | assert_equal 99, s >> 8 19 | refute s.stopped? 20 | assert s.exited? 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: # added using https://github.com/step-security/secure-workflows 6 | contents: read 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 13 | - uses: ruby/setup-ruby@d697be2f83c6234b20877c3b5eac7a7f342f0d0c # v1.269.0 14 | with: 15 | ruby-version: '3.0' 16 | - name: Install dependencies 17 | run: gem install test-unit coveralls 18 | - name: Run test 19 | env: 20 | COVERALLS: "yes" 21 | run: ruby -Ilib exe/rake 22 | -------------------------------------------------------------------------------- /lib/rake/trace_output.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | module TraceOutput # :nodoc: all 4 | 5 | # Write trace output to output stream +out+. 6 | # 7 | # The write is done as a single IO call (to print) to lessen the 8 | # chance that the trace output is interrupted by other tasks also 9 | # producing output. 10 | def trace_on(out, *strings) 11 | sep = $\ || "\n" 12 | if strings.empty? 13 | output = sep 14 | else 15 | output = strings.map { |s| 16 | next if s.nil? 17 | s.end_with?(sep) ? s : s + sep 18 | }.join 19 | end 20 | out.print(output) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/test_rake_reduce_compat.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeReduceCompat < Rake::TestCase # :nodoc: 5 | include RubyRunner 6 | 7 | def invoke_normal(task_name) 8 | rake task_name.to_s 9 | @out 10 | end 11 | 12 | def test_no_deprecated_dsl 13 | rakefile %q{ 14 | task :check_task do 15 | Module.new { p defined?(task) } 16 | end 17 | 18 | task :check_file do 19 | Module.new { p defined?(file) } 20 | end 21 | } 22 | 23 | assert_equal "nil", invoke_normal(:check_task).chomp 24 | assert_equal "nil", invoke_normal(:check_file).chomp 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /doc/example/Rakefile2: -------------------------------------------------------------------------------- 1 | # Example Rakefile -*- ruby -*- 2 | # Using the power of Ruby 3 | 4 | task :default => [:main] 5 | 6 | def ext(fn, newext) 7 | fn.sub(/\.[^.]+$/, newext) 8 | end 9 | 10 | SRCFILES = Dir['*.c'] 11 | OBJFILES = SRCFILES.collect { |fn| ext(fn,".o") } 12 | 13 | OBJFILES.each do |objfile| 14 | srcfile = ext(objfile, ".c") 15 | file objfile => [srcfile] do |t| 16 | sh "gcc #{srcfile} -c -o #{t.name}" 17 | end 18 | end 19 | 20 | file "main" => OBJFILES do |t| 21 | sh "gcc -o #{t.name} main.o a.o b.o" 22 | end 23 | 24 | task :clean do 25 | rm_f FileList['*.o'] 26 | Dir['*~'].each { |fn| rm_f fn } 27 | end 28 | 29 | task :clobber => [:clean] do 30 | rm_f "main" 31 | end 32 | 33 | task :run => ["main"] do 34 | sh "./main" 35 | end 36 | -------------------------------------------------------------------------------- /lib/rake/ext/core.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class Module 3 | # Check for an existing method in the current class before extending. If 4 | # the method already exists, then a warning is printed and the extension is 5 | # not added. Otherwise the block is yielded and any definitions in the 6 | # block will take effect. 7 | # 8 | # Usage: 9 | # 10 | # class String 11 | # rake_extension("xyz") do 12 | # def xyz 13 | # ... 14 | # end 15 | # end 16 | # end 17 | # 18 | def rake_extension(method) # :nodoc: 19 | if method_defined?(method) 20 | $stderr.puts "WARNING: Possible conflict with Rake extension: " + 21 | "#{self}##{method} already exists" 22 | else 23 | yield 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/rake/file_creation_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative "file_task" 3 | require_relative "early_time" 4 | 5 | module Rake 6 | 7 | # A FileCreationTask is a file task that when used as a dependency will be 8 | # needed if and only if the file has not been created. Once created, it is 9 | # not re-triggered if any of its dependencies are newer, nor does trigger 10 | # any rebuilds of tasks that depend on it whenever it is updated. 11 | # 12 | class FileCreationTask < FileTask 13 | # Is this file task needed? Yes if it doesn't exist. 14 | def needed? 15 | !File.exist?(name) 16 | end 17 | 18 | # Time stamp for file creation task. This time stamp is earlier 19 | # than any other time stamp. 20 | def timestamp 21 | Rake::EARLY 22 | end 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /doc/example/Rakefile1: -------------------------------------------------------------------------------- 1 | # Example Rakefile -*- ruby -*- 2 | 3 | task :default => [:main] 4 | 5 | file "a.o" => ["a.c"] do |t| 6 | src = t.name.sub(/\.o$/, '.c') 7 | sh "gcc #{src} -c -o #{t.name}" 8 | end 9 | 10 | file "b.o" => ["b.c"] do |t| 11 | src = t.name.sub(/\.o$/, '.c') 12 | sh "gcc #{src} -c -o #{t.name}" 13 | end 14 | 15 | file "main.o" => ["main.c"] do |t| 16 | src = t.name.sub(/\.o$/, '.c') 17 | sh "gcc #{src} -c -o #{t.name}" 18 | end 19 | 20 | OBJFILES = ["a.o", "b.o", "main.o"] 21 | task :obj => OBJFILES 22 | 23 | file "main" => OBJFILES do |t| 24 | sh "gcc -o #{t.name} main.o a.o b.o" 25 | end 26 | 27 | task :clean do 28 | rm_f FileList['*.o'] 29 | Dir['*~'].each { |fn| rm_f fn } 30 | end 31 | 32 | task :clobber => [:clean] do 33 | rm_f "main" 34 | end 35 | 36 | task :run => ["main"] do 37 | sh "./main" 38 | end 39 | -------------------------------------------------------------------------------- /lib/rake/name_space.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | ## 3 | # The NameSpace class will lookup task names in the scope defined by a 4 | # +namespace+ command. 5 | 6 | class Rake::NameSpace 7 | 8 | ## 9 | # Create a namespace lookup object using the given task manager 10 | # and the list of scopes. 11 | 12 | def initialize(task_manager, scope_list) 13 | @task_manager = task_manager 14 | @scope = scope_list.dup 15 | end 16 | 17 | ## 18 | # Lookup a task named +name+ in the namespace. 19 | 20 | def [](name) 21 | @task_manager.lookup(name, @scope) 22 | end 23 | 24 | ## 25 | # The scope of the namespace (a LinkedList) 26 | 27 | def scope 28 | @scope.dup 29 | end 30 | 31 | ## 32 | # Return the list of tasks defined in this and nested namespaces. 33 | 34 | def tasks 35 | @task_manager.tasks_in_scope(@scope) 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rubocop' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rubocop", "rubocop") 30 | -------------------------------------------------------------------------------- /test/test_rake_early_time.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeEarlyTime < Rake::TestCase # :nodoc: 5 | def test_create 6 | early = Rake::EarlyTime.instance 7 | assert early <= Time.now 8 | assert early < Time.now 9 | assert early != Time.now 10 | assert Time.now > early 11 | assert Time.now >= early 12 | assert Time.now != early 13 | end 14 | 15 | def test_equality 16 | early = Rake::EarlyTime.instance 17 | assert_equal early, early, "two early times should be equal" 18 | end 19 | 20 | def test_original_time_compare_is_not_messed_up 21 | t1 = Time.mktime(1970, 1, 1, 0, 0, 0) 22 | t2 = Time.now 23 | assert t1 < t2 24 | assert t2 > t1 25 | assert t1 == t1 26 | assert t2 == t2 27 | end 28 | 29 | def test_to_s 30 | assert_equal "", Rake::EARLY.to_s 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/test_private_reader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | require "rake/private_reader" 4 | 5 | class TestPrivateAttrs < Rake::TestCase # :nodoc: 6 | 7 | class Sample # :nodoc: 8 | include Rake::PrivateReader 9 | 10 | private_reader :reader, :a 11 | 12 | def initialize 13 | @reader = :RVALUE 14 | end 15 | 16 | def get_reader 17 | reader 18 | end 19 | 20 | end 21 | 22 | def setup 23 | super 24 | @sample = Sample.new 25 | end 26 | 27 | def test_private_reader_is_private 28 | assert_private do @sample.reader end 29 | assert_private do @sample.a end 30 | end 31 | 32 | def test_private_reader_returns_data 33 | assert_equal :RVALUE, @sample.get_reader 34 | end 35 | 36 | private 37 | 38 | def assert_private 39 | ex = assert_raises(NoMethodError) do yield end 40 | assert_match(/private/, ex.message) 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /test/test_rake_dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeDsl < Rake::TestCase # :nodoc: 5 | 6 | def setup 7 | super 8 | Rake::Task.clear 9 | end 10 | 11 | def test_namespace_command 12 | namespace "n" do 13 | task "t" 14 | end 15 | refute_nil Rake::Task["n:t"] 16 | end 17 | 18 | def test_namespace_command_with_bad_name 19 | ex = assert_raises(ArgumentError) do 20 | namespace 1 do end 21 | end 22 | assert_match(/string/i, ex.message) 23 | assert_match(/symbol/i, ex.message) 24 | end 25 | 26 | def test_namespace_command_with_a_string_like_object 27 | name = Object.new 28 | def name.to_str 29 | "bob" 30 | end 31 | namespace name do 32 | task "t" 33 | end 34 | refute_nil Rake::Task["bob:t"] 35 | end 36 | 37 | def test_no_commands_constant 38 | assert ! defined?(Commands), "should not define Commands" 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Rakefile for rake -*- ruby -*- 2 | 3 | # Copyright 2003, 2004, 2005 by Jim Weirich (jim@weirichhouse.org) 4 | # All rights reserved. 5 | 6 | # This file may be distributed under an MIT style license. See 7 | # MIT-LICENSE for details. 8 | 9 | lib = File.expand_path("../lib", __FILE__) 10 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 11 | 12 | begin 13 | old_verbose, $VERBOSE = $VERBOSE, nil 14 | require "bundler/gem_tasks" 15 | rescue LoadError 16 | ensure 17 | $VERBOSE = old_verbose 18 | end 19 | 20 | require "rake/testtask" 21 | Rake::TestTask.new(:test) do |t| 22 | t.libs << "test" 23 | t.verbose = true 24 | t.test_files = FileList["test/**/test_*.rb"] 25 | end 26 | 27 | require "rdoc/task" 28 | RDoc::Task.new do |doc| 29 | doc.main = "README.rdoc" 30 | doc.title = "Rake -- Ruby Make" 31 | doc.rdoc_files = FileList.new %w[lib MIT-LICENSE doc/**/*.rdoc *.rdoc] 32 | doc.rdoc_dir = "_site" # for github pages 33 | end 34 | 35 | task default: :test 36 | -------------------------------------------------------------------------------- /test/support/file_creation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module FileCreation 3 | OLDFILE = "old" 4 | NEWFILE = "new" 5 | 6 | def create_timed_files(oldfile, *newfiles) 7 | return if (File.exist?(oldfile) && 8 | newfiles.all? { |newfile| 9 | File.exist?(newfile) && File.stat(newfile).mtime > File.stat(oldfile).mtime 10 | }) 11 | now = Time.now 12 | 13 | create_file(oldfile, now - 60) 14 | 15 | newfiles.each do |newfile| 16 | create_file(newfile, now) 17 | end 18 | end 19 | 20 | def create_dir(dirname) 21 | FileUtils.mkdir_p(dirname) unless File.exist?(dirname) 22 | File.stat(dirname).mtime 23 | end 24 | 25 | def create_file(name, file_time=nil) 26 | create_dir(File.dirname(name)) 27 | FileUtils.touch(name) unless File.exist?(name) 28 | File.utime(file_time, file_time, name) unless file_time.nil? 29 | File.stat(name).mtime 30 | end 31 | 32 | def delete_file(name) 33 | File.delete(name) rescue nil 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/rake/backtrace.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | module Backtrace # :nodoc: all 4 | SYS_KEYS = RbConfig::CONFIG.keys.grep(/(?:[a-z]prefix|libdir)\z/) 5 | SYS_PATHS = RbConfig::CONFIG.values_at(*SYS_KEYS).uniq + 6 | [ File.join(File.dirname(__FILE__), "..") ] 7 | 8 | SUPPRESSED_PATHS = SYS_PATHS. 9 | map { |s| s.tr("\\", "/") }. 10 | map { |f| File.expand_path(f) }. 11 | reject { |s| s.nil? || s =~ /^ *$/ } 12 | SUPPRESSED_PATHS_RE = SUPPRESSED_PATHS.map { |f| Regexp.quote(f) }.join("|") 13 | SUPPRESSED_PATHS_RE << "|^" 14 | SUPPRESSED_PATHS_RE << "|^org\\/jruby\\/\\w+\\.java" if 15 | Object.const_defined?(:RUBY_ENGINE) and RUBY_ENGINE == "jruby" 16 | 17 | SUPPRESS_PATTERN = %r!(\A(#{SUPPRESSED_PATHS_RE})|bin/rake:\d+)!i 18 | 19 | def self.collapse(backtrace) 20 | pattern = Rake.application.options.suppress_backtrace_pattern || 21 | SUPPRESS_PATTERN 22 | backtrace.reject { |elem| elem =~ pattern } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/rake/scope.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | class Scope < LinkedList # :nodoc: all 4 | 5 | # Path for the scope. 6 | def path 7 | map(&:to_s).reverse.join(":") 8 | end 9 | 10 | # Path for the scope + the named path. 11 | def path_with_task_name(task_name) 12 | "#{path}:#{task_name}" 13 | end 14 | 15 | # Trim +n+ innermost scope levels from the scope. In no case will 16 | # this trim beyond the toplevel scope. 17 | def trim(n) 18 | result = self 19 | while n > 0 && !result.empty? 20 | result = result.tail 21 | n -= 1 22 | end 23 | result 24 | end 25 | 26 | # Scope lists always end with an EmptyScope object. See Null 27 | # Object Pattern) 28 | class EmptyScope < EmptyLinkedList 29 | @parent = Scope 30 | 31 | def path 32 | "" 33 | end 34 | 35 | def path_with_task_name(task_name) 36 | task_name 37 | end 38 | end 39 | 40 | # Singleton null object for an empty scope. 41 | EMPTY = EmptyScope.new 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Jim Weirich 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.4 3 | DisabledByDefault: true 4 | SuggestExtensions: false 5 | Exclude: 6 | - doc/**/*.rb 7 | - rake.gemspec 8 | - bin/* 9 | - vendor/**/* 10 | 11 | Style/HashSyntax: 12 | Enabled: true 13 | 14 | Style/StringLiterals: 15 | Enabled: true 16 | EnforcedStyle: double_quotes 17 | 18 | Style/MultilineIfThen: 19 | Enabled: true 20 | 21 | Style/MethodDefParentheses: 22 | Enabled: true 23 | 24 | Layout/LineLength: 25 | Enabled: true 26 | Max: 120 27 | 28 | Layout/IndentationWidth: 29 | Enabled: true 30 | 31 | Layout/IndentationStyle: 32 | Enabled: true 33 | 34 | Layout/EmptyLines: 35 | Enabled: true 36 | 37 | Layout/TrailingEmptyLines: 38 | Enabled: true 39 | 40 | Layout/TrailingWhitespace: 41 | Enabled: true 42 | 43 | Layout/SpaceBeforeBlockBraces: 44 | Enabled: true 45 | 46 | Layout/SpaceInsideBlockBraces: 47 | Enabled: true 48 | 49 | Layout/SpaceInsideHashLiteralBraces: 50 | Enabled: true 51 | 52 | Layout/CaseIndentation: 53 | Enabled: true 54 | 55 | Layout/EndAlignment: 56 | Enabled: true 57 | EnforcedStyleAlignWith: variable 58 | -------------------------------------------------------------------------------- /test/test_rake_require.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeRequire < Rake::TestCase # :nodoc: 5 | def setup 6 | super 7 | $LOAD_PATH.unshift "." if jruby17? 8 | end 9 | 10 | def test_can_load_rake_library 11 | rakefile_rakelib 12 | app = Rake::Application.new 13 | 14 | assert app.instance_eval { 15 | rake_require("test2", ["rakelib"], []) 16 | } 17 | end 18 | 19 | def test_wont_reload_rake_library 20 | rakefile_rakelib 21 | app = Rake::Application.new 22 | 23 | paths = ["rakelib"] 24 | loaded_files = [] 25 | app.rake_require("test2", paths, loaded_files) 26 | 27 | assert ! app.instance_eval { 28 | rake_require("test2", paths, loaded_files) 29 | } 30 | end 31 | 32 | def test_throws_error_if_library_not_found 33 | rakefile_rakelib 34 | 35 | app = Rake::Application.new 36 | ex = assert_raises(LoadError) { 37 | assert app.instance_eval { 38 | rake_require("testx", ["rakelib"], []) 39 | } 40 | } 41 | assert_match(/(can *not|can't)\s+find/i, ex.message) 42 | assert_match(/testx/, ex.message) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/support/ruby_runner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "open3" 4 | require "fileutils" 5 | 6 | module RubyRunner 7 | include FileUtils 8 | 9 | # Run a shell Ruby command with command line options (using the 10 | # default test options). Output is captured in @out and @err 11 | def ruby(*option_list) 12 | run_ruby(@ruby_options + option_list) 13 | end 14 | 15 | # Run a command line rake with the give rake options. Default 16 | # command line ruby options are included. Output is captured in 17 | # @out and @err 18 | def rake(*rake_options) 19 | run_ruby @ruby_options + [@rake_exec] + rake_options 20 | end 21 | 22 | # Low level ruby command runner ... 23 | def run_ruby(option_list) 24 | puts "COMMAND: [#{RUBY} #{option_list.join ' '}]" if @verbose 25 | 26 | Open3.popen3(RUBY, *option_list) do |inn, out, err, wait| 27 | inn.close 28 | 29 | @exit = wait ? wait.value : $? 30 | @out = out.read 31 | @err = err.read 32 | end 33 | 34 | puts "OUTPUT: [#{@out}]" if @verbose 35 | puts "ERROR: [#{@err}]" if @verbose 36 | puts "EXIT: [#{@exit.inspect}]" if @verbose 37 | puts "PWD: [#{Dir.pwd}]" if @verbose 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/test_rake.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRake < Rake::TestCase # :nodoc: 5 | def test_each_dir_parent 6 | assert_equal ["a"], alldirs("a") 7 | assert_equal ["a/b", "a"], alldirs("a/b") 8 | assert_equal ["/a/b", "/a", "/"], alldirs("/a/b") 9 | if File.dirname("c:/foo") == "c:" 10 | # Under Unix 11 | assert_equal ["c:/a/b", "c:/a", "c:"], alldirs("c:/a/b") 12 | assert_equal ["c:a/b", "c:a"], alldirs("c:a/b") 13 | else 14 | # Under Windows 15 | assert_equal ["c:/a/b", "c:/a", "c:/"], alldirs("c:/a/b") 16 | assert_equal ["c:a/b", "c:a"], alldirs("c:a/b") 17 | end 18 | end 19 | 20 | def alldirs(fn) 21 | result = [] 22 | Rake.each_dir_parent(fn) { |d| result << d } 23 | result 24 | end 25 | 26 | def test_can_override_application 27 | old_app = Rake.application 28 | fake_app = Object.new 29 | Rake.application = fake_app 30 | 31 | assert_equal fake_app, Rake.application 32 | 33 | ensure 34 | Rake.application = old_app 35 | end 36 | 37 | def test_original_dir_reports_current_dir 38 | assert_equal @tempdir, Rake.original_dir 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /test/test_rake_path_map_explode.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakePathMapExplode < Rake::TestCase # :nodoc: 5 | def setup 6 | super 7 | 8 | String.class_eval { public :pathmap_explode } 9 | end 10 | 11 | def teardown 12 | String.class_eval { protected :pathmap_explode } 13 | 14 | super 15 | end 16 | 17 | def test_explode 18 | assert_equal ["a"], "a".pathmap_explode 19 | assert_equal ["a", "b"], "a/b".pathmap_explode 20 | assert_equal ["a", "b", "c"], "a/b/c".pathmap_explode 21 | assert_equal ["/", "a"], "/a".pathmap_explode 22 | assert_equal ["/", "a", "b"], "/a/b".pathmap_explode 23 | assert_equal ["/", "a", "b", "c"], "/a/b/c".pathmap_explode 24 | 25 | if File::ALT_SEPARATOR 26 | assert_equal ["c:.", "a"], "c:a".pathmap_explode 27 | assert_equal ["c:.", "a", "b"], "c:a/b".pathmap_explode 28 | assert_equal ["c:.", "a", "b", "c"], "c:a/b/c".pathmap_explode 29 | assert_equal ["c:/", "a"], "c:/a".pathmap_explode 30 | assert_equal ["c:/", "a", "b"], "c:/a/b".pathmap_explode 31 | assert_equal ["c:/", "a", "b", "c"], "c:/a/b/c".pathmap_explode 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/test_rake_task_manager_argument_resolution.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeTaskManagerArgumentResolution < Rake::TestCase # :nodoc: 5 | 6 | def test_good_arg_patterns 7 | assert_equal [:t, [], [], nil], task(:t) 8 | assert_equal [:t, [], [:x], nil], task(t: :x) 9 | assert_equal [:t, [], [:x, :y], nil], task(t: [:x, :y]) 10 | 11 | assert_equal [:t, [], [], [:m]], task(:t, order_only: [:m]) 12 | assert_equal [:t, [], [:x, :y], [:m, :n]], task(t: [:x, :y], order_only: [:m, :n]) 13 | 14 | assert_equal [:t, [:a, :b], [], nil], task(:t, [:a, :b]) 15 | assert_equal [:t, [:a, :b], [:x], nil], task(:t, [:a, :b] => :x) 16 | assert_equal [:t, [:a, :b], [:x, :y], nil], task(:t, [:a, :b] => [:x, :y]) 17 | 18 | assert_equal [:t, [:a, :b], [], [:m]], task(:t, [:a, :b], order_only: [:m]) 19 | assert_equal [:t, [:a, :b], [:x], [:m]], task(:t, [:a, :b] => :x, order_only: [:m]) 20 | assert_equal [:t, [:a, :b], [:x, :y], [:m, :n]], task(:t, [:a, :b] => [:x, :y], order_only: [:m, :n]) 21 | end 22 | 23 | def task(*args) 24 | tm = Rake::TestCase::TaskManager.new 25 | tm.resolve_args(args) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /exe/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | #-- 4 | # Copyright (c) 2003, 2004, 2005, 2006, 2007 Jim Weirich 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to 8 | # deal in the Software without restriction, including without limitation the 9 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | # sell copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | # IN THE SOFTWARE. 23 | #++ 24 | 25 | require "rake" 26 | 27 | Rake.application.run 28 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository == 'ruby/rake' 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: rubygems.org 18 | url: https://rubygems.org/gems/rake 19 | 20 | permissions: 21 | contents: write 22 | id-token: write 23 | 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 27 | with: 28 | egress-policy: audit 29 | 30 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 31 | 32 | - name: Set up Ruby 33 | uses: ruby/setup-ruby@d697be2f83c6234b20877c3b5eac7a7f342f0d0c # v1.269.0 34 | with: 35 | bundler-cache: true 36 | ruby-version: "ruby" 37 | 38 | - name: Publish to RubyGems 39 | uses: rubygems/release-gem@1c162a739e8b4cb21a676e97b087e8268d8fc40b # v1.1.2 40 | 41 | - name: Create GitHub release 42 | run: | 43 | tag_name="$(git describe --tags --abbrev=0)" 44 | gh release create "${tag_name}" --verify-tag --generate-notes 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /test/test_rake_scope.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeScope < Rake::TestCase # :nodoc: 5 | include Rake 6 | 7 | def test_path_against_empty_scope 8 | scope = Scope.make 9 | assert_equal scope, Scope::EMPTY 10 | assert_equal scope.path, "" 11 | end 12 | 13 | def test_path_against_one_element 14 | scope = Scope.make(:one) 15 | assert_equal "one", scope.path 16 | end 17 | 18 | def test_path_against_two_elements 19 | scope = Scope.make(:inner, :outer) 20 | assert_equal "outer:inner", scope.path 21 | end 22 | 23 | def test_path_with_task_name 24 | scope = Scope.make(:inner, :outer) 25 | assert_equal "outer:inner:task", scope.path_with_task_name("task") 26 | end 27 | 28 | def test_path_with_task_name_against_empty_scope 29 | scope = Scope.make 30 | assert_equal "task", scope.path_with_task_name("task") 31 | end 32 | 33 | def test_conj_against_two_elements 34 | scope = Scope.make.conj("B").conj("A") 35 | assert_equal Scope.make("A", "B"), scope 36 | end 37 | 38 | def test_trim 39 | scope = Scope.make("A", "B") 40 | assert_equal scope, scope.trim(0) 41 | assert_equal scope.tail, scope.trim(1) 42 | assert_equal scope.tail.tail, scope.trim(2) 43 | assert_equal scope.tail.tail, scope.trim(3) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /.github/workflows/dependabot_automerge.yml: -------------------------------------------------------------------------------- 1 | # from https://github.com/gofiber/swagger/blob/main/.github/workflows/dependabot_automerge.yml 2 | name: Dependabot auto-merge 3 | on: 4 | pull_request: 5 | 6 | permissions: 7 | contents: write 8 | pull-requests: write 9 | 10 | jobs: 11 | automerge: 12 | runs-on: ubuntu-latest 13 | if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'ruby/rake' 14 | steps: 15 | - name: Dependabot metadata 16 | uses: dependabot/fetch-metadata@08eff52bf64351f401fb50d4972fa95b9f2c2d1b # v2.4.0 17 | id: metadata 18 | 19 | - name: Wait for status checks 20 | uses: lewagon/wait-on-check-action@3603e826ee561ea102b58accb5ea55a1a7482343 # v1.4.1 21 | with: 22 | repo-token: ${{ secrets.GITHUB_TOKEN }} 23 | ref: ${{ github.event.pull_request.head.sha || github.sha }} 24 | check-regexp: test* 25 | wait-interval: 30 26 | 27 | - name: Auto-merge for Dependabot PRs 28 | if: ${{ steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch'}} 29 | run: gh pr merge --auto --merge "$PR_URL" 30 | env: 31 | PR_URL: ${{ github.event.pull_request.html_url }} 32 | GH_TOKEN: ${{ secrets.MATZBOT_DEPENDABOT_MERGE_TOKEN }} 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.rdoc: -------------------------------------------------------------------------------- 1 | = Source Repository 2 | 3 | Rake is hosted at GitHub. The GitHub Web page is 4 | https://github.com/ruby/rake . The public Git clone URL is 5 | 6 | https://github.com/ruby/rake.git 7 | 8 | = Running the Rake Test Suite 9 | 10 | If you wish to run the unit and functional tests that come with Rake: 11 | 12 | * +cd+ into the top project directory of rake. 13 | * Install gem dependency using bundler: 14 | 15 | $ bin/setup # Install development dependencies 16 | 17 | * Run the test suite 18 | 19 | $ rake 20 | 21 | = RuboCop 22 | 23 | Rake uses RuboCop to enforce a consistent style on new changes being 24 | proposed. You can check your code with RuboCop using: 25 | 26 | $ bin/rubocop 27 | 28 | = Issues and Bug Reports 29 | 30 | Feel free to submit commits or feature requests. If you send a patch, 31 | remember to update the corresponding unit tests. In fact, the team prefers 32 | a new feature to be submitted in the form of new unit tests. 33 | 34 | For other information, feel free to ask on the ruby-talk mailing list. 35 | 36 | If you have found a bug in rake, please try with the latest version of rake 37 | before filing an issue. Also check History.rdoc for bug fixes that may have 38 | addressed your issue. 39 | 40 | When submitting pull requests, please check the status checks on your PR page 41 | to confirm if it says "All checks have passed". 42 | -------------------------------------------------------------------------------- /lib/rake/thread_history_display.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative "private_reader" 3 | 4 | module Rake 5 | 6 | class ThreadHistoryDisplay # :nodoc: all 7 | include Rake::PrivateReader 8 | 9 | private_reader :stats, :items, :threads 10 | 11 | def initialize(stats) 12 | @stats = stats 13 | @items = { _seq_: 1 } 14 | @threads = { _seq_: "A" } 15 | end 16 | 17 | def show 18 | puts "Job History:" 19 | stats.each do |stat| 20 | stat[:data] ||= {} 21 | rename(stat, :thread, threads) 22 | rename(stat[:data], :item_id, items) 23 | rename(stat[:data], :new_thread, threads) 24 | rename(stat[:data], :deleted_thread, threads) 25 | printf("%8d %2s %-20s %s\n", 26 | (stat[:time] * 1_000_000).round, 27 | stat[:thread], 28 | stat[:event], 29 | stat[:data].map do |k, v| "#{k}:#{v}" end.join(" ")) 30 | end 31 | end 32 | 33 | private 34 | 35 | def rename(hash, key, renames) 36 | if hash && hash[key] 37 | original = hash[key] 38 | value = renames[original] 39 | unless value 40 | value = renames[:_seq_] 41 | renames[:_seq_] = renames[:_seq_].succ 42 | renames[original] = value 43 | end 44 | hash[key] = value 45 | end 46 | end 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /test/test_rake_makefile_loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | require "rake/loaders/makefile" 4 | 5 | class TestRakeMakefileLoader < Rake::TestCase # :nodoc: 6 | include Rake 7 | 8 | def test_parse 9 | Dir.chdir @tempdir 10 | 11 | File.write "sample.mf", <<~'SAMPLE_MF' 12 | # Comments 13 | a: a1 a2 a3 a4 14 | b: b1 b2 b3 \ 15 | b4 b5 b6\ 16 | # Mid: Comment 17 | b7 18 | 19 | a : a5 a6 a7 20 | c: c1 21 | d: d1 d2 \ 22 | 23 | e f : e1 f1 24 | 25 | g\ 0: g1 g\ 2 g\ 3 g4 26 | SAMPLE_MF 27 | 28 | Task.clear 29 | loader = Rake::MakefileLoader.new 30 | loader.load "sample.mf" 31 | %w(a b c d).each do |t| 32 | assert Task.task_defined?(t), "#{t} should be a defined task" 33 | end 34 | assert_equal %w(a1 a2 a3 a4 a5 a6 a7).sort, Task["a"].prerequisites.sort 35 | assert_equal %w(b1 b2 b3 b4 b5 b6 b7).sort, Task["b"].prerequisites.sort 36 | assert_equal %w(c1).sort, Task["c"].prerequisites.sort 37 | assert_equal %w(d1 d2).sort, Task["d"].prerequisites.sort 38 | assert_equal %w(e1 f1).sort, Task["e"].prerequisites.sort 39 | assert_equal %w(e1 f1).sort, Task["f"].prerequisites.sort 40 | assert_equal( 41 | ["g1", "g 2", "g 3", "g4"].sort, 42 | Task["g 0"].prerequisites.sort) 43 | assert_equal 7, Task.tasks.size 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy RDoc site to Pages 2 | 3 | on: 4 | push: 5 | branches: [ 'master' ] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: "pages" 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 23 | - name: Setup Ruby 24 | uses: ruby/setup-ruby@d697be2f83c6234b20877c3b5eac7a7f342f0d0c # v1.269.0 25 | with: 26 | ruby-version: '3.2' 27 | bundler-cache: true 28 | - name: Setup Pages 29 | id: pages 30 | uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 31 | - name: Build with RDoc 32 | # Outputs to the './_site' directory by default 33 | run: bundle exec rake rdoc 34 | - name: Upload artifact 35 | uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 36 | 37 | deploy: 38 | environment: 39 | name: github-pages 40 | url: ${{ steps.deployment.outputs.page_url }} 41 | runs-on: ubuntu-latest 42 | needs: build 43 | steps: 44 | - name: Deploy to GitHub Pages 45 | id: deployment 46 | uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 47 | -------------------------------------------------------------------------------- /test/test_rake_name_space.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeNameSpace < Rake::TestCase # :nodoc: 5 | 6 | class TM # :nodoc: 7 | include Rake::TaskManager 8 | end 9 | 10 | def test_namespace_creation 11 | mgr = TM.new 12 | ns = Rake::NameSpace.new(mgr, []) 13 | refute_nil ns 14 | end 15 | 16 | def test_namespace_lookup 17 | mgr = TM.new 18 | ns = mgr.in_namespace("n") do 19 | mgr.define_task(Rake::Task, "t") 20 | end 21 | 22 | refute_nil ns["t"] 23 | assert_equal mgr["n:t"], ns["t"] 24 | end 25 | 26 | def test_namespace_reports_tasks_it_owns 27 | mgr = TM.new 28 | nns = nil 29 | ns = mgr.in_namespace("n") do 30 | mgr.define_task(Rake::Task, :x) 31 | mgr.define_task(Rake::Task, :y) 32 | nns = mgr.in_namespace("nn") do 33 | mgr.define_task(Rake::Task, :z) 34 | end 35 | end 36 | mgr.in_namespace("m") do 37 | mgr.define_task(Rake::Task, :x) 38 | end 39 | 40 | assert_equal ["n:nn:z", "n:x", "n:y"], 41 | ns.tasks.map(&:name) 42 | assert_equal ["n:nn:z"], nns.tasks.map(&:name) 43 | end 44 | 45 | def test_scope 46 | mgr = TM.new 47 | 48 | scope = Rake::LinkedList.new "b" 49 | scope = scope.conj "a" 50 | 51 | ns = Rake::NameSpace.new mgr, scope 52 | 53 | assert_equal scope, ns.scope 54 | 55 | refute_same scope, ns.scope 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /lib/rake/invocation_chain.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | 4 | # InvocationChain tracks the chain of task invocations to detect 5 | # circular dependencies. 6 | class InvocationChain < LinkedList 7 | 8 | # Is the invocation already in the chain? 9 | def member?(invocation) 10 | head == invocation || tail.member?(invocation) 11 | end 12 | 13 | # Append an invocation to the chain of invocations. It is an error 14 | # if the invocation already listed. 15 | def append(invocation) 16 | if member?(invocation) 17 | fail RuntimeError, "Circular dependency detected: #{to_s} => #{invocation}" 18 | end 19 | conj(invocation) 20 | end 21 | 22 | # Convert to string, ie: TOP => invocation => invocation 23 | def to_s 24 | "#{prefix}#{head}" 25 | end 26 | 27 | # Class level append. 28 | def self.append(invocation, chain) 29 | chain.append(invocation) 30 | end 31 | 32 | private 33 | 34 | def prefix 35 | "#{tail} => " 36 | end 37 | 38 | # Null object for an empty chain. 39 | class EmptyInvocationChain < LinkedList::EmptyLinkedList 40 | @parent = InvocationChain 41 | 42 | def member?(obj) 43 | false 44 | end 45 | 46 | def append(invocation) 47 | conj(invocation) 48 | end 49 | 50 | def to_s 51 | "TOP" 52 | end 53 | end 54 | 55 | EMPTY = EmptyInvocationChain.new 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: # added using https://github.com/step-security/secure-workflows 6 | contents: read 7 | 8 | jobs: 9 | ruby-versions: 10 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 11 | with: 12 | min_version: 2.3 13 | engine: cruby-jruby 14 | versions: '["truffleruby"]' 15 | 16 | test: 17 | needs: ruby-versions 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | os: [ 'ubuntu-latest', 'macos-latest', 'windows-latest' ] 22 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 23 | exclude: 24 | - os: macos-latest 25 | ruby: 2.3 26 | - os: macos-latest 27 | ruby: 2.4 28 | - os: macos-latest 29 | ruby: 2.5 30 | - os: windows-latest 31 | ruby: 2.3 32 | - os: windows-latest 33 | ruby: truffleruby 34 | - os: windows-latest 35 | ruby: jruby-head 36 | - os: windows-latest 37 | ruby: jruby 38 | steps: 39 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 40 | - uses: ruby/setup-ruby@d697be2f83c6234b20877c3b5eac7a7f342f0d0c # v1.269.0 41 | with: 42 | ruby-version: ${{ matrix.ruby }} 43 | - name: Install dependencies 44 | run: gem install test-unit 45 | - name: Run test 46 | run: ruby -Ilib exe/rake 47 | -------------------------------------------------------------------------------- /test/test_rake_extension.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | require "stringio" 4 | 5 | class TestRakeExtension < Rake::TestCase # :nodoc: 6 | 7 | module Redirect # :nodoc: 8 | def error_redirect 9 | old_err = $stderr 10 | result = StringIO.new 11 | $stderr = result 12 | yield 13 | result 14 | ensure 15 | $stderr = old_err 16 | end 17 | end 18 | 19 | class Sample # :nodoc: 20 | extend Redirect 21 | 22 | def duplicate_method 23 | :original 24 | end 25 | 26 | OK_ERRS = error_redirect do 27 | rake_extension("a") do 28 | def ok_method 29 | end 30 | end 31 | end 32 | 33 | DUP_ERRS = error_redirect do 34 | rake_extension("duplicate_method") do 35 | def duplicate_method 36 | :override 37 | end 38 | end 39 | end 40 | end 41 | 42 | def test_methods_actually_exist 43 | sample = Sample.new 44 | sample.ok_method 45 | sample.duplicate_method 46 | end 47 | 48 | def test_no_warning_when_defining_ok_method 49 | assert_equal "", Sample::OK_ERRS.string 50 | end 51 | 52 | def test_extension_complains_when_a_method_that_is_present 53 | assert_match(/warning:/i, Sample::DUP_ERRS.string) 54 | assert_match(/already exists/i, Sample::DUP_ERRS.string) 55 | assert_match(/duplicate_method/i, Sample::DUP_ERRS.string) 56 | assert_equal :original, Sample.new.duplicate_method 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /test/test_trace_output.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestTraceOutput < Rake::TestCase # :nodoc: 5 | include Rake::TraceOutput 6 | 7 | class PrintSpy # :nodoc: 8 | attr_reader :result, :calls 9 | 10 | def initialize 11 | @result = "".dup 12 | @calls = 0 13 | end 14 | 15 | def print(string) 16 | @result << string 17 | @calls += 1 18 | end 19 | end 20 | 21 | def test_trace_issues_single_io_for_args_with_empty_args 22 | spy = PrintSpy.new 23 | trace_on(spy) 24 | assert_equal "\n", spy.result 25 | assert_equal 1, spy.calls 26 | end 27 | 28 | def test_trace_issues_single_io_for_args_multiple_strings 29 | spy = PrintSpy.new 30 | trace_on(spy, "HI\n", "LO") 31 | assert_equal "HI\nLO\n", spy.result 32 | assert_equal 1, spy.calls 33 | end 34 | 35 | def test_trace_handles_nil_objects 36 | spy = PrintSpy.new 37 | trace_on(spy, "HI\n", nil, "LO") 38 | assert_equal "HI\nLO\n", spy.result 39 | assert_equal 1, spy.calls 40 | end 41 | 42 | def test_trace_issues_single_io_for_args_multiple_strings_and_alternate_sep 43 | verbose, $VERBOSE = $VERBOSE, nil 44 | old_sep = $\ 45 | $\ = "\r" 46 | $VERBOSE = verbose 47 | spy = PrintSpy.new 48 | trace_on(spy, "HI\r", "LO") 49 | assert_equal "HI\rLO\r", spy.result 50 | assert_equal 1, spy.calls 51 | ensure 52 | $VERBOSE = nil 53 | $\ = old_sep 54 | $VERBOSE = verbose 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/rake/loaders/makefile.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | 4 | # Makefile loader to be used with the import file loader. Use this to 5 | # import dependencies from make dependency tools: 6 | # 7 | # require 'rake/loaders/makefile' 8 | # 9 | # file ".depends.mf" => [SRC_LIST] do |t| 10 | # sh "makedepend -f- -- #{CFLAGS} -- #{t.prerequisites} > #{t.name}" 11 | # end 12 | # 13 | # import ".depends.mf" 14 | # 15 | # See {Importing Dependencies}[link:doc/rakefile_rdoc.html#label-Importing+Dependencies] 16 | # for further details. 17 | 18 | class MakefileLoader 19 | include Rake::DSL 20 | 21 | SPACE_MARK = "\0" # :nodoc: 22 | 23 | # Load the makefile dependencies in +fn+. 24 | def load(fn) # :nodoc: 25 | lines = File.read fn 26 | lines.gsub!(/\\ /, SPACE_MARK) 27 | lines.gsub!(/#[^\n]*\n/m, "") 28 | lines.gsub!(/\\\n/, " ") 29 | lines.each_line do |line| 30 | process_line(line) 31 | end 32 | end 33 | 34 | private 35 | 36 | # Process one logical line of makefile data. 37 | def process_line(line) # :nodoc: 38 | file_tasks, args = line.split(":", 2) 39 | return if args.nil? 40 | dependents = args.split.map { |d| respace(d) } 41 | file_tasks.scan(/\S+/) do |file_task| 42 | file_task = respace(file_task) 43 | file file_task => dependents 44 | end 45 | end 46 | 47 | def respace(str) # :nodoc: 48 | str.tr SPACE_MARK, " " 49 | end 50 | end 51 | 52 | # Install the handler 53 | Rake.application.add_loader("mf", MakefileLoader.new) 54 | end 55 | -------------------------------------------------------------------------------- /test/test_rake_top_level_functions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeTopLevelFunctions < Rake::TestCase # :nodoc: 5 | 6 | def setup 7 | super 8 | 9 | @app = Object.new 10 | 11 | def @app.called 12 | @called 13 | end 14 | 15 | def @app.method_missing(*a, &b) 16 | @called ||= [] 17 | @called << [a, b] 18 | nil 19 | end 20 | 21 | Rake.application = @app 22 | end 23 | 24 | def test_namespace 25 | block = proc do end 26 | 27 | namespace("xyz", &block) 28 | 29 | expected = [ 30 | [[:in_namespace, "xyz"], block] 31 | ] 32 | 33 | assert_equal expected, @app.called 34 | end 35 | 36 | def test_import 37 | import("x", "y", "z") 38 | 39 | expected = [ 40 | [[:add_import, "x"], nil], 41 | [[:add_import, "y"], nil], 42 | [[:add_import, "z"], nil], 43 | ] 44 | 45 | assert_equal expected, @app.called 46 | end 47 | 48 | def test_when_writing 49 | out, = capture_output do 50 | when_writing("NOTWRITING") do 51 | puts "WRITING" 52 | end 53 | end 54 | assert_equal "WRITING\n", out 55 | end 56 | 57 | def test_when_not_writing 58 | Rake::FileUtilsExt.nowrite_flag = true 59 | _, err = capture_output do 60 | when_writing("NOTWRITING") do 61 | puts "WRITING" 62 | end 63 | end 64 | assert_equal "DRYRUN: NOTWRITING\n", err 65 | ensure 66 | Rake::FileUtilsExt.nowrite_flag = false 67 | end 68 | 69 | def test_missing_other_constant 70 | assert_raises(NameError) do Object.const_missing(:Xyz) end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/rake/win32.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "rbconfig" 3 | 4 | module Rake 5 | # Win 32 interface methods for Rake. Windows specific functionality 6 | # will be placed here to collect that knowledge in one spot. 7 | module Win32 # :nodoc: all 8 | 9 | # Error indicating a problem in locating the home directory on a 10 | # Win32 system. 11 | class Win32HomeError < RuntimeError 12 | end 13 | 14 | class << self 15 | # True if running on a windows system. 16 | def windows? 17 | RbConfig::CONFIG["host_os"] =~ %r!(msdos|mswin|djgpp|mingw|[Ww]indows)! 18 | end 19 | 20 | # The standard directory containing system wide rake files on 21 | # Win 32 systems. Try the following environment variables (in 22 | # order): 23 | # 24 | # * HOME 25 | # * HOMEDRIVE + HOMEPATH 26 | # * APPDATA 27 | # * USERPROFILE 28 | # 29 | # If the above are not defined, the return nil. 30 | def win32_system_dir #:nodoc: 31 | win32_shared_path = ENV["HOME"] 32 | if win32_shared_path.nil? && ENV["HOMEDRIVE"] && ENV["HOMEPATH"] 33 | win32_shared_path = ENV["HOMEDRIVE"] + ENV["HOMEPATH"] 34 | end 35 | 36 | win32_shared_path ||= ENV["APPDATA"] 37 | win32_shared_path ||= ENV["USERPROFILE"] 38 | raise Win32HomeError, 39 | "Unable to determine home path environment variable." if 40 | win32_shared_path.nil? or win32_shared_path.empty? 41 | normalize(File.join(win32_shared_path, "Rake")) 42 | end 43 | 44 | # Normalize a win32 path so that the slashes are all forward slashes. 45 | def normalize(path) 46 | path.gsub(/\\/, "/") 47 | end 48 | 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/test_rake_cpu_counter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeCpuCounter < Rake::TestCase # :nodoc: 5 | 6 | def setup 7 | super 8 | 9 | @cpu_counter = Rake::CpuCounter.new 10 | end 11 | 12 | def test_count 13 | num = @cpu_counter.count 14 | omit "cannot count CPU" if num == nil 15 | assert_kind_of Numeric, num 16 | assert_operator num, :>=, 1 17 | end 18 | 19 | def test_count_with_default_nil 20 | def @cpu_counter.count; nil; end 21 | assert_equal(8, @cpu_counter.count_with_default(8)) 22 | assert_equal(4, @cpu_counter.count_with_default) 23 | end 24 | 25 | def test_count_with_default_raise 26 | def @cpu_counter.count; raise; end 27 | assert_equal(8, @cpu_counter.count_with_default(8)) 28 | assert_equal(4, @cpu_counter.count_with_default) 29 | end 30 | 31 | class TestClassMethod < Rake::TestCase # :nodoc: 32 | def setup 33 | super 34 | 35 | @klass = Class.new(Rake::CpuCounter) 36 | end 37 | 38 | def test_count 39 | @klass.class_eval do 40 | def count; 8; end 41 | end 42 | assert_equal(8, @klass.count) 43 | end 44 | 45 | def test_count_nil 46 | counted = false 47 | @klass.class_eval do 48 | define_method(:count) do 49 | counted = true 50 | nil 51 | end 52 | end 53 | assert_equal(4, @klass.count) 54 | assert_equal(true, counted) 55 | end 56 | 57 | def test_count_raise 58 | counted = false 59 | @klass.class_eval do 60 | define_method(:count) do 61 | counted = true 62 | raise 63 | end 64 | end 65 | assert_equal(4, @klass.count) 66 | assert_equal(true, counted) 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/rake/rake_module.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "rake/application" 3 | 4 | module Rake 5 | 6 | class << self 7 | # Current Rake Application 8 | def application 9 | @application ||= Rake::Application.new 10 | end 11 | 12 | # Set the current Rake application object. 13 | def application=(app) 14 | @application = app 15 | end 16 | 17 | def suggested_thread_count # :nodoc: 18 | @cpu_count ||= Rake::CpuCounter.count 19 | @cpu_count + 4 20 | end 21 | 22 | # Return the original directory where the Rake application was started. 23 | def original_dir 24 | application.original_dir 25 | end 26 | 27 | # Load a rakefile. 28 | def load_rakefile(path) 29 | load(path) 30 | end 31 | 32 | # Add files to the rakelib list 33 | def add_rakelib(*files) 34 | application.options.rakelib ||= [] 35 | application.options.rakelib.concat(files) 36 | end 37 | 38 | # Make +block_application+ the default rake application inside a block so 39 | # you can load rakefiles into a different application. 40 | # 41 | # This is useful when you want to run rake tasks inside a library without 42 | # running rake in a sub-shell. 43 | # 44 | # Example: 45 | # 46 | # Dir.chdir 'other/directory' 47 | # 48 | # other_rake = Rake.with_application do |rake| 49 | # rake.load_rakefile 50 | # end 51 | # 52 | # puts other_rake.tasks 53 | 54 | def with_application(block_application = Rake::Application.new) 55 | orig_application = Rake.application 56 | 57 | Rake.application = block_application 58 | 59 | yield block_application 60 | 61 | block_application 62 | ensure 63 | Rake.application = orig_application 64 | end 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /lib/rake/file_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative "task" 3 | require_relative "early_time" 4 | 5 | module Rake 6 | 7 | # A FileTask is a task that includes time based dependencies. If any of a 8 | # FileTask's prerequisites have a timestamp that is later than the file 9 | # represented by this task, then the file must be rebuilt (using the 10 | # supplied actions). 11 | # 12 | class FileTask < Task 13 | 14 | # Is this file task needed? Yes if it doesn't exist, or if its time stamp 15 | # is out of date. 16 | def needed? 17 | begin 18 | out_of_date?(File.mtime(name)) || @application.options.build_all 19 | rescue Errno::ENOENT 20 | true 21 | end 22 | end 23 | 24 | # Time stamp for file task. 25 | def timestamp 26 | begin 27 | File.mtime(name) 28 | rescue Errno::ENOENT 29 | Rake::LATE 30 | end 31 | end 32 | 33 | private 34 | 35 | # Are there any prerequisites with a later time than the given time stamp? 36 | def out_of_date?(stamp) 37 | all_prerequisite_tasks.any? { |prereq| 38 | prereq_task = application[prereq, @scope] 39 | if prereq_task.instance_of?(Rake::FileTask) 40 | prereq_task.timestamp > stamp || @application.options.build_all 41 | else 42 | prereq_task.timestamp > stamp 43 | end 44 | } 45 | end 46 | 47 | # ---------------------------------------------------------------- 48 | # Task class methods. 49 | # 50 | class << self 51 | # Apply the scope to the task name according to the rules for this kind 52 | # of task. File based tasks ignore the scope when creating the name. 53 | def scope_name(scope, task_name) 54 | Rake.from_pathname(task_name) 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/test_rake_invocation_chain.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeInvocationChain < Rake::TestCase # :nodoc: 5 | include Rake 6 | 7 | def setup 8 | super 9 | 10 | @empty = InvocationChain.empty 11 | 12 | @first_member = "A" 13 | @second_member = "B" 14 | @one = @empty.append(@first_member) 15 | @two = @one.append(@second_member) 16 | end 17 | 18 | def test_conj_on_invocation_chains 19 | list = InvocationChain.empty.conj("B").conj("A") 20 | assert_equal InvocationChain.make("A", "B"), list 21 | assert_equal InvocationChain, list.class 22 | end 23 | 24 | def test_make_on_invocation_chains 25 | assert_equal @empty, InvocationChain.make() 26 | assert_equal @one, InvocationChain.make(@first_member) 27 | assert_equal @two, InvocationChain.make(@second_member, @first_member) 28 | end 29 | 30 | def test_append_with_one_argument 31 | chain = @empty.append("A") 32 | 33 | assert_equal "TOP => A", chain.to_s # HACK 34 | end 35 | 36 | def test_append_one_circular 37 | ex = assert_raises RuntimeError do 38 | @one.append(@first_member) 39 | end 40 | assert_match(/circular +dependency/i, ex.message) 41 | assert_match(/A.*=>.*A/, ex.message) 42 | end 43 | 44 | def test_append_two_circular 45 | ex = assert_raises RuntimeError do 46 | @two.append(@first_member) 47 | end 48 | assert_match(/A.*=>.*B.*=>.*A/, ex.message) 49 | end 50 | 51 | def test_member_eh_one 52 | assert @one.member?(@first_member) 53 | end 54 | 55 | def test_member_eh_two 56 | assert @two.member?(@first_member) 57 | assert @two.member?(@second_member) 58 | end 59 | 60 | def test_to_s_empty 61 | assert_equal "TOP", @empty.to_s 62 | assert_equal "TOP => A", @one.to_s 63 | end 64 | 65 | end 66 | -------------------------------------------------------------------------------- /doc/glossary.rdoc: -------------------------------------------------------------------------------- 1 | = Glossary 2 | 3 | action :: 4 | Code to be executed in order to perform a task. Actions in a Rakefile are 5 | specified in a code block. (Usually delimited by +do+/+end+ pairs.) 6 | 7 | execute :: 8 | When a task is executed, all of its actions are performed in the order they 9 | were defined. Note that, unlike invoke, execute always 10 | executes the actions (without invoking or executing the prerequisites). 11 | 12 | file task (Rake::FileTask) :: 13 | A file task is a task whose purpose is to create a file (which has the same 14 | name as the task). When invoked, a file task will only execute if one or 15 | more of the following conditions are true. 16 | 17 | 1. The associated file does not exist. 18 | 2. A prerequisite has a later time stamp than the existing file. 19 | 20 | Because normal Tasks always have the current time as timestamp, a FileTask 21 | that has a normal Task prerequisite will always execute. 22 | 23 | invoke :: 24 | When a task is invoked, first we check to see if it has been invoked before. 25 | If it has been, then nothing else is done. If this is the first time it has 26 | been invoked, then we invoke each of its prerequisites. Finally, we check 27 | to see if we need to execute the actions of this task by calling 28 | Rake::Task#needed?. If the task is needed, we execute its actions. 29 | 30 | NOTE: Prerequisites are still invoked even if the task is not needed. 31 | 32 | prerequisites :: 33 | Every task has a (possibly empty) set of prerequisites. A prerequisite P to 34 | Task T is itself a task that must be invoked before Task T. 35 | 36 | rule :: 37 | A rule is a recipe for synthesizing a task when no task is explicitly 38 | defined. Rules generally synthesize file tasks. 39 | 40 | task (Rake::Task) :: 41 | The basic unit of work in a Rakefile. A task has a name, a set of 42 | prerequisites, and a list of actions to be performed. 43 | -------------------------------------------------------------------------------- /test/test_rake_file_creation_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | require "fileutils" 4 | 5 | class TestRakeFileCreationTask < Rake::TestCase # :nodoc: 6 | include Rake 7 | 8 | DUMMY_DIR = "dummy_dir" 9 | 10 | def setup 11 | super 12 | 13 | Task.clear 14 | end 15 | 16 | def test_file_needed 17 | create_dir DUMMY_DIR 18 | fc_task = Task[DUMMY_DIR] 19 | assert_equal DUMMY_DIR, fc_task.name 20 | FileUtils.rm_rf fc_task.name 21 | assert fc_task.needed?, "file should be needed" 22 | FileUtils.mkdir fc_task.name 23 | assert_nil fc_task.prerequisites.map { |n| Task[n].timestamp }.max 24 | assert ! fc_task.needed?, "file should not be needed" 25 | end 26 | 27 | def test_directory 28 | directory DUMMY_DIR 29 | fc_task = Task[DUMMY_DIR] 30 | assert_equal DUMMY_DIR, fc_task.name 31 | assert FileCreationTask === fc_task 32 | end 33 | 34 | def test_no_retriggers_on_filecreate_task 35 | create_timed_files(OLDFILE, NEWFILE) 36 | t1 = Rake.application.intern(FileCreationTask, OLDFILE).enhance([NEWFILE]) 37 | t2 = Rake.application.intern(FileCreationTask, NEWFILE) 38 | assert ! t2.needed?, "Should not need to build new file" 39 | assert ! t1.needed?, "Should not need to rebuild old file because of new" 40 | end 41 | 42 | def test_no_retriggers_on_file_task 43 | create_timed_files(OLDFILE, NEWFILE) 44 | t1 = Rake.application.intern(FileCreationTask, OLDFILE).enhance([NEWFILE]) 45 | t2 = Rake.application.intern(FileCreationTask, NEWFILE) 46 | assert ! t2.needed?, "Should not need to build new file" 47 | assert ! t1.needed?, "Should not need to rebuild old file because of new" 48 | end 49 | 50 | def test_very_early_timestamp 51 | t1 = Rake.application.intern(FileCreationTask, OLDFILE) 52 | assert t1.timestamp < Time.now 53 | assert t1.timestamp < Time.now - 1_000_000 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/test_rake_rake_test_loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeRakeTestLoader < Rake::TestCase # :nodoc: 5 | 6 | def setup 7 | super 8 | 9 | @loader = File.join @rake_lib, "rake/rake_test_loader.rb" 10 | end 11 | 12 | def test_pattern 13 | orig_loaded_features = $:.dup 14 | FileUtils.touch "foo.rb" 15 | FileUtils.touch "test_a.rb" 16 | FileUtils.touch "test_b.rb" 17 | 18 | ARGV.replace %w[foo.rb test_*.rb -v] 19 | 20 | load @loader 21 | 22 | assert_equal %w[-v], ARGV 23 | ensure 24 | $:.replace orig_loaded_features 25 | end 26 | 27 | def test_load_error_from_missing_test_file 28 | out, err = capture_output do 29 | ARGV.replace %w[no_such_test_file.rb] 30 | 31 | assert_raises SystemExit do 32 | load @loader 33 | end 34 | end 35 | 36 | assert_empty out 37 | 38 | no_such_path = File.join @tempdir, "no_such_test_file" 39 | 40 | expected = 41 | /\A\n 42 | File\ does\ not\ exist:\ #{no_such_path}(\.rb)? # JRuby is different 43 | \n\n\Z/x 44 | 45 | assert_match expected, err 46 | end 47 | 48 | def test_load_error_raised_implicitly 49 | File.write("error_test.rb", "require 'superkalifragilisticoespialidoso'") 50 | out, err = capture_output do 51 | ARGV.replace %w[error_test.rb] 52 | 53 | exc = assert_raises(LoadError) do 54 | load @loader 55 | end 56 | 57 | assert_match(/.* -- superkalifragilisticoespialidoso/, exc.message) 58 | end 59 | assert_empty out 60 | assert_empty err 61 | end 62 | 63 | def test_load_error_raised_explicitly 64 | File.write("error_test.rb", "raise LoadError, 'explicitly raised'") 65 | out, err = capture_output do 66 | ARGV.replace %w[error_test.rb] 67 | 68 | exc = assert_raises(LoadError) do 69 | load @loader 70 | end 71 | assert_equal "explicitly raised", exc.message 72 | end 73 | assert_empty out 74 | assert_empty err 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/test_rake_win32.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeWin32 < Rake::TestCase # :nodoc: 5 | 6 | Win32 = Rake::Win32 # :nodoc: 7 | 8 | def test_win32_system_dir_uses_home_if_defined 9 | ENV["HOME"] = 'C:\\HP' 10 | 11 | assert_equal "C:/HP/Rake", Win32.win32_system_dir 12 | end 13 | 14 | def test_win32_system_dir_uses_homedrive_homepath_when_no_home_defined 15 | ENV["HOME"] = nil 16 | ENV["HOMEDRIVE"] = "C:" 17 | ENV["HOMEPATH"] = '\\HP' 18 | 19 | assert_equal "C:/HP/Rake", Win32.win32_system_dir 20 | end 21 | 22 | def test_win32_system_dir_uses_appdata_when_no_home_or_home_combo 23 | ENV["APPDATA"] = "C:\\Documents and Settings\\HP\\Application Data" 24 | ENV["HOME"] = nil 25 | ENV["HOMEDRIVE"] = nil 26 | ENV["HOMEPATH"] = nil 27 | 28 | assert_equal "C:/Documents and Settings/HP/Application Data/Rake", 29 | Win32.win32_system_dir 30 | end 31 | 32 | def test_win32_system_dir_fallback_to_userprofile_otherwise 33 | ENV["HOME"] = nil 34 | ENV["HOMEDRIVE"] = nil 35 | ENV["HOMEPATH"] = nil 36 | ENV["APPDATA"] = nil 37 | ENV["USERPROFILE"] = "C:\\Documents and Settings\\HP" 38 | 39 | assert_equal "C:/Documents and Settings/HP/Rake", Win32.win32_system_dir 40 | end 41 | 42 | def test_win32_system_dir_nil_of_no_env_vars 43 | ENV["APPDATA"] = nil 44 | ENV["HOME"] = nil 45 | ENV["HOMEDRIVE"] = nil 46 | ENV["HOMEPATH"] = nil 47 | ENV["RAKE_SYSTEM"] = nil 48 | ENV["USERPROFILE"] = nil 49 | 50 | assert_raises(Rake::Win32::Win32HomeError) do 51 | Win32.win32_system_dir 52 | end 53 | end 54 | 55 | def test_win32_backtrace_with_different_case 56 | ex = nil 57 | begin 58 | raise "test exception" 59 | rescue => ex 60 | end 61 | 62 | ex.set_backtrace ["abc", "rakefile"] 63 | 64 | rake = Rake::Application.new 65 | rake.options.trace = true 66 | rake.instance_variable_set(:@rakefile, "Rakefile") 67 | 68 | _, err = capture_output { 69 | rake.set_default_options # reset trace output IO 70 | 71 | rake.display_error_message(ex) 72 | } 73 | 74 | assert_match(/rakefile/, err) 75 | end 76 | 77 | end 78 | -------------------------------------------------------------------------------- /test/test_rake_definitions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | require "fileutils" 4 | 5 | class TestRakeDefinitions < Rake::TestCase # :nodoc: 6 | include Rake 7 | 8 | EXISTINGFILE = "existing" 9 | 10 | def setup 11 | super 12 | 13 | Task.clear 14 | end 15 | 16 | def test_task 17 | done = false 18 | task one: [:two] do done = true end 19 | task :two 20 | task three: [:one, :two] 21 | check_tasks(:one, :two, :three) 22 | assert done, "Should be done" 23 | end 24 | 25 | def test_file_task 26 | done = false 27 | file "one" => "two" do done = true end 28 | file "two" 29 | file "three" => ["one", "two"] 30 | check_tasks("one", "two", "three") 31 | assert done, "Should be done" 32 | end 33 | 34 | def check_tasks(n1, n2, n3) 35 | t = Task[n1] 36 | assert Task === t, "Should be a Task" 37 | assert_equal n1.to_s, t.name 38 | assert_equal [n2.to_s], t.prerequisites.map(&:to_s) 39 | t.invoke 40 | t2 = Task[n2] 41 | assert_equal FileList[], t2.prerequisites 42 | t3 = Task[n3] 43 | assert_equal [n1.to_s, n2.to_s], t3.prerequisites.map(&:to_s) 44 | end 45 | 46 | def test_incremental_definitions 47 | runs = [] 48 | task t1: [:t2] do runs << "A"; 4321 end 49 | task t1: [:t3] do runs << "B"; 1234 end 50 | task t1: [:t3] 51 | task :t2 52 | task :t3 53 | Task[:t1].invoke 54 | assert_equal ["A", "B"], runs 55 | assert_equal ["t2", "t3"], Task[:t1].prerequisites 56 | end 57 | 58 | def test_missing_dependencies 59 | task x: ["missing"] 60 | assert_raises(RuntimeError) { Task[:x].invoke } 61 | end 62 | 63 | def test_falsey_dependencies 64 | task x: nil 65 | assert_equal [], Task[:x].prerequisites 66 | end 67 | 68 | def test_implicit_file_dependencies 69 | runs = [] 70 | create_existing_file 71 | task y: [EXISTINGFILE] do |t| runs << t.name end 72 | Task[:y].invoke 73 | assert_equal runs, ["y"] 74 | end 75 | 76 | private # ---------------------------------------------------------- 77 | 78 | def create_existing_file 79 | Dir.mkdir File.dirname(EXISTINGFILE) unless 80 | File.exist?(File.dirname(EXISTINGFILE)) 81 | File.write(EXISTINGFILE, "HI") unless 82 | File.exist?(EXISTINGFILE) 83 | end 84 | 85 | end 86 | -------------------------------------------------------------------------------- /lib/rake/clean.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # The 'rake/clean' file defines two file lists (CLEAN and CLOBBER) and 3 | # two rake tasks (:clean and :clobber). 4 | # 5 | # [:clean] Clean up the project by deleting scratch files and backup 6 | # files. Add files to the CLEAN file list to have the :clean 7 | # target handle them. 8 | # 9 | # [:clobber] Clobber all generated and non-source files in a project. 10 | # The task depends on :clean, so all the clean files will 11 | # be deleted as well as files in the CLOBBER file list. 12 | # The intent of this task is to return a project to its 13 | # pristine, just unpacked state. 14 | 15 | require_relative "../rake" 16 | 17 | # :stopdoc: 18 | 19 | module Rake 20 | module Cleaner 21 | extend FileUtils 22 | 23 | module_function 24 | 25 | def cleanup_files(file_names) 26 | file_names.each do |file_name| 27 | cleanup(file_name) 28 | end 29 | end 30 | 31 | def cleanup(file_name, **opts) 32 | begin 33 | opts = { verbose: Rake.application.options.trace }.merge(opts) 34 | rm_r file_name, **opts 35 | rescue StandardError => ex 36 | puts "Failed to remove #{file_name}: #{ex}" unless file_already_gone?(file_name) 37 | end 38 | end 39 | 40 | def file_already_gone?(file_name) 41 | return false if File.exist?(file_name) 42 | 43 | path = file_name 44 | prev = nil 45 | 46 | while path = File.dirname(path) 47 | return false if cant_be_deleted?(path) 48 | break if [prev, "."].include?(path) 49 | prev = path 50 | end 51 | true 52 | end 53 | private_class_method :file_already_gone? 54 | 55 | def cant_be_deleted?(path_name) 56 | File.exist?(path_name) && 57 | (!File.readable?(path_name) || !File.executable?(path_name)) 58 | end 59 | private_class_method :cant_be_deleted? 60 | end 61 | end 62 | 63 | CLEAN = ::Rake::FileList["**/*~", "**/*.bak", "**/core"] 64 | CLEAN.clear_exclude.exclude { |fn| 65 | fn.pathmap("%f").downcase == "core" && File.directory?(fn) 66 | } 67 | 68 | desc "Remove any temporary products." 69 | task :clean do 70 | Rake::Cleaner.cleanup_files(CLEAN) 71 | end 72 | 73 | CLOBBER = ::Rake::FileList.new 74 | 75 | desc "Remove any generated files." 76 | task clobber: [:clean] do 77 | Rake::Cleaner.cleanup_files(CLOBBER) 78 | end 79 | -------------------------------------------------------------------------------- /test/test_rake_directory_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | require "fileutils" 4 | require "pathname" 5 | 6 | class TestRakeDirectoryTask < Rake::TestCase # :nodoc: 7 | include Rake 8 | 9 | def test_directory 10 | desc "DESC" 11 | 12 | directory "a/b/c" 13 | 14 | assert_equal FileCreationTask, Task["a"].class 15 | assert_equal FileCreationTask, Task["a/b"].class 16 | assert_equal FileCreationTask, Task["a/b/c"].class 17 | 18 | assert_nil Task["a"].comment 19 | assert_nil Task["a/b"].comment 20 | assert_equal "DESC", Task["a/b/c"].comment 21 | 22 | verbose(false) { 23 | Task["a/b"].invoke 24 | } 25 | 26 | assert File.exist?("a/b") 27 | refute File.exist?("a/b/c") 28 | end 29 | 30 | def test_directory_colon 31 | directory "a:b" 32 | 33 | assert_equal FileCreationTask, Task["a:b"].class 34 | end unless Rake::Win32.windows? 35 | 36 | if Rake::Win32.windows? 37 | def test_directory_win32 38 | desc "WIN32 DESC" 39 | directory "c:/a/b/c" 40 | assert_equal FileTask, Task["c:"].class 41 | assert_equal FileCreationTask, Task["c:/a"].class 42 | assert_equal FileCreationTask, Task["c:/a/b"].class 43 | assert_equal FileCreationTask, Task["c:/a/b/c"].class 44 | assert_nil Task["c:/"].comment 45 | assert_equal "WIN32 DESC", Task["c:/a/b/c"].comment 46 | assert_nil Task["c:/a/b"].comment 47 | end 48 | end 49 | 50 | def test_can_use_blocks 51 | runlist = [] 52 | 53 | t1 = directory("a/b/c" => :t2) { |t| runlist << t.name } 54 | task(:t2) { |t| runlist << t.name } 55 | 56 | verbose(false) { 57 | t1.invoke 58 | } 59 | 60 | assert_equal Task["a/b/c"], t1 61 | assert_equal FileCreationTask, Task["a/b/c"].class 62 | assert_equal ["t2", "a/b/c"], runlist 63 | assert File.directory?("a/b/c") 64 | end 65 | 66 | def test_can_use_pathname 67 | directory Pathname.new "a/b/c" 68 | 69 | assert_equal FileCreationTask, Task["a/b/c"].class 70 | 71 | verbose(false) { 72 | Task["a/b/c"].invoke 73 | } 74 | 75 | assert File.directory?("a/b/c") 76 | end 77 | 78 | def test_can_use_filelist 79 | directory FileList["a", "b", "c"] 80 | 81 | assert_equal FileCreationTask, Task["a"].class 82 | 83 | verbose(false) { 84 | Task["a"].invoke 85 | } 86 | 87 | assert File.directory?("a") 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/rake.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | #-- 3 | # Copyright 2003-2010 by Jim Weirich (jim.weirich@gmail.com) 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to 7 | # deal in the Software without restriction, including without limitation the 8 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | # sell copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | # IN THE SOFTWARE. 22 | #++ 23 | 24 | module Rake; end 25 | 26 | require_relative "rake/version" 27 | 28 | require "rbconfig" 29 | require "fileutils" 30 | require "singleton" 31 | require "monitor" 32 | require "optparse" 33 | 34 | require_relative "rake/ext/string" 35 | 36 | require_relative "rake/win32" 37 | 38 | require_relative "rake/linked_list" 39 | require_relative "rake/cpu_counter" 40 | require_relative "rake/scope" 41 | require_relative "rake/task_argument_error" 42 | require_relative "rake/rule_recursion_overflow_error" 43 | require_relative "rake/rake_module" 44 | require_relative "rake/trace_output" 45 | require_relative "rake/pseudo_status" 46 | require_relative "rake/task_arguments" 47 | require_relative "rake/invocation_chain" 48 | require_relative "rake/task" 49 | require_relative "rake/file_task" 50 | require_relative "rake/file_creation_task" 51 | require_relative "rake/multi_task" 52 | require_relative "rake/dsl_definition" 53 | require_relative "rake/file_utils_ext" 54 | require_relative "rake/file_list" 55 | require_relative "rake/default_loader" 56 | require_relative "rake/early_time" 57 | require_relative "rake/late_time" 58 | require_relative "rake/name_space" 59 | require_relative "rake/task_manager" 60 | require_relative "rake/application" 61 | require_relative "rake/backtrace" 62 | 63 | # :stopdoc: 64 | # 65 | # Some top level Constants. 66 | 67 | FileList = Rake::FileList 68 | RakeFileUtils = Rake::FileUtilsExt 69 | -------------------------------------------------------------------------------- /test/test_rake_linked_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestLinkedList < Rake::TestCase # :nodoc: 5 | include Rake 6 | 7 | def test_empty_list 8 | empty = LinkedList::EMPTY 9 | assert empty.empty?, "should be empty" 10 | end 11 | 12 | def test_list_with_one_item 13 | list = LinkedList.make(:one) 14 | assert ! list.empty?, "should not be empty" 15 | assert_equal :one, list.head 16 | assert_equal LinkedList::EMPTY, list.tail 17 | end 18 | 19 | def test_make_with_no_arguments 20 | empty = LinkedList.make() 21 | assert_equal LinkedList::EMPTY, empty 22 | end 23 | 24 | def test_make_with_one_argument 25 | list = LinkedList.make(:one) 26 | assert ! list.empty? 27 | assert_equal :one, list.head 28 | assert_equal LinkedList::EMPTY, list.tail 29 | end 30 | 31 | def test_make_with_two_arguments 32 | list = LinkedList.make(:one, :two) 33 | assert ! list.empty? 34 | assert_equal :one, list.head 35 | assert_equal :two, list.tail.head 36 | assert_equal LinkedList::EMPTY, list.tail.tail 37 | end 38 | 39 | def test_list_with_several_items 40 | list = LinkedList.make(:one, :two, :three) 41 | 42 | assert ! list.empty?, "should not be empty" 43 | assert_equal :one, list.head 44 | assert_equal :two, list.tail.head 45 | assert_equal :three, list.tail.tail.head 46 | assert_equal LinkedList::EMPTY, list.tail.tail.tail 47 | end 48 | 49 | def test_lists_are_structurally_equivalent 50 | list = LinkedList.make(1, 2, 3) 51 | same = LinkedList.make(1, 2, 3) 52 | diff = LinkedList.make(1, 2, 4) 53 | short = LinkedList.make(1, 2) 54 | 55 | assert_equal list, same 56 | refute_equal list, diff 57 | refute_equal list, short 58 | refute_equal short, list 59 | end 60 | 61 | def test_conversion_to_string 62 | list = LinkedList.make(:one, :two, :three) 63 | assert_equal "LL(one, two, three)", list.to_s 64 | assert_equal "LL()", LinkedList.make().to_s 65 | end 66 | 67 | def test_conversion_with_inspect 68 | list = LinkedList.make(:one, :two, :three) 69 | assert_equal "LL(:one, :two, :three)", list.inspect 70 | assert_equal "LL()", LinkedList.make().inspect 71 | end 72 | 73 | def test_lists_are_enumerable 74 | list = LinkedList.make(1, 2, 3) 75 | new_list = list.map { |item| item + 10 } 76 | expected = [11, 12, 13] 77 | assert_equal expected, new_list 78 | end 79 | 80 | def test_conjunction 81 | list = LinkedList.make.conj("C").conj("B").conj("A") 82 | assert_equal LinkedList.make("A", "B", "C"), list 83 | end 84 | 85 | end 86 | -------------------------------------------------------------------------------- /test/test_rake_package_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | require "rake/packagetask" 4 | 5 | class TestRakePackageTask < Rake::TestCase # :nodoc: 6 | 7 | def test_initialize 8 | touch "install.rb" 9 | touch "a.c" 10 | touch "b.c" 11 | mkdir "CVS" 12 | touch "a.rb~" 13 | 14 | pkg = Rake::PackageTask.new("pkgr", "1.2.3") { |p| 15 | p.package_files << "install.rb" 16 | p.package_files.include "*.c" 17 | p.package_files.exclude(/\bCVS\b/) 18 | p.package_files.exclude(/~$/) 19 | p.package_dir = "pkg" 20 | p.need_tar = true 21 | p.need_tar_gz = true 22 | p.need_tar_bz2 = true 23 | p.need_tar_xz = true 24 | p.need_zip = true 25 | } 26 | 27 | assert_equal "pkg", pkg.package_dir 28 | 29 | assert_includes pkg.package_files, "a.c" 30 | 31 | assert_equal "pkgr", pkg.name 32 | assert_equal "1.2.3", pkg.version 33 | assert Rake::Task[:package] 34 | assert Rake::Task["pkg/pkgr-1.2.3.tgz"] 35 | assert Rake::Task["pkg/pkgr-1.2.3.tar.gz"] 36 | assert Rake::Task["pkg/pkgr-1.2.3.tar.bz2"] 37 | assert Rake::Task["pkg/pkgr-1.2.3.tar.xz"] 38 | assert Rake::Task["pkg/pkgr-1.2.3.zip"] 39 | assert Rake::Task["pkg/pkgr-1.2.3"] 40 | assert Rake::Task[:clobber_package] 41 | assert Rake::Task[:repackage] 42 | end 43 | 44 | def test_initialize_no_version 45 | e = assert_raises RuntimeError do 46 | Rake::PackageTask.new "pkgr" 47 | end 48 | 49 | assert_equal "Version required (or :noversion)", e.message 50 | end 51 | 52 | def test_initialize_noversion 53 | pkg = Rake::PackageTask.new "pkgr", :noversion 54 | 55 | assert_equal "pkg", pkg.package_dir 56 | assert_equal "pkgr", pkg.name 57 | assert_nil pkg.version 58 | end 59 | 60 | def test_clone 61 | pkg = Rake::PackageTask.new("x", :noversion) 62 | p2 = pkg.clone 63 | pkg.package_files << "y" 64 | p2.package_files << "x" 65 | assert_equal ["y"], pkg.package_files 66 | assert_equal ["x"], p2.package_files 67 | end 68 | 69 | def test_package_name 70 | pkg = Rake::PackageTask.new "a", "1" 71 | 72 | assert_equal "a-1", pkg.package_name 73 | end 74 | 75 | def test_package_name_noversion 76 | pkg = Rake::PackageTask.new "a", :noversion 77 | 78 | assert_equal "a", pkg.package_name 79 | end 80 | 81 | def test_without_parent_dir 82 | pkg = Rake::PackageTask.new("foo", :noversion) 83 | 84 | assert_equal "pkg", pkg.working_dir 85 | assert_equal "foo", pkg.target_dir 86 | 87 | pkg.without_parent_dir = true 88 | 89 | assert_equal "pkg/foo", pkg.working_dir 90 | assert_equal ".", pkg.target_dir 91 | end 92 | 93 | end 94 | -------------------------------------------------------------------------------- /lib/rake/promise.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | 4 | # A Promise object represents a promise to do work (a chore) in the 5 | # future. The promise is created with a block and a list of 6 | # arguments for the block. Calling value will return the value of 7 | # the promised chore. 8 | # 9 | # Used by ThreadPool. 10 | # 11 | class Promise # :nodoc: all 12 | NOT_SET = Object.new.freeze # :nodoc: 13 | 14 | attr_accessor :recorder 15 | 16 | # Create a promise to do the chore specified by the block. 17 | def initialize(args, &block) 18 | @mutex = Mutex.new 19 | @result = NOT_SET 20 | @error = NOT_SET 21 | @args = args 22 | @block = block 23 | end 24 | 25 | # Return the value of this promise. 26 | # 27 | # If the promised chore is not yet complete, then do the work 28 | # synchronously. We will wait. 29 | def value 30 | unless complete? 31 | stat :sleeping_on, item_id: object_id 32 | @mutex.synchronize do 33 | stat :has_lock_on, item_id: object_id 34 | chore 35 | stat :releasing_lock_on, item_id: object_id 36 | end 37 | end 38 | error? ? raise(@error) : @result 39 | end 40 | 41 | # If no one else is working this promise, go ahead and do the chore. 42 | def work 43 | stat :attempting_lock_on, item_id: object_id 44 | if @mutex.try_lock 45 | stat :has_lock_on, item_id: object_id 46 | chore 47 | stat :releasing_lock_on, item_id: object_id 48 | @mutex.unlock 49 | else 50 | stat :bailed_on, item_id: object_id 51 | end 52 | end 53 | 54 | private 55 | 56 | # Perform the chore promised 57 | def chore 58 | if complete? 59 | stat :found_completed, item_id: object_id 60 | return 61 | end 62 | stat :will_execute, item_id: object_id 63 | begin 64 | @result = @block.call(*@args) 65 | rescue Exception => e 66 | @error = e 67 | end 68 | stat :did_execute, item_id: object_id 69 | discard 70 | end 71 | 72 | # Do we have a result for the promise 73 | def result? 74 | !@result.equal?(NOT_SET) 75 | end 76 | 77 | # Did the promise throw an error 78 | def error? 79 | !@error.equal?(NOT_SET) 80 | end 81 | 82 | # Are we done with the promise 83 | def complete? 84 | result? || error? 85 | end 86 | 87 | # free up these items for the GC 88 | def discard 89 | @args = nil 90 | @block = nil 91 | end 92 | 93 | # Record execution statistics if there is a recorder 94 | def stat(*args) 95 | @recorder.call(*args) if @recorder 96 | end 97 | 98 | end 99 | 100 | end 101 | -------------------------------------------------------------------------------- /test/test_thread_history_display.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | require "rake/thread_history_display" 5 | 6 | class TestThreadHistoryDisplay < Rake::TestCase # :nodoc: 7 | def setup 8 | super 9 | @time = 1_000_000 10 | @stats = [] 11 | @display = Rake::ThreadHistoryDisplay.new(@stats) 12 | end 13 | 14 | def test_banner 15 | out, _ = capture_output do 16 | @display.show 17 | end 18 | assert_match(/Job History/i, out) 19 | end 20 | 21 | def test_item_queued 22 | @stats << event(:item_queued, item_id: 123) 23 | out, _ = capture_output do 24 | @display.show 25 | end 26 | assert_match(/^ *1000000 +A +item_queued +item_id:1$/, out) 27 | end 28 | 29 | def test_item_dequeued 30 | @stats << event(:item_dequeued, item_id: 123) 31 | out, _ = capture_output do 32 | @display.show 33 | end 34 | assert_match(/^ *1000000 +A +item_dequeued +item_id:1$/, out) 35 | end 36 | 37 | def test_multiple_items 38 | @stats << event(:item_queued, item_id: 123) 39 | @stats << event(:item_queued, item_id: 124) 40 | out, _ = capture_output do 41 | @display.show 42 | end 43 | assert_match(/^ *1000000 +A +item_queued +item_id:1$/, out) 44 | assert_match(/^ *1000001 +A +item_queued +item_id:2$/, out) 45 | end 46 | 47 | def test_waiting 48 | @stats << event(:waiting, item_id: 123) 49 | out, _ = capture_output do 50 | @display.show 51 | end 52 | assert_match(/^ *1000000 +A +waiting +item_id:1$/, out) 53 | end 54 | 55 | def test_continue 56 | @stats << event(:continue, item_id: 123) 57 | out, _ = capture_output do 58 | @display.show 59 | end 60 | assert_match(/^ *1000000 +A +continue +item_id:1$/, out) 61 | end 62 | 63 | def test_thread_deleted 64 | @stats << event( 65 | :thread_deleted, 66 | deleted_thread: 123_456, 67 | thread_count: 12) 68 | out, _ = capture_output do 69 | @display.show 70 | end 71 | assert_match( 72 | /^ *1000000 +A +thread_deleted( +deleted_thread:B| +thread_count:12){2}$/, 73 | out) 74 | end 75 | 76 | def test_thread_created 77 | @stats << event( 78 | :thread_created, 79 | new_thread: 123_456, 80 | thread_count: 13) 81 | out, _ = capture_output do 82 | @display.show 83 | end 84 | assert_match( 85 | /^ *1000000 +A +thread_created( +new_thread:B| +thread_count:13){2}$/, 86 | out) 87 | end 88 | 89 | private 90 | 91 | def event(type, data = {}) 92 | result = { 93 | event: type, 94 | time: @time / 1_000_000.0, 95 | data: data, 96 | thread: Thread.current.object_id 97 | } 98 | @time += 1 99 | result 100 | end 101 | 102 | end 103 | -------------------------------------------------------------------------------- /lib/rake/task_arguments.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | 4 | ## 5 | # TaskArguments manage the arguments passed to a task. 6 | # 7 | class TaskArguments 8 | include Enumerable 9 | 10 | # Argument names 11 | attr_reader :names 12 | 13 | # Create a TaskArgument object with a list of argument +names+ and a set 14 | # of associated +values+. +parent+ is the parent argument object. 15 | def initialize(names, values, parent=nil) 16 | @names = names 17 | @parent = parent 18 | @hash = {} 19 | @values = values 20 | names.each_with_index { |name, i| 21 | next if values[i].nil? || values[i] == "" 22 | @hash[name.to_sym] = values[i] 23 | } 24 | end 25 | 26 | # Retrieve the complete array of sequential values 27 | def to_a 28 | @values.dup 29 | end 30 | 31 | # Retrieve the list of values not associated with named arguments 32 | def extras 33 | @values[@names.length..-1] || [] 34 | end 35 | 36 | # Create a new argument scope using the prerequisite argument 37 | # names. 38 | def new_scope(names) 39 | values = names.map { |n| self[n] } 40 | self.class.new(names, values + extras, self) 41 | end 42 | 43 | # Find an argument value by name or index. 44 | def [](index) 45 | lookup(index.to_sym) 46 | end 47 | 48 | # Specify a hash of default values for task arguments. Use the 49 | # defaults only if there is no specific value for the given 50 | # argument. 51 | def with_defaults(defaults) 52 | @hash = defaults.merge(@hash) 53 | end 54 | 55 | # Enumerates the arguments and their values 56 | def each(&block) 57 | @hash.each(&block) 58 | end 59 | 60 | # Extracts the argument values at +keys+ 61 | def values_at(*keys) 62 | keys.map { |k| lookup(k) } 63 | end 64 | 65 | # Returns the value of the given argument via method_missing 66 | def method_missing(sym, *args) 67 | lookup(sym.to_sym) 68 | end 69 | 70 | # Returns a Hash of arguments and their values 71 | def to_hash 72 | @hash.dup 73 | end 74 | 75 | def to_s # :nodoc: 76 | inspect 77 | end 78 | 79 | def inspect # :nodoc: 80 | inspection = @hash.map do |k,v| 81 | "#{k.to_s}: #{v.to_s}" 82 | end.join(", ") 83 | 84 | "#<#{self.class} #{inspection}>" 85 | end 86 | 87 | # Returns true if +key+ is one of the arguments 88 | def has_key?(key) 89 | @hash.has_key?(key) 90 | end 91 | alias key? has_key? 92 | 93 | def fetch(*args, &block) 94 | @hash.fetch(*args, &block) 95 | end 96 | 97 | def deconstruct_keys(keys) 98 | keys ? @hash.slice(*keys) : to_hash 99 | end 100 | 101 | protected 102 | 103 | def lookup(name) # :nodoc: 104 | if @hash.has_key?(name) 105 | @hash[name] 106 | elsif @parent 107 | @parent.lookup(name) 108 | end 109 | end 110 | end 111 | 112 | EMPTY_TASK_ARGS = TaskArguments.new([], []) # :nodoc: 113 | end 114 | -------------------------------------------------------------------------------- /doc/proto_rake.rdoc: -------------------------------------------------------------------------------- 1 | = Original Prototype Rake 2 | 3 | This is the original 100 line prototype rake program. 4 | 5 | --- 6 | #!/usr/bin/env ruby 7 | 8 | require 'ftools' 9 | 10 | class Task 11 | TASKS = Hash.new 12 | 13 | attr_reader :prerequisites 14 | 15 | def initialize(task_name) 16 | @name = task_name 17 | @prerequisites = [] 18 | @actions = [] 19 | end 20 | 21 | def enhance(deps=nil, &block) 22 | @prerequisites |= deps if deps 23 | @actions << block if block_given? 24 | self 25 | end 26 | 27 | def name 28 | @name.to_s 29 | end 30 | 31 | def invoke 32 | @prerequisites.each { |n| Task[n].invoke } 33 | execute if needed? 34 | end 35 | 36 | def execute 37 | return if @triggered 38 | @triggered = true 39 | @actions.collect { |act| result = act.call(self) }.last 40 | end 41 | 42 | def needed? 43 | true 44 | end 45 | 46 | def timestamp 47 | Time.now 48 | end 49 | 50 | class << self 51 | def [](task_name) 52 | TASKS[intern(task_name)] or fail "Don't know how to rake #{task_name}" 53 | end 54 | 55 | def define_task(args, &block) 56 | case args 57 | when Hash 58 | fail "Too Many Target Names: #{args.keys.join(' ')}" if args.size > 1 59 | fail "No Task Name Given" if args.size < 1 60 | task_name = args.keys[0] 61 | deps = args[task_name] 62 | else 63 | task_name = args 64 | deps = [] 65 | end 66 | deps = deps.collect {|d| intern(d) } 67 | get(task_name).enhance(deps, &block) 68 | end 69 | 70 | def get(task_name) 71 | name = intern(task_name) 72 | TASKS[name] ||= self.new(name) 73 | end 74 | 75 | def intern(task_name) 76 | (Symbol === task_name) ? task_name : task_name.intern 77 | end 78 | end 79 | end 80 | 81 | class FileTask < Task 82 | def needed? 83 | return true unless File.exist?(name) 84 | latest_prereq = @prerequisites.collect{|n| Task[n].timestamp}.max 85 | return false if latest_prereq.nil? 86 | timestamp < latest_prereq 87 | end 88 | 89 | def timestamp 90 | File.new(name.to_s).mtime 91 | end 92 | end 93 | 94 | def task(args, &block) 95 | Task.define_task(args, &block) 96 | end 97 | 98 | def file(args, &block) 99 | FileTask.define_task(args, &block) 100 | end 101 | 102 | def sys(cmd) 103 | puts cmd 104 | system(cmd) or fail "Command Failed: [#{cmd}]" 105 | end 106 | 107 | def rake 108 | begin 109 | here = Dir.pwd 110 | while ! File.exist?("Rakefile") 111 | Dir.chdir("..") 112 | fail "No Rakefile found" if Dir.pwd == here 113 | here = Dir.pwd 114 | end 115 | puts "(in #{Dir.pwd})" 116 | load "./Rakefile" 117 | ARGV.push("default") if ARGV.size == 0 118 | ARGV.each { |task_name| Task[task_name].invoke } 119 | rescue Exception => ex 120 | puts "rake aborted ... #{ex.message}" 121 | puts ex.backtrace.find {|str| str =~ /Rakefile/ } || "" 122 | end 123 | end 124 | 125 | if __FILE__ == $0 then 126 | rake 127 | end 128 | -------------------------------------------------------------------------------- /test/test_rake_multi_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | require "thread" 4 | 5 | class TestRakeMultiTask < Rake::TestCase # :nodoc: 6 | include Rake 7 | 8 | def setup 9 | super 10 | 11 | Task.clear 12 | @runs = Array.new 13 | @mutex = Mutex.new 14 | end 15 | 16 | def teardown 17 | Rake.application.thread_pool.join 18 | 19 | super 20 | end 21 | 22 | def add_run(obj) 23 | @mutex.synchronize do 24 | @runs << obj 25 | end 26 | end 27 | 28 | def test_running_multitasks 29 | task :a do 3.times do |i| add_run("A#{i}"); sleep 0.01; end end 30 | task :b do 3.times do |i| add_run("B#{i}"); sleep 0.01; end end 31 | multitask both: [:a, :b] 32 | Task[:both].invoke 33 | assert_equal 6, @runs.size 34 | assert @runs.index("A0") < @runs.index("A1") 35 | assert @runs.index("A1") < @runs.index("A2") 36 | assert @runs.index("B0") < @runs.index("B1") 37 | assert @runs.index("B1") < @runs.index("B2") 38 | end 39 | 40 | def test_all_multitasks_wait_on_slow_prerequisites 41 | task :slow do 3.times do |i| add_run("S#{i}"); sleep 0.05 end end 42 | task a: [:slow] do 3.times do |i| add_run("A#{i}"); sleep 0.01 end end 43 | task b: [:slow] do 3.times do |i| add_run("B#{i}"); sleep 0.01 end end 44 | multitask both: [:a, :b] 45 | Task[:both].invoke 46 | assert_equal 9, @runs.size 47 | assert @runs.index("S0") < @runs.index("S1") 48 | assert @runs.index("S1") < @runs.index("S2") 49 | assert @runs.index("S2") < @runs.index("A0") 50 | assert @runs.index("S2") < @runs.index("B0") 51 | assert @runs.index("A0") < @runs.index("A1") 52 | assert @runs.index("A1") < @runs.index("A2") 53 | assert @runs.index("B0") < @runs.index("B1") 54 | assert @runs.index("B1") < @runs.index("B2") 55 | end 56 | 57 | def test_multitasks_with_parameters 58 | task :a, [:arg] do |t, args| add_run(args[:arg]) end 59 | multitask :b, [:arg] => [:a] do |t, args| add_run(args[:arg] + "mt") end 60 | Task[:b].invoke "b" 61 | assert @runs[0] == "b" 62 | assert @runs[1] == "bmt" 63 | end 64 | 65 | def test_cross_thread_prerequisite_failures 66 | failed = false 67 | 68 | multitask :fail_once do 69 | fail_now = !failed 70 | failed = true 71 | raise "failing once" if fail_now 72 | end 73 | 74 | task a: :fail_once 75 | task b: :fail_once 76 | 77 | assert_raises RuntimeError do 78 | Rake::Task[:a].invoke 79 | end 80 | 81 | assert_raises RuntimeError do 82 | Rake::Task[:b].invoke 83 | end 84 | end 85 | 86 | def test_task_not_executed_if_dependant_task_failed_concurrently 87 | multitask default: [:one, :two] 88 | 89 | task :one do 90 | raise 91 | end 92 | 93 | task_two_was_executed = false 94 | task two: :one do 95 | task_two_was_executed = true 96 | end 97 | 98 | begin 99 | Rake::Task[:default].invoke 100 | rescue RuntimeError 101 | ensure 102 | sleep 0.5 103 | refute task_two_was_executed 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/rake/linked_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | 4 | # Polylithic linked list structure used to implement several data 5 | # structures in Rake. 6 | class LinkedList 7 | include Enumerable 8 | attr_reader :head, :tail 9 | 10 | # Polymorphically add a new element to the head of a list. The 11 | # type of head node will be the same list type as the tail. 12 | def conj(item) 13 | self.class.cons(item, self) 14 | end 15 | 16 | # Is the list empty? 17 | # .make guards against a list being empty making any instantiated LinkedList 18 | # object not empty by default 19 | # You should consider overriding this method if you implement your own .make method 20 | def empty? 21 | false 22 | end 23 | 24 | # Lists are structurally equivalent. 25 | def ==(other) 26 | current = self 27 | while !current.empty? && !other.empty? 28 | return false if current.head != other.head 29 | current = current.tail 30 | other = other.tail 31 | end 32 | current.empty? && other.empty? 33 | end 34 | 35 | # Convert to string: LL(item, item...) 36 | def to_s 37 | items = map(&:to_s).join(", ") 38 | "LL(#{items})" 39 | end 40 | 41 | # Same as +to_s+, but with inspected items. 42 | def inspect 43 | items = map(&:inspect).join(", ") 44 | "LL(#{items})" 45 | end 46 | 47 | # For each item in the list. 48 | def each 49 | current = self 50 | while !current.empty? 51 | yield(current.head) 52 | current = current.tail 53 | end 54 | self 55 | end 56 | 57 | # Make a list out of the given arguments. This method is 58 | # polymorphic 59 | def self.make(*args) 60 | # return an EmptyLinkedList if there are no arguments 61 | return empty if !args || args.empty? 62 | 63 | # build a LinkedList by starting at the tail and iterating 64 | # through each argument 65 | # inject takes an EmptyLinkedList to start 66 | args.reverse.inject(empty) do |list, item| 67 | list = cons(item, list) 68 | list # return the newly created list for each item in the block 69 | end 70 | end 71 | 72 | # Cons a new head onto the tail list. 73 | def self.cons(head, tail) 74 | new(head, tail) 75 | end 76 | 77 | # The standard empty list class for the given LinkedList class. 78 | def self.empty 79 | self::EMPTY 80 | end 81 | 82 | protected 83 | 84 | def initialize(head, tail=EMPTY) 85 | @head = head 86 | @tail = tail 87 | end 88 | 89 | # Represent an empty list, using the Null Object Pattern. 90 | # 91 | # When inheriting from the LinkedList class, you should implement 92 | # a type specific Empty class as well. Make sure you set the class 93 | # instance variable @parent to the associated list class (this 94 | # allows conj, cons and make to work polymorphically). 95 | class EmptyLinkedList < LinkedList 96 | @parent = LinkedList 97 | 98 | def initialize 99 | end 100 | 101 | def empty? 102 | true 103 | end 104 | 105 | def self.cons(head, tail) 106 | @parent.cons(head, tail) 107 | end 108 | end 109 | 110 | EMPTY = EmptyLinkedList.new 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("../../lib", __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | 6 | begin 7 | if ENV["COVERALLS"] 8 | gem "coveralls" 9 | require "coveralls" 10 | Coveralls.wear! 11 | end 12 | rescue Gem::LoadError 13 | end 14 | 15 | require "test/unit" 16 | require "rake" 17 | require "tmpdir" 18 | 19 | require_relative "support/file_creation" 20 | require_relative "support/ruby_runner" 21 | require_relative "support/rakefile_definitions" 22 | 23 | class Rake::TestCase < Test::Unit::TestCase 24 | include FileCreation 25 | 26 | include Rake::DSL 27 | 28 | class TaskManager 29 | include Rake::TaskManager 30 | end 31 | 32 | RUBY = File.realpath(ENV["RUBY"] || Gem.ruby) 33 | 34 | def setup 35 | ARGV.clear 36 | 37 | @verbose = ENV["VERBOSE"] 38 | 39 | @rake_root = File.expand_path "../../", __FILE__ 40 | @rake_exec = File.join @rake_root, "exe", "rake" 41 | @rake_lib = File.join @rake_root, "lib" 42 | @ruby_options = ["-I#{@rake_lib}", "-I."] 43 | 44 | @orig_pwd = Dir.pwd 45 | @orig_appdata = ENV["APPDATA"] 46 | @orig_home = ENV["HOME"] 47 | @orig_homedrive = ENV["HOMEDRIVE"] 48 | @orig_homepath = ENV["HOMEPATH"] 49 | @orig_rake_columns = ENV["RAKE_COLUMNS"] 50 | @orig_rake_system = ENV["RAKE_SYSTEM"] 51 | @orig_rakeopt = ENV["RAKEOPT"] 52 | @orig_userprofile = ENV["USERPROFILE"] 53 | ENV.delete "RAKE_COLUMNS" 54 | ENV.delete "RAKE_SYSTEM" 55 | ENV.delete "RAKEOPT" 56 | 57 | tmpdir = Dir.chdir Dir.tmpdir do Dir.pwd end 58 | @tempdir = File.join tmpdir, "test_rake_#{$$}" 59 | 60 | FileUtils.mkdir_p @tempdir 61 | 62 | Dir.chdir @tempdir 63 | 64 | Rake.application = Rake::Application.new 65 | Rake::TaskManager.record_task_metadata = true 66 | RakeFileUtils.verbose_flag = false 67 | end 68 | 69 | def teardown 70 | Dir.chdir @orig_pwd 71 | FileUtils.rm_rf @tempdir 72 | 73 | if @orig_appdata 74 | ENV["APPDATA"] = @orig_appdata 75 | else 76 | ENV.delete "APPDATA" 77 | end 78 | 79 | ENV["HOME"] = @orig_home 80 | ENV["HOMEDRIVE"] = @orig_homedrive 81 | ENV["HOMEPATH"] = @orig_homepath 82 | ENV["RAKE_COLUMNS"] = @orig_rake_columns 83 | ENV["RAKE_SYSTEM"] = @orig_rake_system 84 | ENV["RAKEOPT"] = @orig_rakeopt 85 | ENV["USERPROFILE"] = @orig_userprofile 86 | end 87 | 88 | def ignore_deprecations 89 | Rake.application.options.ignore_deprecate = true 90 | yield 91 | ensure 92 | Rake.application.options.ignore_deprecate = false 93 | end 94 | 95 | def rake_system_dir 96 | system_dir = "system" 97 | 98 | FileUtils.mkdir_p system_dir 99 | 100 | File.write File.join(system_dir, "sys1.rake"), <<~SYS 101 | task "sys1" do 102 | puts "SYS1" 103 | end 104 | SYS 105 | 106 | ENV["RAKE_SYSTEM"] = system_dir 107 | end 108 | 109 | def rakefile(contents) 110 | File.write "Rakefile", contents 111 | end 112 | 113 | def jruby? 114 | defined?(JRUBY_VERSION) 115 | end 116 | 117 | def jruby17? 118 | jruby? && (JRUBY_VERSION < "9.0.0.0") 119 | end 120 | 121 | def jruby9? 122 | jruby? && (JRUBY_VERSION >= "9.0.0.0") 123 | end 124 | 125 | include RakefileDefinitions 126 | end 127 | -------------------------------------------------------------------------------- /test/test_rake_backtrace.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestBacktraceSuppression < Rake::TestCase # :nodoc: 5 | def test_bin_rake_suppressed 6 | paths = ["something/bin/rake:12"] 7 | 8 | actual = Rake::Backtrace.collapse(paths) 9 | 10 | assert_equal [], actual 11 | end 12 | 13 | def test_system_dir_suppressed 14 | path = RbConfig::CONFIG["rubylibprefix"] 15 | path = File.expand_path path 16 | 17 | paths = [path + ":12"] 18 | 19 | actual = Rake::Backtrace.collapse(paths) 20 | 21 | assert_equal [], actual 22 | end 23 | 24 | def test_near_system_dir_isnt_suppressed 25 | path = RbConfig::CONFIG["rubylibprefix"] 26 | path = File.expand_path path 27 | 28 | paths = [" " + path + ":12"] 29 | 30 | actual = Rake::Backtrace.collapse(paths) 31 | 32 | assert_equal paths, actual 33 | end 34 | 35 | def test_ruby_array_each_suppressed 36 | paths = [":52:in 'Array#each'"] 37 | 38 | actual = Rake::Backtrace.collapse(paths) 39 | 40 | assert_equal [], actual 41 | end 42 | end 43 | 44 | class TestRakeBacktrace < Rake::TestCase # :nodoc: 45 | include RubyRunner 46 | 47 | def setup 48 | super 49 | 50 | omit "tmpdir is suppressed in backtrace" if 51 | Rake::Backtrace::SUPPRESS_PATTERN =~ Dir.pwd 52 | end 53 | 54 | def invoke(*args) 55 | rake(*args) 56 | @err 57 | end 58 | 59 | def test_single_collapse 60 | rakefile %q{ 61 | task :foo do 62 | raise "foooo!" 63 | end 64 | } 65 | 66 | lines = invoke("foo").split("\n") 67 | 68 | assert_equal "rake aborted!", lines[0] 69 | assert_equal "foooo!", lines[1] 70 | assert_something_matches %r!\A#{Regexp.quote Dir.pwd}/Rakefile:3!i, lines 71 | assert_something_matches %r!\ATasks:!, lines 72 | end 73 | 74 | def test_multi_collapse 75 | rakefile %q{ 76 | task :foo do 77 | Rake.application.invoke_task(:bar) 78 | end 79 | task :bar do 80 | raise "barrr!" 81 | end 82 | } 83 | 84 | lines = invoke("foo").split("\n") 85 | 86 | assert_equal "rake aborted!", lines[0] 87 | assert_equal "barrr!", lines[1] 88 | assert_something_matches %r!\A#{Regexp.quote Dir.pwd}/Rakefile:6!i, lines 89 | assert_something_matches %r!\A#{Regexp.quote Dir.pwd}/Rakefile:3!i, lines 90 | assert_something_matches %r!\ATasks:!, lines 91 | end 92 | 93 | def test_suppress_option 94 | rakefile %q{ 95 | task :baz do 96 | raise "bazzz!" 97 | end 98 | } 99 | 100 | lines = invoke("baz").split("\n") 101 | assert_equal "rake aborted!", lines[0] 102 | assert_equal "bazzz!", lines[1] 103 | assert_something_matches %r!Rakefile!i, lines 104 | 105 | lines = invoke("--suppress-backtrace", ".ak.file", "baz").split("\n") 106 | assert_equal "rake aborted!", lines[0] 107 | assert_equal "bazzz!", lines[1] 108 | refute_match %r!Rakefile!i, lines[2] 109 | end 110 | 111 | private 112 | 113 | # Assert that the pattern matches at least one line in +lines+. 114 | def assert_something_matches(pattern, lines) 115 | lines.each do |ln| 116 | if pattern =~ ln 117 | assert_match pattern, ln 118 | return 119 | end 120 | end 121 | flunk "expected #{pattern.inspect} to match something in:\n" + 122 | "#{lines.join("\n ")}" 123 | end 124 | 125 | end 126 | -------------------------------------------------------------------------------- /lib/rake/cpu_counter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Rake 3 | 4 | # Based on a script at: 5 | # http://stackoverflow.com/questions/891537/ruby-detect-number-of-cpus-installed 6 | class CpuCounter # :nodoc: all 7 | def self.count 8 | new.count_with_default 9 | end 10 | 11 | def count_with_default(default=4) 12 | count || default 13 | rescue StandardError 14 | default 15 | end 16 | 17 | begin 18 | require "etc" 19 | rescue LoadError 20 | else 21 | if Etc.respond_to?(:nprocessors) 22 | def count 23 | return Etc.nprocessors 24 | end 25 | end 26 | end 27 | end 28 | end 29 | 30 | unless Rake::CpuCounter.method_defined?(:count) 31 | Rake::CpuCounter.class_eval <<-'end;', __FILE__, __LINE__+1 32 | require 'rbconfig' 33 | 34 | def count 35 | if RUBY_PLATFORM == 'java' 36 | count_via_java_runtime 37 | else 38 | case RbConfig::CONFIG['host_os'] 39 | when /linux/ 40 | count_via_cpuinfo 41 | when /darwin|bsd/ 42 | count_via_sysctl 43 | when /mswin|mingw/ 44 | count_via_win32 45 | else 46 | # Try everything 47 | count_via_win32 || 48 | count_via_sysctl || 49 | count_via_cpuinfo 50 | end 51 | end 52 | end 53 | 54 | def count_via_java_runtime 55 | Java::Java.lang.Runtime.getRuntime.availableProcessors 56 | rescue StandardError 57 | nil 58 | end 59 | 60 | def count_via_win32 61 | # Get-CimInstance introduced in PowerShell 3 or earlier: https://learn.microsoft.com/en-us/previous-versions/powershell/module/cimcmdlets/get-ciminstance?view=powershell-3.0 62 | result = run_win32( 63 | 'powershell -command "Get-CimInstance -ClassName Win32_Processor -Property NumberOfCores ' \ 64 | '| Select-Object -Property NumberOfCores"' 65 | ) 66 | if !result || $?.exitstatus != 0 67 | # fallback to deprecated wmic for older systems 68 | result = run_win32("wmic cpu get NumberOfCores") 69 | end 70 | 71 | # powershell: "\nNumberOfCores\n-------------\n 4\n\n\n" 72 | # wmic: "NumberOfCores \n\n4 \n\n\n\n" 73 | result.scan(/\d+/).map(&:to_i).reduce(:+) if result 74 | rescue StandardError 75 | nil 76 | end 77 | 78 | def count_via_cpuinfo 79 | open('/proc/cpuinfo') { |f| f.readlines }.grep(/processor/).size 80 | rescue StandardError 81 | nil 82 | end 83 | 84 | def count_via_sysctl 85 | run 'sysctl', '-n', 'hw.ncpu' 86 | end 87 | 88 | def run(command, *args) 89 | cmd = resolve_command(command) 90 | if cmd 91 | IO.popen [cmd, *args] do |io| 92 | io.read.to_i 93 | end 94 | else 95 | nil 96 | end 97 | end 98 | 99 | def run_win32(command, *args) 100 | IO.popen(command, &:read) 101 | rescue Errno::ENOENT 102 | nil 103 | end 104 | 105 | def resolve_command(command) 106 | look_for_command("/usr/sbin", command) || 107 | look_for_command("/sbin", command) || 108 | in_path_command(command) 109 | end 110 | 111 | def look_for_command(dir, command) 112 | path = File.join(dir, command) 113 | File.exist?(path) ? path : nil 114 | end 115 | 116 | def in_path_command(command) 117 | IO.popen ['which', command] do |io| 118 | io.eof? ? nil : command 119 | end 120 | end 121 | end; 122 | end 123 | -------------------------------------------------------------------------------- /test/test_rake_task_argument_parsing.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeTaskArgumentParsing < Rake::TestCase # :nodoc: 5 | def setup 6 | super 7 | 8 | @app = Rake::Application.new 9 | end 10 | 11 | def test_name_only 12 | name, args = @app.parse_task_string("name") 13 | assert_equal "name", name 14 | assert_equal [], args 15 | end 16 | 17 | def test_empty_args 18 | name, args = @app.parse_task_string("name[]") 19 | assert_equal "name", name 20 | assert_equal [], args 21 | end 22 | 23 | def test_one_argument 24 | name, args = @app.parse_task_string("name[one]") 25 | assert_equal "name", name 26 | assert_equal ["one"], args 27 | end 28 | 29 | def test_two_arguments 30 | name, args = @app.parse_task_string("name[one,two]") 31 | assert_equal "name", name 32 | assert_equal ["one", "two"], args 33 | end 34 | 35 | def test_can_handle_spaces_between_args 36 | name, args = @app.parse_task_string("name[one, two,\tthree , \tfour]") 37 | assert_equal "name", name 38 | assert_equal ["one", "two", "three", "four"], args 39 | end 40 | 41 | def test_can_handle_spaces_between_all_args 42 | name, args = @app.parse_task_string("name[ one , two ,\tthree , \tfour ]") 43 | assert_equal "name", name 44 | assert_equal ["one", "two", "three", "four"], args 45 | end 46 | 47 | def test_keeps_embedded_spaces 48 | name, args = @app.parse_task_string("name[a one ana, two]") 49 | assert_equal "name", name 50 | assert_equal ["a one ana", "two"], args 51 | end 52 | 53 | def test_can_handle_commas_in_args 54 | name, args = @app.parse_task_string("name[one, two, three_a\\, three_b, four]") 55 | assert_equal "name", name 56 | assert_equal ["one", "two", "three_a, three_b", "four"], args 57 | end 58 | 59 | def test_treat_blank_arg_as_empty_string 60 | name, args = @app.parse_task_string("name[one,]") 61 | assert_equal "name", name 62 | assert_equal ["one", ""], args 63 | 64 | name, args = @app.parse_task_string("name[one,,two]") 65 | assert_equal "name", name 66 | assert_equal ["one", "", "two"], args 67 | end 68 | 69 | def test_terminal_width_using_env 70 | app = Rake::Application.new 71 | app.terminal_columns = 1234 72 | 73 | assert_equal 1234, app.terminal_width 74 | end 75 | 76 | def test_terminal_width_using_stty 77 | def @app.unix?() true end 78 | def @app.dynamic_width_stty() 1235 end 79 | def @app.dynamic_width_tput() 0 end 80 | 81 | assert_equal 1235, @app.terminal_width 82 | end 83 | 84 | def test_terminal_width_using_tput 85 | def @app.unix?() true end 86 | def @app.dynamic_width_stty() 0 end 87 | def @app.dynamic_width_tput() 1236 end 88 | 89 | assert_equal 1236, @app.terminal_width 90 | end 91 | 92 | def test_terminal_width_using_hardcoded_80 93 | def @app.unix?() false end 94 | 95 | assert_equal 80, @app.terminal_width 96 | end 97 | 98 | def test_terminal_width_with_failure 99 | def @app.unix?() raise end 100 | 101 | assert_equal 80, @app.terminal_width 102 | end 103 | 104 | def test_no_rakeopt 105 | app = Rake::Application.new 106 | app.init %w[--trace] 107 | assert !app.options.silent 108 | end 109 | 110 | def test_rakeopt_with_blank_options 111 | app = Rake::Application.new 112 | app.init %w[--trace] 113 | assert !app.options.silent 114 | end 115 | 116 | def test_rakeopt_with_silent_options 117 | ENV["RAKEOPT"] = "-s" 118 | app = Rake::Application.new 119 | 120 | app.init 121 | 122 | assert app.options.silent 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /rake.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/rake/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "rake" 7 | s.version = Rake::VERSION 8 | s.authors = ["Hiroshi SHIBATA", "Eric Hodel", "Jim Weirich"] 9 | s.email = ["hsbt@ruby-lang.org", "drbrain@segment7.net", ""] 10 | 11 | s.summary = "Rake is a Make-like program implemented in Ruby" 12 | s.description = <<~DESCRIPTION 13 | Rake is a Make-like program implemented in Ruby. Tasks and dependencies are 14 | specified in standard Ruby syntax. 15 | Rake has the following features: 16 | * Rakefiles (rake's version of Makefiles) are completely defined in standard Ruby syntax. 17 | No XML files to edit. No quirky Makefile syntax to worry about (is that a tab or a space?) 18 | * Users can specify tasks with prerequisites. 19 | * Rake supports rule patterns to synthesize implicit tasks. 20 | * Flexible FileLists that act like arrays but know about manipulating file names and paths. 21 | * Supports parallel execution of tasks. 22 | DESCRIPTION 23 | s.homepage = "https://github.com/ruby/rake" 24 | s.licenses = ["MIT"] 25 | 26 | s.metadata = { 27 | "bug_tracker_uri" => "https://github.com/ruby/rake/issues", 28 | "changelog_uri" => "https://github.com/ruby/rake/releases", 29 | "documentation_uri" => "https://ruby.github.io/rake", 30 | "source_code_uri" => s.homepage 31 | } 32 | 33 | s.files = [ 34 | "History.rdoc", 35 | "MIT-LICENSE", 36 | "README.rdoc", 37 | "doc/command_line_usage.rdoc", 38 | "doc/example/Rakefile1", 39 | "doc/example/Rakefile2", 40 | "doc/example/a.c", 41 | "doc/example/b.c", 42 | "doc/example/main.c", 43 | "doc/glossary.rdoc", 44 | "doc/jamis.rb", 45 | "doc/proto_rake.rdoc", 46 | "doc/rake.1", 47 | "doc/rakefile.rdoc", 48 | "doc/rational.rdoc", 49 | "exe/rake", 50 | "lib/rake.rb", 51 | "lib/rake/application.rb", 52 | "lib/rake/backtrace.rb", 53 | "lib/rake/clean.rb", 54 | "lib/rake/cloneable.rb", 55 | "lib/rake/cpu_counter.rb", 56 | "lib/rake/default_loader.rb", 57 | "lib/rake/dsl_definition.rb", 58 | "lib/rake/early_time.rb", 59 | "lib/rake/ext/core.rb", 60 | "lib/rake/ext/string.rb", 61 | "lib/rake/file_creation_task.rb", 62 | "lib/rake/file_list.rb", 63 | "lib/rake/file_task.rb", 64 | "lib/rake/file_utils.rb", 65 | "lib/rake/file_utils_ext.rb", 66 | "lib/rake/invocation_chain.rb", 67 | "lib/rake/invocation_exception_mixin.rb", 68 | "lib/rake/late_time.rb", 69 | "lib/rake/linked_list.rb", 70 | "lib/rake/loaders/makefile.rb", 71 | "lib/rake/multi_task.rb", 72 | "lib/rake/name_space.rb", 73 | "lib/rake/packagetask.rb", 74 | "lib/rake/phony.rb", 75 | "lib/rake/private_reader.rb", 76 | "lib/rake/promise.rb", 77 | "lib/rake/pseudo_status.rb", 78 | "lib/rake/rake_module.rb", 79 | "lib/rake/rake_test_loader.rb", 80 | "lib/rake/rule_recursion_overflow_error.rb", 81 | "lib/rake/scope.rb", 82 | "lib/rake/task.rb", 83 | "lib/rake/task_argument_error.rb", 84 | "lib/rake/task_arguments.rb", 85 | "lib/rake/task_manager.rb", 86 | "lib/rake/tasklib.rb", 87 | "lib/rake/testtask.rb", 88 | "lib/rake/thread_history_display.rb", 89 | "lib/rake/thread_pool.rb", 90 | "lib/rake/trace_output.rb", 91 | "lib/rake/version.rb", 92 | "lib/rake/win32.rb", 93 | "rake.gemspec" 94 | ] 95 | s.bindir = "exe" 96 | s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) } 97 | s.require_paths = ["lib"] 98 | 99 | s.required_ruby_version = Gem::Requirement.new(">= 2.3") 100 | s.rdoc_options = ["--main", "README.rdoc"] 101 | end 102 | -------------------------------------------------------------------------------- /test/test_rake_thread_pool.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | require "rake/thread_pool" 4 | 5 | class TestRakeTestThreadPool < Rake::TestCase # :nodoc: 6 | include Rake 7 | 8 | def test_pool_executes_in_current_thread_for_zero_threads 9 | pool = ThreadPool.new(0) 10 | f = pool.future { Thread.current } 11 | pool.join 12 | assert_equal Thread.current, f.value 13 | end 14 | 15 | def test_pool_executes_in_other_thread_for_pool_of_size_one 16 | pool = ThreadPool.new(1) 17 | f = pool.future { Thread.current } 18 | pool.join 19 | refute_equal Thread.current, f.value 20 | end 21 | 22 | def test_pool_executes_in_two_other_threads_for_pool_of_size_two 23 | pool = ThreadPool.new(2) 24 | threads = 2.times.map { 25 | pool.future { 26 | sleep 0.1 27 | Thread.current 28 | } 29 | }.each(&:value) 30 | 31 | refute_equal threads[0], threads[1] 32 | refute_equal Thread.current, threads[0] 33 | refute_equal Thread.current, threads[1] 34 | ensure 35 | pool.join 36 | end 37 | 38 | def test_pool_creates_the_correct_number_of_threads 39 | pool = ThreadPool.new(2) 40 | threads = Set.new 41 | t_mutex = Mutex.new 42 | 10.times.each do 43 | pool.future do 44 | sleep 0.02 45 | t_mutex.synchronize { threads << Thread.current } 46 | end 47 | end 48 | pool.join 49 | assert_equal 2, threads.count 50 | end 51 | 52 | def test_pool_future_does_not_duplicate_arguments 53 | pool = ThreadPool.new(2) 54 | obj = Object.new 55 | captured = nil 56 | pool.future(obj) { |var| captured = var } 57 | pool.join 58 | assert_equal obj, captured 59 | end 60 | 61 | def test_pool_join_empties_queue 62 | pool = ThreadPool.new(2) 63 | repeat = 25 64 | repeat.times { 65 | pool.future do 66 | repeat.times { 67 | pool.future do 68 | repeat.times { 69 | pool.future do end 70 | } 71 | end 72 | } 73 | end 74 | } 75 | 76 | pool.join 77 | assert_equal( 78 | true, 79 | pool.__send__(:__queue__).empty?, 80 | "queue should be empty") 81 | end 82 | 83 | CustomError = Class.new(StandardError) 84 | 85 | # test that throwing an exception way down in the blocks propagates 86 | # to the top 87 | def test_exceptions 88 | pool = ThreadPool.new(10) 89 | 90 | deep_exception_block = lambda do |count| 91 | raise CustomError if count < 1 92 | pool.future(count - 1, &deep_exception_block).value 93 | end 94 | 95 | assert_raises(CustomError) do 96 | pool.future(2, &deep_exception_block).value 97 | end 98 | ensure 99 | pool.join 100 | end 101 | 102 | def test_pool_prevents_deadlock 103 | pool = ThreadPool.new(5) 104 | 105 | common_dependency_a = pool.future { sleep 0.2 } 106 | futures_a = 10.times.map { 107 | pool.future { 108 | common_dependency_a.value 109 | sleep(rand() * 0.01) 110 | } 111 | } 112 | 113 | common_dependency_b = pool.future { futures_a.each(&:value) } 114 | futures_b = 10.times.map { 115 | pool.future { 116 | common_dependency_b.value 117 | sleep(rand() * 0.01) 118 | } 119 | } 120 | 121 | futures_b.each(&:value) 122 | pool.join 123 | end 124 | 125 | def test_pool_reports_correct_results 126 | pool = ThreadPool.new(7) 127 | 128 | a = 18 129 | b = 5 130 | c = 3 131 | 132 | result = a.times.map do 133 | pool.future do 134 | b.times.map do 135 | pool.future { sleep rand * 0.001; c } 136 | end.reduce(0) { |m, f| m + f.value } 137 | end 138 | end.reduce(0) { |m, f| m + f.value } 139 | 140 | assert_equal a * b * c, result 141 | pool.join 142 | end 143 | 144 | end 145 | -------------------------------------------------------------------------------- /test/test_rake_clean.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | require "rake/clean" 4 | 5 | class TestRakeClean < Rake::TestCase # :nodoc: 6 | def test_clean 7 | if RUBY_ENGINE == "truffleruby" and RUBY_ENGINE_VERSION.start_with?("19.3.") 8 | load "rake/clean.rb" # TruffleRuby 19.3 does not set self correctly with wrap=true 9 | else 10 | load "rake/clean.rb", true 11 | end 12 | 13 | assert Rake::Task["clean"], "Should define clean" 14 | assert Rake::Task["clobber"], "Should define clobber" 15 | assert Rake::Task["clobber"].prerequisites.include?("clean"), 16 | "Clobber should require clean" 17 | end 18 | 19 | def test_cleanup 20 | file_name = create_undeletable_file 21 | 22 | out, _ = capture_output do 23 | Rake::Cleaner.cleanup(file_name, verbose: false) 24 | end 25 | assert_match(/failed to remove/i, out) 26 | 27 | ensure 28 | remove_undeletable_file 29 | end 30 | 31 | def test_cleanup_ignores_missing_files 32 | file_name = File.join(@tempdir, "missing_directory", "no_such_file") 33 | 34 | out, _ = capture_output do 35 | Rake::Cleaner.cleanup(file_name, verbose: false) 36 | end 37 | refute_match(/failed to remove/i, out) 38 | end 39 | 40 | def test_cleanup_trace 41 | file_name = create_file 42 | 43 | out, err = capture_output do 44 | with_trace true do 45 | Rake::Cleaner.cleanup(file_name) 46 | end 47 | end 48 | 49 | if err == "" 50 | # Current FileUtils 51 | assert_equal "rm -r #{file_name}\n", out 52 | else 53 | # Old FileUtils 54 | assert_equal "", out 55 | assert_equal "rm -r #{file_name}\n", err 56 | end 57 | end 58 | 59 | def test_cleanup_without_trace 60 | file_name = create_file 61 | 62 | out, err = capture_output do 63 | with_trace false do 64 | Rake::Cleaner.cleanup(file_name) 65 | end 66 | end 67 | assert_empty out 68 | assert_empty err 69 | end 70 | 71 | def test_cleanup_opt_overrides_trace_silent 72 | file_name = create_file 73 | 74 | out, err = capture_output do 75 | with_trace true do 76 | Rake::Cleaner.cleanup(file_name, verbose: false) 77 | end 78 | end 79 | assert_empty out 80 | assert_empty err 81 | end 82 | 83 | def test_cleanup_opt_overrides_trace_verbose 84 | file_name = create_file 85 | 86 | out, err = capture_output do 87 | with_trace false do 88 | Rake::Cleaner.cleanup(file_name, verbose: true) 89 | end 90 | end 91 | 92 | if err == "" 93 | assert_equal "rm -r #{file_name}\n", out 94 | else 95 | assert_equal "", out 96 | assert_equal "rm -r #{file_name}\n", err 97 | end 98 | end 99 | 100 | private 101 | 102 | def create_file 103 | dir_name = File.join(@tempdir, "deletedir") 104 | file_name = File.join(dir_name, "deleteme") 105 | FileUtils.mkdir(dir_name) 106 | FileUtils.touch(file_name) 107 | file_name 108 | end 109 | 110 | def create_undeletable_file 111 | dir_name = File.join(@tempdir, "deletedir") 112 | file_name = File.join(dir_name, "deleteme") 113 | FileUtils.mkdir(dir_name) 114 | FileUtils.touch(file_name) 115 | FileUtils.chmod(0, file_name) 116 | FileUtils.chmod(0, dir_name) 117 | begin 118 | FileUtils.chmod(0644, file_name) 119 | rescue 120 | file_name 121 | else 122 | omit "Permission to delete files is different on this system" 123 | end 124 | end 125 | 126 | def remove_undeletable_file 127 | dir_name = File.join(@tempdir, "deletedir") 128 | file_name = File.join(dir_name, "deleteme") 129 | FileUtils.chmod(0777, dir_name) 130 | FileUtils.chmod(0777, file_name) 131 | Rake::Cleaner.cleanup(file_name, verbose: false) 132 | Rake::Cleaner.cleanup(dir_name, verbose: false) 133 | end 134 | 135 | def with_trace(value) 136 | old, Rake.application.options.trace = 137 | Rake.application.options.trace, value 138 | 139 | # FileUtils caches the $stderr object, which breaks capture_output et. al. 140 | # We hack it here where it's convenient to do so. 141 | Rake::Cleaner.instance_variable_set :@fileutils_output, nil 142 | yield 143 | ensure 144 | Rake.application.options.trace = old 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /lib/rake/file_utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "rbconfig" 3 | require "fileutils" 4 | 5 | #-- 6 | # This a FileUtils extension that defines several additional commands to be 7 | # added to the FileUtils utility functions. 8 | module FileUtils 9 | # Path to the currently running Ruby program 10 | RUBY = ENV["RUBY"] || File.join( 11 | RbConfig::CONFIG["bindir"], 12 | RbConfig::CONFIG["ruby_install_name"] + RbConfig::CONFIG["EXEEXT"]). 13 | sub(/.*\s.*/m, '"\&"') 14 | 15 | # Run the system command +cmd+. If multiple arguments are given the command 16 | # is run directly (without the shell, same semantics as Kernel::exec and 17 | # Kernel::system). 18 | # 19 | # It is recommended you use the multiple argument form over interpolating 20 | # user input for both usability and security reasons. With the multiple 21 | # argument form you can easily process files with spaces or other shell 22 | # reserved characters in them. With the multiple argument form your rake 23 | # tasks are not vulnerable to users providing an argument like 24 | # ; rm # -rf /. 25 | # 26 | # If a block is given, upon command completion the block is called with an 27 | # OK flag (true on a zero exit status) and a Process::Status object. 28 | # Without a block a RuntimeError is raised when the command exits non-zero. 29 | # 30 | # Examples: 31 | # 32 | # sh 'ls -ltr' 33 | # 34 | # sh 'ls', 'file with spaces' 35 | # 36 | # # check exit status after command runs 37 | # sh %{grep pattern file} do |ok, res| 38 | # if !ok 39 | # puts "pattern not found (status = #{res.exitstatus})" 40 | # end 41 | # end 42 | # 43 | def sh(*cmd, &block) 44 | options = (Hash === cmd.last) ? cmd.pop : {} 45 | shell_runner = block_given? ? block : create_shell_runner(cmd) 46 | 47 | set_verbose_option(options) 48 | verbose = options.delete :verbose 49 | noop = options.delete(:noop) || Rake::FileUtilsExt.nowrite_flag 50 | 51 | Rake.rake_output_message sh_show_command cmd if verbose 52 | 53 | unless noop 54 | res = (Hash === cmd.last) ? system(*cmd) : system(*cmd, options) 55 | status = $? 56 | status = Rake::PseudoStatus.new(1) if !res && status.nil? 57 | shell_runner.call(res, status) 58 | end 59 | end 60 | 61 | def create_shell_runner(cmd) # :nodoc: 62 | show_command = sh_show_command cmd 63 | lambda do |ok, status| 64 | ok or 65 | fail "Command failed with status (#{status.exitstatus}): " + 66 | "[#{show_command}]" 67 | end 68 | end 69 | private :create_shell_runner 70 | 71 | def sh_show_command(cmd) # :nodoc: 72 | cmd = cmd.dup 73 | 74 | if Hash === cmd.first 75 | env = cmd.first 76 | env = env.map { |name, value| "#{name}=#{value}" }.join " " 77 | cmd[0] = env 78 | end 79 | 80 | cmd.join " " 81 | end 82 | private :sh_show_command 83 | 84 | def set_verbose_option(options) # :nodoc: 85 | unless options.key? :verbose 86 | options[:verbose] = 87 | (Rake::FileUtilsExt.verbose_flag == Rake::FileUtilsExt::DEFAULT) || 88 | Rake::FileUtilsExt.verbose_flag 89 | end 90 | end 91 | private :set_verbose_option 92 | 93 | # Run a Ruby interpreter with the given arguments. 94 | # 95 | # Example: 96 | # ruby %{-pe '$_.upcase!' 1 100 | sh(RUBY, *args, **options, &block) 101 | else 102 | sh("#{RUBY} #{args.first}", **options, &block) 103 | end 104 | end 105 | 106 | LN_SUPPORTED = [true] 107 | 108 | # Attempt to do a normal file link, but fall back to a copy if the link 109 | # fails. 110 | def safe_ln(*args, **options) 111 | if LN_SUPPORTED[0] 112 | begin 113 | return options.empty? ? ln(*args) : ln(*args, **options) 114 | rescue StandardError, NotImplementedError 115 | LN_SUPPORTED[0] = false 116 | end 117 | end 118 | options.empty? ? cp(*args) : cp(*args, **options) 119 | end 120 | 121 | # Split a file path into individual directory names. 122 | # 123 | # Example: 124 | # split_all("a/b/c") => ['a', 'b', 'c'] 125 | # 126 | def split_all(path) 127 | head, tail = File.split(path) 128 | return [tail] if head == "." || tail == "/" 129 | return [head, tail] if head == "/" 130 | return split_all(head) + [tail] 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /doc/rake.1: -------------------------------------------------------------------------------- 1 | .Dd June 12, 2016 2 | .Dt RAKE 1 3 | .Os rake 11.2.2 4 | .Sh NAME 5 | .Nm rake 6 | .Nd make-like build utility for Ruby 7 | .Sh SYNOPSIS 8 | .Nm 9 | .Op Fl f Ar rakefile 10 | .Op Ar options 11 | .Ar targets ... 12 | .Sh DESCRIPTION 13 | .Nm 14 | is a 15 | .Xr make 1 Ns -like 16 | build utility for Ruby. 17 | Tasks and dependencies are specified in standard Ruby syntax. 18 | .Sh OPTIONS 19 | .Bl -tag -width Ds 20 | .It Fl m , Fl -multitask 21 | Treat all tasks as multitasks. 22 | .It Fl B , Fl -build-all 23 | Build all prerequisites, including those which are up\-to\-date. 24 | .It Fl j , Fl -jobs Ar num_jobs 25 | Specifies the maximum number of tasks to execute in parallel (default is number of CPU cores + 4). 26 | .El 27 | .Ss Modules 28 | .Bl -tag -width Ds 29 | .It Fl I , Fl -libdir Ar libdir 30 | Include 31 | .Ar libdir 32 | in the search path for required modules. 33 | .It Fl r , Fl -require Ar module 34 | Require 35 | .Ar module 36 | before executing 37 | .Pa rakefile . 38 | .El 39 | .Ss Rakefile location 40 | .Bl -tag -width Ds 41 | .It Fl f , Fl -rakefile Ar filename 42 | Use 43 | .Ar filename 44 | as the rakefile to search for. 45 | .It Fl N , Fl -no-search , Fl -nosearch 46 | Do not search parent directories for the Rakefile. 47 | .It Fl G , Fl -no-system , Fl -nosystem 48 | Use standard project Rakefile search paths, ignore system wide rakefiles. 49 | .It Fl R , Fl -rakelib Ar rakelibdir , Fl -rakelibdir Ar rakelibdir 50 | Auto-import any .rake files in 51 | .Ar rakelibdir 52 | (default is 53 | .Sq rakelib ) 54 | .It Fl g , Fl -system 55 | Use system-wide (global) rakefiles (usually 56 | .Pa ~/.rake/*.rake ) . 57 | .El 58 | .Ss Debugging 59 | .Bl -tag -width Ds 60 | .It Fl -backtrace Ns = Ns Ar out 61 | Enable full backtrace. 62 | .Ar out 63 | can be 64 | .Dv stderr 65 | (default) or 66 | .Dv stdout . 67 | .It Fl t , Fl -trace Ns = Ns Ar out 68 | Turn on invoke/execute tracing, enable full backtrace. 69 | .Ar out 70 | can be 71 | .Dv stderr 72 | (default) or 73 | .Dv stdout . 74 | .It Fl -suppress-backtrace Ar pattern 75 | Suppress backtrace lines matching regexp 76 | .Ar pattern . 77 | Ignored if 78 | .Fl -trace 79 | is on. 80 | .It Fl -rules 81 | Trace the rules resolution. 82 | .It Fl n , Fl -dry-run 83 | Do a dry run without executing actions. 84 | .It Fl T , Fl -tasks Op Ar pattern 85 | Display the tasks (matching optional 86 | .Ar pattern ) 87 | with descriptions, then exit. 88 | .It Fl D , Fl -describe Op Ar pattern 89 | Describe the tasks (matching optional 90 | .Ar pattern ) , 91 | then exit. 92 | .It Fl W , Fl -where Op Ar pattern 93 | Describe the tasks (matching optional 94 | .Ar pattern ) , 95 | then exit. 96 | .It Fl P , Fl -prereqs 97 | Display the tasks and dependencies, then exit. 98 | .It Fl e , Fl -execute Ar code 99 | Execute some Ruby code and exit. 100 | .It Fl p , Fl -execute-print Ar code 101 | Execute some Ruby code, print the result, then exit. 102 | .It Fl E , Fl -execute-continue Ar code 103 | Execute some Ruby code, then continue with normal task processing. 104 | .El 105 | .Ss Information 106 | .Bl -tag -width Ds 107 | .It Fl v , Fl -verbose 108 | Log message to standard output. 109 | .It Fl q , Fl -quiet 110 | Do not log messages to standard output. 111 | .It Fl s , Fl -silent 112 | Like 113 | .Fl -quiet , 114 | but also suppresses the 115 | .Sq in directory 116 | announcement. 117 | .It Fl X , Fl -no-deprecation-warnings 118 | Disable the deprecation warnings. 119 | .It Fl -comments 120 | Show commented tasks only 121 | .It Fl A , Fl -all 122 | Show all tasks, even uncommented ones (in combination with 123 | .Fl T 124 | or 125 | .Fl D ) 126 | .It Fl -job-stats Op Ar level 127 | Display job statistics. 128 | If 129 | .Ar level 130 | is 131 | .Sq history , 132 | displays a complete job list. 133 | .It Fl V , Fl -version 134 | Display the program version. 135 | .It Fl h , Fl H , Fl -help 136 | Display a help message. 137 | .El 138 | .Sh SEE ALSO 139 | The complete documentation for 140 | .Nm rake 141 | has been installed at 142 | .Pa /usr/share/doc/rake-doc/html/index.html . 143 | It is also available online at 144 | .Lk https://ruby.github.io/rake . 145 | .Sh AUTHORS 146 | .An -nosplit 147 | .Nm 148 | was written by 149 | .An Jim Weirich Aq Mt jim@weirichhouse.org . 150 | .Pp 151 | This manual was created by 152 | .An Caitlin Matos Aq Mt caitlin.matos@zoho.com 153 | for the Debian project (but may be used by others). 154 | It was inspired by the manual by 155 | .An Jani Monoses Aq Mt jani@iv.ro 156 | for the Ubuntu project. 157 | -------------------------------------------------------------------------------- /lib/rake/file_utils_ext.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative "file_utils" 3 | 4 | module Rake 5 | # 6 | # FileUtilsExt provides a custom version of the FileUtils methods 7 | # that respond to the verbose and nowrite 8 | # commands. 9 | # 10 | module FileUtilsExt 11 | include FileUtils 12 | 13 | class << self 14 | attr_accessor :verbose_flag, :nowrite_flag 15 | end 16 | 17 | DEFAULT = Object.new 18 | 19 | FileUtilsExt.verbose_flag = DEFAULT 20 | FileUtilsExt.nowrite_flag = false 21 | 22 | FileUtils.commands.each do |name| 23 | opts = FileUtils.options_of name 24 | default_options = [] 25 | if opts.include?("verbose") 26 | default_options << "verbose: FileUtilsExt.verbose_flag" 27 | end 28 | if opts.include?("noop") 29 | default_options << "noop: FileUtilsExt.nowrite_flag" 30 | end 31 | 32 | next if default_options.empty? 33 | module_eval(<<-EOS, __FILE__, __LINE__ + 1) 34 | def #{name}(*args, **options, &block) 35 | super(*args, 36 | #{default_options.join(', ')}, 37 | **options, &block) 38 | end 39 | EOS 40 | end 41 | 42 | # Get/set the verbose flag controlling output from the FileUtils 43 | # utilities. If verbose is true, then the utility method is 44 | # echoed to standard output. 45 | # 46 | # Examples: 47 | # verbose # return the current value of the 48 | # # verbose flag 49 | # verbose(v) # set the verbose flag to _v_. 50 | # verbose(v) { code } # Execute code with the verbose flag set 51 | # # temporarily to _v_. Return to the 52 | # # original value when code is done. 53 | def verbose(value=nil) 54 | oldvalue = FileUtilsExt.verbose_flag 55 | FileUtilsExt.verbose_flag = value unless value.nil? 56 | if block_given? 57 | begin 58 | yield 59 | ensure 60 | FileUtilsExt.verbose_flag = oldvalue 61 | end 62 | end 63 | FileUtilsExt.verbose_flag 64 | end 65 | 66 | # Get/set the nowrite flag controlling output from the FileUtils 67 | # utilities. If verbose is true, then the utility method is 68 | # echoed to standard output. 69 | # 70 | # Examples: 71 | # nowrite # return the current value of the 72 | # # nowrite flag 73 | # nowrite(v) # set the nowrite flag to _v_. 74 | # nowrite(v) { code } # Execute code with the nowrite flag set 75 | # # temporarily to _v_. Return to the 76 | # # original value when code is done. 77 | def nowrite(value=nil) 78 | oldvalue = FileUtilsExt.nowrite_flag 79 | FileUtilsExt.nowrite_flag = value unless value.nil? 80 | if block_given? 81 | begin 82 | yield 83 | ensure 84 | FileUtilsExt.nowrite_flag = oldvalue 85 | end 86 | end 87 | oldvalue 88 | end 89 | 90 | # Use this function to prevent potentially destructive ruby code 91 | # from running when the :nowrite flag is set. 92 | # 93 | # Example: 94 | # 95 | # when_writing("Building Project") do 96 | # project.build 97 | # end 98 | # 99 | # The following code will build the project under normal 100 | # conditions. If the nowrite(true) flag is set, then the example 101 | # will print: 102 | # 103 | # DRYRUN: Building Project 104 | # 105 | # instead of actually building the project. 106 | # 107 | def when_writing(msg=nil) 108 | if FileUtilsExt.nowrite_flag 109 | $stderr.puts "DRYRUN: #{msg}" if msg 110 | else 111 | yield 112 | end 113 | end 114 | 115 | # Send the message to the default rake output (which is $stderr). 116 | def rake_output_message(message) 117 | $stderr.puts(message) 118 | end 119 | 120 | # Check that the options do not contain options not listed in 121 | # +optdecl+. An ArgumentError exception is thrown if non-declared 122 | # options are found. 123 | def rake_check_options(options, *optdecl) 124 | h = options.dup 125 | optdecl.each do |name| 126 | h.delete name 127 | end 128 | raise ArgumentError, "no such option: #{h.keys.join(' ')}" unless 129 | h.empty? 130 | end 131 | 132 | extend self 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /lib/rake/thread_pool.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "promise" 4 | require "set" 5 | 6 | module Rake 7 | 8 | class ThreadPool # :nodoc: all 9 | 10 | # Creates a ThreadPool object. The +thread_count+ parameter is the size 11 | # of the pool. 12 | def initialize(thread_count) 13 | @max_active_threads = [thread_count, 0].max 14 | @threads = Set.new 15 | @threads_mon = Monitor.new 16 | @queue = Queue.new 17 | @join_cond = @threads_mon.new_cond 18 | 19 | @history_start_time = nil 20 | @history = [] 21 | @history_mon = Monitor.new 22 | @total_threads_in_play = 0 23 | end 24 | 25 | # Creates a future executed by the +ThreadPool+. 26 | # 27 | # The args are passed to the block when executing (similarly to 28 | # Thread#new) The return value is an object representing 29 | # a future which has been created and added to the queue in the 30 | # pool. Sending #value to the object will sleep the 31 | # current thread until the future is finished and will return the 32 | # result (or raise an exception thrown from the future) 33 | def future(*args, &block) 34 | promise = Promise.new(args, &block) 35 | promise.recorder = lambda { |*stats| stat(*stats) } 36 | 37 | @queue.enq promise 38 | stat :queued, item_id: promise.object_id 39 | start_thread 40 | promise 41 | end 42 | 43 | # Waits until the queue of futures is empty and all threads have exited. 44 | def join 45 | @threads_mon.synchronize do 46 | begin 47 | stat :joining 48 | @join_cond.wait unless @threads.empty? 49 | stat :joined 50 | rescue Exception => e 51 | stat :joined 52 | $stderr.puts e 53 | $stderr.print "Queue contains #{@queue.size} items. " + 54 | "Thread pool contains #{@threads.count} threads\n" 55 | $stderr.print "Current Thread #{Thread.current} status = " + 56 | "#{Thread.current.status}\n" 57 | $stderr.puts e.backtrace.join("\n") 58 | @threads.each do |t| 59 | $stderr.print "Thread #{t} status = #{t.status}\n" 60 | $stderr.puts t.backtrace.join("\n") 61 | end 62 | raise e 63 | end 64 | end 65 | end 66 | 67 | # Enable the gathering of history events. 68 | def gather_history #:nodoc: 69 | @history_start_time = Time.now if @history_start_time.nil? 70 | end 71 | 72 | # Return a array of history events for the thread pool. 73 | # 74 | # History gathering must be enabled to be able to see the events 75 | # (see #gather_history). Best to call this when the job is 76 | # complete (i.e. after ThreadPool#join is called). 77 | def history # :nodoc: 78 | @history_mon.synchronize { @history.dup }. 79 | sort_by { |i| i[:time] }. 80 | each { |i| i[:time] -= @history_start_time } 81 | end 82 | 83 | # Return a hash of always collected statistics for the thread pool. 84 | def statistics # :nodoc: 85 | { 86 | total_threads_in_play: @total_threads_in_play, 87 | max_active_threads: @max_active_threads, 88 | } 89 | end 90 | 91 | private 92 | 93 | # processes one item on the queue. Returns true if there was an 94 | # item to process, false if there was no item 95 | def process_queue_item #:nodoc: 96 | return false if @queue.empty? 97 | 98 | # Even though we just asked if the queue was empty, it 99 | # still could have had an item which by this statement 100 | # is now gone. For this reason we pass true to Queue#deq 101 | # because we will sleep indefinitely if it is empty. 102 | promise = @queue.deq(true) 103 | stat :dequeued, item_id: promise.object_id 104 | promise.work 105 | return true 106 | 107 | rescue ThreadError # this means the queue is empty 108 | false 109 | end 110 | 111 | def start_thread # :nodoc: 112 | @threads_mon.synchronize do 113 | next unless @threads.count < @max_active_threads 114 | 115 | t = Thread.new do 116 | begin 117 | loop do 118 | break unless process_queue_item 119 | end 120 | ensure 121 | @threads_mon.synchronize do 122 | @threads.delete Thread.current 123 | stat :ended, thread_count: @threads.count 124 | @join_cond.broadcast if @threads.empty? 125 | end 126 | end 127 | end 128 | 129 | @threads << t 130 | stat( 131 | :spawned, 132 | new_thread: t.object_id, 133 | thread_count: @threads.count) 134 | @total_threads_in_play = @threads.count if 135 | @threads.count > @total_threads_in_play 136 | end 137 | end 138 | 139 | def stat(event, data=nil) # :nodoc: 140 | return if @history_start_time.nil? 141 | info = { 142 | event: event, 143 | data: data, 144 | time: Time.now, 145 | thread: Thread.current.object_id, 146 | } 147 | @history_mon.synchronize { @history << info } 148 | end 149 | 150 | # for testing only 151 | 152 | def __queue__ # :nodoc: 153 | @queue 154 | end 155 | end 156 | 157 | end 158 | -------------------------------------------------------------------------------- /test/test_rake_task_arguments.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeTaskArguments < Rake::TestCase # :nodoc: 5 | def teardown 6 | ENV.delete("rev") 7 | ENV.delete("VER") 8 | 9 | super 10 | end 11 | 12 | def test_empty_arg_list_is_empty 13 | ta = Rake::TaskArguments.new([], []) 14 | assert_equal({}, ta.to_hash) 15 | end 16 | 17 | def test_multiple_values_in_args 18 | ta = Rake::TaskArguments.new([:a, :b, :c], [:one, :two, :three]) 19 | assert_equal({ a: :one, b: :two, c: :three }, ta.to_hash) 20 | end 21 | 22 | def test_blank_values_in_args 23 | ta = Rake::TaskArguments.new([:a, :b, :c], ["", :two, ""]) 24 | assert_equal({ b: :two }, ta.to_hash) 25 | end 26 | 27 | def test_has_key 28 | ta = Rake::TaskArguments.new([:a], [:one]) 29 | assert(ta.has_key?(:a)) 30 | assert(ta.key?(:a)) 31 | refute(ta.has_key?(:b)) 32 | refute(ta.key?(:b)) 33 | end 34 | 35 | def test_fetch 36 | ta = Rake::TaskArguments.new([:one], [1]) 37 | assert_equal 1, ta.fetch(:one) 38 | assert_equal 2, ta.fetch(:two) { 2 } 39 | assert_raises(KeyError) { ta.fetch(:three) } 40 | end 41 | 42 | def test_to_s 43 | ta = Rake::TaskArguments.new([:a, :b, :c], [1, 2, 3]) 44 | expectation = "#" 45 | assert_equal expectation, ta.to_s 46 | assert_equal expectation, ta.inspect 47 | end 48 | 49 | def test_to_hash 50 | ta = Rake::TaskArguments.new([:one], [1]) 51 | h = ta.to_hash 52 | h[:one] = 0 53 | assert_equal 1, ta.fetch(:one) 54 | assert_equal 0, h.fetch(:one) 55 | end 56 | 57 | def test_deconstruct_keys 58 | omit "No stable pattern matching until Ruby 3.1 (testing #{RUBY_VERSION})" if RUBY_VERSION < "3.1" 59 | 60 | ta = Rake::TaskArguments.new([:a, :b, :c], [1, 2, 3]) 61 | assert_equal({ a: 1, b: 2, c: 3 }, ta.deconstruct_keys(nil)) 62 | assert_equal({ a: 1, b: 2 }, ta.deconstruct_keys([:a, :b])) 63 | end 64 | 65 | def test_enumerable_behavior 66 | ta = Rake::TaskArguments.new([:a, :b, :c], [1, 2, 3]) 67 | assert_equal [10, 20, 30], ta.map { |k, v| v * 10 }.sort 68 | end 69 | 70 | def test_named_args 71 | ta = Rake::TaskArguments.new(["aa", "bb"], [1, 2]) 72 | assert_equal 1, ta.aa 73 | assert_equal 1, ta[:aa] 74 | assert_equal 1, ta["aa"] 75 | assert_equal 2, ta.bb 76 | assert_nil ta.cc 77 | end 78 | 79 | def test_args_knows_its_names 80 | ta = Rake::TaskArguments.new(["aa", "bb"], [1, 2]) 81 | assert_equal ["aa", "bb"], ta.names 82 | end 83 | 84 | def test_extra_names_are_nil 85 | ta = Rake::TaskArguments.new(["aa", "bb", "cc"], [1, 2]) 86 | assert_nil ta.cc 87 | end 88 | 89 | def test_args_do_not_reference_env_values 90 | ta = Rake::TaskArguments.new(["aa"], [1]) 91 | ENV["rev"] = "1.2" 92 | ENV["VER"] = "2.3" 93 | assert_nil ta.rev 94 | assert_nil ta.ver 95 | end 96 | 97 | def test_creating_new_argument_scopes 98 | parent = Rake::TaskArguments.new(["p"], [1]) 99 | child = parent.new_scope(["c", "p"]) 100 | assert_equal({ p: 1 }, child.to_hash) 101 | assert_equal 1, child.p 102 | assert_equal 1, child["p"] 103 | assert_equal 1, child[:p] 104 | assert_nil child.c 105 | end 106 | 107 | def test_child_hides_parent_arg_names 108 | parent = Rake::TaskArguments.new(["aa"], [1]) 109 | child = Rake::TaskArguments.new(["aa"], [2], parent) 110 | assert_equal 2, child.aa 111 | end 112 | 113 | def test_default_arguments_values_can_be_merged 114 | ta = Rake::TaskArguments.new(["aa", "bb"], [nil, "original_val"]) 115 | ta.with_defaults(aa: "default_val") 116 | assert_equal "default_val", ta[:aa] 117 | assert_equal "original_val", ta[:bb] 118 | end 119 | 120 | def test_default_arguments_that_dont_match_names_are_ignored 121 | ta = Rake::TaskArguments.new(["aa", "bb"], [nil, "original_val"]) 122 | ta.with_defaults("cc" => "default_val") 123 | assert_nil ta[:cc] 124 | end 125 | 126 | def test_all_and_extra_arguments_without_named_arguments 127 | app = Rake::Application.new 128 | _, args = app.parse_task_string("task[1,two,more]") 129 | ta = Rake::TaskArguments.new([], args) 130 | assert_equal [], ta.names 131 | assert_equal ["1", "two", "more"], ta.to_a 132 | assert_equal ["1", "two", "more"], ta.extras 133 | end 134 | 135 | def test_all_and_extra_arguments_with_named_arguments 136 | app = Rake::Application.new 137 | _, args = app.parse_task_string("task[1,two,more,still more]") 138 | ta = Rake::TaskArguments.new([:first, :second], args) 139 | assert_equal [:first, :second], ta.names 140 | assert_equal "1", ta[:first] 141 | assert_equal "two", ta[:second] 142 | assert_equal ["1", "two", "more", "still more"], ta.to_a 143 | assert_equal ["more", "still more"], ta.extras 144 | end 145 | 146 | def test_extra_args_with_less_than_named_arguments 147 | app = Rake::Application.new 148 | _, args = app.parse_task_string("task[1,two]") 149 | ta = Rake::TaskArguments.new([:first, :second, :third], args) 150 | assert_equal [:first, :second, :third], ta.names 151 | assert_equal "1", ta[:first] 152 | assert_equal "two", ta[:second] 153 | assert_nil ta[:third] 154 | assert_equal ["1", "two"], ta.to_a 155 | assert_equal [], ta.extras 156 | end 157 | 158 | end 159 | -------------------------------------------------------------------------------- /test/test_rake_file_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | require "fileutils" 4 | require "pathname" 5 | 6 | class TestRakeFileTask < Rake::TestCase # :nodoc: 7 | include Rake 8 | 9 | def setup 10 | super 11 | 12 | Task.clear 13 | @runs = Array.new 14 | FileUtils.rm_f NEWFILE 15 | FileUtils.rm_f OLDFILE 16 | end 17 | 18 | def test_file_need 19 | name = "dummy" 20 | file name 21 | 22 | ftask = Task[name] 23 | 24 | assert_equal name.to_s, ftask.name 25 | File.delete(ftask.name) rescue nil 26 | 27 | assert ftask.needed?, "file should be needed" 28 | assert_equal Rake::LATE, ftask.timestamp 29 | 30 | File.write(ftask.name, "HI\n") 31 | 32 | assert_nil ftask.prerequisites.map { |n| Task[n].timestamp }.max 33 | assert ! ftask.needed?, "file should not be needed" 34 | ensure 35 | File.delete(ftask.name) rescue nil 36 | end 37 | 38 | def test_file_times_new_depends_on_old 39 | create_timed_files(OLDFILE, NEWFILE) 40 | 41 | t1 = Rake.application.intern(FileTask, NEWFILE).enhance([OLDFILE]) 42 | t2 = Rake.application.intern(FileTask, OLDFILE) 43 | assert ! t2.needed?, "Should not need to build old file" 44 | assert ! t1.needed?, "Should not need to rebuild new file because of old" 45 | end 46 | 47 | def test_file_times_new_depend_on_regular_task_timestamps 48 | load_phony 49 | 50 | name = "dummy" 51 | task name 52 | 53 | create_timed_files(NEWFILE) 54 | 55 | t1 = Rake.application.intern(FileTask, NEWFILE).enhance([name]) 56 | 57 | assert t1.needed?, "depending on non-file task uses Time.now" 58 | 59 | task(name => :phony) 60 | 61 | assert t1.needed?, "unless the non-file task has a timestamp" 62 | end 63 | 64 | def test_file_times_old_depends_on_new 65 | create_timed_files(OLDFILE, NEWFILE) 66 | 67 | t1 = Rake.application.intern(FileTask, OLDFILE).enhance([NEWFILE]) 68 | t2 = Rake.application.intern(FileTask, NEWFILE) 69 | assert ! t2.needed?, "Should not need to build new file" 70 | preq_stamp = t1.prerequisites.map { |t| Task[t].timestamp }.max 71 | assert_equal t2.timestamp, preq_stamp 72 | assert t1.timestamp < preq_stamp, "T1 should be older" 73 | assert t1.needed?, "Should need to rebuild old file because of new" 74 | end 75 | 76 | def test_file_depends_on_task_depend_on_file 77 | create_timed_files(OLDFILE, NEWFILE) 78 | 79 | file NEWFILE => [:obj] do |t| @runs << t.name end 80 | task obj: [OLDFILE] do |t| @runs << t.name end 81 | file OLDFILE do |t| @runs << t.name end 82 | 83 | Task[:obj].invoke 84 | Task[NEWFILE].invoke 85 | assert @runs.include?(NEWFILE) 86 | end 87 | 88 | def test_existing_file_depends_on_non_existing_file 89 | create_file(OLDFILE) 90 | delete_file(NEWFILE) 91 | file NEWFILE do |t| @runs << t.name end 92 | file OLDFILE => NEWFILE do |t| @runs << t.name end 93 | 94 | Task[OLDFILE].invoke 95 | 96 | assert_equal [NEWFILE, OLDFILE], @runs 97 | end 98 | 99 | def test_needed_eh_build_all 100 | create_file "a" 101 | 102 | file "a" 103 | 104 | a_task = Task["a"] 105 | 106 | refute a_task.needed? 107 | 108 | Rake.application.options.build_all = true 109 | 110 | assert a_task.needed? 111 | ensure 112 | delete_file "a" 113 | end 114 | 115 | def test_needed_eh_dependency 116 | create_file "a", Time.now 117 | create_file "b", Time.now - 60 118 | 119 | create_file "c", Time.now 120 | create_file "d", Time.now - 60 121 | 122 | file "b" => "a" 123 | 124 | b_task = Task["b"] 125 | 126 | assert b_task.needed? 127 | 128 | file "c" => "d" 129 | 130 | c_task = Task["c"] 131 | 132 | refute c_task.needed? 133 | ensure 134 | delete_file "old" 135 | delete_file "new" 136 | end 137 | 138 | def test_needed_eh_exists 139 | name = "dummy" 140 | file name 141 | 142 | ftask = Task[name] 143 | 144 | assert ftask.needed? 145 | 146 | create_file name 147 | 148 | refute ftask.needed? 149 | ensure 150 | delete_file name 151 | end 152 | 153 | def test_source_is_first_prerequisite 154 | t = file f: ["preqA", "preqB"] 155 | assert_equal "preqA", t.source 156 | end 157 | 158 | def test_sources_is_all_prerequisites 159 | t = file f: ["preqA", "preqB"] 160 | assert_equal ["preqA", "preqB"], t.sources 161 | end 162 | 163 | def test_task_can_be_pathname 164 | name = "dummy" 165 | file Pathname.new name 166 | 167 | ftask = Task[name] 168 | 169 | assert_equal name.to_s, ftask.name 170 | end 171 | 172 | def test_prerequisite_can_be_pathname 173 | t = file f: Pathname.new("preq") 174 | assert_equal "preq", t.source 175 | end 176 | 177 | # I have currently disabled this test. I'm not convinced that 178 | # deleting the file target on failure is always the proper thing to 179 | # do. I'm willing to hear input on this topic. 180 | def ztest_file_deletes_on_failure 181 | task :obj 182 | file NEWFILE => [:obj] do |t| 183 | FileUtils.touch NEWFILE 184 | fail "Ooops" 185 | end 186 | assert Task[NEWFILE] 187 | begin 188 | Task[NEWFILE].invoke 189 | rescue Exception 190 | end 191 | assert(! File.exist?(NEWFILE), "NEWFILE should be deleted") 192 | end 193 | 194 | def load_phony 195 | load File.join(@rake_lib, "rake/phony.rb") 196 | end 197 | 198 | end 199 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = RAKE -- Ruby Make 2 | 3 | home :: https://github.com/ruby/rake 4 | bugs :: https://github.com/ruby/rake/issues 5 | docs :: https://ruby.github.io/rake 6 | 7 | == Description 8 | 9 | Rake is a Make-like program implemented in Ruby. Tasks and dependencies are 10 | specified in standard Ruby syntax. 11 | 12 | Rake has the following features: 13 | 14 | * Rakefiles (rake's version of Makefiles) are completely defined in 15 | standard Ruby syntax. No XML files to edit. No quirky Makefile 16 | syntax to worry about (is that a tab or a space?) 17 | 18 | * Users can specify tasks with prerequisites. 19 | 20 | * Rake supports rule patterns to synthesize implicit tasks. 21 | 22 | * Flexible FileLists that act like arrays but know about manipulating 23 | file names and paths. 24 | 25 | * A library of prepackaged tasks to make building rakefiles easier. For example, 26 | tasks for building tarballs. (Formerly 27 | tasks for building RDoc, Gems, and publishing to FTP were included in rake but they're now 28 | available in RDoc, RubyGems, and rake-contrib respectively.) 29 | 30 | * Supports parallel execution of tasks. 31 | 32 | == Installation 33 | 34 | === Gem Installation 35 | 36 | Download and install rake with the following. 37 | 38 | gem install rake 39 | 40 | == Usage 41 | 42 | === Simple Example 43 | 44 | First, you must write a "Rakefile" file which contains the build rules. Here's 45 | a simple example: 46 | 47 | task default: %w[test] 48 | 49 | task :test do 50 | ruby "test/unittest.rb" 51 | end 52 | 53 | This Rakefile has two tasks: 54 | 55 | * A task named "test", which -- upon invocation -- will run a unit test file 56 | in Ruby. 57 | * A task named "default". This task does nothing by itself, but it has exactly 58 | one dependency, namely the "test" task. Invoking the "default" task will 59 | cause Rake to invoke the "test" task as well. 60 | 61 | Running the "rake" command without any options will cause it to run the 62 | "default" task in the Rakefile: 63 | 64 | % ls 65 | Rakefile test/ 66 | % rake 67 | (in /home/some_user/Projects/rake) 68 | ruby test/unittest.rb 69 | ....unit test output here... 70 | 71 | Type "rake --help" for all available options. 72 | 73 | == Resources 74 | 75 | === Rake Information 76 | 77 | * {Rake command-line}[rdoc-ref:doc/command_line_usage.rdoc] 78 | * {Writing Rakefiles}[rdoc-ref:doc/rakefile.rdoc] 79 | * The original {Rake announcement}[rdoc-ref:doc/rational.rdoc] 80 | * Rake {glossary}[rdoc-ref:doc/glossary.rdoc] 81 | 82 | === Presentations and Articles about Rake 83 | 84 | * Avdi Grimm's rake series: 85 | 1. {Rake Basics}[https://avdi.codes/rake-part-1-basics/] 86 | 2. {Rake File Lists}[https://avdi.codes/rake-part-2-file-lists-2/] 87 | 3. {Rake Rules}[https://avdi.codes/rake-part-3-rules/] 88 | 4. {Rake Pathmap}[https://avdi.codes/rake-part-4-pathmap/] 89 | 5. {File Operations}[https://avdi.codes/rake-part-5-file-operations/] 90 | 6. {Clean and Clobber}[https://avdi.codes/rake-part-6-clean-and-clobber/] 91 | 7. {MultiTask}[https://avdi.codes/rake-part-7-multitask/] 92 | * {Jim Weirich's 2003 RubyConf presentation}[https://web.archive.org/web/20140221123354/http://onestepback.org/articles/buildingwithrake/] 93 | * Martin Fowler's article on Rake: https://martinfowler.com/articles/rake.html 94 | 95 | == Other Make Re-envisionings ... 96 | 97 | Rake is a late entry in the make replacement field. Here are links to 98 | other projects with similar (and not so similar) goals. 99 | 100 | * https://directory.fsf.org/wiki/Bras -- Bras, one of earliest 101 | implementations of "make in a scripting language". 102 | * http://www.a-a-p.org -- Make in Python 103 | * https://ant.apache.org -- The Ant project 104 | * https://search.cpan.org/search?query=PerlBuildSystem -- The Perl Build System 105 | * https://www.rubydoc.info/gems/rant/0.5.7/frames -- Rant, another Ruby make tool. 106 | 107 | == Credits 108 | 109 | [Jim Weirich] Who originally created Rake. 110 | 111 | [Ryan Dlugosz] For the initial conversation that sparked Rake. 112 | 113 | [Nobuyoshi Nakada ] For the initial patch for rule support. 114 | 115 | [Tilman Sauerbeck ] For the recursive rule patch. 116 | 117 | [Eric Hodel] For aid in maintaining rake. 118 | 119 | [Hiroshi SHIBATA] Maintainer of Rake 10 and later 120 | 121 | == License 122 | 123 | Rake is available under an MIT-style license. 124 | 125 | :include: MIT-LICENSE 126 | 127 | --- 128 | 129 | = Other stuff 130 | 131 | Author:: Jim Weirich 132 | Requires:: Ruby 2.0.0 or later 133 | License:: Copyright Jim Weirich. 134 | Released under an MIT-style license. See the MIT-LICENSE 135 | file included in the distribution. 136 | 137 | == Warranty 138 | 139 | This software is provided "as is" and without any express or implied 140 | warranties, including, without limitation, the implied warranties of 141 | merchantability and fitness for a particular purpose. 142 | 143 | == Historical 144 | 145 | Rake was originally created by Jim Weirich, who unfortunately passed away in 146 | February 2014. This repository was originally hosted at 147 | {github.com/jimweirich/rake}[https://github.com/jimweirich/rake/], however 148 | with his passing, has been moved to {ruby/rake}[https://github.com/ruby/rake]. 149 | 150 | You can view Jim's last commit here: 151 | https://github.com/jimweirich/rake/commit/336559f28f55bce418e2ebcc0a57548dcbac4025 152 | 153 | You can {read more about Jim}[https://en.wikipedia.org/wiki/Jim_Weirich] at Wikipedia. 154 | 155 | Thank you for this great tool, Jim. We'll remember you. 156 | -------------------------------------------------------------------------------- /test/test_rake_test_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | require "rake/testtask" 4 | 5 | class TestRakeTestTask < Rake::TestCase # :nodoc: 6 | include Rake 7 | 8 | def test_initialize 9 | tt = Rake::TestTask.new do |t| end 10 | refute_nil tt 11 | assert_equal :test, tt.name 12 | assert_equal ["lib"], tt.libs 13 | assert_equal "test/test*.rb", tt.pattern 14 | assert_equal false, tt.verbose 15 | assert_equal true, tt.warning 16 | assert_equal [], tt.deps 17 | assert Task.task_defined?(:test) 18 | end 19 | 20 | def test_initialize_deps 21 | tt = Rake::TestTask.new(example: :bar) 22 | refute_nil tt 23 | assert_equal :bar, tt.deps 24 | assert Task.task_defined?(:example) 25 | end 26 | 27 | def test_initialize_multi_deps 28 | tt = Rake::TestTask.new(example: [:foo, :bar]) 29 | refute_nil tt 30 | assert_equal [:foo, :bar], tt.deps 31 | assert Task.task_defined?(:example) 32 | end 33 | 34 | def test_initialize_override 35 | tt = Rake::TestTask.new(example: :bar) do |t| 36 | t.description = "Run example tests" 37 | t.libs = ["src", "ext"] 38 | t.pattern = "test/tc_*.rb" 39 | t.warning = true 40 | t.verbose = true 41 | t.deps = [:env] 42 | end 43 | refute_nil tt 44 | assert_equal "Run example tests", tt.description 45 | assert_equal :example, tt.name 46 | assert_equal ["src", "ext"], tt.libs 47 | assert_equal "test/tc_*.rb", tt.pattern 48 | assert_equal true, tt.warning 49 | assert_equal true, tt.verbose 50 | assert_equal [:env], tt.deps 51 | assert_match(/-w/, tt.ruby_opts_string) 52 | assert Task.task_defined?(:example) 53 | end 54 | 55 | def test_file_list_env_test 56 | ENV["TEST"] = "testfile.rb" 57 | tt = Rake::TestTask.new do |t| 58 | t.pattern = "*" 59 | end 60 | 61 | assert_equal ["testfile.rb"], tt.file_list.to_a 62 | ensure 63 | ENV.delete "TEST" 64 | end 65 | 66 | def test_libs_equals 67 | test_task = Rake::TestTask.new do |t| 68 | t.libs << ["A", "B"] 69 | end 70 | 71 | path = %w[lib A B].join File::PATH_SEPARATOR 72 | 73 | assert_equal "-w -I\"#{path}\"", test_task.ruby_opts_string 74 | end 75 | 76 | def test_libs_equals_empty 77 | test_task = Rake::TestTask.new do |t| 78 | t.libs = [] 79 | t.warning = false 80 | end 81 | 82 | assert_equal "", test_task.ruby_opts_string 83 | end 84 | 85 | def test_pattern_equals 86 | ["gl.rb", "ob.rb"].each do |f| 87 | create_file(f) 88 | end 89 | tt = Rake::TestTask.new do |t| 90 | t.pattern = "*.rb" 91 | end 92 | assert_equal ["gl.rb", "ob.rb"], tt.file_list.to_a 93 | end 94 | 95 | def test_pattern_equals_test_files_equals 96 | ["gl.rb", "ob.rb"].each do |f| 97 | create_file(f) 98 | end 99 | tt = Rake::TestTask.new do |t| 100 | t.test_files = FileList["a.rb", "b.rb"] 101 | t.pattern = "*.rb" 102 | end 103 | assert_equal ["a.rb", "b.rb", "gl.rb", "ob.rb"], tt.file_list.to_a 104 | end 105 | 106 | def test_run_code_direct 107 | globbed = ["test_gl.rb", "test_ob.rb"].map { |f| File.join("test", f) } 108 | others = ["a.rb", "b.rb"].map { |f| File.join("test", f) } 109 | (globbed + others).each do |f| 110 | create_file(f) 111 | end 112 | test_task = Rake::TestTask.new do |t| 113 | t.loader = :direct 114 | # if t.pettern and t.test_files are nil, 115 | # t.pettern is "test/test*.rb" 116 | end 117 | 118 | assert_equal '-e "ARGV.each{|f| require f}"', test_task.run_code 119 | assert_equal globbed, test_task.file_list.to_a 120 | end 121 | 122 | def test_run_code_rake 123 | spec = Gem::Specification.new "rake", 0 124 | spec.loaded_from = File.join Gem::Specification.dirs.last, "rake-0.gemspec" 125 | rake, Gem.loaded_specs["rake"] = Gem.loaded_specs["rake"], spec 126 | 127 | test_task = Rake::TestTask.new do |t| 128 | t.loader = :rake 129 | end 130 | 131 | assert_includes test_task.run_code, "lib/rake/rake_test_loader.rb" 132 | ensure 133 | Gem.loaded_specs["rake"] = rake 134 | end 135 | 136 | def test_test_files_equals 137 | tt = Rake::TestTask.new do |t| 138 | t.test_files = FileList["a.rb", "b.rb"] 139 | end 140 | 141 | assert_equal ["a.rb", "b.rb"], tt.file_list.to_a 142 | end 143 | 144 | def test_task_prerequisites 145 | Rake::TestTask.new :parent 146 | Rake::TestTask.new child: :parent 147 | 148 | task = Rake::Task[:child] 149 | assert_includes task.prerequisites, "parent" 150 | end 151 | 152 | def test_task_prerequisites_multi 153 | Rake::TestTask.new :parent 154 | Rake::TestTask.new :parent2 155 | Rake::TestTask.new child: [:parent, :parent2] 156 | 157 | task = Rake::Task[:child] 158 | assert_includes task.prerequisites, "parent" 159 | assert_includes task.prerequisites, "parent2" 160 | end 161 | 162 | def test_task_prerequisites_deps 163 | Rake::TestTask.new :parent 164 | 165 | Rake::TestTask.new :child do |t| 166 | t.deps = :parent 167 | end 168 | 169 | task = Rake::Task[:child] 170 | assert_includes task.prerequisites, "parent" 171 | end 172 | 173 | def test_task_order_only_prerequisites 174 | t = task(a: "b") { 175 | :aaa 176 | } | "c" 177 | b, c = task("b"), task("c") 178 | assert_equal ["b"], t.prerequisites 179 | assert_equal ["c"], t.order_only_prerequisites 180 | assert_equal [b, c], t.prerequisite_tasks 181 | end 182 | 183 | def test_task_order_only_prerequisites_key 184 | t = task "a" => "b", order_only: ["c"] 185 | b, c = task("b"), task("c") 186 | assert_equal ["b"], t.prerequisites 187 | assert_equal ["c"], t.order_only_prerequisites 188 | assert_equal [b, c], t.prerequisite_tasks 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /test/test_rake_task_with_arguments.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeTaskWithArguments < Rake::TestCase # :nodoc: 5 | include Rake 6 | 7 | def setup 8 | super 9 | 10 | Task.clear 11 | Rake::TaskManager.record_task_metadata = true 12 | end 13 | 14 | def teardown 15 | Rake::TaskManager.record_task_metadata = false 16 | Rake.application.thread_pool.join 17 | 18 | super 19 | end 20 | 21 | def test_no_args_given 22 | t = task :t 23 | assert_equal [], t.arg_names 24 | end 25 | 26 | def test_args_given 27 | t = task :t, :a, :b 28 | assert_equal [:a, :b], t.arg_names 29 | end 30 | 31 | def test_name_and_needs 32 | t = task(t: [:pre]) 33 | assert_equal "t", t.name 34 | assert_equal [], t.arg_names 35 | assert_equal ["pre"], t.prerequisites 36 | end 37 | 38 | def test_name_args_and_prereqs 39 | t = task(:t, [:x, :y] => [:pre]) 40 | assert_equal "t", t.name 41 | assert_equal [:x, :y], t.arg_names 42 | assert_equal ["pre"], t.prerequisites 43 | end 44 | 45 | def test_arg_list_is_empty_if_no_args_given 46 | t = task(:t) { |tt, args| assert_equal({}, args.to_hash) } 47 | t.invoke(1, 2, 3) 48 | end 49 | 50 | def test_tasks_can_access_arguments_as_hash 51 | t = task :t, :a, :b, :c do |tt, args| 52 | assert_equal({ a: 1, b: 2, c: 3 }, args.to_hash) 53 | assert_equal 1, args[:a] 54 | assert_equal 2, args[:b] 55 | assert_equal 3, args[:c] 56 | assert_equal 1, args.a 57 | assert_equal 2, args.b 58 | assert_equal 3, args.c 59 | end 60 | t.invoke(1, 2, 3) 61 | end 62 | 63 | def test_actions_of_various_arity_are_ok_with_args 64 | notes = [] 65 | t = task(:t, :x) do 66 | notes << :a 67 | end 68 | t.enhance do | | 69 | notes << :b 70 | end 71 | t.enhance do |task| 72 | notes << :c 73 | assert_kind_of Task, task 74 | end 75 | t.enhance do |t2, args| 76 | notes << :d 77 | assert_equal t, t2 78 | assert_equal({ x: 1 }, args.to_hash) 79 | end 80 | t.invoke(1) 81 | assert_equal [:a, :b, :c, :d], notes 82 | end 83 | 84 | def test_actions_adore_keywords 85 | # https://github.com/ruby/rake/pull/174#issuecomment-263460761 86 | omit if jruby9? 87 | eval <<~RUBY, binding, __FILE__, __LINE__+1 88 | notes = [] 89 | t = task :t, [:reqr, :ovrd, :dflt] # required, overridden-optional, default-optional 90 | verify = lambda do |name, expecteds, actuals| 91 | notes << name 92 | assert_equal expecteds.length, actuals.length 93 | expecteds.zip(actuals) { |e, a| assert_equal e, a, "(TEST \#{name})" } 94 | end 95 | 96 | t.enhance { |dflt: 'd', **| verify.call :a, ['d'], [dflt] } 97 | t.enhance { |ovrd: '-', **| verify.call :b, ['o'], [ovrd] } 98 | t.enhance { |reqr: , **| verify.call :c, ['r'], [reqr] } 99 | 100 | t.enhance { |t2, dflt: 'd', **| verify.call :d, [t,'d'], [t2,dflt] } 101 | t.enhance { |t2, ovrd: 'd', **| verify.call :e, [t,'o'], [t2,ovrd] } 102 | t.enhance { |t2, reqr: , **| verify.call :f, [t,'r'], [t2,reqr] } 103 | 104 | t.enhance { |t2, dflt: 'd', reqr:, **| verify.call :g, [t,'d','r'], [t2,dflt,reqr] } 105 | t.enhance { |t2, ovrd: '-', reqr:, **| verify.call :h, [t,'o','r'], [t2,ovrd,reqr] } 106 | 107 | t.invoke('r', 'o') 108 | assert_equal [*:a..:h], notes 109 | RUBY 110 | end 111 | 112 | def test_arguments_are_passed_to_block 113 | t = task(:t, :a, :b) { |tt, args| 114 | assert_equal({ a: 1, b: 2 }, args.to_hash) 115 | } 116 | t.invoke(1, 2) 117 | end 118 | 119 | def test_extra_parameters_are_ignored 120 | t = task(:t, :a) { |tt, args| 121 | assert_equal 1, args.a 122 | assert_nil args.b 123 | } 124 | t.invoke(1, 2) 125 | end 126 | 127 | def test_arguments_are_passed_to_all_blocks 128 | counter = 0 129 | t = task :t, :a 130 | task :t do |tt, args| 131 | assert_equal 1, args.a 132 | counter += 1 133 | end 134 | task :t do |tt, args| 135 | assert_equal 1, args.a 136 | counter += 1 137 | end 138 | t.invoke(1) 139 | assert_equal 2, counter 140 | end 141 | 142 | def test_block_with_no_parameters_is_ok 143 | t = task(:t) {} 144 | t.invoke(1, 2) 145 | end 146 | 147 | def test_name_with_args 148 | desc "T" 149 | t = task(:tt, :a, :b) 150 | assert_equal "tt", t.name 151 | assert_equal "T", t.comment 152 | assert_equal "[a,b]", t.arg_description 153 | assert_equal "tt[a,b]", t.name_with_args 154 | assert_equal [:a, :b], t.arg_names 155 | end 156 | 157 | def test_named_args_are_passed_to_prereqs 158 | value = nil 159 | task(:pre, :rev) { |t, args| value = args.rev } 160 | t = task(:t, [:name, :rev] => [:pre]) 161 | t.invoke("bill", "1.2") 162 | assert_equal "1.2", value 163 | end 164 | 165 | def test_args_not_passed_if_no_prereq_names_on_task 166 | task(:pre) { |t, args| 167 | assert_equal({}, args.to_hash) 168 | assert_equal "bill", args.name 169 | } 170 | t = task(:t, [:name, :rev] => [:pre]) 171 | t.invoke("bill", "1.2") 172 | end 173 | 174 | def test_args_not_passed_if_no_prereq_names_on_multitask 175 | task(:pre) { |t, args| 176 | assert_equal({}, args.to_hash) 177 | assert_equal "bill", args.name 178 | } 179 | t = multitask(:t, [:name, :rev] => [:pre]) 180 | t.invoke("bill", "1.2") 181 | end 182 | 183 | def test_args_not_passed_if_no_arg_names 184 | task(:pre, :rev) { |t, args| 185 | assert_equal({}, args.to_hash) 186 | } 187 | t = task(t: [:pre]) 188 | t.invoke("bill", "1.2") 189 | end 190 | 191 | def test_values_at 192 | t = task(:pre, [:a, :b, :c]) { |task, args| 193 | a, b, c = args.values_at(:a, :b, :c) 194 | assert_equal %w[1 2 3], [a, b, c] 195 | } 196 | 197 | t.invoke(*%w[1 2 3]) 198 | 199 | # HACK no assertions 200 | end 201 | end 202 | -------------------------------------------------------------------------------- /test/test_rake_task_manager.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakeTaskManager < Rake::TestCase # :nodoc: 5 | 6 | def setup 7 | super 8 | 9 | @tm = Rake::TestCase::TaskManager.new 10 | end 11 | 12 | def test_create_task_manager 13 | refute_nil @tm 14 | assert_equal [], @tm.tasks 15 | end 16 | 17 | def test_define_task 18 | t = @tm.define_task(Rake::Task, :t) 19 | assert_equal "t", t.name 20 | assert_equal @tm, t.application 21 | end 22 | 23 | def test_index 24 | e = assert_raises RuntimeError do 25 | @tm["bad"] 26 | end 27 | 28 | assert_equal "Don't know how to build task 'bad' (See the list of available tasks with `rake --tasks`)", e.message 29 | end 30 | 31 | def test_undefined_task_with_custom_application 32 | Rake.application.init("myrake", nil) 33 | 34 | e = assert_raises RuntimeError do 35 | @tm["bad"] 36 | end 37 | 38 | assert_equal "Don't know how to build task 'bad' (See the list of available tasks with `myrake --tasks`)", e.message 39 | end 40 | 41 | def test_name_lookup 42 | t = @tm.define_task(Rake::Task, :t) 43 | assert_equal t, @tm[:t] 44 | end 45 | 46 | def test_namespace_task_create 47 | @tm.in_namespace("x") do 48 | t = @tm.define_task(Rake::Task, :t) 49 | assert_equal "x:t", t.name 50 | end 51 | assert_equal ["x:t"], @tm.tasks.map(&:name) 52 | end 53 | 54 | def test_define_namespaced_task 55 | t = @tm.define_task(Rake::Task, "n:a:m:e:t") 56 | assert_equal Rake::Scope.make("e", "m", "a", "n"), t.scope 57 | assert_equal "n:a:m:e:t", t.name 58 | assert_equal @tm, t.application 59 | end 60 | 61 | def test_define_namespace_in_namespace 62 | t = nil 63 | @tm.in_namespace("n") do 64 | t = @tm.define_task(Rake::Task, "a:m:e:t") 65 | end 66 | assert_equal Rake::Scope.make("e", "m", "a", "n"), t.scope 67 | assert_equal "n:a:m:e:t", t.name 68 | assert_equal @tm, t.application 69 | end 70 | 71 | def test_anonymous_namespace 72 | anon_ns = @tm.in_namespace(nil) do 73 | t = @tm.define_task(Rake::Task, :t) 74 | assert_equal "_anon_1:t", t.name 75 | end 76 | task = anon_ns[:t] 77 | assert_equal "_anon_1:t", task.name 78 | end 79 | 80 | def test_create_filetask_in_namespace 81 | @tm.in_namespace("x") do 82 | t = @tm.define_task(Rake::FileTask, "fn") 83 | assert_equal "fn", t.name 84 | end 85 | 86 | assert_equal ["fn"], @tm.tasks.map(&:name) 87 | end 88 | 89 | def test_namespace_yields_same_namespace_as_returned 90 | yielded_namespace = nil 91 | returned_namespace = @tm.in_namespace("x") do |ns| 92 | yielded_namespace = ns 93 | end 94 | assert_equal returned_namespace, yielded_namespace 95 | end 96 | 97 | def test_name_lookup_with_implicit_file_tasks 98 | FileUtils.touch "README.rdoc" 99 | 100 | t = @tm["README.rdoc"] 101 | 102 | assert_equal "README.rdoc", t.name 103 | assert Rake::FileTask === t 104 | end 105 | 106 | def test_name_lookup_with_nonexistent_task 107 | assert_raises(RuntimeError) { 108 | @tm["DOES NOT EXIST"] 109 | } 110 | end 111 | 112 | def test_name_lookup_in_multiple_scopes 113 | aa = nil 114 | bb = nil 115 | xx = @tm.define_task(Rake::Task, :xx) 116 | top_z = @tm.define_task(Rake::Task, :z) 117 | @tm.in_namespace("a") do 118 | aa = @tm.define_task(Rake::Task, :aa) 119 | mid_z = @tm.define_task(Rake::Task, :z) 120 | ns_d = @tm.define_task(Rake::Task, "n:t") 121 | @tm.in_namespace("b") do 122 | bb = @tm.define_task(Rake::Task, :bb) 123 | bot_z = @tm.define_task(Rake::Task, :z) 124 | 125 | assert_equal Rake::Scope.make("b", "a"), @tm.current_scope 126 | 127 | assert_equal bb, @tm["a:b:bb"] 128 | assert_equal aa, @tm["a:aa"] 129 | assert_equal xx, @tm["xx"] 130 | assert_equal bot_z, @tm["z"] 131 | assert_equal mid_z, @tm["^z"] 132 | assert_equal top_z, @tm["^^z"] 133 | assert_equal top_z, @tm["^^^z"] # Over the top 134 | assert_equal top_z, @tm["rake:z"] 135 | end 136 | 137 | assert_equal Rake::Scope.make("a"), @tm.current_scope 138 | 139 | assert_equal bb, @tm["a:b:bb"] 140 | assert_equal aa, @tm["a:aa"] 141 | assert_equal xx, @tm["xx"] 142 | assert_equal bb, @tm["b:bb"] 143 | assert_equal aa, @tm["aa"] 144 | assert_equal mid_z, @tm["z"] 145 | assert_equal top_z, @tm["^z"] 146 | assert_equal top_z, @tm["^^z"] # Over the top 147 | assert_equal top_z, @tm["rake:z"] 148 | assert_equal ns_d, @tm["n:t"] 149 | assert_equal ns_d, @tm["a:n:t"] 150 | end 151 | 152 | assert_equal Rake::Scope.make, @tm.current_scope 153 | 154 | assert_equal Rake::Scope.make, xx.scope 155 | assert_equal Rake::Scope.make("a"), aa.scope 156 | assert_equal Rake::Scope.make("b", "a"), bb.scope 157 | end 158 | 159 | def test_lookup_with_explicit_scopes 160 | t1, t2, t3, s = (0...4).map { nil } 161 | t1 = @tm.define_task(Rake::Task, :t) 162 | @tm.in_namespace("a") do 163 | t2 = @tm.define_task(Rake::Task, :t) 164 | s = @tm.define_task(Rake::Task, :s) 165 | @tm.in_namespace("b") do 166 | t3 = @tm.define_task(Rake::Task, :t) 167 | end 168 | end 169 | assert_equal t1, @tm[:t, Rake::Scope.make] 170 | assert_equal t2, @tm[:t, Rake::Scope.make("a")] 171 | assert_equal t3, @tm[:t, Rake::Scope.make("b", "a")] 172 | assert_equal s, @tm[:s, Rake::Scope.make("b", "a")] 173 | assert_equal s, @tm[:s, Rake::Scope.make("a")] 174 | end 175 | 176 | def test_correctly_scoped_prerequisites_are_invoked 177 | values = [] 178 | @tm = Rake::Application.new 179 | @tm.define_task(Rake::Task, :z) do values << "top z" end 180 | @tm.in_namespace("a") do 181 | @tm.define_task(Rake::Task, :z) do values << "next z" end 182 | @tm.define_task(Rake::Task, x: :z) 183 | end 184 | 185 | @tm["a:x"].invoke 186 | assert_equal ["next z"], values 187 | end 188 | 189 | end 190 | -------------------------------------------------------------------------------- /lib/rake/testtask.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative "../rake" 3 | require_relative "tasklib" 4 | 5 | module Rake 6 | 7 | # Create a task that runs a set of tests. 8 | # 9 | # Example: 10 | # require "rake/testtask" 11 | # 12 | # Rake::TestTask.new do |t| 13 | # t.libs << "test" 14 | # t.test_files = FileList['test/test*.rb'] 15 | # t.verbose = true 16 | # end 17 | # 18 | # If rake is invoked with a "TEST=filename" command line option, 19 | # then the list of test files will be overridden to include only the 20 | # filename specified on the command line. This provides an easy way 21 | # to run just one test. 22 | # 23 | # If rake is invoked with a "TESTOPTS=options" command line option, 24 | # then the given options are passed to the test process after a 25 | # '--'. This allows Test::Unit options to be passed to the test 26 | # suite. 27 | # 28 | # Examples: 29 | # 30 | # rake test # run tests normally 31 | # rake test TEST=just_one_file.rb # run just one test file. 32 | # rake test TESTOPTS="-v" # run in verbose mode 33 | # rake test TESTOPTS="--runner=fox" # use the fox test runner 34 | # 35 | class TestTask < TaskLib 36 | 37 | # Name of test task. (default is :test) 38 | attr_accessor :name 39 | 40 | # List of directories added to $LOAD_PATH before running the 41 | # tests. (default is 'lib') 42 | attr_accessor :libs 43 | 44 | # True if verbose test output desired. (default is false) 45 | attr_accessor :verbose 46 | 47 | # Test options passed to the test suite. An explicit 48 | # TESTOPTS=opts on the command line will override this. (default 49 | # is NONE) 50 | attr_accessor :options 51 | 52 | # Request that the tests be run with the warning flag set. 53 | # E.g. warning=true implies "ruby -w" used to run the tests. 54 | # (default is true) 55 | attr_accessor :warning 56 | 57 | # Glob pattern to match test files. (default is 'test/test*.rb') 58 | attr_accessor :pattern 59 | 60 | # Style of test loader to use. Options are: 61 | # 62 | # * :rake -- Rake provided test loading script (default). 63 | # * :testrb -- Ruby provided test loading script. 64 | # * :direct -- Load tests using command line loader. 65 | # 66 | attr_accessor :loader 67 | 68 | # Array of command line options to pass to ruby when running test loader. 69 | attr_accessor :ruby_opts 70 | 71 | # Description of the test task. (default is 'Run tests') 72 | attr_accessor :description 73 | 74 | # Task prerequisites. 75 | attr_accessor :deps 76 | 77 | # Explicitly define the list of test files to be included in a 78 | # test. +list+ is expected to be an array of file names (a 79 | # FileList is acceptable). If both +pattern+ and +test_files+ are 80 | # used, then the list of test files is the union of the two. 81 | def test_files=(list) 82 | @test_files = list 83 | end 84 | 85 | # Create a testing task. 86 | def initialize(name=:test) 87 | @name = name 88 | @libs = ["lib"] 89 | @pattern = nil 90 | @options = nil 91 | @test_files = nil 92 | @verbose = false 93 | @warning = true 94 | @loader = :rake 95 | @ruby_opts = [] 96 | @description = "Run tests" + (@name == :test ? "" : " for #{@name}") 97 | @deps = [] 98 | if @name.is_a?(Hash) 99 | @deps = @name.values.first 100 | @name = @name.keys.first 101 | end 102 | yield self if block_given? 103 | @pattern = "test/test*.rb" if @pattern.nil? && @test_files.nil? 104 | define 105 | end 106 | 107 | # Create the tasks defined by this task lib. 108 | def define 109 | desc @description 110 | task @name => Array(deps) do 111 | FileUtilsExt.verbose(@verbose) do 112 | puts "Use TESTOPTS=\"--verbose\" to pass --verbose" \ 113 | ", etc. to runners." if ARGV.include? "--verbose" 114 | args = 115 | "#{ruby_opts_string} #{run_code} " + 116 | "#{file_list_string} #{option_list}" 117 | ruby args do |ok, status| 118 | if !ok && status.respond_to?(:signaled?) && status.signaled? 119 | raise SignalException.new(status.termsig) 120 | elsif !ok 121 | status = "Command failed with status (#{status.exitstatus})" 122 | details = ": [ruby #{args}]" 123 | message = 124 | if Rake.application.options.trace or @verbose 125 | status + details 126 | else 127 | status 128 | end 129 | 130 | fail message 131 | end 132 | end 133 | end 134 | end 135 | self 136 | end 137 | 138 | def option_list # :nodoc: 139 | (ENV["TESTOPTS"] || 140 | ENV["TESTOPT"] || 141 | ENV["TEST_OPTS"] || 142 | ENV["TEST_OPT"] || 143 | @options || 144 | "") 145 | end 146 | 147 | def ruby_opts_string # :nodoc: 148 | opts = @ruby_opts.dup 149 | opts.unshift("-I\"#{lib_path}\"") unless @libs.empty? 150 | opts.unshift("-w") if @warning 151 | opts.join(" ") 152 | end 153 | 154 | def lib_path # :nodoc: 155 | @libs.join(File::PATH_SEPARATOR) 156 | end 157 | 158 | def file_list_string # :nodoc: 159 | file_list.map { |fn| "\"#{fn}\"" }.join(" ") 160 | end 161 | 162 | def file_list # :nodoc: 163 | if ENV["TEST"] 164 | FileList[ENV["TEST"]] 165 | else 166 | result = [] 167 | result += @test_files.to_a if @test_files 168 | result += FileList[@pattern].to_a if @pattern 169 | result 170 | end 171 | end 172 | 173 | def ruby_version # :nodoc: 174 | RUBY_VERSION 175 | end 176 | 177 | def run_code # :nodoc: 178 | case @loader 179 | when :direct 180 | "-e \"ARGV.each{|f| require f}\"" 181 | when :testrb 182 | "-S testrb" 183 | when :rake 184 | "#{__dir__}/rake_test_loader.rb" 185 | end 186 | end 187 | 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /test/test_rake_path_map.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require File.expand_path("../helper", __FILE__) 3 | 4 | class TestRakePathMap < Rake::TestCase # :nodoc: 5 | 6 | def test_returns_self_with_no_args 7 | assert_equal "abc.rb", "abc.rb".pathmap 8 | end 9 | 10 | def test_s_returns_file_separator 11 | sep = File::ALT_SEPARATOR || File::SEPARATOR 12 | assert_equal sep, "abc.rb".pathmap("%s") 13 | assert_equal sep, "".pathmap("%s") 14 | assert_equal "a#{sep}b", "a/b".pathmap("%d%s%f") 15 | end 16 | 17 | def test_f_returns_basename 18 | assert_equal "abc.rb", "abc.rb".pathmap("%f") 19 | assert_equal "abc.rb", "this/is/a/dir/abc.rb".pathmap("%f") 20 | assert_equal "abc.rb", "/this/is/a/dir/abc.rb".pathmap("%f") 21 | end 22 | 23 | def test_n_returns_basename_without_extension 24 | assert_equal "abc", "abc.rb".pathmap("%n") 25 | assert_equal "abc", "abc".pathmap("%n") 26 | assert_equal "abc", "this/is/a/dir/abc.rb".pathmap("%n") 27 | assert_equal "abc", "/this/is/a/dir/abc.rb".pathmap("%n") 28 | assert_equal "abc", "/this/is/a/dir/abc".pathmap("%n") 29 | end 30 | 31 | def test_d_returns_dirname 32 | assert_equal ".", "abc.rb".pathmap("%d") 33 | assert_equal "/", "/abc".pathmap("%d") 34 | assert_equal "this/is/a/dir", "this/is/a/dir/abc.rb".pathmap("%d") 35 | assert_equal "/this/is/a/dir", "/this/is/a/dir/abc.rb".pathmap("%d") 36 | end 37 | 38 | def test_9d_returns_partial_dirname 39 | assert_equal "this/is", "this/is/a/dir/abc.rb".pathmap("%2d") 40 | assert_equal "this", "this/is/a/dir/abc.rb".pathmap("%1d") 41 | assert_equal ".", "this/is/a/dir/abc.rb".pathmap("%0d") 42 | assert_equal "dir", "this/is/a/dir/abc.rb".pathmap("%-1d") 43 | assert_equal "a/dir", "this/is/a/dir/abc.rb".pathmap("%-2d") 44 | assert_equal "this/is/a/dir", "this/is/a/dir/abc.rb".pathmap("%100d") 45 | assert_equal "this/is/a/dir", "this/is/a/dir/abc.rb".pathmap("%-100d") 46 | end 47 | 48 | def test_x_returns_extension 49 | assert_equal "", "abc".pathmap("%x") 50 | assert_equal ".rb", "abc.rb".pathmap("%x") 51 | assert_equal ".rb", "abc.xyz.rb".pathmap("%x") 52 | assert_equal "", ".depends".pathmap("%x") 53 | assert_equal "", "dir/.depends".pathmap("%x") 54 | end 55 | 56 | def test_x_returns_everything_but_extension 57 | assert_equal "abc", "abc".pathmap("%X") 58 | assert_equal "abc", "abc.rb".pathmap("%X") 59 | assert_equal "abc.xyz", "abc.xyz.rb".pathmap("%X") 60 | assert_equal "ab.xyz", "ab.xyz.rb".pathmap("%X") 61 | assert_equal "a.xyz", "a.xyz.rb".pathmap("%X") 62 | assert_equal "abc", "abc.rb".pathmap("%X") 63 | assert_equal "ab", "ab.rb".pathmap("%X") 64 | assert_equal "a", "a.rb".pathmap("%X") 65 | assert_equal ".depends", ".depends".pathmap("%X") 66 | assert_equal "a/dir/.depends", "a/dir/.depends".pathmap("%X") 67 | assert_equal "/.depends", "/.depends".pathmap("%X") 68 | end 69 | 70 | def test_p_returns_entire_pathname 71 | assert_equal "abc.rb", "abc.rb".pathmap("%p") 72 | assert_equal "this/is/a/dir/abc.rb", "this/is/a/dir/abc.rb".pathmap("%p") 73 | assert_equal "/this/is/a/dir/abc.rb", "/this/is/a/dir/abc.rb".pathmap("%p") 74 | end 75 | 76 | def test_dash_returns_empty_string 77 | assert_equal "", "abc.rb".pathmap("%-") 78 | assert_equal "abc.rb", "abc.rb".pathmap("%X%-%x") 79 | end 80 | 81 | def test_percent_percent_returns_percent 82 | assert_equal "a%b", "".pathmap("a%%b") 83 | end 84 | 85 | def test_undefined_percent_causes_error 86 | assert_raises(ArgumentError) { 87 | "dir/abc.rb".pathmap("%z") 88 | } 89 | end 90 | 91 | def test_pattern_returns_substitutions 92 | assert_equal "bin/org/osb", 93 | "src/org/osb/Xyz.java".pathmap("%{src,bin}d") 94 | end 95 | 96 | def test_pattern_can_use_backreferences 97 | assert_equal "dir/hi/is", "dir/this/is".pathmap("%{t(hi)s,\\1}p") 98 | end 99 | 100 | def test_pattern_with_star_replacement_string_uses_block 101 | assert_equal "src/ORG/osb", 102 | "src/org/osb/Xyz.java".pathmap("%{/org,*}d") { |d| d.upcase } 103 | assert_equal "Xyz.java", 104 | "src/org/osb/Xyz.java".pathmap("%{.*,*}f") { |f| f.capitalize } 105 | end 106 | 107 | def test_pattern_with_no_replacement_nor_block_substitutes_empty_string 108 | assert_equal "bc.rb", "abc.rb".pathmap("%{a}f") 109 | end 110 | 111 | def test_pattern_works_with_certain_valid_operators 112 | assert_equal "dir/xbc.rb", "dir/abc.rb".pathmap("%{a,x}p") 113 | assert_equal "d1r", "dir/abc.rb".pathmap("%{i,1}d") 114 | assert_equal "xbc.rb", "dir/abc.rb".pathmap("%{a,x}f") 115 | assert_equal ".Rb", "dir/abc.rb".pathmap("%{r,R}x") 116 | assert_equal "xbc", "dir/abc.rb".pathmap("%{a,x}n") 117 | end 118 | 119 | def test_multiple_patterns 120 | assert_equal "this/is/b/directory/abc.rb", 121 | "this/is/a/dir/abc.rb".pathmap("%{a,b;dir,\\0ectory}p") 122 | end 123 | 124 | def test_partial_directory_selection_works_with_patterns 125 | assert_equal "this/is/a/long", 126 | "this/is/a/really/long/path/ok.rb".pathmap("%{/really/,/}5d") 127 | end 128 | 129 | def test_pattern_with_invalid_operator 130 | ex = assert_raises(ArgumentError) do 131 | "abc.xyz".pathmap("%{src,bin}z") 132 | end 133 | assert_match(/unknown.*pathmap.*spec.*z/i, ex.message) 134 | end 135 | 136 | def test_works_with_windows_separators 137 | if File::ALT_SEPARATOR 138 | assert_equal "abc", 'dir\abc.rb'.pathmap("%n") 139 | assert_equal 'this\is\a\dir', 140 | 'this\is\a\dir\abc.rb'.pathmap("%d") 141 | end 142 | end 143 | 144 | def test_complex_patterns 145 | sep = "".pathmap("%s") 146 | assert_equal( 147 | "dir/abc.rb", 148 | "dir/abc.rb".pathmap("%d/%n%x")) 149 | assert_equal( 150 | "./abc.rb", 151 | "abc.rb".pathmap("%d/%n%x")) 152 | assert_equal( 153 | "Your file extension is '.rb'", 154 | "dir/abc.rb".pathmap("Your file extension is '%x'")) 155 | assert_equal( 156 | "bin/org/onestepback/proj/A.class", 157 | "src/org/onestepback/proj/A.java".pathmap("%{src,bin}d/%n.class")) 158 | assert_equal( 159 | "src_work/bin/org/onestepback/proj/A.class", 160 | "src_work/src/org/onestepback/proj/A.java". 161 | pathmap('%{\bsrc\b,bin}X.class')) 162 | assert_equal( 163 | ".depends.bak", 164 | ".depends".pathmap("%X.bak")) 165 | assert_equal( 166 | "d#{sep}a/b/c#{sep}file.txt", 167 | "a/b/c/d/file.txt".pathmap("%-1d%s%3d%s%f")) 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /lib/rake/dsl_definition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Rake DSL functions. 3 | require_relative "file_utils_ext" 4 | 5 | module Rake 6 | 7 | ## 8 | # DSL is a module that provides #task, #desc, #namespace, etc. Use this 9 | # when you'd like to use rake outside the top level scope. 10 | # 11 | # For a Rakefile you run from the command line this module is automatically 12 | # included. 13 | 14 | module DSL 15 | 16 | #-- 17 | # Include the FileUtils file manipulation functions in the top 18 | # level module, but mark them private so that they don't 19 | # unintentionally define methods on other objects. 20 | #++ 21 | 22 | include FileUtilsExt 23 | private(*FileUtils.instance_methods(false)) 24 | private(*FileUtilsExt.instance_methods(false)) 25 | 26 | private 27 | 28 | # :call-seq: 29 | # task(task_name) 30 | # task(task_name: dependencies) 31 | # task(task_name, arguments => dependencies) 32 | # 33 | # Declare a basic task. The +task_name+ is always the first argument. If 34 | # the task name contains a ":" it is defined in that namespace. 35 | # 36 | # The +dependencies+ may be a single task name or an Array of task names. 37 | # The +argument+ (a single name) or +arguments+ (an Array of names) define 38 | # the arguments provided to the task. 39 | # 40 | # The task, argument and dependency names may be either symbols or 41 | # strings. 42 | # 43 | # A task with a single dependency: 44 | # 45 | # task clobber: %w[clean] do 46 | # rm_rf "html" 47 | # end 48 | # 49 | # A task with an argument and a dependency: 50 | # 51 | # task :package, [:version] => :test do |t, args| 52 | # # ... 53 | # end 54 | # 55 | # To invoke this task from the command line: 56 | # 57 | # $ rake package[1.2.3] 58 | # 59 | def task(*args, &block) # :doc: 60 | Rake::Task.define_task(*args, &block) 61 | end 62 | 63 | # Declare a file task. 64 | # 65 | # Example: 66 | # file "config.cfg" => ["config.template"] do 67 | # open("config.cfg", "w") do |outfile| 68 | # open("config.template") do |infile| 69 | # while line = infile.gets 70 | # outfile.puts line 71 | # end 72 | # end 73 | # end 74 | # end 75 | # 76 | def file(*args, &block) # :doc: 77 | Rake::FileTask.define_task(*args, &block) 78 | end 79 | 80 | # Declare a file creation task. 81 | # (Mainly used for the directory command). 82 | def file_create(*args, &block) 83 | Rake::FileCreationTask.define_task(*args, &block) 84 | end 85 | 86 | # Declare a set of files tasks to create the given directories on 87 | # demand. 88 | # 89 | # Example: 90 | # directory "testdata/doc" 91 | # 92 | def directory(*args, &block) # :doc: 93 | args = args.flat_map { |arg| arg.is_a?(FileList) ? arg.to_a.flatten : arg } 94 | result = file_create(*args, &block) 95 | dir, _ = *Rake.application.resolve_args(args) 96 | dir = Rake.from_pathname(dir) 97 | Rake.each_dir_parent(dir) do |d| 98 | file_create d do |t| 99 | mkdir_p t.name unless File.exist?(t.name) 100 | end 101 | end 102 | result 103 | end 104 | 105 | # Declare a task that performs its prerequisites in 106 | # parallel. Multitasks does *not* guarantee that its prerequisites 107 | # will execute in any given order (which is obvious when you think 108 | # about it) 109 | # 110 | # Example: 111 | # multitask deploy: %w[deploy_gem deploy_rdoc] 112 | # 113 | def multitask(*args, &block) # :doc: 114 | Rake::MultiTask.define_task(*args, &block) 115 | end 116 | 117 | # Create a new rake namespace and use it for evaluating the given 118 | # block. Returns a NameSpace object that can be used to lookup 119 | # tasks defined in the namespace. 120 | # 121 | # Example: 122 | # 123 | # ns = namespace "nested" do 124 | # # the "nested:run" task 125 | # task :run 126 | # end 127 | # task_run = ns[:run] # find :run in the given namespace. 128 | # 129 | # Tasks can also be defined in a namespace by using a ":" in the task 130 | # name: 131 | # 132 | # task "nested:test" do 133 | # # ... 134 | # end 135 | # 136 | def namespace(name=nil, &block) # :doc: 137 | name = name.to_s if name.kind_of?(Symbol) 138 | name = name.to_str if name.respond_to?(:to_str) 139 | unless name.kind_of?(String) || name.nil? 140 | raise ArgumentError, "Expected a String or Symbol for a namespace name" 141 | end 142 | Rake.application.in_namespace(name, &block) 143 | end 144 | 145 | # Declare a rule for auto-tasks. 146 | # 147 | # Example: 148 | # rule '.o' => '.c' do |t| 149 | # sh 'cc', '-c', '-o', t.name, t.source 150 | # end 151 | # 152 | def rule(*args, &block) # :doc: 153 | Rake::Task.create_rule(*args, &block) 154 | end 155 | 156 | # Describes the next rake task. Duplicate descriptions are discarded. 157 | # Descriptions are shown with rake -T (up to the first 158 | # sentence) and rake -D (the entire description). 159 | # 160 | # Example: 161 | # desc "Run the Unit Tests" 162 | # task test: [:build] do 163 | # # ... run tests 164 | # end 165 | # 166 | def desc(description) # :doc: 167 | Rake.application.last_description = description 168 | end 169 | 170 | # Import the partial Rakefiles +fn+. Imported files are loaded 171 | # _after_ the current file is completely loaded. This allows the 172 | # import statement to appear anywhere in the importing file, and yet 173 | # allowing the imported files to depend on objects defined in the 174 | # importing file. 175 | # 176 | # A common use of the import statement is to include files 177 | # containing dependency declarations. 178 | # 179 | # See also the --rakelibdir command line option. 180 | # 181 | # Example: 182 | # import ".depend", "my_rules" 183 | # 184 | def import(*fns) # :doc: 185 | fns.each do |fn| 186 | Rake.application.add_import(fn) 187 | end 188 | end 189 | end 190 | extend FileUtilsExt 191 | end 192 | 193 | # Extend the main object with the DSL commands. This allows top-level 194 | # calls to task, etc. to work from a Rakefile without polluting the 195 | # object inheritance tree. 196 | self.extend Rake::DSL 197 | -------------------------------------------------------------------------------- /lib/rake/ext/string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative "core" 3 | 4 | class String 5 | 6 | rake_extension("ext") do 7 | # Replace the file extension with +newext+. If there is no extension on 8 | # the string, append the new extension to the end. If the new extension 9 | # is not given, or is the empty string, remove any existing extension. 10 | # 11 | # +ext+ is a user added method for the String class. 12 | # 13 | # This String extension comes from Rake 14 | def ext(newext="") 15 | return self.dup if [".", ".."].include? self 16 | if newext != "" 17 | newext = "." + newext unless newext =~ /^\./ 18 | end 19 | self.chomp(File.extname(self)) << newext 20 | end 21 | end 22 | 23 | rake_extension("pathmap") do 24 | # Explode a path into individual components. Used by +pathmap+. 25 | # 26 | # This String extension comes from Rake 27 | def pathmap_explode 28 | head, tail = File.split(self) 29 | return [self] if head == self 30 | return [tail] if head == "." || tail == "/" 31 | return [head, tail] if head == "/" 32 | return head.pathmap_explode + [tail] 33 | end 34 | protected :pathmap_explode 35 | 36 | # Extract a partial path from the path. Include +n+ directories from the 37 | # front end (left hand side) if +n+ is positive. Include |+n+| 38 | # directories from the back end (right hand side) if +n+ is negative. 39 | # 40 | # This String extension comes from Rake 41 | def pathmap_partial(n) 42 | dirs = File.dirname(self).pathmap_explode 43 | partial_dirs = 44 | if n > 0 45 | dirs[0...n] 46 | elsif n < 0 47 | dirs.reverse[0...-n].reverse 48 | else 49 | "." 50 | end 51 | File.join(partial_dirs) 52 | end 53 | protected :pathmap_partial 54 | 55 | # Perform the pathmap replacement operations on the given path. The 56 | # patterns take the form 'pat1,rep1;pat2,rep2...'. 57 | # 58 | # This String extension comes from Rake 59 | def pathmap_replace(patterns, &block) 60 | result = self 61 | patterns.split(";").each do |pair| 62 | pattern, replacement = pair.split(",") 63 | pattern = Regexp.new(pattern) 64 | if replacement == "*" && block_given? 65 | result = result.sub(pattern, &block) 66 | elsif replacement 67 | result = result.sub(pattern, replacement) 68 | else 69 | result = result.sub(pattern, "") 70 | end 71 | end 72 | result 73 | end 74 | protected :pathmap_replace 75 | 76 | # Map the path according to the given specification. The specification 77 | # controls the details of the mapping. The following special patterns are 78 | # recognized: 79 | # 80 | # %p :: The complete path. 81 | # %f :: The base file name of the path, with its file extension, 82 | # but without any directories. 83 | # %n :: The file name of the path without its file extension. 84 | # %d :: The directory list of the path. 85 | # %x :: The file extension of the path. An empty string if there 86 | # is no extension. 87 | # %X :: Everything *but* the file extension. 88 | # %s :: The alternate file separator if defined, otherwise use # 89 | # the standard file separator. 90 | # %% :: A percent sign. 91 | # 92 | # The %d specifier can also have a numeric prefix (e.g. '%2d'). 93 | # If the number is positive, only return (up to) +n+ directories in the 94 | # path, starting from the left hand side. If +n+ is negative, return (up 95 | # to) +n+ directories from the right hand side of the path. 96 | # 97 | # Examples: 98 | # 99 | # 'a/b/c/d/file.txt'.pathmap("%2d") => 'a/b' 100 | # 'a/b/c/d/file.txt'.pathmap("%-2d") => 'c/d' 101 | # 102 | # Also the %d, %p, %f, %n, 103 | # %x, and %X operators can take a pattern/replacement 104 | # argument to perform simple string substitutions on a particular part of 105 | # the path. The pattern and replacement are separated by a comma and are 106 | # enclosed by curly braces. The replacement spec comes after the % 107 | # character but before the operator letter. (e.g. "%{old,new}d"). 108 | # Multiple replacement specs should be separated by semi-colons (e.g. 109 | # "%{old,new;src,bin}d"). 110 | # 111 | # Regular expressions may be used for the pattern, and back refs may be 112 | # used in the replacement text. Curly braces, commas and semi-colons are 113 | # excluded from both the pattern and replacement text (let's keep parsing 114 | # reasonable). 115 | # 116 | # For example: 117 | # 118 | # "src/org/onestepback/proj/A.java".pathmap("%{^src,class}X.class") 119 | # 120 | # returns: 121 | # 122 | # "class/org/onestepback/proj/A.class" 123 | # 124 | # If the replacement text is '*', then a block may be provided to perform 125 | # some arbitrary calculation for the replacement. 126 | # 127 | # For example: 128 | # 129 | # "/path/to/file.TXT".pathmap("%X%{.*,*}x") { |ext| 130 | # ext.downcase 131 | # } 132 | # 133 | # Returns: 134 | # 135 | # "/path/to/file.txt" 136 | # 137 | # This String extension comes from Rake 138 | def pathmap(spec=nil, &block) 139 | return self if spec.nil? 140 | result = "".dup 141 | spec.scan(/%\{[^}]*\}-?\d*[sdpfnxX%]|%-?\d+d|%.|[^%]+/) do |frag| 142 | case frag 143 | when "%f" 144 | result << File.basename(self) 145 | when "%n" 146 | result << File.basename(self).ext 147 | when "%d" 148 | result << File.dirname(self) 149 | when "%x" 150 | result << File.extname(self) 151 | when "%X" 152 | result << self.ext 153 | when "%p" 154 | result << self 155 | when "%s" 156 | result << (File::ALT_SEPARATOR || File::SEPARATOR) 157 | when "%-" 158 | # do nothing 159 | when "%%" 160 | result << "%" 161 | when /%(-?\d+)d/ 162 | result << pathmap_partial($1.to_i) 163 | when /^%\{([^}]*)\}(\d*[dpfnxX])/ 164 | patterns, operator = $1, $2 165 | result << pathmap("%" + operator).pathmap_replace(patterns, &block) 166 | when /^%/ 167 | fail ArgumentError, "Unknown pathmap specifier #{frag} in '#{spec}'" 168 | else 169 | result << frag 170 | end 171 | end 172 | result 173 | end 174 | end 175 | 176 | end 177 | -------------------------------------------------------------------------------- /doc/rational.rdoc: -------------------------------------------------------------------------------- 1 | = Why rake? 2 | 3 | Ok, let me state from the beginning that I never intended to write this 4 | code. I'm not convinced it is useful, and I'm not convinced anyone 5 | would even be interested in it. All I can say is that Why's onion truck 6 | must by been passing through the Ohio valley. 7 | 8 | What am I talking about? ... A Ruby version of Make. 9 | 10 | See, I can sense you cringing already, and I agree. The world certainly 11 | doesn't need yet another reworking of the "make" program. I mean, we 12 | already have "ant". Isn't that enough? 13 | 14 | It started yesterday. I was helping a coworker fix a problem in one of 15 | the Makefiles we use in our project. Not a particularly tough problem, 16 | but during the course of the conversation I began lamenting some of the 17 | shortcomings of make. In particular, in one of my makefiles I wanted to 18 | determine the name of a file dynamically and had to resort to some 19 | simple scripting (in Ruby) to make it work. "Wouldn't it be nice if you 20 | could just use Ruby inside a Makefile" I said. 21 | 22 | My coworker (a recent convert to Ruby) agreed, but wondered what it 23 | would look like. So I sketched the following on the whiteboard... 24 | 25 | "What if you could specify the make tasks in Ruby, like this ..." 26 | 27 | task "build" do 28 | java_compile(...args, etc ...) 29 | end 30 | 31 | "The task function would register "build" as a target to be made, 32 | and the block would be the action executed whenever the build 33 | system determined that it was time to do the build target." 34 | 35 | We agreed that would be cool, but writing make from scratch would be WAY 36 | too much work. And that was the end of that! 37 | 38 | ... Except I couldn't get the thought out of my head. What exactly 39 | would be needed to make the about syntax work as a make file? Hmmm, you 40 | would need to register the tasks, you need some way of specifying 41 | dependencies between tasks, and some way of kicking off the process. 42 | Hey! What if we did ... and fifteen minutes later I had a working 43 | prototype of Ruby make, complete with dependencies and actions. 44 | 45 | I showed the code to my coworker and we had a good laugh. It was just 46 | about a page worth of code that reproduced an amazing amount of the 47 | functionality of make. We were both truly stunned with the power of 48 | Ruby. 49 | 50 | But it didn't do everything make did. In particular, it didn't have 51 | timestamp based file dependencies (where a file is rebuilt if any of its 52 | prerequisite files have a later timestamp). Obviously THAT would be a 53 | pain to add and so Ruby Make would remain an interesting experiment. 54 | 55 | ... Except as I walked back to my desk, I started thinking about what 56 | file based dependencies would really need. Rats! I was hooked again, 57 | and by adding a new class and two new methods, file/timestamp 58 | dependencies were implemented. 59 | 60 | Ok, now I was really hooked. Last night (during CSI!) I massaged the 61 | code and cleaned it up a bit. The result is a bare-bones replacement 62 | for make in exactly 100 lines of code. 63 | 64 | For the curious, you can see it at ... 65 | * doc/proto_rake.rdoc 66 | 67 | Oh, about the name. When I wrote the example Ruby Make task on my 68 | whiteboard, my coworker exclaimed "Oh! I have the perfect name: Rake ... 69 | Get it? Ruby-Make. Rake!" He said he envisioned the tasks as leaves 70 | and Rake would clean them up ... or something like that. Anyways, the 71 | name stuck. 72 | 73 | Some quick examples ... 74 | 75 | A simple task to delete backup files ... 76 | 77 | task :clean do 78 | Dir['*~'].each {|fn| rm fn rescue nil} 79 | end 80 | 81 | Note that task names are symbols (they are slightly easier to type 82 | than quoted strings ... but you may use quoted string if you would 83 | rather). Rake makes the methods of the FileUtils module directly 84 | available, so we take advantage of the rm command. Also note 85 | the use of "rescue nil" to trap and ignore errors in the rm 86 | command. 87 | 88 | To run it, just type "rake clean". Rake will automatically find a 89 | Rakefile in the current directory (or above!) and will invoke the 90 | targets named on the command line. If there are no targets explicitly 91 | named, rake will invoke the task "default". 92 | 93 | Here's another task with dependencies ... 94 | 95 | task :clobber => [:clean] do 96 | rm_r "tempdir" 97 | end 98 | 99 | Task :clobber depends upon task :clean, so :clean will be run before 100 | :clobber is executed. 101 | 102 | Files are specified by using the "file" command. It is similar to the 103 | task command, except that the task name represents a file, and the task 104 | will be run only if the file doesn't exist, or if its modification time 105 | is earlier than any of its prerequisites. 106 | 107 | Here is a file based dependency that will compile "hello.cc" to 108 | "hello.o". 109 | 110 | file "hello.cc" 111 | file "hello.o" => ["hello.cc"] do |t| 112 | srcfile = t.name.sub(/\.o$/, ".cc") 113 | sh %{g++ #{srcfile} -c -o #{t.name}} 114 | end 115 | 116 | I normally specify file tasks with string (rather than symbols). Some 117 | file names can't be represented by symbols. Plus it makes the 118 | distinction between them more clear to the casual reader. 119 | 120 | Currently writing a task for each and every file in the project would be 121 | tedious at best. I envision a set of libraries to make this job 122 | easier. For instance, perhaps something like this ... 123 | 124 | require 'rake/ctools' 125 | Dir['*.c'].each do |fn| 126 | c_source_file(fn) 127 | end 128 | 129 | where "c_source_file" will create all the tasks need to compile all the 130 | C source files in a directory. Any number of useful libraries could be 131 | created for rake. 132 | 133 | That's it. There's no documentation (other than whats in this 134 | message). Does this sound interesting to anyone? If so, I'll continue 135 | to clean it up and write it up and publish it on RAA. Otherwise, I'll 136 | leave it as an interesting exercise and a tribute to the power of Ruby. 137 | 138 | Why /might/ rake be interesting to Ruby programmers. I don't know, 139 | perhaps ... 140 | 141 | * No weird make syntax (only weird Ruby syntax :-) 142 | * No need to edit or read XML (a la ant) 143 | * Platform independent build scripts. 144 | * Will run anywhere Ruby exists, so no need to have "make" installed. 145 | If you stay away from the "sys" command and use things like 146 | 'ftools', you can have a perfectly platform independent 147 | build script. Also rake is only 100 lines of code, so it can 148 | easily be packaged along with the rest of your code. 149 | 150 | So ... Sorry for the long rambling message. Like I said, I never 151 | intended to write this code at all. 152 | --------------------------------------------------------------------------------