1
0
Fork 0

Merge pull request #1567 from naderman/feature-dist

Distributions/Archives
pull/1745/head
Nils Adermann 2013-03-28 05:37:49 -07:00
commit 78c250da19
28 changed files with 1420 additions and 3 deletions

View File

@ -9,5 +9,7 @@ php:
before_script: before_script:
- echo '' > ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini - echo '' > ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini
- composer install --dev --prefer-source - composer install --dev --prefer-source
- git config --global user.name travis-ci
- git config --global user.email travis@example.com
script: ./vendor/bin/phpunit -c tests/complete.phpunit.xml script: ./vendor/bin/phpunit -c tests/complete.phpunit.xml

View File

@ -656,4 +656,29 @@ See [Vendor Binaries](articles/vendor-binaries.md) for more details.
Optional. 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) → ← [Command-line interface](03-cli.md) | [Repositories](05-repositories.md) →

View File

@ -202,6 +202,16 @@
} }
} }
}, },
"archive": {
"type": ["object"],
"description": "Options for creating package archives for distribution.",
"properties": {
"exclude": {
"type": "array",
"description": "A list of patterns for paths to exclude or include if prefixed with an exclamation mark."
}
}
},
"repositories": { "repositories": {
"type": ["object", "array"], "type": ["object", "array"],
"description": "A set of additional repositories where packages can be found.", "description": "A set of additional repositories where packages can be found.",

View File

@ -0,0 +1,123 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Command;
use Composer\Factory;
use Composer\IO\IOInterface;
use Composer\DependencyResolver\Pool;
use Composer\Package\LinkConstraint\VersionConstraint;
use Composer\Repository\CompositeRepository;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Creates an archive of a package for distribution.
*
* @author Nils Adermann <naderman@naderman.de>
*/
class ArchiveCommand extends Command
{
protected function configure()
{
$this
->setName('archive')
->setDescription('Create an archive of this composer package')
->setDefinition(array(
new InputArgument('package', InputArgument::OPTIONAL, 'The package to archive instead of the current project'),
new InputArgument('version', InputArgument::OPTIONAL, 'The package version to archive'),
new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the resulting archive: tar or zip', 'tar'),
new InputOption('dir', false, InputOption::VALUE_REQUIRED, 'Write the archive to this directory', '.'),
))
->setHelp(<<<EOT
The <info>archive</info> command creates an archive of the specified format
containing the files and directories of the Composer project or the specified
package in the specified version and writes it to the specified directory.
<info>php composer.phar archive [--format=zip] [--dir=/foo] [package [version]]</info>
EOT
)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
return $this->archive(
$this->getIO(),
$input->getArgument('package'),
$input->getArgument('version'),
$input->getOption('format'),
$input->getOption('dir')
);
}
protected function archive(IOInterface $io, $packageName = null, $version = null, $format = 'tar', $dest = '.')
{
$config = Factory::createConfig();
$factory = new Factory;
$archiveManager = $factory->createArchiveManager($config);
if ($packageName) {
$package = $this->selectPackage($io, $packageName, $version);
if (!$package) {
return 1;
}
} else {
$package = $this->getComposer()->getPackage();
}
$io->write('<info>Creating the archive.</info>');
$archiveManager->archive($package, $format, $dest);
return 0;
}
protected function selectPackage(IOInterface $io, $packageName, $version = null)
{
$io->write('<info>Searching for the specified package.</info>');
if ($composer = $this->getComposer(false)) {
$localRepo = $composer->getRepositoryManager()->getLocalRepository();
$repos = new CompositeRepository(array_merge(array($localRepo), $composer->getRepositoryManager()->getRepositories()));
} else {
$defaultRepos = Factory::createDefaultRepositories($this->getIO());
$output->writeln('No composer.json found in the current directory, searching packages from ' . implode(', ', array_keys($defaultRepos)));
$repos = new CompositeRepository($defaultRepos);
}
$pool = new Pool();
$pool->addRepository($repos);
$constraint = ($version) ? new VersionConstraint('>=', $version) : null;
$packages = $pool->whatProvides($packageName, $constraint);
if (count($packages) > 1) {
$package = $packages[0];
$io->write('<info>Found multiple matches, selected '.$package->getPrettyString().'.</info>');
$io->write('Alternatives were '.implode(', ', array_map(function ($p) { return $p->getPrettyString(); }, $packages)).'.');
$io->write('<comment>Please use a more specific constraint to pick a different package.</comment>');
} elseif ($packages) {
$package = $packages[0];
$io->write('<info>Found an exact match '.$package->getPrettyString().'.</info>');
} else {
$io->write('<error>Could not find a package matching '.$packageName.'.</error>');
return false;
}
return $package;
}
}

View File

@ -194,6 +194,7 @@ class Application extends BaseApplication
$commands[] = new Command\RequireCommand(); $commands[] = new Command\RequireCommand();
$commands[] = new Command\DumpAutoloadCommand(); $commands[] = new Command\DumpAutoloadCommand();
$commands[] = new Command\StatusCommand(); $commands[] = new Command\StatusCommand();
$commands[] = new Command\ArchiveCommand();
if ('phar:' === substr(__FILE__, 0, 5)) { if ('phar:' === substr(__FILE__, 0, 5)) {
$commands[] = new Command\SelfUpdateCommand(); $commands[] = new Command\SelfUpdateCommand();

View File

@ -15,6 +15,7 @@ namespace Composer;
use Composer\Config\JsonConfigSource; use Composer\Config\JsonConfigSource;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Package\Archiver;
use Composer\Repository\ComposerRepository; use Composer\Repository\ComposerRepository;
use Composer\Repository\RepositoryManager; use Composer\Repository\RepositoryManager;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
@ -317,6 +318,24 @@ class Factory
return $dm; return $dm;
} }
/**
* @param Config $config The configuration
* @param Downloader\DownloadManager $dm Manager use to download sources
*
* @return Archiver\ArchiveManager
*/
public function createArchiveManager(Config $config, Downloader\DownloadManager $dm = null)
{
if (null === $dm) {
$dm = $this->createDownloadManager(new IO\NullIO(), $config);
}
$am = new Archiver\ArchiveManager($dm);
$am->addArchiver(new Archiver\PharArchiver);
return $am;
}
/** /**
* @return Installer\InstallationManager * @return Installer\InstallationManager
*/ */

View File

@ -311,6 +311,10 @@ class AliasPackage extends BasePackage implements CompletePackageInterface
{ {
return $this->aliasOf->getNotificationUrl(); return $this->aliasOf->getNotificationUrl();
} }
public function getArchiveExcludes()
{
return $this->aliasOf->getArchiveExcludes();
}
public function __toString() public function __toString()
{ {
return parent::__toString().' (alias of '.$this->aliasOf->getVersion().')'; return parent::__toString().' (alias of '.$this->aliasOf->getVersion().')';

View File

@ -0,0 +1,77 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Package\Archiver;
use Composer\Package\BasePackage;
use Composer\Package\PackageInterface;
use Symfony\Component\Finder;
/**
* A Symfony Finder wrapper which locates files that should go into archives
*
* Handles .gitignore, .gitattributes and .hgignore files as well as composer's
* own exclude rules from composer.json
*
* @author Nils Adermann <naderman@naderman.de>
*/
class ArchivableFilesFinder extends \FilterIterator
{
/**
* @var Symfony\Component\Finder\Finder
*/
protected $finder;
/**
* Initializes the internal Symfony Finder with appropriate filters
*
* @param string $sources Path to source files to be archived
* @param array $excludes Composer's own exclude rules from composer.json
*/
public function __construct($sources, array $excludes)
{
$sources = realpath($sources);
$filters = array(
new HgExcludeFilter($sources),
new GitExcludeFilter($sources),
new ComposerExcludeFilter($sources, $excludes),
);
$this->finder = new Finder\Finder();
$this->finder
->in($sources)
->filter(function (\SplFileInfo $file) use ($sources, $filters) {
$relativePath = preg_replace(
'#^'.preg_quote($sources, '#').'#',
'',
str_replace(DIRECTORY_SEPARATOR, '/', $file->getRealPath())
);
$exclude = false;
foreach ($filters as $filter) {
$exclude = $filter->filter($relativePath, $exclude);
}
return !$exclude;
})
->ignoreVCS(true)
->ignoreDotFiles(false);
parent::__construct($this->finder->getIterator());
}
public function accept()
{
return !$this->getInnerIterator()->current()->isDir();
}
}

View File

@ -0,0 +1,147 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Package\Archiver;
use Composer\Downloader\DownloadManager;
use Composer\Factory;
use Composer\IO\NullIO;
use Composer\Package\PackageInterface;
use Composer\Package\RootPackage;
use Composer\Util\Filesystem;
/**
* @author Matthieu Moquet <matthieu@moquet.net>
* @author Till Klampaeckel <till@php.net>
*/
class ArchiveManager
{
protected $downloadManager;
protected $archivers = array();
/**
* @var bool
*/
protected $overwriteFiles = true;
/**
* @param DownloadManager $downloadManager A manager used to download package sources
*/
public function __construct(DownloadManager $downloadManager)
{
$this->downloadManager = $downloadManager;
}
/**
* @param ArchiverInterface $archiver
*/
public function addArchiver(ArchiverInterface $archiver)
{
$this->archivers[] = $archiver;
}
/**
* Set whether existing archives should be overwritten
*
* @param bool $overwriteFiles New setting
*
* @return $this
*/
public function setOverwriteFiles($overwriteFiles)
{
$this->overwriteFiles = $overwriteFiles;
return $this;
}
/**
* Generate a distinct filename for a particular version of a package.
*
* @param PackageInterface $package The package to get a name for
*
* @return string A filename without an extension
*/
public function getPackageFilename(PackageInterface $package)
{
$nameParts = array(preg_replace('#[^a-z0-9-_.]#i', '-', $package->getName()));
if (preg_match('{^[a-f0-9]{40}$}', $package->getDistReference())) {
$nameParts = array_merge($nameParts, array($package->getDistReference(), $package->getDistType()));
} else {
$nameParts = array_merge($nameParts, array($package->getPrettyVersion(), $package->getDistReference()));
}
if ($package->getSourceReference()) {
$nameParts[] = substr(sha1($package->getSourceReference()), 0, 6);
}
return implode('-', array_filter($nameParts, function ($p) {
return !empty($p);
}));
}
/**
* Create an archive of the specified package.
*
* @param PackageInterface $package The package to archive
* @param string $format The format of the archive (zip, tar, ...)
* @param string $targetDir The diretory where to build the archive
*
* @return string The path of the created archive
*/
public function archive(PackageInterface $package, $format, $targetDir)
{
if (empty($format)) {
throw new \InvalidArgumentException('Format must be specified');
}
// Search for the most appropriate archiver
$usableArchiver = null;
foreach ($this->archivers as $archiver) {
if ($archiver->supports($format, $package->getSourceType())) {
$usableArchiver = $archiver;
break;
}
}
// Checks the format/source type are supported before downloading the package
if (null === $usableArchiver) {
throw new \RuntimeException(sprintf('No archiver found to support %s format', $format));
}
$filesystem = new Filesystem();
$packageName = $this->getPackageFilename($package);
// Archive filename
$filesystem->ensureDirectoryExists($targetDir);
$target = realpath($targetDir).'/'.$packageName.'.'.$format;
$filesystem->ensureDirectoryExists(dirname($target));
if (!$this->overwriteFiles && file_exists($target)) {
return $target;
}
if ($package instanceof RootPackage) {
$sourcePath = realpath('.');
} else {
// Directory used to download the sources
$sourcePath = sys_get_temp_dir().'/composer_archiver/'.$packageName;
$filesystem->ensureDirectoryExists($sourcePath);
// Download sources
$this->downloadManager->download($package, $sourcePath, true);
}
// Create the archive
return $usableArchiver->archive($sourcePath, $target, $format, $package->getArchiveExcludes());
}
}

View File

@ -0,0 +1,45 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Package\Archiver;
use Composer\Package\PackageInterface;
/**
* @author Till Klampaeckel <till@php.net>
* @author Matthieu Moquet <matthieu@moquet.net>
* @author Nils Adermann <naderman@naderman.de>
*/
interface ArchiverInterface
{
/**
* Create an archive from the sources.
*
* @param string $sources The sources directory
* @param string $target The target file
* @param string $format The format used for archive
* @param array $excludes A list of patterns for files to exclude
*
* @return string The path to the written archive file
*/
public function archive($sources, $target, $format, array $excludes = array());
/**
* Format supported by the archiver.
*
* @param string $format The archive format
* @param string $sourceType The source type (git, svn, hg, etc.)
*
* @return boolean true if the format is supported by the archiver
*/
public function supports($format, $sourceType);
}

View File

@ -0,0 +1,141 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Package\Archiver;
use Symfony\Component\Finder;
/**
* @author Nils Adermann <naderman@naderman.de>
*/
abstract class BaseExcludeFilter
{
/**
* @var string
*/
protected $sourcePath;
/**
* @var array
*/
protected $excludePatterns;
/**
* @param string $sourcePath Directory containing sources to be filtered
*/
public function __construct($sourcePath)
{
$this->sourcePath = $sourcePath;
$this->excludePatterns = array();
}
/**
* Checks the given path against all exclude patterns in this filter
*
* Negated patterns overwrite exclude decisions of previous filters.
*
* @param string $relativePath The file's path relative to the sourcePath
* @param bool $exclude Whether a previous filter wants to exclude this file
*
* @return bool Whether the file should be excluded
*/
public function filter($relativePath, $exclude)
{
foreach ($this->excludePatterns as $patternData) {
list($pattern, $negate, $stripLeadingSlash) = $patternData;
if ($stripLeadingSlash) {
$path = substr($relativePath, 1);
} else {
$path = $relativePath;
}
if (preg_match($pattern, $path)) {
$exclude = !$negate;
}
}
return $exclude;
}
/**
* Processes a file containing exclude rules of different formats per line
*
* @param array $lines A set of lines to be parsed
* @param callback $lineParser The parser to be used on each line
*
* @return array Exclude patterns to be used in filter()
*/
protected function parseLines(array $lines, $lineParser)
{
return array_filter(
array_map(
function ($line) use ($lineParser) {
$line = trim($line);
$commentHash = strpos($line, '#');
if ($commentHash !== false) {
$line = substr($line, 0, $commentHash);
}
if ($line) {
return call_user_func($lineParser, $line);
}
return null;
}, $lines),
function ($pattern) {
return $pattern !== null;
}
);
}
/**
* Generates a set of exclude patterns for filter() from gitignore rules
*
* @param array $rules A list of exclude rules in gitignore syntax
*
* @return array Exclude patterns
*/
protected function generatePatterns($rules)
{
$patterns = array();
foreach ($rules as $rule) {
$patterns[] = $this->generatePattern($rule);
}
return $patterns;
}
/**
* Generates an exclude pattern for filter() from a gitignore rule
*
* @param string An exclude rule in gitignore syntax
*
* @param array An exclude pattern
*/
protected function generatePattern($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);
return array($pattern . '#', $negate, false);
}
}

View File

@ -0,0 +1,31 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Package\Archiver;
/**
* An exclude filter which processes composer's own exclude rules
*
* @author Nils Adermann <naderman@naderman.de>
*/
class ComposerExcludeFilter extends BaseExcludeFilter
{
/**
* @param string $sourcePath Directory containing sources to be filtered
* @param array $excludeRules An array of exclude rules from composer.json
*/
public function __construct($sourcePath, array $excludeRules)
{
parent::__construct($sourcePath);
$this->excludePatterns = $this->generatePatterns($excludeRules);
}
}

View File

@ -0,0 +1,80 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Package\Archiver;
/**
* An exclude filter that processes gitignore and gitattributes
*
* It respects export-ignore git attributes
*
* @author Nils Adermann <naderman@naderman.de>
*/
class GitExcludeFilter extends BaseExcludeFilter
{
/**
* Parses .gitignore and .gitattributes files if they exist
*
* @param string $sourcePath
*/
public function __construct($sourcePath)
{
parent::__construct($sourcePath);
if (file_exists($sourcePath.'/.gitignore')) {
$this->excludePatterns = $this->parseLines(
file($sourcePath.'/.gitignore'),
array($this, 'parseGitIgnoreLine')
);
}
if (file_exists($sourcePath.'/.gitattributes')) {
$this->excludePatterns = array_merge(
$this->excludePatterns,
$this->parseLines(
file($sourcePath.'/.gitattributes'),
array($this, 'parseGitAttributesLine')
));
}
}
/**
* Callback line parser which process gitignore lines
*
* @param string $line A line from .gitignore
*
* @return array An exclude pattern for filter()
*/
public function parseGitIgnoreLine($line)
{
return $this->generatePattern($line);
}
/**
* Callback parser which finds export-ignore rules in git attribute lines
*
* @param string $line A line from .gitattributes
*
* @return array An exclude pattern for filter()
*/
public function parseGitAttributesLine($line)
{
$parts = preg_split('#\s+#', $line);
if (count($parts) != 2) {
return null;
}
if ($parts[1] === 'export-ignore') {
return $this->generatePattern($parts[0]);
}
}
}

View File

@ -0,0 +1,104 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Package\Archiver;
use Symfony\Component\Finder;
/**
* An exclude filter that processes hgignore files
*
* @author Nils Adermann <naderman@naderman.de>
*/
class HgExcludeFilter extends BaseExcludeFilter
{
const HG_IGNORE_REGEX = 1;
const HG_IGNORE_GLOB = 2;
/**
* Either HG_IGNORE_REGEX or HG_IGNORE_GLOB
* @var integer
*/
protected $patternMode;
/**
* Parses .hgignore file if it exist
*
* @param string $sourcePath
*/
public function __construct($sourcePath)
{
parent::__construct($sourcePath);
$this->patternMode = self::HG_IGNORE_REGEX;
if (file_exists($sourcePath.'/.hgignore')) {
$this->excludePatterns = $this->parseLines(
file($sourcePath.'/.hgignore'),
array($this, 'parseHgIgnoreLine')
);
}
}
/**
* Callback line parser which process hgignore lines
*
* @param string $line A line from .hgignore
*
* @return array An exclude pattern for filter()
*/
public function parseHgIgnoreLine($line)
{
if (preg_match('#^syntax\s*:\s*(glob|regexp)$#', $line, $matches)) {
if ($matches[1] === 'glob') {
$this->patternMode = self::HG_IGNORE_GLOB;
} else {
$this->patternMode = self::HG_IGNORE_REGEX;
}
return null;
}
if ($this->patternMode == self::HG_IGNORE_GLOB) {
return $this->patternFromGlob($line);
} else {
return $this->patternFromRegex($line);
}
}
/**
* Generates an exclude pattern for filter() from a hg glob expression
*
* @param string $line A line from .hgignore in glob mode
*
* @return array An exclude pattern for filter()
*/
protected function patternFromGlob($line)
{
$pattern = '#'.substr(Finder\Glob::toRegex($line), 2, -1).'#';
$pattern = str_replace('[^/]*', '.*', $pattern);
return array($pattern, false, true);
}
/**
* Generates an exclude pattern for filter() from a hg regexp expression
*
* @param string $line A line from .hgignore in regexp mode
*
* @return array An exclude pattern for filter()
*/
public function patternFromRegex($line)
{
// WTF need to escape the delimiter safely
$pattern = '#'.preg_replace('/((?:\\\\\\\\)*)(\\\\?)#/', '\1\2\2\\#', $line).'#';
return array($pattern, false, true);
}
}

View File

@ -0,0 +1,65 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Package\Archiver;
use Composer\Package\BasePackage;
use Composer\Package\PackageInterface;
/**
* @author Till Klampaeckel <till@php.net>
* @author Nils Adermann <naderman@naderman.de>
* @author Matthieu Moquet <matthieu@moquet.net>
*/
class PharArchiver implements ArchiverInterface
{
protected static $formats = array(
'zip' => \Phar::ZIP,
'tar' => \Phar::TAR,
);
/**
* {@inheritdoc}
*/
public function archive($sources, $target, $format, array $excludes = array())
{
$sources = realpath($sources);
// Phar would otherwise load the file which we don't want
if (file_exists($target)) {
unlink($target);
}
try {
$phar = new \PharData($target, null, null, static::$formats[$format]);
$files = new ArchivableFilesFinder($sources, $excludes);
$phar->buildFromIterator($files, $sources);
return $target;
} catch (\UnexpectedValueException $e) {
$message = sprintf("Could not create archive '%s' from '%s': %s",
$target,
$sources,
$e->getMessage()
);
throw new \RuntimeException($message, $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
public function supports($format, $sourceType)
{
return isset(static::$formats[$format]);
}
}

View File

@ -58,6 +58,10 @@ class ArrayDumper
$data['dist']['shasum'] = $package->getDistSha1Checksum(); $data['dist']['shasum'] = $package->getDistSha1Checksum();
} }
if ($package->getArchiveExcludes()) {
$data['archive']['exclude'] = $package->getArchiveExcludes();
}
foreach (BasePackage::$supportedLinkTypes as $type => $opts) { foreach (BasePackage::$supportedLinkTypes as $type => $opts) {
if ($links = $package->{'get'.ucfirst($opts['method'])}()) { if ($links = $package->{'get'.ucfirst($opts['method'])}()) {
foreach ($links as $link) { foreach ($links as $link) {

View File

@ -150,6 +150,10 @@ class ArrayLoader implements LoaderInterface
$package->setNotificationUrl($config['notification-url']); $package->setNotificationUrl($config['notification-url']);
} }
if (!empty($config['archive']['exclude'])) {
$package->setArchiveExcludes($config['archive']['exclude']);
}
if ($package instanceof Package\CompletePackageInterface) { if ($package instanceof Package\CompletePackageInterface) {
if (isset($config['scripts']) && is_array($config['scripts'])) { if (isset($config['scripts']) && is_array($config['scripts'])) {
foreach ($config['scripts'] as $event => $listeners) { foreach ($config['scripts'] as $event => $listeners) {

View File

@ -51,6 +51,7 @@ class Package extends BasePackage
protected $suggests = array(); protected $suggests = array();
protected $autoload = array(); protected $autoload = array();
protected $includePaths = array(); protected $includePaths = array();
protected $archiveExcludes = array();
/** /**
* Creates a new in memory package. * Creates a new in memory package.
@ -525,4 +526,22 @@ class Package extends BasePackage
{ {
return $this->notificationUrl; return $this->notificationUrl;
} }
/**
* Sets a list of patterns to be excluded from archives
*
* @param array $excludes
*/
public function setArchiveExcludes(array $excludes)
{
$this->archiveExcludes = $excludes;
}
/**
* {@inheritDoc}
*/
public function getArchiveExcludes()
{
return $this->archiveExcludes;
}
} }

View File

@ -308,4 +308,11 @@ interface PackageInterface
* @return string * @return string
*/ */
public function getPrettyString(); public function getPrettyString();
/**
* Returns a list of patterns to exclude from package archives
*
* @return array
*/
public function getArchiveExcludes();
} }

View File

@ -55,9 +55,8 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase
$fs->ensureDirectoryExists(dirname(self::$pharPath)); $fs->ensureDirectoryExists(dirname(self::$pharPath));
chdir(dirname(self::$pharPath)); chdir(dirname(self::$pharPath));
$proc = new Process('php '.escapeshellarg(__DIR__.'/../../../bin/compile')); $proc = new Process('php '.escapeshellarg(__DIR__.'/../../../bin/compile'), dirname(self::$pharPath));
$exitcode = $proc->run(); $exitcode = $proc->run();
if ($exitcode !== 0 || trim($proc->getOutput())) { if ($exitcode !== 0 || trim($proc->getOutput())) {
$this->fail($proc->getOutput()); $this->fail($proc->getOutput());
} }
@ -76,7 +75,7 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase
putenv('COMPOSER_HOME='.$this->testDir.'home'); putenv('COMPOSER_HOME='.$this->testDir.'home');
$cmd = 'php '.escapeshellarg(self::$pharPath).' --no-ansi '.$testData['RUN']; $cmd = 'php '.escapeshellarg(self::$pharPath).' --no-ansi '.$testData['RUN'];
$proc = new Process($cmd); $proc = new Process($cmd, __DIR__.'/Fixtures/functional');
$exitcode = $proc->run(); $exitcode = $proc->run();
if (isset($testData['EXPECT'])) { if (isset($testData['EXPECT'])) {

View File

@ -0,0 +1,206 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Test\Package\Archiver;
use Composer\Package\Archiver\ArchivableFilesFinder;
use Composer\Util\Filesystem;
use Symfony\Component\Process\Process;
/**
* @author Nils Adermann <naderman@naderman.de>
*/
class ArchivableFilesFinderTest extends \PHPUnit_Framework_TestCase
{
protected $sources;
protected $finder;
protected function setup()
{
$fs = new Filesystem;
$this->sources = sys_get_temp_dir().
'/composer_archiver_test'.uniqid(mt_rand(), true);
$fileTree = array(
'A/prefixA.foo',
'A/prefixB.foo',
'A/prefixC.foo',
'A/prefixD.foo',
'A/prefixE.foo',
'A/prefixF.foo',
'B/sub/prefixA.foo',
'B/sub/prefixB.foo',
'B/sub/prefixC.foo',
'B/sub/prefixD.foo',
'B/sub/prefixE.foo',
'B/sub/prefixF.foo',
'toplevelA.foo',
'toplevelB.foo',
'prefixA.foo',
'prefixB.foo',
'prefixC.foo',
'prefixD.foo',
'prefixE.foo',
'prefixF.foo',
);
foreach ($fileTree as $relativePath) {
$path = $this->sources.'/'.$relativePath;
$fs->ensureDirectoryExists(dirname($path));
file_put_contents($path, '');
}
}
protected function tearDown()
{
$fs = new Filesystem;
$fs->removeDirectory($this->sources);
}
public function testManualExcludes()
{
$excludes = array(
'prefixB.foo',
'!/prefixB.foo',
'/prefixA.foo',
'prefixC.*',
'!*/*/*/prefixC.foo'
);
$this->finder = new ArchivableFilesFinder($this->sources, $excludes);
$this->assertArchivableFiles(array(
'/A/prefixA.foo',
'/A/prefixD.foo',
'/A/prefixE.foo',
'/A/prefixF.foo',
'/B/sub/prefixA.foo',
'/B/sub/prefixC.foo',
'/B/sub/prefixD.foo',
'/B/sub/prefixE.foo',
'/B/sub/prefixF.foo',
'/prefixB.foo',
'/prefixD.foo',
'/prefixE.foo',
'/prefixF.foo',
'/toplevelA.foo',
'/toplevelB.foo',
));
}
public function testGitExcludes()
{
file_put_contents($this->sources.'/.gitignore', implode("\n", array(
'# gitignore rules with comments and blank lines',
'',
'prefixE.foo',
'# and more',
'# comments',
'',
'!/prefixE.foo',
'/prefixD.foo',
'prefixF.*',
'!/*/*/prefixF.foo',
'',
)));
// git does not currently support negative git attributes
file_put_contents($this->sources.'/.gitattributes', implode("\n", array(
'',
'# gitattributes rules with comments and blank lines',
'prefixB.foo export-ignore',
//'!/prefixB.foo export-ignore',
'/prefixA.foo export-ignore',
'prefixC.* export-ignore',
//'!/*/*/prefixC.foo export-ignore'
)));
$this->finder = new ArchivableFilesFinder($this->sources, array());
$this->assertArchivableFiles($this->getArchivedFiles('git init && '.
'git add .git* && '.
'git commit -m "ignore rules" && '.
'git add . && '.
'git commit -m "init" && '.
'git archive --format=zip --prefix=archive/ -o archive.zip HEAD'
));
}
public function testHgExcludes()
{
file_put_contents($this->sources.'/.hgignore', implode("\n", array(
'# hgignore rules with comments, blank lines and syntax changes',
'',
'pre*A.foo',
'prefixE.foo',
'# and more',
'# comments',
'',
'^prefixD.foo',
'syntax: glob',
'prefixF.*',
'B/*',
)));
$this->finder = new ArchivableFilesFinder($this->sources, array());
$expectedFiles = $this->getArchivedFiles('hg init && '.
'hg add && '.
'hg commit -m "init" && '.
'hg archive archive.zip'
);
array_shift($expectedFiles); // remove .hg_archival.txt
$this->assertArchivableFiles($expectedFiles);
}
protected function getArchivableFiles()
{
$files = array();
foreach ($this->finder as $file) {
if (!$file->isDir()) {
$files[] = preg_replace('#^'.preg_quote($this->sources, '#').'#', '', $file->getRealPath());
}
}
sort($files);
return $files;
}
protected function getArchivedFiles($command)
{
$process = new Process($command, $this->sources);
$process->run();
$archive = new \PharData($this->sources.'/archive.zip');
$iterator = new \RecursiveIteratorIterator($archive);
$files = array();
foreach ($iterator as $file) {
$files[] = preg_replace('#^phar://'.preg_quote($this->sources, '#').'/archive\.zip/archive#', '', $file);
}
unlink($this->sources.'/archive.zip');
return $files;
}
protected function assertArchivableFiles($expectedFiles)
{
$actualFiles = $this->getArchivableFiles();
$this->assertEquals($expectedFiles, $actualFiles);
}
}

View File

@ -0,0 +1,99 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Test\Package\Archiver;
use Composer\Factory;
use Composer\IO\NullIO;
use Composer\Package\Archiver;
use Composer\Package\Archiver\ArchiveManager;
use Composer\Package\PackageInterface;
/**
* @author Till Klampaeckel <till@php.net>
* @author Matthieu Moquet <matthieu@moquet.net>
*/
class ArchiveManagerTest extends ArchiverTest
{
protected $manager;
protected $targetDir;
public function setUp()
{
parent::setUp();
$factory = new Factory();
$this->manager = $factory->createArchiveManager($factory->createConfig());
$this->targetDir = $this->testDir.'/composer_archiver_tests';
}
public function testUnknownFormat()
{
$this->setExpectedException('RuntimeException');
$package = $this->setupPackage();
$this->manager->archive($package, '__unknown_format__', $this->targetDir);
}
public function testArchiveTar()
{
$this->setupGitRepo();
$package = $this->setupPackage();
$this->manager->archive($package, 'tar', $this->targetDir);
$target = $this->getTargetName($package, 'tar');
$this->assertFileExists($target);
unlink($target);
}
protected function getTargetName(PackageInterface $package, $format)
{
$packageName = $this->manager->getPackageFilename($package);
$target = $this->targetDir.'/'.$packageName.'.'.$format;
return $target;
}
/**
* Create local git repository to run tests against!
*/
protected function setupGitRepo()
{
$currentWorkDir = getcwd();
chdir($this->testDir);
$output = null;
$result = $this->process->execute('git init -q', $output, $this->testDir);
if ($result > 0) {
chdir($currentWorkDir);
throw new \RuntimeException('Could not init: '.$this->process->getErrorOutput());
}
$result = file_put_contents('b', 'a');
if (false === $result) {
chdir($currentWorkDir);
throw new \RuntimeException('Could not save file.');
}
$result = $this->process->execute('git add b && git commit -m "commit b" -q', $output, $this->testDir);
if ($result > 0) {
chdir($currentWorkDir);
throw new \RuntimeException('Could not commit: '.$this->process->getErrorOutput());
}
chdir($currentWorkDir);
}
}

View File

@ -0,0 +1,67 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Test\Package\Archiver;
use Composer\Util\Filesystem;
use Composer\Util\ProcessExecutor;
use Composer\Package\Package;
/**
* @author Till Klampaeckel <till@php.net>
* @author Matthieu Moquet <matthieu@moquet.net>
*/
abstract class ArchiverTest extends \PHPUnit_Framework_TestCase
{
/**
* @var \Composer\Util\Filesystem
*/
protected $filesystem;
/**
* @var \Composer\Util\ProcessExecutor
*/
protected $process;
/**
* @var string
*/
protected $testDir;
public function setUp()
{
$this->filesystem = new Filesystem();
$this->process = new ProcessExecutor();
$this->testDir = sys_get_temp_dir().'/composer_archiver_test_'.mt_rand();
$this->filesystem->ensureDirectoryExists($this->testDir);
}
public function tearDown()
{
$this->filesystem->removeDirectory($this->testDir);
}
/**
* Util method to quickly setup a package using the source path built.
*
* @return \Composer\Package\Package
*/
protected function setupPackage()
{
$package = new Package('archivertest/archivertest', 'master', 'master');
$package->setSourceUrl(realpath($this->testDir));
$package->setSourceReference('master');
$package->setSourceType('git');
return $package;
}
}

View File

@ -0,0 +1,42 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Test\Package\Archiver;
use Composer\Package\Archiver\HgExcludeFilter;
/**
* @author Nils Adermann <naderman@naderman.de>
*/
class HgExcludeFilterTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider patterns
*/
public function testPatternEscape($ignore, $expected)
{
$filter = new HgExcludeFilter('/');
$this->assertEquals($expected, $filter->patternFromRegex($ignore));
}
public function patterns()
{
return array(
array('.#', array('#.\\##', false, true)),
array('.\\#', array('#.\\\\\\##', false, true)),
array('\\.#', array('#\\.\\##', false, true)),
array('\\\\.\\\\\\\\#', array('#\\\\.\\\\\\\\\\##', false, true)),
array('.\\\\\\\\\\#', array('#.\\\\\\\\\\\\\\##', false, true)),
);
}
}

View File

@ -0,0 +1,82 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Test\Package\Archiver;
use Composer\Package\Archiver\PharArchiver;
/**
* @author Till Klampaeckel <till@php.net>
* @author Matthieu Moquet <matthieu@moquet.net>
*/
class PharArchiverTest extends ArchiverTest
{
public function testTarArchive()
{
// Set up repository
$this->setupDummyRepo();
$package = $this->setupPackage();
$target = sys_get_temp_dir().'/composer_archiver_test.tar';
// Test archive
$archiver = new PharArchiver();
$archiver->archive($package->getSourceUrl(), $target, 'tar', array('foo/bar', 'baz', '!/foo/bar/baz'));
$this->assertFileExists($target);
unlink($target);
}
public function testZipArchive()
{
// Set up repository
$this->setupDummyRepo();
$package = $this->setupPackage();
$target = sys_get_temp_dir().'/composer_archiver_test.zip';
// Test archive
$archiver = new PharArchiver();
$archiver->archive($package->getSourceUrl(), $target, 'zip');
$this->assertFileExists($target);
unlink($target);
}
/**
* Create a local dummy repository to run tests against!
*/
protected function setupDummyRepo()
{
$currentWorkDir = getcwd();
chdir($this->testDir);
$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.');
}
}
}

View File

@ -130,6 +130,14 @@ class ArrayDumperTest extends \PHPUnit_Framework_TestCase
'extra', 'extra',
array('class' => 'MyVendor\\Installer') array('class' => 'MyVendor\\Installer')
), ),
array(
'archive',
array('/foo/bar', 'baz', '!/foo/bar/baz'),
'archiveExcludes',
array(
'exclude' => array('/foo/bar', 'baz', '!/foo/bar/baz'),
),
),
array( array(
'require', 'require',
array(new Link('foo', 'foo/bar', new VersionConstraint('=', '1.0.0.0'), 'requires', '1.0.0')), array(new Link('foo', 'foo/bar', new VersionConstraint('=', '1.0.0.0'), 'requires', '1.0.0')),

View File

@ -114,6 +114,9 @@ class ArrayLoaderTest extends \PHPUnit_Framework_TestCase
'target-dir' => 'some/prefix', 'target-dir' => 'some/prefix',
'extra' => array('random' => array('things' => 'of', 'any' => 'shape')), 'extra' => array('random' => array('things' => 'of', 'any' => 'shape')),
'bin' => array('bin1', 'bin/foo'), 'bin' => array('bin1', 'bin/foo'),
'archive' => array(
'exclude' => array('/foo/bar', 'baz', '!/foo/bar/baz'),
),
); );
$package = $this->loader->load($config); $package = $this->loader->load($config);

View File

@ -123,6 +123,9 @@ class ValidatingArrayLoaderTest extends \PHPUnit_Framework_TestCase
'vendor-dir' => 'vendor', 'vendor-dir' => 'vendor',
'process-timeout' => 10000, 'process-timeout' => 10000,
), ),
'archive' => array(
'exclude' => array('/foo/bar', 'baz', '!/foo/bar/baz'),
),
'scripts' => array( 'scripts' => array(
'post-update-cmd' => 'Foo\\Bar\\Baz::doSomething', 'post-update-cmd' => 'Foo\\Bar\\Baz::doSomething',
'post-install-cmd' => array( 'post-install-cmd' => array(