Skip to content

Commit 78bb006

Browse files
committed
[ADD] Install from Packagist.
1 parent 9dde134 commit 78bb006

3 files changed

Lines changed: 292 additions & 234 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
CLI tool for creating new Evolution CMS projects.
44

5+
[![Latest Stable Version](https://img.shields.io/packagist/v/evolution-cms/installer?label=version)](https://packagist.org/packages/evolution-cms/installer)
6+
[![CMS Evolution](https://img.shields.io/badge/CMS-Evolution-brightgreen.svg)](https://github.com/evolution-cms/evolution)
7+
![PHP version](https://img.shields.io/packagist/php-v/evolution-cms/installer)
8+
[![License](https://img.shields.io/packagist/l/evolution-cms/installer)](https://packagist.org/packages/evolution-cms/installer)
9+
[![Issues](https://img.shields.io/github/issues/evolution-cms/installer)](https://github.com/evolution-cms/installer/issues)
10+
[![Stars](https://img.shields.io/packagist/stars/evolution-cms/installer)](https://packagist.org/packages/evolution-cms/installer)
11+
[![Total Downloads](https://img.shields.io/packagist/dt/evolution-cms/installer)](https://packagist.org/packages/evolution-cms/installer)
12+
513
## Requirements
614

715
- PHP 8.3+

src/Commands/InstallCommand.php

Lines changed: 136 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use EvolutionCMS\Installer\Utilities\VersionResolver;
1111
use EvolutionCMS\Installer\Validators\PhpValidator;
1212
use GuzzleHttp\Client;
13-
use GuzzleHttp\TransferStats;
1413
use PDOException;
1514
use Symfony\Component\Console\Command\Command;
1615
use Symfony\Component\Console\Input\InputArgument;
@@ -753,15 +752,35 @@ protected function downloadEvolutionCMS(string $name, array $options, ?string $b
753752
$isCurrentDir = !empty($options['install_in_current_dir']);
754753
$targetPath = $isCurrentDir ? $name : (getcwd() . '/' . $name);
755754

756-
// If branch is specified, download from branch
757-
if ($branch !== null) {
755+
$sourceLabel = null;
756+
$composerConstraint = null;
757+
$composerRepository = null;
758+
$preferSource = false;
759+
760+
if ($branch !== null && trim($branch) !== '') {
758761
$branch = trim($branch);
759-
$this->tui->addLog("Downloading Evolution CMS from branch: {$branch}...");
762+
$this->tui->addLog("Preparing Evolution CMS from branch: {$branch}...");
763+
764+
// Packagist for evolution-cms/evolution currently publishes only tagged releases (no dev branches).
765+
// For explicit branches, use the upstream Git repository through Composer.
766+
$composerRepository = [
767+
// Use explicit "git" to avoid GitHub API auth/rate-limit issues from the GitHub VCS driver.
768+
'type' => 'git',
769+
'url' => 'https://github.com/evolution-cms/evolution.git',
770+
];
760771

761-
$downloadUrl = $versionResolver->getBranchDownloadUrl($branch);
762-
$displayName = $branch;
772+
$preferSource = true;
773+
774+
// Accept branch aliases like `3.5.x-dev`, otherwise treat as a branch name (`dev-branchName`).
775+
if (str_starts_with($branch, 'dev-') || str_ends_with($branch, '-dev')) {
776+
$composerConstraint = $branch;
777+
} elseif (preg_match('/^\\d+\\.\\d+\\.x$/', $branch) === 1) {
778+
$composerConstraint = $branch . '-dev';
779+
} else {
780+
$composerConstraint = 'dev-' . $branch;
781+
}
782+
$sourceLabel = "branch {$branch}";
763783
} else {
764-
// Get latest compatible version
765784
$this->tui->addLog('Finding compatible Evolution CMS version...');
766785
$phpVersion = PHP_VERSION;
767786
$version = $versionResolver->getLatestCompatibleVersion($phpVersion, true);
@@ -771,184 +790,175 @@ protected function downloadEvolutionCMS(string $name, array $options, ?string $b
771790
throw new \RuntimeException("Could not find a compatible Evolution CMS version for PHP {$phpVersion}.");
772791
}
773792

774-
// Remove 'v' prefix if present
775793
$versionTag = ltrim($version, 'v');
776794
$this->tui->replaceLastLogs('<fg=green>✔</> Found compatible version: ' . $version . '.', 2);
777-
$this->tui->addLog("Downloading Evolution CMS {$versionTag}...");
795+
$this->tui->addLog("Downloading Evolution CMS {$versionTag} via Composer...");
778796

779-
$downloadUrl = $versionResolver->getDownloadUrl($version);
780-
$displayName = $versionTag;
797+
// Use the normalized version without `v` prefix (Packagist versions are `X.Y.Z`).
798+
$composerConstraint = $versionTag;
799+
$sourceLabel = "version {$versionTag}";
781800
}
782801

783-
// Create temp file for download
784-
$tempFile = sys_get_temp_dir() . '/evo-installer-' . uniqid() . '.zip';
802+
$tempDir = rtrim(sys_get_temp_dir(), "/\\") . '/evo-installer-' . uniqid('', true);
785803

786804
try {
787-
// Download with progress
788-
$this->downloadFile($downloadUrl, $tempFile);
805+
$composerWorkDir = rtrim(sys_get_temp_dir(), "/\\");
806+
$composerCommand = $this->resolveComposerCommand($composerWorkDir);
807+
808+
$args = [
809+
'create-project',
810+
$preferSource ? '--prefer-source' : '--prefer-dist',
811+
'--no-install',
812+
'evolution-cms/evolution',
813+
$tempDir,
814+
];
815+
if (is_array($composerRepository)) {
816+
$args[] = '--stability=dev';
817+
$args[] = '--repository=' . json_encode($composerRepository, JSON_UNESCAPED_SLASHES);
818+
$args[] = '--remove-vcs';
819+
}
820+
if (is_string($composerConstraint) && trim($composerConstraint) !== '') {
821+
$args[] = trim($composerConstraint);
822+
}
789823

790-
// Extract archive
791-
$this->tui->addLog('Extracting archive...');
792-
$this->extractZip($tempFile, $targetPath);
824+
$process = $this->runComposer($composerCommand, $args, $composerWorkDir);
825+
if (!$process->isSuccessful()) {
826+
$out = $this->sanitizeComposerOutput($process->getOutput() . "\n" . $process->getErrorOutput());
827+
throw new \RuntimeException(trim($out) !== '' ? trim($out) : 'Composer create-project failed.');
828+
}
829+
830+
$this->tui->replaceLastLogs('<fg=green>✔</> Download completed.');
793831

794-
// Clean up
795-
@unlink($tempFile);
832+
$this->tui->addLog('Extracting files...');
833+
$this->copyDirectoryWithProgress($tempDir, $targetPath);
834+
$this->removeDirectory($tempDir);
796835

797-
$sourceLabel = $branch ? "branch {$branch}" : "version {$displayName}";
798836
$this->tui->addLog("Evolution CMS from {$sourceLabel} downloaded and extracted successfully!", 'success');
799837

800838
// Mark Step 3 (Download Evolution CMS) as completed
801839
$this->steps['download']['completed'] = true;
802840
$this->tui->setQuestTrack($this->steps);
803841
} catch (\Exception $e) {
804842
// Clean up on error
805-
@unlink($tempFile);
806-
$this->tui->addLog("Failed to download Evolution CMS: " . $e->getMessage(), 'error');
807-
throw $e;
808-
}
809-
}
810-
811-
/**
812-
* Download file with progress bar.
813-
*/
814-
protected function downloadFile(string $url, string $destination): void
815-
{
816-
$client = new Client(['timeout' => 300]);
817-
818-
// First, get content length for progress tracking
819-
$totalBytes = 0;
820-
try {
821-
$headResponse = $client->head($url, ['allow_redirects' => true]);
822-
$totalBytes = (int) $headResponse->getHeaderLine('Content-Length');
823-
} catch (\Exception $e) {
824-
// If HEAD fails, we'll track progress from GET request
825-
}
826-
827-
$downloadedBytes = 0;
828-
$lastUpdate = 0;
829-
830-
// Download with on_stats callback for progress tracking
831-
$response = $client->get($url, [
832-
'sink' => $destination,
833-
'on_stats' => function (TransferStats $stats) use (&$downloadedBytes, &$lastUpdate, $totalBytes) {
834-
$downloadedBytes = $stats->getHandlerStat('size_download') ?: 0;
835-
$totalSize = $totalBytes > 0 ? $totalBytes : ($stats->getHandlerStat('size_download') ?: 0);
836-
837-
// Update progress every 100KB or every second
838-
$now = microtime(true);
839-
if ($totalSize > 0 && ($downloadedBytes - $lastUpdate > 100000 || $now - ($lastUpdate / 1000000) > 1)) {
840-
$this->tui->updateProgress('Downloading', $downloadedBytes, $totalSize);
841-
$lastUpdate = $downloadedBytes;
842-
}
843-
},
844-
]);
845-
846-
// Final progress update
847-
if ($totalBytes > 0) {
848-
$this->tui->updateProgress('Downloading', $totalBytes, $totalBytes);
849-
} else {
850-
$actualSize = (int) $response->getHeaderLine('Content-Length');
851-
if ($actualSize > 0) {
852-
$this->tui->updateProgress('Downloading', $actualSize, $actualSize);
843+
if (is_dir($tempDir)) {
844+
$this->removeDirectory($tempDir);
853845
}
854-
}
855846

856-
$this->tui->replaceLastLogs('<fg=green>✔</> Download completed.');
847+
$this->tui->addLog('Failed to download Evolution CMS: ' . $e->getMessage(), 'error');
848+
throw $e;
849+
}
857850
}
858851

859852
/**
860-
* Extract ZIP archive.
853+
* Copy package files into the destination directory.
861854
*/
862-
protected function extractZip(string $zipFile, string $destination): void
855+
protected function copyDirectoryWithProgress(string $source, string $destination): void
863856
{
864-
if (!extension_loaded('zip')) {
865-
throw new \RuntimeException('ZIP extension is required to extract the archive.');
866-
}
857+
$source = rtrim($source, "/\\");
858+
$destination = rtrim($destination, "/\\");
867859

868-
$zip = new \ZipArchive();
869-
if ($zip->open($zipFile) !== true) {
870-
throw new \RuntimeException("Failed to open ZIP archive: {$zipFile}");
860+
if (!is_dir($source)) {
861+
throw new \RuntimeException("Temporary download directory not found: {$source}");
871862
}
872863

873-
// Create destination directory if it doesn't exist
874864
if (!is_dir($destination)) {
875-
mkdir($destination, 0755, true);
865+
if (!@mkdir($destination, 0755, true) && !is_dir($destination)) {
866+
throw new \RuntimeException("Failed to create destination directory: {$destination}");
867+
}
876868
}
877869

878-
// Extract all files (count only entries that will be extracted so progress can reach 100%)
879-
$totalEntries = $zip->numFiles;
880870
$totalFiles = 0;
881-
for ($i = 0; $i < $totalEntries; $i++) {
882-
$filename = $zip->getNameIndex($i);
883-
if (!$filename) {
871+
$it = new \RecursiveIteratorIterator(
872+
new \RecursiveDirectoryIterator($source, \FilesystemIterator::SKIP_DOTS),
873+
\RecursiveIteratorIterator::SELF_FIRST
874+
);
875+
foreach ($it as $item) {
876+
$rel = substr($item->getPathname(), strlen($source) + 1);
877+
if ($rel === false || $rel === '') {
884878
continue;
885879
}
886-
887-
// Skip directory entries
888-
if (substr($filename, -1) === '/') {
880+
if ($this->shouldSkipCopiedPath($rel)) {
889881
continue;
890882
}
891-
892-
// Remove source prefix from path (e.g., "evolution-3.5.0/" or "evolution-branch-name/" -> "")
893-
$localPath = preg_replace('/^[^\/]+\//', '', $filename);
894-
if (empty($localPath)) {
883+
if ($item->isDir()) {
895884
continue;
896885
}
897-
898886
$totalFiles++;
899887
}
900888

901-
$extractedFiles = 0;
889+
$copiedFiles = 0;
902890

903-
for ($i = 0; $i < $totalEntries; $i++) {
904-
$filename = $zip->getNameIndex($i);
905-
if (!$filename) {
891+
$it = new \RecursiveIteratorIterator(
892+
new \RecursiveDirectoryIterator($source, \FilesystemIterator::SKIP_DOTS),
893+
\RecursiveIteratorIterator::SELF_FIRST
894+
);
895+
foreach ($it as $item) {
896+
$srcPath = $item->getPathname();
897+
$rel = substr($srcPath, strlen($source) + 1);
898+
if ($rel === false || $rel === '') {
906899
continue;
907900
}
908-
909-
// Skip directory entries
910-
if (substr($filename, -1) === '/') {
901+
if ($this->shouldSkipCopiedPath($rel)) {
911902
continue;
912903
}
913904

914-
// Remove source prefix from path (e.g., "evolution-3.5.0/" or "evolution-branch-name/" -> "")
915-
$localPath = preg_replace('/^[^\/]+\//', '', $filename);
905+
$dstPath = $destination . '/' . str_replace('\\', '/', $rel);
916906

917-
if (empty($localPath)) {
907+
if ($item->isDir()) {
908+
if (!is_dir($dstPath)) {
909+
@mkdir($dstPath, 0755, true);
910+
}
918911
continue;
919912
}
920913

921-
$targetPath = $destination . '/' . $localPath;
922-
$targetDir = dirname($targetPath);
923-
924-
// Create directory if needed
925-
if (!is_dir($targetDir)) {
926-
mkdir($targetDir, 0755, true);
914+
$dstDir = dirname($dstPath);
915+
if (!is_dir($dstDir) && !@mkdir($dstDir, 0755, true) && !is_dir($dstDir)) {
916+
throw new \RuntimeException("Failed to create directory: {$dstDir}");
927917
}
928918

929-
// Extract file
930-
$content = $zip->getFromIndex($i);
931-
if ($content === false) {
932-
throw new \RuntimeException("Failed to extract file: {$filename}");
933-
}
934-
if (file_put_contents($targetPath, $content) === false) {
935-
throw new \RuntimeException("Failed to write extracted file: {$targetPath}");
919+
if ($item->isLink()) {
920+
$target = @readlink($srcPath);
921+
if ($target === false) {
922+
throw new \RuntimeException("Failed to read symlink: {$srcPath}");
923+
}
924+
if (is_link($dstPath) || file_exists($dstPath)) {
925+
@unlink($dstPath);
926+
}
927+
if (!@symlink($target, $dstPath)) {
928+
// Fallback: copy the resolved target content if symlinks are not permitted.
929+
if (!@copy($srcPath, $dstPath)) {
930+
throw new \RuntimeException("Failed to copy symlink target: {$srcPath}");
931+
}
932+
}
933+
} else {
934+
if (!@copy($srcPath, $dstPath)) {
935+
throw new \RuntimeException("Failed to copy file: {$rel}");
936+
}
937+
@chmod($dstPath, $item->getPerms() & 0777);
936938
}
937-
$extractedFiles++;
938939

939-
// Update progress every 10 files
940-
if ($totalFiles > 0 && ($extractedFiles % 10 === 0 || $extractedFiles === $totalFiles)) {
941-
$this->tui->updateProgress('Extracting', $extractedFiles, $totalFiles, 'files');
940+
$copiedFiles++;
941+
if ($totalFiles > 0 && ($copiedFiles % 25 === 0 || $copiedFiles === $totalFiles)) {
942+
$this->tui->updateProgress('Extracting', $copiedFiles, $totalFiles, 'files');
942943
}
943944
}
944945

945-
$zip->close();
946-
947-
// Ensure progress reaches 100% when extraction finished successfully
948-
if ($totalFiles > 0 && $extractedFiles === $totalFiles) {
946+
if ($totalFiles > 0 && $copiedFiles === $totalFiles) {
949947
$this->tui->updateProgress('Extracting', $totalFiles, $totalFiles, 'files');
950948
}
951-
$this->tui->replaceLastLogs('<fg=green>✔</> Extracted ' . $extractedFiles . ' files.');
949+
950+
$this->tui->replaceLastLogs('<fg=green>✔</> Extracted ' . $copiedFiles . ' files.');
951+
}
952+
953+
protected function shouldSkipCopiedPath(string $relativePath): bool
954+
{
955+
$parts = preg_split('#[\\\\/]+#', $relativePath) ?: [];
956+
foreach ($parts as $part) {
957+
if ($part === '.git' || $part === '.hg' || $part === '.svn') {
958+
return true;
959+
}
960+
}
961+
return false;
952962
}
953963

954964
/**

0 commit comments

Comments
 (0)