Unverified Commit 350cac13 authored by Elis Hirwing's avatar Elis Hirwing Committed by GitHub
Browse files

Merge pull request #248184 from NixOS/php/add-new-builder-only

php: add new Composer builder
parents 2d7b472c 3eb168da
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -257,7 +257,8 @@ pkgs/development/python-modules/buildcatrust/ @ajs124 @lukegb @mweinelt
# PHP interpreter, packages, extensions, tests and documentation
/doc/languages-frameworks/php.section.md          @aanderse @drupol @etu @globin @ma27 @talyz
/nixos/tests/php                                  @aanderse @drupol @etu @globin @ma27 @talyz
/pkgs/build-support/build-pecl.nix                @aanderse @drupol @etu @globin @ma27 @talyz
/pkgs/build-support/php/build-pecl.nix            @aanderse @drupol @etu @globin @ma27 @talyz
/pkgs/build-support/php                                     @drupol @etu
/pkgs/development/interpreters/php       @jtojnar @aanderse @drupol @etu @globin @ma27 @talyz
/pkgs/development/php-packages                    @aanderse @drupol @etu @globin @ma27 @talyz
/pkgs/top-level/php-packages.nix         @jtojnar @aanderse @drupol @etu @globin @ma27 @talyz
+139 −1
Original line number Diff line number Diff line
@@ -130,6 +130,7 @@ package: a project may depend on certain extensions and `composer`
won't work with that project unless those extensions are loaded.

Example of building `composer` with additional extensions:

```nix
(php.withExtensions ({ all, enabled }:
  enabled ++ (with all; [ imagick redis ]))
@@ -138,7 +139,9 @@ Example of building `composer` with additional extensions:

### Overriding PHP packages {#ssec-php-user-guide-overriding-packages}

`php-packages.nix` form a scope, allowing us to override the packages defined within. For example, to apply a patch to a `mysqlnd` extension, you can simply pass an overlay-style function to `php`’s `packageOverrides` argument:
`php-packages.nix` form a scope, allowing us to override the packages defined
within. For example, to apply a patch to a `mysqlnd` extension, you can simply
pass an overlay-style function to `php`’s `packageOverrides` argument:

```nix
php.override {
@@ -153,3 +156,138 @@ php.override {
  };
}
```

### Building PHP projects {#ssec-building-php-projects}

With [Composer](https://getcomposer.org/), you can effectively build PHP
projects by streamlining dependency management. As the de-facto standard
dependency manager for PHP, Composer enables you to declare and manage the
libraries your project relies on, ensuring a more organized and efficient
development process.

Composer is not a package manager in the same sense as `Yum` or `Apt` are. Yes,
it deals with "packages" or libraries, but it manages them on a per-project
basis, installing them in a directory (e.g. `vendor`) inside your project. By
default, it does not install anything globally. This idea is not new and
Composer is strongly inspired by node's `npm` and ruby's `bundler`.

Currently, there is no other PHP tool that offers the same functionality as
Composer. Consequently, incorporating a helper in Nix to facilitate building
such applications is a logical choice.

In a Composer project, dependencies are defined in a `composer.json` file,
while their specific versions are locked in a `composer.lock` file. Some
Composer-based projects opt to include this `composer.lock` file in their source
code, while others choose not to.

In Nix, there are multiple approaches to building a Composer-based project.

One such method is the `php.buildComposerProject` helper function, which serves
as a wrapper around `mkDerivation`.

Using this function, you can build a PHP project that includes both a
`composer.json` and `composer.lock` file. If the project specifies binaries
using the `bin` attribute in `composer.json`, these binaries will be
automatically linked and made accessible in the derivation. In this context,
"binaries" refer to PHP scripts that are intended to be executable.

To use the helper effectively, simply add the `vendorHash` attribute, which
enables the wrapper to handle the heavy lifting.

Internally, the helper operates in three stages:

1. It constructs a `composerRepository` attribute derivation by creating a
   composer repository on the filesystem containing dependencies specified in
   `composer.json`. This process uses the function
   `php.mkComposerRepository` which in turn uses the
   `php.composerHooks.composerRepositoryHook` hook. Internaly this function uses
   a custom
   [Composer plugin](https://github.com/nix-community/composer-local-repo-plugin) to
   generate the repository.
2. The resulting `composerRepository` derivation is then used by the
   `php.composerHooks.composerInstallHook` hook, which is responsible for
   creating the final `vendor` directory.
3. Any "binary" specified in the `composer.json` are linked and made accessible
   in the derivation.

As the autoloader optimization can be activated directly within the
`composer.json` file, we do not enable any autoloader optimization flags.

To customize the PHP version, you can specify the `php` attribute. Similarly, if
you wish to modify the Composer version, use the `composer` attribute. It is
important to note that both attributes should be of the `derivation` type.

Here's an example of working code example using `php.buildComposerProject`:

```nix
{ php, fetchFromGitHub }:

php.buildComposerProject (finalAttrs: {
  pname = "php-app";
  version = "1.0.0";

  src = fetchFromGitHub {
    owner = "git-owner";
    repo = "git-repo";
    rev = finalAttrs.version;
    hash = "sha256-VcQRSss2dssfkJ+iUb5qT+FJ10GHiFDzySigcmuVI+8=";
  };

  # PHP version containing the `ast` extension enabled
  php = php.buildEnv {
    extensions = ({ enabled, all }: enabled ++ (with all; [
      ast
    ]));
  };

  # The composer vendor hash
  vendorHash = "sha256-86s/F+/5cBAwBqZ2yaGRM5rTGLmou5//aLRK5SA0WiQ=";

  # If the composer.lock file is missing from the repository, add it:
  # composerLock = ./path/to/composer.lock;
})
```

In case the file `composer.lock` is missing from the repository, it is possible
to specify it using the `composerLock` attribute.

The other method is to use all these methods and hooks individually. This has
the advantage of building a PHP library within another derivation very easily
when necessary.

Here's a working code example to build a PHP library using `mkDerivation` and
separate functions and hooks:

```nix
{ stdenvNoCC, fetchFromGitHub, php }:

stdenvNoCC.mkDerivation (finalAttrs:
let
  src = fetchFromGitHub {
    owner = "git-owner";
    repo = "git-repo";
    rev = finalAttrs.version;
    hash = "sha256-VcQRSss2dssfkJ+iUb5qT+FJ10GHiFDzySigcmuVI+8=";
  };
in {
  inherit src;
  pname = "php-app";
  version = "1.0.0";

  buildInputs = [ php ];

  nativeBuildInputs = [
    php.packages.composer
    # This hook will use the attribute `composerRepository`
    php.composerHooks.composerInstallHook
  ];

  composerRepository = php.mkComposerRepository {
    inherit (finalAttrs) src;
    # Specifying a custom composer.lock since it is not present in the sources.
    composerLock = ./composer.lock;
    # The composer vendor hash
    vendorHash = "sha256-86s/F+/5cBAwBqZ2yaGRM5rTGLmou5//aLRK5SA0WiQ=";
  };
})
```
+71 −0
Original line number Diff line number Diff line
{ callPackage, stdenvNoCC, lib, writeTextDir, php, makeBinaryWrapper, fetchFromGitHub, fetchurl }:

let
  buildComposerProjectOverride = finalAttrs: previousAttrs:

    let
      phpDrv = finalAttrs.php or php;
      composer = finalAttrs.composer or phpDrv.packages.composer;
      composer-local-repo-plugin = callPackage ./pkgs/composer-local-repo-plugin.nix { };
    in
    {
      composerLock = previousAttrs.composerLock or null;
      composerNoDev = previousAttrs.composerNoDev or true;
      composerNoPlugins = previousAttrs.composerNoPlugins or true;
      composerNoScripts = previousAttrs.composerNoScripts or true;

      nativeBuildInputs = (previousAttrs.nativeBuildInputs or [ ]) ++ [
        composer
        composer-local-repo-plugin
        phpDrv.composerHooks.composerInstallHook
      ];

      buildInputs = (previousAttrs.buildInputs or [ ]) ++ [
        phpDrv
      ];

      patches = previousAttrs.patches or [ ];
      strictDeps = previousAttrs.strictDeps or true;

      # Should we keep these empty phases?
      configurePhase = previousAttrs.configurePhase or ''
        runHook preConfigure

        runHook postConfigure
      '';

      buildPhase = previousAttrs.buildPhase or ''
        runHook preBuild

        runHook postBuild
      '';

      doCheck = previousAttrs.doCheck or true;
      checkPhase = previousAttrs.checkPhase or ''
        runHook preCheck

        runHook postCheck
      '';

      installPhase = previousAttrs.installPhase or ''
        runHook preInstall

        runHook postInstall
      '';

      composerRepository = phpDrv.mkComposerRepository {
        inherit composer composer-local-repo-plugin;
        inherit (finalAttrs) patches pname src vendorHash version;

        composerLock = previousAttrs.composerLock or null;
        composerNoDev = previousAttrs.composerNoDev or true;
        composerNoPlugins = previousAttrs.composerNoPlugins or true;
        composerNoScripts = previousAttrs.composerNoScripts or true;
      };

      meta = previousAttrs.meta or { } // {
        platforms = lib.platforms.all;
      };
    };
in
args: (stdenvNoCC.mkDerivation args).overrideAttrs buildComposerProjectOverride
+87 −0
Original line number Diff line number Diff line
{ callPackage, stdenvNoCC, lib, writeTextDir, fetchFromGitHub, php }:

let
  mkComposerRepositoryOverride =
    /*
      We cannot destruct finalAttrs since the attrset below is used to construct it
      and Nix currently does not support lazy attribute names.
      {
      php ? null,
      composer ? null,
      composerLock ? "composer.lock",
      src,
      vendorHash,
      ...
      }@finalAttrs:
    */
    finalAttrs: previousAttrs:

    let
      phpDrv = finalAttrs.php or php;
      composer = finalAttrs.composer or phpDrv.packages.composer;
      composer-local-repo-plugin = callPackage ./pkgs/composer-local-repo-plugin.nix { };
    in
    assert (lib.assertMsg (previousAttrs ? src) "mkComposerRepository expects src argument.");
    assert (lib.assertMsg (previousAttrs ? vendorHash) "mkComposerRepository expects vendorHash argument.");
    assert (lib.assertMsg (previousAttrs ? version) "mkComposerRepository expects version argument.");
    assert (lib.assertMsg (previousAttrs ? pname) "mkComposerRepository expects pname argument.");
    assert (lib.assertMsg (previousAttrs ? composerNoDev) "mkComposerRepository expects composerNoDev argument.");
    assert (lib.assertMsg (previousAttrs ? composerNoPlugins) "mkComposerRepository expects composerNoPlugins argument.");
    assert (lib.assertMsg (previousAttrs ? composerNoScripts) "mkComposerRepository expects composerNoScripts argument.");
    {
      composerNoDev = previousAttrs.composerNoDev or true;
      composerNoPlugins = previousAttrs.composerNoPlugins or true;
      composerNoScripts = previousAttrs.composerNoScripts or true;

      name = "${previousAttrs.pname}-${previousAttrs.version}-composer-repository";

      # See https://github.com/NixOS/nix/issues/6660
      dontPatchShebangs = previousAttrs.dontPatchShebangs or true;

      nativeBuildInputs = (previousAttrs.nativeBuildInputs or [ ]) ++ [
        composer
        composer-local-repo-plugin
        phpDrv.composerHooks.composerRepositoryHook
      ];

      buildInputs = previousAttrs.buildInputs or [ ];

      strictDeps = previousAttrs.strictDeps or true;

      # Should we keep these empty phases?
      configurePhase = previousAttrs.configurePhase or ''
        runHook preConfigure

        runHook postConfigure
      '';

      buildPhase = previousAttrs.buildPhase or ''
        runHook preBuild

        runHook postBuild
      '';

      doCheck = previousAttrs.doCheck or true;
      checkPhase = previousAttrs.checkPhase or ''
        runHook preCheck

        runHook postCheck
      '';

      installPhase = previousAttrs.installPhase or ''
        runHook preInstall

        runHook postInstall
      '';

      COMPOSER_CACHE_DIR = "/dev/null";
      COMPOSER_MIRROR_PATH_REPOS = "1";
      COMPOSER_HTACCESS_PROTECT = "0";
      COMPOSER_DISABLE_NETWORK = "0";

      outputHashMode = "recursive";
      outputHashAlgo = if (finalAttrs ? vendorHash && finalAttrs.vendorHash != "") then null else "sha256";
      outputHash = finalAttrs.vendorHash or "";
    };
in
args: (stdenvNoCC.mkDerivation args).overrideAttrs mkComposerRepositoryOverride
Loading