[ Index ]

PHP Cross Reference of Drupal 6 (gatewave)

title

Body

[close]

/sites/all/modules/project/release/ -> package-release-nodes.php (source)

   1  #!/usr/bin/php
   2  <?php
   3  
   4  // $Id: package-release-nodes.php,v 1.62 2009/12/03 01:51:26 dww Exp $
   5  
   6  /**
   7   * @file
   8   * Automated packaging script to generate tarballs from release nodes.
   9   *
  10   * @author Derek Wright (http://drupal.org/user/46549)
  11   *
  12   * TODO:
  13   * - translation stats
  14   * - Package to .zip and .tgz, etc.
  15   *
  16   */
  17  
  18  // ------------------------------------------------------------
  19  // Required customization
  20  // ------------------------------------------------------------
  21  
  22  // The root of your Drupal installation, so we can properly bootstrap
  23  // Drupal. This should be the full path to the directory that holds
  24  // your index.php file, the "includes" subdirectory, etc.
  25  $drupal_root = '';
  26  
  27  // The name of your site. Required so that when we bootstrap Drupal in
  28  // this script, we find the right settings.php file in your sites folder.
  29  // For example, on drupal.org:
  30  // $site_name = 'drupal.org';
  31  $site_name = '';
  32  
  33  // The CVSROOT for the repository this script will be packaging
  34  // releases from. For example, on drupal.org:
  35  // $cvs_root = ':pserver:anonymous@cvs.drupal.org:/cvs/drupal';
  36  $cvs_root = '';
  37  
  38  // Root of the temporary directory where you want packages to be
  39  // made. Subdirectories will be created depending on the task.
  40  $tmp_root = '';
  41  
  42  // Location of the LICENSE.txt file you want included in all packages.
  43  $license = '';
  44  
  45  // Location of the INSTALL.txt file you want included in all
  46  // translation packages.
  47  $trans_install = '';
  48  
  49  // -------------------------
  50  // drush/drush_make settings
  51  // -------------------------
  52  // Full path to the drush executable.
  53  $drush = '';
  54  
  55  // Full path to the directory where drush_make is located. This is needed to
  56  // manually include it as a searchable path for drush extensions, as this
  57  // script's owner will not likely have a home directory to search.
  58  $drush_make_dir = '';
  59  
  60  
  61  // ------------------------------------------------------------
  62  // Optional customization
  63  // ------------------------------------------------------------
  64  
  65  // ----------------
  66  // File destination
  67  // ----------------
  68  // This assumes you want to install the packaged releases in the
  69  // "files/projects" directory of your root Drupal installation. If
  70  // that's not the case, you should customize these.
  71  $dest_root = $drupal_root;
  72  $dest_rel = 'files/projects';
  73  
  74  // --------------
  75  // External tools
  76  // --------------
  77  // If you want this program to always use absolute paths for all the
  78  // tools it invokes, provide a full path for each one. Otherwise,
  79  // the script will find these tools in your PATH.
  80  $tar = '/usr/bin/tar';
  81  $gzip = '/usr/bin/gzip';
  82  $cvs = '/usr/bin/cvs';
  83  $ln = '/bin/ln';
  84  $rm = '/bin/rm';
  85  $msgcat = 'msgcat';
  86  $msgattrib = 'msgattrib';
  87  $msgfmt = 'msgfmt';
  88  $php = '/usr/bin/php';
  89  
  90  // If you are using project-release-create-history.php to generate XML release
  91  // history files for Update status clients, if you include the full path to
  92  // your copy of that script here, after all the packages are re(generated),
  93  // this script will regenerate the XML release history files for any projects
  94  // with new/updated releases.
  95  $project_release_create_history = '';
  96  
  97  // The repository ID's for core and contributions.
  98  define('DRUPAL_CORE_REPOSITORY_ID', 1);
  99  define('DRUPAL_CONTRIB_REPOSITORY_ID', 2);
 100  
 101  
 102  // ------------------------------------------------------------
 103  // Initialization
 104  // (Real work begins here, nothing else to customize)
 105  // ------------------------------------------------------------
 106  
 107  // Check if all required variables are defined
 108  $vars = array(
 109    'drupal_root' => $drupal_root,
 110    'site_name' => $site_name,
 111    'cvs_root' => $cvs_root,
 112    'tmp_root' => $tmp_root,
 113    'license' => $license,
 114    'trans_install' => $trans_install,
 115  );
 116  foreach ($vars as $name => $val) {
 117    if (empty($val)) {
 118      print "ERROR: \"\$$name\" variable not set, aborting\n";
 119      $fatal_err = true;
 120    }
 121  }
 122  if (!empty($fatal_err)) {
 123    exit(1);
 124  }
 125  
 126  putenv("CVSROOT=$cvs_root");
 127  putenv("TERM=vt100");  // drush requires a terminal.
 128  $script_name = $argv[0];
 129  
 130  // Find what kind of packaging we need to do
 131  if (!empty($argv[1])) {
 132    $task = $argv[1];
 133  }
 134  else {
 135    $task = 'tag';
 136  }
 137  switch($task) {
 138    case 'tag':
 139    case 'branch':
 140    case 'check':
 141    case 'repair':
 142      break;
 143    default:
 144      print "ERROR: $argv[0] invoked with invalid argument: \"$task\"\n";
 145      exit (1);
 146  }
 147  
 148  $project_id = 0;
 149  if (!empty($argv[2])) {
 150    $project_id = $argv[2];
 151  }
 152  
 153  // Setup variables for Drupal bootstrap
 154  $_SERVER['HTTP_HOST'] = $site_name;
 155  $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
 156  $_SERVER['REQUEST_URI'] = '/' . $script_name;
 157  $_SERVER['SCRIPT_NAME'] = '/' . $script_name;
 158  $_SERVER['PHP_SELF'] = '/' . $script_name;
 159  $_SERVER['SCRIPT_FILENAME'] = $_SERVER['PWD'] . '/' . $script_name;
 160  $_SERVER['PATH_TRANSLATED'] = $_SERVER['SCRIPT_FILENAME'];
 161  
 162  if (!chdir($drupal_root)) {
 163    print "ERROR: Can't chdir($drupal_root): aborting.\n";
 164    exit(1);
 165  }
 166  
 167  // Force the right umask while this script runs, so that everything is created
 168  // with sane file permissions.
 169  umask(0022);
 170  
 171  require_once  'includes/bootstrap.inc';
 172  drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
 173  // We have to initialize the theme() system before we leave $drupal_root
 174  $hack = theme('placeholder', 'hack');
 175  
 176  if ($task == 'check' || $task == 'repair') {
 177    verify_packages($task, $project_id);
 178  }
 179  else {
 180    initialize_tmp_dir($task);
 181    initialize_repository_info();
 182    package_releases($task, $project_id);
 183    // Now that we're done, clean out the tmp/task dir we created
 184    chdir($tmp_root);
 185    drupal_exec("$rm -rf $tmp_dir");
 186  }
 187  
 188  if ($task == 'branch') {
 189    // Clear any cached data set to expire.
 190    cache_clear_all(NULL, 'cache_project_release');
 191  }
 192  elseif ($task == 'repair') {
 193    // Clear all cached data
 194    cache_clear_all('*', 'cache_project_release', TRUE);
 195  }
 196  
 197  // ------------------------------------------------------------
 198  // Functions: main work
 199  // ------------------------------------------------------------
 200  
 201  function package_releases($type, $project_id = 0) {
 202    global $drupal_root, $wd_err_msg;
 203    global $php, $project_release_create_history;
 204  
 205    $rel_node_join = '';
 206    $where_args = array();
 207    if ($type == 'tag') {
 208      $where = " AND (prn.rebuild = %d) AND (f.filepath IS NULL OR f.filepath = '')";
 209      $where_args[] = 0;  // prn.rebuild
 210      $plural = t('tags');
 211    }
 212    elseif ($type == 'branch') {
 213      $rel_node_join = " INNER JOIN {node} nr ON prn.nid = nr.nid";
 214      $where = " AND (prn.rebuild = %d) AND ((f.filepath IS NULL) OR (f.filepath = '') OR (nr.status = %d))";
 215      $where_args[] = 1;  // prn.rebuild
 216      $where_args[] = 1;  // nr.status
 217      $plural = t('branches');
 218      if (empty($project_id)) {
 219        wd_msg("Starting to package all snapshot releases.");
 220      }
 221      else {
 222        wd_msg("Starting to package snapshot releases for project id: @project_id.", array('@project_id' => $project_id), l(t('view'), 'node/' . $project_id));
 223      }
 224    }
 225    else {
 226      wd_err("ERROR: package_releases() called with unknown type: %type", array('%type' => $type));
 227      return FALSE;
 228    }
 229    $args = array();
 230    $args[] = 1;    // Account for np.status = 1.
 231    $args[] = 1;    // Account for prp.releases = 1.
 232    if (!empty($project_id)) {
 233      $where .= ' AND prn.pid = %d';
 234      $where_args[] = $project_id;
 235    }
 236    $args = array_merge($args, $where_args);
 237    $query = db_query("SELECT pp.uri, prn.nid, prn.pid, prn.tag, prn.version, c.directory, c.rid FROM {project_release_nodes} prn $rel_node_join LEFT JOIN {project_release_file} prf ON prn.nid = prf.nid LEFT JOIN {files} f ON prf.fid = f.fid 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 np.status = %d AND prp.releases = %d " . $where . ' ORDER BY pp.uri', $args);
 238  
 239    $num_built = 0;
 240    $num_considered = 0;
 241    $project_nids = array();
 242  
 243    // Read everything out of the query immediately so that we don't leave the
 244    // query object/connection open while doing other queries.
 245    $releases = array();
 246    while ($release = db_fetch_object($query)) {
 247      // This query could pull multiple rows of the same release since multiple
 248      // files per release node are allowed. Account for this by keying on
 249      // release nid.
 250      $releases[$release->nid] = $release;
 251    }
 252    foreach ($releases as $release) {
 253      $wd_err_msg = array();
 254      $version = $release->version;
 255      $project_short_name = $release->uri;
 256      $tag = $release->tag;
 257      $nid = $release->nid;
 258      $pid = $release->pid;
 259      $tag = ($tag == 'TRUNK') ? 'HEAD' : $tag;
 260      $project_short_name = escapeshellcmd($project_short_name);
 261      $version = escapeshellcmd($version);
 262      $tag = escapeshellcmd($tag);
 263      db_query("DELETE FROM {project_release_package_errors} WHERE nid = %d", $nid);
 264      if ($release->rid == DRUPAL_CORE_REPOSITORY_ID) {
 265        $built = package_release_core($type, $nid, $project_short_name, $version, $tag);
 266      }
 267      else {
 268        $release_dir = escapeshellcmd($release->directory);
 269        $built = package_release_contrib($type, $nid, $project_short_name, $version, $tag, $release_dir);
 270      }
 271      chdir($drupal_root);
 272  
 273      if ($built) {
 274        $num_built++;
 275        $project_nids[$pid] = TRUE;
 276      }
 277      $num_considered++;
 278      if (count($wd_err_msg)) {
 279        db_query("INSERT INTO {project_release_package_errors} (nid, messages) values (%d, '%s')", $nid, serialize($wd_err_msg));
 280      }
 281    }
 282    if ($num_built || $type == 'branch') {
 283      if (!empty($project_id)) {
 284        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));
 285      }
 286      else {
 287        wd_msg("Done packaging releases from !plural: !num_built built, !num_considered considered.", array('!plural' => $plural, '!num_built' => $num_built, '!num_considered' => $num_considered));
 288      }
 289    }
 290  
 291    // Finally, regenerate release history XML files for all projects we touched.
 292    if (!empty($project_nids) && !empty($project_release_create_history)) {
 293      wd_msg('Re-generating release history XML files');
 294      $i = $fails = 0;
 295      foreach ($project_nids as $project_nid => $value) {
 296        if (drupal_exec("$php $project_release_create_history $project_nid")) {
 297          $i++;
 298        }
 299        else {
 300          $fails++;
 301        }
 302      }
 303      if (!empty($fails)) {
 304        wd_msg('ERROR: Failed to re-generate release history XML files for !num project(s)', array('!num' => $fails));
 305      }
 306      wd_msg('Done re-generating release history XML files for !num project(s)', array('!num' => $i));
 307    }
 308  }
 309  
 310  function package_release_core($type, $nid, $project_short_name, $version, $tag) {
 311    global $tmp_dir, $repositories, $dest_root, $dest_rel;
 312    global $cvs, $tar, $gzip, $rm;
 313  
 314    if (!drupal_chdir($tmp_dir)) {
 315      return false;
 316    }
 317  
 318    $release_file_id = $project_short_name . '-' . $version;
 319    $release_node_view_link = l(t('view'), 'node/' . $nid);
 320    $file_path_tgz = $dest_rel . '/' . $release_file_id . '.tar.gz';
 321    $full_dest_tgz = $dest_root . '/' . $file_path_tgz;
 322  
 323    // Remember if the tar.gz version of this release file already exists.
 324    $tgz_exists = is_file($full_dest_tgz);
 325  
 326    // Don't use drupal_exec or return if this fails, we expect it to be empty.
 327    exec("$rm -rf $tmp_dir/$release_file_id");
 328  
 329    // Actually generate the tarball:
 330    if (!drupal_exec("$cvs -q export -r $tag -d $release_file_id drupal")) {
 331      return false;
 332    }
 333  
 334    $info_files = array();
 335    $exclude = array('.', '..', 'LICENSE.txt');
 336    $youngest = file_find_youngest($release_file_id, 0, $exclude, $info_files);
 337    if ($type == 'branch' && $tgz_exists && filectime($full_dest_tgz) + 300 > $youngest) {
 338      // The existing tarball for this release is newer than the youngest
 339      // file in the directory, we're done.
 340      return false;
 341    }
 342  
 343    // Update any .info files with packaging metadata.
 344    foreach ($info_files as $file) {
 345      if (!fix_info_file_version($file, $project_short_name, $version)) {
 346        wd_err("ERROR: Failed to update version in %file, aborting packaging", array('%file' => $file), $release_node_view_link);
 347        return false;
 348      }
 349    }
 350  
 351    if (!drupal_exec("$tar -c --file=- $release_file_id | $gzip -9 --no-name > $full_dest_tgz")) {
 352      return false;
 353    }
 354    $files[] = $file_path_tgz;
 355  
 356    // As soon as the tarball exists, we want to update the DB about it.
 357    package_release_update_node($nid, $files);
 358  
 359    if ($tgz_exists) {
 360      wd_msg("%id has changed, re-packaged.", array('%id' => $release_file_id), $view_link);
 361    }
 362    else {
 363      wd_msg("Packaged %id.", array('%id' => $release_file_id), $view_link);
 364    }
 365  
 366    // Don't consider failure to remove this directory a build failure.
 367    drupal_exec("$rm -rf $tmp_dir/$release_file_id");
 368    return true;
 369  }
 370  
 371  function package_release_contrib($type, $nid, $project_short_name, $version, $tag, $release_dir) {
 372    global $tmp_dir, $repositories, $dest_root, $dest_rel;
 373    global $cvs, $tar, $gzip, $rm, $ln;
 374    global $drush, $drush_make_dir;
 375    global $license, $trans_install;
 376  
 377    // Files to ignore when checking timestamps:
 378    $exclude = array('.', '..', 'LICENSE.txt');
 379  
 380    $parts = split('/', $release_dir);
 381    // modules, themes, theme-engines, profiles, or translations
 382    $contrib_type = $parts[1];
 383    // specific directory (same as uri)
 384    $project_short_name = $parts[2];
 385  
 386    $project_build_root = "$tmp_dir/$project_short_name";
 387    $cvs_export_dir = "{$repositories[DRUPAL_CONTRIB_REPOSITORY_ID]['modules']}/$contrib_type/$project_short_name";
 388  
 389    $release_file_id = $project_short_name . '-' . $version;
 390    $release_node_view_link = l(t('view'), 'node/' . $nid);
 391    $file_path_tgz = $dest_rel . '/' . $release_file_id . '.tar.gz';
 392    $full_dest_tgz = $dest_root . '/' . $file_path_tgz;
 393  
 394    // Remember if the tar.gz version of this release file already exists.
 395    $tgz_exists = is_file($full_dest_tgz);
 396  
 397    // Clean up any old build directory if it exists.
 398    // Don't use drupal_exec or return if this fails, we expect it to be empty.
 399    exec("$rm -rf $project_build_root");
 400  
 401    // Make a fresh build directory and move inside it.
 402    if (!mkdir($project_build_root) || !drupal_chdir($project_build_root)) {
 403      return false;
 404    }
 405  
 406    // Checkout this release from CVS, and see if we need to rebuild it
 407    if (!drupal_exec("$cvs -q export -r $tag -d $project_short_name $cvs_export_dir")) {
 408      return false;
 409    }
 410    if (!is_dir("$project_build_root/$project_short_name")) {
 411      wd_err("ERROR: %dir does not exist after cvs export -r %tag -d %dir %cvs_export_dir", array('%dir' => $project_short_name, '%rev' =>  $tag, '%cvs_export_dir' => $cvs_export_dir), $release_node_view_link);
 412      return false;
 413    }
 414  
 415    $info_files = array();
 416    $youngest = file_find_youngest($project_short_name, 0, $exclude, $info_files);
 417    if ($type == 'branch' && $tgz_exists && filectime($full_dest_tgz) + 300 > $youngest) {
 418      // The existing tarball for this release is newer than the youngest
 419      // file in the directory, we're done.
 420      return false;
 421    }
 422  
 423    // Update any .info files with packaging metadata.
 424    foreach ($info_files as $file) {
 425      if (!fix_info_file_version($file, $project_short_name, $version)) {
 426        wd_err("ERROR: Failed to update version in %file, aborting packaging", array('%file' => $file), $release_node_view_link);
 427        return false;
 428      }
 429    }
 430  
 431    // Link not copy, since we want to preserve the date...
 432    if (!drupal_exec("$ln -sf $license $project_short_name/LICENSE.txt")) {
 433      return false;
 434    }
 435    // Do we want a subdirectory in the tarball or not?
 436    $tarball_needs_subdir = TRUE;
 437    if ($contrib_type == 'translations' && $project_short_name != 'drupal-pot') {
 438      // Translation projects are packaged differently based on core version.
 439      if (intval($version) > 5) {
 440        if (!($to_tar = package_release_contrib_d6_translation($project_short_name, $version, $release_node_view_link))) {
 441          // Return on error.
 442          return FALSE;
 443        }
 444        $tarball_needs_subdir = FALSE;
 445      }
 446      elseif (!($to_tar = package_release_contrib_pre_d6_translation($project_short_name, $version, $release_node_view_link))) {
 447        // Return on error.
 448        return FALSE;
 449      }
 450    }
 451    else {
 452      // Not a translation: just grab the whole directory.
 453      $to_tar = $project_short_name;
 454    }
 455  
 456    if (!$tarball_needs_subdir) {
 457      if (!drupal_chdir($project_short_name)) {
 458        return false;
 459      }
 460    }
 461  
 462    // 'h' is for dereference, we want to include the files, not the links
 463    if (!drupal_exec("$tar -ch --file=- $to_tar | $gzip -9 --no-name > $full_dest_tgz")) {
 464      return false;
 465    }
 466    $files[] = $file_path_tgz;
 467  
 468    // Start with no package contents, since this is only valid for profiles.
 469    $package_contents = array();
 470  
 471    // This is a profile, so invoke the drush_make routines to package core
 472    // and/or any other contrib releases specified in the profile's .make file.
 473    if ($contrib_type == 'profiles') {
 474      // Move inside the profile directory.
 475      if (!drupal_chdir("$project_build_root/$project_short_name")) {
 476        return false;
 477      }
 478  
 479      // In order for extended packaging to take place, the profile must have a
 480      // file named drupal-org.make in the main directory of their profile.
 481      $drupalorg_makefile = 'drupal-org.make';
 482  
 483      if (file_exists($drupalorg_makefile)) {
 484      // Search the .make file for the required 'core' attribute.
 485        $info = drupal_parse_info_file($drupalorg_makefile);
 486  
 487        // Only proceed if a core release was found.
 488        if (!isset($info['core'])) {
 489          wd_err("ERROR: %profile does not have the required 'core' attribute.", array('%profile' => $release_file_id), $release_node_view_link);
 490          return FALSE;
 491        }
 492        else {
 493  
 494          // Basic sanity check for the format of the attribute. The CVS checkout
 495          // attempt of core will handle the rest of the validation (ie, it will
 496          // fail if a non-existant tag is specified.
 497          if (!preg_match("/^(\d+)\.(\d+)$/", $info['core'], $matches)) {
 498            wd_err("ERROR: %profile specified an invalid 'core' attribute -- both API version and release are required.", array('%profile' => $release_file_id), $release_node_view_link);
 499            return FALSE;
 500          }
 501          else {
 502            // Compare the Drupal API version in the profile's version string with
 503            // the API version of core specificed in the .make file -- these should
 504            // match.
 505            $profile_api_version = $matches[1];
 506            $parts = explode('.', $version);
 507            $release_api_version = $parts[0];
 508            if ($profile_api_version != $release_api_version) {
 509              wd_err("ERROR: %profile specified an invalid 'core' attribute -- the API version must match the API version of the release.", array('%profile' => $release_file_id), $release_node_view_link);
 510              return FALSE;
 511            }
 512          }
 513  
 514          // NO-CORE DISTRIBUTION.
 515  
 516          $no_core_id = "$release_file_id-no-core";
 517          // Build the drupal file path and the full file path.
 518          $no_core_file_path = "$dest_rel/$no_core_id.tar.gz";
 519          $no_core_full_dest = "$dest_root/$no_core_file_path";
 520  
 521          // Run drush_make to build the profile's contents.
 522          // --drupal-org: Invoke drupal.org specific validation/processing.
 523          // --drupal-org-build-root: Let the script know where to place it's
 524          //   build-related files.
 525          // --drupal-org-log-errors-to-file: Store build errors for later output.
 526          // --drupal-org-log-package-items-to-file: Store package items for
 527          //   later recording in the database.
 528          if (!drupal_exec("$drush --include=$drush_make_dir make --drupal-org --drupal-org-build-root=$project_build_root --drupal-org-log-errors-to-file --drupal-org-log-package-items-to-file $drupalorg_makefile .")) {
 529            // The build failed, get any output error messages and include them
 530            // in the packaging error report.
 531            $build_errors_file = "$project_build_root/build_errors.txt";
 532            if (file_exists($build_errors_file)) {
 533              $lines = file($build_errors_file, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
 534              foreach ($lines as $line) {
 535                  wd_err("ERROR: $line");
 536              }
 537            }
 538            wd_err("ERROR: Build for %profile failed.", array('%profile' => $no_core_id), $release_node_view_link);
 539            return FALSE;
 540          }
 541  
 542          // Change into the profile build directory.
 543          if (!drupal_chdir($project_build_root)) {
 544            return FALSE;
 545          }
 546  
 547          // Package the no-core distribution.
 548          // 'h' is for dereference, we want to include the files, not the links
 549          if (!drupal_exec("$tar -ch --file=- $project_short_name | $gzip -9 --no-name > $no_core_full_dest")) {
 550            return false;
 551          }
 552          $files[] = $no_core_file_path;
 553  
 554          // CORE DISTRIBUTION.
 555  
 556          // Write a small .make file used to build core.
 557          $core_version = $info['core'];
 558          $core_build_dir = "drupal-$core_version";
 559          $core_makefile = "$core_build_dir.make";
 560          file_put_contents($core_makefile, core_make_file($core_version));
 561  
 562          // Run drush_make to build core.
 563          if (!drupal_exec("$drush --include=$drush_make_dir make $core_makefile $core_build_dir")) {
 564            // The build failed, bail out.
 565            wd_err("ERROR: Build for %core failed.", array('%core' => $core_build_dir), $release_node_view_link);
 566            return FALSE;
 567          }
 568  
 569          // Move the profile into place inside core.
 570          if (!rename($project_short_name, "$core_build_dir/profiles/$project_short_name")) {
 571            return FALSE;
 572          }
 573  
 574          $core_id = "$release_file_id-core";
 575          // Build the drupal file path and the full file path.
 576          $core_file_path = "$dest_rel/$core_id.tar.gz";
 577          $core_full_dest = "$dest_root/$core_file_path";
 578  
 579          // Package the core distribution.
 580          // 'h' is for dereference, we want to include the files, not the links
 581          if (!drupal_exec("$tar -ch --file=- $core_build_dir | $gzip -9 --no-name > $core_full_dest")) {
 582            return FALSE;
 583          }
 584          $files[] = $core_file_path;
 585  
 586          // Core was built without the drupal.org drush extension, so the
 587          // package item for core isn't in the package contents file. Retrieve
 588          // it manually.
 589          $core_tag = 'DRUPAL-'. str_replace('.', '-', $core_version);
 590          if (!($core_release_nid = db_result(db_query("SELECT nid FROM {project_release_nodes} WHERE tag = '%s'", $core_tag)))) {
 591            return FALSE;
 592          }
 593          $package_contents[] = $core_release_nid;
 594  
 595          // Retrieve the package contents for the release.
 596          $package_contents_file = "$project_build_root/package_contents.txt";
 597          if (file_exists($package_contents_file)) {
 598            $lines = file($package_contents_file, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
 599            foreach ($lines as $line) {
 600              if (is_numeric($line)) {
 601                $package_contents[] = $line;
 602              }
 603            }
 604          }
 605          else {
 606            wd_err("ERROR: %file does not exist for %profile release.", array('%file' => $package_contents_file, '%profile' => $release_file_id), $release_node_view_link);
 607            return FALSE;
 608          }
 609        }
 610      }
 611      else {
 612        wd_msg("No makefile for %profile profile -- skipping extended packaging.", array('%profile' => $release_file_id), $release_node_view_link);
 613      }
 614    }
 615  
 616    // As soon as the tarball exists, update the DB
 617    package_release_update_node($nid, $files, $package_contents);
 618  
 619    if ($tgz_exists) {
 620      wd_msg("%id has changed, re-packaged.", array('%id' => $release_file_id), $view_link);
 621    }
 622    else {
 623      wd_msg("Packaged %id.", array('%id' => $release_file_id), $view_link);
 624    }
 625  
 626    // Don't consider failure to remove this directory a build failure.
 627    drupal_exec("$rm -rf $project_build_root");
 628    return true;
 629  }
 630  
 631  function package_release_contrib_pre_d6_translation($project_short_name, $version, $release_node_view_link) {
 632    global $msgcat, $msgattrib, $msgfmt;
 633  
 634    if ($handle = opendir($project_short_name)) {
 635      $po_files = array();
 636      while ($file = readdir($handle)) {
 637        if ($file == 'general.po') {
 638          $found_general_po = TRUE;
 639        }
 640        elseif ($file == 'installer.po') {
 641          $found_installer_po = TRUE;
 642        }
 643        elseif (preg_match('/.*\.po/', $file)) {
 644          $po_files[] = "$project_short_name/$file";
 645        }
 646      }
 647      if ($found_general_po) {
 648        @unlink("$project_short_name/$project_short_name.po");
 649        $po_targets = "$project_short_name/general.po ";
 650        $po_targets .= implode(' ', $po_files);
 651        if (!drupal_exec("$msgcat --use-first $po_targets | $msgattrib --no-fuzzy -o $project_short_name/$project_short_name.po")) {
 652          return FALSE;
 653        }
 654      }
 655    }
 656    if (is_file("$project_short_name/$project_short_name.po")) {
 657      if (!drupal_exec("$msgfmt --statistics $project_short_name/$project_short_name.po 2>> $project_short_name/STATUS.txt")) {
 658        return FALSE;
 659      }
 660      $to_tar = "$project_short_name/*.txt $project_short_name/$project_short_name.po";
 661      if ($found_installer_po) {
 662        $to_tar .= " $project_short_name/installer.po";
 663      }
 664    }
 665    else {
 666      wd_err("ERROR: %project_short_name translation does not contain a %project_short_name_po file for version %version, not packaging", array('%project_short_name' => $project_short_name, '%project_short_name_po' => "$project_short_name.po", '%version' => $version), $release_node_view_link);
 667      return FALSE;
 668    }
 669  
 670    // Return with list of files to package.
 671    return $to_tar;
 672  }
 673  
 674  function package_release_contrib_d6_translation($project_short_name, $version, $release_node_view_link) {
 675    global $msgattrib, $msgfmt;
 676  
 677    if ($handle = opendir($project_short_name)) {
 678      $po_files = array();
 679      while ($file = readdir($handle)) {
 680        if (preg_match('!(.*)\.txt$!', $file, $name) && ($file != "STATUS.$project_short_name.txt")) {
 681          // Rename text files to $name[1].$project_short_name.txt so there will be no conflict
 682          // with core text files when the package is deployed.
 683          if (!rename("$project_short_name/$file", "$project_short_name/$name[1].$project_short_name.txt")) {
 684            wd_err("ERROR: Unable to rename text files in %project_short_name translation in version %version, not packaging", array('%project_short_name' => $project_short_name, '%version' => $version), $release_node_view_link);
 685            return FALSE;
 686          }
 687        }
 688        elseif (preg_match('!.*\.po$!', $file)) {
 689  
 690          // Generate stats information about the .po file handled.
 691          if (!drupal_exec("$msgfmt --statistics $project_short_name/$file 2>> $project_short_name/STATUS.$project_short_name.txt")) {
 692            wd_err("ERROR: Unable to generate statistics for %file in %project_short_name translation in version %version, not packaging", array('%project_short_name' => $project_short_name, '%version' => $version, '%file' => $file), $release_node_view_link);
 693            return FALSE;
 694          }
 695  
 696          // File names are formatted in directory-subdirectory.po or
 697          // directory.po format and aggregate files from the named directory.
 698          // The installer.po file is special in that it aggregates all strings
 699          // possibly used in the installer. We move that to the default install
 700          // profile. We move all other root directory files (misc.po,
 701          // includes.po, etc) to the system module and all remaining files to
 702          // the corresponding subdirectory in the named directory.
 703          if (!strpos($file, '-')) {
 704            if ($file == 'installer.po') {
 705              // Special file, goes to install profile.
 706              $target = 'profiles/default/translations/'. $project_short_name .'.po';
 707            }
 708            else {
 709              // 'Root' files go to system module.
 710              $target = 'modules/system/translations/'. str_replace('.po', '.'. $project_short_name .'.po', $file);
 711            }
 712          }
 713          else {
 714            // Other files go to their module or theme folder.
 715            $target = str_replace(array('-', '.po'), array('/', ''), $file) .'/translations/'. str_replace('.po', '.'. $project_short_name .'.po', $file);
 716          }
 717          $project_short_name_target = "$project_short_name/$target";
 718  
 719          // Create target folder and copy file there, while removing fuzzies.
 720          $target_dir = dirname($project_short_name_target);
 721          if (!is_dir($target_dir) && !mkdir($target_dir, 0777, TRUE)) {
 722            wd_err("ERROR: Unable to generate directory structure in %project_short_name translation in version %version, not packaging", array('%project_short_name' => $project_short_name, '%version' => $version), $release_node_view_link);
 723            return FALSE;
 724          }
 725          if (!drupal_exec("$msgattrib --no-fuzzy $project_short_name/$file -o $project_short_name_target")) {
 726            wd_err("ERROR: Unable to filter fuzzy strings and copying the translation files in %project_short_name translation in version %version, not packaging", array('%project_short_name' => $project_short_name, '%version' => $version), $release_node_view_link);
 727            return FALSE;
 728          }
 729  
 730          // Add file to package.
 731          $to_tar .= ' '. $target;
 732        }
 733      }
 734    }
 735  
 736    // Return with list of files to package.
 737    return "*.txt". $to_tar;
 738  }
 739  
 740  // ------------------------------------------------------------
 741  // Functions: metadata validation functions
 742  // ------------------------------------------------------------
 743  
 744  /**
 745   * Check that file metadata on disk matches the values stored in the DB.
 746   */
 747  function verify_packages($task, $project_id) {
 748    global $dest_root;
 749    $do_repair = $task == 'repair' ? TRUE : FALSE;
 750    $args = array(1);
 751    $where = '';
 752    if (!empty($project_id)) {
 753      $where = ' AND prn.pid = %d';
 754      $args[] = $project_id;
 755    }
 756    $query = db_query("SELECT prn.nid, f.filepath, f.timestamp, prf.filehash FROM {project_release_nodes} prn INNER JOIN {node} n ON prn.nid = n.nid INNER JOIN {project_release_file} prf ON prn.nid = prf.nid INNER JOIN {files} f ON prf.fid = f.fid WHERE n.status = %d AND f.filepath <> '' $where", $args);
 757    while ($release = db_fetch_object($query)) {
 758      // Grab all the results into RAM to free up the DB connection for
 759      // when we need to update the DB to correct metadata or log messages.
 760      $releases[] = $release;
 761    }
 762  
 763    $num_failed = 0;
 764    $num_repaired = 0;
 765    $num_not_exist = 0;
 766    $num_need_repair = 0;
 767    $num_considered = 0;
 768    $num_wrong_date = 0;
 769    $num_wrong_hash = 0;
 770  
 771    // Now, process the files, and check metadata
 772    foreach ($releases as $release) {
 773      $valid_hash = TRUE;
 774      $valid_date = TRUE;
 775      $num_considered++;
 776      $nid = $release->nid;
 777      $release_node_view_link = l(t('view'), 'node/' . $nid);
 778      $file_path = $release->filepath;
 779      $full_path = $dest_root . '/' . $file_path;
 780      $db_date = (int)$release->timestamp;
 781      $db_hash = $release->filehash;
 782  
 783      if (!is_file($full_path)) {
 784        $num_not_exist++;
 785        wd_err('WARNING: %file does not exist.', array('%file' => $full_path), $release_node_view_link);
 786        continue;
 787      }
 788      $real_date = filemtime($full_path);
 789      $real_hash = md5_file($full_path);
 790  
 791      $variables = array();
 792      $variables['%file'] = $file_path;
 793      if ($real_hash != $db_hash) {
 794        $valid_hash = FALSE;
 795        $num_wrong_hash++;
 796        $variables['@db_hash'] = $db_hash;
 797        $variables['@real_hash'] = $real_hash;
 798      }
 799      if ($real_date != $db_date) {
 800        $valid_date = FALSE;
 801        $num_wrong_date++;
 802        $variables['!db_date'] = format_date($db_date);
 803        $variables['!db_date_raw'] = $db_date;
 804        $variables['!real_date'] = format_date($real_date);
 805        $variables['!real_date_raw'] = $real_date;
 806      }
 807      if ($valid_date && $valid_hash) {
 808        // Nothing else to do.
 809        continue;
 810      }
 811  
 812      if (!$valid_date && !$valid_hash) {
 813        wd_check('All file meta data for %file is incorrect: saved date: !db_date (!db_date_raw), real date: !real_date (!real_date_raw); saved md5hash: @db_hash, real md5hash: @real_hash', $variables, $release_node_view_link);
 814      }
 815      else if (!$valid_date) {
 816        wd_check('File date for %file is incorrect: saved date: !db_date (!db_date_raw), real date: !real_date (!real_date_raw)', $variables, $release_node_view_link);
 817      }
 818      else { // !$valid_hash
 819        wd_check('File md5hash for %file is incorrect: saved: @db_hash, real: @real_hash', $variables, $release_node_view_link);
 820      }
 821  
 822      if (!$do_repair) {
 823        $num_need_repair++;
 824      }
 825      else {
 826        $ret1 = $ret2 = FALSE;
 827        // TODO: Broken for N>1 files per release.
 828        $fid = db_result(db_query("SELECT fid FROM {project_release_file} WHERE nid = %d", $nid));
 829        if (!empty($fid)) {
 830          $ret1 = db_query("UPDATE {project_release_file} SET filehash = '%s' WHERE fid = %d", $real_hash, $fid);
 831          $ret2 = db_query("UPDATE {files} SET timestamp = %d WHERE fid = %d", $real_date, $fid);
 832        }
 833        if ($ret1 && $ret2) {
 834          $num_repaired++;
 835        }
 836        else {
 837          wd_err('ERROR: db_query() failed trying to update metadata for %file', array('%file' => $file_path), $release_node_view_link);
 838          $num_failed++;
 839        }
 840      }
 841    }
 842  
 843    $num_vars = array(
 844      '!num_considered' => $num_considered,
 845      '!num_repaired' => $num_repaired,
 846      '!num_need_repair' => $num_need_repair,
 847      '!num_wrong_date' => $num_wrong_date,
 848      '!num_wrong_hash' => $num_wrong_hash,
 849    );
 850    if ($num_failed) {
 851      wd_err('ERROR: unable to repair !num_failed releases due to db_query() failures.', array('!num_failed' => $num_failed));
 852    }
 853    if ($num_not_exist) {
 854      wd_err('ERROR: !num_not_exist files are in the database but do not exist on disk.', array('!num_not_exist' => $num_not_exist));
 855    }
 856    if ($do_repair) {
 857      wd_check('Done checking releases: !num_repaired repaired, !num_wrong_date invalid dates, !num_wrong_hash invalid md5 hashes, !num_considered considered.', $num_vars);
 858    }
 859    else {
 860      if (empty($project_id)) {
 861        wd_check('Done checking releases: !num_need_repair need repairing, !num_wrong_date invalid dates, !num_wrong_hash invalid md5 hashes, !num_considered considered.', $num_vars);
 862      }
 863      else {
 864        $num_vars['@project_id'] = $project_id;
 865        wd_check('Done checking releases for project id @project_id: !num_need_repair need repairing, !num_wrong_date invalid dates, !num_wrong_hash invalid md5 hashes, !num_considered considered.', $num_vars, l(t('view'), 'node/' . $project_id));
 866      }
 867    }
 868  }
 869  
 870  // ------------------------------------------------------------
 871  // Functions: utility methods
 872  // ------------------------------------------------------------
 873  
 874  /**
 875   * Wrapper for exec() that logs errors to the watchdog.
 876   * @param $cmd
 877   *   String of the command to execute (assumed to be safe, the caller is
 878   *   responsible for calling escapeshellcmd() if necessary).
 879   * @return true if the command was successful (0 exit status), else false.
 880   */
 881  function drupal_exec($cmd) {
 882    // Made sure we grab stderr, too...
 883    exec("$cmd 2>&1", $output, $rval);
 884    if ($rval) {
 885      wd_err("ERROR: %cmd failed with status !rval" . '<pre>' . implode("\n", array_map('htmlspecialchars', $output)), array('%cmd' => $cmd, '!rval' => $rval));
 886      return false;
 887    }
 888    return true;
 889  }
 890  
 891  /**
 892   * Wrapper for chdir() that logs errors to the watchdog.
 893   * @param $dir Directory to change into.
 894   * @return true if the command was successful (0 exit status), else false.
 895   */
 896  function drupal_chdir($dir) {
 897    if (!chdir($dir)) {
 898      wd_err("ERROR: Can't chdir(@dir)", array('@dir' => $dir));
 899      return false;
 900    }
 901    return true;
 902  }
 903  
 904  /// TODO: remove this before the final script goes live -- debugging only.
 905  function wprint($var) {
 906    watchdog('package_debug', '<pre>' . var_export($var, TRUE));
 907  }
 908  
 909  /**
 910   * Wrapper function for watchdog() to log notice messages. Uses a
 911   * different watchdog message type depending on the task (branch vs. tag).
 912   */
 913  function wd_msg($msg, $variables = array(), $link = NULL) {
 914    global $task;
 915    watchdog('package_' . $task, $msg, $variables, WATCHDOG_NOTICE, $link);
 916    echo $msg ."\n";
 917  }
 918  
 919  /**
 920   * Wrapper function for watchdog() to log error messages.
 921   */
 922  function wd_err($msg, $variables = array(), $link = NULL) {
 923    global $wd_err_msg;
 924    if (!isset($wd_err_msg)) {
 925      $wd_err_msg = array();
 926    }
 927    watchdog('package_error', $msg, $variables, WATCHDOG_ERROR, $link);
 928    echo t($msg, $variables) ."\n";
 929    $wd_err_msg[] = t($msg, $variables);
 930  }
 931  
 932  /**
 933   * Wrapper function for watchdog() to log messages about checking
 934   * package metadata.
 935   */
 936  function wd_check($msg, $variables = array(), $link = NULL) {
 937    watchdog('package_check', $msg, $variables, WATCHDOG_NOTICE, $link);
 938    echo $msg ."\n";
 939  }
 940  
 941  /**
 942   * Initialize the tmp directory. Use different subdirs for building
 943   * snapshots than official tags, so there's no potential directory
 944   * collisions and race conditions if both are running at the same time
 945   * (due to how long it takes to complete a branch snapshot run, and
 946   * how often we run this for tag-based releases).
 947   */
 948  function initialize_tmp_dir($task) {
 949    global $tmp_dir, $tmp_root, $rm;
 950  
 951    if (!is_dir($tmp_root) && !@mkdir($tmp_root, 0777, TRUE)) {
 952      wd_err("ERROR: mkdir(@dir) (tmp_root) failed", array('@dir' => $tmp_root));
 953      exit(1);
 954    }
 955  
 956    // Use a tmp directory *specific* to this invocation, so that we don't
 957    // clobber other runs if the script is invoked twice (e.g. via cron and
 958    // manually, etc).
 959    $tmp_dir = $tmp_root . '/' . $task . '.' . getmypid();
 960    if (is_dir($tmp_dir)) {
 961      // Make sure we start with a clean slate
 962      drupal_exec("$rm -rf $tmp_dir/*");
 963    }
 964    else if (!@mkdir($tmp_dir, 0777, TRUE)) {
 965      wd_err("ERROR: mkdir(@dir) failed", array('@dir' => $tmp_dir));
 966      exit(1);
 967    }
 968  }
 969  
 970  /**
 971   * Initialize info from the {cvs_repositories} table, since there are
 972   * usually only a tiny handful of records, and it'll be faster to do
 973   * whatever we need via php than another JOIN...
 974   */
 975  function initialize_repository_info() {
 976    global $repositories;
 977    $query = db_query("SELECT rid, root, modules, name FROM {cvs_repositories}");
 978    while ($repo = db_fetch_object($query)) {
 979      $repositories[$repo->rid] = array('root' => $repo->root, 'modules' => $repo->modules, 'name' => $repo->name);
 980    }
 981  }
 982  
 983  
 984  /**
 985   * Fix the given .info file with the specified version string
 986   */
 987  function fix_info_file_version($file, $project_short_name, $version) {
 988    global $site_name;
 989  
 990    $info = "\n; Information added by $site_name packaging script on " . date('Y-m-d') . "\n";
 991    $info .= "version = \"$version\"\n";
 992    // .info files started with 5.x, so we don't have to worry about version
 993    // strings like "4.7.x-1.0" in this regular expression. If we can't parse
 994    // the version (also from an old "HEAD" release), or the version isn't at
 995    // least 6.x, don't add any "core" attribute at all.
 996    $matches = array();
 997    if (preg_match('/^((\d+)\.x)-.*/', $version, $matches) && $matches[2] >= 6) {
 998      $info .= "core = \"$matches[1]\"\n";
 999    }
1000    $info .= "project = \"$project_short_name\"\n";
1001    $info .= 'datestamp = "'. time() ."\"\n";
1002    $info .= "\n";
1003  
1004    if (!chmod($file, 0644)) {
1005      wd_err("ERROR: chmod(@file, 0644) failed", array('@file' => $file));
1006      return false;
1007    }
1008    if (!$info_fd = fopen($file, 'ab')) {
1009      wd_err("ERROR: fopen(@file, 'ab') failed", array('@file' => $file));
1010      return false;
1011    }
1012    if (!fwrite($info_fd, $info)) {
1013      wd_err("ERROR: fwrite(@file) failed". '<pre>' . $info, array('@file' => $file));
1014      return false;
1015    }
1016    return true;
1017  }
1018  
1019  /**
1020   * Update the DB with the new file info for a given release node.
1021   *
1022   * @param $nid
1023   *   The node ID of the release node to update.
1024   * @param $files
1025   *   Array of files to add to the release node.
1026   * @param $package_contents
1027   *   Optional. Array of nids of releases contained in a release package.
1028   */
1029  function package_release_update_node($nid, $files, $package_contents = array()) {
1030    global $drupal_root, $dest_root, $task;
1031  
1032    // PHP will cache the results of stat() and give us stale answers
1033    // here, unless we manually tell it otherwise!
1034    clearstatcache();
1035  
1036    // Make sure we're back at the webroot so node_load() and node_save()
1037    // can always find any files they (and the hooks they invoke) need.
1038    if (!drupal_chdir($drupal_root)) {
1039      return FALSE;
1040    }
1041  
1042    // If the site is using DB replication, force this node_load() to use the
1043    // primary database to avoid node_load() failures.
1044    if (function_exists('db_set_ignore_slave')) {
1045      db_set_ignore_slave();
1046    }
1047    // We don't want to waste too much RAM by leaving all these loaded nodes
1048    // in RAM, so we reset the node_load() cache each time we call it.
1049    $node = node_load($nid, NULL, TRUE);
1050    if (empty($node->nid)) {
1051      wd_err('node_load(@nid) failed', array('@nid' => $nid));
1052      return FALSE;
1053    }
1054  
1055    foreach ($files as $file_path) {
1056      // Compute the metadata for this file that we care about.
1057      $full_path = $dest_root . '/' . $file_path;
1058      $file_name = basename($file_path);
1059      $file_date = filemtime($full_path);
1060      $file_size = filesize($full_path);
1061      $file_hash = md5_file($full_path);
1062      $file_mime = file_get_mimetype($full_path);
1063  
1064      // First, see if we already have this file for this release node
1065      $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'", $node->nid, $file_name));
1066  
1067      // Insert or update the record in the DB as need.
1068      if (empty($file_data)) {
1069        // Don't have this file, insert a new record.
1070        db_query("INSERT INTO {files} (uid, filename, filepath, filemime, filesize, status, timestamp) VALUES (%d, '%s', '%s', '%s', %d, %d, %d)", $node->uid, $file_name, $file_path, $file_mime, $file_size, FILE_STATUS_PERMANENT, $file_date);
1071        $fid = db_last_insert_id('files', 'fid');
1072        db_query("INSERT INTO {project_release_file} (fid, nid, filehash) VALUES (%d, %d, '%s')", $fid, $node->nid, $file_hash);
1073      }
1074      else {
1075        // Already have this file for this release, update it.
1076        db_query("UPDATE {files} SET uid = %d, filename = '%s', filepath = '%s', filemime = '%s', filesize = %d, status = %d, timestamp = %d WHERE fid = %d", $node->uid, $file_name, $file_path, $file_mime, $file_size, FILE_STATUS_PERMANENT, $file_date, $file_data->fid);
1077        db_query("UPDATE {project_release_file} SET filehash = '%s' WHERE fid = %d", $file_hash, $file_data->fid);
1078      }
1079    }
1080  
1081    // Store package contents if necessary.
1082    if (!empty($package_contents) && module_exists('project_package')) {
1083      foreach ($package_contents as $item_nid) {
1084        db_query("INSERT INTO {project_package_local_release_item} (package_nid, item_nid) VALUES (%d, %d)", $nid, $item_nid);
1085      }
1086    }
1087  
1088    // Don't auto-publish security updates.
1089    $security_update_tid = variable_get('project_release_security_update_tid', 0);
1090    if ($task == 'tag' && !empty($node->taxonomy[$security_update_tid])) {
1091      watchdog('package_security', 'Not auto-publishing security update release.', array(), WATCHDOG_NOTICE, l(t('view'), 'node/' . $node->nid));
1092      return;
1093    }
1094  
1095    // Finally publish the node if it is currently unpublished. Instead of
1096    // directly updating {node}.status, we use node_save() so that other modules
1097    // which implement hook_nodeapi() will know that this node is now published.
1098    if (empty($node->status)) {
1099      $node->status = 1;
1100      node_save($node);
1101    }
1102  }
1103  
1104  /**
1105   * Find the youngest (newest) file in a directory tree.
1106   * Stolen wholesale from the original package-drupal.php script.
1107   * Modified to also notice any files that end with ".info" and store
1108   * all of them in the array passed in as an argument. Since we have to
1109   * recurse through the whole directory tree already, we should just
1110   * record all the info we need in one pass instead of doing it twice.
1111   */
1112  function file_find_youngest($dir, $timestamp, $exclude, &$info_files) {
1113    if (is_dir($dir)) {
1114      $fp = opendir($dir);
1115      while (FALSE !== ($file = readdir($fp))) {
1116        if (!in_array($file, $exclude)) {
1117          if (is_dir("$dir/$file")) {
1118            $timestamp = file_find_youngest("$dir/$file", $timestamp, $exclude, $info_files);
1119          }
1120          else {
1121            $mtime = filemtime("$dir/$file");
1122            $timestamp = ($mtime > $timestamp) ? $mtime : $timestamp;
1123            if (preg_match('/^.+\.info$/', $file)) {
1124              $info_files[] = "$dir/$file";
1125            }
1126          }
1127        }
1128      }
1129      closedir($fp);
1130    }
1131    return $timestamp;
1132  }
1133  
1134  
1135  // ------------------------------------------------------------
1136  // Functions: translation-status-related methods
1137  // TODO: get all this working. ;)
1138  // ------------------------------------------------------------
1139  
1140  
1141  /**
1142   * Extract some translation statistics:
1143   */
1144  function translation_status($dir, $version) {
1145    global $translations;
1146  
1147    $number_of_strings = translation_number_of_strings('drupal-pot', $version);
1148  
1149    $line = exec("$msgfmt --statistics $dir/$dir.po 2>&1");
1150    $words = preg_split('[\s]', $line, -1, PREG_SPLIT_NO_EMPTY);
1151  
1152    if (is_numeric($words[0]) && is_numeric($number_of_strings)) {
1153      $percentage = floor((100 * $words[0]) / ($number_of_strings));
1154      if ($percentage >= 100) {
1155        $translations[$dir][$version] = "<td style=\"color: green; font-weight: bold;\">100% (complete)</td>";
1156      }
1157      else {
1158        $translations[$dir][$version] = "<td>". $percentage ."% (". ($number_of_strings - $words[0]). " missing)</td>";
1159      }
1160    }
1161    else {
1162      $translations[$dir][$version] = "<td style=\"color: red; font-weight: bold;\">translation broken</td>";
1163    }
1164  }
1165  
1166  function translation_report($versions) {
1167    global $dest, $translations;
1168  
1169    $output  = "<table>\n";
1170    $output .= " <tr><th>Language</th>";
1171    foreach ($versions as $version) {
1172      $output .= "<th>$version</th>";
1173    }
1174    $output .= " </tr>\n";
1175  
1176    ksort($translations);
1177    foreach ($translations as $language => $data) {
1178      $output .= " <tr><td><a href=\"project/$language\">$language</a></td>";
1179      foreach ($versions as $version) {
1180        if ($data[$version]) {
1181          $output .= $data[$version];
1182        }
1183        else {
1184          $output .= "<td></td>";
1185        }
1186      }
1187      $output .= "</tr>\n";
1188    }
1189    $output .= "</table>";
1190  
1191    $fd = fopen("$dest/translation-status.txt", 'w');
1192    fwrite($fd, $output);
1193    fclose($fd);
1194    wprint("wrote $dest/translation-status.txt");
1195  }
1196  
1197  function translation_number_of_strings($dir, $version) {
1198    static $number_of_strings = array();
1199    if (!isset($number_of_strings[$version])) {
1200      drupal_exec("$msgcat $dir/general.pot $dir/[^g]*.pot | $msgattrib --no-fuzzy -o $dir/$dir.pot");
1201      $line = exec("$msgfmt --statistics $dir/$dir.pot 2>&1");
1202      $words = preg_split('[\s]', $line, -1, PREG_SPLIT_NO_EMPTY);
1203      $number_of_strings[$version] = $words[3];
1204      @unlink("$dir/$dir.pot");
1205    }
1206    return $number_of_strings[$version];
1207  }
1208  
1209  /**
1210   * Construct a .make file which will build Drupal core.
1211   *
1212   * This is a very simple 'bootstrap' .make file, which should only ever include
1213   * the minimal package metadata to build core.
1214   *
1215   * All arguments should be in a format that drush_make can understand.
1216   *
1217   * @param $core
1218   *   The core release to package with the profile.
1219   */
1220  function core_make_file($core) {
1221  
1222    $output = '';
1223    $output .= "core = $core\n";
1224    $output .= "projects[drupal] = $core\n";
1225  
1226    return $output;
1227  }


Generated: Thu Mar 24 11:18:33 2011 Cross-referenced by PHPXref 0.7