Index: drupalorg_project/drupalorg_project.module
===================================================================
RCS file: /Users/wright/drupal/local_repo/contributions/modules/drupalorg/drupalorg_project/drupalorg_project.module,v
retrieving revision 1.50
diff -u -p -r1.50 drupalorg_project.module
--- drupalorg_project/drupalorg_project.module	8 Jan 2011 15:26:40 -0000	1.50
+++ drupalorg_project/drupalorg_project.module	12 Jan 2011 19:35:17 -0000
@@ -971,3 +971,12 @@ function drupalorg_project_get_project_t
     );
   }
 }
+
+/**
+ * Implement hook_ctools_plugin_directory().
+ */
+function drupalorg_project_ctools_plugin_directory($module, $plugin) {
+  if ($module == 'project_release' && $plugin == 'release_packager') {
+    return "plugins/$plugin";
+  }
+}
Index: drupalorg_project/plugins/release_packager/DrupalorgProjectPackageRelease.class.php
===================================================================
RCS file: drupalorg_project/plugins/release_packager/DrupalorgProjectPackageRelease.class.php
diff -N drupalorg_project/plugins/release_packager/DrupalorgProjectPackageRelease.class.php
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ drupalorg_project/plugins/release_packager/DrupalorgProjectPackageRelease.class.php	16 Jan 2011 14:52:07 -0000
@@ -0,0 +1,255 @@
+<?php
+
+class DrupalorgProjectPackageRelease implements ProjectReleasePackagerInterface {
+  /**
+   * Configuration settings.
+   */
+  protected $conf = array(
+    'license' => '/tmp/skel/LICENSE.txt',
+    'tar' => '/usr/bin/tar',
+    'gzip' => '/usr/bin/gzip',
+    'zip' => '/usr/bin/zip',
+    'cvs' => '/usr/bin/cvs',
+    'ln' => '/bin/ln',
+    'rm' => '/bin/rm',
+    'core_repo_id' => 1,
+    'contrib_repo_id' => 2,
+  );
+
+  /// Protected data members of the class
+  protected $release_node;
+  protected $release_version = '';
+  protected $release_file_id = '';
+  protected $release_node_view_link = '';
+  protected $project_node;
+  protected $project_short_name = '';
+  protected $filenames = array();
+
+  protected $file_destination_root = '';
+  protected $file_destination_subdirectory = '';
+  protected $temp_directory = '';
+  protected $project_build_root = '';
+
+  /// Data about CVS respositories shared across all instances of this class.
+  protected static $repositories = array();
+
+  /// Have we initialized our shared static data yet?
+  protected static $shared_init = FALSE;
+
+  public function __construct($release_node, $file_destination_root, $file_destination_subdir, $temp_directory) {
+    // Make sure the shared data is initialized.
+    self::shared_init();
+
+    // Stash the release node this packager is going to be working on.
+    $this->release_node = $release_node;
+
+    // Save all the directory information.
+    $this->file_destination_root = $file_destination_root;
+    $this->file_destination_subdir = $file_destination_subdir;
+    $this->temp_directory = $temp_directory;
+
+    // Load the project for this release, using our static node_load() cache.
+    $this->project_node = project_release_packager_node_load($release_node->project_release['pid']);
+
+    // We use all of these a lot in a number of functions, so initialize them
+    // once here so we can just reuse them whenever we need them.
+    $this->project_short_name = $this->project_node->project['uri'];
+    $this->release_version = check_plain($release_node->project_release['version']);
+    $this->release_file_id = $this->project_short_name . '-' . $this->release_version;
+    $this->release_node_view_link = l(t('view'), 'node/' . $this->release_node->nid);
+    $this->project_build_root = $this->temp_directory . '/' . $this->project_short_name;
+
+    // Figure out the filenames we're going to be using for our packages.
+    $this->filenames['file_path_tgz'] = $file_destination_subdir . '/' . $this->release_file_id . '.tar.gz';
+    $this->filenames['full_dest_tgz'] = $file_destination_root . '/' . $this->filenames['file_path_tgz'];
+    $this->filenames['file_path_zip'] = $file_destination_subdir . '/' . $this->release_file_id . '.zip';
+    $this->filenames['full_dest_zip'] = $file_destination_root . '/' . $this->filenames['file_path_zip'];
+  }
+  
+  /**
+   * One-time initialization shared across all instances of this class.
+   */
+  protected static function shared_init() {
+    if (self::$shared_init) {
+      return;
+    }
+    
+    $query = db_query("SELECT rid, root, modules, name FROM {cvs_repositories}");
+    while ($repo = db_fetch_object($query)) {
+      self::$repositories[$repo->rid] = array(
+        'root' => $repo->root,
+        'modules' => $repo->modules,
+        'name' => $repo->name,
+      );
+    }
+
+    putenv("TERM=vt100");  // drush requires a terminal.
+    
+    self::$shared_init = TRUE;
+  }
+
+  public function createPackage(&$files, &$contents) {
+    // Files to ignore when checking timestamps:
+    $exclude = array('.', '..', 'LICENSE.txt');
+
+    // Remember if the tar.gz version of this release file already exists.
+    $tgz_exists = is_file($this->filenames['full_dest_tgz']);
+
+    // Figure out how to check this thing out from CVS.
+    $cvs_tag = check_plain($this->release_node->project_release['tag']);
+    $cvs_repo_id = $this->project_node->cvs['repository'];
+    $cvs_root = self::$repositories[$cvs_repo_id]['root'];
+    $cvs_module = self::$repositories[$cvs_repo_id]['modules'];
+    $cvs_export_from = $cvs_module . $this->project_node->cvs['directory'];
+
+    // For core, we want to checkout into a directory named via the version,
+    // e.g. "drupal-7.0".
+    if ($this->project_node->cvs['repository'] == $this->conf['core_repo_id']) {
+      $cvs_export_to = $this->release_file_id;
+    }
+    // For everything else, just use the project shortname.
+    else {
+      $cvs_export_to = $this->project_short_name;
+    }
+
+    // Clean up any old build directory if it exists.
+    // Don't use drupal_exec or return if this fails, we expect it to be empty.
+    exec("{$this->conf['rm']} -rf {$this->project_build_root}");
+
+    // Make a fresh build directory and move inside it.
+    if (!mkdir($this->project_build_root) || !drupal_chdir($this->project_build_root)) {
+      return 'error';
+    }
+
+    // Checkout this release from CVS, and see if we need to rebuild it
+    if (!drupal_exec("{$this->conf['cvs']} -d $cvs_root -q export -r $cvs_tag -d $cvs_export_to $cvs_export_from")) {
+      return 'error';
+    }
+    if (!is_dir("{$this->project_build_root}/$cvs_export_to")) {
+      wd_err("ERROR: %cvs_export_to does not exist after cvs export -r %cvs_tag -d %cvs_export_to %cvs_export_from", array('%cvs_export_to' => $cvs_export_to, '%cvs_tag' =>  $cvs_tag, '%cvs_export_dir' => $cvs_export_from), $release_node_view_link);
+      return 'error';
+    }
+
+    $info_files = array();
+    $youngest = $this->fileFindYoungest($cvs_export_to, 0, $exclude, $info_files);
+    if (!empty($release_node->project_release['rebuild']) && $tgz_exists && filemtime($this->filenames['full_dest_tgz']) + 300 > $youngest) {
+      // The existing tarball for this release is newer than the youngest
+      // file in the directory, we're done.
+      return 'no-op';
+    }
+
+    // Update any .info files with packaging metadata.
+    foreach ($info_files as $file) {
+      if (!$this->fixInfoFileVersion($file, $this->project_short_name, $this->release_version)) {
+        wd_err("ERROR: Failed to update version in %file, aborting packaging", array('%file' => $file), $release_node_view_link);
+        return 'error';
+      }
+    }
+
+    // Link not copy, since we want to preserve the date...
+    if (!drupal_exec("{$this->conf['ln']} -sf {$this->conf['license']} $cvs_export_to/LICENSE.txt")) {
+      return 'error';
+    }
+
+    // 'h' is for dereference, we want to include the files, not the links
+    if (!drupal_exec("{$this->conf['tar']} -ch --file=- $cvs_export_to | {$this->conf['gzip']} -9 --no-name > {$this->filenames['full_dest_tgz']}")) {
+      return 'error';
+    }
+    $files[$this->filenames['file_path_tgz']] = 0;
+
+    // If we're rebuilding, make sure the previous .zip is gone, since just
+    // running zip again with the same zip archive won't give us the semantics
+    // we want. For example, files that are removed in CVS will still be left
+    // in the .zip archive.
+    @unlink($this->filenames['full_dest_zip']);
+    if (!drupal_exec("{$this->conf['zip']} -rq {$this->filenames['full_dest_zip']} $cvs_export_to")) {
+      return 'error';
+    }
+    $files[$this->filenames['file_path_zip']] = 1;
+
+    return $tgz_exists ? 'rebuild' : 'success';
+  }
+
+  public function cleanupFailedBuild() {
+    $extensions = array('.tar.gz', '.zip');
+    $release_file_base = $this->file_destination_root . '/' . $this->file_destination_subdir . '/' . $this->release_file_id;
+
+    // Remove the main release files.
+    foreach ($extensions as $extension) {
+      $filename = $release_file_base . $extension;
+      if (file_exists($filename)) {
+        unlink($filename);
+      }
+    }
+  }
+
+  public function cleanupSuccessfulBuild() {
+    drupal_exec("{$this->conf['rm']} -rf {$this->project_build_root}");
+  }
+
+  /**
+   * Fix the given .info file with the specified version string
+   */
+  protected function fixInfoFileVersion($file, $project_short_name, $version) {
+    global $site_name;
+
+    $info = "\n; Information added by $site_name packaging script on " . format_date('Y-m-d') . "\n";
+    $info .= "version = \"$version\"\n";
+    // .info files started with 5.x, so we don't have to worry about version
+    // strings like "4.7.x-1.0" in this regular expression. If we can't parse
+    // the version (also from an old "HEAD" release), or the version isn't at
+    // least 6.x, don't add any "core" attribute at all.
+    $matches = array();
+    if (preg_match('/^((\d+)\.x)-.*/', $version, $matches) && $matches[2] >= 6) {
+      $info .= "core = \"$matches[1]\"\n";
+    }
+    $info .= "project = \"$project_short_name\"\n";
+    $info .= 'datestamp = "'. time() ."\"\n";
+    $info .= "\n";
+
+    if (!chmod($file, 0644)) {
+      wd_err("ERROR: chmod(@file, 0644) failed", array('@file' => $file));
+      return FALSE;
+    }
+    if (!$info_fd = fopen($file, 'ab')) {
+      wd_err("ERROR: fopen(@file, 'ab') failed", array('@file' => $file));
+      return FALSE;
+    }
+    if (!fwrite($info_fd, $info)) {
+      wd_err("ERROR: fwrite(@file) failed". '<pre>' . $info, array('@file' => $file));
+      return FALSE;
+    }
+    return TRUE;
+  }
+
+  /**
+   * Find the youngest (newest) file in a directory tree.
+   * Stolen wholesale from the original package-drupal.php script.
+   * Modified to also notice any files that end with ".info" and store
+   * all of them in the array passed in as an argument. Since we have to
+   *  recurse through the whole directory tree already, we should just
+   * record all the info we need in one pass instead of doing it twice.
+   */
+  public function fileFindYoungest($dir, $timestamp, $exclude, &$info_files) {
+    if (is_dir($dir)) {
+      $fp = opendir($dir);
+      while (FALSE !== ($file = readdir($fp))) {
+        if (!in_array($file, $exclude)) {
+          if (is_dir("$dir/$file")) {
+            $timestamp = $this->fileFindYoungest("$dir/$file", $timestamp, $exclude, $info_files);
+          }
+          else {
+            $mtime = filemtime("$dir/$file");
+            $timestamp = ($mtime > $timestamp) ? $mtime : $timestamp;
+            if (preg_match('/^.+\.info$/', $file)) {
+              $info_files[] = "$dir/$file";
+            }
+          }
+        }
+      }
+      closedir($fp);
+    }
+    return $timestamp;
+  }
+
+}
Index: drupalorg_project/plugins/release_packager/DrupalorgProjectPackageReleaseDistro.class.php
===================================================================
RCS file: drupalorg_project/plugins/release_packager/DrupalorgProjectPackageReleaseDistro.class.php
diff -N drupalorg_project/plugins/release_packager/DrupalorgProjectPackageReleaseDistro.class.php
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ drupalorg_project/plugins/release_packager/DrupalorgProjectPackageReleaseDistro.class.php	16 Jan 2011 15:02:41 -0000
@@ -0,0 +1,245 @@
+<?php
+
+/**
+ * Packaging plugin that handles packaged drupal distributions.
+ *
+ * This just extends the default packaging case to add the drush make logic.
+ */
+class DrupalorgProjectPackageReleaseDistro extends DrupalorgProjectPackageRelease implements ProjectReleasePackagerInterface {
+
+  public function __construct($release_node, $file_destination_root, $file_destination_subdir, $temp_directory) {
+    parent::__construct($release_node, $file_destination_root, $file_destination_subdir, $temp_directory);
+
+    // Full path to the drush executable. This also needs to define --include
+    // with the full path to the directory where drush_make is located. This
+    // is needed to manually include it as a searchable path for drush
+    // extensions, as this script's owner will not likely have a home
+    // directory to search.
+    $this->conf['drush'] = '/full/path/to/drush --include=/full/path/to/drush_make';
+  }
+
+  public function createPackage(&$files, &$contents) {
+
+    // First, do the default packaging.
+    $rval = parent::createPackage($files, $contents);
+
+    if ($rval == 'error' || $rval == 'no-op') {
+      // If we already failed or determined there's nothing to do, return
+      // immediately instead of doing any of the distro-specific packaging.
+      return $rval;
+    }
+
+    // Move inside the profile directory.
+    if (!drupal_chdir("{$this->project_build_root}/{$this->project_short_name}")) {
+      return 'error';
+    }
+
+    // In order for extended packaging to take place, the profile must have a
+    // file named drupal-org.make in the main directory of their profile.
+    $drupalorg_makefile = 'drupal-org.make';
+
+    if (file_exists($drupalorg_makefile)) {
+      // On packaged install profiles, we want the profile-only tarball to be
+      // the heaviest weight in the {project_release_file} table so it sinks
+      // to the bottom of various listings.
+      $files[$this->filenames['file_path_tgz']] = 10;
+      $files[$this->filenames['file_path_zip']] = 11;
+
+      // Search the .make file for the required 'core' attribute.
+      $info = drupal_parse_info_file($drupalorg_makefile);
+
+      // Only proceed if a core release was found.
+      if (!isset($info['core'])) {
+        wd_err("ERROR: %profile does not have the required 'core' attribute.", array('%profile' => $this->release_file_id), $release_node_view_link);
+        return 'error';
+      }
+      else {
+
+        // Basic sanity check for the format of the attribute. The CVS checkout
+        // attempt of core will handle the rest of the validation (ie, it will
+        // fail if a non-existant tag is specified.
+        if (!preg_match("/^(\d+)\.(\d+)(-[a-z0-9]+)?$/", $info['core'], $matches)) {
+          wd_err("ERROR: %profile specified an invalid 'core' attribute -- both API version and release are required.", array('%profile' => $this->release_file_id), $release_node_view_link);
+          return 'error';
+        }
+        else {
+          // Compare the Drupal API version in the profile's version string
+          // with the API version of core specificed in the .make file --
+          // these should match.
+          $profile_api_version = $matches[1];
+          $parts = explode('.', $this->release_version);
+          $release_api_version = $parts[0];
+          
+          if ($profile_api_version != $release_api_version) {
+            wd_err("ERROR: %profile specified an invalid 'core' attribute -- the API version must match the API version of the release.", array('%profile' => $this->release_file_id), $release_node_view_link);
+            return 'error';
+          }
+        }
+
+        // NO-CORE DISTRIBUTION.
+
+        $no_core_id = "{$this->release_file_id}-no-core";
+
+        // Build the drupal file path and the full file path for tgz and zip.
+        $no_core_file_path_tgz = "{$this->file_destination_subdir}/$no_core_id.tar.gz";
+        $no_core_full_dest_tgz = "{$this->file_destination_root}/$no_core_file_path_tgz";
+        $no_core_file_path_zip = "{$this->file_destination_subdir}/$no_core_id.zip";
+        $no_core_full_dest_zip = "{$this->file_destination_root}/$no_core_file_path_zip";
+
+        // Run drush_make to build the profile's contents.
+        // --drupal-org: Invoke drupal.org specific validation/processing.
+        // --drupal-org-build-root: Let the script know where to place it's
+        //   build-related files.
+        // --drupal-org-log-errors-to-file: Store build errors for later output.
+        // --drupal-org-log-package-items-to-file: Store package items for
+        //   later recording in the database.
+        if (!drupal_exec("{$this->conf['drush']} make --drupal-org --drupal-org-build-root={$this->project_build_root} --drupal-org-log-errors-to-file --drupal-org-log-package-items-to-file $drupalorg_makefile .")) {
+          // The build failed, get any output error messages and include them
+          // in the packaging error report.
+          $build_errors_file = "{$this->project_build_root}/build_errors.txt";
+          if (file_exists($build_errors_file)) {
+            $lines = file($build_errors_file, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
+            foreach ($lines as $line) {
+            	wd_err("ERROR: $line");
+            }
+          }
+          wd_err("ERROR: Build for %profile failed.", array('%profile' => $no_core_id), $release_node_view_link);
+          return 'error';
+        }
+
+        // Change into the profile build directory.
+        if (!drupal_chdir($this->project_build_root)) {
+          return 'error';
+        }
+
+        // Package the no-core distribution.
+        // 'h' is for dereference, we want to include the files, not the links
+        if (!drupal_exec("{$this->conf['tar']} -ch --file=- {$this->project_short_name} | {$this->conf['gzip']} -9 --no-name > $no_core_full_dest_tgz")) {
+          return 'error';
+        }
+        $files[$no_core_file_path_tgz] = 6;
+
+        @unlink($no_core_full_dest_zip);
+        if (!drupal_exec("{$this->conf['zip']} -rq $no_core_full_dest_zip {$this->project_short_name}")) {
+          return 'error';
+        }
+        $files[$no_core_file_path_zip] = 7;
+
+
+        // CORE DISTRIBUTION.
+
+        // Write a small .make file used to build core.
+        $core_version = $info['core'];
+        $core_build_dir = "drupal-$core_version";
+        $core_makefile = "$core_build_dir.make";
+        $this->createCoreMakeFile($core_version, $core_makefile);
+
+        // Run drush_make to build core.
+        if (!drupal_exec("{$this->conf['drush']} make $core_makefile $core_build_dir")) {
+          // The build failed, bail out.
+          wd_err("ERROR: Build for %core failed.", array('%core' => $core_build_dir), $release_node_view_link);
+          return 'error';
+        }
+
+        // Move the profile into place inside core.
+        if (!rename($this->project_short_name, "$core_build_dir/profiles/{$this->project_short_name}")) {
+          wd_err("ERROR: can't rename {$this->project_short_name} to $core_build_dir/profiles/{$this->project_short_name}");
+          return 'error';
+        }
+
+        $core_id = "{$this->release_file_id}-core";
+
+        // Build the drupal file path and the full file path for tgz and zip.
+        $core_file_path_tgz = "{$this->file_destination_subdir}/$core_id.tar.gz";
+        $core_full_dest_tgz = "{$this->file_destination_root}/$core_file_path_tgz";
+        $core_file_path_zip = "{$this->file_destination_subdir}/$core_id.zip";
+        $core_full_dest_zip = "{$this->file_destination_root}/$core_file_path_zip";
+
+        // Package the core distribution.
+        // 'h' is for dereference, we want to include the files, not the links
+        if (!drupal_exec("{$this->conf['tar']} -ch --file=- $core_build_dir | {$this->conf['gzip']} -9 --no-name > $core_full_dest_tgz")) {
+          return 'error';
+        }
+        // We want this to float to the top, so give it the lightest weight.
+        $files[$core_file_path_tgz] = 0;
+
+        @unlink($core_full_dest_zip);
+        if (!drupal_exec("{$this->conf['zip']} -rq $core_full_dest_zip $core_build_dir")) {
+          return 'error';
+        }
+        // We want the .zip version with core to be ligher than the non-core
+        // files, but heavier than .tar.gz.
+        $files[$core_file_path_zip] = 1;
+
+        // Development releases may have changed package contents -- clear out
+        // their package item summary so a fresh item summary will be inserted.
+        if ($release_node->project_release['rebuild'] && module_exists('project_package')) {
+          db_query("DELETE FROM {project_package_local_release_item} WHERE package_nid = %d", $nid);
+        }
+
+        // Core was built without the drupal.org drush extension, so the
+        // package item for core isn't in the package contents file. Retrieve
+        // it manually.
+        $core_tag = 'DRUPAL-'. str_replace('.', '-', $core_version);
+        if (!($core_release_nid = db_result(db_query("SELECT nid FROM {project_release_nodes} WHERE tag = '%s'", $core_tag)))) {
+          wd_err("ERROR: Can't find core release for $core_tag");
+          return 'error';
+        }
+        $contents[] = $core_release_nid;
+
+        // Retrieve the package contents for the release.
+        $package_contents_file = "{$this->project_build_root}/package_contents.txt";
+        if (file_exists($package_contents_file)) {
+          $lines = file($package_contents_file, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
+          foreach ($lines as $line) {
+            if (is_numeric($line)) {
+              $contents[] = $line;
+            }
+          }
+        }
+        else {
+          wd_err("ERROR: %file does not exist for %profile release.", array('%file' => $package_contents_file, '%profile' => $this->release_file_id), $release_node_view_link);
+          return 'error';
+        }
+      }
+    }
+    else {
+      wd_msg("No makefile for %profile profile -- skipping extended packaging.", array('%profile' => $this->release_file_id), $release_node_view_link);
+    }
+    return $rval;
+  }
+
+  public function cleanupFailedBuild() {
+    $extensions = array('.tar.gz', '.zip');
+    $release_file_base = $this->file_destination_root . '/' . $this->file_destination_subdir . '/' . $this->release_file_id;
+
+    foreach (array('', '-no-core', '-core') as $variant) {
+      foreach ($extensions as $extension) {
+        $filename = $release_file_base . $variant . $extension;
+        if (file_exists($filename)) {
+          unlink($filename);
+        }
+      }
+    }
+  }
+
+  /**
+   * Construct a .make file which will build Drupal core.
+   *
+   * This is a very simple 'bootstrap' .make file, which should only ever
+   * include the minimal package metadata to build core.
+   *
+   * All arguments should be in a format that drush_make can understand.
+   *
+   * @param $core_version
+   *   The core release to package with the profile.
+   * @param $core_makefile
+   *   The path to the make file to create.
+   */
+  protected function createCoreMakeFile($core_version, $core_makefile) {
+    $output = '';
+    $output .= "core = $core_version\n";
+    $output .= "projects[drupal] = $core_version\n";
+    file_put_contents($core_makefile, $output);
+  }
+}
Index: drupalorg_project/plugins/release_packager/drupalorg.inc
===================================================================
RCS file: drupalorg_project/plugins/release_packager/drupalorg.inc
diff -N drupalorg_project/plugins/release_packager/drupalorg.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ drupalorg_project/plugins/release_packager/drupalorg.inc	16 Jan 2011 13:01:05 -0000
@@ -0,0 +1,8 @@
+<?php
+
+$plugin = array(
+  'title' => t('Packaging logic for drupal.org projects'),
+  'packager' => array(
+    'class' => 'DrupalorgProjectPackageRelease',
+  ),
+);
Index: drupalorg_project/plugins/release_packager/drupalorg_distro.inc
===================================================================
RCS file: drupalorg_project/plugins/release_packager/drupalorg_distro.inc
diff -N drupalorg_project/plugins/release_packager/drupalorg_distro.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ drupalorg_project/plugins/release_packager/drupalorg_distro.inc	16 Jan 2011 13:01:20 -0000
@@ -0,0 +1,9 @@
+<?php
+
+$plugin = array(
+  'title' => t('Packaging logic for drupal.org installation profile distributions'),
+  'packager' => array(
+    'class' => 'DrupalorgProjectPackageReleaseDistro',
+    'parent' => 'drupalorg',  // plugin name, not class name
+  ),
+);
