Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow dumping requirements from the composer.json file #203

Merged
merged 2 commits into from
May 4, 2018
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ if you are using a PHAR of an application compatible with PHP 7.2 in PHP 7.0 or
required extension, it will simply break with a non-friendly error.

By default, when building your PHAR with Box, Box will look up for the PHP versions and extensions required to execute your
application according to your `composer.json` and `composer.lock` files and ship a micro
[requirements checker][check-requirements] which will be executed when starting your PHAR.
application according to your `composer.json` and `composer.lock` files and ship a micro (>300KB uncompressed and >40KB
compressed) [requirements checker][check-requirements] which will be executed when starting your PHAR.

The following are screenshots of the output when an error occurs (left) in a non-quiet verbosity and when all requirements
are passing on the right in debug verbosity.
Expand Down
11 changes: 9 additions & 2 deletions doc/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,15 @@ When running the PHAR, before running the actual application, the PHAR will chec
that the extension `iconv` is loaded. If those requirements are not met, then a user friendly error message will be given
to the user.

This check will work for PHP 5.3+ and requires the existence of the `composer.lock` file. If no `composer.lock` file is found
then the requirements check will not be added to the PHAR.
This check will work for PHP 5.3+ and requires the existence of the `composer.json` or `composer.lock` file. If neither of
those files are found then the requirements check will not be added to the PHAR.

Be wary that when a `composer.lock` file is found, it will be taken as the source of truth for establishing the requirements
regardless of the content of the `composer.json`.

If a `composer.json` is found without a `composer.lock`, the it will be taken as the source of truth for establishing the
requirements regardless of whether there is no `composer.lock` because there is no dependencies or because it has been
removed.


## Including files
Expand Down
5 changes: 3 additions & 2 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ public static function create(?string $file, stdClass $raw): self

$checkRequirements = self::retrieveCheckRequirements(
$raw,
null !== $composerJson[0],
null !== $composerLock[0],
$isStubGenerated
);
Expand Down Expand Up @@ -1768,11 +1769,11 @@ private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath
return null === $stubPath && (false === isset($raw->stub) || false !== $raw->stub);
}

private static function retrieveCheckRequirements(stdClass $raw, bool $hasComposerLock, bool $generateStub): bool
private static function retrieveCheckRequirements(stdClass $raw, bool $hasComposerJson, bool $hasComposerLock, bool $generateStub): bool
{
// TODO: emit warning when stub is not generated and check requirements is explicitly set to true
// TODO: emit warning when no composer lock is found but check requirements is explicitely set to true
if (false === $hasComposerLock) {
if (false === $hasComposerJson && false === $hasComposerLock) {
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Console/Command/Build.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use const E_USER_DEPRECATED;
use function trigger_error;
use const E_USER_DEPRECATED;

/**
* @deprecated
Expand Down
9 changes: 5 additions & 4 deletions src/Console/Command/Compile.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
use const DATE_ATOM;
use const POSIX_RLIMIT_INFINITY;
use const POSIX_RLIMIT_NOFILE;
use function array_shift;
use function count;
use function decoct;
Expand All @@ -63,6 +60,9 @@
use function strtolower;
use function substr;
use function trim;
use const DATE_ATOM;
use const POSIX_RLIMIT_INFINITY;
use const POSIX_RLIMIT_NOFILE;

/**
* @final
Expand Down Expand Up @@ -498,7 +498,8 @@ private function registerRequirementsChecker(Configuration $config, Box $box, Bu
);

$checkFiles = RequirementsDumper::dump(
$config->getComposerLockDecodedContents(),
$config->getComposerJsonDecodedContents() ?? [],
$config->getComposerLockDecodedContents() ?? [],
null !== $config->getCompressionAlgorithm()
);

Expand Down
122 changes: 96 additions & 26 deletions src/RequirementChecker/AppRequirementsFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,39 +29,62 @@ final class AppRequirementsFactory
private const SELF_PACKAGE = '__APPLICATION__';

/**
* @param array $composerJsonDecodedContents Decoded JSON contents of the `composer.json` file
* @param array $composerLockDecodedContents Decoded JSON contents of the `composer.lock` file
*
* @return array Serialized configured requirements
*/
public static function create(array $composerLockDecodedContents, bool $compressed): array
public static function create(array $composerJsonDecodedContents, array $composerLockDecodedContents, bool $compressed): array
{
return self::configureExtensionRequirements(
self::configurePhpVersionRequirements([], $composerLockDecodedContents),
self::retrievePhpVersionRequirements([], $composerJsonDecodedContents, $composerLockDecodedContents),
$composerJsonDecodedContents,
$composerLockDecodedContents,
$compressed
);
}

private static function configurePhpVersionRequirements(array $requirements, array $composerLockContents): array
{
if (isset($composerLockContents['platform']['php'])) {
$requiredPhpVersion = $composerLockContents['platform']['php'];
private static function retrievePhpVersionRequirements(
array $requirements,
array $composerJsonContents,
array $composerLockContents
): array {
if (([] === $composerLockContents && isset($composerJsonContents['require']['php']))
|| isset($composerLockContents['platform']['php'])
) {
// No need to check the packages requirements: the application platform config is the authority here
return self::retrievePlatformPhpRequirement($requirements, $composerJsonContents, $composerLockContents);
}

$requirements[] = [
self::generatePhpCheckStatement((string) $requiredPhpVersion),
sprintf(
'The application requires the version "%s" or greater.',
$requiredPhpVersion
),
sprintf(
'The application requires the version "%s" or greater.',
$requiredPhpVersion
),
];
return self::retrievePackagesPhpRequirement($requirements, $composerLockContents);
}

return $requirements; // No need to check the packages requirements: the application platform config is the authority here
}
private static function retrievePlatformPhpRequirement(
array $requirements,
array $composerJsonContents,
array $composerLockContents
): array {
$requiredPhpVersion = [] === $composerLockContents
? $composerJsonContents['require']['php']
: $composerLockContents['platform']['php'];

$requirements[] = [
self::generatePhpCheckStatement((string) $requiredPhpVersion),
sprintf(
'The application requires the version "%s" or greater.',
$requiredPhpVersion
),
sprintf(
'The application requires the version "%s" or greater.',
$requiredPhpVersion
),
];

return $requirements;
}

private static function retrievePackagesPhpRequirement(array $requirements, array $composerLockContents): array
{
$packages = $composerLockContents['packages'] ?? [];

foreach ($packages as $packageInfo) {
Expand Down Expand Up @@ -89,9 +112,13 @@ private static function configurePhpVersionRequirements(array $requirements, arr
return $requirements;
}

private static function configureExtensionRequirements(array $requirements, array $composerLockContents, bool $compressed): array
{
$extensionRequirements = self::collectExtensionRequirements($composerLockContents, $compressed);
private static function configureExtensionRequirements(
array $requirements,
array $composerJsonContents,
array $composerLockContents,
bool $compressed
): array {
$extensionRequirements = self::collectExtensionRequirements($composerJsonContents, $composerLockContents, $compressed);

foreach ($extensionRequirements as $extension => $packages) {
foreach ($packages as $package) {
Expand Down Expand Up @@ -132,11 +159,9 @@ private static function configureExtensionRequirements(array $requirements, arra
* Collects the extension required. It also accounts for the polyfills, i.e. if the polyfill `symfony/polyfill-mbstring` is provided
* then the extension `ext-mbstring` will not be required.
*
* @param array $composerLockContents
*
* @return array Associative array containing the list of extensions required
*/
private static function collectExtensionRequirements(array $composerLockContents, bool $compressed): array
private static function collectExtensionRequirements(array $composerJsonContents, array $composerLockContents, bool $compressed): array
{
$requirements = [];
$polyfills = [];
Expand All @@ -155,6 +180,51 @@ private static function collectExtensionRequirements(array $composerLockContents
}
}

[$polyfills, $requirements] = [] === $composerLockContents
? self::collectComposerJsonExtensionRequirements($composerJsonContents, $polyfills, $requirements)
: self::collectComposerLockExtensionRequirements($composerLockContents, $polyfills, $requirements)
;

return array_diff_key($requirements, $polyfills);
}

private static function collectComposerJsonExtensionRequirements(array $composerJsonContents, $polyfills, $requirements): array
{
$packages = $composerJsonContents['require'] ?? [];

foreach ($packages as $packageName => $constraint) {
if (1 === preg_match('/symfony\/polyfill-(?<extension>.+)/', $packageName, $matches)) {
$extension = $matches['extension'];

if ('php' !== substr($extension, 0, 3)) {
$polyfills[$extension] = true;

continue;
}
}

if ('paragonie/sodium_compat' === $packageName) {
$polyfills['libsodium'] = true;

continue;
}

if ('phpseclib/mcrypt_compat' === $packageName) {
$polyfills['mcrypt'] = true;

continue;
}

if ('php' !== $packageName && preg_match('/^ext-(?<extension>.+)$/', $packageName, $matches)) {
$requirements[$matches['extension']] = [self::SELF_PACKAGE];
}
}

return [$polyfills, $requirements];
}

private static function collectComposerLockExtensionRequirements(array $composerLockContents, $polyfills, $requirements): array
{
$packages = $composerLockContents['packages'] ?? [];

foreach ($packages as $packageInfo) {
Expand Down Expand Up @@ -189,7 +259,7 @@ private static function collectExtensionRequirements(array $composerLockContents
}
}

return array_diff_key($requirements, $polyfills);
return [$polyfills, $requirements];
}

private static function generatePhpCheckStatement(string $requiredPhpVersion): string
Expand Down
13 changes: 8 additions & 5 deletions src/RequirementChecker/RequirementsDumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ final class RequirementsDumper
/**
* @return string[][]
*/
public static function dump(array $composerLockDecodedContents, bool $compressed): array
public static function dump(array $composerJsonDecodedContents, array $composerLockDecodedContents, bool $compressed): array
{
$filesWithContents = [
self::dumpRequirementsConfig($composerLockDecodedContents, $compressed),
self::dumpRequirementsConfig($composerJsonDecodedContents, $composerLockDecodedContents, $compressed),
[self::CHECK_FILE_NAME, self::REQUIREMENTS_CHECKER_TEMPLATE],
];

Expand All @@ -80,9 +80,12 @@ public static function dump(array $composerLockDecodedContents, bool $compressed
return $filesWithContents;
}

private static function dumpRequirementsConfig(array $composerLockDecodedContents, bool $compressed): array
{
$config = AppRequirementsFactory::create($composerLockDecodedContents, $compressed);
private static function dumpRequirementsConfig(
array $composerJsonDecodedContents,
array $composerLockDecodedContents,
bool $compressed
): array {
$config = AppRequirementsFactory::create($composerJsonDecodedContents, $composerLockDecodedContents, $compressed);

return [
'.requirements.php',
Expand Down
24 changes: 17 additions & 7 deletions tests/ConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use const DIRECTORY_SEPARATOR;
use function file_put_contents;
use function KevinGH\Box\FileSystem\dump_file;
use function KevinGH\Box\FileSystem\remove;
use function KevinGH\Box\FileSystem\rename;

/**
Expand Down Expand Up @@ -288,7 +289,7 @@ public function test_it_throws_an_error_when_a_composer_lock_is_found_but_invali
}
}

public function test_the_autoloader_is_dumped_by_default_if_a_composer_json_file_is_found()
public function test_the_autoloader_is_dumped_by_default_if_a_composer_json_file_is_found(): void
{
$this->assertFalse($this->config->dumpAutoload());
$this->assertFalse($this->getNoFileConfig()->dumpAutoload());
Expand All @@ -309,7 +310,7 @@ public function test_the_autoloader_is_dumped_by_default_if_a_composer_json_file
$this->assertTrue($this->config->dumpAutoload());
}

public function test_the_autoloader_is_can_be_configured()
public function test_the_autoloader_is_can_be_configured(): void
{
file_put_contents('composer.json', '{}');

Expand All @@ -328,7 +329,7 @@ public function test_the_autoloader_is_can_be_configured()
$this->assertTrue($this->getNoFileConfig()->dumpAutoload());
}

public function test_the_autoloader_cannot_be_dumped_if_no_composer_json_file_is_found()
public function test_the_autoloader_cannot_be_dumped_if_no_composer_json_file_is_found(): void
{
$this->setConfig([
'dump-autoload' => true,
Expand Down Expand Up @@ -1232,18 +1233,27 @@ public function testIsPrivateKeyPromptSetString(): void
$this->assertFalse($this->config->isPrivateKeyPrompt());
}

public function test_the_requirement_checker_is_enabled_by_default(): void
public function test_the_requirement_checker_is_enabled_by_default_if_a_composer_lock_or_json_file_is_found(): void
{
$this->assertFalse($this->config->checkRequirements());
}

public function test_the_requirement_checker_is_enabled_by_default_if_a_composer_lock_file_is_found(): void
{
file_put_contents('composer.lock', '{}');

$this->reloadConfig();

$this->assertTrue($this->config->checkRequirements());

file_put_contents('composer.json', '{}');

$this->reloadConfig();

$this->assertTrue($this->config->checkRequirements());

remove('composer.lock');

$this->reloadConfig();

$this->assertTrue($this->config->checkRequirements());
}

public function test_the_requirement_checker_can_be_disabled(): void
Expand Down
Loading