├── .gitignore ├── .editorconfig ├── res ├── bin-wrapper.sh.php ├── default.nix.php └── composer-project.nix.php ├── src ├── CommandProvider.php ├── NixifyCommand.php ├── NixUtils.php ├── Plugin.php ├── InstallBinCommand.php └── NixGenerator.php ├── composer.json ├── LICENSE.md ├── .github └── workflows │ └── build.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.php] 2 | indent_size = 4 3 | -------------------------------------------------------------------------------- /res/bin-wrapper.sh.php: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec "$@" 3 | -------------------------------------------------------------------------------- /res/default.nix.php: -------------------------------------------------------------------------------- 1 | # This is a minimal `default.nix` by composer-plugin-nixify. You can customize 2 | # it as needed, it will not be overwritten by the plugin. 3 | 4 | { pkgs ? import { } }: 5 | 6 | pkgs.callPackage ./composer-project.nix { } ./. 7 | -------------------------------------------------------------------------------- /src/CommandProvider.php: -------------------------------------------------------------------------------- 1 | setName('nixify') 19 | ->setDescription('Manually generate the Nix expressions for this Composer project.') 20 | ; 21 | } 22 | 23 | protected function execute(InputInterface $input, OutputInterface $output) 24 | { 25 | $generator = new NixGenerator($this->getComposer(), $this->getIO()); 26 | $generator->collect(); 27 | $generator->generate(); 28 | if ($generator->shouldPreload) { 29 | $generator->preload(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, composer-plugin-nixify contributors 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | php: ['7.3', '7.4', '8.0', '8.1'] 12 | composer: ['v1', 'v2'] 13 | steps: 14 | 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | with: 18 | path: composer-plugin-nixify 19 | 20 | - name: Setup PHP 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: "${{ matrix.php }}" 24 | tools: "composer:${{ matrix.composer }}" 25 | 26 | - name: Create test project 27 | run: | 28 | cat > composer.json << 'EOF' 29 | { 30 | "minimum-stability": "dev", 31 | "repositories": [ 32 | { 33 | "type": "path", 34 | "url": "./composer-plugin-nixify" 35 | } 36 | ], 37 | "require": { 38 | "psr/log": "1.1.3", 39 | "stephank/composer-plugin-nixify": "*" 40 | } 41 | } 42 | EOF 43 | 44 | - name: Test without Nix 45 | run: composer upgrade 46 | 47 | - name: Install Nix 48 | uses: nixbuild/nix-quick-install-action@v4 49 | 50 | - name: Setup Cachix 51 | if: github.event_name == 'push' && github.repository_owner == 'stephank' 52 | uses: cachix/cachix-action@v8 53 | with: 54 | name: stephank 55 | signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' 56 | 57 | - name: Test with Nix 58 | run: composer upgrade 59 | 60 | - name: Test refetch 61 | run: | 62 | composer clearcache 63 | composer upgrade 64 | 65 | - name: Test nix-build 66 | run: | 67 | rm -fr vendor 68 | nix-build 69 | env: 70 | NIX_PATH: nixpkgs=channel:nixpkgs-unstable 71 | -------------------------------------------------------------------------------- /src/NixUtils.php: -------------------------------------------------------------------------------- 1 | $byte) { 16 | $result[$idx % $size] ^= ord($byte); 17 | } 18 | 19 | return implode('', array_map('chr', $result)); 20 | } 21 | 22 | /** 23 | * Nix-compatible base32 encoding. 24 | * 25 | * This is probably a super inefficient implementation, but we only process 26 | * small inputs. (20 bytes) 27 | */ 28 | public static function encodeBase32(string $bin): string 29 | { 30 | $bits = ''; 31 | foreach (array_reverse(str_split($bin)) as $byte) { 32 | $bits .= str_pad(decbin(ord($byte)), 8, '0', STR_PAD_LEFT); 33 | } 34 | 35 | $result = ''; 36 | while ($bits) { 37 | $result .= self::CHARSET[bindec(substr($bits, 0, 5))]; 38 | $bits = substr($bits, 5); 39 | } 40 | 41 | return $result; 42 | } 43 | 44 | /** 45 | * Compute the Nix store path for a fixed-output derivation. 46 | */ 47 | public static function computeFixedOutputStorePath( 48 | string $name, 49 | string $hashAlgorithm, 50 | string $hashHex, 51 | string $storePath = '/nix/store' 52 | ) { 53 | $innerStr = "fixed:out:$hashAlgorithm:$hashHex:"; 54 | $innerHashHex = hash('sha256', $innerStr); 55 | 56 | $outerStr = "output:out:sha256:$innerHashHex:$storePath:$name"; 57 | $outerHash = hash('sha256', $outerStr, true); 58 | $outerHash32 = self::encodeBase32(self::compressHash($outerHash, 20)); 59 | 60 | return "$storePath/$outerHash32-$name"; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | getComposer(), $event->getIO()); 17 | if ($generator->shouldPreload) { 18 | $generator->collect(); 19 | $generator->preload(); 20 | } 21 | } 22 | 23 | public function postUpdate(Event $event) 24 | { 25 | $generator = new NixGenerator($event->getComposer(), $event->getIO()); 26 | $generator->collect(); 27 | $generator->generate(); 28 | if ($generator->shouldPreload) { 29 | $generator->preload(); 30 | } 31 | } 32 | 33 | // PluginInterface 34 | 35 | public function activate(Composer $composer, IOInterface $io) 36 | { 37 | } 38 | 39 | public function deactivate(Composer $composer, IOInterface $io) 40 | { 41 | } 42 | 43 | public function uninstall(Composer $composer, IOInterface $io) 44 | { 45 | if (file_exists('composer-project.nix') || file_exists('default.nix')) { 46 | $io->writeError( 47 | 'You may also want to delete the generated "*.nix" files.' 48 | ); 49 | } 50 | } 51 | 52 | // Capable 53 | 54 | public function getCapabilities() 55 | { 56 | return [ 57 | 'Composer\Plugin\Capability\CommandProvider' => 'Nixify\CommandProvider', 58 | ]; 59 | } 60 | 61 | // EventSubscriberInterface 62 | 63 | public static function getSubscribedEvents() 64 | { 65 | return [ 66 | 'post-install-cmd' => 'postInstall', 67 | 'post-update-cmd' => 'postUpdate', 68 | ]; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/InstallBinCommand.php: -------------------------------------------------------------------------------- 1 | setName('nixify-install-bin') 19 | ->setDescription('Internal Nixify plugin command to create executable wrappers') 20 | ->addArgument('bin-dir', InputArgument::REQUIRED, 'Directory to create wrappers in') 21 | ; 22 | } 23 | 24 | protected function execute(InputInterface $input, OutputInterface $output) 25 | { 26 | $binDir = $input->getArgument('bin-dir'); 27 | $io = $this->getIO(); 28 | $fs = new Filesystem(); 29 | 30 | $scriptFiles = $this->getComposer()->getPackage()->getBinaries(); 31 | foreach ($scriptFiles as $scriptFile) { 32 | $scriptPath = realpath($scriptFile); 33 | if (!$scriptPath) { 34 | $io->writeError(sprintf( 35 | 'Skipped binary "%s" because the file does not exist', 36 | $scriptFile 37 | )); 38 | continue; 39 | } 40 | 41 | $caller = BinaryInstaller::determineBinaryCaller($scriptPath); 42 | $scriptPath = ProcessExecutor::escape($scriptPath); 43 | 44 | // In Nix, the PHP executable is a small shell wrapper. Using this 45 | // as a shebang fails at least on macOS. Detect a PHP shebang, and 46 | // make sure PHP is properly invoked in our binary wrapper. 47 | if ($caller === 'php') { 48 | $exeFinder = new ExecutableFinder; 49 | $interpPath = $exeFinder->find($caller); 50 | if ($interpPath) { 51 | $interpPath = ProcessExecutor::escape($interpPath); 52 | $scriptPath = "$interpPath $scriptPath"; 53 | } 54 | } 55 | 56 | $outputPath = sprintf('%s/%s', $binDir, basename($scriptFile)); 57 | $fs->ensureDirectoryExists(dirname($outputPath)); 58 | 59 | ob_start(); 60 | require __DIR__ . '/../res/bin-wrapper.sh.php'; 61 | file_put_contents($outputPath, ob_get_clean()); 62 | 63 | chmod($outputPath, 0755); 64 | } 65 | 66 | return 0; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /res/composer-project.nix.php: -------------------------------------------------------------------------------- 1 | # This file is generated by composer-plugin-nixify. 2 | # Manual changes might be lost - proceed with caution! 3 | 4 | { lib, php, phpPackages, unzip, stdenv, runCommandLocal, writeText, fetchurl 5 | 6 | # Default fetcher. 7 | , fetcher ? (args: fetchurl { inherit (args) name urls sha256; }) 8 | 9 | }: src: 10 | 11 | with lib; 12 | 13 | let 14 | 15 | composerPath = ; 16 | cacheEntries = ; 17 | localPackages = ; 18 | 19 | # Shell snippet to collect all project dependencies. 20 | collectCacheScript = writeText "collect-cache.sh" ( 21 | concatMapStrings (args: '' 22 | ( 23 | cacheFile=${escapeShellArg args.filename} 24 | cacheFilePath="$COMPOSER_CACHE_DIR/files/$cacheFile" 25 | mkdir -p "$(dirname "$cacheFilePath")" 26 | cp ${escapeShellArg (fetcher args)} "$cacheFilePath" 27 | ) 28 | '') cacheEntries 29 | ); 30 | 31 | replaceLocalPaths = writeText "replace-local-paths.sh" ( 32 | concatMapStrings (args: '' 33 | sed -i -e "s|\"${args.string}\"|\"${args.path}\"|" composer.lock 34 | '') localPackages 35 | ); 36 | 37 | in stdenv.mkDerivation { 38 | name = ; 39 | inherit src; 40 | 41 | # Make sure the build uses the right PHP version everywhere. 42 | # Also include unzip for Composer. 43 | buildInputs = [ php unzip ]; 44 | 45 | # Defines the shell alias to run Composer. 46 | postHook = '' 47 | composer () { 48 | php "$NIX_COMPOSER_PATH" "$@" 49 | } 50 | ''; 51 | 52 | configurePhase = '' 53 | runHook preConfigure 54 | 55 | # Set the cache directory for Composer. 56 | export COMPOSER_CACHE_DIR="$NIX_BUILD_TOP/.composer/cache" 57 | 58 | # Build the cache directory contents. 59 | source ${collectCacheScript} 60 | 61 | # Replace local package paths with their Nix store equivalent. 62 | source ${replaceLocalPaths} 63 | 64 | # Store the absolute path to Composer for the 'composer' alias. 65 | export NIX_COMPOSER_PATH="$(readlink -f ${escapeShellArg composerPath})" 66 | 67 | # Run normal Composer install to complete dependency installation. 68 | composer install 69 | 70 | runHook postConfigure 71 | ''; 72 | 73 | buildPhase = '' 74 | runHook preBuild 75 | runHook postBuild 76 | ''; 77 | 78 | installPhase = '' 79 | runHook preInstall 80 | 81 | mkdir -p $out/libexec $out/bin 82 | 83 | # Move the entire project to the output directory. 84 | mv $PWD "$out/libexec/$sourceRoot" 85 | cd "$out/libexec/$sourceRoot" 86 | 87 | # Update the path to Composer. 88 | export NIX_COMPOSER_PATH="$(readlink -f ${escapeShellArg composerPath})" 89 | 90 | # Invoke a plugin internal command to setup binaries. 91 | composer nixify-install-bin "$out/bin" 92 | 93 | runHook postInstall 94 | ''; 95 | 96 | passthru = { 97 | inherit php; 98 | }; 99 | } 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # composer-plugin-nixify 2 | 3 | Generates a [Nix] expression to build a [Composer] project. 4 | 5 | - A default configure-phase that runs `composer install` in your project. (May 6 | be all that's needed to build your project.) 7 | 8 | - A default install-phase that creates executables for you based on `"bin"` in 9 | your `composer.json`, making your package readily installable. 10 | 11 | - Granular fetching of dependencies in Nix, speeding up rebuilds and 12 | potentially allowing downloads to be shared between projects. 13 | 14 | - Preloading of your Composer cache into the Nix store, speeding up local 15 | `nix-build`. 16 | 17 | - Automatically keeps your Nix expression up-to-date as you `composer require` 18 | / `composer update` dependencies. 19 | 20 | - **No Nix installation required** for the plugin itself, so it should be safe 21 | to add to your project even if some developers don't use Nix. 22 | 23 | [nix]: https://nixos.org 24 | [composer]: https://getcomposer.org 25 | 26 | Related projects: 27 | 28 | - [composer2nix]: Does a similar job, but as a separate command. By comparison, 29 | this plugin tries to automate the process and make things easy for Nix and 30 | non-Nix devs alike. 31 | 32 | - [yarn-plugin-nixify]: Similar solution for Node.js with Yarn v2. 33 | 34 | [composer2nix]: https://github.com/svanderburg/composer2nix 35 | [yarn-plugin-nixify]: https://github.com/stephank/yarn-plugin-nixify 36 | 37 | ## Usage 38 | 39 | The Nixify plugin should work fine with Composer versions all the way back to 40 | 1.3.0, and also supports Composer 2.0. 41 | 42 | To use the plugin: 43 | 44 | ```sh 45 | # Install the plugin 46 | composer require stephank/composer-plugin-nixify 47 | 48 | # Build your project with Nix 49 | nix-build 50 | ``` 51 | 52 | Running Composer with this plugin enabled will generate two files: 53 | 54 | - `composer-project.nix`: This file is always overwritten, and contains a basic 55 | derivation for your project. 56 | 57 | - `default.nix`: Only generated if it does not exist yet. This file is intended 58 | to be customized with any project-specific logic you need. 59 | 60 | This may already build successfully! But if your project needs extra build 61 | steps, you may have to customize `default.nix` a bit. Some examples of what's 62 | possible: 63 | 64 | ```nix 65 | { pkgs ? import { } }: 66 | 67 | let 68 | 69 | # Example of providing a different source tree. 70 | src = pkgs.lib.cleanSource ./.; 71 | 72 | project = pkgs.callPackage ./composer-project.nix { 73 | 74 | # Example of selecting a specific version of PHP. 75 | php = pkgs.php74; 76 | 77 | } src; 78 | 79 | in project.overrideAttrs (oldAttrs: { 80 | 81 | # Example of overriding the default package name taken from composer.json. 82 | name = "myproject"; 83 | 84 | # Example of adding packages to the build environment. 85 | buildInputs = oldAttrs.buildInputs ++ [ pkgs.imagemagick ]; 86 | 87 | # Example of invoking a build step in your project. 88 | buildPhase = '' 89 | composer run lint 90 | ''; 91 | 92 | }) 93 | ``` 94 | 95 | ## Settings 96 | 97 | Some additional settings are available which can be set in the `"extra"` object 98 | in your `composer.json`: 99 | 100 | - `nix-expr-path` can be set to customize the path where the Nixify plugin 101 | writes `composer-project.nix`. For example, if you're also using [Niv] in 102 | your project, you may prefer to set this to `nix/composer-project.nix`. 103 | 104 | - `generate-default-nix` can be set to `false` to disable generating a 105 | `default.nix`. This file is only generated if it doesn't exist yet, but this 106 | flag can be useful if you don't want a `default.nix` at all. 107 | 108 | - `enable-nix-preload` can be set to `false` to disable preloading Composer 109 | cache into the Nix store. This preloading is intended to speed up a local 110 | `nix-build`, because Nix will not have to download dependencies again. 111 | Preloading does mean another copy of dependencies on disk, even if you don't 112 | do local Nix builds, but the size is usually not an issue on modern disks. 113 | 114 | [niv]: https://github.com/nmattia/niv 115 | -------------------------------------------------------------------------------- /src/NixGenerator.php: -------------------------------------------------------------------------------- 1 | composer = $composer; 26 | $this->io = $io; 27 | $this->config = $composer->getConfig(); 28 | $this->fs = new Filesystem(); 29 | 30 | // From: `Cache::__construct()` 31 | $this->cacheDir = rtrim($this->config->get('cache-files-dir'), '/\\') . '/'; 32 | 33 | $this->collected = []; 34 | 35 | $this->shouldPreload = $composer->getPackage()->getExtra()['enable-nix-preload'] ?? true; 36 | if ($this->shouldPreload) { 37 | $exeFinder = new ExecutableFinder; 38 | $this->shouldPreload = (bool) $exeFinder->find('nix-store'); 39 | } 40 | } 41 | 42 | public function collect(): void 43 | { 44 | // Collect lockfile packages we know how to create Nix fetch 45 | // derivations for. 46 | $this->collected = []; 47 | $numToFetch = 0; 48 | 49 | foreach ($this->iterLockedPackages() as $package) { 50 | switch ($package->getDistType()) { 51 | case 'tar': 52 | case 'xz': 53 | case 'zip': 54 | case 'gzip': 55 | case 'phar': 56 | case 'rar': 57 | $type = 'cache'; 58 | $urls = $package->getDistUrls(); 59 | 60 | // Cache is keyed by URL. Use the first URL to derive a 61 | // cache key, because Composer also tries URLs in order. 62 | $cacheUrl = $urls[0]; 63 | 64 | // From: `FileDownloader::getCacheKey()` 65 | if ($package->getDistReference()) { 66 | $cacheUrl = UrlUtil::updateDistReference($this->config, $cacheUrl, $package->getDistReference()); 67 | } 68 | $cacheKey = sprintf('%s/%s.%s', $package->getName(), sha1($cacheUrl), $package->getDistType()); 69 | 70 | // From: `Cache::read()` 71 | $cacheFile = preg_replace('{[^a-z0-9_./]}i', '-', $cacheKey); 72 | 73 | // Collect package info. 74 | $name = self::safeNixStoreName($package->getUniqueName()); 75 | $sha256 = @hash_file('sha256', $this->cacheDir . $cacheFile); 76 | $this->collected[] = compact('package', 'type', 'name', 'urls', 'cacheFile', 'sha256'); 77 | 78 | if ($sha256 === false) { 79 | $numToFetch += 1; 80 | } 81 | 82 | break; 83 | 84 | case 'path': 85 | $type = 'local'; 86 | $name = self::safeNixStoreName($package->getName()); 87 | $path = $package->getDistUrl(); 88 | 89 | $this->collected[] = compact('package', 'type', 'name', 'path'); 90 | 91 | break; 92 | 93 | default: 94 | $this->io->warning(sprintf( 95 | "Package '%s' has dist-type '%s' which is not support by the Nixify plugin", 96 | $package->getPrettyName(), 97 | $package->getDistType() 98 | )); 99 | break; 100 | } 101 | } 102 | 103 | // If some packages were previously installed but since removed from 104 | // cache, `sha256` will be false for those packages in `collected`. 105 | // Here, we amend cache by refetching, so we can then determine the 106 | // file hash again. 107 | if ($numToFetch !== 0) { 108 | $this->io->writeError(sprintf( 109 | 'Nixify could not find cache for %d package(s), which will be refetched', 110 | $numToFetch 111 | )); 112 | 113 | $downloader = $this->composer->getDownloadManager()->getDownloader('file'); 114 | $reflectDownload = new \ReflectionMethod(get_class($downloader), 'download'); 115 | $isComposer2 = $reflectDownload->getNumberOfParameters() === 4; 116 | 117 | $tempDir = $this->cacheDir . '.nixify-tmp-' . substr(md5(uniqid('', true)), 0, 8); 118 | $this->fs->ensureDirectoryExists($tempDir); 119 | try { 120 | foreach ($this->collected as &$info) { 121 | if ($info['type'] !== 'cache' || $info['sha256'] !== false) { 122 | continue; 123 | } 124 | 125 | $package = $info['package']; 126 | 127 | $this->io->writeError(sprintf( 128 | ' - Fetching %s (%s): ', 129 | $package->getName(), 130 | $package->getFullPrettyVersion() 131 | ), false); 132 | 133 | $tempFile = ''; 134 | if ($isComposer2) { 135 | $promise = $downloader->download($package, $tempDir, null, false) 136 | ->then(function ($filename) use (&$tempFile) { 137 | $tempFile = $filename; 138 | }); 139 | $this->composer->getLoop()->wait([$promise]); 140 | $this->io->writeError('OK'); 141 | } else { 142 | $tempFile = $downloader->download($package, $tempDir, false); 143 | $this->io->writeError(''); 144 | } 145 | 146 | 147 | $cachePath = $this->cacheDir . $info['cacheFile']; 148 | $this->fs->ensureDirectoryExists(dirname($cachePath)); 149 | $this->fs->rename($tempFile, $cachePath); 150 | 151 | $info['sha256'] = hash_file('sha256', $cachePath); 152 | } 153 | } finally { 154 | $this->fs->removeDirectory($tempDir); 155 | } 156 | } 157 | } 158 | 159 | /** 160 | * Generates Nix files based on the lockfile and cache. 161 | */ 162 | public function generate(): void 163 | { 164 | // Build Nix code for cache entries. 165 | $cacheEntries = "[\n"; 166 | foreach ($this->collected as $info) { 167 | if ($info['type'] !== 'cache') { 168 | continue; 169 | } 170 | 171 | $cacheEntries .= sprintf( 172 | " { name = %s; filename = %s; sha256 = %s; urls = %s; }\n", 173 | self::nixString($info['name']), 174 | self::nixString($info['cacheFile']), 175 | self::nixString($info['sha256']), 176 | self::nixStringArray($info['urls']) 177 | ); 178 | } 179 | $cacheEntries .= ' ]'; 180 | 181 | $localPackages = "[\n"; 182 | foreach ($this->collected as $info) { 183 | if ($info['type'] !== 'local') { 184 | continue; 185 | } 186 | 187 | $localPackages .= sprintf( 188 | " { path = %s; string = %s; }\n", 189 | $info['path'], 190 | self::nixString($info['path']) 191 | ); 192 | } 193 | $localPackages .= ' ]'; 194 | 195 | 196 | // If the user bundled Composer, use that in the Nix build as well. 197 | $cwd = getcwd() . '/'; 198 | $composerPath = realpath($_SERVER['PHP_SELF']); 199 | if (substr($composerPath, 0, strlen($cwd)) === $cwd) { 200 | $composerPath = self::nixString(substr($composerPath, strlen($cwd))); 201 | } else { 202 | // Otherwise, use Composer from Nixpkgs. Don't reference the wrapper, 203 | // which depends on a specific PHP version, but the src phar instead. 204 | $composerPath = 'phpPackages.composer.src'; 205 | } 206 | 207 | // Use the root package name as default derivation name. 208 | $package = $this->composer->getPackage(); 209 | $projectName = self::nixString(self::safeNixStoreName($package->getName())); 210 | 211 | // Generate composer-project.nix. 212 | $projectFile = $package->getExtra()['nix-expr-path'] ?? 'composer-project.nix'; 213 | ob_start(); 214 | require __DIR__ . '/../res/composer-project.nix.php'; 215 | file_put_contents($projectFile, ob_get_clean()); 216 | 217 | // Generate default.nix if it does not exist yet. 218 | $generateDefaultNix = $package->getExtra()['generate-default-nix'] ?? true; 219 | if ($generateDefaultNix && !file_exists('default.nix') && !file_exists('flake.nix')) { 220 | ob_start(); 221 | require __DIR__ . '/../res/default.nix.php'; 222 | file_put_contents('default.nix', ob_get_clean()); 223 | $this->io->writeError( 224 | 'A minimal default.nix was created. You may want to customize it.' 225 | ); 226 | } 227 | } 228 | 229 | /** 230 | * Preload packages into the Nix store. 231 | */ 232 | public function preload(): void 233 | { 234 | $tempDir = $this->cacheDir . '.nixify-tmp-' . substr(md5(uniqid('', true)), 0, 8); 235 | $this->fs->ensureDirectoryExists($tempDir); 236 | try { 237 | $toPreload = []; 238 | foreach ($this->collected as $info) { 239 | if ($info['type'] !== 'cache') { 240 | continue; 241 | } 242 | $storePath = NixUtils::computeFixedOutputStorePath($info['name'], 'sha256', $info['sha256']); 243 | if (!file_exists($storePath)) { 244 | // The nix-store command requires a correct filename on disk, so we 245 | // prepare a temporary directory containing all the files to preload. 246 | $src = $this->cacheDir . $info['cacheFile']; 247 | $dst = sprintf('%s/%s', $tempDir, $info['name']); 248 | if (!copy($src, $dst)) { 249 | $this->io->writeError( 250 | 'Preloading into Nix store failed: could not write to temporary directory.' 251 | ); 252 | break; 253 | } 254 | 255 | $toPreload[] = $dst; 256 | } 257 | } 258 | 259 | if (!empty($toPreload)) { 260 | // Preload in batches, to keep the exec arguments reasonable. 261 | $process = new ProcessExecutor($this->io); 262 | $numPreloaded = 0; 263 | foreach (array_chunk($toPreload, 100) as $chunk) { 264 | $command = "nix-store --add-fixed sha256 " 265 | . implode(' ', array_map(['Composer\\Util\\ProcessExecutor', 'escape'], $chunk)); 266 | if ($process->execute($command, $output) !== 0) { 267 | $this->io->writeError('Preloading into Nix store failed.'); 268 | $this->io->writeError($output); 269 | break; 270 | } 271 | $numPreloaded += count($chunk); 272 | } 273 | 274 | $this->io->writeError(sprintf( 275 | 'Preloaded %d packages into the Nix store.', 276 | $numPreloaded 277 | )); 278 | } 279 | } finally { 280 | $this->fs->removeDirectory($tempDir); 281 | } 282 | } 283 | 284 | /** 285 | * Generator function that iterates lockfile packages. 286 | */ 287 | private function iterLockedPackages() 288 | { 289 | $locker = $this->composer->getLocker(); 290 | if ($locker->isLocked() === false) { 291 | return; 292 | } 293 | 294 | $data = $locker->getLockData(); 295 | $loader = new ArrayLoader(null, true); 296 | foreach ($data['packages'] ?? [] as $info) { 297 | yield $loader->load($info); 298 | } 299 | foreach ($data['packages-dev'] ?? [] as $info) { 300 | yield $loader->load($info); 301 | } 302 | } 303 | 304 | /** 305 | * Sanitizes a string so it's safe to use as a Nix store name. 306 | */ 307 | private static function safeNixStoreName(string $value): string 308 | { 309 | return preg_replace('/[^a-z0-9._-]/i', '_', $value); 310 | } 311 | 312 | /** 313 | * Naive conversion of a string to a Nix literal 314 | */ 315 | private static function nixString(string $value): string 316 | { 317 | return json_encode($value, JSON_UNESCAPED_SLASHES); 318 | } 319 | 320 | /** 321 | * Naive conversion of a string array to a Nix literal 322 | */ 323 | private static function nixStringArray(array $value): string 324 | { 325 | $strings = array_map([self::class, 'nixString'], $value); 326 | return '[ ' . implode(' ', $strings) . ' ]'; 327 | } 328 | } 329 | --------------------------------------------------------------------------------