A thing to make PHAR self-updating easy and secure.
laravel-zero/phar-updater is a Laravel package for a thing to make phar self-updating easy and secure..
It currently has 17 GitHub stars and 3.298.253 downloads on Packagist (latest version v1.4.2).
Install it with composer require laravel-zero/phar-updater.
Discover more Laravel packages by laravel-zero
or browse all Laravel packages to compare alternatives.
Last updated
This is a fork of the Humbug PHAR Updater for internal use in Laravel Zero.
The backend for the self-update command in Laravel Zero PHARs. Originally created by Humbug.
Table of Contents
The laravel-zero/phar-updater package has the following features:
Apart from the detailed documentation below, you can find the package being used within Laravel Zero's self-update component.
Via Composer
composer require laravel-zero/phar-updater
Via the Laravel Zero component installer
php <application> app:install self-update
The package utilises PHP Streams for remote requests, so it will require the openssl extension and the allow_url_open
setting to both be enabled. Support for curl will follow in time.
The default update strategy uses a SHA-512 hash of the current remote phar in a version file, and will update the local phar when this version is changed. There is also a Github strategy which tracks Github Releases where you can upload a new phar file for a release.
NOTE: The SHA-1 strategy has been removed. If you were using it, switch to the default SHA-512 strategy (which is what Box generates by default) and publish a SHA-512 version file instead.
Create your self-update command, or even an update command for some other phar other than the current one, and include this.
/**
* The simplest usage assumes the currently running phar is to be updated.
*
* The first constructor parameter is the path to a phar if you are not updating
* the currently running phar.
*/
use Humbug\SelfUpdate\Updater;
$updater = new Updater();
// Add the below to use the SHA-256 strategy. It will default to SHA-512 if excluded.
$updater->setStrategy(Updater::STRATEGY_SHA256);
$updater->getStrategy()->setPharUrl('https://example.com/current.phar');
$updater->getStrategy()->setVersionUrl('https://example.com/current.version');
try {
$result = $updater->update();
echo $result ? "Updated!\n" : "No update needed!\n";
} catch (\Exception $e) {
echo "Well, something happened! Either an oopsie or something involving hackers.\n";
exit(1);
}
If you need version information:
use Humbug\SelfUpdate\Updater;
$updater = new Updater();
$updater->getStrategy()->setPharUrl('https://example.com/current.phar');
$updater->getStrategy()->setVersionUrl('https://example.com/current.version');
try {
$result = $updater->update();
if ($result) {
$new = $updater->getNewVersion();
$old = $updater->getOldVersion();
printf(
'Updated from SHA-512 %s to SHA-512 %s', $old, $new
);
} else {
echo "No update needed!\n";
}
} catch (\Exception $e) {
echo "Well, something happened! Either an oopsie or something involving hackers.\n";
exit(1);
}
See the Update Strategies section for an overview of how to set up the SHA-256 or SHA-512 strategy. It's a simple to maintain choice for development or nightly versions of phars which are released to a specific numbered version.
Beyond development or nightly phars, if you are releasing numbered versions on Github (i.e. tags), you can upload additional files (such as phars) to include in the Github Release.
/**
* Other than somewhat different setters for the strategy, all other operations
* are identical.
*/
use Humbug\SelfUpdate\Updater;
$updater = new Updater();
$updater->setStrategy(Updater::STRATEGY_GITHUB);
$updater->getStrategy()->setPackageName('myvendor/myapp');
$updater->getStrategy()->setPharName('myapp.phar');
$updater->getStrategy()->setCurrentLocalVersion('v1.0.1');
try {
$result = $updater->update();
echo $result ? "Updated!\n" : "No update needed!\n";
} catch (\Exception $e) {
echo "Well, something happened! Either an oopsie or something involving hackers.\n";
exit(1);
}
Package name refers to the name used by Packagist, and phar name is the phar's file name assumed to be constant across versions.
It's left to the implementation to supply the current release version associated with the local phar. This needs to be
stored within the phar and should match the version string used by Github. This can follow any standard practice with
recognisable pre- and postfixes, e.g.
v1.0.3, 1.0.3, 1.1, 1.3rc, 1.3.2pl2.
If you wish to update to a non-stable version, for example where users want to update according to a development track,
you can set the stability flag for the Github strategy. By default this is set to stable or, in constant form,
\Humbug\SelfUpdate\Strategy\GithubStrategy::STABLE:
$updater->getStrategy()->setStability('unstable');
If you want to ignore stability and just update to the most recent version regardless:
$updater->getStrategy()->setStability('any');
The Updater automatically copies a backup of the original phar to myname-old.phar. You can trigger a rollback quite easily using this convention:
use Humbug\SelfUpdate\Updater;
/**
* Same constructor parameters as you would use for updating. Here, just defaults.
*/
$updater = new Updater();
try {
$result = $updater->rollback();
if (!$result) {
echo "Failure!\n";
exit(1);
}
echo "Success!\n";
} catch (\Exception $e) {
echo "Well, something happened! Either an oopsie or something involving hackers.\n";
exit(1);
}
As users may have diverse requirements in naming and locating backups, you can explicitly manage the precise path to
where a backup should be written, or read from using the setBackupPath() function when updating a current phar or the
setRestorePath() prior to triggering a rollback. These will be used instead of the simple built in convention.
The SHA strategies check that the downloaded phar matches the hash in the version file, but that file is usually hosted alongside the phar, so anyone able to replace one can replace the other. To also verify who built the phar, you can sign each release with GPG, as recommended by Box, and have the updater verify the signature before installing an update.
Sign the phar with a detached, ASCII-armored signature, and publish it next to the phar with .asc appended:
gpg --local-user <key-id> --armor --detach-sign myapp.phar # Creates myapp.phar.asc
Then bundle your public key with your application (for example, in a file included in the phar), and pass it, along
with the full fingerprint of the signing key, to a GpgvVerifier:
use Humbug\SelfUpdate\Signature\GpgvVerifier;
use Humbug\SelfUpdate\Updater;
$updater = new Updater();
$updater->getStrategy()->setPharUrl('https://example.com/myapp.phar');
$updater->getStrategy()->setVersionUrl('https://example.com/myapp.version');
$updater->setSignatureVerifier(GpgvVerifier::fromFile(
__DIR__.'/../resources/signing-key.asc',
'0123 4567 89AB CDEF 0123 4567 89AB CDEF 0123 4567' // The full fingerprint, from `gpg --fingerprint`
));
When a signature verifier is set, an update is only installed if the signature is valid and was made by the key with
the given fingerprint. If the signature is missing, invalid or made by any other key, a SignatureVerificationException
or HttpRequestException is thrown and the current phar is left in place.
The signature is downloaded from:
.asc appended, or the URL passed to setSignatureUrl()..asc appended, e.g. myapp.phar.asc in the same release..asc appended, which can be changed by overriding getSignatureUrl().Custom strategies can support signatures by implementing Humbug\SelfUpdate\Strategy\SignedStrategyInterface.
A failed verification can mean two very different things, so it's worth handling them separately in your self-update command:
GpgvUnavailableException (a subclass of SignatureVerificationException) means GnuPG isn't installed, so the update
couldn't be checked at all. Nothing suggests the update has been tampered with, so you may want to offer an explicit,
per-run opt-out, such as a --skip-signature-check option, which skips calling setSignatureVerifier().SignatureVerificationException means the signature is invalid or was made by the wrong key. This is
exactly what a tampered release looks like, so don't offer a way to continue (especially not a "continue anyway?"
prompt). Instead, tell the user their current version is unchanged and point them to where they can download a release
manually.A missing signature file is reported as an HttpRequestException, like any other failed download.
use Humbug\SelfUpdate\Exception\GpgvUnavailableException;
use Humbug\SelfUpdate\Exception\SignatureVerificationException;
use Humbug\SelfUpdate\Signature\GpgvVerifier;
use Humbug\SelfUpdate\Updater;
$updater = new Updater();
$updater->getStrategy()->setPharUrl('https://example.com/myapp.phar');
$updater->getStrategy()->setVersionUrl('https://example.com/myapp.version');
if (! in_array('--skip-signature-check', $argv, true)) {
$updater->setSignatureVerifier(GpgvVerifier::fromFile($publicKeyFile, $fingerprint));
}
try {
$result = $updater->update();
echo $result ? "Updated!\n" : "No update needed!\n";
} catch (GpgvUnavailableException $e) {
echo "GnuPG (gpgv) is not installed, so this update cannot be verified.\n"
."Install GnuPG, or re-run with --skip-signature-check to update without verification.\n";
exit(1);
} catch (SignatureVerificationException $e) {
echo "The update could not be verified and was not installed. Your current version is unchanged.\n"
."If this persists, download the latest release manually from https://example.com/myapp.\n";
exit(1);
}
Please note:
gpgv tool, so GnuPG must be installed wherever your application is updated. Signatures
are only checked against the public key you provide, never against the user's own keyring.gpgv treats the provided key as trusted, and does not check whether it has expired or been revoked. If you need to
change your signing key, ship the new key in a release signed by the old key.The Updater constructor is fairly simple. The three basic variations are:
/**
* Default: Update currently running phar.
*/
$updater = new Updater;
/**
* Use a strategy other than the default SHA Hash.
*/
$updater = new Updater(null, Updater::STRATEGY_GITHUB);
/**
* Update a different phar.
*/
$updater = new Updater('/path/to/impersonatephil.phar');
You can tell users what updates are available, across any stability track, by making use of the hasUpdate method. This
gets the most recent remote version for a stability level, compares it to the current version, and returns a simple
true/false result, i.e. it will only be false where the local version is identical or where there was no remote version
for that stability level at all. You can easily differentiate between the two false states as the new version will be a
string where a version did exist, but false if not.
use Humbug\SelfUpdate\Updater;
/**
* Configuration is identical in every way for actual updates. You can run this
* across multiple configuration variants to get recent stable, unstable, and dev
* versions available.
*/
$updater = new Updater();
$updater->setStrategy(Updater::STRATEGY_GITHUB);
$updater->getStrategy()->setPackageName('myvendor/myapp');
$updater->getStrategy()->setPharName('myapp.phar');
$updater->getStrategy()->setCurrentLocalVersion('v1.0.1');
try {
$result = $updater->hasUpdate();
if ($result) {
printf(
'The current stable build available remotely is: %s',
$updater->getNewVersion()
);
} elseif (false === $updater->getNewVersion()) {
echo "There are no stable builds available.\n";
} else {
echo "You have the current stable build installed.\n";
}
} catch (\Exception $e) {
echo "Well, something happened! Either an oopsie or something involving hackers.\n";
exit(1);
}
Updating a currently running phar is made trickier since, once replaced, attempts to load files from it within a process
originating from an older phar is likely to create an internal corruption of phar error. For example, if you're using
Symfony Console and have created an event dispatcher for your commands, the lazy loading of some event classes will have
this impact.
The solution is to disable or remove the dispatcher for your self-update command.
In general, when writing your self-update CLI commands, either pre-load any classes likely needed prior to updating, or disable their loading if not essential.
All update strategies revolve around checking for updates, and downloading updates. The actual work behind replacing
local files and backups is handled separately. To create a custom strategy, you can
implement Humbug\SelfUpdate\Strategy\StrategyInterface
and pass a new instance of your implementation post-construction.
$updater = new Updater();
$updater->setStrategyObject(new MyStrategy);
The similar setStrategy() method is solely used to pass flags matching internal strategies.
The phar-updater package supports an update strategy where phars are updated according to the SHA-256 or SHA-512 hash of the current PHAR file available remotely. This assumes the existence of only two remote files:
The myname.phar is the most recently built phar.
The myname.version contains the SHA-256 or SHA-512 hash of the most recently built phar where the hash is the very first
string (if not the only string). You can generate this quite easily from bash using:
# For SHA-256
sha256sum myname.phar > myname.version
# For SHA-512
sha512sum myname.phar > myname.version
Remember to regenerate the version file for each new phar build you want to distribute. Using sha256sum/sha512sum
adds additional data after the hash, but it's fine since the hash is the first string in the file which is the only
requirement.
When tagging new versions on Github, these are created and hosted as Github Releases
which allow you to attach a changelog and additional file downloads. Using this Github feature allows you to attach new
phars to releases, associating them with a version string that is published on Packagist.
Taking advantage of this architecture, the Github Strategy for updating phars can compare the existing local phar's version against remote release versions and update to the most recent stable (or unstable) version from Github.
At present, it's assume that phar files all bear the same name across releases, i.e. just a plain name like myapp.phar
without versioning information in the file name.
You can read more about Github releases here.
While you can draft a release, Github releases are created automatically whenever you create a new git tag. If you use
git tagging, you can go to the matching release on Github, click the Edit button and attach files. It's recommended to
do this as soon as possible after tagging to limit the window whereby a new release exists without an updated phar
attached.
PHAR Updater provides an abstract Humbug\SelfUpdate\Strategy\DirectDownloadStrategyAbstract class
which can be used to quickly and easily create download strategies with just a getDownloadUrl(): string method.
For example, if a PHAR downloads it's latest updates from https://example.com/latest/example.phar, you can utilise this
with the following code:
use Humbug\SelfUpdate\Strategy\DirectDownloadStrategyAbstract;
class ExampleDirectDownloadStrategy extends DirectDownloadStrategyAbstract
{
public function getDownloadUrl(): string
{
return 'https://example.com/latest/example.phar';
}
}
The abstract strategy also supports overriding the getCurrentRemoteVersion() method, so that you could add a custom
HTTP call or other method for seeing what the latest version is. By default, it returns the string latest.
use Illuminate\Support\Facades\Http;
use Humbug\SelfUpdate\Strategy\DirectDownloadStrategyAbstract;
class ExampleDirectDownloadStrategy extends DirectDownloadStrategyAbstract
{
/** {@inheritdoc} */
public function getCurrentRemoteVersion(Updater $updater)
{
return Http::get('https://example.com/example-releases.json')->object()->latest_version;
}
public function getDownloadUrl(): string
{
return "https://example.com/{$this->getCurrentRemoteVersion()}/example.phar";
}
}
You can also set and retrieve the current local version using the setCurrentLocalVersion() and getCurrentLocalVersion()
methods, which will be used for comparison with the remote version.
Please see GitHub Releases for more information on what has changed recently.