svanderburg / pndp
PNDP: An internal DSL for Nix in PHP
Installs: 3 936
Dependents: 1
Suggesters: 0
Security: 0
Stars: 4
Watchers: 3
Forks: 0
Open Issues: 0
This package is not auto-updated.
Last update: 2024-12-15 11:16:59 UTC
README
Many programming language environments provide their own language specific package manager, implementing features that are already well supported by generic ones. This package is a response to that phenomenon.
This package contains a library and command-line utility providing an internal DSL for the Nix package manager in PHP. Nix is a generic package manager that borrows concepts from purely functional programming languages to make deployment reliable, reproducible and efficient. It serves as the basis of the NixOS Linux distribution, but it can also be used seperately on regular Linux distributions, FreeBSD, Mac OS X and Windows (through Cygwin).
The internal PHP DSL makes it possible for developers to convienently perform deployment operations, such as building, upgrading and installing packages with the Nix package manager from PHP programs. Moreover, it offers a number of additional features.
Prerequisites
- PHP 7.4.x or higher
- Of course, since this package provides a feature for Nix, we require the Nix package manager to be installed
Installation
This package can be installed globally with composer:
$ php composer.phar global require svanderburg/pndp
It is also possible to install the package with Nix by checking out the Git repository and running:
$ nix-env -f release.nix -iA package.x86_64-linux
Usage
This package offers a number of interesting features.
Calling a Nix function from PHP
The most important use case of this package is to be able to call Nix functions from PHP. To call a Nix function, we must create a simple proxy that translates a PHP object method invocation to a string containing a semantically equivalent Nix function invocation.
The following code fragment demonstrates a PHP function proxy that relays a call
to the stdenv.mkDerivation {}
function in Nix:
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); } }
As can be observed, the function proxy has a very simple structure. It takes an
arbitrary PHP object as a parameter and returns a PHP object representing a Nix
function invocation to stdenv.mkDerivation {}
using the args
object as an
argument.
The conversion to Nix is done automatically by PHP through a function called
phpToNix()
.
Compiling PHP language constructs into Nix language constructs
The phpToNix()
function is used by PNDP to transform PHP language constructs
to Nix expression language constructs.
PHP objects are translated into semantically equivalent (or similar) Nix expression language objects as follows:
- Variables of type
bool
,int
andfloat
are translated verbatim - Variables of type
string
are translated verbatim and are automatically escaped - When an
array
appears to be sequential (i.e. it has numeric keys appearing in sequential order) it will be recursively translated into a list of objects. Associative arrays will be recursively translated into attribute sets of objects. Keys are translated into identifiers unless they contain characters not allowing it do to so. If the latter is the case, they are translated into strings. object
s (that are instantiated from classes) are translated into attribute sets exposing their public properties.- A variable that has a
NULL
reference is translated into anull
value.
Some Nix expression language constructs have no semantic equivalent in PHP.
Nonetheless, they can be generated by composing an abstract syntax tree of
objects that are instances of classes that inherit from NixObject
:
- To force a PHP array to appear as a Nix list, you can compose a
NixList
object. - To force a PHP array to appear as a Nix attribute set, you can compose a
NixAttrSet
object. - An object instance of
NixFile
can be used to specify a relative or absolute path to a file. Nix checks whether the file exists and imports it into the Nix store. - To encode URLs, an object instance of
NixURL
can be used. - Recursive attribute sets (in which attributes are allowed to refer to each
other) can be defined by creating an object instance of the
NixRecursiveAttrSet
prototype. - Referring to an attribute of an attribute set can be done by creating an
object instance of
NixAttrReference
- Defining functions in the Nix expression language can be done by instantiating
NixFunction
. - A Nix function can be invoked by creating an object that is an instance of
NixFunInvocation
. - An external Nix expression file can be imported by creating a
NixImport
object that refers to an external file. - Referring to existing Nix store paths can be done by creating objects that are
instances of
NixStorePath
. - An if-then-else block can be generated by defining an
NixIf
object. - An assert block can be defined with an
NixAssert
object. - A let-block containing private values can be defined by means of a
NixLet
object. - A value can be imported into the lexical scope of a block by assigning a
member of an object,
NixLet
, orNixRecursiveAttrSet
to an object instance ofNixInherit
. - Attributes member of an attribute set can be imported into the lexical scope
by creating a
NixWith
object. - Two attribute sets can be merged by creating a
NixMergeAttrs
object. - To literally do stuff in the Nix expression language, compose objects that are
instances of the
NixExpression
prototype. - Defining build instructions in PHP (as opposed to bash code embedded in
strings) can be done by creating a
NixInlinePHP
object (see section: 'Writing inline PHP code in a PNDP package specification').
Check the API documentation for more details on how to use the above classes. More details on the Nix expression language can be found in the Nix manual.
Specifying packages in PHP
The Stdenv::mkDerivation()
function shown earlier is a very important function
in Nix. It's directly and indirectly used by nearly every package recipe to
perform a build from source code. In the tests/
folder, we have defined a
packages repository: Pkgs.php
that provides a proxy to this function.
By using this proxy we can also describe our own package specifications in
PHP, instead of the Nix expression language. Every package build recipe
can be written as a class that provides a static composePackage
method:
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("http://www.gnu.org/software/hello/manual"), "license" => "GPLv3+" ) )); } }
In the body of the composePackage()
method, we return the result of an
invocation to the mkDerivation()
method that builds a package from source
code. To this method we pass essential build parameters, such as the URL from
which the source code can be obtained.
Nix has special types for URLs and files to check whether they are in the valid
format and that they are automatically imported into the Nix store for purity.
As they are not in the PHP language, we can artificially create them through
objects that are instances of the NixFile
and NixURL
classes.
Moreover, there are more prototypes for some other Nix expression language constructs that have no PHP equivalent. Check the API documentation for more information.
Composing packages
As with ordinary Nix expressions, we cannot use a class (that defines a method)
to build a package directly. We have to compose it by calling the method with
its required arguments. Composition is done in a composition class, named: Pkgs
in the tests/
folder. The structure of this class looks as follows:
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); } }
With the exception of stdenv
, the above class exposes all packages as method
invocations that will generate the Nix expression code for the corresponding
package.
Each method invocation invokes the composition method of a package (shown in the previous code fragment) and propagates the entire package set as parameters so that its dependencies can be found.
Exposing package compositions through object methods is a very repetitive
process. We can also generalize the method invocation of any package by
implementing the __call()
magic method:
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); }
The above magic method takes the name of the method to be invoked, translates it into the corresponding classname of the package (by generating a camel case name from the method name), composes an argument list (providing a reference to the packages object and any remaining function arguments) and finally invokes the composition method with the generated parameters.
Writing inline PHP code in a PNDP package specification
When implementing a custom build procedure in a PNDP package module, we may
also run into the inconvenience of having to embed custom build steps as shell
code embedded in strings. We can also use the pndpInlineProxy
from a PNDP
package class, by creating an object that is an instance of the NixInlinePHP
prototype:
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) )); } }
The above PNDP class module shows the PNDP equivalent of our first Nix expression example containing inline PHP code.
The buildCommand
parameter is bound to an instance of the NixInlinePHP
prototype. The code
parameter is a string that contains embedded PHP code.
Building packages programmatically
The PNDPBuild::callNixBuild()
function can be used to build a generated Nix
expression:
/* Evaluate the package */ $expr = PNDPBuild::evaluatePackage("Pkgs.php", "hello", false); /* Call nix-build */ PNDPBuild::callNixBuild($expr, array());
In the code fragment above, we open the composition class file, named:
Pkgs.php
and we evaluate the hello()
method to generate the Nix expression.
Finally, we call the callNixBuild
function, in which we evaluate the generated
expression by the Nix package manager. When the build succeeds, the resulting
Nix store path is printed on the standard output.
Building packages through a command-line utility
As the previous code example is so common, there is also a command-line utility
that can do the same. The following instruction builds the hello package from the
composition class (Pkgs.php
):
$ pndp-build -f Pkgs.php -A hello
It may also be useful to see what kind of Nix expression is generated for
debugging or testing purposes. The --eval-only
option prints the generated
Nix expression on the standard output:
$ pndp-build -f Pkgs.js -A hello --eval-only
We can also nicely format the generated expression to improve readability:
$ pndp-build -f Pkgs.js -A hello --eval-only --format
Building PNDP packages from a Nix expression
We can also call the composition class from a Nix expression. This is useful to build PNDP packages from Hydra, a continuous build and integration server built around Nix.
The following Nix expression builds the hello package defined in the Pkgs.php
class shown earlier:
{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"; }; ... }
Transforming custom object structures into Nix expressions
As explained earlier, PNDP transforms objects in the PHP language to semantically equivalent (or similar) constructs in the Nix expression language.
Sometimes it may also be desired to generate Nix expressions from a domain model, designed for solving a specific non-deployment related problem, with properties and structures that cannot be literally translated into a representation in the Nix expression language.
It is also possible to specify for an object how to generate a Nix expression
from it. This can be done by inheriting from the NixASTNode
class and
overriding the toNixAST
method.
For example, we may have a system already providing a representation of a file that should be downloaded from an external source:
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"; } }
The above class' constructor composes an object that refers to the GNU Hello package provided by a GNU mirror site.
A direct translation of the above constructed object to the Nix expression language does not provide anything meaningful -- it can, for example, not be used to let Nix fetch the package from the mirror site.
We can inherit from NixASTNode
and implement our own custom toNixAST()
method to provide a more meaningful Nix translation:
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 )); } }
The toNixAST()
method shown above composes an abstract syntax tree (AST) for
a function invocation to fetchurl {}
in the Nix expression language with the
url
and sha256
properties a parameters.
An object that inherits from the NixASTNode
class also indirectly inherits
from NixObject
. This means that we can directly attach such an object to any
other AST object. The generator uses the underlying toNixAST()
method to
automatically convert it to its AST representation.
For example, we can also define the GNU Hello package as an object that is an instance of a custom class:
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" => "http://www.gnu.org/software/hello/manual", "license" => "GPLv3+" ); } }
In the above function, we construct a build recipe for GNU Hello using a convention that is not directly usable in Nix. The object also has a reference to a source object that is an instance of the class shown in the previous example.
By inheriting from NixASTNode
and overriding toNixAST()
, we can construct an
AST for a Nix expression that builds the package:
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 )); } }
The above function constructs an AST for a function invocation to
stdenv.mkDerivation {}
, using the object's properties a parameters. It
directly refers to the source object ($this->source
) without any conversions
-- because the source object inherits from NixAST
and (indirectly) from
NixObject
, the generator will automatically convert it to an AST for a
fetchurl {}
function invocation.
In some cases, it may not be possible to inherit from NixASTNode
, for example,
when the object already inherits from another class that is beyond the user's
control.
It is also possible to use the NixASTNode
constructor function as an adapter
for any object that implements the NixASTConvertable
interface.
For example, we may want to use a wrapper to transform the metadata in a more suitable representation for conversion to a Nix expression:
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"] ); } }
By wrapping the MetaDataWrapper
object instance into the NixASTNode
constructor, we can convert it to an object that is an instance of NixASTNode
:
new NixASTNode(new MetaDataWrapper($this->meta))
Examples
The tests/
directory contains a collection of example packages:
Pkgs.php
is a composition module containing a collection of PNDP packages. Each class in thetests/pkgs
sub folder defines a package composition.
License
The contents of this package is available under the MIT license