@@ -263,9 +263,167 @@ final class EvoBootstrapper
263263 }
264264 $ this ->out ("Current: {$ current }" );
265265 $ this ->out ("Latest: {$ tag }" );
266+ $ this ->updatePhpInstallerPackageBestEffort ();
266267 $ this ->installBinary (true );
267268 }
268269
270+ private function updatePhpInstallerPackageBestEffort (): void
271+ {
272+ $ composer = $ this ->findComposerBinary ();
273+ if ($ composer === null ) {
274+ $ this ->out ('PHP installer update: skipped (Composer not found). ' );
275+ return ;
276+ }
277+
278+ $ workDir = $ this ->findComposerWorkDir ();
279+ if ($ workDir === null ) {
280+ $ this ->out ('PHP installer update: skipped (unable to detect Composer working dir). ' );
281+ return ;
282+ }
283+
284+ if (!is_writable ($ workDir )) {
285+ $ this ->out ("PHP installer update: skipped (working dir not writable: {$ workDir }). " );
286+ return ;
287+ }
288+
289+ $ this ->out ('Updating PHP installer package via Composer... ' );
290+ $ code = $ this ->runProcess (
291+ [
292+ $ composer ,
293+ 'update ' ,
294+ 'evolution-cms/installer ' ,
295+ '--no-interaction ' ,
296+ '--no-ansi ' ,
297+ '--no-progress ' ,
298+ ],
299+ $ workDir ,
300+ [
301+ 'COMPOSER_ALLOW_SUPERUSER ' => '1 ' ,
302+ ]
303+ );
304+ if ($ code !== 0 ) {
305+ $ this ->out ("PHP installer update: failed (exit code {$ code }). " );
306+ return ;
307+ }
308+
309+ $ this ->out ('PHP installer update: OK ' );
310+ }
311+
312+ private function findComposerBinary (): ?string
313+ {
314+ $ bins = ['composer ' , 'composer2 ' ];
315+ $ binEnv = getenv ('EVO_COMPOSER_BIN ' ) ?: '' ;
316+ if ($ binEnv !== '' ) {
317+ array_unshift ($ bins , $ binEnv );
318+ }
319+ $ home = getenv ('HOME ' ) ?: '' ;
320+ if ($ home !== '' ) {
321+ $ bins [] = $ home . '/.composer/composer ' ;
322+ $ bins [] = $ home . '/.composer/vendor/bin/composer ' ;
323+ $ bins [] = $ home . '/bin/composer ' ;
324+ }
325+
326+ $ re = '/Composer \\s+(?:version \\s+)?([0-9]+ \\.[0-9]+ \\.[0-9]+)/i ' ;
327+ foreach ($ bins as $ bin ) {
328+ $ arg = escapeshellarg ($ bin );
329+ $ out = $ this ->runCommandCaptureOutput ($ arg . ' -V 2>&1 ' );
330+ if (is_string ($ out ) && preg_match ($ re , $ out ) === 1 ) {
331+ return $ bin ;
332+ }
333+ $ out = $ this ->runCommandCaptureOutput ($ arg . ' --version 2>&1 ' );
334+ if (is_string ($ out ) && preg_match ($ re , $ out ) === 1 ) {
335+ return $ bin ;
336+ }
337+ }
338+
339+ return null ;
340+ }
341+
342+ private function findComposerWorkDir (): ?string
343+ {
344+ $ candidate = $ this ->detectComposerRootFromVendorLayout ();
345+ if ($ candidate !== null ) {
346+ return $ candidate ;
347+ }
348+
349+ // Fallback: walk up a few levels and look for a composer root with vendor metadata.
350+ $ dir = $ this ->packageRoot ;
351+ for ($ i = 0 ; $ i < 6 ; $ i ++) {
352+ if (is_file ($ dir . '/composer.json ' ) && is_file ($ dir . '/vendor/composer/installed.json ' )) {
353+ return $ dir ;
354+ }
355+ $ parent = dirname ($ dir );
356+ if ($ parent === $ dir ) {
357+ break ;
358+ }
359+ $ dir = $ parent ;
360+ }
361+
362+ return null ;
363+ }
364+
365+ private function detectComposerRootFromVendorLayout (): ?string
366+ {
367+ $ vendorDir = dirname ($ this ->packageRoot , 2 );
368+ if (basename ($ vendorDir ) !== 'vendor ' ) {
369+ return null ;
370+ }
371+
372+ $ root = dirname ($ vendorDir );
373+ if (!is_file ($ root . '/composer.json ' )) {
374+ return null ;
375+ }
376+ if (!is_file ($ vendorDir . '/composer/installed.json ' ) && !is_file ($ vendorDir . '/composer/installed.php ' )) {
377+ return null ;
378+ }
379+
380+ return $ root ;
381+ }
382+
383+ /**
384+ * @param array<int, string> $cmd
385+ * @param array<string, string> $env
386+ */
387+ private function runProcess (array $ cmd , ?string $ cwd = null , array $ env = []): int
388+ {
389+ if (function_exists ('proc_open ' ) && !$ this ->isFunctionDisabled ('proc_open ' )) {
390+ $ descriptors = [
391+ 0 => STDIN ,
392+ 1 => STDOUT ,
393+ 2 => STDERR ,
394+ ];
395+
396+ $ mergedEnv = array_merge ($ _ENV , $ env );
397+ $ proc = @proc_open ($ cmd , $ descriptors , $ pipes , $ cwd ?: null , $ mergedEnv );
398+ if (is_resource ($ proc )) {
399+ $ code = proc_close ($ proc );
400+ return (int )$ code ;
401+ }
402+ }
403+
404+ // Fallback: execute via shell.
405+ $ cmdParts = [];
406+ foreach ($ cmd as $ part ) {
407+ $ cmdParts [] = escapeshellarg ($ part );
408+ }
409+ $ full = implode (' ' , $ cmdParts );
410+ $ exitCode = 0 ;
411+ if ($ cwd ) {
412+ $ full = 'cd ' . escapeshellarg ($ cwd ) . ' && ' . $ full ;
413+ }
414+ passthru ($ full , $ exitCode );
415+ return (int )$ exitCode ;
416+ }
417+
418+ private function isFunctionDisabled (string $ name ): bool
419+ {
420+ $ disabled = (string )ini_get ('disable_functions ' );
421+ if ($ disabled === '' ) {
422+ return false ;
423+ }
424+ return stripos ($ disabled , $ name ) !== false ;
425+ }
426+
269427 private function installBinary (bool $ isSelfUpdate ): void
270428 {
271429 $ this ->assertWritableDir ($ this ->binDir );
0 commit comments