Automating packages upload for PEAR channels running Chiara_PEAR_Server

Setting up Chiara_PEAR_Server with Crtx front end is a little bit confusing(even if you follow the official guide) but at least it happens just once ;)

Another important matter is maintaining your lovely PEAR channel up-to-date which, of course, assumes nice and easy way of uploading new packages. Actually Crtx’s web interface provides such a way and it works just fine for a couple of packages but if you have more than a dozen of them uploading new packages quickly becomes a boring process(e.g. we have 20+ packages at Limb3 PEAR channel)

That’s why I hacked up a script which automates this process quite a bit ;)

Here it comes:

publish_package_release.php

<?php
set_time_limit(0);

//set this equal to the url of your PEAR channel's admin area
$PEAR = 'http://url/to/your/admin.php';

if($argc < 4)
{
  echo "Usage: publish_package_release <dir> <user> <password>";
  exit(1);
}

$pkg = $argv[1];
$user = $argv[2];
$password = $argv[3];
$file = create_archive($pkg);
login($user, $password);
upload($file);
unlink($file);

echo "File '$file' uploaded\n";

function create_archive($dir)
{
  $old = getcwd();
  chdir($dir);

  if(!is_file("./VERSION"))
    fatal("VERSION file is not present\n");

  list($name, $version) = explode('-', trim(file_get_contents("./VERSION")));

  system("php package.php", $res);

  if($res != 0)
    fatal("Package XML creation error\n");

  system("pear package", $res);

  if($res != 0)
    fatal("'pear package' failed\n");

  unlink("package.xml");
  chdir($old);

  return "$dir/$name-$version.tgz";
}

function login($user, $password)
{
  global $PEAR;

  $login_form = array("user" => $user,
                      "password" => $password,
                      "login" => "Login");
  $curl = new CURL();
  $page = $curl->post($PEAR, $login_form);

  if(preg_match('~Invalid\s+Login~', $page))
    fatal("Could not login to admin page\n");
}

function upload($file)
{
  global $PEAR;

  $upload_form = array("MAX_FILE_SIZE" => "2097152",
                       "submitted" => '1',
                       "release" => "@$file",
                       "Submit" => 'Submit',
                       "f" => "0");
  $curl = new CURL();

  $page = $curl->post($PEAR, $upload_form);

  if(!preg_match('~Release\s+successfully\s+saved~', $page))
     fatal("Could not upload release '$file'\n");
}

class CURL
{
  protected $handle;
  protected $opts = array();

  function __construct()
  {
    $this->verbose(false);
  }

  protected function _ensureCurl()
  {
    if(!is_resource($this->handle))
     $this->handle = curl_init();
  }

  protected function _resetCurl()
  {
    if(is_resource($this->handle))
      curl_close($this->handle);
    $this->opts = array();
  }

  protected function _browserInit()
  {
    @mkdir('/tmp');
    $this->setOpt(CURLOPT_HEADER, 1);
    $this->setOpt(CURLOPT_USERAGENT, "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322;)");
    $this->setOpt(CURLOPT_FOLLOWLOCATION, 1);
    $this->setOpt(CURLOPT_COOKIEJAR, '/tmp/cookie.txt');
    $this->setOpt(CURLOPT_COOKIEFILE, '/tmp/cookie.txt');
    $this->setOpt(CURLOPT_SSL_VERIFYHOST, 0);
    $this->setOpt(CURLOPT_SSL_VERIFYPEER, 0);
    if($proxy = getenv('http_proxy'))
      $this->setOpt(CURLOPT_PROXY, $proxy);
  }

  function exec()
  {
    $this->_ensureCurl();

    foreach($this->opts as $opt => $value)
      curl_setopt($this->handle, $opt, $value);

    $res = curl_exec($this->handle);
    if (curl_errno($this->handle) == 0)
    {
      $this->_resetCurl();
      return $res;
    }
    else
    {
      $error = curl_error($this->handle);
      $this->_resetCurl();
      fatal($error . "\n");
    }
  }

  function get($url)
  {
    $this->_browserInit();
    $this->setOpt(CURLOPT_URL, $url);
    $this->setOpt(CURLOPT_RETURNTRANSFER, 1);
    return $this->exec();
  }

  function post($url, $vars)
  {
    $this->_browserInit();
    $this->setOpt(CURLOPT_URL, $url);
    $this->setOpt(CURLOPT_POST, 1);
    $this->setOpt(CURLOPT_RETURNTRANSFER, 1);
    $this->setOpt(CURLOPT_POSTFIELDS, $vars);
    return $this->exec();
  }

  function verbose($flag = true)
  {
    $this->setOpt(CURLOPT_VERBOSE, $flag ? 1 : 0);
  }

  function setOpt($opt, $value)
  {
    $this->opts[$opt] = $value;
  }
}

function fatal($msg)
{
  echo $msg;
  exit(1);
}

This is what this script does:

  1. It asks for the path to package directory, your PEAR channel username and password.
  2. It packages the new release using pear package command
  3. It uploads the new release onto the channel using curl extension

Here’s an example of its possible usage:

$ php publish_package_release.php /path/to/package/foo/dir bob secret

…after lots of output it should print something like: File ‘foo-0.0.1-beta.tgz’ uploaded

Before using this script you should make sure the following assumptions are met:

  1. As stated above, you should have Chiara_PEAR_Server with Crtx front end installed somewhere on the remote box and you should know your PEAR channel super admin login and password. You need to set an url to your admin area using $PEAR global variable in the script.
  2. Each package has package.php script in its top level directory and this script is responsible for generating package.xml descriptor required by low level pear package shell command which in its turn creates an uploadable tgz archive of the release. Here is an example of such a script from limb/active_record package.
  3. I’m using Crtx_PEAR_Channel_Frontend-0.3.1-alpha and haven’t tested this script with any other versions. While currently it works fine it may simply not work if there are any significant changes in the html layout of the upload page since this scripts emulates the browser and requires certain items be present on the page. Well, you are warned!

Leave a Reply