Rakefile 格式

首先,Rakefile 没有特殊的格式。Rakefile 包含可执行的 Ruby 代码。在 Ruby 脚本中合法的任何内容都可以在 Rakefile 中使用。

既然我们了解 Rakefile 中没有特殊的语法,那么在 Rakefile 中使用的一些约定在典型的 Ruby 程序中有点不寻常。由于 Rakefile 专为指定任务和操作而定制,因此 Rakefile 中使用的习语旨在支持这一点。

那么,Rakefile 中包含什么呢?

任务

任务是 Rakefile 中的主要工作单元。任务具有名称(通常以符号或字符串形式给出)、先决条件列表(更多符号或字符串)和操作列表(以块的形式给出)。

简单任务

通过使用 task 方法声明任务。task 接收一个参数,即任务的名称。

task :name

带有先决条件的任务

任何先决条件都作为列表(用方括号括起来)在名称和箭头(=>)之后给出。

task name: [:prereq1, :prereq2]

注意: 虽然此语法看起来有点古怪,但它是合法的 Ruby。我们正在构建一个哈希,其中键是 :name,而该键的值是先决条件列表。它等效于以下内容……

hash = Hash.new
hash[:name] = [:prereq1, :prereq2]
task(hash)

您也可以使用字符串作为任务名称和先决条件,rake 不关心。这与以下任务定义相同

task 'name' => %w[prereq1 prereq2]

就像这样

task name: %w[prereq1 prereq2]

在本文档的其余部分中,我们将首选这种用于带有先决条件的常规任务的样式。使用字符串数组作为先决条件意味着如果您需要将任务移动到命名空间或执行其他重构,则需要进行较少的更改。

带有操作的任务

通过将块传递给 task 方法来定义操作。任何 Ruby 代码都可以放置在块中。该块可以通过块参数引用任务对象。

task name: [:prereq1, :prereq2] do |t|
  # actions (may reference t)
end

多重定义

可以多次指定任务。每个规范都会将其先决条件和操作添加到现有定义中。这允许 rakefile 的一部分指定操作,而不同的 rakefile(可能单独生成)指定依赖项。

例如,以下内容等效于上面给出的单个任务规范。

task :name
task name: :prereq1
task name: %w[prereq2]
task :name do |t|
  # actions
end

文件任务

某些任务旨在从一个或多个其他文件创建文件。如果文件已存在,则可以跳过生成这些文件的任务。文件任务用于指定文件创建任务。

文件任务使用 file 方法(而不是 task 方法)声明。此外,文件任务通常使用字符串而不是符号来命名。

以下文件任务在给定两个名为 a.ob.o 的目标文件的情况下创建一个可执行程序(名为 prog)。未显示用于创建 a.ob.o 的任务。

file "prog" => ["a.o", "b.o"] do |t|
  sh "cc -o #{t.name} #{t.prerequisites.join(' ')}"
end

目录任务

通常需要按需创建目录。directory 便利方法是创建 FileTask 的简写,该 FileTask 创建目录。例如,以下声明…

directory "testdata/examples/doc"

等效于…

file "testdata" do |t| mkdir t.name end
file "testdata/examples" => ["testdata"] do |t| mkdir t.name end
file "testdata/examples/doc" => ["testdata/examples"] do |t| mkdir t.name end

directory 方法不接受先决条件或操作,但以后可以添加先决条件和操作。例如…

directory "testdata"
file "testdata" => ["otherdata"]
file "testdata" do
  cp Dir["standard_data/*.data"], "testdata"
end

带有并行先决条件的任务

Rake 允许使用以下语法并行执行先决条件

multitask copy_files: %w[copy_src copy_doc copy_bin] do
  puts "All Copies Complete"
end

在此示例中,copy_files 是一个普通的 rake 任务。每当其所有先决条件完成时,都会执行其操作。最大的区别在于先决条件(copy_srccopy_bincopy_doc)是并行执行的。每个先决条件都在其自己的 Ruby 线程中运行,这可能会加快整体运行时。

二级先决条件

如果多任务的任何主要先决条件具有共同的二级先决条件,则所有主要/并行先决条件将等待直到运行了共同的先决条件。

例如,如果 copy_xxx 任务具有以下先决条件

task copy_src: :prep_for_copy
task copy_bin: :prep_for_copy
task copy_doc: :prep_for_copy

那么 prep_for_copy 任务会在并行启动所有复制之前运行。一旦 prep_for_copy 完成,copy_srccopy_bincopy_doc 都会并行运行。请注意,即使在多个线程中引用了 prep_for_copy,它也只会运行一次。

线程安全

Rake 内部数据结构在多任务并行执行方面是线程安全的,因此用户无需为 Rake 的好处进行额外的同步。但是,如果并行先决条件之间存在共享的用户数据结构,则用户必须采取任何必要的措施来防止竞争条件。

带有参数的任务

在 0.8.0 版本之前,rake 只能处理通过 ENV 哈希传递到 Rake 中的 NAME=VALUE 形式的命令行参数。许多人要求使用某种简单的命令行参数,也许可以使用“–”来分隔命令行中的常规任务名称和参数值。问题在于,没有简单的方法可以将命令行上的位置参数与不同的任务相关联。假设任务 :a 和 :b 都需要一个命令行参数:第一个值是与 :a 一起使用吗?如果 :b 先运行怎么办?它应该获得第一个命令行参数吗。

Rake 0.8.0 通过将值直接传递给需要它们的任务来解决此问题。例如,如果我有一个需要版本号的发布任务,则可以说

rake release[0.8.2]

字符串“0.8.2”将传递给 :release 任务。可以通过用逗号分隔来传递多个参数,例如

rake name[john,doe]

只需注意几点。rake 任务名称及其参数需要是 rake 的单个命令行参数。通常这意味着没有空格。如果需要空格,则应该引用整个名称 + 参数字符串。类似这样

rake "name[billy bob, smith]"

(不同操作系统和 shell 之间的引用规则有所不同,因此请确保查阅您的 OS/shell 的正确文档)。

需要参数的任务

参数只会提供给设置为需要它们的任务。为了处理命名参数,任务的任务声明语法已略有扩展。

例如,需要名字和姓氏的任务可以声明为

task :name, [:first_name, :last_name]

第一个参数仍然是任务的名称(在本例中为 :name)。接下来的两个参数是 :name 在数组中需要的参数的名称(示例中的 :first_name 和 :last_name)。

要访问参数的值,定义任务行为的块现在可以接受第二个参数

task :name, [:first_name, :last_name] do |t, args|
  puts "First name is #{args.first_name}"
  puts "Last  name is #{args.last_name}"
end

块“t”的第一个参数始终绑定到当前任务对象。第二个参数“args”是一个类似开放结构的对象,允许访问任务参数。额外的任务命令行参数将被忽略。

如果您希望为参数指定默认值,则可以在任务主体中使用 with_defaults 方法。这是上面的示例,我们在其中为名字和姓氏指定默认值

task :name, [:first_name, :last_name] do |t, args|
  args.with_defaults(:first_name => "John", :last_name => "Dough")
  puts "First name is #{args.first_name}"
  puts "Last  name is #{args.last_name}"
end

需要参数且具有先决条件的任务

使用参数的任务的先决条件格式略有不同。使用箭头符号表示带有参数的任务的先决条件。例如

task :name, [:first_name, :last_name] => [:pre_name] do |t, args|
  args.with_defaults(:first_name => "John", :last_name => "Dough")
  puts "First name is #{args.first_name}"
  puts "Last  name is #{args.last_name}"
end

接受可变长度参数的任务

需要将值列表作为参数处理的任务可以使用 args 变量的 extras 方法。这允许可以循环遍历可变数量的值的任务,并且它也与命名参数兼容

task :email, [:message] do |t, args|
  mail = Mail.new(args.message)
  recipients = args.extras
  recipients.each do |target|
    mail.send_to(target)
  end
end

还有一个方便的方法 to_a,它按给定的顺序返回所有参数,包括与命名参数关联的参数。

已弃用的任务参数格式

有一种较旧的格式用于声明省略任务参数数组并使用 :needs 关键字引入依赖项的任务参数。该格式仍然支持兼容性,但不建议使用。旧格式可能会在以后的 rake 版本中删除。

以编程方式访问任务

有时在 Rakefile 中以编程方式操作任务很有用。要查找任务对象,请使用 Rake::Task.[]

编程任务示例

例如,以下 Rakefile 定义了两个任务。:doit 任务仅打印一个简单的“DONE”消息。:dont 类将查找 doit 类并删除(清除)其所有先决条件和操作。

task :doit do
  puts "DONE"
end

task :dont do
  Rake::Task[:doit].clear
end

运行此示例

$ rake doit
(in /Users/jim/working/git/rake/x)
DONE
$ rake dont doit
(in /Users/jim/working/git/rake/x)
$

以编程方式操作任务的能力使 Rake 在任务执行方面具有非常强大的元编程能力,但应谨慎使用。

规则

当一个文件被命名为先决条件,但没有为其定义文件任务时,Rake 将尝试通过查看 Rakefile 中提供的一系列规则来合成一个任务。

假设我们尝试调用任务“mycode.o”,但没有为其定义任务。但是,rakefile 中有一个如下所示的规则……

rule '.o' => ['.c'] do |t|
  sh "cc #{t.source} -c -o #{t.name}"
end

此规则将合成任何以“.o”结尾的任务。它有一个先决条件,即必须存在一个扩展名为“.c”的源文件。如果 Rake 能够找到一个名为“mycode.c”的文件,它将自动创建一个任务,该任务从“mycode.c”构建“mycode.o”。

如果文件“mycode.c”不存在,Rake 将尝试递归合成一个适用于它的规则。

当从规则合成任务时,任务的 source 属性设置为匹配的源文件。这允许我们编写带有引用源文件操作的规则。

高级规则

任何正则表达式都可以用作规则模式。此外,可以使用 proc 来计算源文件的名称。这允许复杂的模式和来源。

以下规则与上面的示例等效。

rule( /\.o$/ => [
  proc {|task_name| task_name.sub(/\.[^.]+$/, '.c') }
]) do |t|
  sh "cc #{t.source} -c -o #{t.name}"
end

注意:由于 Ruby 语法中的一个怪癖,当第一个参数是正则表达式时,rule 需要使用括号。

以下规则可能用于 Java 文件……

rule '.class' => [
  proc { |tn| tn.sub(/\.class$/, '.java').sub(/^classes\//, 'src/') }
] do |t|
  java_compile(t.source, t.name)
end

注意: java_compile 是一个假设的方法,它调用 java 编译器。

导入依赖项

可以使用标准的 Ruby require 命令包含任何 ruby 文件(包括其他 rakefile)。所需文件中的规则和声明只是添加到已累积的定义中。

因为文件是在评估 rake 目标之前加载的,所以当调用 rake 命令时,加载的文件必须“准备就绪”。这使得生成依赖文件变得难以使用。在 rake 开始更新依赖文件时,加载它为时已晚。

import 命令通过指定一个在加载主 rakefile 之后,但在调用命令行上的任何目标之前加载的文件来解决此问题。此外,如果文件名与显式任务匹配,则在加载文件之前调用该任务。这允许在单个 rake 命令调用中生成和使用依赖文件。

示例

require 'rake/loaders/makefile'

file ".depends.mf" => [SRC_LIST] do |t|
  sh "makedepend -f- -- #{CFLAGS} -- #{t.prerequisites} > #{t.name}"
end

import ".depends.mf"

如果 “.depends” 不存在,或者相对于源文件过时,则在使用 makedepend 加载之前,会生成一个新的 “.depends” 文件。

注释

标准的 Ruby 注释(以“#”开头)可以用于 Ruby 源代码中任何合法的位置,包括任务和规则的注释。但是,如果您希望使用“-T”开关描述任务,则需要使用 desc 命令来描述该任务。

示例

desc "Create a distribution package"
task package: %w[ ... ] do ... end

“-T”开关(如果您喜欢拼写出来,则为“--tasks”)将显示具有描述的任务列表。如果您使用 desc 来描述您的主要任务,您将获得一种半自动的方式来生成 Rake 文件的摘要。

$ rake -T
(in /home/.../rake)
rake clean            # Remove any temporary products.
rake clobber          # Remove any generated file.
rake clobber_rdoc     # Remove rdoc products
rake contrib_test     # Run tests for contrib_test
rake default          # Default Task
rake install          # Install the application
rake lines            # Count lines in the main rake file
rake rdoc             # Build the rdoc HTML Files
rake rerdoc           # Force a rebuild of the RDOC files
rake test             # Run tests
rake testall          # Run all test targets

只有具有描述的任务才会显示在“-T”开关中。使用“-P”(或“--prereqs”)来获取所有任务及其先决条件的列表。

命名空间

随着项目的发展(以及任务数量的增加),任务名称经常开始冲突。例如,您可能有一个主程序和一组由单个 Rakefile 构建的示例程序。通过将与主程序相关的任务放在一个命名空间中,并将用于构建示例程序的任务放在另一个命名空间中,任务名称将不会相互干扰。

例如

namespace "main" do
  task :build do
    # Build the main program
  end
end

namespace "samples" do
  task :build do
    # Build the sample programs
  end
end

task build: %w[main:build samples:build]

可以通过在任务名称前加上命名空间和一个冒号来引用单独命名空间中的任务(例如,“main:build”是指 main 命名空间中的 :build 任务)。支持嵌套命名空间。

请注意,task 命令中给出的名称始终是不带任何命名空间前缀的未修饰的任务名称。task 命令始终在当前命名空间中定义任务。

文件任务

文件任务名称不受命名空间命令的作用域限制。由于文件任务的名称是文件系统中实际文件的名称,因此在命名空间中包含文件任务名称没有多大意义。目录任务(由 directory 命令创建)是一种文件任务,也不受命名空间的影响。

名称解析

当查找任务名称时,rake 将从当前命名空间开始,并尝试在那里找到该名称。如果在当前命名空间中找不到名称,它将搜索父命名空间,直到找到匹配项(如果没有任何匹配项,则会发生错误)。

“rake”命名空间是一个特殊的隐式命名空间,它引用顶层名称。

如果任务名称以“^”字符开头,则名称解析将从父命名空间开始。允许使用多个“^”字符。

这是一个包含多个 :run 任务的示例文件,以及各种名称在不同位置的解析方式。

task :run

namespace "one" do
  task :run

  namespace "two" do
    task :run

    # :run            => "one:two:run"
    # "two:run"       => "one:two:run"
    # "one:two:run"   => "one:two:run"
    # "one:run"       => "one:run"
    # "^run"          => "one:run"
    # "^^run"         => "rake:run" (the top level task)
    # "rake:run"      => "rake:run" (the top level task)
  end

  # :run       => "one:run"
  # "two:run"  => "one:two:run"
  # "^run"     => "rake:run"
end

# :run           => "rake:run"
# "one:run"      => "one:run"
# "one:two:run"  => "one:two:run"

文件列表

FileList 是 Rake 管理文件列表的方式。在大多数情况下,您可以将 FileList 视为字符串数组,但 FileList 支持一些其他操作。

创建 FileList

创建文件列表很容易。只需提供文件名列表即可

fl = FileList['file1.rb', file2.rb']

或提供一个 glob 模式

fl = FileList['*.rb']

杂项

do/end 与 { }

可以使用 do/end 对或 Ruby 中的花括号来指定块。我们强烈建议使用 do/end 来指定任务和规则的操作。由于 rakefile 习惯在 task/file/rule 方法上省略括号,因此在使用花括号时可能会出现不寻常的歧义。

例如,假设方法 object_files 返回项目中的对象文件列表。现在,我们在规则中使用 object_files 作为先决条件,并在花括号中指定操作。

# DON'T DO THIS!
file "prog" => object_files {
  # Actions are expected here (but it doesn't work)!
}

因为花括号的优先级高于 do/end,所以该块与 object_files 方法而不是 file 方法关联。

这是指定任务的正确方法…

# THIS IS FINE
file "prog" => object_files do
  # Actions go here
end

Rakefile 路径

当在终端中发出 rake 命令时,Rake 将在当前目录中查找 Rakefile。如果找不到 Rakefile,它将搜索父目录,直到找到一个为止。

例如,如果 Rakefile 位于 project/ 目录中,则深入到项目的目录树中不会对 rake 任务产生不利影响

$ pwd
/home/user/project

$ cd lib/foo/bar
$ pwd
/home/user/project/lib/foo/bar

$ rake run_pwd
/home/user/project

就 rake 而言,所有任务都是从 Rakefile 所在的目录中运行的。

多个 Rake 文件

并非所有任务都需要包含在单个 Rakefile 中。其他 rake 文件(文件扩展名为 “.rake”)可以放置在位于项目顶层的 rakelib 目录中(即,与主 Rakefile 相同的目录)。

此外,rails 项目可以在 lib/tasks 目录中包含其他 rake 文件。

Clean 和 Clobber 任务

通过 require 'rake/clean',Rake 提供 cleanclobber 任务

clean

通过删除临时文件和备份文件来清理项目。将文件添加到 CLEAN FileList,以便让 clean 目标处理它们。

clobber

清空项目中所有生成的文件和非源文件。该任务依赖于 clean,因此所有 CLEAN 文件都将被删除,以及 CLOBBER FileList 中的文件。此任务的目的是将项目恢复到其原始的、刚刚解包的状态。

您可以将文件名或 glob 模式添加到 CLEANCLOBBER 列表中。

伪任务

伪任务可以用作依赖项,以允许基于文件的任务使用非基于文件的任务作为先决条件,而无需强制它们重建。您可以 require 'rake/phony' 来添加 phony 任务。


参见