模块::构建

Dave Rolsky是最近发布的《在Mason中嵌入Perl》一书的合著者。[该书链接](http://www.oreilly.com/catalog/perlhtmlmason/)

本文最初于2003年2月发表,由Dave Rolsky和Michael Schwern于2008年1月更新。

如果你曾经为CPAN分发创建过Perl模块,那么你已经使用过ExtUtils::MakeMaker模块了。这个古老的模块可以追溯到现代Perl的起源,始于Perl 5.000的发布。

最近,Ken Williams创建了一个名为Module::Build的潜在替代品,它于2002年8月首次发布。Perl当前开发版本的负责人Hugo van der Sanden表示有兴趣在Perl 5.10版本中使用Module::Build替换ExtUtils::MakeMaker,当前的ExtUtils::MakeMaker维护者Michael Schwern也同意他的看法。ExtUtils::MakeMaker不会很快消失,但我们希望逐步过渡到一个新且改进的构建系统。

为什么ExtUtils::MakeMaker很重要

ExtUtils::MakeMaker模块与h2xs脚本一起,对Perl社区来说是一个巨大的福音,因为它为Perl模块的标准分发和安装方式提供了可能。它自动化了许多模块作者本应手动实现的任务,例如将XS转换为C,编译C代码,从POD生成手册页,运行模块的测试套件,以及当然,安装模块。

ExtUtils::MakeMaker是PAUSE和CPAN可能存在的重要因素,它是一个相当了不起的编程壮举。Python直到1999年9月发布distutils之前没有类似的实用工具,PHP的PEAR直到2000年中才开始,Ruby也刚刚开始开发标准库分发机制。

ExtUtils::MakeMaker的可怕内核

ExtUtils::MakeMaker通过生成一个包含维护或安装模块所需各种任务的目标的makefile来工作。以下是一个示例目标:

  install :: all pure_install doc_install

如果你熟悉makefile语法,你会意识到这个目标所做的只是调用三个其他目标,分别是“all”、“pure_install”和“doc_install”。这些目标随后可能会调用其他目标,或者使用系统命令执行所需的任何操作,在这个例子中是安装模块及其相关文档。

ExtUtils::MakeMaker生成的makefile相当复杂。例如,使用ExtUtils::MakeMaker的6.05版本安装我的Exception::Class模块(一个仅包含一个模块的纯Perl分发),生成了一个包含大约390行makefile代码的makefile。理解这个makefile实际做什么并不是一件简单的事情,因为它由一个错综复杂的宏迷宫组成,其中许多宏只是从命令行调用Perl单行脚本以执行某些任务。

ExtUtils::MakeMaker代码本身非常复杂,因为它需要在许多操作系统上工作(几乎和Perl本身一样多),并且需要适应它们的文件系统、命令行外壳、不同的make版本等。所有这些都是在额外的一层间接层上完成的,因为它生成一个执行所有工作的makefile。

如果您想自定义模块构建或安装过程,祝您好运。为此,您必须扩展 ExtUtils::MakeMaker,覆盖生成特定 makefile 目标的函数,然后微调生成的文本以包含您的自定义行为,同时保留目标的基本行为。鉴于没有文档描述这些目标应该有什么预期,并且实际的目标文本可能在 ExtUtils::MakeMaker 的不同版本之间或在不同操作系统之间发生变化,这可能会非常痛苦地实现和维护。

顺便说一下,您实际上无法扩展 ExtUtils::MakeMaker,相反,您是在扩展 MY 包。这是一个非常奇怪的技巧,但最终结果是您只能覆盖 ExtUtils::MakeMaker 中的某些预定义方法。

例如,HTML::Mason 模块包含以下片段

  package MY;

  sub test {
      my $self = shift;

      my $test = $self->SUPER::test(@_);

      # %MY::APACHE is set in makeconfig.pl.
      # If we are not going to test with Apache there is no harm in
      # setting this anyway.

      # The PORT env var is used by Apache::test.  Don't delete it!
      my $port = $MY::APACHE{port} || 8228;
      $MY::APACHE{apache_dir} ||= ";

      my $is_maintainer = main::is_maintainer();

      # This works for older MakeMakers
      $test =~ s/(runtests \@ARGV;)/\$\$ENV{MASON_VERBOSE} ==
      \$(TEST_VERBOSE) ? \$(TEST_VERBOSE) : \$\$ENV{MASON_VERBOSE};
      \$\$ENV{PORT}=$port;
      \$\$ENV{APACHE_DIR}=q^$MY::APACHE{apache_dir}^;
      \$\$ENV{MASON_MAINTAINER}=$is_maintainer; $1/;

      my $bs = $^O =~ /Win32/i ? " : '\\';

      # This works for newer MakeMakers (5.48_01 +)
      $test =~ s/("-MExtUtils::Command::MM" "-e" ")
      (test_harness\(\$\(TEST_VERBOSE\).*?\)"
      \$\(TEST_FILES\))/$1 $bs\$\$ENV{MASON_VERBOSE} == \$(TEST_VERBOSE) ?
      \$(TEST_VERBOSE) : $bs\$\$ENV{MASON_VERBOSE}; $bs\$\$ENV{PORT}=$port;
      $bs\$\$ENV{APACHE_DIR}=q^$MY::APACHE{apache_dir}^;
      $bs\$\$ENV{MASON_MAINTAINER}=$is_maintainer; $2/;

      return $test;
  }

所有这些代码的目的是在测试脚本运行时传递一些额外的环境信息,这样我们就可以使用实时 Apache 服务器进行测试。它兼容几个版本的 ExtUtils::MakeMaker,并尝试在多个操作系统(至少 Win32 和 *nix)上正常工作,并且必须小心地转义事物,以便正确地从 shell 执行。

为什么不使用 Perl 呢?

所有这些都引发了这样的问题:“为什么不直接使用 Perl 呢?”Ken Williams 就是用 Module::Build 回答了这个问题。 Module::Build 的目标是做 ExtUtils::MakeMaker 做的所有有用的事情,但尽可能全部用纯 Perl 完成。

这大大简化了构建系统代码,并且 Module::Build 可以在通常不包含 make 的系统上运行,如 Win32 和 Mac OS。当然,如果模块安装需要编译 C 代码,您仍然需要一个外部的 C 编译器。

此外,自定义 Module::Build 的行为通常非常简单,只需了解 Perl 即可,而无需了解 make 语法,可能还需要学习多个命令行环境。

Module::Build 还旨在改进 ExtUtils::MakeMaker 提供的一些功能。一个例子是它的依赖项检查系统,它提供了比 ExtUtils::MakeMaker 允许的更多灵活性。虽然这些功能可以添加到 ExtUtils::MakeMaker 中,但修改这样一个重要模块,特别是具有如此复杂内部结构的模块,风险很大。

使用 Module::Build

从最终用户的角度来看,使用 Module::Build 的模块看起来与使用 ExtUtils::MakeMaker 的模块非常相似,这是故意的。因此,要使用 Module::Build 安装模块,您需要从命令行输入以下几行

  perl Build.PL
  ./Build
  ./Build test
  ./Build install

Build.PL 脚本会告诉 Module::Build 创建一个 Build 脚本。在这个过程中,Module::Build 还会将一些文件写入到 _build/ 目录。这些文件用于在 Build 脚本的调用之间存储构建系统的状态。当调用此脚本时,它只是再次加载 Module::Build,并告诉它执行指定的操作。操作是 Module::Build 的 makefile 目标的版本,并且尽可能在纯 Perl 中实现操作。

一个简单的 Build.PL 脚本可能看起来像这样

  use Module::Build;

  Module::Build->new
      ( module_name => 'My::Module',
        license => 'perl',
      )->create_build_script;

“module_name”参数类似于 ExtUtils::MakeMaker 的“NAME”参数。

“license”参数是 Module::Build 的新特性,其目的是允许自动化工具确定您的模块在哪种许可证下分发。

要确定模块的版本,Module::Build 会查看由“module_name”参数指定的模块,尽管这可以通过指定不同的模块来查看或直接提供版本号来覆盖。

当然,还有更多选项。例如,Module::Build实现了与ExtUtils::MakeMaker类似的需求,因此我们可以写

  Module::Build->new
      ( module_name => 'My::Module',
        license => 'perl',
        requires => { 'CGI' => 0,
                      'DBD::mysql' => 2.1013,
                    },
      )->create_build_script;

如果你有使用ExtUtils::MakeMaker的经验,你可能能看出这表示我们的模块需要任何版本的CGI,以及DBD::mysql的2.1013或更高版本。到目前为止,这看起来和ExtUtils::MakeMaker提供的一样,但Module::Build做得更多。

考虑一下,如果我们知道我们需要的某些功能最早在DBD::mysql 2.1013版本中存在。但是,也许在这个版本之后的某个版本2.1014中引入了一个新的错误,这破坏了我们的应用程序。如果这个错误在2.1015版本中被修复,我们可以简单地要求版本2.1015,但这不是最佳选择。没有必要因为一个他们没有的版本中的错误而强迫已经拥有2.1013的人升级。

幸运的是,Module::Build提供了一个更灵活的版本指定选项,可以处理这种确切的情况,因此我们可以写

  Module::Build->new
      ( module_name => 'My::Module',
        license => 'perl',
        requires => { 'CGI' => 0,
                      'DBD::mysql' => '>= 2.1013, != 2.1014',
                    },
      )->create_build_script;

这表示我们需要一个大于2.1013的版本,但不是2.1014的版本。拥有2.1013或2.1015或更高版本的用戶不需要强制升级,但拥有2.1014的用戶则需要升级。

如果我们知道版本3.0与我们的模块不兼容,我们可以更改我们的指定

  Module::Build->new
      ( module_name => 'My::Module',
        license => 'perl',
        requires => { 'CGI' => 0,
                      'DBD::mysql' => '>= 2.1013, != 2.1014, < 3.0',
                    },
      )->create_build_script;

如果用户安装了3.0或更高版本,它至少会让他们知道这不会与我们的模块兼容。不幸的是,目前唯一可能使用我们的模块的方法是让最终用户手动降级他们的DBD::mysql安装,因为Perl不允许多个版本的模块和平共存。尽管如此,这比让模块安装后运行时尝试使用过时的DBD::mysql API失败要好。

还有其他与需求相关的选项,例如“推荐”和“构建需求”,这些对于在构建模块时需要但安装后不需要的需求非常有用。还有一个“冲突”选项,可以用来警告用户他们正在安装的模块与他们已安装的模块之间可能存在冲突。

行动!

截至版本1.15,Module::Build实现了以下操作,其中大部分基于现有的ExtUtils::MakeMaker功能

  • build

    这是默认操作,如果你没有添加任何额外参数运行./Build,就会发生这种情况。它负责创建blib/目录并将文件复制到其中,以及编译XS和C文件。如果你有任何类似lib/My/Module.pm.PL的脚本,这些脚本也会在这个操作期间运行。

  • test, testdb

    使用Test::Harness模块运行模块的测试。可以使用“testdb”操作在Perl的调试器下运行测试。同样,可以将“debugger”参数传递给“test”操作以获得相同的效果。

  • clean, realclean

    这两个操作都会删除“build”操作创建的任何文件。 “realclean”操作还会删除现有的Build脚本。

  • diff

    此操作用于比较即将安装的文件与任何相应的已存在文件。这个功能是Module::Build独有的。

  • install

    安装模块文件。截至版本0.15,这还没有创建或安装任何手册页面。

  • fakeinstall

    告诉您“install”将执行的操作。

  • dist

    创建你分发的一个gzip压缩的tar包。

  • manifest 为你的分发创建一个MANIFEST文件。

  • distcheck

    告诉你构建目录中有哪些文件不在MANIFEST文件中,反之亦然。

  • skipcheck

    告诉你根据你的MANIFEST.SKIP文件的内容,哪些文件不会被“manifest”操作添加到MANIFEST中。

  • distclean

    这是“realclean”后跟“distcheck”的快捷方式。

  • distdir

    根据您的发行版名称和版本创建一个目录,然后将MANIFEST中列出的所有文件复制到该目录。这就是人们在下载和解包您的发行版时会看到的目录。

    Module::Build还会创建一个名为META.yaml的文件,其中包含有关您的发行版的元数据。在未来,可能可以使用(当然是用Perl编写的)命令行工具读取此文件,并使用其内容安装您的发行版,而无需运行Build.PL脚本。这也使得元数据更容易被MetaCPAN或CPAN shell等工具获取。

  • disttest

    这执行“distdir”操作,切换到新创建的目录,然后运行perl Build.PL./Build./Build test。这可以让您确保您的发行版实际上是可安装的。

  • help

    告诉您有哪些可用操作。如果某个发行版实现了额外的操作,则会在此列出。

任何这些选项都可以通过简单的子类化来覆盖,因此本文前面提到的HTML::Mason示例可以写成如下这样

  package MyFancyBuilder;

  use base 'Module::Build';

  sub ACTION_test {
      my $self = shift;

      # %MY::APACHE is set in makeconfig.pl.

      $ENV{PORT}             = $MY::APACHE{port}       || 8228;
      $ENV{APACHE_DIR}       = $MY::APACHE{apache_dir} || ";
      $ENV{MASON_VERBOSE}  ||= $self->{properties}{verbose};
      # _is_maintainer_mode would be another method of our subclass
      $ENV{MASON_MAINTAINER} = $self->_is_maintainer_mode();

      return $self->SUPER::ACTION_test(@_);
  }

这个版本实际上是可读的,并且不太可能因为Module::Build内部的更改而损坏。这突出了使用ExtUtils::MakeMaker完成简单任务是多么困难,以及纯Perl解决方案是多么自然。

整体情况和向后兼容性

Module::Build推广到广泛使用的一个困难是ExtUtils::MakeMaker的支持与CPAN安装程序紧密结合。

虽然随5.8版一起提供的CPAN.pm版本不知道如何处理Build.PL,但从CPAN提供的最新版本可以处理。它甚至可以为您安装Module::Build。截至2008年1月,另一个CPAN shell的CPANPLUS理解Build.PL,但不会为您安装Module::Build,但在未来的版本中将会解决这个问题。

然而,旧版本的CPAN.pm仍在非常广泛地使用中,并且用户在尝试安装依赖Module::Build的发行版之前,不一定升级CPAN.pm

为此问题有一些解决方案。最简单的是只包含一个Build.PL脚本,并在您的发行版中包含的READMEINSTALL文件中对此进行说明。这种方法实施起来非常简单,但缺点是当您的发行版无法正确安装时,期望CPAN shell直接工作的用户可能会放弃。

另一个可能性是创建功能等效的Build.PLMakefile.PL脚本。如果您使用Module::Build是因为您需要以难以使用ExtUtils::MakeMaker完成的方式自定义安装行为,那么这几乎就消除了使用Module::Build的目的,而且在任何情况下,有两个执行相同操作的不同代码块总是不吸引人的。

然后是使用一个Makefile.PL脚本的方法,该脚本在需要时安装Module::Build,然后生成一个Makefile,该文件将所有内容传递给./Build脚本。这被称为“中继”方法。

我认为这种方法在所付出的努力和所得到的结果之间取得了最佳平衡,这也是我首选的方法。《Module::Build》发行版包含一个Module::Build::Compat模块,它为此方法执行必要的脏活。

只需将create_makefile_pl => 'passthrough'添加到Build.PL参数中,就会在Build dist过程中创建一个Makefile.PL

下面是一个这样的Makefile.PL脚本的示例

    # Note: this file was auto-generated by Module::Build::Compat version 0.03

    unless (eval "use Module::Build::Compat 0.02; 1" ) {
      print "This module requires Module::Build to install itself.\n";

      require ExtUtils::MakeMaker;
      my $yn = ExtUtils::MakeMaker::prompt
        ('  Install Module::Build now from CPAN?', 'y');

      unless ($yn =~ /^y/i) {
        die " *** Cannot install without Module::Build.  Exiting ...\n";
      }

      require Cwd;
      require File::Spec;
      require CPAN;

      # Save this 'cause CPAN will chdir all over the place.
      my $cwd = Cwd::cwd();

      CPAN::Shell->install('Module::Build::Compat');
      CPAN::Shell->expand("Module", "Module::Build::Compat")->uptodate
        or die "Couldn't install Module::Build, giving up.\n";

      chdir $cwd or die "Cannot chdir() back to $cwd: $!";
    }
    eval "use Module::Build::Compat 0.02; 1" or die $@;

    Module::Build::Compat->run_build_pl(args => \@ARGV);
    require Module::Build;
    Module::Build::Compat->write_makefile(build_class => 'Module::Build');

那么这里到底发生了什么?这是一个很好的问题。让我们来分析一下代码。

    unless (eval "use Module::Build::Compat 0.02; 1" ) {
      print "This module requires Module::Build to install itself.\n";

      require ExtUtils::MakeMaker;
      my $yn = ExtUtils::MakeMaker::prompt
        ('  Install Module::Build now from CPAN?', 'y');

      unless ($yn =~ /^y/i) {
        die " *** Cannot install without Module::Build.  Exiting ...\n";
      }

首先尝试加载版本0.02或更高版本的Module::Build::Compat模块。如果没有安装,我们知道我们需要安装Module::Build。因为我们是礼貌的,所以在继续之前我们会询问用户是否想要安装Module::Build。有些人不喜欢交互式安装,但幸运的是,prompt()命令在检测行尾是否有用户方面非常聪明。

假设用户同意安装Module::Build(如果不同意,安装程序必须放弃),接下来是以下步骤

      # Save this 'cause CPAN will chdir all over the place.
      my $cwd = Cwd::cwd();

      CPAN::Shell->install('Module::Build::Compat');
      CPAN::Shell->expand("Module", "Module::Build::Compat")->uptodate
        or die "Couldn't install Module::Build, giving up.\n";

      chdir $cwd or die "Cannot chdir() back to $cwd: $!";

我们希望使用CPAN.pm来实际安装Module::Build,但我们需要首先保存我们的当前目录,因为CPAN.pm会多次调用chdir(),安装Module::Build后我们需要回到起始目录。

然后我们加载CPAN.pm并告诉它安装Module::Build。之后,我们使用chdir()回到原始目录。

    eval "use Module::Build::Compat 0.02; 1" or die $@;

    Module::Build::Compat->run_build_pl(args => \@ARGV);
    require Module::Build;
    Module::Build::Compat->write_makefile(build_class => 'Module::Build');

首先检查Module::Build的安装是否成功。然后简单地告诉Module::Build::Compat运行Build.PL脚本,并输出一个“passthrough”MakefileModule::Build::Compat将尝试将类似于“PREFIX”的ExtUtils::MakeMaker风格参数转换为Module::Build可以理解的参数,如“–prefix”。

Module::Build::Compat生成的“passthrough”Makefile看起来像这样

  all :
          ./Build
  realclean :
          ./Build realclean
          rm -f \$(THISFILE)
  .DEFAULT :
          ./Build \$@
  .PHONY   : install manifest

当没有与命令行给出的目标匹配的make目标时,会调用“.DEFAULT”目标。它使用“$@”make变量,它将包含传递给make的目标名称。因此,如果调用“make install”,则“$@”包含“install”,最终运行“./Build install”。

生成的Makefile还包含一个注释,指定了模块的依赖项,因为这是CPAN.pm确定模块依赖项的方式(虽然令人惊讶,但这是真的)。

这种方法是最优雅的,但将ExtUtils::MakeMaker参数转换为Module::Build可以理解的代码非常简略,无法处理所有可能性。

我已经为CPAN模块Thesaurus.pm使用了这种方法,在我的有限测试中它确实有效。如果您愿意尝试安装此模块,请将错误报告发送给我或Module::Build用户列表,module-build-general@lists.sf.net

最近,Autrijus Tang提交了一个更复杂的Makefile.PL脚本,它实现了几个额外的功能。首先,它确保脚本正在由可以实际安装Module::Build的用户运行。其次,它优先于CPANPLUS.pm而不是CPAN.pm

Autrijus的脚本很有希望,但由于它还没有经过测试,我选择不在这里包含它。很可能他的脚本的某个版本将在Module::Build的未来版本中得到记录。

自定义行为

正如之前所暗示的,您可以直接继承Module::Build以实现自定义行为。这是一个很大的话题,并将是Perl.com上未来一篇文章的主题。

未来

Module::Build上还有很多工作要做。据我所知,以下是一些仍然需要完成的事情

安装阶段还没有根据分布中包含的POD创建man页面。

Module::Build需要实现类似于ExtUtils::MakeMaker“PREFIX”参数提供的“局部安装”功能。在ExtUtils::MakeMaker中实现这一功能的逻辑非常复杂,但这是因为正确实现这一点非常复杂。需要为Module::Build实现这一逻辑。

Module::Build需要与ExtUtils::MakeMaker更好的向后兼容性。Module::Build::Compat中的参数转换目前只是一个占位符。诸如“PREFIX”,“LIB”和“UNINST=1”之类的所有内容都需要由Module::Build::Compat转换,并且需要在Module::Build中实现等效的功能。

CPANPLUS.pm 可以利用更多的 Module::Build 功能。例如,它目前忽略了 Module::Build 提供的“冲突”信息,并且没有尝试区分“build_requires”,“requires” 或 “recommends”。

Module::Build 提供的一些功能是为了供外部工具使用,例如由 META.yaml 文件提供的元数据。 CPANPLUS.pm 可以利用这个功能来避免运行 Build.PLBuild 脚本,从而避免安装任何“build_requires”模块。像 rpm 或 Debian 工具这样的包管理器也可以用它更容易地构建 Perl 模块的安装包。

将至少基本的 Module::Build 支持添加到 CPAN.pm 中将会很棒。如果有人对此感兴趣,我有一个旧补丁可能为这个目标提供一个不错的起点。如果您感兴趣,请与我联系。

更多信息

如果您想了解更多关于 Module::Build 的信息,您应该首先安装它(它将在 CPAN.pm 下很好地安装自己)并阅读 Module::BuildModule::Build::Compat 模块的文档。源代码位于 GitHub 上:Module-Build

感谢

感谢 Ken Williams 在发表前审阅了这篇文章,并编写了 Module::Build

标签

反馈

这篇文章有什么问题吗?请在 GitHub 上打开一个 issue 或 pull request 来帮助我们:GitHub