From c46bf0a777bebf0ad080534ac665f400ff40d0f4 Mon Sep 17 00:00:00 2001 From: Sander van der Burg Date: Tue, 11 Jul 2017 20:24:44 +0200 Subject: [PATCH] - Split generator function - Add hg dependency support - Add example case --- README.md | 93 ++++++++++- bin/{composer2nix.php => composer2nix} | 15 +- composer.json | 12 +- src/Composer2Nix/Generator.php | 223 +++++++++++++++---------- src/Composer2Nix/composer-env.nix | 2 +- 5 files changed, 244 insertions(+), 101 deletions(-) rename bin/{composer2nix.php => composer2nix} (87%) diff --git a/README.md b/README.md index 822aba8..8681c35 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,102 @@ including its dependencies: $ nix-build +An example use case scenario +============================ +We can use `composer2nix` to automate the deployment of a web application as +part of a NixOS configuration. + +For example, we can create the following trivial PHP web application +(`index.php`) that uses the [dompdf](http://dompdf.github.io/) library to +generate a PDF file from an HTML page: + +```php +loadHtml('hello world'); + +$dompdf->setPaper('A4', 'landscape'); +$dompdf->render(); + +$dompdf->stream(); +?> +``` + +We can write the following `composer.json` configuration file to configure the +`dompdf` dependency: + +```json +{ + "name": "exampleapp/exampleapp", + + "require": { + "dompdf/dompdf": "^0.8.0" + } +} +``` + +With the following commmand we can let `composer` deploy the dependencies (and +pinpoint the used versions in a `composer.lock` file): + + $ composer install + +Instead, we can also use `composer2nix`: + + $ composer2nix + +The above command generates Nix expressions that can be used to deploy the web +application and its dependencies. + +We can use Nix to build a bundle of our web application including its +dependencies: + +$ nix-build +$ ls result/ +index.php vendor/ + +(As may be observed, the `vendor/` folder contains all dependency artifacts). + +We can attach the generated package to a document root of the Apache server in +a NixOS configuration: + +```nix +{pkgs, config, ...}: + +let + myexampleapp = import /home/sander/myexampleapp { + inherit pkgs; + }; +in +{ + services.httpd = { + enable = true; + adminAddr = "admin@localhost"; + extraModules = [ + { name = "php7"; path = "${pkgs.php}/modules/libphp7.so"; } + ]; + documentRoot = myexampleapp; + }; + + ... +} +``` + +We can deploy the above NixOS configuration as follows: + + $ nixos-rebuild switch + +If the above command succeeds, we have a running system with the Apache +webserver serving our web application. + 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. +Most importantly, only the `zip`, `git`, and `hg` dependencies are supported. License ======= diff --git a/bin/composer2nix.php b/bin/composer2nix similarity index 87% rename from bin/composer2nix.php rename to bin/composer2nix index bc0d36e..a8c8f33 100755 --- a/bin/composer2nix.php +++ b/bin/composer2nix @@ -25,6 +25,7 @@ Options: packages (defaults to: composer-env.nix) --prefer-source Forces installation from package sources when possible --prefer-dist Forces installation from package dist + --no-dev Do not install the development packages --name Name of the generated package (defaults to the name provided in the composer.json file) --no-copy-composer-env @@ -51,6 +52,7 @@ $options = getopt("hv", array( "composer-env:", "prefer-source", "prefer-dist", + "no-dev", "name:", "no-copy-composer-env", "help", @@ -105,13 +107,18 @@ else $noCopyComposerEnv = array_key_exists("no-copy-composer-env", $options); -$installType = "dist"; // TODO: consult composer.json's preferred-install property. defaults to: auto +$preferredInstall = "dist"; // TODO: consult composer.json's preferred-install property. defaults to: auto if(array_key_exists("prefer-source", $options)) - $installType = "source"; + $preferredInstall = "source"; if(array_key_exists("prefer-dist", $options)) - $installType = "dist"; + $preferredInstall = "dist"; + +if(array_key_exists("no-dev", $options)) + $noDev = true; +else + $noDev = false; if(array_key_exists("name", $options)) $name = $options["name"]; @@ -122,7 +129,7 @@ else try { - Generator::generateNixExpressions($name, $installType, $configFile, $lockFile, $outputFile, $compositionFile, $composerEnvFile, $noCopyComposerEnv); + Generator::generateNixExpressions($name, $preferredInstall, $noDev, $configFile, $lockFile, $outputFile, $compositionFile, $composerEnvFile, $noCopyComposerEnv); } catch(Exception $ex) { diff --git a/composer.json b/composer.json index 185ba53..7073f11 100644 --- a/composer.json +++ b/composer.json @@ -4,15 +4,15 @@ "type": "library", "license": "MIT", "authors": [ - { - "name": "Sander van der Burg", - "email": "svanderburg@gmail.com", - "homepage": "http://sandervanderburg.nl" - } + { + "name": "Sander van der Burg", + "email": "svanderburg@gmail.com", + "homepage": "http://sandervanderburg.nl" + } ], "autoload": { - "psr-4": { "Composer2Nix\\": "src/Composer2Nix" } + "psr-4": { "Composer2Nix\\": "src/Composer2Nix" } }, "bin": [ "bin/composer2nix" ] diff --git a/src/Composer2Nix/Generator.php b/src/Composer2Nix/Generator.php index e6377d9..b97d9a1 100644 --- a/src/Composer2Nix/Generator.php +++ b/src/Composer2Nix/Generator.php @@ -4,7 +4,125 @@ use Exception; class Generator { - function generateNixExpressions($name, $installType, $configFile, $lockFile, $outputFile, $compositionFile, $composerEnvFile, $noCopyComposerEnv) + private static function composeNixFilePath($path) + { + if((strlen($path) > 0 && substr($path, 0, 1) === "/") || (strlen($path) > 1 && substr($path, 0, 2) === "./")) + return $path; + else + return "./".$path; + } + + private static function selectSourceObject($preferredInstall, $package) + { + if($preferredInstall === "source") + { + // Use the source unless no source has been provided + if(array_key_exists("source", $package)) + return $package["source"]; + else if(array_key_exists("dist", $package)) + return $package["dist"]; + else + throw new Exception("Encountered a dangling package reference"); + } + else + { + if(array_key_exists("dist", $package)) + return $package["dist"]; + else + throw new Exception("Encountered a dangling package reference"); + } + } + + private static function generatePackagesExpression($outputFile, $name, $preferredInstall, array $packages) + { + $handle = fopen($outputFile, "w"); + + if($handle === false) + throw new Exception("Cannot write to: ".$outputFile); + + fwrite($handle, "{composerEnv, fetchgit ? null}:\n\n"); + fwrite($handle, "let\n"); + fwrite($handle, " dependencies = {\n"); + + foreach($packages as $package) + { + $sourceObj = Generator::selectSourceObject($preferredInstall, $package); + + 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; + case "hg": + $outputStr = shell_exec('nix-prefetch-hg "'.$sourceObj['url'].'" '.$sourceObj["reference"]); + + $output = json_decode($outputStr, true); + $hash = $output["sha256"]; + + fwrite($handle, ' "'.$package["name"].'" = fetchhg {'."\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"); + 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); + } + + private static function generateCompositionExpression($compositionFile, $outputFile, $composerEnvFile) + { + $handle = fopen($compositionFile, "w"); + + if($handle === false) + throw new Exception("Cannot write to: ".$compositionFile); + + fwrite($handle, "{ pkgs ? import { inherit system; }\n"); + fwrite($handle, ", system ? builtins.currentSystem\n"); + fwrite($handle, "}:\n\n"); + + fwrite($handle, "let\n"); + fwrite($handle, " composerEnv = import ".Generator::composeNixFilePath($composerEnvFile)." {\n"); + fwrite($handle, " inherit (pkgs) stdenv writeTextFile fetchurl php unzip;\n"); + fwrite($handle, " };\n"); + fwrite($handle, "in\n"); + fwrite($handle, "import ".Generator::composeNixFilePath($outputFile)." {\n"); + fwrite($handle, " inherit composerEnv;\n"); + fwrite($handle, " inherit (pkgs) fetchgit;\n"); + fwrite($handle, "}\n"); + + fclose($handle); + } + + public static function generateNixExpressions($name, $preferredInstall, $noDev, $configFile, $lockFile, $outputFile, $compositionFile, $composerEnvFile, $noCopyComposerEnv) { /* Open the composer.json file and decode it */ $composerJSONStr = file_get_contents($configFile); @@ -38,103 +156,30 @@ class Generator throw new Exception("Cannot open contents of: ".$lockFile); $lockConfig = json_decode($composerLockStr, true); + + if(array_key_exists("packages", $lockConfig)) + $packages = $lockConfig["packages"]; + else + $packages = array(); + + if(!$noDev && array_key_exists("packages-dev", $lockConfig)) + { + foreach($lockConfig["packages-dev"] as $identifier => $devPackage) + $packages[$identifier] = $devPackage; + } } else - $lockConfig = null; + $packages = array(); /* 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); + Generator::generatePackagesExpression($outputFile, $name, $preferredInstall, $packages); /* 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 { 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); + Generator::generateCompositionExpression($compositionFile, $outputFile, $composerEnvFile); /* Copy composer-env.nix */ - if(!$noCopyComposerEnv && !copy(dirname(__FILE__)."/composer-env.nix", $composerEnvFile)) - throw new Exception("Cannot copy node-env.nix!"); + throw new Exception("Cannot copy composer-env.nix!"); } } ?> diff --git a/src/Composer2Nix/composer-env.nix b/src/Composer2Nix/composer-env.nix index fc6bda1..f223c25 100644 --- a/src/Composer2Nix/composer-env.nix +++ b/src/Composer2Nix/composer-env.nix @@ -64,7 +64,7 @@ rec { if($composerLockStr === false) { - print("Cannot open composer.lock contents"); + fwrite(STDERR, "Cannot open composer.lock contents\n"); exit(1); } else