['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $directory); if (! is_resource($process)) { throw new RuntimeException('Could not start build command'); } fclose($pipes[0]); $stdout = stream_get_contents($pipes[1]); $stderr = stream_get_contents($pipes[2]); fclose($pipes[1]); fclose($pipes[2]); if (proc_close($process) !== 0) { throw new RuntimeException(trim($stderr ?: $stdout)); } return $stdout; } function copyTree(string $from, string $to): void { if (! is_dir($to) && ! mkdir($to, 0755, true) && ! is_dir($to)) { throw new RuntimeException('Could not create staging directory'); } foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($from, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST) as $file) { if ($file->isLink()) { throw new RuntimeException('Symlink in production dependencies'); } $destination = $to.'/'.substr($file->getPathname(), strlen($from) + 1); if ($file->isDir()) { if (! is_dir($destination)) { mkdir($destination, 0755, true); } } elseif (! copy($file->getPathname(), $destination)) { throw new RuntimeException('Could not stage dependency'); } } } $stage = sys_get_temp_dir().'/pharos-local-package-'.bin2hex(random_bytes(8)); $archive = null; try { mkdir($stage, 0755, true); $tracked = explode("\0", runCommand(['git', 'ls-files', '-z'], $source)); $skipRoots = ['.github', '.git', 'tests', 'node_modules', 'vendor', 'docker', 'scripts', 'brag-output', 'dist']; $skipFiles = ['Dockerfile', 'compose.yaml', 'package.json', 'package-lock.json', 'vite.config.js', 'phpunit.xml', 'phpstan.neon', 'pint.json', '.dockerignore', '.editorconfig', '.gitattributes', '.gitignore', '.npmrc']; foreach ($tracked as $relative) { if ($relative === '' || in_array(explode('/', $relative)[0], $skipRoots, true) || in_array($relative, $skipFiles, true) || (str_starts_with(basename($relative), '.env') && basename($relative) !== '.env.example') || str_starts_with($relative, 'storage/') || str_starts_with($relative, 'bootstrap/cache/') || str_contains($relative, '..') || is_link($source.'/'.$relative)) { continue; } $target = $stage.'/'.$relative; if (! is_dir(dirname($target))) { mkdir(dirname($target), 0755, true); } if (! copy($source.'/'.$relative, $target)) { throw new RuntimeException('Could not stage source file'); } } $config = $stage.'/config/pharos.php'; $stamped = preg_replace("/('version'\s*=>\s*env\('PHAROS_VERSION',\s*)'[^']*'/", '$1'."'$version'", file_get_contents($config), 1, $count); if ($count !== 1) { throw new RuntimeException('Version stamp not found'); } file_put_contents($config, $stamped); file_put_contents($stage.'/VERSION', $version."\n"); foreach (['storage/app/public', 'storage/app/backups', 'storage/logs', 'storage/framework/cache/data', 'storage/framework/sessions', 'storage/framework/views', 'bootstrap/cache'] as $directory) { mkdir($stage.'/'.$directory, 0755, true); file_put_contents($stage.'/'.$directory.'/.gitkeep', ''); } if ($vendor !== null) { $vendor = realpath($vendor); if ($vendor === false || ! is_file($vendor.'/autoload.php')) { throw new RuntimeException('Supplied production vendor is invalid'); } copyTree($vendor, $stage.'/vendor'); } else { runCommand(['composer', 'install', '--no-dev', '--no-scripts', '--prefer-dist', '--no-interaction', '--no-progress', '--optimize-autoloader'], $stage); } if (! is_dir($output) && ! mkdir($output, 0755, true) && ! is_dir($output)) { throw new RuntimeException('Could not create output directory'); } $output = realpath($output); $name = 'pharos-'.$version.'.zip'; $archive = new ZipArchive; if ($archive->open($output.'/'.$name, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { throw new RuntimeException('Could not create hosting archive'); } foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($stage, FilesystemIterator::SKIP_DOTS)) as $file) { if ($file->isFile()) { $archive->addFile($file->getPathname(), substr($file->getPathname(), strlen($stage) + 1)); } } $archive->close(); $archive = null; copy($output.'/'.$name, $output.'/pharos-latest.zip'); $sha = hash_file('sha256', $output.'/'.$name); file_put_contents($output.'/'.$name.'.sha256', $sha.' '.$name."\n"); file_put_contents($output.'/pharos-latest.zip.sha256', $sha." pharos-latest.zip\n"); $metadata = ['version' => $version, 'url' => $name, 'sha256' => $sha, 'released_at' => gmdate('c'), 'prerelease' => str_contains($version, '-')]; file_put_contents($output.'/release-info.json', json_encode($metadata, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)."\n"); $package = json_decode(file_get_contents($stage.'/composer.json'), true, flags: JSON_THROW_ON_ERROR); $package['version'] = $version; $package['dist'] = ['type' => 'zip', 'url' => 'file://'.$output.'/'.$name, 'shasum' => hash_file('sha1', $output.'/'.$name)]; file_put_contents($output.'/packages.json', json_encode(['packages' => ['solutionmax/pharos' => [$version => $package]]], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)."\n"); fwrite(STDOUT, json_encode(['version' => $version, 'archive' => $output.'/'.$name, 'sha256' => $sha], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)."\n"); } catch (Throwable $error) { fwrite(STDERR, $error->getMessage()."\n"); $failed = true; } finally { $archive?->close(); if (is_dir($stage)) { foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($stage, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST) as $file) { $file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname()); } rmdir($stage); } } exit(isset($failed) ? 1 : 0);