内容


使用 MakeMaker 构建 Perl 项目

制作文件和更多其它事物的模块

Comments

编程产品 vs. 程序

在 Frederick Brook 的 The Mythical Man-Month,他提出了下列问题:为什么有报道称,创建相同功能的新程序时,只有几个人的小作坊的用时明显少于更大的实体或公司。为描述这个难题,他区别两种类型的软件:程序和编程产品。

程序是在某一环境下具有一定功能的一个软件。它可以是本地 bin 目录中的一组脚本。 编程产品需要采用一个程序并添加许多附加项。这包括详尽和可用的文档,易于安装的过程以及一组测试用例以确保程序完成设想的操作。 Brooks 从理论上指出创建编程产品至少需要付出三倍于创建程序的工作。虽然这象是很大的工作量,但是却是徒劳的。 程序对于更多人来说是非常有用的,对于您自己则更有用。有几次发现每次使用您的脚本时需要阅读其源代码的情况?查阅每个脚本的帮助手册是否更方便呢?

Perl 常常被认为是“瑞士军刀”或一大卷管带。使用 UNIX 的人(无论其使用时间长短),最终都无法避免编写一些 Perl 代码。不幸的是,许多人没有认识到 Perl 也包含 Brooksian 观念中的构建编程产品的健壮的基础结构。这个基础结构的核心 是称为 ExtUtils::MakeMaker 的 Perl 模块。在其核心处, MakeMaker 提供生成 Makefile 的 Perl 接口。但是,它所完成的工作还不止这些。由 MakeMaker 创建 的 Makefile 将不仅构建和安装项目,还将生成帮助手册或 HTML 格式的文档,并对整个应用程序运行一组自动化的测试用例。

因为这个功能与核心 Perl 基础结构结合得很紧密,所以学习和使用它的开销很小。优势却是很大的。一旦为应用程序创建了正确的测试用例,没有您的知识项目将无法回归。任何破坏旧功能的更改将突然抛出红色标志。这会更加确保您对于每个添加到代码中的新补丁不会重新引入旧的错误。

与 Brooks 的三个工作增长的因素相比,我已经发现通过使用这些方法,确实节省了许多时间。一旦修订了错误,可能它就不变了。嵌入到代码(文档描述了这些代码)中的文档比外部文档更易于更新。另外,免费获得一个安装脚本。

本文不是教您如何编写 Perl,而是向您演示如何将 Perl 程序转换成更为健壮的编程项目。该项目将很通用,适合于广泛分发到许多完全不同的平台上。

MakeMaker 项目剖析

Programming Perl,3rd Editio 封面上的短语是“There's More Than One Way To Do It”,意思是做一件事情不不止一种方法。这是 Perl 社区的口号。对于任何给定的问题,该语言的灵活性允许许多差别很大但功能等同的解决方案。

使用 MakeMaker 构建 Perl 项目与任何 Perl 的其它方面并没有不同:有很多实现的方法。为了简便性,我只提供一种方法。根据我对现有 CPAN(综合 Perl 档案网络 (Comprehensive Perl Archive Network))上的许多相关项目的经验,我认为这是一组最佳实践(请参阅本文后面的 参考资料)。 当您熟悉使用 MakeMaker 构建项目时,可以自由开发构造它们的自用风格。

基本 Perl 项目将包含许多包含在项目目录中的文件和目录。我将简要讨论每一个,然后在构造 Perl 应用程序时依次重新讨论每一个。

Makefile.PL

Perl 项目的核心是 Makefile.PL 文件。Makefile.PL 是为整个项目生成 gnu Makefile 的 Perl 脚本。这是通过调用包含在 ExtUtils::MakeMaker 库中的 WriteMakefile 函数实现的。简单的 Makefile.PL 类似于:

最小 Makefile.PL
   use ExtUtils::MakeMaker;
    WriteMakefile(
        'NAME'              => 'myproject',
        'VERSION_FROM'      => 'lib/MyModule.pm', # finds $VERSION
    );

在这个示例中,我们将所需的两段信息传递给了 WriteMakefileNAMEVERSION 。Name 是显式指定的。Version 是通过使用 VERSION_FROM 变量隐含指定的。这向 MakeMaker 表明,哪一个模块包含项目的权威 $VERSION 变量。

构造 Makefile 所需的所有其它变量将取自 Perl 解释器缺省值。这些包含类似 PREFIX 的东西,它是应该在其中安装的应用程序的路径(通常缺省设为 /usr 或 /usr/local), MAN1PATH 是应该安装第一节(用于命令)帮助手册的位置, INSTALLSITELIB 是应该安装库的位置。对于变量的完整列表,请参考 ExtUtils::MakeMaker 帮助手册或位于 Perl.com(请参阅 参考资料)的 HTML 文档。

MANIFEST

Perl 项目中的第二个重要文件是 MANIFEST。MANIFEST 是项目作为项目一部分的所有文件的列表。Makefile.PL 运行时,它首先检查 MANIFEST,并检查是否存在项目所需的所有文件。如果它们不存在,它将生成丢失文件的列表,并不会构建主 Makefile。

MANIFEST 也不仅仅用于一致性检查。由 MakeMaker 提供的一个附加目标是 dist ,我们将在以后讨论。 make dist 使用 MANIFEST 来生成其 tar 文件。使这个文件保持最新的是重要的。

应该注意 MakeMaker 为生成 MANIFEST 而提供的目标称为 manifest 。通过输入 make manifest ,将当前目录和所有子目录中的所有文件都添加到 MANIFEST 中。缺省情况下, manifest 目标不排除任何文件。可以通过创建 MANIFEST.SKIP 文件来更改它。有关更多信息,请参阅 ExtUtils::Manifest 帮助手册。

lib 和 bin 目录

如何组织项目的文件通常是一个与传统有关的问题。UNIX 的传统是在目录结构的许多层次上具有兄弟目录,lib 和 bin。我已经发现如果在 Perl 项目中使用相同的惯例,则非常容易跟踪每个文件的角色。对于大型的多开发者项目尤为如此。

UNIX 中的 lib 目录是指可能由多个应用程序使用的库。就 Perl 而言,它是指“Perl 模块”(.pm)文件(每个文件由包声明开头),以及“Perl 库”(.pl)文件(通常不包含包声明,但是完全由子例程声明构成)。 MakeMaker 在项目的顶层目录中查找“Perl 模块”,这虽然还是可配置的,但是缺省情况下在 lib 目录中。

UNIX 中的 bin 目录是存储二进制和其它可运行程序的位置。在 bin 目录中,应该放置以某种形式的 #!/usr/bin/perl 开头的所有脚本。不必关心 #! 行是否适合另一个平台。稍后,您将看到 MakeMaker 将在安装期间处理这个问题。

为清晰起见,许多人喜欢将 .pl 扩展名附加到所有 Perl 程序。运行这些程序并不需要这样。 MakeMaker 有没有后缀都可以同样很好地工作。因为这个原因,我不想给 Perl 应用程序加上任何特殊的后缀。

t/,测试用例目录

MakeMaker 框架的最重要部分是拥有自动测试用例的能力。这个功能是由 Test::Harness 模块提供的,它指定一个良好定义的接口来创建包含一个或多个测试用例的测试脚本。

在构建“Perl 项目”的测试阶段期间, MakeMaker 将运行 t 子目录下作为测试脚本以 .t 为后缀的所有文件。它还将项目根目录中的 test.pl 作为测试脚本运行,因此小心不要偶然创建这样一个程序,除非您确实想让它符合 Test::Harness 接口。

测试阶段是 Perl 构建过程的整体性步骤。它是构成整体所必需的以至于除非测试成功,否则通常无法安装应用程序。编写健壮的测试脚本将不仅使您更容易保证项目的质量,而且将允许其它人更容易地将您的代码移植到新的环境。不仅仅声明它可以在 X 硬件上工作,他们还可以通知您 testabc.t 中的测试用例 15-18 失败。它为您提供出错的更多信息,并且将导致更快和更可靠的修正。

在本文的后面,我将深入描述如何编写测试脚本以及测试用例。

旧的纯文本文档(Plain Old Documentation)

大多数项目的一个最大的问题是使文档与实际实现它的代码保持同步。当更新代码以包含新特性或更改现有的行为方式时,跟踪并修改与该特性有关的所有文档将变得很困难和费时。如果需要用多种格式(例如,帮助手册或 HTML)保持文档时,尤为如此。

Perl 提供的一个非常重要的功能是嵌入式文档标记语言 POD(旧的纯文本文档)。POD 非常简单,并且由很少的标记命令组成。Perl 解释器将 POD 标记文本视为注释,因此将 POD 文档散布在代码中。核心 Perl 发行版带有将 POD 转换成文本、HTML 和帮助手册格式的解析器。在 CPAN 上还有贡献的模块用于直接转换成 Latex、PDF 和 Postscript。

在 Perl 构建过程期间, MakeMaker 将检查 POD 文档的所有脚本和模块。在 UNIX 平台上,将为所有可执行文件生成第一节,为每个库生成第三节帮助页面。可以选择在安装期间生成 HTML 文档。如果用 POD 格式正确地编制了程序文档,则在用户安装程序后,他(或她)可以运行 man your-program ,并获得其上的所有文档。这总好过期望用户从某处找出一个自述文件,并且它为您的文档创建一个熟悉的界面。

关于 POD 的更多信息,可以在 Programming Perl, 3rd Edition 中查找,通过运行 man perlpod 或在 Perl.com 上在线查找(请参阅 参考资料)。

您的第一个 MakeMaker 项目(make)

这个样本 MakeMaker 项目的灵感是由 Colin McMillen 添加到 perl5-porters 邮件列表中的关于 Net::Ping 模块的贴子激发的。Ping 是非常有用的工具,它用于确定机器是否正在接收包,但是通常您想知道的更多,比如 Web 服务器是否确实响应该机器。同时,在 ICMP 袭击的时代,许多主机已经禁用了返回标准 ping 包,因此它不再是网络连接性的有用指示。这个项目是为了构建 pingwww,它看起来和用起来很象 ping,但是实际上将发送 HTTP 请求,并且测试有问题的 Web 服务器是否可以起作用。

这个项目将包含核心模块 Net::Ping::HTTP ,它将实现正在发生的事件,以及 pingwww ― 一个向 Net::Ping::HTTP 提供用户界面的程序。

编写 Makefile.PL

我将从编写 Makefile.PL 开始。首先,其中只有最基本的项。

Net::Ping::HTTP 的 Makefile.PL,镜头 1
 use ExtUtils::MakeMaker;
  WriteMakefile(
      'NAME'              => 'Net::Ping::HTTP',
      'VERSION_FROM'      => 'lib/Net/Ping/HTTP.pm', # finds $VERSION
    );

这指定项目名为 Net::Ping::HTTP ,并且包含在 lib/Net/Ping/HTTP.pm 中的变量 $VERSION 将被认为是项目的权威版本。

现在我们已经在项目中拥有了 Makefile.PL 和另一个文件。现在,是时候创建 MANIFEST,以使构建过程将正确进行。

MANIFEST 第一遍的内容将类似于:

MANIFEST,镜头 1
 MANIFEST
  Makefile.PL
  lib/Net/Ping/HTTP.pm

请记住,每次向项目添加新文件时,必须向 MANIFEST 文件中为每个文件添加一项。

Net::Ping::HTTP

这个模块的代码将是在 LWP::UserAgent 上的一个简单层,它包含在 libwww-perl 包中。缺省情况下,Linux 和 UNIX 的许多衍生系统在其发行版中都包含这个库。 同时,如果已经安装了 ActiveState Perl Windows 版,则已经拥有了 LWP::UserAgent 。但是,如果不幸使用缺省情况下不提供这个模块的操作系统,则还可以从 CPAN(请参阅 参考资料)下载它。

Net::Ping::HTTP 的策略很简单。HTTP 协议支持方法 HEAD ,它设计成仅返回 HTTP 头而不返回内容。 Net::Ping::HTTP 将向您试图 ping 的 Web 服务器的根文档发送 HTTP HEAD 请求,并且将返回由 Web 服务器返回的状态码。在 Web 服务器可用的情况下,这个状态码应该是 200(虽然在罕见情况下可能会是 30x 或 40x)。

这是在 Net::Ping::HTTP 的第一遍的内容,包括模块的 POD 文档:

Net::Ping::HTTP 库
 package Net::Ping::HTTP;
  
  =head1 NAME
  
  Net::Ping::HTTP - An interface for determining whether an HTTP
  server is listening
  
  =head1 SYNOPSIS
  
  my $pinger = new Net::Ping::HTTP;
  my $rc = $pinger->ping('http://localhost');
  
  my $pinger = new Net::Ping::HTTP(
                                 TIMEOUT => 15,
                                 PROXY => 'http://someproxy.net',
                                );
  my $rc = $pinger->ping('http://www.ibm.com');
  
  =head1 DESCRIPTION
  
  Net::Ping::HTTP is a simple interface on top of LWP::UserAgent
  that lets you ping a Web server to see if it is responding to HTTP
  requests.  This would make it suitable for monitoring Web applications.
  
  =head1 AUTHOR
  
  Sean Dague <sldague@us.ibm.com>
  
  =head1 SEE ALSO
  
  L<perl>, <LWP::UserAgent>
  
  =cut
  
  use strict;
  use LWP::UserAgent;
  use HTTP::Request;
  use vars qw($VERSION);
  
  $VERSION = '0.02';
  
  sub new {
      my $class = shift;
      my %this = (
                PROXY => undef,
                TIMEOUT => 10,
                @_,
                );
    
      my $ua = new LWP::UserAgent("Net::Ping::HTTP - $VERSION");
      $this{UA} = $ua;
      $this{UA}->timeout($this{TIMEOUT});
      if($this{PROXY}) {
          $this{UA}->proxy('http',$this{PROXY});
      }
  
      return bless \%this, $class;
  }
  
  sub ping {
      my ($this, $url) = @_;
    
      my $request = new HTTP::Request('HEAD',"$url");
      my $response = $this->{UA}->request($request);
    
      return $response->code();
  }
  1;

Net::Ping::HTTP 同时使用 LWP::UserAgentHTTP::Request ,并且没有它们就无法工作。不是让用户安装这个模块,然后因为没有相关性而使之中断,而是模块可以指定运行它需要的其它模块。

可以将这些模块作为需求添加到 Makefile.PL。在生成主 Makefile 之前将验证它们的存在。Makefile.PL 的这个新变量的格式如下:

Makefile.PL 先决条件
 'PREREQ_PM' => {
      Module::Name => minimum.version.number,
      ...
  },

该代码对于我已经安装在工作站上的 LWP::UserAgentHTTP::Request 版本有效,因此那些版本将是我的模块中的先决条件。要确定那些版本,可以在命令行上运行下列命令:

查找模块版本
 perl -MLWP::UserAgent -e 'print $LWP::UserAgent::VERSION'
  perl -MHTTP::Request -e 'print $HTTP::Request::VERSION'

来自 perl 的 -M 标志装入该模块, -e 标志将下列字符串作为 perl 脚本运行。

带有搜集的信息,Makefile.PL 如下:

Makefile.PL 镜头 2
 use ExtUtils::MakeMaker;
  
  WriteMakefile(
              'NAME'              => 'Net::Ping::HTTP',
              'VERSION_FROM'      => 'lib/Net/Ping/HTTP.pm', # finds $VERSION
              'PREREQ_PM' => {
                              LWP::UserAgent => 1.73,
                              HTTP::Request => 1.27,
              });

pingwww

该项目的核心库还不完整。为了使它有用,必须存在一个利用其功能并提供用户界面的程序。这个程序将称为 pingwww,并且将放置于项目树的 bin/ 目录中。

在 pingwww 中将很好地模拟 ping 命令很有用的两个功能,一个是指定在其退出前 ping 向服务器发出包的次数的能力,以及同步显示响应每个请求所用的时间。

要添加这些特性,程序将需要两个附加模块,而 Net::Ping::HTTP 不是必需它们。这些模块是 Getopt::Std ,用于解析命令行选项,以及 Time::HiRes ,用于提供微秒级精度计时。 Getopt::Std 是核心 Perl 发行版的一部分,因此不必将它作为 PREREQ_PM 添加,但是 Time::HiRes 不是(虽然它看起来象是 Perl 5.8 的标准库的一部分)。因此,将 Time::HiRes 添加到 Makefile.PL 中的 PREREQ_PM 变量中。

pingwww 的第一遍的内容如下:

pingwww 程序
 #!/usr/bin/perl
  #
  #   This program is licensed under the same terms as Perl itself
  #
  #   Sean Dague <sldague@us.ibm.com>
  
  =head1 NAME
  
  pingwww - program for "pinging" webservers
  
  =head1 SYNOPSIS
  
  pingwww [-c iterations] [-p proxy] [-t timeout] [-h] hostname
  
  =head1 DESCRIPTION
  
  Pingwww uses the Net::Ping::HTTP module to send HEAD requests
  to the host specified by hostname.
  
  =head1 AUTHOR
  
  Sean Dague <sldague@us.ibm.com>
  
  =head1 SEE ALSO
  
  L<Net::Ping::HTTP>, L<Time::HiRes>, L<perl> 
  
  =cut
  
  use strict;
  use Carp;
  use Getopt::Std;
  use Net::Ping::HTTP;
  use Time::HiRes qw(gettimeofday tv_interval);
  
  $| = 1;
  my %opts;
  
  getopt('cpth',\%opts) or croak("Couldn't parse options");
  
  if($opts{h}) {
      usage();
  }
  
  my $max = $opts{c} || 1000;
  
  my %vars = ();
  
  if($opts{p}) {
      $vars{PROXY} = $opts{p};
  }
  
  if($opts{t}) {
      $vars{TIMEOUT} = $opts{t};
  }
  
  my $pinger = new Net::Ping::HTTP(%vars);
  my $host = $ARGV[0];
  my $packedip = gethostbyname($host);
  my $ip = inet_ntoa($packedip);
  
  print "PING HTTP $host ($ip):\n";
  
  for(my $i = 0 ; $i < $max; $i++) {
      my $start = [gettimeofday()];
      my $rc = $pinger->ping("http://$host");
      if ($rc < 400) {
          print "Response $rc from $ip: ";
          my $elapsed = tv_interval ( $start );
          print "$elapsed seconds\n";
      } else {
          print "Response $rc from $ip: ";
          print "failed\n";
      }
  }
  
  sub usage {
      print <<USAGE;
  usage: $0 [-c number of iterations] [-p proxy] [-t timeout] [-h] hostname
    where 
      -h : this message
      -c : number of iterations to ping the host.  Defaults to 1000
      -p : http proxy to use for the ping
      -t : timeout value.  Defaults to 10 secs if none is specified
  USAGE
  
    exit(1);
  }

因为 EXE_FILES 包括 pingwww,在安装期间发生了一些有趣的事,我将在以后讨论。MANIFEST 文件还必须包含新文件的项。新 MANIFEST 文件如下:

Manifest,镜头 2
 MANIFEST
  Makefile.PL
  lib/Net/Ping/HTTP.pm
  bin/perlwww

构建测试用例(make test)

既然已经编写了代码,是时候编写测试用例了。可能值得从测试用例开始,然后构建符合那些测试的代码,这取决于您的风格和代码的复杂程度。如前所述,有多种完成该任务的做法:

如前所述,测试脚本存于 t 目录。测试脚本是以已定义的格式打印到 STDOUT 的任何程序。这个输出的第一行必须包含字符串 1..N ,其中 N 是将要运行的测试数。其后的行包含“ok”或“not ok”,这取决于测试成功还是失败。

Test.pm

为便于构建测试脚本,在 Perl 核心发行版中包含了 Test 模块。 Test 提供了许多自动编写测试脚本的函数。

第一个是 plan 函数,它用来生成 1..N 行。如果测试脚本包含 5 个测试,可能用下列代码开始您的脚本:

测试脚本头
  use Test;
   BEGIN { plan tests => 5}

为什么这比 print "1..5\n" 还简单呢?如果 plan 所做的只是生成了该行,它就不会这样。但是, Plan 支持许多 todo 列表中的附加功能,将在以后讨论。

另一个感兴趣的函数是 ok 函数。它将根据不同的真值测试来生成适当的“ok”和“not ok”输出。如果使用一个自变量调用 ok() ,则如果在 Perl 中对该自变量求值为真,则它生成“ok”,如果求值为假,则生成“not ok”。在两个自变量的情况下,如果两个自变量由 perl“eq”运算符测试为相等,则 ok 将生成“ok”。它测试 ASCII 等价性,因此 1 将不等于 1.0,即使它们有相同的数值。

当使用两个自变量时, Ok() 也有特殊情况。第二个自变量实际上可能是通过 /pattern/ 形式的字符串指定或通过使用 qr( ) 运算符而得到的 Perl 正规表达式。在这种情况下,如果第一个自变量满足由第二个自变量提供的正规表达式,则 ok 将生成“ok”。

Test 模块还提供几个其它特性。可以将这样的测试用例指定在“todo 列表”上。如果这些测试失败,它们将不生成错误。如果它们成功,将生成意外的成功警告。这通常是一个好的指示,这时可以将测试从 todo 列表取出并使它成为必需的测试。一种方法是通过将 todo 数组传送到 plan 函数中来指定 todo 测试。下列示例将设置 10 个测试用例,最后 3 个在“todo 列表”上,因此不必为整个测试脚本成功而返回“ok”。

带有 todo 列表的测试脚本
  use Test;
   BEGIN { plan tests => 10, todo => [8,9,10] }

Test 模块提供的最后一个函数是 skip() 。有时候,测试用例并不适合所有平台或所有配置,但是如果它适合,则可以很好地运行测试用例。 skip() 函数获取 3 个自变量。如果第一个为真,则跳过测试;否则,按以上文档所述将第二个和第三个自变量传递到 ok

测试 Net::Ping::HTTP

Net::Ping::HTTP 中没有太多功能,所以测试将会相当容易和简短。我将为这个模块创建 7 个测试。相比之下,版本 5.6.1 的 Perl 解释器指定了 12967 个测试用例,每次编译解释器时运行。

任何模块的第一次测试是究竟是否装入它。对于这组测试,一切将取决于第一次测试是否成功。如果不成功,代码调用 croak() (它是由 Carp 提供的 die 的更好版本),因为没有理由运行其它测试用例。就 Net::Ping::HTTP 来说,可以通过下列脚本完成:

简单装入测试用例
  eval { require Net::Ping::HTTP; return 1;};
   ok($@,'');
   croak() if $@;  # If Net::Ping::HTTP didn't load... bail hard now

当您编写越来越多的测试用例时,将发现对于许多测试, eval 块很有用。然后,仅需要测试是否设置了 $@ 以查看是否成功。在需求失败的情况下,将设置 $@ ,因此测试失败。

在简单装入测试后,将测试对象以确保它是正确实例化的。在这种特殊情况下,可能有些过度,但是带有更复杂的模块,这无疑是个好主意。这里有用来检查对象是否良好格式的一组测试。

Ok 测试用例
 my $pinger = new Net::Ping::HTTP();
  ok($pinger->{TIMEOUT},10);
  ok(ref($pinger->{UA}),"LWP::UserAgent");
  ok($pinger->{TIMEOUT},$pinger->{UA}->timeout());

它测试是否将 TIMEOUT 正确地设置成指定的缺省值,是否正确实例化 LWP::UserAgent ,以及是否向预期对象传播了超时值。

如果正在 UNIX 机器上安装这个代码,则很可能在该机器上运行 apache。如果是这样,则很可能运行在端口 80 上。如果 apache 在进程列表中,则可以进行 skip 测试以尝试 ping 本地主机。显示的代码片断将完成这一任务。

Skip 测试用例
 my $count = 0;
  # eval this code in case ps is not a happy camper on this platform
  eval {
    my @process = `ps -ef`; 
    $count = grep /httpd/, @process;
    my $rc = $pinger->ping("http://localhost");
    if ($rc >= 500) {
        croak();
    }
  };
  skip(!$count,$@,'');

首先,运行 'ps -ef' 以抽取进程列表。在 ps 不是有效命令的机器上,这应该返回空数组。下一步,查询进程列表以查找其中的 httpd 进程数。然后发射器(pinger)ping 本地主机的 80 端口,如果返回无效状态码,则停止。如果进程表中没有 httpd 进程,则测试结果将不予考虑。

这个测试用例有一些问题,例如,httpd 可以在更高的端口上运行,或者本地主机可能解析本地接口以外的接口,但是由于时间和空间,我将忽略这些。取决于您程序所需的健壮性,您的测试可能要考虑这些可能性。

最后,我将添加几个测试来 ping 因特网上的实际站点,www.ibm.com 和 www.yahoo.com,因为这两个站点非常易于访问。这些测试确实需要与这些网站的网络连接,而这种连接不能通过指定代理服务器来完成。对于许多机器,这种环境可能不存在。因为这个原因,我将通过修改 BEGIN 块来将这些测试放在 todo 列表上。如果它们起作用,那就太好了,如果不,也没关系。新的 BEGIN 块如下:

最终测试脚本头
 BEGIN {plan tests => 7 , todo => [6,7]};

现在完成了 Net::Ping::HTTP 的测试脚本。它有 4 个常规测试,一个特定于平台的测试以及两个标记为 todo 的测试(因为它们是可选的)。

在将测试脚本加入 MANIFEST 后,MANIFEST 如下:

MANIFEST 镜头 3
 MANIFEST
  Makefile.PL
  lib/Net/Ping/HTTP.pm
  bin/pingwww
  t/pingpm.t

关于测试二进制文件的几句话

测试二进制文件比测试模块更困难,因为您通过 system() 调用运行并且仅测试退出码,或者通过使用单反引号运行它们并且解析它们的文本输出。在每种情况下,速度都很慢。这就是我为什么喜欢尽可能多地将核心函数放入模块的模式,而将脚本只作为这些模块的小型封装器的多个原因之一。

由于这些原因,对于 pingwww 程序,我不会构建测试装置。

安装(make install)

现在准备构建该项目。我将讨论整个标准构建过程并查看发生的每个步骤。

perl Makefile.PL

第一步是运行 perl Makefile.PL 。将生成项目的 Makefile。在我的工作站上,我已经将 perl 5.6.1 安装到 /usr/bin/perl 中。因为我没有在 Makefile.PL 中指定 PREFIX ,所以将从我的 perl 解释器中收集这个变量。在这种情况下, PREFIX 将被设置成 /usr。因此将 pingwww 程序安装成 /usr/bin/pingwww,将 Net::Ping::HTTP 模块安装成 /usr/lib/perl5/site_perl/5.6.1/Net/Ping/HTTP.pm。

在 Makefile.PL 中和命令行上可以覆盖 PREFIX 变量和许多其它 MakeMaker 变量。这种变量赋值的命令行语法是 perl Makefile.PL VARIABLE=VALUE 。例如,如果想将 pingwww 安装成 /usr/local/bin/pingwww,则可以覆盖 PREFIX

Makefile.PL 的命令行自变量
 perl Makefile.PL PREFIX=/usr/local

它会将二进制文件放入 /usr/local/bin 中,并将模块放入 /usr/local/lib/perl5/site_perl/5.6.1 中。虽然 /usr/local/bin 可能在路径中,但是 /usr/local/lib/perl5/site_perl/5.6.1 可能不在 Perl 包括路径中,(除非 Perl 真在 /usr/local/bin 中),因此不建议不加区别地更改 PREFIX

这里是 perl Makefile.PL 的预期输出:

perl Makefile.PL 输出
 rigel:~/NetPingHTTP> perl Makefile.PL
  Checking if your kit is complete...
  Looks good
  Writing Makefile for Net::Ping::HTTP

MakeMaker 已经检查以确保您已经具有 MANIFEST 需要您应当有的所有文件,并且它可以找到您已经在 PREREQ_PM 中列出的所有模块。

make

下一步是运行 make 。这将所有 perl 二进制文件和库构建到临时 blib 目录中。在那儿,将进行测试和安装。

如果已经花时间在二进制文件和模块中编写正确的 POD 文档,则将获得自动文档生成的好处。在 UNIX 机器上,make 过程将自动为所有带有 POD 标记的二进制文件和模块生成帮助手册。如前所述, 这让您将所有文档嵌入在它们所描述的代码之后,并只维护一个文件。

对于 perl 5.6.0,还可以让 MakeMaker 为模块构建 HTML 文档。如果在 perl Makefile.PL 命令行上或 Makefile.PL 中定义了 INSTALLHTMLSITELIBDIR,则 HTML 文档将在安装期间被安装到该目录中。我自己喜欢生成 HTML 文档,因此将按如下重新运行 perl Makefile.PL:

生成 HTML 文档
 perl Makefile.PL INSTALLHTMLSITELIBDIR=~/public_html

它将在我的个人 Web 目录中构建 HTML docs。

在构建期间, MakeMaker 所做的另一件有用的事是将所有 perl 二进制文件中的 shebang 行都修改成适合环境的那一个。这样我就不必担心,无论是谁安装我的代码,都不会将 perl 放在 /opt/fromsource/bin/perl 或别的临时位置,因为这样的话,程序不能运行。

运行 make 生成下列输出:

make 输出
 rigel:~/NetPingHTTP> make
  cp lib/Net/Ping/HTTP.pm blib/lib/Net/Ping/HTTP.pm
  cp bin/pingwww blib/script/pingwww
   /usr/bin/perl -I/usr/lib/perl5/5.6.1/i686-linux -I/usr/lib/perl5/5.6.1 
          -MExtUtils::MakeMaker -e "MY->fixin(shift)" blib/script/pingwww
  Htmlifying blib/html/lib/lib/Net/Ping/HTTP.html
  /usr/bin/pod2html: lib/Net/Ping/HTTP.pm: cannot resolve L<perl> in paragraph 13. at 
          /usr/lib/perl5/5.6.1/Pod/Html.pm line 1562.
  Manifying blib/man3/Net::Ping::HTTP.3
  Manifying blib/man1/pingwww.1

在 make 期间生成警告。这是因为我在 POD 文档中使用了 <lt>E<gt> 语法,它指定对外部实体的引用。 Pod::HTML 有某些生成任意外部链接方面的问题,因此在此阶段,将经常看到警告。通常情况下,不必担心由 pod2html 生成的错误,因为它们对项目几乎没有影响。

make test

下一步是执行项目的测试阶段。应该期望输出如下:

make test 输出
  rigel:~/NetPingHTTP> make test
   PERL_DL_NONLAZY=1 /usr/bin/perl -Iblib/arch -Iblib/lib 
          -I/usr/lib/perl5/5.6.1/i686-linux -I/usr/lib/perl5/5.6.1 -e 
          'use Test::Harness qw(&runtests $verbose); $verbose=0; 
          runtests @ARGV;' t/*.t
   t/pingpm............ok, 1/7 skipped: unknown reason                          
   All tests successful, 1 subtest skipped.
   Files=1, Tests=7, 20 wallclock secs ( 0.36 cusr +  0.03 csys =  0.39 CPU)

运行测试时,我没有在机器上运行 apache,因此跳过测试号码为 5 的测试。

可以看到在测试过程期间 Test::Harness 还运行了计时代码。它跟踪“wallclock secs”,即从测试开始到结束所用的时间,以及 CPU 时间,即 CPU 上总的处理时间。这些测试的 wallclock 时间为 20 秒,而 CPU 时间却不到一秒的原因是因为在我的环境中,工作站与 www.ibm.com 和 www.yahoo.com 之间存在防火墙。由 Net::Ping::HTTP 实例化的 LWP::UserAgent 是使用 10 秒超时创建的,并且因此在继续之前有 2 次超时。因为将测试 6 和 7 指定为 todo 测试,所以没有因为这些故障而生成错误。

为了说明,让我们运行 make test ,但不断言测试 6 和 7 为 todo 测试。在我的环境中这将产生失败。由于刚开始使用测试用例时,将看到失败比成功更多,因此最好知道如何解释它们:

make test 故障输出
  rigel:~/NetPingHTTP> make test
   PERL_DL_NONLAZY=1 /usr/bin/perl -Iblib/arch -Iblib/lib 
          -I/usr/lib/perl5/5.6.1/i686-linux -I/usr/lib/perl5/5.6.1 -e 
          'use Test::Harness qw(&runtests $verbose); $verbose=0; 
          runtests @ARGV;' t/*.t
   t/pingpm............FAILED tests 6-7                                         
           Failed 2/7 tests, 71.43% okay (-1 skipped test: 4 okay, 57.14%)
   Failed Test Status Wstat Total Fail  Failed  List of Failed
   --------------------------------------------------------------------------------
   t/pingpm.t                     7    2  28.57%  6-7
   1 subtest skipped.
   Failed 1/1 test scripts, 0.00% okay. 2/7 subtests failed, 71.43% okay.
   make: *** [test_dynamic] Error 29

输出有些忙,但是意思很清楚。pingpm 测试脚本的测试 6 和 7 失败。在输出的多处都出现了这一事实。结果还提供了对成功和失败的测试数的统计分析。如果已经创建了多个测试脚本,最后的“Failed”行将告诉您有多少测试脚本不是 100% 成功的,以及有多少测试用例是失败的。

make install

构建项目的最后一部分是运行 make install 。如果正在安装到特权目录(例如,/usr),则必须以 root 进行安装。这时,将安装所有二进制文件、模块、帮助手册和 Web 页面。

make install 输出
  [root@rigel]# make install
   Installing /home/sdague/public_html/lib/Net/Ping/HTTP.html
   Installing /usr/man/man1/pingwww.1
   Installing /usr/man/man3/Net::Ping::HTTP.3
   Installing /usr/bin/pingwww
   Writing /usr/lib/perl5/site_perl/5.6.1/i686-linux/auto/Net/Ping/HTTP/.packlist
   Appending installation info to /usr/lib/perl5/5.6.1/i686-linux/perllocal.pod

现在,安装了代码,并且完成了构建过程。

分发代码(make dist)

如果对于您的项目遵循这个过程,您将会得到一个易于别人安装的好程序。如果想分发那个代码, MakeMaker 提供了 make dist 命令,它将构建称为“project-version.tar.gz”的项目的 tar.gz 发行版(在 Manifest 中列出的所有文件)。然后,可以方便地放上这个文件以进行分发。

在完成该操作之前,通常还有几个与 perl 代码一起分发的其它文档。

README

这个文件是关于项目以及其提供的内容的简要概述。

COPYING(或者 LICENSE)

这个文件描述这些文件采用的许可证,以及对用户复制、修改或重新分发代码的限制。许多 perl 模块是“与 Perl 本身具有相同的特许”,这意味着在 GNU Public License 和 Artistic License 的双重许可之下。但是,也可以自己选择觉得适合您代码的任何许可证。

Changes

这是一个更改日志,它记录了添加到每个文件发行版中的内容。对这个文件保持及时更新很有好处,因为它有助于其他人弄清添加了什么特性,以及它们何时成为主发行版的一部分。

TODO

这是将来的错误修订和特性的 todo 列表。在发行版中包含这样一个文件总是个好主意。它为您的用户提供了关于项目发展方向的信息。您永远不会知道,但是一些喜欢冒险的用户甚至可能决定实现 Todo 列表之外的特性,然后将它们发送给您。

请记住,确保将任何附加文件添加到 MANIFEST 中,否则,运行 make dist 时将不会捆绑它们。

如果您的代码具有较高的可重用性,其他人认为它是可用的,则应该考虑将它提交给 CPAN(请参阅 参考资料)。使用这里描述的方法,您的代码将非常接近于符合 CPAN 模块格式。

结束语

这里我只是涉及了冰山一角。除了纯 Perl 项目,还可以在这个基础结构下管理 C 和 Perl 结合的项目。基础结构是用 Perl 构建的,这意味着它有很高的可移植性,可以在从 Linux 到 Windows 以及 S/390 的平台上运行。一旦习惯了这种基础结构,将发现它对于您从事的所有项目来说都是无价的。您将不必再次编写安装脚本,并且通过使用良好格式的测试用例,可以对程序按所期望的方式执行充满信心。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=20830
ArticleTitle=使用 MakeMaker 构建 Perl 项目
publish-date=11012001