Files
Gnuboard7/app/Support/ComposerInstallInfo.php
T
HeuJung ffab451b4a fix(core,installer): dev vendor 설치본의 코어 업데이트 중단 수정 — stale 패키지 매니페스트 3계층
Laravel PackageManifest 는 bootstrap/cache/packages.php 가 있으면 stale 여부를 검사하지
않고 그대로 읽어 등재된 provider 를 new 한다. 코어 업데이트는 Step 6/8 에서 vendor 를
--no-dev 로 교체하지만 그 파일은 Step 11 까지 이전 설치본의 것이 남으므로, 옵션 없는
`composer install` 로 깔린 사이트에서는 Step 10 spawn 자식이 새 vendor 에 없는 provider 를
찾다 부팅 단계에서 죽는다. 부팅 전이라 앱 로그에 흔적이 없고 부모에게는 자식의 비정상
종료로만 보여, 운영자에게는 「Class ... not found」 와 수동 재개 안내만 남는다.
(sir.kr 커뮤니티 제보, 7.0.9 → 7.0.10)

3계층으로 막는다.

 1. 부모 — spawn 직전 PackageManifestCacheHelper::clear
 2. 자식 — bootstrap/app.php 가 G7_UPDATE_IN_PROGRESS 를 보고 스스로 정리한다.
 이미 배포된 7.0.9·7.0.10 부모는 고칠 수 없으므로 그 아래에서 도는 신버전
 자식의 유일한 방어다. App\ 클래스를 참조하지 않고 실패는 무시한다.
 3. 범위 — CoreVersionChecker 의 env APP_VERSION 우선을 CoreUpdateContext 트리 안으로
 축소한다. 업데이트 전에 뜬 artisan serve·큐 워커가 옛 값을 물고 확장을
 incompatible_core 로 끄던 경로를 닫는다(관리자 템플릿이 대상이면 복구 UI
 자체에 도달할 수 없다).

업데이트 트리 판정은 App\Support\CoreUpdateContext 가 단독 소유하고
CoreServiceProvider::isCoreUpdateInProgress 는 위임으로 남는다. bootstrap/app.php 의
복제본은 부팅 전이라 불가피한 예외이며, 두 조건의 동형성을 테스트가 단언한다.

실측 중 드러난 결함 2건을 함께 고쳤다.

 - ConfigCacheHelper::withPreservedContainer 가 파사드 애플리케이션을 되돌리지 않아
 Step 11 이 `Target class [command.tinker] does not exist` 로 실패·롤백했다.
 - updateVersionInEnv 가 프로세스 환경을 갱신하지 않아 config 캐시에 이전 버전이 구워졌다
 (Laravel env 저장소가 불변이라 재부팅으로도 덮이지 않는다).

인스톨러는 재사용 vendor 의 개발용 패키지를 installed.json 으로 감지해 설치 환경 확인
카드·설치 로그로 알리되 설치를 차단하지 않고( 결정 D1), 재사용 경로에서도 컴파일 캐시를
정리한다. 실행되는 명령만이 아니라 실패 시 안내하는 수동 명령까지 --no-dev 로 맞췄다.
코어 업데이트 완료·핸드오프·단독 재개 사후 단계에서 queue:restart 신호를 보낸다(D3).

코어 7.0.10 → 7.0.11.
2026-09-08 09:46:57 +09:00

94 lines
3.5 KiB
PHP

<?php
namespace App\Support;
/**
* vendor 디렉토리가 개발용(require-dev 포함) 설치인지 판정하는 유틸리티.
*
* `composer install`(옵션 없음)은 require-dev 와 그 전이 의존성까지 설치하고, 그 목록이
* `bootstrap/cache/packages.php` 에 provider 로 등재된다. 운영 사이트가 그 상태로 남으면
* 이후 코어 업데이트가 vendor 를 `--no-dev` 로 교체할 때 매니페스트에만 남은 provider 를
* 찾다 부팅이 깨진다. 그래서 인스톨러와 코어 업데이트가 이 판정으로 운영자에게 알린다.
*
* 프레임워크에 의존하지 않는다 — 인스톨러는 Laravel 오토로드 없이 도는 순수 PHP 라
* 이 클래스를 `require_once` 로 직접 읽는다.
*/
final class ComposerInstallInfo
{
/**
* vendor 디렉토리 기준 Composer 설치 정보 파일의 상대 경로
*/
public const INSTALLED_JSON = 'composer/installed.json';
/**
* vendor 디렉토리의 Composer 설치 구성을 조사합니다.
*
* 판정: `installed.json` 의 최상위 `dev` 가 true 이거나 `dev-package-names` 가 비어 있지
* 않으면 개발용 설치. 두 신호를 OR 로 보는 이유는 Composer 버전에 따라 한쪽만 채워질 수
* 있기 때문이다.
*
* @param string $vendorPath vendor 디렉토리 절대 경로
* @return array{dev: bool|null, packages: array<int, string>} dev 가 null 이면 판정 불가
* (파일 부재 · JSON 오류 · Composer 1 형식)
*/
public static function inspect(string $vendorPath): array
{
$unknown = ['dev' => null, 'packages' => []];
$path = rtrim($vendorPath, '/\\').'/'.self::INSTALLED_JSON;
if (! is_file($path)) {
return $unknown;
}
$raw = @file_get_contents($path);
if ($raw === false || $raw === '') {
return $unknown;
}
$decoded = json_decode($raw, true);
// Composer 1 은 최상위가 패키지 배열이라 dev 정보 자체가 없다 — 판정 불가로 둔다.
if (! is_array($decoded) || ! array_key_exists('packages', $decoded)) {
return $unknown;
}
$names = [];
if (isset($decoded['dev-package-names']) && is_array($decoded['dev-package-names'])) {
foreach ($decoded['dev-package-names'] as $name) {
if (is_string($name) && trim($name) !== '') {
$names[] = $name;
}
}
}
$devFlag = $decoded['dev'] ?? null;
return [
'dev' => $devFlag === true || $names !== [],
'packages' => $names,
];
}
/**
* vendor 가 개발용 설치인지 반환합니다.
*
* @param string $vendorPath vendor 디렉토리 절대 경로
* @return bool|null 판정 불가 시 null
*/
public static function isDevInstall(string $vendorPath): ?bool
{
return self::inspect($vendorPath)['dev'];
}
/**
* vendor 에 설치된 개발용(require-dev) 패키지 이름 목록을 반환합니다.
*
* @param string $vendorPath vendor 디렉토리 절대 경로
* @return array<int, string> 개발용 패키지가 없거나 판정 불가면 빈 배열
*/
public static function devPackageNames(string $vendorPath): array
{
return self::inspect($vendorPath)['packages'];
}
}