#!/usr/bin/env php run()); } /** * Command line interface for Pirum. * * @package Pirum * @author Fabien Potencier */ class Pirum_CLI { const VERSION = '@package_version@'; protected $options; protected $formatter; protected $commands = array( 'build', 'add', 'remove', ); public function __construct(array $options) { $this->options = $options; $this->formatter = new Pirum_CLI_Formatter(); } public function run() { echo $this->getUsage(); if (!isset($this->options[1])) { return 0; } $command = $this->options[1]; if (!$this->isCommand($command)) { echo $this->formatter->formatSection('ERROR', sprintf('"%s" is not a valid command.', $command)); return 1; } echo $this->formatter->format(sprintf("Running the %s command:\n", $command), 'COMMENT'); if (!isset($this->options[2]) || !is_dir($this->options[2])) { echo $this->formatter->formatSection('ERROR', "You must give the root directory of the PEAR channel server."); return 1; } $target = $this->options[2]; $ret = 0; try { switch ($command) { case 'build': $this->runBuild($target); break; case 'add': $ret = $this->runAdd($target); break; case 'remove': $ret = $this->runRemove($target); break; } if (0 == $ret) { echo $this->formatter->formatSection('INFO', sprintf("Command %s run successfully.", $command)); } } catch (Exception $e) { echo $this->formatter->formatSection('ERROR', sprintf("%s (%s, %s)", $e->getMessage(), get_class($e), $e->getCode())); return 1; } return $ret; } public static function removeDir($target) { $fp = opendir($target); while (false !== $file = readdir($fp)) { if (in_array($file, array('.', '..'))) { continue; } if (is_dir($target.'/'.$file)) { self::removeDir($target.'/'.$file); } else { unlink($target.'/'.$file); } } closedir($fp); rmdir($target); } public static function version() { if (strpos(self::VERSION, '@package_version') === 0) { return 'DEV'; } else { return self::VERSION; } } protected function runRemove($target) { if (!isset($this->options[3])) { echo $this->formatter->formatSection('ERROR', "You must pass a PEAR package name."); return 1; } if (!preg_match(Pirum_Package::PACKAGE_FILE_PATTERN, $this->options[3])) { echo $this->formatter->formatSection('ERROR', sprintf('The PEAR package "%s" filename is badly formatted.', $this->options[3])); return 1; } if (!is_file($target.'/get/'.basename($this->options[3]))) { echo $this->formatter->formatSection('ERROR', sprintf('The PEAR package "%s" does not exist in this channel.', $this->options[3])); return 1; } unlink($target.'/get/'.basename($this->options[3])); unlink($target.'/get/'.substr_replace(basename($this->options[3]), '.tar', -4)); $this->runBuild($target); } protected function runAdd($target) { if (!isset($this->options[3])) { echo $this->formatter->formatSection('ERROR', "You must pass a PEAR package file path."); return 1; } if (!is_file($this->options[3])) { echo $this->formatter->formatSection('ERROR', sprintf('The PEAR package "%s" does not exist.', $this->options[3])); return 1; } if (!preg_match(Pirum_Package::PACKAGE_FILE_PATTERN, $this->options[3])) { echo $this->formatter->formatSection('ERROR', sprintf('The PEAR package "%s" filename is badly formatted.', $this->options[3])); return 1; } if (!is_dir($target.'/get')) { mkdir($target.'/get', 0777, true); } copy($this->options[3], $target.'/get/'.basename($this->options[3])); $this->runBuild($target); } protected function runBuild($target) { $builder = new Pirum_Builder($target, $this->formatter); $builder->build(); } protected function isCommand($cmd) { return in_array($cmd, $this->commands); } protected function getUsage() { return $this->formatter->format(sprintf("Pirum %s by Fabien Potencier\n", self::version()), 'INFO') . $this->formatter->format("Available commands:\n", 'COMMENT') . " pirum build target_dir\n" . " pirum add target_dir Pirum-1.0.0.tgz\n" . " pirum remove target_dir Pirum-1.0.0.tgz\n\n"; } } /** * Builds all the files for a PEAR channel. * * @package Pirum * @author Fabien Potencier */ class Pirum_Builder { protected $buildDir; protected $targetDir; protected $server; protected $packages; protected $formatter; protected $templateConf; public function __construct($targetDir, $formatter = false) { if (!file_exists($targetDir.'/pirum.xml')) { throw new InvalidArgumentException('You must create a pirum.xml configuration file in the root of the target directory.'); } if (!is_dir($targetDir.'/get')) { mkdir($targetDir.'/get', 0777, true); } $this->server = simplexml_load_file($targetDir.'/pirum.xml'); if (!$this->server) { throw new InvalidArgumentException('Your pirum.xml configuration is invalid - tag missing.'); } $emptyFields = array(); if (empty($this->server->name)) { $emptyFields[] = 'name'; } if (empty($this->server->summary)) { $emptyFields[] = 'summary'; } if (empty($this->server->url)) { $emptyFields[] = 'url'; } if (!empty($emptyFields)) { throw new InvalidArgumentException(sprintf('You must fill required tags in your pirum.xml configuration file: %s.', implode(', ', $emptyFields))); } $this->server->url = rtrim($this->server->url, '/'); if (DIRECTORY_SEPARATOR == '/') { $configFile = '.pirumrc'; } else { $configFile = 'pirum.ini'; } $defaultConf = array( 'pirum' => array( 'template' => '', 'templatedir' => dirname(__FILE__) . '/templates' ) ); if (file_exists(dirname(__FILE__) . "/$configFile")) { $customConf = parse_ini_file($configFile, true); if ($customConf === false) { // if config file could not be parsed; use default values $customConf = array(); } } else { $customConf = array(); } $this->templateConf = array_merge($defaultConf, $customConf); $this->formatter = $formatter; $this->targetDir = $targetDir; $this->buildDir = sys_get_temp_dir().'/pirum_build_'.uniqid(); mkdir($this->buildDir.'/rest', 0777, true); } public function __destruct() { Pirum_CLI::removeDir($this->buildDir); } public function build() { $this->extractInformationFromPackages(); $this->fixArchives(); $this->buildChannel(); $this->buildMaintainers(); $this->buildCategories(); $this->buildPackages(); $this->buildComposerRepository(); $this->buildReleasePackages(); $this->buildIndex(); $this->buildCss(); $this->buildFeed(); $this->updateTargetDir(); } protected function fixArchives() { // create tar files when missing foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->targetDir.'/get'), RecursiveIteratorIterator::LEAVES_ONLY) as $file) { if (!preg_match(Pirum_Package::PACKAGE_FILE_PATTERN, $file->getFileName())) { continue; } $tar = preg_replace('/\.tgz/', '.tar', $file); if (!file_exists($tar)) { if (function_exists('gzopen')) { $gz = gzopen($file, 'r'); if ($gz === false) { throw new Exception(sprintf('Failed extracting archive "%s"!', $this->archive)); } $fp = fopen(str_replace('.tgz', '.tar', $file), 'wb'); while (!gzeof($gz)) { fwrite($fp, gzread($gz, 10000)); } gzclose($gz); fclose($fp); } else { system('cd '.$this->targetDir.'/get/ && gunzip -c -f '.basename($file)); } } } } protected function updateTargetDir() { $this->formatter and print $this->formatter->formatSection('INFO', "Updating PEAR server files."); $this->updateComposerRepository(); $this->updateChannel(); $this->updateIndex(); $this->updateCss(); $this->updateFeed(); $this->updatePackages(); } protected function updateComposerRepository() { copy($this->buildDir.'/packages.json', $this->targetDir.'/packages.json'); } protected function updateChannel() { if (!file_exists($this->targetDir.'/channel.xml') || file_get_contents($this->targetDir.'/channel.xml') != file_get_contents($this->buildDir.'/channel.xml')) { if (file_exists($this->targetDir.'/channel.xml')) { unlink($this->targetDir.'/channel.xml'); } rename($this->buildDir.'/channel.xml', $this->targetDir.'/channel.xml'); } } protected function updateIndex() { copy($this->buildDir.'/index.html', $this->targetDir.'/index.html'); } protected function updateCss() { copy($this->buildDir.'/pirum.css', $this->targetDir.'/pirum.css'); } protected function updateFeed() { copy($this->buildDir.'/feed.xml', $this->targetDir.'/feed.xml'); } protected function updatePackages() { $this->mirrorDir($this->buildDir.'/rest', $this->targetDir.'/rest'); } protected function buildFeed() { $this->formatter and print $this->formatter->formatSection('INFO', "Building feed."); $entries = ''; foreach ($this->packages as $package) { foreach ($package['releases'] as $release) { $date = date(DATE_ATOM, strtotime($release['date'])); reset($release['maintainers']); $maintainer = current($release['maintainers']); $entries .= << {$package['name']} {$release['version']} ({$release['stability']}) {$this->server->url}/get/{$package['name']}-{$release['version']}.tgz {$maintainer['nickname']} $date {$package['description']} {$release['notes']} EOF; } } $date = date(DATE_ATOM); $index = << {$this->server->url} $date {$this->server->summary} Latest Releases {$this->server->url} $entries EOF; file_put_contents($this->buildDir.'/feed.xml', $index); } protected function getTemplateFile($name) { if (array_key_exists($this->targetDir, $this->templateConf)) { // check first if a specific server template is defined $templateConf = $this->templateConf[$this->targetDir]; if (isset($templateConf['templatedir'])) { $dir = $templateConf['templatedir']; } else { // fallback to default template location $dir = $this->templateConf['pirum']['templatedir']; } $file = $dir; if (isset($templateConf['template']) && !empty($templateConf['template']) ) { $file .= '/' . $templateConf['template']; } } else { // use default template $file = $this->templateConf['pirum']['templatedir']; if (!empty($this->templateConf['pirum']['template'])) { $file .= '/' . $this->templateConf['pirum']['template']; } } $file .= "/$name"; return $file; } protected function buildCss() { if (file_exists($file = $this->getTemplateFile('pirum.css')) || file_exists($file = $this->buildDir.'/templates/pirum.css')) { $content = file_get_contents($file); } else { // fallback in case of external template is not readable $content = <<buildDir.'/pirum.css', $content); } protected function buildIndex() { $this->formatter and print $this->formatter->formatSection('INFO', "Building index."); $version = Pirum_CLI::version(); if (file_exists($file = $this->getTemplateFile('index.html')) || file_exists($file = $this->buildDir.'/templates/index.html')) { ob_start(); include $file; $html = ob_get_clean(); file_put_contents($this->buildDir.'/index.html', $html); return; } // fallback in case of external template is not readable ob_start(); ?> <?php echo $this->server->summary ?>

server->summary ?>

Registering the channel:
pear channel-discover server->name ?>
Listing available packages:
pear remote-list -c server->alias ?>
Installing a package:
pear install server->alias ?>/package_name
Installing a specific version/stability:
pear install server->alias ?>/package_name-1.0.0
pear install server->alias ?>/package_name-beta
Receiving updates via a feed:
server->url ?>/feed.xml
packages as $package): $deps = array(); if (isset($package['deps']['required']['package'])) { if (isset($package['deps']['required']['package']['name'])) { $package['deps']['required']['package'] = array($package['deps']['required']['package']); } foreach ($package['deps']['required']['package'] as $p) { if (isset($p['conflicts'])) { continue; } if (!isset($deps[$p['channel']])) { $deps[$p['channel']] = array(); } $deps[$p['channel']][] = $p; } } $grps = array(); if (isset($package['deps']['group'])) { $grps = $package['deps']['group']; } ?>

-

server->alias.'/'.$package['name'].'#'.$grp['attribs']['name'].' ('.$grp['attribs']['hint'].')'; } $groups = implode('
', $groups); ?> (as '.$maintainer['role'].')'; } $maintainers = implode(', ', $maintainers); ?> server->url}/get/{$package['name']}-{$release['version']}.tgz\">{$release['version']}({$release['stability']})"; } $releases = implode(', ', $releases); ?>
Install command install server->alias ?>/
Install groups:
License' . $package['license'] . '') ?>
Dependencies
$ch_deps): ?>
Maintainers
Releases

The server->name ?> PEAR Channel Server is proudly powered by Pirum

buildDir.'/index.html', $index); } protected function buildComposerRepository() { $this->formatter and print $this->formatter->formatSection('INFO', "Building composer repository."); $repository = array(); $vendor = !empty($this->server->alias) ? $this->server->alias : $this->server->name; foreach ($this->packages as $package) { // standard package fields $packageDef = array('versions' => array()); $packageDef['name'] = 'pear-'.$vendor.'/'.$package['name']; $packageDef['description'] = !empty($package['summary']) ? $package['summary'] : $package['description']; // package maintainers foreach ($package['current_maintainers'] as $maintainer) { $packageDef['maintainers'][] = array( 'name' => $maintainer['name'], 'email' => $maintainer['email'], 'homepage' => $maintainer['url'], ); } $repository[$packageDef['name']] = $packageDef; $pearChannels = array(); foreach ($package['releases'] as $release) { // standard version fields $version = array( 'name' => $packageDef['name'], 'description' => $packageDef['description'], 'dist' => array( 'type' => 'pear', 'url' => $this->server->url.'/get/'.$package['name'].'-'.$release['version'].'.tgz', ), ); if (isset($release['license'])) { $version['license'] = $release['license']; } if (isset($release['version'])) { $version['version'] = $release['version']; } if (isset($release['date'])) { $version['time'] = date(DATE_ATOM, strtotime($release['date'])); } // version requirements if (isset($release['php'])) { $version['require']['php'] = '>='.$release['php']; } if (isset($release['deps']) && $deps = @unserialize($release['deps'])) { $depsTypes = array( 'required' => 'require', 'optional' => 'suggest', ); foreach ($depsTypes as $pearType => $composerType) { $requirements = isset($deps[$pearType]) ? $deps[$pearType] : array(); foreach ($requirements as $reqType => $spec) { // wrap single requirements as an array of requirements if (isset($spec['name'])) { $spec = array($spec); } if ('php' === $reqType) { $version[$composerType]['php'] = $this->createComposerConstraint($spec); } elseif ('package' === $reqType) { foreach ($spec as $key => $value) { if (isset($value['providesextension'])) { // skip PECL dependencies continue; } if (isset($value['uri'])) { // skip uri-based dependencies continue; } if (is_array($value)) { $dataKey = $value['name']; if (false === strpos($dataKey, '/')) { $channelUrl = 'http://'.$value['channel']; if (!isset($pearChannels[$channelUrl])) { try { $channelInfo = @new SimpleXMLElement($channelUrl.'/channel.xml', null, true); } catch (Exception $e) { throw new Exception(sprintf('Channel "%s" could not be reached for package %s version %s.', $channelUrl, $package['name'], $version['version'])); } if ($tmp = (string) $channelInfo->suggestedalias[0]) { $pearChannels[$channelUrl] = $tmp; } else { $pearChannels[$channelUrl] = (string) $channelInfo->name[0]; } } $dataKey = $pearChannels[$channelUrl].'/'.$dataKey; } $version[$composerType]['pear-'.$dataKey] = $this->createComposerConstraint($value); } } } elseif ('extension' == $reqType) { foreach ($spec as $key => $value) { $dataKey = 'ext-' . $value['name']; $version[$composerType][$dataKey] = $this->createComposerConstraint($value); } } } } } // version authors foreach ($package['maintainers'] as $maintainer) { $version['authors'][] = array( 'name' => $maintainer['name'], 'email' => $maintainer['email'], 'homepage' => $maintainer['url'], ); } // autoload $version['autoload'] = $this->guessComposerAutoload($release['info']->getPackageXml()); $repository[$packageDef['name']]['versions'][$version['version']] = $version; } } if (version_compare(PHP_VERSION, '5.3.0', '>=')) { file_put_contents($this->buildDir.'/packages.json',json_encode($repository, JSON_PRETTY_PRINT)); } else { file_put_contents($this->buildDir.'/packages.json',json_encode($repository)); } } protected function createComposerConstraint(array $data) { if (!isset($data['min']) && !isset($data['max'])) { return '*'; } $versions = array(); if (isset($data['min'])) { $versions[] = '>=' . $data['min']; } if (isset($data['max'])) { $versions[] = '<=' . $data['max']; } return implode(',', $versions); } protected function guessComposerAutoload(SimpleXMLElement $packageInfo) { $files = array(); if (count($packageInfo->contents->dir)) { foreach ($packageInfo->contents->dir as $dir) { if (count($dir->file)) { foreach ($dir->file as $file) { if ('php' !== (string) $file['role']) { continue; } $files[] = ltrim($dir['name'].$file['name'], '/'); } } } } // reduce to meaningful unique paths while (true) { foreach ($files as $key => $file) { foreach ($files as $key2 => $file2) { if ($key === $key2) { continue; } if (false !== strpos($file, '/') && dirname($file) === dirname($file2)) { unset($files[$key2]); $files[$key] = dirname($file); continue 3; } if (0 === strpos($file2, $file.'/') || $file === $file2) { unset($files[$key2]); continue 3; } } } break; } return array( 'classmap' => $files ? $files : array(''), ); } protected function buildReleasePackages() { $this->formatter and print $this->formatter->formatSection('INFO', "Building releases."); mkdir($this->buildDir.'/rest/r', 0777, true); foreach ($this->packages as $package) { mkdir($dir = $this->buildDir.'/rest/r/'.strtolower($package['name']), 0777, true); $this->buildReleasePackage($dir, $package); } } protected function buildReleasePackage($dir, $package) { $this->formatter and print $this->formatter->formatSection('INFO', "Building releases for {$package['name']}."); $url = strtolower($package['name']); $alpha = ''; $beta = ''; $stable = ''; $snapshot = ''; $allreleases = ''; $allreleases2 = ''; $first = true; $this->formatter and print $this->formatter->formatSection('INFO', 'Building release', false); foreach ($package['releases'] as $release) { if ('stable' == $release['stability'] && !$stable) { $stable = $release['version']; } elseif ('beta' == $release['stability'] && !$beta) { $beta = $release['version']; } elseif ('alpha' == $release['stability'] && !$alpha) { $alpha = $release['version']; } elseif ('snapshot' == $release['stability'] && !$snapshot) { $snapshot = $release['version']; } $allreleases .= << {$release['version']} {$release['stability']} EOF; $allreleases2 .= << {$release['version']} {$release['stability']} {$release['php']} EOF; $this->buildRelease($dir, $package, $release, $first); $first = false; } $this->formatter and print "\n"; if (count($package['releases'])) { file_put_contents($dir.'/latest.txt', $package['releases'][0]['version']); } if ($stable) { file_put_contents($dir.'/stable.txt', $stable); } if ($beta) { file_put_contents($dir.'/beta.txt', $beta); } if ($alpha) { file_put_contents($dir.'/alpha.txt', $alpha); } if ($snapshot) { file_put_contents($dir.'/snapshot.txt', $snapshot); } file_put_contents($dir.'/allreleases.xml', <<

{$package['name']}

{$this->server->name} $allreleases
EOF ); file_put_contents($dir.'/allreleases2.xml', <<

{$package['name']}

{$this->server->name} $allreleases2
EOF ); } protected function buildRelease($dir, $package, $release, $first) { !$first and $this->formatter and print $this->formatter->format(','); $this->formatter and print $this->formatter->format(' ' . $release['version']); $url = strtolower($package['name']); reset($release['maintainers']); $maintainer = current($release['maintainers']); file_put_contents($dir.'/'.$release['version'].'.xml', <<

{$package['name']}

{$this->server->name} {$release['version']} {$release['stability']} {$package['license']} {$maintainer['nickname']} {$package['summary']} {$package['description']} {$release['date']} {$release['notes']} {$release['filesize']} {$this->server->url}/get/{$package['name']}-{$release['version']}
EOF ); file_put_contents($dir.'/v2.'.$release['version'].'.xml', <<

{$package['name']}

{$this->server->name} {$release['version']} {$release['api_version']} {$release['php']} {$release['stability']} {$package['license']} {$maintainer['nickname']} {$package['summary']} {$package['description']} {$release['date']} {$release['notes']} {$release['filesize']} {$this->server->url}/get/{$package['name']}-{$release['version']}
EOF ); file_put_contents($dir.'/deps.'.$release['version'].'.txt', $release['deps']); $release['info']->copyPackageXml($dir."/package.{$release['version']}.xml"); } protected function buildPackages() { $this->formatter and print $this->formatter->formatSection('INFO', "Building packages."); mkdir($this->buildDir.'/rest/p', 0777, true); $packages = ''; foreach ($this->packages as $package) { $packages .= "

{$package['name']}

\n"; mkdir($dir = $this->buildDir.'/rest/p/'.strtolower($package['name']), 0777, true); $this->buildPackage($dir, $package); } file_put_contents($this->buildDir.'/rest/p/packages.xml', << {$this->server->name} $packages EOF ); } protected function buildPackage($dir, $package) { $this->formatter and print $this->formatter->formatSection('INFO', "Building package {$package['name']}."); $url = strtolower($package['name']); file_put_contents($dir.'/info.xml', <<

{$package['name']} {$this->server->name} Default {$package['license']} {$package['summary']} {$package['description']}

EOF ); $maintainers = ''; $maintainers2 = ''; foreach ($package['current_maintainers'] as $nickname => $maintainer) { $maintainers .= << {$nickname} {$maintainer['active']} EOF; $maintainers2 .= << {$nickname} {$maintainer['active']} {$maintainer['role']} EOF; } file_put_contents($dir.'/maintainers.xml', <<

{$package['name']}

{$this->server->name} $maintainers
EOF ); file_put_contents($dir.'/maintainers2.xml', <<

{$package['name']}

{$this->server->name} $maintainers2
EOF ); } protected function buildCategories() { $this->formatter and print $this->formatter->formatSection('INFO', "Building categories."); mkdir($this->buildDir.'/rest/c/Default', 0777, true); file_put_contents($this->buildDir.'/rest/c/categories.xml', << {$this->server->name} Default EOF ); file_put_contents($this->buildDir.'/rest/c/Default/info.xml', << Default {$this->server->name} Default Default category EOF ); $packages = ''; $packagesinfo = ''; foreach ($this->packages as $package) { $url = strtolower($package['name']); $packages .= "

{$package['name']}

\n"; $deps = ''; $releases = ''; foreach ($package['releases'] as $release) { $releases .= << {$release['version']} {$release['stability']} EOF; $deps .= << {$release['version']} {$release['deps']} EOF; } $packagesinfo .= <<

{$package['name']} {$this->server->name} Default {$package['license']} {$package['summary']} {$package['description']}

$releases $deps EOF; } file_put_contents($this->buildDir.'/rest/c/Default/packages.xml', << $packages EOF ); file_put_contents($this->buildDir.'/rest/c/Default/packagesinfo.xml', << $packagesinfo EOF ); } protected function buildMaintainers() { $this->formatter and print $this->formatter->formatSection('INFO', "Building maintainers."); mkdir($dir = $this->buildDir.'/rest/m/', 0777, true); $all = ''; foreach ($this->packages as $package) { foreach ($package['maintainers'] as $nickname => $maintainer) { $dir = $this->buildDir.'/rest/m/'.$nickname; $info = << {$nickname} {$maintainer['name']} {$maintainer['url']} EOF; if (!is_dir($dir)) { mkdir($dir, 0777, true); $all .= " {$nickname}\n"; } file_put_contents($dir.'/info.xml', $info); } } $all = << $all EOF; file_put_contents($this->buildDir.'/rest/m/allmaintainers.xml', $all); } protected function buildChannel() { $this->formatter and print $this->formatter->formatSection('INFO', "Building channel."); $suggestedalias = ''; if (!empty($this->server->alias)) { $suggestedalias = ' '.$this->server->alias.''; } $validator = ''; if (!empty($this->server->validatepackage) && !empty($this->server->validateversion)) { $validator = ' '.$this->server->validatepackage.''; } $content = << {$this->server->name} {$this->server->summary}{$suggestedalias} {$this->server->url}/rest/ {$this->server->url}/rest/ {$this->server->url}/rest/ {$this->server->url}/rest/ EOF; foreach ($this->server->mirror as $mirror) { $mirror = rtrim($mirror, '/'); $ssl = ''; if ('https' === substr($mirror, 0, 5)) { $ssl = ' ssl="yes"'; } $content .= << {$mirror}/rest/ {$mirror}/rest/ {$mirror}/rest/ {$mirror}/rest/ EOF; } $content .= <<{$validator} EOF; file_put_contents($this->buildDir.'/channel.xml', $content); } protected function extractInformationFromPackages() { $this->packages = array(); // get all package files $files = array(); foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->targetDir.'/get'), RecursiveIteratorIterator::LEAVES_ONLY) as $file) { if (!preg_match(Pirum_Package::PACKAGE_FILE_PATTERN, $file->getFileName(), $match)) { continue; } $files[$match['release']] = (string) $file; } // order files to have latest versions first uksort($files, 'version_compare'); $files = array_reverse($files); // get information for each package $packages = array(); foreach ($files as $file) { $package = new Pirum_Package($file); if (file_exists($file = $this->targetDir.'/rest/r/'.strtolower($package->getName()).'/package.'.$package->getVersion().'.xml')) { $package->loadPackageFromFile($file); } else { $package->loadPackageFromArchive(); } $packages[$file] = $package; } $first = true; $this->formatter and print $this->formatter->formatSection('INFO', 'Parsing package', false); foreach ($packages as $file => $package) { !$first and $this->formatter and print $this->formatter->format(','); $this->formatter and print $this->formatter->format(' ' . $package->getName() . ' ' . $package->getVersion()); if ($package->getChannel() != $this->server->name) { throw new Exception(sprintf('Package "%s" channel (%s) is not %s.', $package->getName(), $package->getChannel(), $this->server->name)); } if (!isset($this->packages[$package->getName()])) { $this->packages[$package->getName()] = array( 'name' => htmlspecialchars($package->getName()), 'license' => htmlspecialchars($package->getLicense()), 'licenseuri' => htmlspecialchars($package->getLicenseUri()), 'summary' => htmlspecialchars($package->getSummary()), 'description' => htmlspecialchars($package->getDescription()), 'extension' => $package->getProvidedExtension(), 'releases' => array(), 'maintainers' => array(), 'deps' => unserialize($package->getDeps()), 'current_maintainers' => $package->getMaintainers(), ); } $this->packages[$package->getName()]['releases'][] = array( 'version' => $package->getVersion(), 'api_version' => $package->getApiVersion(), 'stability' => $package->getStability(), 'date' => $package->getDate(), 'filesize' => $package->getFilesize(), 'php' => $package->getMinPhp(), 'deps' => $package->getDeps(), 'notes' => htmlspecialchars($package->getNotes()), 'maintainers' => $package->getMaintainers(), 'info' => $package, ); $this->packages[$package->getName()]['maintainers'] = array_merge($package->getMaintainers(), $this->packages[$package->getName()]['maintainers']); $first = false; } $this->formatter and print "\n"; ksort($this->packages); } protected function mirrorDir($build, $target) { if (!is_dir($target)) { mkdir($target, 0777, true); } $this->removeFilesFromDir($target, $build); $this->copyFiles($build, $target); } protected function copyFiles($build, $target) { $fp = opendir($build); while (false !== $file = readdir($fp)) { if (in_array($file, array('.', '..'))) { continue; } if (is_dir($build.'/'.$file)) { if (!is_dir($target.'/'.$file)) { mkdir($target.'/'.$file, 0777, true); } $this->copyFiles($build.'/'.$file, $target.'/'.$file); } else { rename($build.'/'.$file, $target.'/'.$file); } } closedir($fp); } protected function removeFilesFromDir($target, $build) { $fp = opendir($target); while (false !== $file = readdir($fp)) { if (in_array($file, array('.', '..'))) { continue; } if (is_dir($target.'/'.$file)) { if (!in_array($file, array('.svn', 'CVS'))) { $this->removeFilesFromDir($target.'/'.$file, $build.'/'.$file); if (!is_dir($build.'/'.$file)) { rmdir($target.'/'.$file); } } } else { unlink($target.'/'.$file); } } closedir($fp); } } /** * Parses a PEAR package and retrieves useful information from it. * * @package Pirum * @author Fabien Potencier */ class Pirum_Package { const PACKAGE_FILE_PATTERN = '#^(?P(?P.+)\-(?P[\d\.]+((?:RC|beta|alpha|dev|snapshot|a|b)\d*)?))\.tgz$#i'; protected $package; protected $tmpDir; protected $name; protected $version; protected $archive; protected $packageFile; public function __construct($archive) { $this->archive = $archive; if (!preg_match(self::PACKAGE_FILE_PATTERN, $filename = basename($archive), $match)) { throw new InvalidArgumentException(sprintf('The archive "%s" does not follow PEAR conventions.', $filename)); } $this->name = $match['name']; $this->version = $match['version']; $this->tmpDir = sys_get_temp_dir().'/pirum_package_'.uniqid(); mkdir($this->tmpDir, 0777, true); } public function __destruct() { Pirum_CLI::removeDir($this->tmpDir); } public function getPackageXml() { return $this->package; } public function getDate($format = 'Y-m-d H:i:s') { return date($format, strtotime($this->package->date.' '.$this->package->time)); } public function getLicense() { return (string) $this->package->license; } public function getLicenseUri() { return (string) $this->package->license['uri']; } public function getDescription() { return (string) $this->package->description; } public function getSummary() { return (string) $this->package->summary; } public function getChannel() { return (string) $this->package->channel; } public function getNotes() { return (string) $this->package->notes; } public function getFileSize() { return filesize($this->archive); } public function getApiVersion() { return (string) $this->package->version->api; } public function getApiStability() { return (string) $this->package->stability->api; } public function getStability() { return (string) $this->package->stability->release; } public function getMaintainers() { $maintainers = array(); foreach (array('lead', 'developer', 'contributor', 'helper') as $role) { foreach ($this->package->{$role} as $person) { $maintainers[(string) $person->user] = array( 'nickname' => (string) $person->user, 'role' => $role, 'email' => (string) $person->email, 'name' => (string) $person->name, 'url' => (string) $person->url, 'active' => strtolower((string) $person->active) == 'yes' ? 1 : 0, ); } } return $maintainers; } public function getDeps() { $deps = $this->XMLToArray($this->package->dependencies); if (isset($this->package->dependencies->group) && $this->package->dependencies->group->length > 0) { $deps['group'] = $this->XMLGroupDepsToArray($this->package->dependencies->group); } return serialize($deps); } public function getMinPhp() { return isset($this->package->dependencies->required->php->min) ? (string) $this->package->dependencies->required->php->min : null; } public function getName() { return $this->name; } public function getVersion() { return $this->version; } public function getProvidedExtension() { return isset($this->package->providesextension) ? (string)$this->package->providesextension : null; } public function copyPackageXml($target) { copy($this->packageFile, $target); } public function loadPackageFromFile($file) { $this->packageFile = $file; $this->package = new SimpleXMLElement(file_get_contents($file)); // check name if ($this->name != (string) $this->package->name) { throw new InvalidArgumentException(sprintf('The package.xml name "%s" does not match the name of the archive file "%s".', $this->package->name, $this->name)); } // check version if ($this->version != (string) $this->package->version->release) { throw new InvalidArgumentException(sprintf('The package.xml version "%s" does not match the version of the archive file "%s".', $this->package->version->release, $this->version)); } } public function loadPackageFromArchive() { if (!function_exists('gzopen')) { copy($this->archive, $this->tmpDir.'/archive.tgz'); system('cd '.$this->tmpDir.' && tar zxpf archive.tgz'); if (!is_file($this->tmpDir.'/package.xml')) { throw new InvalidArgumentException('The PEAR package does not have a package.xml file.'); } $this->loadPackageFromFile($this->tmpDir.'/package.xml'); return; } $gz = gzopen($this->archive, 'r'); if ($gz === false) { throw new Exception(sprintf('Failed extracting archive "%s"!', $this->archive)); } $tar = ''; while (!gzeof($gz)) { $tar .= gzread($gz, 10000); } gzclose($gz); while (strlen($tar)) { $filename = rtrim(substr($tar, 0, 100), chr(0)); $filesize = octdec(rtrim(substr($tar, 124, 12), chr(0))); if ($filename != 'package.xml') { $offset = $filesize % 512 == 0 ? $filesize : $filesize + (512 - $filesize % 512); $tar = substr($tar, 512 + $offset); continue; } $checksum = octdec(rtrim(substr($tar, 148, 8), chr(0))); $cchecksum = 0; $tar = substr_replace($tar, ' ', 148, 8); for ($i = 0; $i < 512; $i++) { $cchecksum += ord($tar[$i]); } if ($checksum != $cchecksum) { throw new InvalidArgumentException('The PEAR archive is not a valid archive.'); } $package = substr($tar, 512, $filesize); $this->packageFile = $this->tmpDir.'/package.xml'; file_put_contents($this->packageFile, $package); $this->loadPackageFromFile($this->tmpDir.'/package.xml'); return; } throw new InvalidArgumentException('The PEAR package does not have a package.xml file.'); } protected function XMLToArray($xml) { $array = array(); foreach ($xml->children() as $element => $value) { $key = (string) $element; $value = count($value->children()) ? $this->XMLToArray($value) : (string) $value; if (array_key_exists($key, $array)) { if (!is_array($array[$key]) || !isset($array[$key][0])) { $array[$key] = array($array[$key]); } $array[$key][] = $value; } else { $array[$key] = $value; } } return $array; } protected function XMLGroupDepsToArray($xml) { if ($xml->count() == 0) { return array(); } $deps = array(); foreach ($xml as $group) { // Extract group attribs (shared by all children) $attribs = array(); foreach ($group->attributes() as $key => $val) { $attribs[$key] = (string) $val; } // Create one group dependency per childern foreach ($group->children() as $type => $value) { $deps[] = array('attribs' => $attribs, $type => $this->XMLToArray($value)); } } return $deps; } } /** * Command line colorizer for Pirum. * * @package Pirum * @author Fabien Potencier */ class Pirum_CLI_Formatter { protected $styles = array( 'ERROR_SECTION' => array('bg' => 'red', 'fg' => 'white'), 'INFO_SECTION' => array('bg' => 'green', 'fg' => 'white'), 'COMMENT_SECTION' => array('bg' => 'yellow', 'fg' => 'white'), 'ERROR' => array('fg' => 'red'), 'INFO' => array('fg' => 'green'), 'COMMENT' => array('fg' => 'yellow'), ); protected $options = array('bold' => 1, 'underscore' => 4, 'blink' => 5, 'reverse' => 7, 'conceal' => 8); protected $foreground = array('black' => 30, 'red' => 31, 'green' => 32, 'yellow' => 33, 'blue' => 34, 'magenta' => 35, 'cyan' => 36, 'white' => 37); protected $background = array('black' => 40, 'red' => 41, 'green' => 42, 'yellow' => 43, 'blue' => 44, 'magenta' => 45, 'cyan' => 46, 'white' => 47); protected $supportsColors; public function __construct() { $this->supportsColors = DIRECTORY_SEPARATOR != '\\' && function_exists('posix_isatty') && @posix_isatty(STDOUT); } /** * Formats a text according to the given style or parameters. * * @param string $text The text to style * @param string $style A style name * * @return string The styled text */ public function format($text = '', $style = 'NONE') { if (!$this->supportsColors) { return $text; } if ('NONE' == $style || !isset($this->styles[$style])) { return $text; } $parameters = $this->styles[$style]; $codes = array(); if (isset($parameters['fg'])) { $codes[] = $this->foreground[$parameters['fg']]; } if (isset($parameters['bg'])) { $codes[] = $this->background[$parameters['bg']]; } foreach ($this->options as $option => $value) { if (isset($parameters[$option]) && $parameters[$option]) { $codes[] = $value; } } return "\033[".implode(';', $codes).'m'.$text."\033[0m"; } /** * Formats a message within a section. * * @param string $section The section name * @param string $text The text message * @param boolean $newline End with line break? */ public function formatSection($section, $text, $newline = true) { $section = $style = array_key_exists($section, $this->styles) ? $section : 'INFO'; $section = " $section ".str_repeat(' ', max(0, 5 - strlen($section))); $style .= '_SECTION'; return sprintf(' %s %s%s', $this->format($section, $style), $text, $newline ? "\n" : ''); } }