Initial commit

This commit is contained in:
Sander van der Burg 2017-07-07 22:51:59 +02:00
commit e66636acb9
7 changed files with 475 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
vendor/

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2017 Sander van der Burg <svanderburg@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

40
README.md Normal file
View file

@ -0,0 +1,40 @@
composer2nix
============
`composer2nix` is a tool that can be used to generate [Nix](http://nixos.org)
expressions for PHP [composer](https://getcomposer.org) packages.
Nix integration makes it possible to use the Nix package manager (as opposed to
composer) to deploy PHP packages including all their required dependencies.
In addition, generated Nix composer packages
support convenient integration of PHP applications with NixOS services, such as
NixOS' Apache HTTP service.
Usage
=====
You need a project providing both a `composer.json` and a `composer.lock`
configuration file.
Running the following command generates Nix expressions from the composer
configuration files:
$ composer2nix
The above command produces three expressions: `php-packages.nix` containing the
dependencies, `composer-env.nix` the build infrastructure and `default.nix` that
can be used to compose the package from its dependencies.
Running the following command-line instruction deploys the package with Nix
including its dependencies:
$ nix-build
Limitations
===========
Currently, the state of this tool is that it is just a proof on concept
implementation. As a result, it is lacking many features and probably buggy.
Most importantly, only the `zip` and `git` dependencies are supported.
License
=======
The contents of this package is available under the [MIT license](http://opensource.org/licenses/MIT)

132
bin/composer2nix.php Executable file
View file

@ -0,0 +1,132 @@
#!/usr/bin/env php
<?php
require_once(dirname(__FILE__)."/../vendor/autoload.php");
use Composer2Nix\Generator;
function displayHelp($command)
{
print("Usage: ".$command." [OPTION]\n\n");
echo <<<EOT
This executable can be used to generate Nix expressions from a composer.lock
(and a composer.json) file so that a package and all its dependencies can be
deployed by the Nix package manager.
Options:
--config-file=FILE Path to the composer.json file (defaults to:
composer.json)
--lock-file=FILE Path to the composer.lock file (defaults to:
composer.lock)
--output=FILE Path to the Nix expression containing the generated
packages (defaults to: php-packages.nix)
--composition=FILE Path to the Nix expression that composes the package
(defaults to: default.nix)
--composer-env=FILE Path to the Nix expression deploying the composer
packages (defaults to: composer-env.nix)
--prefer-source Forces installation from package sources when possible
--prefer-dist Forces installation from package dist
--name Name of the generated package (defaults to the name
provided in the composer.json file)
--no-copy-composer-env
Do not create a copy of the Nix expression that builds
composer packages
-h, --help Shows the usage of this command
-v, --version Shows the version of this command
EOT;
}
function displayVersion($command)
{
print($command." (composer2nix 0.0.1)\n\nCopyright (C) 2017 Sander van der Burg\n");
}
/* Parse command line options */
$options = getopt("hv", array(
"config-file:",
"lock-file:",
"output:",
"composition:",
"composer-env:",
"prefer-source",
"prefer-dist",
"name:",
"no-copy-composer-env",
"help",
"version"
));
if($options === false)
{
fwrite(STDERR, "Cannot parse the command-line options!\n");
exit(1);
}
/* Parse the options themselves */
if(array_key_exists("h", $options) || array_key_exists("help", $options))
{
displayHelp($argv[0]);
exit();
}
if(array_key_exists("v", $options) || array_key_exists("version", $options))
{
displayVersion($argv[0]);
exit();
}
if(array_key_exists("config-file", $options))
$configFile = $options["config-file"];
else
$configFile = "composer.json";
if(array_key_exists("lock-file", $options))
$lockFile = $options["lock-file"];
else
$lockFile = "composer.lock";
if(array_key_exists("output", $options))
$outputFile = $options["output"];
else
$outputFile = "php-packages.nix";
if(array_key_exists("composition", $options))
$compositionFile = $options["composition"];
else
$compositionFile = "default.nix";
if(array_key_exists("composer-env", $options))
$composerEnvFile = $options["composer-env"];
else
$composerEnvFile = "composer-env.nix";
$noCopyComposerEnv = array_key_exists("no-copy-composer-env", $options);
$installType = "dist"; // TODO: consult composer.json's preferred-install property. defaults to: auto
if(array_key_exists("prefer-source", $options))
$installType = "source";
if(array_key_exists("prefer-dist", $options))
$installType = "dist";
if(array_key_exists("name", $options))
$name = $options["name"];
else
$name = null;
/* Execute the generator */
try
{
Generator::generateNixExpressions($name, $installType, $configFile, $lockFile, $outputFile, $compositionFile, $composerEnvFile, $noCopyComposerEnv);
}
catch(Exception $ex)
{
fwrite(STDERR, $ex->getMessage()."\n");
exit(1);
}
?>

19
composer.json Normal file
View file

@ -0,0 +1,19 @@
{
"name": "svanderburg/composer2nix",
"description": "Generate Nix expressions to build PHP composer packages",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Sander van der Burg",
"email": "svanderburg@gmail.com",
"homepage": "http://sandervanderburg.nl"
}
],
"autoload": {
"psr-4": { "Composer2Nix\\": "src/Composer2Nix" }
},
"bin": [ "bin/composer2nix" ]
}

View file

@ -0,0 +1,140 @@
<?php
namespace Composer2Nix;
use Exception;
class Generator
{
function generateNixExpressions($name, $installType, $configFile, $lockFile, $outputFile, $compositionFile, $composerEnvFile, $noCopyComposerEnv)
{
/* Open the composer.json file and decode it */
$composerJSONStr = file_get_contents($configFile);
if($composerJSONStr === false)
throw new Exception("Cannot open contents of: ".$configFile);
$config = json_decode($composerJSONStr, true);
/* If no package name has been provided, attempt to use the name in the composer config file */
if($name === null)
{
if(array_key_exists("name", $config))
$name = $config["name"];
else
{
throw new Exception("Cannot determine a package name! Either add a name\n".
"property to the composer.json file or provide a --name parameter!");
}
}
$name = strtr($name, "/", "-"); // replace / by - since / is not allowed in Nix package names
/* Open the lock file and decode it */
if(file_exists($lockFile))
{
$composerLockStr = file_get_contents($lockFile);
if($composerLockStr === false)
throw new Exception("Cannot open contents of: ".$lockFile);
$lockConfig = json_decode($composerLockStr, true);
}
else
$lockConfig = null;
/* Generate packages expression */
$handle = fopen($outputFile, "w");
if($handle === false)
throw new Exception("Cannot write to: ".$outputFile);
if($lockConfig === null)
$packages = array();
else
$packages = $lockConfig["packages"];
fwrite($handle, "{composerEnv, fetchgit ? null}:\n\n");
fwrite($handle, "let\n");
fwrite($handle, " dependencies = {\n");
foreach($packages as $package)
{
$sourceObj = $package[$installType];
switch($sourceObj["type"])
{
case "zip":
$hash = shell_exec('nix-prefetch-url "'.$sourceObj['url'].'"');
fwrite($handle, ' "'.$package["name"].'" = composerEnv.buildZipPackage {'."\n");
fwrite($handle, ' name = "'.strtr($package["name"], "/", "-").'-'.$sourceObj["reference"].'";'."\n");
fwrite($handle, ' url = "'.$sourceObj["url"].'";'."\n");
fwrite($handle, ' sha256 = "'.substr($hash, 0, -1).'";'."\n");
break;
case "git":
$outputStr = shell_exec('nix-prefetch-git "'.$sourceObj['url'].'" '.$sourceObj["reference"]);
$output = json_decode($outputStr, true);
$hash = $output["sha256"];
fwrite($handle, ' "'.$package["name"].'" = fetchgit {'."\n");
fwrite($handle, ' name = "'.strtr($package["name"], "/", "-").'-'.$sourceObj["reference"].'";'."\n");
fwrite($handle, ' url = "'.$sourceObj["url"].'";'."\n");
fwrite($handle, ' rev = "'.$sourceObj["reference"].'";'."\n");
fwrite($handle, ' sha256 = "'.$hash.'";'."\n");
break;
default:
throw new Exception("Cannot convert dependency of type: ".$sourceObj["type"]);
}
fwrite($handle, " };\n");
}
fwrite($handle, " };\n");
fwrite($handle, "in\n");
fwrite($handle, "composerEnv.buildPackage {\n");
fwrite($handle, ' name = "'.$name.'";'."\n");
fwrite($handle, " src = ./.;\n");
fwrite($handle, " inherit dependencies;\n");
fwrite($handle, "}\n");
fclose($handle);
/* Generate composition expression */
$handle = fopen($compositionFile, "w");
if($handle === false)
throw new Exception("Cannot write to: ".$compositionFile);
function composeNixFilePath($path)
{
if((strlen($path) > 0 && substr($path, 0, 1) === "/") || (strlen($path) > 1 && substr($path, 0, 2) === "./"))
return $path;
else
return "./".$path;
}
fwrite($handle, "{ pkgs ? import <nixpkgs> { inherit system; }\n");
fwrite($handle, ", system ? builtins.currentSystem\n");
fwrite($handle, "}:\n\n");
fwrite($handle, "let\n");
fwrite($handle, " composerEnv = import ".composeNixFilePath($composerEnvFile)." {\n");
fwrite($handle, " inherit (pkgs) stdenv writeTextFile fetchurl php unzip;\n");
fwrite($handle, " };\n");
fwrite($handle, "in\n");
fwrite($handle, "import ".composeNixFilePath($outputFile)." {\n");
fwrite($handle, " inherit composerEnv;\n");
fwrite($handle, " inherit (pkgs) fetchgit;\n");
fwrite($handle, "}\n");
fclose($handle);
/* Copy composer-env.nix */
if(!$noCopyComposerEnv && !copy(dirname(__FILE__)."/composer-env.nix", $composerEnvFile))
throw new Exception("Cannot copy node-env.nix!");
}
}
?>

View file

@ -0,0 +1,124 @@
# This file originates from composer2nix
{ stdenv, writeTextFile, fetchurl, php, unzip }:
rec {
composer = stdenv.mkDerivation {
name = "composer-1.4.2";
src = fetchurl {
url = https://github.com/composer/composer/releases/download/1.4.2/composer.phar;
sha256 = "1x467ngxb976ba2r9kqba7jpvm95a0db8nwaa2z14zs7xv1la6bb";
};
buildInputs = [ php ];
# We must wrap the composer.phar because of the impure shebang.
# We cannot use patchShebangs because the executable verifies its own integrity and will detect that somebody has tampered with it.
buildCommand = ''
# Copy phar file
mkdir -p $out/share/php
cp $src $out/share/php/composer.phar
chmod 755 $out/share/php/composer.phar
# Create wrapper executable
mkdir -p $out/bin
cat > $out/bin/composer <<EOF
#! ${stdenv.shell} -e
exec ${php}/bin/php $out/share/php/composer.phar "\$@"
EOF
chmod +x $out/bin/composer
'';
meta = {
description = "Dependency Manager for PHP";
#license = stdenv.licenses.mit;
maintainers = [ stdenv.lib.maintainers.sander ];
platforms = stdenv.lib.platforms.unix;
};
};
buildZipPackage = { name, url, sha256 }:
stdenv.mkDerivation {
inherit name;
src = fetchurl {
inherit url sha256;
};
buildInputs = [ unzip ];
buildCommand = ''
unzip $src
baseDir=$(find . -type d -mindepth 1 -maxdepth 1)
cd $baseDir
mkdir -p $out
mv * $out
'';
};
buildPackage = { name, src, dependencies ? [], removeComposerArtifacts ? false }:
let
reconstructInstalled = writeTextFile {
name = "reconstructinstalled.php";
executable = true;
text = ''
#! ${php}/bin/php
<?php
$composerLockStr = file_get_contents($argv[1]);
if($composerLockStr === false)
{
print("Cannot open composer.lock contents");
exit(1);
}
else
{
$config = json_decode($composerLockStr);
$packagesStr = json_encode($config->packages, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
print($packagesStr);
}
?>
'';
};
in
stdenv.mkDerivation {
inherit name src;
buildInputs = [ php composer ];
buildCommand = ''
cp -av $src $out
chmod -R u+w $out
cd $out
# Remove unwanted files
rm -f *.nix
export HOME=$TMPDIR
# Reconstruct the installed.json file from the lock file
mkdir -p vendor/composer
${reconstructInstalled} composer.lock > vendor/composer/installed.json
# Symlink the provided dependencies
cd vendor
${stdenv.lib.concatMapStrings (dependencyName:
let
dependency = dependencies.${dependencyName};
in
''
vendorDir="$(dirname ${dependencyName})"
mkdir -p "$vendorDir"
ln -s "${dependency}" "$vendorDir/$(basename "${dependencyName}")"
'') (builtins.attrNames dependencies)}
cd ..
# Reconstruct autoload scripts
# We use the optimize feature because Nix packages cannot change after they have been built
# Using the dynamic loader for a Nix package is useless since there is nothing to dynamically reload.
composer dump-autoload --optimize
# Run the install step as a validation to confirm that everything works out as expected
composer install --optimize-autoloader
${stdenv.lib.optionalString (removeComposerArtifacts) ''
# Remove composer stuff
rm composer.json composer.lock
''}
'';
};
}