/** * Figure out what project and API terms to generate the history for. */ function project_release_history_generate_all($project_id = 0) { if (!empty($project_id)) { if (is_numeric($project_id)) { $project_nid = $project_id; } else { $project_nid = db_result(db_query("SELECT nid FROM {project_projects} WHERE uri = '%s'", $project_id)); } if (empty($project_nid)) { wd_err(array('message' => 'Project ID %id not found', 'args' => array('%id' => $project_id))); return FALSE; } wd_msg(array('message' => 'Generating XML release history files for project: %id.', 'args' => array('%id' => $project_id))); } else { wd_msg(array('message' => 'Generating XML release history files for all projects.', 'args' => array())); } $api_terms = project_release_compatibility_list(); $i = 0; if (empty($project_nid)) { // Generate all.xml files for projects with releases. $query = db_query("SELECT DISTINCT(pid) FROM {project_release_nodes}"); while ($project = db_fetch_object($query)) { project_release_history_generate_project_xml($project->pid); $i++; } } else { project_release_history_generate_project_xml($project_nid); $i++; } if ($i == 1) { wd_msg(array('message' => 'Generated an XML release history summary for a project.')); } else { wd_msg(array('message' => 'Generated XML release history summaries for @count projects.', 'args' => array('@count' => $i))); } // Generate XML files based on API compatibility. $i = 0; $args = array_keys($api_terms); $placeholders = db_placeholders($args); $where = ''; if (!empty($project_nid)) { $args[] = $project_nid; $where = 'AND pid = %d'; } $query = db_query("SELECT DISTINCT(pid), version_api_tid FROM {project_release_nodes} WHERE version_api_tid IN ({$placeholders}) {$where}", $args); while ($project = db_fetch_object($query)) { project_release_history_generate_project_xml($project->pid, $project->version_api_tid); $i++; } if ($i == 1) { wd_msg(array('message' => 'Completed XML release history files for 1 project/version pair')); } else { wd_msg(array('message' => 'Completed XML release history files for @count project/version pairs', 'args' => array('@count' => $i))); } return TRUE; }
function package_release_contrib($type, $nid, $project_short_name, $version, $tag, $release_dir) { global $tmp_dir, $repositories, $dest_root, $dest_rel; global $cvs, $tar, $gzip, $rm, $ln; global $drush, $drush_make_dir; global $license, $trans_install; // Files to ignore when checking timestamps: $exclude = array('.', '..', 'LICENSE.txt'); $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}"; $cvs_export_dir = "{$repositories[DRUPAL_CONTRIB_REPOSITORY_ID]['modules']}/{$contrib_type}/{$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; // Remember if the tar.gz version of this release file already exists. $tgz_exists = is_file($full_dest_tgz); // 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'; } // Checkout this release from CVS, and see if we need to rebuild it if (!drupal_exec("{$cvs} -q export -r {$tag} -d {$project_short_name} {$cvs_export_dir}")) { return 'error'; } if (!is_dir("{$project_build_root}/{$project_short_name}")) { 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); return 'error'; } $info_files = array(); $youngest = file_find_youngest($project_short_name, 0, $exclude, $info_files); if ($type == 'branch' && $tgz_exists && filemtime($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 (!fix_info_file_version($file, $project_short_name, $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("{$ln} -sf {$license} {$project_short_name}/LICENSE.txt")) { return 'error'; } // Do we want a subdirectory in the tarball or not? $tarball_needs_subdir = TRUE; if ($contrib_type == 'translations' && $project_short_name != 'drupal-pot') { // Translation projects are packaged differently based on core version. if (intval($version) > 5) { if (!($to_tar = package_release_contrib_d6_translation($project_short_name, $version, $release_node_view_link))) { // Return on error. return 'error'; } $tarball_needs_subdir = FALSE; } elseif (!($to_tar = package_release_contrib_pre_d6_translation($project_short_name, $version, $release_node_view_link))) { // Return on error. return 'error'; } } else { // Not a translation: just grab the whole directory. $to_tar = $project_short_name; } if (!$tarball_needs_subdir) { if (!drupal_chdir($project_short_name)) { return 'error'; } } // 'h' is for dereference, we want to include the files, not the links if (!drupal_exec("{$tar} -ch --file=- {$to_tar} | {$gzip} -9 --no-name > {$full_dest_tgz}")) { return 'error'; } $files[] = $file_path_tgz; // Start with no package contents, since this is only valid for profiles. $package_contents = array(); // This is a profile, so invoke the drush_make routines to package core // and/or any other contrib releases specified in the profile's .make file. if ($contrib_type == 'profiles') { // Move inside the profile directory. if (!drupal_chdir("{$project_build_root}/{$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)) { // 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' => $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' => $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('.', $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' => $release_file_id), $release_node_view_link); return 'error'; } } // NO-CORE DISTRIBUTION. $no_core_id = "{$release_file_id}-no-core"; // Build the drupal file path and the full file path. $no_core_file_path = "{$dest_rel}/{$no_core_id}.tar.gz"; $no_core_full_dest = "{$dest_root}/{$no_core_file_path}"; // 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("{$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} .")) { // The build failed, get any output error messages and include them // in the packaging error report. $build_errors_file = "{$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($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("{$tar} -ch --file=- {$project_short_name} | {$gzip} -9 --no-name > {$no_core_full_dest}")) { return 'error'; } $files[] = $no_core_file_path; // 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"; file_put_contents($core_makefile, core_make_file($core_version)); // Run drush_make to build core. if (!drupal_exec("{$drush} --include={$drush_make_dir} 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($project_short_name, "{$core_build_dir}/profiles/{$project_short_name}")) { return 'error'; } $core_id = "{$release_file_id}-core"; // Build the drupal file path and the full file path. $core_file_path = "{$dest_rel}/{$core_id}.tar.gz"; $core_full_dest = "{$dest_root}/{$core_file_path}"; // Package the core distribution. // 'h' is for dereference, we want to include the files, not the links if (!drupal_exec("{$tar} -ch --file=- {$core_build_dir} | {$gzip} -9 --no-name > {$core_full_dest}")) { return 'error'; } $files[] = $core_file_path; // Development releases may have changed package contents -- clear out // their package item summary so a fresh item summary will be inserted. if ($type == 'branch' && 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)))) { return 'error'; } $package_contents[] = $core_release_nid; // Retrieve the package contents for the release. $package_contents_file = "{$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)) { $package_contents[] = $line; } } } else { wd_err("ERROR: %file does not exist for %profile release.", array('%file' => $package_contents_file, '%profile' => $release_file_id), $release_node_view_link); return 'error'; } } } else { wd_msg("No makefile for %profile profile -- skipping extended packaging.", array('%profile' => $release_file_id), $release_node_view_link); } } // As soon as the tarball exists, update the DB package_release_update_node($nid, $files, $package_contents); if ($tgz_exists) { wd_msg("%id has changed, re-packaged.", array('%id' => $release_file_id), $view_link); } else { 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'; }
function package_release($release_nid, $project, $repository, $version, $label) { global $tmp_dir, $drupal_root, $dest_root, $dest_rel; global $tar, $gzip, $rm, $ln, $mkdir; global $license; $project_directory_item = versioncontrol_get_item($repository, $project['directory'], array('label' => $label)); if (empty($project_directory_item)) { wd_err('ERROR: Could not retrieve project directory item.'); } // In Version Control API, item paths start with a slash. $relative_project_dir = escapeshellcmd(substr($project['directory'], 1)); $uri = escapeshellcmd($project['uri']); ///TODO: drupal.org specific hack, get rid of it somehow $is_core = $site_name == 'drupal.org' && $repository['repo_id'] == 1 && $uri == 'drupal'; $is_contrib = $site_name == 'drupal.org' && $repository['repo_id'] == 2; $id = $uri . '-' . $version; $view_link = l(t('view'), 'node/' . $release_nid); $file_name = $id . '.tar.gz'; $file_path = $dest_rel . '/' . $file_name; $full_dest = $dest_root . '/' . $file_path; $export_dir = $tmp_dir . '/' . $id; if ($is_contrib) { ///TODO: drupal.org specific hack, get rid of it somehow $export_dir = $tmp_dir . '/' . $relative_project_dir; } $success = versioncontrol_export_directory($repository, $project_directory_item, $export_dir); if (!$success) { wd_err('ERROR: %dir @ !labeltype %labelname could not be exported', array('%dir' => $export_dir, '!labeltype' => $label['type'] == VERSIONCONTROL_OPERATION_BRANCH ? t('branch') : t('tag'), '%labelname' => $label['name']), $view_link); return FALSE; } $info_files = array(); // Files to ignore when checking timestamps: $exclude = array('.', '..', 'LICENSE.txt'); $youngest = file_find_youngest($export_dir, 0, $exclude, $info_files); if (is_file($full_dest) && filectime($full_dest) + 300 > $youngest) { // The existing tarball for this release is newer than the youngest // file in the directory, we're done. return FALSE; } ///TODO: drupal.org specific hack, get rid of it somehow // Fix any .info files. foreach ($info_files as $file) { if (!fix_info_file_version($file, $uri, $version)) { wd_err('ERROR: Failed to update version in %file, aborting packaging', array('%file' => $file), $view_link); return FALSE; } } // Do we want a subdirectory in the tarball or not? $tarball_needs_subdir = TRUE; ///TODO: drupal.org specific hack, get rid of it somehow if ($is_contrib) { // Link not copy, since we want to preserve the date. if (!drupal_exec("{$ln} -sf {$license} {$export_dir}/LICENSE.txt")) { return FALSE; } $parts = split('/', $relative_project_dir); $contrib_type = $parts[1]; // modules, themes, theme-engines, or translations if ($contrib_type == 'translations' && $project['uri'] != 'drupal-pot') { ///TODO: drupal.org specific hack (contd.), get rid of it somehow // Translation projects are packaged differently based on core version. if (intval($version) == 6) { if (!($to_tar = package_release_contrib_d6_translation($export_dir, $uri, $version, $view_link))) { // Return on error. return FALSE; } $tarball_needs_subdir = FALSE; } elseif (!($to_tar = package_release_contrib_pre_d6_translation($export_dir, $uri, $version, $view_link))) { // Return on error. return FALSE; } } } if (empty($to_tar)) { // Not a translation: just grab the whole directory. $to_tar = basename($export_dir); } // We want tar to get a relative list of paths, so we tell it to change into // the parent directory, except for D6 translations which get special cased. $tar_dir = $tarball_needs_subdir ? dirname($export_dir) : $export_dir; // 'h' is for dereference, we want to include the files, not the links if (!drupal_exec("{$tar} -ch --directory {$tar_dir} --file=- {$to_tar} | {$gzip} -9 --no-name > {$full_dest}")) { return FALSE; } // As soon as the tarball exists, we want to update the DB about it. package_release_update_node($release_nid, $file_path); wd_msg("%id has changed, re-packaged.", array('%id' => $id), $view_link); // Don't consider failure to remove this directory a build failure. drupal_exec("{$rm} -rf {$export_dir}"); return TRUE; }
function package_releases($type, $project_id = 0) { global $drupal_root, $dest_root, $dest_rel, $tmp_dir, $wd_err_msg; global $php, $project_release_create_history; if (!empty($project_id)) { if (is_numeric($project_id)) { $project_nid = $project_id; } else { $project_nid = db_result(db_query("SELECT nid FROM {project_projects} WHERE uri = '%s'", $project_id)); } // We repeatedly clear the node_load() cache, but we have our own cache // for loading the project nodes since those tend to repeat and we need to // load less of them. $project_node = project_release_packager_node_load($project_nid); if (empty($project_node)) { wd_err('ERROR: Project ID %id not found', array('%id' => $project_id)); return FALSE; } } $rel_node_join = ''; $where_args = array(); if ($type == 'tag') { $where = " AND (prn.rebuild = %d) AND (f.filepath IS NULL OR f.filepath = '')"; $where_args[] = 0; // prn.rebuild $plural = t('tags'); } elseif ($type == 'branch') { $rel_node_join = " INNER JOIN {node} nr ON prn.nid = nr.nid"; $where = " AND (prn.rebuild = %d) AND ((f.filepath IS NULL) OR (f.filepath = '') OR (nr.status = %d))"; $where_args[] = 1; // prn.rebuild $where_args[] = 1; // nr.status $plural = t('branches'); if (empty($project_node)) { wd_msg("Starting to package all snapshot releases."); } else { wd_msg("Starting to package snapshot releases for project id: %project_short_name.", array('%project_short_name' => $project_node->project['uri']), l(t('view'), 'node/' . $project_node->nid)); } } else { wd_err("ERROR: package_releases() called with unknown type: %type", array('%type' => $type)); return FALSE; } $args = array(); $args[] = 1; // Account for np.status = 1. $args[] = 1; // Account for prp.releases = 1. if (!empty($project_node)) { $where .= ' AND prn.pid = %d'; $where_args[] = $project_node->nid; } $args = array_merge($args, $where_args); $query = db_query("SELECT prn.nid 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 WHERE 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)) { // This query could pull multiple rows of the same release since multiple // files per release node are allowed. Account for this by keying on // release nid. $releases[$release->nid] = $release->nid; } foreach ($releases as $release_nid) { $wd_err_msg = array(); // We don't want to waste too much RAM by leaving all these loaded nodes // in RAM, so we reset the node_load() cache each time we call it. $release_node = node_load($release_nid, NULL, TRUE); if (empty($release_node)) { wd_err("ERROR: Can't load release node for release ID %nid", array('%nid' => $release_nid)); continue; } $packager = project_release_get_packager_plugin($release_node, $dest_root, $dest_rel, $tmp_dir); if (empty($packager)) { wd_err("ERROR: Can't find packager plugin to use for %release", array('%release' => $release_node->title)); continue; } db_query("DELETE FROM {project_release_package_errors} WHERE nid = %d", $release_node->nid); chdir($drupal_root); $files = array(); $contents = array(); $rval = $packager->createPackage($files, $contents); $num_considered++; chdir($drupal_root); switch ($rval) { case 'success': case 'rebuild': project_release_packager_update_node($release_node, $dest_root, $files, $contents); module_invoke_all('project_release_create_package', $project_node, $release_node); $num_built++; $packager->cleanupSuccessfulBuild(); $release_pid = $release_node->project_release['pid']; $project_nids[$release_pid] = TRUE; $release_node_view_link = l(t('View'), 'node/' . $release_node->nid); if ($rval == 'rebuild') { $msg = '%release_title has changed, re-packaged.'; } else { $msg = 'Packaged %release_title.'; } wd_msg($msg, array('%release_title' => $release_node->title), $release_node_view_link); break; case 'error': $packager->cleanupFailedBuild(); break; } if (count($wd_err_msg)) { db_query("INSERT INTO {project_release_package_errors} (nid, messages) values (%d, '%s')", $release_node->nid, serialize($wd_err_msg)); } } if ($num_built || $type == 'branch') { if (!empty($project_node)) { wd_msg("Done packaging releases for @project_short_name from !plural: !num_built built, !num_considered considered.", array('@project_short_name' => $project_node->project['uri'], '!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)); } } // Finally, regenerate release history XML files for all projects we touched. if (!empty($project_nids) && !empty($project_release_create_history)) { wd_msg('Re-generating release history XML files'); $i = $fails = 0; foreach ($project_nids as $project_nid => $value) { if (drupal_exec("{$php} {$project_release_create_history} {$project_nid}")) { $i++; } else { $fails++; } } if (!empty($fails)) { wd_msg('ERROR: Failed to re-generate release history XML files for !num project(s)', array('!num' => $fails)); } wd_msg('Done re-generating release history XML files for !num project(s)', array('!num' => $i)); } }