Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -432,12 +432,6 @@ parameters:
count: 1
path: src/Platform/TargetPlatform.php

-
message: '#^Parameter \#1 \$callback of function array_map expects \(callable\(mixed\)\: mixed\)\|null, Closure\(array\)\: non\-empty\-array given\.$#'
identifier: argument.type
count: 1
path: src/SelfManage/Update/FetchPieReleaseFromGitHub.php

-
message: '#^Dead catch \- Php\\Pie\\SelfManage\\Verify\\GithubCliNotAvailable is never thrown in the try block\.$#'
identifier: catch.neverThrown
Expand Down Expand Up @@ -675,7 +669,7 @@ parameters:
-
message: '#^Parameter \#4 \$body of class Composer\\Util\\Http\\Response constructor expects string\|null, string\|false given\.$#'
identifier: argument.type
count: 2
count: 1
path: test/unit/SelfManage/Update/FetchPieReleaseFromGitHubTest.php

-
Expand Down
18 changes: 18 additions & 0 deletions resources/pie-settings-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://raw.githubusercontent.com/php/pie/main/resources/pie-config-schema.json",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this read settings instead of config?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, thanks, will fix :)

"title": "PIE configuration file schema",
"description": "Schema for PIE tool configuration file",
"type": "object",
"properties": {
"channel": {
"type": "string",
"description": "Which update channel to use when running self-update",
"enum": [
"nightly",
"preview",
"stable"
]
}
}
}
62 changes: 44 additions & 18 deletions src/Command/SelfUpdateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@

namespace Php\Pie\Command;

use Composer\Semver\Semver;
use Composer\Util\AuthHelper;
use Composer\Util\HttpDownloader;
use Php\Pie\ComposerIntegration\PieComposerFactory;
use Php\Pie\ComposerIntegration\PieComposerRequest;
use Php\Pie\ComposerIntegration\QuieterConsoleIO;
use Php\Pie\File\FullPathToSelf;
use Php\Pie\File\SudoFilePut;
use Php\Pie\Platform;
use Php\Pie\SelfManage\Update\Channel;
use Php\Pie\SelfManage\Update\FetchPieReleaseFromGitHub;
use Php\Pie\SelfManage\Update\PiePharMissingFromLatestRelease;
use Php\Pie\SelfManage\Update\ReleaseIsNewer;
use Php\Pie\SelfManage\Update\ReleaseMetadata;
use Php\Pie\SelfManage\Verify\FailedToVerifyRelease;
use Php\Pie\SelfManage\Verify\VerifyPieReleaseUsingAttestation;
use Php\Pie\Settings;
use Php\Pie\Util\Emoji;
use Php\Pie\Util\PieVersion;
use Psr\Container\ContainerInterface;
Expand All @@ -25,9 +27,9 @@
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;

use function file_get_contents;
use function preg_match;
use function sprintf;
use function unlink;

Expand All @@ -37,6 +39,8 @@
)]
final class SelfUpdateCommand extends Command
{
private const OPTION_STABLE_UPDATE = 'stable';
private const OPTION_PREVIEW_UPDATE = 'preview';
private const OPTION_NIGHTLY_UPDATE = 'nightly';

/** @param non-empty-string $githubApiBaseUrl */
Expand All @@ -60,6 +64,18 @@ public function configure(): void
InputOption::VALUE_NONE,
'Update to the latest nightly version.',
);
$this->addOption(
self::OPTION_PREVIEW_UPDATE,
null,
InputOption::VALUE_NONE,
'Update to the latest preview version.',
);
$this->addOption(
self::OPTION_STABLE_UPDATE,
null,
InputOption::VALUE_NONE,
'Update to the latest stable version.',
);
}

public function execute(InputInterface $input, OutputInterface $output): int
Expand All @@ -70,6 +86,22 @@ public function execute(InputInterface $input, OutputInterface $output): int
return Command::FAILURE;
}

$settings = new Settings(Platform::getPieBaseWorkingDirectory());
$updateChannel = $settings->updateChannel();

if ($input->hasOption(self::OPTION_NIGHTLY_UPDATE) && $input->getOption(self::OPTION_NIGHTLY_UPDATE)) {
$settings->changeUpdateChannel(Channel::Nightly);
$updateChannel = Channel::Nightly;
} elseif ($input->hasOption(self::OPTION_PREVIEW_UPDATE) && $input->getOption(self::OPTION_PREVIEW_UPDATE)) {
$settings->changeUpdateChannel(Channel::Preview);
$updateChannel = Channel::Preview;
} elseif ($input->hasOption(self::OPTION_STABLE_UPDATE) && $input->getOption(self::OPTION_STABLE_UPDATE)) {
$settings->changeUpdateChannel(Channel::Stable);
$updateChannel = Channel::Stable;
}

$output->writeln(sprintf('Updating using the <info>%s</> channel.', $updateChannel->value));

$targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);

$composer = PieComposerFactory::createPieComposer(
Expand All @@ -85,7 +117,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
$fetchLatestPieRelease = new FetchPieReleaseFromGitHub($this->githubApiBaseUrl, $httpDownloader, $authHelper);
$verifyPiePhar = VerifyPieReleaseUsingAttestation::factory();

if ($input->hasOption(self::OPTION_NIGHTLY_UPDATE) && $input->getOption(self::OPTION_NIGHTLY_UPDATE)) {
if ($updateChannel === Channel::Nightly) {
$latestRelease = new ReleaseMetadata(
'nightly',
'https://php.github.io/pie/pie-nightly.phar',
Expand All @@ -94,28 +126,22 @@ public function execute(InputInterface $input, OutputInterface $output): int
$output->writeln('Downloading the latest nightly release.');
} else {
try {
$latestRelease = $fetchLatestPieRelease->latestReleaseMetadata();
} catch (PiePharMissingFromLatestRelease $piePharMissingFromLatestRelease) {
$output->writeln(sprintf('<error>%s</error>', $piePharMissingFromLatestRelease->getMessage()));
$latestRelease = $fetchLatestPieRelease->latestReleaseMetadata($updateChannel);
} catch (Throwable $throwable) {
$output->writeln(sprintf('<error>%s</error>', $throwable->getMessage()));

return Command::FAILURE;
}

$pieVersion = PieVersion::get();

if (preg_match('/^(?<tag>.+)@(?<hash>[a-f0-9]{7})$/', $pieVersion, $matches)) {
// Have to change the version to something the Semver library understands
$pieVersion = sprintf('dev-main#%s', $matches['hash']);
$output->writeln(sprintf(
'It looks like you are running a nightly build; if you want to get the newest nightly, specify the --%s flag.',
self::OPTION_NIGHTLY_UPDATE,
));
}

$output->writeln(sprintf('You are currently running PIE version %s', $pieVersion));

if (! Semver::satisfies($latestRelease->tag, '> ' . $pieVersion)) {
$output->writeln('<info>You already have the latest version 😍</info>');
if (! ReleaseIsNewer::forChannel($updateChannel, $pieVersion, $latestRelease)) {
$output->writeln(sprintf(
'<info>You already have the latest version for the %s channel 😍</info>',
$updateChannel->value,
));

return Command::SUCCESS;
}
Expand Down
51 changes: 28 additions & 23 deletions src/Platform.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,29 +55,11 @@ private static function getUserDir(): string
return rtrim(strtr($home, '\\', '/'), '/');
}

/**
* This is essentially a Composer-controlled `vendor` directory that has downloaded sources
*
* @throws RuntimeException
*/
public static function getPieWorkingDirectory(TargetPlatform $targetPlatform): string
public static function getPieBaseWorkingDirectory(): string
{
// Simple hash of the target platform so we can build against different PHP installs on the same system
$targetPlatformPath = DIRECTORY_SEPARATOR . 'php' . $targetPlatform->phpBinaryPath->majorMinorVersion() . '_' . md5(implode(
'|',
[
$targetPlatform->operatingSystem->name,
$targetPlatform->phpBinaryPath->phpBinaryPath,
$targetPlatform->phpBinaryPath->version(),
$targetPlatform->architecture->name,
$targetPlatform->threadSafety->name,
$targetPlatform->windowsCompiler?->name ?? 'x',
],
));

$home = ComposerPlatform::getEnv('PIE_WORKING_DIRECTORY');
if ($home !== false && $home !== '') {
return $home . $targetPlatformPath;
return $home;
}

if (ComposerPlatform::isWindows()) {
Expand All @@ -86,7 +68,7 @@ public static function getPieWorkingDirectory(TargetPlatform $targetPlatform): s
throw new RuntimeException('The APPDATA or PIE_WORKING_DIRECTORY environment variable must be set for PIE to run correctly');
}

return rtrim(strtr($appData, '\\', '/'), '/') . '/PIE' . $targetPlatformPath . '/';
return rtrim(strtr($appData, '\\', '/'), '/') . '/PIE';
}

$userDir = self::getUserDir();
Expand All @@ -107,12 +89,35 @@ public static function getPieWorkingDirectory(TargetPlatform $targetPlatform): s
// select first dir which exists of: $XDG_CONFIG_HOME/pie or ~/.pie
foreach ($dirs as $dir) {
if (Silencer::call('is_dir', $dir)) {
return $dir . $targetPlatformPath;
return $dir;
}
}

// if none exists, we default to first defined one (XDG one if system uses it, or ~/.pie otherwise)
return $dirs[0] . $targetPlatformPath;
return $dirs[0];
}

/**
* This is essentially a Composer-controlled `vendor` directory that has downloaded sources
*
* @throws RuntimeException
*/
public static function getPieWorkingDirectory(TargetPlatform $targetPlatform): string
{
// Simple hash of the target platform so we can build against different PHP installs on the same system
$targetPlatformPath = DIRECTORY_SEPARATOR . 'php' . $targetPlatform->phpBinaryPath->majorMinorVersion() . '_' . md5(implode(
'|',
[
$targetPlatform->operatingSystem->name,
$targetPlatform->phpBinaryPath->phpBinaryPath,
$targetPlatform->phpBinaryPath->version(),
$targetPlatform->architecture->name,
$targetPlatform->threadSafety->name,
$targetPlatform->windowsCompiler?->name ?? 'x',
],
));

return self::getPieBaseWorkingDirectory() . $targetPlatformPath;
}

/** @return non-empty-string */
Expand Down
12 changes: 12 additions & 0 deletions src/SelfManage/Update/Channel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Php\Pie\SelfManage\Update;

enum Channel: string
{
case Stable = 'stable';
case Preview = 'preview';
case Nightly = 'nightly';
}
2 changes: 1 addition & 1 deletion src/SelfManage/Update/FetchPieRelease.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
interface FetchPieRelease
{
/** @throws PiePharMissingFromLatestRelease */
public function latestReleaseMetadata(): ReleaseMetadata;
public function latestReleaseMetadata(Channel $updateChannel): ReleaseMetadata;

/** Download the given pie.phar and return the filename (should be a temp file) */
public function downloadContent(ReleaseMetadata $releaseMetadata): BinaryFile;
Expand Down
86 changes: 57 additions & 29 deletions src/SelfManage/Update/FetchPieReleaseFromGitHub.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Php\Pie\SelfManage\Update;

use Composer\Package\Version\VersionParser;
use Composer\Util\AuthHelper;
use Composer\Util\HttpDownloader;
use Php\Pie\File\BinaryFile;
Expand All @@ -21,8 +22,8 @@
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
final class FetchPieReleaseFromGitHub implements FetchPieRelease
{
private const PIE_PHAR_NAME = 'pie.phar';
private const PIE_LATEST_RELEASE_URL = '/repos/php/pie/releases/latest';
private const PIE_PHAR_NAME = 'pie.phar';
private const PIE_RELEASES_URL = '/repos/php/pie/releases';

public function __construct(
private readonly string $githubApiBaseUrl,
Expand All @@ -31,9 +32,9 @@ public function __construct(
) {
}

public function latestReleaseMetadata(): ReleaseMetadata
public function latestReleaseMetadata(Channel $updateChannel): ReleaseMetadata
{
$url = $this->githubApiBaseUrl . self::PIE_LATEST_RELEASE_URL;
$url = $this->githubApiBaseUrl . self::PIE_RELEASES_URL;

$decodedResponse = $this->httpDownloader->get(
$url,
Expand All @@ -46,40 +47,67 @@ public function latestReleaseMetadata(): ReleaseMetadata
],
)->decodeJson();

Assert::isArray($decodedResponse);
Assert::keyExists($decodedResponse, 'tag_name');
Assert::stringNotEmpty($decodedResponse['tag_name']);
Assert::keyExists($decodedResponse, 'assets');
Assert::isList($decodedResponse['assets']);
Assert::isList($decodedResponse);
Assert::allIsArray($decodedResponse);

$assetsNamedPiePhar = array_filter(
$releases = array_filter(
array_map(
/** @return array{name: non-empty-string, browser_download_url: non-empty-string, ...} */
static function (array $asset): array {
Assert::keyExists($asset, 'name');
Assert::stringNotEmpty($asset['name']);
Assert::keyExists($asset, 'browser_download_url');
Assert::stringNotEmpty($asset['browser_download_url']);

return $asset;
static function (array $releaseResponse): ReleaseMetadata|null {
Assert::keyExists($releaseResponse, 'tag_name');
Assert::stringNotEmpty($releaseResponse['tag_name']);
Assert::keyExists($releaseResponse, 'assets');
Assert::isList($releaseResponse['assets']);
Assert::allIsArray($releaseResponse['assets']);

$assetsNamedPiePhar = array_filter(
array_map(
static function (array $asset): array {
Assert::keyExists($asset, 'name');
Assert::stringNotEmpty($asset['name']);
Assert::keyExists($asset, 'browser_download_url');
Assert::stringNotEmpty($asset['browser_download_url']);

return $asset;
},
$releaseResponse['assets'],
),
static function (array $asset): bool {
return $asset['name'] === self::PIE_PHAR_NAME;
},
);

if (! count($assetsNamedPiePhar)) {
return null;
}

$firstAssetNamedPiePhar = reset($assetsNamedPiePhar);

return new ReleaseMetadata(
$releaseResponse['tag_name'],
$firstAssetNamedPiePhar['browser_download_url'],
);
},
$decodedResponse['assets'],
$decodedResponse,
),
static function (array $asset): bool {
return $asset['name'] === self::PIE_PHAR_NAME;
static function (ReleaseMetadata|null $releaseMetadata) use ($updateChannel): bool {
if ($releaseMetadata === null) {
return false;
}

$stability = VersionParser::parseStability($releaseMetadata->tag);

return ($updateChannel === Channel::Stable && $stability === 'stable')
|| $updateChannel === Channel::Preview;
},
);

if (! count($assetsNamedPiePhar)) {
throw PiePharMissingFromLatestRelease::fromRelease($decodedResponse['tag_name']);
}
$first = reset($releases);

$firstAssetNamedPiePhar = reset($assetsNamedPiePhar);
if (! $first instanceof ReleaseMetadata) {
throw new RuntimeException('No PIE release found for channel ' . $updateChannel->value);
}

return new ReleaseMetadata(
$decodedResponse['tag_name'],
$firstAssetNamedPiePhar['browser_download_url'],
);
return $first;
}

public function downloadContent(ReleaseMetadata $releaseMetadata): BinaryFile
Expand Down
Loading