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

feat: composer cleanup task #1877

Merged
merged 15 commits into from
Jul 23, 2020
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ composer installed.
Once composer is installed, execute the following command in your project root to install this library:

```sh
composer require google/apiclient:"^2.0"
composer require google/apiclient:"^2.7"
```

Finally, be sure to include the autoloader:
Expand All @@ -43,6 +43,59 @@ require_once '/path/to/your-project/vendor/autoload.php';

This library relies on `google/apiclient-services`. That library provides up-to-date API wrappers for a large number of Google APIs. In order that users may make use of the latest API clients, this library does not pin to a specific version of `google/apiclient-services`. **In order to prevent the accidental installation of API wrappers with breaking changes**, it is highly recommended that you pin to the [latest version](https://github.com/googleapis/google-api-php-client-services/releases) yourself prior to using this library in production.

#### Cleaning up unused services

There are over 200 Google API services. The chances are good that you will not
want them all. In order to avoid shipping these dependencies with your code,
you can run the `Google_Task_Composer::cleanup` task and specify the services
you want to keep in `composer.json`:

```json
{
"require": {
"google/apiclient": "^2.7"
},
"scripts": {
"post-update-cmd": "Google_Task_Composer::cleanup"
},
"extra": {
"google/apiclient-services": [
"Drive",
"YouTube"
]
}
}
```

This example will remove all services other than "Drive" and "YouTube" when
`composer update` or a fresh `composer install` is run.

**IMPORTANT**: If you add any services back in `composer.json`, you will need to
remove the `vendor/google/apiclient-services` directory explicity for the
change you made to have effect:

```sh
rm -r vendor/google/apiclient-services
composer update
```

**NOTE**: This command performs an exact match on the service name, so to keep
`YouTubeReporting` and `YouTubeAnalytics` as well, you'd need to add each of
them explicitly:

```json
{
"extra": {
"google/apiclient-services": [
"Drive",
"YouTube",
"YouTubeAnalytics",
"YouTubeReporting"
]
}
}
```

### Download the Release

If you prefer not to use composer, you can download the package in its entirety. The [Releases](https://github.com/googleapis/google-api-php-client/releases) page lists all stable versions. Download any file
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"symfony/css-selector": "~2.1",
"cache/filesystem-adapter": "^0.3.2",
"phpcompatibility/php-compatibility": "^9.2",
"dealerdirect/phpcodesniffer-composer-installer": "^0.5.0"
"dealerdirect/phpcodesniffer-composer-installer": "^0.5.0",
"composer/composer": "^1.10"
},
"suggest": {
"cache/filesystem-adapter": "For caching certs and tokens (using Google_Client::setCache)"
Expand Down
104 changes: 104 additions & 0 deletions src/Google/Task/Composer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

use Composer\Script\Event;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;

class Google_Task_Composer
{
/**
* @param Event $event Composer event passed in for any script method
* @param FilesystemInterface $filesystem Optional. Used for testing.
*/
public static function cleanup(
Event $event,
Filesystem $filesystem = null
) {
$composer = $event->getComposer();
$extra = $composer->getPackage()->getExtra();
$servicesToKeep = isset($extra['google/apiclient-services']) ?
$extra['google/apiclient-services'] : [];
if ($servicesToKeep) {
$serviceDir = sprintf(
'%s/google/apiclient-services/src/Google/Service',
$composer->getConfig()->get('vendor-dir')
);
self::verifyServicesToKeep($serviceDir, $servicesToKeep);
$finder = self::getServicesToRemove($serviceDir, $servicesToKeep);
$filesystem = $filesystem ?: new Filesystem();
if (0 !== $count = count($finder)) {
$event->getIO()->write(
sprintf(
'Removing %s google services',
$count
)
);
foreach ($finder as $file) {
$realpath = $file->getRealPath();
$filesystem->remove($realpath);
$filesystem->remove($realpath . '.php');
}
}
}
}

/**
* @throws InvalidArgumentException when the service doesn't exist
*/
private static function verifyServicesToKeep(
$serviceDir,
array $servicesToKeep
) {
$finder = (new Finder())
->directories()
->depth('== 0');

foreach ($servicesToKeep as $service) {
if (!preg_match('/^[a-zA-Z0-9]*$/', $service)) {
throw new \InvalidArgumentException(
sprintf(
'Invalid Google service name "%s"',
$service
)
);
}
try {
$finder->in($serviceDir . '/' . $service);
} catch (\InvalidArgumentException $e) {
throw new \InvalidArgumentException(
sprintf(
'Google service "%s" does not exist or was removed previously',
$service
)
);
}
}
}

private static function getServicesToRemove(
$serviceDir,
array $servicesToKeep
) {
// find all files in the current directory
return (new Finder())
->directories()
->depth('== 0')
->in($serviceDir)
->exclude($servicesToKeep);
}
}
206 changes: 206 additions & 0 deletions tests/Google/Task/ComposerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<?php
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

class Google_Task_ComposerTest extends BaseTest
{
/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessage Google service "Foo" does not exist
*/
public function testInvalidServiceName()
{
Google_Task_Composer::cleanup($this->createMockEvent(['Foo']));
}

/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessage Invalid Google service name "../YouTube"
*/
public function testRelatePathServiceName()
{
Google_Task_Composer::cleanup($this->createMockEvent(['../YouTube']));
}

/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessage Google service "" does not exist
*/
public function testEmptyServiceName()
{
Google_Task_Composer::cleanup($this->createMockEvent(['']));
}

/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessage Invalid Google service name "YouTube*"
*/
public function testWildcardServiceName()
{
Google_Task_Composer::cleanup($this->createMockEvent(['YouTube*']));
}

public function testRemoveServices()
{
$vendorDir = sys_get_temp_dir() . '/rand-' . rand();
$serviceDir = sprintf(
'%s/google/apiclient-services/src/Google/Service/',
$vendorDir
);
$dirs = [
'ServiceToKeep',
'ServiceToDelete1',
'ServiceToDelete2',
];
$files = [
'ServiceToKeep/ServiceFoo.php',
'ServiceToKeep.php',
'SomeRandomFile.txt',
'ServiceToDelete1/ServiceFoo.php',
'ServiceToDelete1.php',
'ServiceToDelete2/ServiceFoo.php',
'ServiceToDelete2.php',
];
foreach ($dirs as $dir) {
@mkdir($serviceDir . $dir, 0777, true);
}
foreach ($files as $file) {
touch($serviceDir . $file);
}
$print = 'Removing 2 google services';
Google_Task_Composer::cleanup(
$this->createMockEvent(['ServiceToKeep'], $vendorDir, $print),
$this->createMockFilesystem([
'ServiceToDelete2',
'ServiceToDelete2.php',
'ServiceToDelete1',
'ServiceToDelete1.php',
], $serviceDir)
);
}

private function createMockFilesystem(array $files, $serviceDir)
{
$mockFilesystem = $this->prophesize('Symfony\Component\Filesystem\Filesystem');
foreach ($files as $filename) {
$file = new \SplFileInfo($serviceDir . $filename);
$mockFilesystem->remove($file->getRealPath())
->shouldBeCalledTimes(1);
}

return $mockFilesystem->reveal();
}

private function createMockEvent(
array $servicesToKeep,
$vendorDir = '',
$print = null
) {
$mockPackage = $this->prophesize('Composer\Package\RootPackage');
$mockPackage->getExtra()
->shouldBeCalledTimes(1)
->willReturn(['google/apiclient-services' => $servicesToKeep]);

$mockConfig = $this->prophesize('Composer\Config');
$mockConfig->get('vendor-dir')
->shouldBeCalledTimes(1)
->willReturn($vendorDir);

$mockComposer = $this->prophesize('Composer\Composer');
$mockComposer->getPackage()
->shouldBeCalledTimes(1)
->willReturn($mockPackage->reveal());
$mockComposer->getConfig()
->shouldBeCalledTimes(1)
->willReturn($mockConfig->reveal());

$mockEvent = $this->prophesize('Composer\Script\Event');
$mockEvent->getComposer()
->shouldBeCalledTimes(1)
->willReturn($mockComposer);

if ($print) {
$mockIO = $this->prophesize('Composer\IO\ConsoleIO');
$mockIO->write($print)
->shouldBeCalledTimes(1);

$mockEvent->getIO()
->shouldBeCalledTimes(1)
->willReturn($mockIO->reveal());
}

return $mockEvent->reveal();
}

public function testE2E()
{
$composerJson = json_encode([
'require' => [
'google/apiclient' => 'dev-add-composer-cleanup'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We will need to change this before tagging a new version!

],
'scripts' => [
'post-update-cmd' => 'Google_Task_Composer::cleanup'
],
'extra' => [
'google/apiclient-services' => [
'Drive',
'YouTube'
]
]
]);

$tmpDir = sys_get_temp_dir() . '/test-' . rand();
$serviceDir = $tmpDir . '/vendor/google/apiclient-services/src/Google/Service';

mkdir($tmpDir);
file_put_contents($tmpDir . '/composer.json', $composerJson);
passthru('composer install -d ' . $tmpDir);

$this->assertFileExists($serviceDir . '/Drive.php');
$this->assertFileExists($serviceDir . '/Drive');
$this->assertFileExists($serviceDir . '/YouTube.php');
$this->assertFileExists($serviceDir . '/YouTube');
$this->assertFileNotExists($serviceDir . '/YouTubeReporting.php');
$this->assertFileNotExists($serviceDir . '/YouTubeReporting');

$composerJson = json_encode([
'require' => [
'google/apiclient' => 'dev-add-composer-cleanup'
],
'scripts' => [
'post-update-cmd' => 'Google_Task_Composer::cleanup'
],
'extra' => [
'google/apiclient-services' => [
'Drive',
'YouTube',
'YouTubeReporting',
]
]
]);

file_put_contents($tmpDir . '/composer.json', $composerJson);
passthru('rm -r ' . $tmpDir . '/vendor/google/apiclient-services');
passthru('composer update -d ' . $tmpDir);

$this->assertFileExists($serviceDir . '/Drive.php');
$this->assertFileExists($serviceDir . '/Drive');
$this->assertFileExists($serviceDir . '/YouTube.php');
$this->assertFileExists($serviceDir . '/YouTube');
$this->assertFileExists($serviceDir . '/YouTubeReporting.php');
$this->assertFileExists($serviceDir . '/YouTubeReporting');
}
}