#!/usr/bin/php $drupal_root, 'site_name' => $site_name, 'cvs_root' => $cvs_root, 'tmp_root' => $tmp_root, 'license' => $license, 'trans_install' => $trans_install, ); foreach ($vars as $name => $val) { if (empty($val)) { print "ERROR: \"\$$name\" variable not set, aborting\n"; $fatal_err = true; } } if (!empty($fatal_err)) { exit(1); } putenv("CVSROOT=$cvs_root"); putenv("TERM=vt100"); // drush requires a terminal. $script_name = $argv[0]; // Find what kind of packaging we need to do if (!empty($argv[1])) { $task = $argv[1]; } else { $task = 'tag'; } switch($task) { case 'tag': case 'branch': case 'check': case 'repair': break; default: print "ERROR: $argv[0] invoked with invalid argument: \"$task\"\n"; exit (1); } $project_id = 0; if (!empty($argv[2])) { $project_id = $argv[2]; } // Setup variables for Drupal bootstrap $_SERVER['HTTP_HOST'] = $site_name; $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $_SERVER['REQUEST_URI'] = '/' . $script_name; $_SERVER['SCRIPT_NAME'] = '/' . $script_name; $_SERVER['PHP_SELF'] = '/' . $script_name; $_SERVER['SCRIPT_FILENAME'] = $_SERVER['PWD'] . '/' . $script_name; $_SERVER['PATH_TRANSLATED'] = $_SERVER['SCRIPT_FILENAME']; if (!chdir($drupal_root)) { print "ERROR: Can't chdir($drupal_root): aborting.\n"; exit(1); } // Force the right umask while this script runs, so that everything is created // with sane file permissions. umask(0022); require_once 'includes/bootstrap.inc'; drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); // We have to initialize the theme() system before we leave $drupal_root $hack = theme('placeholder', 'hack'); if ($task == 'check' || $task == 'repair') { verify_packages($task, $project_id); } else { initialize_tmp_dir($task); initialize_repository_info(); package_releases($task, $project_id); // Now that we're done, clean out the tmp/task dir we created chdir($tmp_root); drupal_exec("$rm -rf $tmp_dir"); } if ($task == 'branch') { // Clear any cached data set to expire. cache_clear_all(NULL, 'cache_project_release'); } elseif ($task == 'repair') { // Clear all cached data cache_clear_all('*', 'cache_project_release', TRUE); } // ------------------------------------------------------------ // Functions: main work // ------------------------------------------------------------ function package_releases($type, $project_id = 0) { global $drupal_root, $wd_err_msg; global $php, $project_release_create_history; $rel_node_join = ''; $where_args = array(); if ($type == 'tag') { $where = " AND (prn.rebuild = %d)"; $where_args[] = 0; // prn.rebuild $plural = t('tags'); } elseif ($type == 'branch') { $where = " AND (prn.rebuild = %d)"; $where_args[] = 1; // prn.rebuild $plural = t('branches'); if (empty($project_id)) { wd_msg("Starting to package all snapshot releases."); } else { wd_msg("Starting to package snapshot releases for project id: @project_id.", array('@project_id' => $project_id), l(t('view'), 'node/' . $project_id)); } } else { wd_err("ERROR: package_releases() called with unknown type: %type", array('%type' => $type)); return FALSE; } $args = array(); $args[] = 1; // nr.status = 1. $args[] = 1; // np.status = 1. $args[] = 1; // prp.releases = 1. if (!empty($project_id)) { $where .= ' AND prn.pid = %d'; $where_args[] = $project_id; } $args = array_merge($args, $where_args); $query = db_query("SELECT pp.uri, prn.nid, prn.pid, prn.tag, prn.version, c.directory, c.rid FROM {project_release_nodes} prn INNER JOIN {node} nr ON prn.nid = nr.nid INNER JOIN {project_projects} pp ON prn.pid = pp.nid INNER JOIN {node} np ON prn.pid = np.nid INNER JOIN {project_release_projects} prp ON prp.nid = prn.pid INNER JOIN {cvs_projects} c ON prn.pid = c.nid WHERE nr.status = %d AND np.status = %d AND prp.releases = %d " . $where . ' ORDER BY pp.uri', $args); $num_built = 0; $num_considered = 0; $project_nids = array(); // Read everything out of the query immediately so that we don't leave the // query object/connection open while doing other queries. $releases = array(); while ($release = db_fetch_object($query)) { $releases[$release->nid] = $release; } foreach ($releases as $release) { $wd_err_msg = array(); $version = $release->version; $project_short_name = $release->uri; $tag = $release->tag; $nid = $release->nid; $pid = $release->pid; $tag = ($tag == 'TRUNK') ? 'HEAD' : $tag; $project_short_name = escapeshellcmd($project_short_name); $version = escapeshellcmd($version); $tag = escapeshellcmd($tag); $release_dir = ''; db_query("DELETE FROM {project_release_package_errors} WHERE nid = %d", $nid); if ($release->rid == DRUPAL_CORE_REPOSITORY_ID) { $built = package_release_core($type, $nid, $project_short_name, $version, $tag); } else { $release_dir = escapeshellcmd($release->directory); $built = package_release_contrib($type, $nid, $project_short_name, $version, $tag, $release_dir); } chdir($drupal_root); if ($built == 'success') { $num_built++; $project_nids[$pid] = TRUE; } $num_considered++; if (count($wd_err_msg)) { db_query("INSERT INTO {project_release_package_errors} (nid, messages) values (%d, '%s')", $nid, serialize($wd_err_msg)); } } if ($num_built || $type == 'branch') { if (!empty($project_id)) { wd_msg("Done packaging releases for @project_short_name from !plural: !num_built built, !num_considered considered.", array('@project_short_name' => $project_short_name, '!plural' => $plural, '!num_built' => $num_built, '!num_considered' => $num_considered)); } else { wd_msg("Done packaging releases from !plural: !num_built built, !num_considered considered.", array('!plural' => $plural, '!num_built' => $num_built, '!num_considered' => $num_considered)); } } } function package_release_core($type, $nid, $project_short_name, $version, $tag) { global $tmp_dir, $repositories, $dest_root, $dest_rel; global $cvs, $tar, $zip, $rm; if (!drupal_chdir($tmp_dir)) { return 'error'; } $release_file_id = $project_short_name . '-' . $version; $release_node_view_link = l(t('view'), 'node/' . $nid); $file_path_tgz = $dest_rel . '/' . $release_file_id . '.tar.gz'; $full_dest_tgz = $dest_root . '/' . $file_path_tgz; $file_path_zip = $dest_rel . '/' . $release_file_id . '.zip'; $full_dest_zip = $dest_root . '/' . $file_path_zip; // Remember if the zip version of this release file already exists. $zip_exists = is_file($full_dest_zip); if ($zip_exists) { return 'no-op'; } if (!is_file($full_dest_tgz)) { return 'no-op'; } // Don't use drupal_exec or return if this fails, we expect it to be empty. exec("$rm -rf $tmp_dir/$release_file_id"); // Unpack the existing tarball (so we have the right .info stuff from the // original packaging run). if (!drupal_exec("$tar -zxf $full_dest_tgz")) { return 'error'; } if (!drupal_exec("$zip -rq $full_dest_zip $release_file_id")) { return 'error'; } $files[$file_path_zip] = 1; // As soon as the zip exists, we want to update the DB about it. package_release_update_node($nid, $files); wd_msg("Packaged %id.", array('%id' => $release_file_id), $view_link); // Don't consider failure to remove this directory a build failure. drupal_exec("$rm -rf $tmp_dir/$release_file_id"); return 'success'; } function package_release_contrib($type, $nid, $project_short_name, $version, $tag, $release_dir) { global $tmp_dir, $repositories, $dest_root, $dest_rel; global $cvs, $tar, $zip, $rm; $parts = split('/', $release_dir); // modules, themes, theme-engines, profiles, or translations $contrib_type = $parts[1]; // specific directory (same as uri) $project_short_name = $parts[2]; $project_build_root = "$tmp_dir/$project_short_name"; $release_file_id = $project_short_name . '-' . $version; $release_node_view_link = l(t('view'), 'node/' . $nid); $file_path_tgz = $dest_rel . '/' . $release_file_id . '.tar.gz'; $full_dest_tgz = $dest_root . '/' . $file_path_tgz; $file_path_zip = $dest_rel . '/' . $release_file_id . '.zip'; $full_dest_zip = $dest_root . '/' . $file_path_zip; // Remember if the tar.gz version of this release file already exists. $zip_exists = is_file($full_dest_zip); if ($zip_exists) { return 'no-op'; } if ($contrib_type == 'translations' && $project_short_name != 'drupal-pot') { // We're not going to repackage translations. return 'no-op'; } if (!is_file($full_dest_tgz)) { return 'no-op'; } // 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("$rm -rf $project_build_root"); // Make a fresh build directory and move inside it. if (!mkdir($project_build_root) || !drupal_chdir($project_build_root)) { return 'error'; } // 'h' is for dereference, we want to include the files, not the links if (!drupal_exec("$tar -zxf $full_dest_tgz")) { return 'error'; } if (!drupal_exec("$zip -rq $full_dest_zip $project_short_name")) { return 'error'; } $files[$file_path_zip] = 1; // Clean up build directory exec("$rm -rf ./$project_short_name"); if ($contrib_type == 'profiles') { // Build the drupal file path and the full file path for tgz and zip. $no_core_id = "$release_file_id-no-core"; $no_core_file_path_tgz = "$dest_rel/$no_core_id.tar.gz"; $no_core_full_dest_tgz = "$dest_root/$no_core_file_path_tgz"; $no_core_file_path_zip = "$dest_rel/$no_core_id.zip"; $no_core_full_dest_zip = "$dest_root/$no_core_file_path_zip"; $core_id = "$release_file_id-core"; $core_file_path_tgz = "$dest_rel/$core_id.tar.gz"; $core_full_dest_tgz = "$dest_root/$core_file_path_tgz"; $core_file_path_zip = "$dest_rel/$core_id.zip"; $core_full_dest_zip = "$dest_root/$core_file_path_zip"; if (is_file($no_core_full_dest_tgz)) { // 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[$file_path_zip] = 11; if (!is_file($no_core_full_dest_zip)) { if (!drupal_exec("$tar -zxf $no_core_full_dest_tgz")) { return 'error'; } if (!drupal_exec("$zip -rq $no_core_full_dest_zip $project_short_name")) { return 'error'; } $files[$no_core_file_path_zip] = 7; exec("$rm -rf ./$project_short_name"); } } if (is_file($core_full_dest_tgz)) { if (!drupal_exec("$tar -zxf $core_full_dest_tgz")) { return 'error'; } $fp = opendir($project_build_root); while (FALSE !== ($file = readdir($fp))) { if (strpos($file, 'drupal') !== FALSE) { $core_build_dir = $file; break; } } closedir($fp); if (empty($core_build_dir)) { wd_err("ERROR: Could not find drupal directory for %filename", array('%filename' => $core_full_dest_tgz)); return 'error'; } if (!drupal_exec("$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; } } // Now that all the files exist, update the DB about them. package_release_update_node($nid, $files); wd_msg("Packaged %id.", array('%id' => $release_file_id), $view_link); // Don't consider failure to remove this directory a build failure. drupal_exec("$rm -rf $project_build_root"); return 'success'; } // ------------------------------------------------------------ // Functions: utility methods // ------------------------------------------------------------ /** * Wrapper for exec() that logs errors to the watchdog. * @param $cmd * String of the command to execute (assumed to be safe, the caller is * responsible for calling escapeshellcmd() if necessary). * @return true if the command was successful (0 exit status), else false. */ function drupal_exec($cmd) { // Made sure we grab stderr, too... exec("$cmd 2>&1", $output, $rval); if ($rval) { wd_err("ERROR: %cmd failed with status !rval" . '
' . implode("\n", array_map('htmlspecialchars', $output)), array('%cmd' => $cmd, '!rval' => $rval));
    return false;
  }
  return true;
}

/**
 * Wrapper for chdir() that logs errors to the watchdog.
 * @param $dir Directory to change into.
 * @return true if the command was successful (0 exit status), else false.
 */
function drupal_chdir($dir) {
  if (!chdir($dir)) {
    wd_err("ERROR: Can't chdir(@dir)", array('@dir' => $dir));
    return false;
  }
  return true;
}

/// TODO: remove this before the final script goes live -- debugging only.
function wprint($var) {
  watchdog('package_debug', '
' . var_export($var, TRUE));
}

/**
 * Wrapper function for watchdog() to log notice messages. Uses a
 * different watchdog message type depending on the task (branch vs. tag).
 */
function wd_msg($msg, $variables = array(), $link = NULL) {
  global $task;
  watchdog('package_' . $task, $msg, $variables, WATCHDOG_NOTICE, $link);
  echo t($msg, $variables) . "\n";
}

/**
 * Wrapper function for watchdog() to log error messages.
 */
function wd_err($msg, $variables = array(), $link = NULL) {
  global $wd_err_msg;
  if (!isset($wd_err_msg)) {
    $wd_err_msg = array();
  }
  watchdog('package_error', $msg, $variables, WATCHDOG_ERROR, $link);
  echo t($msg, $variables) . "\n";
  $wd_err_msg[] = t($msg, $variables);
}

/**
 * Wrapper function for watchdog() to log messages about checking
 * package metadata.
 */
function wd_check($msg, $variables = array(), $link = NULL) {
  watchdog('package_check', $msg, $variables, WATCHDOG_NOTICE, $link);
  echo t($msg, $variables) . "\n";
}

/**
 * Initialize the tmp directory. Use different subdirs for building
 * snapshots than official tags, so there's no potential directory
 * collisions and race conditions if both are running at the same time
 * (due to how long it takes to complete a branch snapshot run, and
 * how often we run this for tag-based releases).
 */
function initialize_tmp_dir($task) {
  global $tmp_dir, $tmp_root, $rm;

  if (!is_dir($tmp_root) && !@mkdir($tmp_root, 0777, TRUE)) {
    wd_err("ERROR: mkdir(@dir) (tmp_root) failed", array('@dir' => $tmp_root));
    exit(1);
  }

  // Use a tmp directory *specific* to this invocation, so that we don't
  // clobber other runs if the script is invoked twice (e.g. via cron and
  // manually, etc).
  $tmp_dir = $tmp_root . '/' . $task . '.' . getmypid();
  if (is_dir($tmp_dir)) {
    // Make sure we start with a clean slate
    drupal_exec("$rm -rf $tmp_dir/*");
  }
  else if (!@mkdir($tmp_dir, 0777, TRUE)) {
    wd_err("ERROR: mkdir(@dir) failed", array('@dir' => $tmp_dir));
    exit(1);
  }
}

/**
 * Initialize info from the {cvs_repositories} table, since there are
 * usually only a tiny handful of records, and it'll be faster to do
 * whatever we need via php than another JOIN...
 */
function initialize_repository_info() {
  global $repositories;
  $query = db_query("SELECT rid, root, modules, name FROM {cvs_repositories}");
  while ($repo = db_fetch_object($query)) {
    $repositories[$repo->rid] = array('root' => $repo->root, 'modules' => $repo->modules, 'name' => $repo->name);
  }
}

/**
 * Update the DB with the new file info for a given release node.
 *
 * @param $nid
 *   The node ID of the release node to update.
 * @param $files
 *   Array of files to add to the release node. The keys are filenames, and
 *   the values are the weight for the {project_release_file}.weight column.
 */
function package_release_update_node($nid, $files) {
  global $drupal_root, $dest_root;

  // PHP will cache the results of stat() and give us stale answers
  // here, unless we manually tell it otherwise!
  clearstatcache();

  // Make sure we're back at the webroot so node_load() and node_save()
  // can always find any files they (and the hooks they invoke) need.
  if (!drupal_chdir($drupal_root)) {
    return FALSE;
  }

  // If the site is using DB replication, force this node_load() to use the
  // primary database to avoid node_load() failures.
  if (function_exists('db_set_ignore_slave')) {
    db_set_ignore_slave();
  }

  foreach ($files as $file_path => $file_weight) {
    // Compute the metadata for this file that we care about.
    $full_path = $dest_root . '/' . $file_path;
    $file_name = basename($file_path);
    $file_date = filemtime($full_path);
    $file_size = filesize($full_path);
    $file_hash = md5_file($full_path);
    $file_mime = file_get_mimetype($full_path);

    // First, see if we already have this file for this release node
    $file_data = db_fetch_object(db_query("SELECT prf.* FROM {project_release_file} prf INNER JOIN {files} f ON prf.fid = f.fid WHERE prf.nid = %d AND f.filename = '%s'", $nid, $file_name));

    // Insert or update the record in the DB as need.
    if (empty($file_data)) {
      // Don't have this file, insert a new record.
      $uid = db_result(db_query("SELECT uid FROM {node} WHERE nid = %d", $nid));
      db_query("INSERT INTO {files} (uid, filename, filepath, filemime, filesize, status, timestamp) VALUES (%d, '%s', '%s', '%s', %d, %d, %d)", $uid, $file_name, $file_path, $file_mime, $file_size, FILE_STATUS_PERMANENT, $file_date);
      $fid = db_last_insert_id('files', 'fid');
      db_query("INSERT INTO {project_release_file} (fid, nid, filehash, weight) VALUES (%d, %d, '%s', %d)", $fid, $nid, $file_hash, $file_weight);
    }
    else {
      // Already have this file for this release, update it.
      db_query("UPDATE {files} SET filename = '%s', filepath = '%s', filemime = '%s', filesize = %d, status = %d, timestamp = %d WHERE fid = %d", $file_name, $file_path, $file_mime, $file_size, FILE_STATUS_PERMANENT, $file_date, $file_data->fid);
      db_query("UPDATE {project_release_file} SET filehash = '%s', weight = %d WHERE fid = %d", $file_hash, $file_weight, $file_data->fid);
    }
  }
}