Browse Source

Define an option to exclude files in the archive command

Nils Adermann 12 years ago
parent
commit
afcdad4b23

+ 25 - 0
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) →

+ 14 - 0
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.",

+ 4 - 0
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().')';

+ 1 - 1
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());
     }
 }

+ 2 - 1
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.

+ 57 - 2
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 <till@php.net>
+ * @author Nils Adermann <naderman@naderman.de>
  * @author Matthieu Moquet <matthieu@moquet.net>
  */
 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}
      */

+ 4 - 0
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) {

+ 4 - 0
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) {

+ 19 - 0
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;
+    }
 }

+ 7 - 0
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();
 }

+ 17 - 4
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);
     }
 }

+ 8 - 0
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')),

+ 3 - 0
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);

+ 3 - 0
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(