PNDP: PHP 中 Nix 的内部 DSL

v0.0.4 2022-02-26 22:15 UTC

This package is not auto-updated.

Last update: 2024-09-22 10:18:47 UTC


README

许多编程语言环境都提供自己的特定语言的包管理器,实现了一些通用包管理器已经很好地支持的功能。这个包是对这种现象的回应。

这个包包含一个库和命令行工具,提供了 PHP 中 Nix 包管理器的 内部 DSL。Nix 是一个通用的包管理器,它借鉴了纯函数编程语言的概念,以使部署可靠、可重复和高效。它是 NixOS Linux 发行版的基础,但也可以在常规 Linux 发行版、FreeBSD、Mac OS X 和 Windows(通过 Cygwin)上单独使用。

内部 PHP DSL 使得开发人员能够方便地从 PHP 程序中执行部署操作,例如使用 Nix 包管理器构建、升级和安装软件包。此外,它还提供了一些其他功能。

先决条件

  • PHP 7.4.x 或更高版本
  • 当然,由于这个包提供了 Nix 的功能,我们需要安装 Nix 包管理器

安装

可以使用 composer 全局安装此包

$ php composer.phar global require svanderburg/pndp

也可以通过检出 Git 仓库并运行来使用 Nix 安装此包

$ nix-env -f release.nix -iA package.x86_64-linux

使用方法

这个包提供了一些有趣的功能。

从 PHP 调用 Nix 函数

此包最重要的用途是能够从 PHP 中调用 Nix 函数。要调用 Nix 函数,我们必须创建一个简单的代理,将 PHP 对象方法调用转换为包含语义上等效的 Nix 函数调用的字符串。

以下代码片段演示了一个 PHP 函数代理,它将调用转发到 Nix 中的 stdenv.mkDerivation {} 函数

namespace Pkgs;
use PNDP\AST\NixFunInvocation;
use PNDP\AST\NixExpression;

class Stdenv
{
    public function mkDerivation(array $args)
    {
        return new NixFunInvocation(new NixExpression("pkgs.stdenv.mkDerivation"), $args);
    }
}

如观察到的,函数代理结构非常简单。它接受一个任意的 PHP 对象作为参数,并返回一个表示对 stdenv.mkDerivation {} 的 Nix 函数调用的 PHP 对象,使用 args 对象作为参数。

转换到 Nix 是通过 PHP 中的 phpToNix() 函数自动完成的。

将 PHP 语言结构编译成 Nix 语言结构

phpToNix() 函数被 PNDP 用于将 PHP 语言结构转换为 Nix 表达式语言结构。

PHP 对象被转换为语义上等效(或类似)的 Nix 表达式语言对象,如下所示

  • boolintfloat 类型的变量被原样转换
  • string 类型的变量被原样转换,并自动转义
  • 当一个 array 看起来是 顺序的(即它具有按顺序出现的数字键)时,它将被递归地转换为对象的列表。关联数组将被递归地转换为对象的属性集。键被转换为标识符,除非它们包含不允许这样做的人物。如果后一种情况成立,它们将被转换为字符串。
  • (从类实例化的)object 被转换为公开属性的属性集。
  • 具有 NULL 引用的变量将被翻译成 null 值。

某些 Nix 表达式语言结构在 PHP 中没有语义等价物。然而,它们可以通过组合继承自 NixObject 的类的对象来生成抽象语法树。

  • 要强制 PHP 数组以 Nix 列表的形式出现,可以组合一个 NixList 对象。
  • 要强制 PHP 数组以 Nix 属性集的形式出现,可以组合一个 NixAttrSet 对象。
  • 可以使用 NixFile 对象的实例来指定文件相对于或绝对的位置。Nix 会检查该文件是否存在并将其导入 Nix 存储库。
  • 要编码 URL,可以使用 NixURL 对象的实例。
  • 可以通过创建 NixRecursiveAttrSet 原型的对象实例来定义递归属性集(其中属性可以相互引用)。
  • 可以通过创建 NixAttrReference 对象的实例来引用属性集的属性。
  • 可以在 Nix 表达式语言中通过实例化 NixFunction 来定义函数。
  • 可以通过创建一个实例为 NixFunInvocation 的对象来调用 Nix 函数。
  • 可以通过创建一个指向外部文件的 NixImport 对象来导入外部 Nix 表达式文件。
  • 可以通过创建一个实例为 NixStorePath 的对象来引用现有的 Nix 存储库路径。
  • 可以通过定义一个 NixIf 对象来生成 if-then-else 块。
  • 可以使用 NixAssert 对象定义 assert 块。
  • 可以通过 NixLet 对象来定义包含私有值的 let 块。
  • 可以通过将对象、NixLetNixRecursiveAttrSet 的成员赋值给 NixInherit 对象的实例,将值导入块的词法作用域。
  • 可以通过创建一个 NixWith 对象将属性集的属性导入词法作用域。
  • 可以通过创建一个 NixMergeAttrs 对象合并两个属性集。
  • 要直接在 Nix 表达式语言中执行操作,可以组合实例为 NixExpression 原型的对象。
  • 可以通过创建一个 NixInlinePHP 对象(请参阅“在 PNDP 包规范中写入内联 PHP 代码”部分)来在 PHP 中定义构建指令(而不是在字符串中嵌入的 bash 代码)。

请参阅 API 文档以获取有关如何使用上述类的更多详细信息。有关 Nix 表达式语言的更多详细信息,请参阅 Nix 手册。

在 PHP 中指定包

前面显示的 Stdenv::mkDerivation() 函数是 Nix 中一个非常重要的函数。它直接和间接地被几乎每个包配方使用,以从源代码执行构建。在 tests/ 文件夹中,我们定义了一个包存储库:Pkgs.php,它提供了一个对此函数的代理。

通过使用此代理,我们也可以用 PHP 描述自己的包规范,而不是 Nix 表达式语言。每个包构建配方都可以写成提供一个静态 composePackage 方法的类。

namespace Pkgs;
use PNDP\AST\NixURL;

class Hello
{
    public static function composePackage(object $args)
    {
        return $args->stdenv->mkDerivation(array(
            "name" => "hello-2.10",

            "src" => $args->fetchurl(array(
                "url" => new NixURL("mirror://gnu/hello/hello-2.10.tar.gz"),
                "sha256" => "0ssi1wpaf7plaswqqjwigppsg5fyh99vdlb9kzl7c9lng89ndq1i"
            )),

            "doCheck" => true,

            "meta" => array(
                "description" => "A program that produces a familiar, friendly greeting",
                "homepage" => new NixURL("https://gnu.ac.cn/software/hello/manual"),
                "license" => "GPLv3+"
            )
        ));
    }
}

composePackage() 方法的主体中,我们返回对 mkDerivation() 方法的调用结果,该方法从源代码构建一个包。我们将必要构建参数传递给此方法,例如获取源代码的 URL。

Nix 有特殊的 URL 和文件类型来检查它们是否处于有效格式,并且它们会自动导入 Nix 存储库以确保纯净性。由于它们不是 PHP 语言的一部分,我们可以通过实例为 NixFileNixURL 类的对象来人工创建它们。

此外,还有一些其他Nix表达式语言结构的原型,它们没有PHP等价物。请检查API文档以获取更多信息。

组合包

与普通Nix表达式一样,我们无法直接使用一个类(定义了一个方法)来构建一个包。我们必须通过调用带有其所需参数的方法来组合它。组合是在一个名为Pkgs的组合类中完成的,位于tests/文件夹中。该类的结构如下所示

class Pkgs
{
    public $stdenv;

    public function __construct()
    {
        $this->stdenv = new Pkgs\Stdenv();
    }

    public function fetchurl(array $args)
    {
        return Pkgs\Fetchurl::composePackage($this, $args);
    }

    public function hello()
    {
        return Pkgs\Hello::composePackage($this);
    }
}

除了stdenv外,上述类将所有包都暴露为方法调用,这些方法调用将生成相应包的Nix表达式代码。

每个方法调用都会调用包的组成方法(如前一段代码所示),并将整个包集作为参数传播,以便找到其依赖项。

通过对象方法公开包组合是一个非常重复的过程。我们也可以通过实现__call()魔术方法来泛化任何包的方法调用

public function __call(string $name, array $arguments)
{
    // Compose the classname from the function name
    $className = ucfirst($name);
    // Compose the name of the method to compose the package
    $methodName = 'Pkgs\\'.$className.'::composePackage';
    // Prepend $this so that it becomes the first function parameter
    array_unshift($arguments, $this);
    // Dynamically the invoke the class' composition method with $this as first parameter and the remaining parameters
    return call_user_func_array($methodName, $arguments);
}

上述魔术方法接受要调用的方法名称,将其转换为包的相应类名(通过从方法名称生成驼峰命名),组合一个参数列表(提供一个对包对象的引用以及任何剩余的函数参数),并最终使用生成的参数调用组合方法。

在PNDP包规范中编写内联PHP代码

在实现PNDP包模块中的自定义构建过程时,我们可能还会遇到不便,即需要将自定义构建步骤作为字符串中的shell代码嵌入。我们还可以使用PNDP包类中的pndpInlineProxy,通过创建一个实例为NixInlinePHP原型的对象来实现

namespace Pkgs;
use PNDP\AST\NixInlinePHP;
use PNDP\AST\NixURL;

class CreateFileWithMessageTest
{
	public static function composePackage(object $args)
	{
		$buildCommand = <<<EOT
mkdir(getenv("out"));
file_put_contents(getenv("out")."/message.txt", "Hello world written through inline PHP!");
EOT;

		return $args->stdenv->mkDerivation(array(
			"name" => "createFileWithMessageTest",
			"buildCommand" => new NixInlinePHP($buildCommand)
		));
	}
}

上述PNDP类模块显示了包含内联PHP代码的我们的第一个Nix表达式示例的PNDP等价物。

buildCommand参数绑定到NixInlinePHP原型的实例。code参数是一个包含嵌入PHP代码的字符串。

以编程方式构建包

可以使用PNDPBuild::callNixBuild()函数来构建生成的Nix表达式

/* Evaluate the package */
$expr = PNDPBuild::evaluatePackage("Pkgs.php", "hello", false);

/* Call nix-build */
PNDPBuild::callNixBuild($expr, array());

在上面的代码片段中,我们打开名为Pkgs.php的组合类文件,并评估hello()方法以生成Nix表达式。最后,我们调用callNixBuild函数,该函数使用Nix包管理器评估生成的表达式。当构建成功时,生成的Nix存储路径将打印在标准输出上。

通过命令行实用程序构建包

由于之前的代码示例非常常见,因此还有一个可以执行相同操作的命令行实用程序。以下指令从组合类(Pkgs.php)构建hello包

$ pndp-build -f Pkgs.php -A hello

为了调试或测试目的,查看生成的Nix表达式类型可能也很有用。--eval-only选项将在标准输出上打印生成的Nix表达式

$ pndp-build -f Pkgs.js -A hello --eval-only

我们还可以很好地格式化生成的表达式以提高可读性

$ pndp-build -f Pkgs.js -A hello --eval-only --format

从Nix表达式构建PNDP包

我们还可以从Nix表达式调用组合类。这对于从Hydra(一个围绕Nix构建和集成服务器构建的持续构建和集成服务器)构建PNDP包非常有用。

以下Nix表达式构建了之前在Pkgs.php类中显示的hello包

{nixpkgs, system, pndp}:

let
  pndpImportPackage = import ./src/PNDP/importPackage.nix {
    inherit nixpkgs;
    system = builtins.currentSystem;
    pndp = builtins.getAttr (builtins.currentSystem) (jobs.package);
  };
in
{
  hello = pndpImportPackage {
    pkgsPhpFile = "${./.}/tests/Pkgs.php";
    autoloadPhpFile = "${./.}/vendor/autoload.php";
    attrName = "hello";
  };
  ...
}

将自定义对象结构转换为Nix表达式

如前所述,PNDP将PHP语言中的对象转换为Nix表达式语言中的语义等效(或类似)结构。

有时,可能还需要从领域模型生成 Nix 表达式,该模型用于解决特定与部署无关的问题,其属性和结构不能直接转换为 Nix 表达式语言中的表示。

也可以为对象指定如何从中生成 Nix 表达式。这可以通过继承 NixASTNode 类并重写 toNixAST 方法来实现。

例如,我们可能有一个系统,该系统已经提供了一个文件的表示,该文件需要从外部源下载

class HelloSourceModel
{
    private object $args;
    private string $src;
    private string $sha256;

    public function __construct(object $args)
    {
        $this->args = $args;
        $this->src = "mirror://gnu/hello/hello-2.10.tar.gz";
        $this->sha256 = "0ssi1wpaf7plaswqqjwigppsg5fyh99vdlb9kzl7c9lng89ndq1i";
    }
}

上述类的构造函数组合了一个对象,该对象引用了由 GNU 镜像站点提供的 GNU Hello 软件包。

将上述构建的对象直接转换为 Nix 表达式语言没有任何意义 - 例如,不能用它让 Nix 从镜像站点获取软件包。

我们可以继承 NixASTNode 并实现我们自己的自定义 toNixAST() 方法,以提供更有意义的 Nix 转换

use PNDP\AST\NixASTNode;
use PNDP\AST\NixURL;

class HelloSourceModel extends NixASTNode
{
    ...
    /**
     * @see NixASTConvertable::toNixAST()
     */
    public function toNixAST()
    {
        return $this->args->fetchurl(array(
            "url" => new NixURL($this->src),
            "sha256" => $this->sha256
        ));
    }
}

上述 toNixAST() 方法在 Nix 表达式语言中组合了一个函数调用的抽象语法树 (AST),即 fetchurl {},并将 urlsha256 属性作为参数。

NixASTNode 继承的对象也间接继承了 NixObject。这意味着我们可以直接将此类对象附加到任何其他 AST 对象上。生成器使用底层的 toNixAST() 方法自动将其转换为其 AST 表示。

例如,我们也可以将 GNU Hello 软件包定义为自定义类的实例

class HelloModel
{
    private object $args;
    private string $name;
    private HelloSourceModel $source;
    private array $meta;

    public function __construct(object $args)
    {
        $this->args = $args;

        $this->name = "hello-2.10";
        $this->source = new HelloSourceModel($args);
        $this->meta = array(
            "description" => "A program that produces a familiar, friendly greeting",
            "homepage" => "https://gnu.ac.cn/software/hello/manual",
            "license" => "GPLv3+"
        );
    }
}

在上面的函数中,我们使用 Nix 中无法直接使用的约定构建了 GNU Hello 的构建配方。该对象还引用了一个源对象,该对象是前面示例中所示类的实例。

通过继承 NixASTNode 并重写 toNixAST(),我们可以构建一个构建软件包的 Nix 表达式的 AST

use PNDP\AST\NixASTNode;

class HelloModel extends NixASTNode
{
    ...
    /**
     * @see NixASTConvertable::toNixAST()
     */
    public function toNixAST()
    {
        return $this->args->stdenv->mkDerivation(array(
            "name" => $this->name,
            "src" => $this->source,
            "doCheck" => true,
            "meta" => $this->meta
        ));
    }
}

上述函数使用对象的属性作为参数,构建了一个调用 stdenv.mkDerivation {} 函数的 AST。它直接引用源对象($this->source),无需任何转换 - 因为源对象继承自 NixAST 和(间接)继承自 NixObject,生成器将自动将其转换为 fetchurl {} 函数调用的 AST。

在某些情况下,可能无法继承 NixASTNode,例如,当对象已经继承自用户无法控制的另一个类时。

还可以将 NixASTNode 构造函数用作任何实现 NixASTConvertable 接口的对象的适配器。

例如,我们可能想使用包装器将元数据转换为更适合转换为 Nix 表达式的表示

use PNDP\AST\NixASTConvertable;
use PNDP\AST\NixURL;

class MetaDataWrapper implements NixASTConvertable
{
    private array $meta;

    public function __construct(array $meta)
    {
        $this->meta = $meta;
    }

    public function toNixAST()
    {
        return array(
            "description" => $this->meta["description"],
            "homepage" => new NixURL($this->meta["homepage"]),
            "license" => $this->meta["license"]
        );
    }
}

通过将 MetaDataWrapper 对象实例包装到 NixASTNode 构造函数中,我们可以将其转换为 NixASTNode 的实例

new NixASTNode(new MetaDataWrapper($this->meta))

示例

tests/ 目录中包含了一组示例软件包

  • Pkgs.php 是一个组合模块,包含了一组 PNDP 软件包。在 tests/pkgs 子目录中的每个类定义了一个软件包组合。

许可证

本软件包的内容可在 MIT 许可证 下使用。