diff --git a/doc/04-schema.md b/doc/04-schema.md index a222c06e0..f7de1a3df 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -656,4 +656,29 @@ See [Vendor Binaries](articles/vendor-binaries.md) for more details. Optional. +### archive + +A set of options for creating package archives. + +The following options are supported: + +* **exclude:** Allows configuring a list of patterns for excluded paths. The + pattern syntax matches .gitignore files. A leading exclamation mark (!) will + result in any matching files to be included even if a previous pattern + excluded them. A leading slash will only match at the beginning of the project + relative path. An asterisk will not expand to a directory separator. + +Example: + + { + "archive": { + "exclude": ["/foo/bar", "baz", "/*.test", "!/foo/bar/baz"] + } + } + +The example will include `/dir/foo/bar/file`, `/foo/bar/baz`, `/file.php`, +`/foo/my.test` but it will exclude `/foo/bar/any`, `/foo/baz`, and `/my.test`. + +Optional. + ← [Command-line interface](03-cli.md) | [Repositories](05-repositories.md) → diff --git a/res/composer-schema.json b/res/composer-schema.json index aefaa0463..3eb22cff0 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -202,6 +202,20 @@ } } }, + "archive": { + "type": ["object"], + "description": "Options for creating package archives for distribution.", + "properties": { + "exclude": { + "type": "array", + "description": "A list of paths to exclude." + }, + "include": { + "type": "array", + "description": "A list of paths to include even though an exclude rule exists for them." + } + } + }, "repositories": { "type": ["object", "array"], "description": "A set of additional repositories where packages can be found.", diff --git a/src/Composer/Package/AliasPackage.php b/src/Composer/Package/AliasPackage.php index e2f748092..7f16aaac9 100644 --- a/src/Composer/Package/AliasPackage.php +++ b/src/Composer/Package/AliasPackage.php @@ -311,6 +311,10 @@ class AliasPackage extends BasePackage implements CompletePackageInterface { return $this->aliasOf->getNotificationUrl(); } + public function getArchiveExcludes() + { + return $this->aliasOf->getArchiveExcludes(); + } public function __toString() { return parent::__toString().' (alias of '.$this->aliasOf->getVersion().')'; diff --git a/src/Composer/Package/Archiver/ArchiveManager.php b/src/Composer/Package/Archiver/ArchiveManager.php index 5da4d89ae..c3a2c818e 100644 --- a/src/Composer/Package/Archiver/ArchiveManager.php +++ b/src/Composer/Package/Archiver/ArchiveManager.php @@ -89,6 +89,6 @@ class ArchiveManager // Create the archive $sourceRef = $package->getSourceReference(); - $usableArchiver->archive($sourcePath, $target, $format, $sourceRef); + $usableArchiver->archive($sourcePath, $target, $format, $sourceRef, $package->getArchiveExcludes()); } } diff --git a/src/Composer/Package/Archiver/ArchiverInterface.php b/src/Composer/Package/Archiver/ArchiverInterface.php index 38eb2cdd3..7a688d251 100644 --- a/src/Composer/Package/Archiver/ArchiverInterface.php +++ b/src/Composer/Package/Archiver/ArchiverInterface.php @@ -28,8 +28,9 @@ interface ArchiverInterface * @param string $format The format used for archive * @param string $sourceRef The reference of the source to archive or null * for the current reference + * @param array $excludes A list of patterns for files to exclude */ - public function archive($sources, $target, $format, $sourceRef = null); + public function archive($sources, $target, $format, $sourceRef = null, $excludes = array()); /** * Format supported by the archiver. diff --git a/src/Composer/Package/Archiver/PharArchiver.php b/src/Composer/Package/Archiver/PharArchiver.php index 24e376d2a..950832ce2 100644 --- a/src/Composer/Package/Archiver/PharArchiver.php +++ b/src/Composer/Package/Archiver/PharArchiver.php @@ -15,8 +15,11 @@ namespace Composer\Package\Archiver; use Composer\Package\BasePackage; use Composer\Package\PackageInterface; +use Symfony\Component\Finder; + /** * @author Till Klampaeckel + * @author Nils Adermann * @author Matthieu Moquet */ class PharArchiver implements ArchiverInterface @@ -29,11 +32,34 @@ class PharArchiver implements ArchiverInterface /** * {@inheritdoc} */ - public function archive($sources, $target, $format, $sourceRef = null) + public function archive($sources, $target, $format, $sourceRef = null, $excludes = array()) { + $sources = realpath($sources); + + $excludePatterns = $this->generatePatterns($excludes); + try { + if (file_exists($target)) { + unlink($target); + } $phar = new \PharData($target, null, null, static::$formats[$format]); - $phar->buildFromDirectory($sources); + $finder = new Finder\Finder(); + $finder + ->in($sources) + ->filter(function (\SplFileInfo $file) use ($sources, $excludePatterns) { + $relativePath = preg_replace('#^'.preg_quote($sources, '#').'#', '', $file->getRealPath()); + + $include = true; + foreach ($excludePatterns as $patternData) { + list($pattern, $negate) = $patternData; + if (preg_match($pattern, $relativePath)) { + $include = $negate; + } + } + return $include; + }) + ->ignoreVCS(true); + $phar->buildFromIterator($finder->getIterator(), $sources); } catch (\UnexpectedValueException $e) { $message = sprintf("Could not create archive '%s' from '%s': %s", $target, @@ -45,6 +71,35 @@ class PharArchiver implements ArchiverInterface } } + /** + * Generates a set of PCRE patterns from a set of exclude rules. + * + * @param array $rules A list of exclude rules similar to gitignore syntax + */ + protected function generatePatterns($rules) + { + $patterns = array(); + foreach ($rules as $rule) { + $negate = false; + $pattern = '#'; + + if (strlen($rule) && $rule[0] === '!') { + $negate = true; + $rule = substr($rule, 1); + } + + if (strlen($rule) && $rule[0] === '/') { + $pattern .= '^/'; + $rule = substr($rule, 1); + } + + $pattern .= substr(Finder\Glob::toRegex($rule), 2, -2); + $patterns[] = array($pattern . '#', $negate); + } + + return $patterns; + } + /** * {@inheritdoc} */ diff --git a/src/Composer/Package/Dumper/ArrayDumper.php b/src/Composer/Package/Dumper/ArrayDumper.php index 22d62381b..bca932d71 100644 --- a/src/Composer/Package/Dumper/ArrayDumper.php +++ b/src/Composer/Package/Dumper/ArrayDumper.php @@ -58,6 +58,10 @@ class ArrayDumper $data['dist']['shasum'] = $package->getDistSha1Checksum(); } + if ($package->getArchiveExcludes()) { + $data['archive']['exclude'] = $package->getArchiveExcludes(); + } + foreach (BasePackage::$supportedLinkTypes as $type => $opts) { if ($links = $package->{'get'.ucfirst($opts['method'])}()) { foreach ($links as $link) { diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index 559fd86fe..ffb0aa126 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -150,6 +150,10 @@ class ArrayLoader implements LoaderInterface $package->setNotificationUrl($config['notification-url']); } + if (!empty($config['archive']['exclude'])) { + $package->setArchiveExcludes($config['archive']['exclude']); + } + if ($package instanceof Package\CompletePackageInterface) { if (isset($config['scripts']) && is_array($config['scripts'])) { foreach ($config['scripts'] as $event => $listeners) { diff --git a/src/Composer/Package/Package.php b/src/Composer/Package/Package.php index ddd4b4ec5..be57df64f 100644 --- a/src/Composer/Package/Package.php +++ b/src/Composer/Package/Package.php @@ -51,6 +51,7 @@ class Package extends BasePackage protected $suggests = array(); protected $autoload = array(); protected $includePaths = array(); + protected $archiveExcludes = array(); /** * Creates a new in memory package. @@ -525,4 +526,22 @@ class Package extends BasePackage { return $this->notificationUrl; } + + /** + * Sets a list of patterns to be excluded from archives + * + * @param array $excludes + */ + public function setArchiveExcludes($excludes) + { + $this->archiveExcludes = $excludes; + } + + /** + * {@inheritDoc} + */ + public function getArchiveExcludes() + { + return $this->archiveExcludes; + } } diff --git a/src/Composer/Package/PackageInterface.php b/src/Composer/Package/PackageInterface.php index 6c2b48b4f..227ce42c3 100644 --- a/src/Composer/Package/PackageInterface.php +++ b/src/Composer/Package/PackageInterface.php @@ -308,4 +308,11 @@ interface PackageInterface * @return string */ public function getPrettyString(); + + /** + * Returns a list of patterns to exclude from package archives + * + * @return array + */ + public function getArchiveExcludes(); } diff --git a/tests/Composer/Test/Package/Archiver/PharArchiverTest.php b/tests/Composer/Test/Package/Archiver/PharArchiverTest.php index 0e5099668..97910938d 100644 --- a/tests/Composer/Test/Package/Archiver/PharArchiverTest.php +++ b/tests/Composer/Test/Package/Archiver/PharArchiverTest.php @@ -29,7 +29,7 @@ class PharArchiverTest extends ArchiverTest // Test archive $archiver = new PharArchiver(); - $archiver->archive($package->getSourceUrl(), $target, 'tar'); + $archiver->archive($package->getSourceUrl(), $target, 'tar', null, array('foo/bar', 'baz', '!/foo/bar/baz')); $this->assertFileExists($target); unlink($target); @@ -58,12 +58,25 @@ class PharArchiverTest extends ArchiverTest $currentWorkDir = getcwd(); chdir($this->testDir); - $result = file_put_contents('b', 'a'); + $this->writeFile('file.txt', 'content', $currentWorkDir); + $this->writeFile('foo/bar/baz', 'content', $currentWorkDir); + $this->writeFile('foo/bar/ignoreme', 'content', $currentWorkDir); + $this->writeFile('x/baz', 'content', $currentWorkDir); + $this->writeFile('x/includeme', 'content', $currentWorkDir); + + chdir($currentWorkDir); + } + + protected function writeFile($path, $content, $currentWorkDir) + { + if (!file_exists(dirname($path))) { + mkdir(dirname($path), 0777, true); + } + + $result = file_put_contents($path, 'a'); if (false === $result) { chdir($currentWorkDir); throw new \RuntimeException('Could not save file.'); } - - chdir($currentWorkDir); } } diff --git a/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php b/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php index ec80984be..4b9877523 100644 --- a/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php +++ b/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php @@ -130,6 +130,14 @@ class ArrayDumperTest extends \PHPUnit_Framework_TestCase 'extra', array('class' => 'MyVendor\\Installer') ), + array( + 'archive', + array('/foo/bar', 'baz', '!/foo/bar/baz'), + 'archiveExcludes', + array( + 'exclude' => array('/foo/bar', 'baz', '!/foo/bar/baz'), + ), + ), array( 'require', array(new Link('foo', 'foo/bar', new VersionConstraint('=', '1.0.0.0'), 'requires', '1.0.0')), diff --git a/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php b/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php index ef1295ff8..68aed0a23 100644 --- a/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php @@ -114,6 +114,9 @@ class ArrayLoaderTest extends \PHPUnit_Framework_TestCase 'target-dir' => 'some/prefix', 'extra' => array('random' => array('things' => 'of', 'any' => 'shape')), 'bin' => array('bin1', 'bin/foo'), + 'archive' => array( + 'exclude' => array('/foo/bar', 'baz', '!/foo/bar/baz'), + ), ); $package = $this->loader->load($config); diff --git a/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php b/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php index e095f6e3d..23686d08f 100644 --- a/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php @@ -123,6 +123,9 @@ class ValidatingArrayLoaderTest extends \PHPUnit_Framework_TestCase 'vendor-dir' => 'vendor', 'process-timeout' => 10000, ), + 'archive' => array( + 'exclude' => array('/foo/bar', 'baz', '!/foo/bar/baz'), + ), 'scripts' => array( 'post-update-cmd' => 'Foo\\Bar\\Baz::doSomething', 'post-install-cmd' => array(