svanderburg / pndp
PNDP: PHP 中 Nix 的内部 DSL
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 表达式语言对象,如下所示
bool
、int
和float
类型的变量被原样转换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 块。 - 可以通过将对象、
NixLet
或NixRecursiveAttrSet
的成员赋值给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 语言的一部分,我们可以通过实例为 NixFile
和 NixURL
类的对象来人工创建它们。
此外,还有一些其他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 {}
,并将 url
和 sha256
属性作为参数。
从 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 许可证 下使用。