<?php
|
|
|
|
// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
|
|
//
|
|
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
|
|
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
|
|
// $Id$
|
|
|
|
// ** This is the main script to release Tiki **
|
|
//
|
|
// To get the Tiki release HOWTO, try:
|
|
// php doc/devtools/release.php --howto
|
|
//
|
|
// You can also get a detailed help on this script with:
|
|
// php doc/devtools/release.php --help
|
|
//
|
|
|
|
define('TOOLS', __DIR__);
|
|
define('ROOT', realpath(TOOLS . '/../..'));
|
|
define('TEMP_DIR', 'temp');
|
|
|
|
define('CHANGELOG_FILENAME', 'changelog.txt');
|
|
define('CHANGELOG', ROOT . '/' . CHANGELOG_FILENAME);
|
|
define('COPYRIGHTS_FILENAME', 'copyright.txt');
|
|
define('COPYRIGHTS', ROOT . '/' . COPYRIGHTS_FILENAME);
|
|
define('SF_TW_MEMBERS_URL', 'http://sourceforge.net/p/tikiwiki/_members');
|
|
define('DEV_TW_MEMBERS_URL', 'http://dev.tiki.org/getTikiUser.php');
|
|
define('README_FILENAME', 'README');
|
|
define('README', ROOT . '/' . README_FILENAME);
|
|
define('LICENSE_FILENAME', 'license.txt');
|
|
|
|
define('PIPELINE_STATUS_PASSED', 'passed');
|
|
define('PIPELINE_STATUS_FAILED', 'failed');
|
|
define('PIPELINES_FETCH_AMOUNT', 25);
|
|
|
|
// Display all errors and warnings, including strict level
|
|
define('ERROR_REPORTING_LEVEL', E_ALL | E_STRICT);
|
|
error_reporting(ERROR_REPORTING_LEVEL);
|
|
|
|
chdir(ROOT . '/');
|
|
|
|
require_once ROOT . '/lib/setup/third_party.php';
|
|
require_once ROOT . '/doc/devtools/vcscommons.php';
|
|
|
|
if (version_compare(PHP_VERSION, '5.0.0', '<')) {
|
|
error("You need PHP version 5 or more to run this script\n");
|
|
}
|
|
$phpCommand = isset($_SERVER['_']) ? $_SERVER['_'] : 'php';
|
|
$phpCommandArguments = implode(' ', $_SERVER['argv']);
|
|
|
|
if (! ($options = get_options()) || $options['help']) {
|
|
display_usage();
|
|
}
|
|
|
|
$vcs = 'svn';
|
|
if ($options['use-git'] || is_dir(ROOT . '/.git')) {
|
|
$vcs = 'git';
|
|
}
|
|
|
|
require_once TOOLS . '/' . $vcs . 'tools.php';
|
|
|
|
|
|
if ($options['devmode']) {
|
|
$options['no-commit'] = true;
|
|
$options['no-check-vcs'] = true;
|
|
$options['no-first-update'] = true;
|
|
$options['debug-packaging'] = true;
|
|
}
|
|
if ($options['howto']) {
|
|
display_howto();
|
|
}
|
|
|
|
|
|
if (! check_bin_version()) {
|
|
error("You need the VCS '" . getBinName() . "' program at least at version " . getMinVersion() . "\n");
|
|
}
|
|
|
|
if (! $options['no-check-vcs'] && has_uncommited_changes('.')) {
|
|
error("Uncommitted changes exist in the working folder.\n");
|
|
}
|
|
|
|
include_once('lib/setup/twversion.class.php');
|
|
$TWV = new TWVersion();
|
|
|
|
if ($options['only-secdb']) {
|
|
updateSecdb($TWV->version);
|
|
exit;
|
|
}
|
|
|
|
$script = $_SERVER['argv'][0];
|
|
$version = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
|
|
$subrelease = isset($_SERVER['argv'][2]) ? $_SERVER['argv'][2] : '';
|
|
|
|
if (! preg_match("/^\d+\.\d+$/", $version)) {
|
|
error("Version number should be in X.X format.\n");
|
|
}
|
|
|
|
$isPre = strpos($subrelease, 'pre') === 0;
|
|
if ($isPre) {
|
|
$subrelease = substr($subrelease, 3);
|
|
$pre = 'pre';
|
|
} else {
|
|
$pre = '';
|
|
}
|
|
$splitedversion = explode('.', $version);
|
|
$mainversion = $splitedversion[0];
|
|
|
|
$check_version = $version . $subrelease;
|
|
if ($TWV->version !== $check_version && ! $options['devmode']) {
|
|
error("The version in the code " . strtolower($TWV->version) . " differs from the version provided to the script $check_version.\nThe version should be modified in lib/setup/twversion.class.php to match the released version.");
|
|
}
|
|
|
|
echo color("\nTiki release process started for version '$version" . ($subrelease ? " $subrelease" : '') . "'\n", 'cyan');
|
|
if ($isPre) {
|
|
echo color("The script is running in 'pre-release' mode, which means that no tag will be created.\n", 'yellow');
|
|
}
|
|
|
|
if (! $options['no-first-update'] && important_step('Update working copy to the last revision')) {
|
|
echo "Update in progress...";
|
|
update_working_copy('.');
|
|
|
|
if (! $options['no-check-vcs'] && has_uncommited_changes('.')) {
|
|
error("\rUncommitted changes exist in the working folder.\n");
|
|
}
|
|
$revision = get_revision('.');
|
|
info("\r>> Checkout updated to revision $revision.");
|
|
}
|
|
|
|
if (empty($subrelease)) {
|
|
$branch = $vcs == 'svn' ? "branches/$mainversion.x" : "$mainversion.x";
|
|
$tag = "tags/$version";
|
|
$packageVersion = $version;
|
|
if (! empty($pre)) {
|
|
$packageVersion .= ".$pre";
|
|
}
|
|
$secdbVersion = $version;
|
|
} else {
|
|
$branch = $vcs == 'svn' ? "branches/$mainversion.x" : "$mainversion.x";
|
|
$tag = "tags/$version$subrelease";
|
|
$packageVersion = "$version.$pre$subrelease";
|
|
$secdbVersion = "$version$subrelease";
|
|
}
|
|
|
|
if (! $options['no-readme-update'] && important_step("Update '" . README_FILENAME . "' file")) {
|
|
update_readme_file($secdbVersion, $version);
|
|
info('>> ' . README_FILENAME . ' file updated.');
|
|
important_step('Commit updated ' . README_FILENAME . ' file', true, "[REL] Update " . README_FILENAME . " file for $secdbVersion");
|
|
}
|
|
|
|
if (! $options['no-lang-update'] && important_step("Update language files")) {
|
|
passthru("$phpCommand console.php translation:getstrings");
|
|
$removeFiles = glob('lang/*/language.php.old');
|
|
foreach ($removeFiles as $rf) {
|
|
unlink($rf);
|
|
}
|
|
unset($removeFiles);
|
|
info('>> Language files updated and temporary files removed.');
|
|
important_step('Commit updated language files', true, "[REL] Update language.php files for $secdbVersion");
|
|
}
|
|
|
|
if (! $options['no-changelog-update'] && important_step("Update '" . CHANGELOG_FILENAME . "' file (using final version number '$version')")) {
|
|
if ($ucf = update_changelog_file($version)) {
|
|
if ($ucf['nbCommits'] == 0) {
|
|
info('>> Changelog updated (last commits were already inside)');
|
|
} else {
|
|
if ($ucf['sameFinalVersion']) {
|
|
info(">> There were already some commits for the same final version number in the changelog. Merging them with the new ones.");
|
|
}
|
|
info(">> Changelog updated with {$ucf['nbCommits']} new commits (revision {$ucf['firstRevision']} to {$ucf['lastRevision']}), excluding duplicates, merges and release-related commits.");
|
|
}
|
|
important_step("Commit new " . CHANGELOG_FILENAME, true, "[REL] Update " . CHANGELOG_FILENAME . " for $secdbVersion");
|
|
} else {
|
|
error('Changelog update failed.');
|
|
}
|
|
unset($ucf);
|
|
}
|
|
|
|
$nbCommiters = 0;
|
|
if (! $options['no-copyright-update'] && important_step("Update '" . COPYRIGHTS_FILENAME . "' file (using final version number '$version')")) {
|
|
if ($ucf = update_copyright_file($mainversion . '.0')) {
|
|
info(
|
|
"\r>> Copyrights updated: "
|
|
. ($ucf['newContributors'] == 0 ? 'No new contributor, ' : "+{$ucf['newContributors']} contributor(s), ")
|
|
. ($ucf['newCommits'] == 0 ? 'No new commit' : "+{$ucf['newCommits']} commit(s)")
|
|
);
|
|
important_step("Commit new " . COPYRIGHTS_FILENAME, true, "[REL] Update " . COPYRIGHTS_FILENAME . " for $secdbVersion");
|
|
} else {
|
|
error('Copyrights update failed.');
|
|
}
|
|
}
|
|
|
|
if (! $options['no-check-db'] && important_step("Check Database related files and upgrade scripts")) {
|
|
$error_msg = '';
|
|
check_database_files_and_upgrade($mainversion, $error_msg) or error($error_msg . "If you want to disable this checks use --no-check-db\n");
|
|
info('>> Current database scripts successfully passed the check.');
|
|
}
|
|
|
|
if (! $options['no-check-php'] && important_step("Check syntax of all PHP files")) {
|
|
$error_msg = '';
|
|
$dir = '.';
|
|
check_php_syntax($dir, $error_msg, $options['no-check-php-warnings']) or error($error_msg);
|
|
info('>> Current PHP code successfully passed the syntax check.');
|
|
}
|
|
|
|
if (! $options['no-check-smarty'] && important_step("Check syntax of all Smarty templates")) {
|
|
$error_msg = '';
|
|
require_once ROOT . '/lib/core/TikiDb.php';
|
|
require_once ROOT . '/lib/core/TikiDb/Bridge.php';
|
|
require_once ROOT . '/lib/language/Language.php';
|
|
check_smarty_syntax($error_msg);
|
|
info('>> Current Smarty code successfully passed the syntax check.');
|
|
}
|
|
|
|
if (! $options['no-secdb'] && important_step("Update SecDB file(s) 'db/tiki-secdb_{$version}_mysql.sql'")) {
|
|
if (updateSecdb($TWV->version)) {
|
|
important_step("Commit SecDB file changes", true, "[REL] SecDB for $secdbVersion");
|
|
}
|
|
}
|
|
|
|
if ($isPre) {
|
|
if (! $options['no-packaging'] && important_step("Build packages files")) {
|
|
build_packages($packageVersion);
|
|
echo color("\nMake sure these tarballs are tested by at least 3 different people.\n\n", 'cyan');
|
|
} else {
|
|
echo color("This was the last step.\n", 'cyan');
|
|
}
|
|
} else {
|
|
if (! $options['no-tagging']) {
|
|
$tagAlreadyExists = tag_exists($tag, true);
|
|
if ($tagAlreadyExists && important_step("The Tag '$tag' already exists: Delete the existing tag in order to create a new one")) {
|
|
$commit_msg = "[REL] Deleting tag '$tag' in order to create a new one";
|
|
if ($options['no-commit']) {
|
|
print "Skipping actual commit ('$commit_msg') because no-commit = true\n";
|
|
} else {
|
|
delete_tag($tag, $commit_msg);
|
|
$tagAlreadyExists = false;
|
|
info(">> Tag '$tag' deleted.");
|
|
}
|
|
}
|
|
if (! $tagAlreadyExists) {
|
|
update_working_copy('.');
|
|
$revision = get_revision(ROOT);
|
|
if (important_step("Tag release using branch '$branch' at revision $revision")) {
|
|
$commit_msg = '[REL] Tagging release';
|
|
if ($options['no-commit']) {
|
|
print "Skipping actual commit ('$commit_msg') because no-commit = true\n";
|
|
} else {
|
|
create_tag($tag, $commit_msg, $branch, $revision);
|
|
info(">> Tag '$tag' created.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (! $options['no-packaging'] && important_step("Build packages files")) {
|
|
build_packages($packageVersion);
|
|
} else {
|
|
info("This was the last step.\n");
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
|
|
/**
|
|
*
|
|
* Will remove old secdb files and generate a new one based on working copy files.
|
|
*
|
|
* @param $version string The current tiki version to use eg. 17.0 or 21.0RC1
|
|
* @return bool true on success
|
|
*/
|
|
|
|
function updateSecdb($version)
|
|
{
|
|
// first unset any preexisting files.
|
|
echo(">>");
|
|
$vcs = preg_match('/' . getBinName() . '$/', $version);
|
|
|
|
// if we are not creating a release skip deleting old files.
|
|
if (! $vcs) {
|
|
$files = glob(ROOT . '/db/tiki-secdb_*_mysql.sql');
|
|
foreach ($files as $file) {
|
|
$file = escapeshellarg($file);
|
|
delete_file($file);
|
|
}
|
|
echo(' Removed ' . count($files) . ' old secdb files.');
|
|
|
|
$excludes = [];
|
|
} else {
|
|
$excludes = array_keys(files_differ(ROOT));
|
|
}
|
|
|
|
$file = "/db/tiki-secdb_{$version}_mysql.sql";
|
|
|
|
if (! $fp = @fopen(ROOT . $file, 'w')) {
|
|
error('The SecDB file "' . ROOT . $file . '" is not writable or can\'t be created.');
|
|
return false;
|
|
}
|
|
$queries = [];
|
|
build_secdb_queries(ROOT, $version, $queries, $excludes);
|
|
|
|
if (! empty($queries)) {
|
|
sort($queries);
|
|
fwrite($fp, "start transaction;\n");
|
|
fwrite($fp, "DELETE FROM `tiki_secdb`;\n");
|
|
// This index was originally created with a size limit that would raise an error on some versions,
|
|
// notably on 18.0. Since this file is executed before any patch in installer/schema, the fix had to
|
|
// be done here. It's a quick operation because table is empty, so no harm in leaving this here forever.
|
|
fwrite($fp, "ALTER TABLE `tiki_secdb` DROP PRIMARY KEY, ADD PRIMARY KEY (`filename`(171),`tiki_version`(20));\n\n");
|
|
|
|
$insertString = 'INSERT INTO `tiki_secdb` (`filename`, `md5_value`, `tiki_version`) VALUES ';
|
|
|
|
$extendedInsertSize=0;
|
|
$extendedInsertMaxSize=1*1024*1024 - 100; // 1MB with 100 bytes safety limit, some old versions of Mysql had max_allowed_packet=1MB
|
|
|
|
foreach ($queries as $q) {
|
|
if (($extendedInsertSize + strlen($q) + 2) > $extendedInsertMaxSize ) {
|
|
fwrite($fp,";\n");
|
|
$extendedInsertSize=0;
|
|
}
|
|
if ($extendedInsertSize === 0) {
|
|
fwrite($fp, $insertString ."\n");
|
|
$extendedInsertSize = strlen($insertString) + 1;
|
|
} else {
|
|
fwrite($fp, ",\n");
|
|
$extendedInsertSize+=2;
|
|
}
|
|
fwrite($fp, $q);
|
|
$extendedInsertSize+=strlen($q);
|
|
}
|
|
fwrite($fp, ";\n");
|
|
|
|
fwrite($fp, "commit;\n");
|
|
}
|
|
fclose($fp);
|
|
|
|
echo(" $file was generated.\n");
|
|
if (! $vcs) {
|
|
$file = escapeshellarg(ROOT . $file); // escape file name for use in command line.
|
|
add($file);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Similar to md5_check_dir in tiki-admin_security.php but creates the sql queries for /db/tiki-secdb_{$version}_mysql.sql
|
|
*
|
|
* @param string $dir
|
|
* @param string $version
|
|
* @param array $queries queries returned
|
|
* @param array $excludes files to exclude when doing secdb on an svn or checkout
|
|
*/
|
|
function build_secdb_queries($dir, $version, &$queries, $excludes = [])
|
|
{
|
|
$d = dir($dir);
|
|
$link = null;
|
|
|
|
while (false !== ($e = $d->read())) {
|
|
$entry = $dir . '/' . $e;
|
|
if (is_link($entry)) {
|
|
continue; // if is a symlink we should not run any hash
|
|
}
|
|
if (is_dir($entry)) {
|
|
// do not descend and no CVS/Subversion files
|
|
if ($e != '..' && $e != '.' && $e != 'CVS' && $e != '.git' && $e != '.gitignore' && $e != '.svn' && $entry != ROOT . '/temp' && $entry != ROOT . '/vendor_custom' && $entry != ROOT . '/_custom') {
|
|
build_secdb_queries($entry, $version, $queries, $excludes);
|
|
}
|
|
} else {
|
|
if (preg_match('/\.(sql|css|tpl|js|php)$/', $e) && realpath($entry) != __FILE__ && $entry != './db/local.php') {
|
|
$file = '.' . substr($entry, strlen(ROOT));
|
|
|
|
if (in_array($entry, $excludes)) {
|
|
continue;
|
|
}
|
|
|
|
// Escape filename. Since this requires a connection to MySQL (due to the charset), do so conditionally to reduce the risk of connection failure.
|
|
if (! preg_match('/^[a-zA-Z!-9\/ _+.-@]+$/', $file)) {
|
|
if (! $link) {
|
|
$link = mysqli_connect();
|
|
|
|
if (mysqli_connect_errno()) {
|
|
global $phpCommand, $phpCommandArguments;
|
|
error(
|
|
"SecDB step failed because some filenames (e.g. {$file}) need escaping but no MySQL connection has been found (" . mysqli_connect_error() . ")."
|
|
. "\nTry this command line instead (replace HOST, USER and PASS by a valid MySQL host, user and password) :"
|
|
. "\n\n\t" . $phpCommand
|
|
. " -d mysqli.default_host=HOST -d mysqli.default_user=USER -d mysqli.default_pw=PASS "
|
|
. $phpCommandArguments . "\n"
|
|
);
|
|
}
|
|
}
|
|
$file = @mysqli_real_escape_string($link, $file);
|
|
}
|
|
|
|
if (is_readable($entry)) {
|
|
$hash = md5_file($entry);
|
|
$queries[] = "('$file', '$hash', '$version')";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$d->close();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Deletes a directory and its contents.
|
|
*
|
|
* @param string $dir directory to delete.
|
|
* @return string|bool returns the filename that an error occurred in, false otherwise
|
|
*/
|
|
|
|
function rrmdir($dir)
|
|
{
|
|
if (is_dir($dir)) {
|
|
$objects = scandir($dir);
|
|
foreach ($objects as $object) {
|
|
if ($object != "." && $object != "..") {
|
|
@chmod($dir . "/" . $object, 0777);
|
|
if (filetype($dir . "/" . $object) === 'dir') {
|
|
$error = rrmdir($dir . "/" . $object);
|
|
if ($error) {
|
|
return $error;
|
|
}
|
|
} elseif (! @unlink($dir . "/" . $object)) {
|
|
return 'Could not delete ' . $dir . "/" . $object . "\n";
|
|
}
|
|
}
|
|
}
|
|
reset($objects);
|
|
@unlink($dir . '/.DS_store');
|
|
if (! @rmdir($dir)) {
|
|
return 'Could not delete ' . $dir . "\n";
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
* Recursivley deletes specific files or directories
|
|
*
|
|
* @param string $src Directory to search through
|
|
* @param array $files An array of file names to delete.
|
|
*/
|
|
|
|
function removeFiles($src, $files)
|
|
{
|
|
$dir = opendir($src);
|
|
while (false !== ($file = readdir($dir))) {
|
|
if (($file != '.') && ($file != '..')) {
|
|
$full = $src . '/' . $file;
|
|
if (is_dir($full)) {
|
|
$flag = false;
|
|
|
|
foreach ($files as $delfile) {
|
|
if (basename($full) === $delfile) {
|
|
rrmdir($full);
|
|
$flag = true;
|
|
break;
|
|
}
|
|
}
|
|
if (! $flag) {
|
|
removeFiles($full, $files);
|
|
}
|
|
} else {
|
|
foreach ($files as $delfile) {
|
|
if (basename($full) === $delfile) {
|
|
@chmod($full, 0777);
|
|
unlink($full);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
closedir($dir);
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
* Recursively sets permissions. Files get 775 and directories 664.
|
|
*
|
|
* @param string $src The directory to set permissions for
|
|
*/
|
|
|
|
function setPermissions($src)
|
|
{
|
|
$dir = opendir($src);
|
|
while (false !== ($file = readdir($dir))) {
|
|
if (($file != '.') && ($file != '..')) {
|
|
$full = $src . '/' . $file;
|
|
if (is_dir($full)) {
|
|
setPermissions($full);
|
|
chmod($full, 0755);
|
|
} else {
|
|
if (is_link($full)) {
|
|
continue;
|
|
}
|
|
chmod($full, 0664);
|
|
}
|
|
}
|
|
}
|
|
closedir($dir);
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
* Prepares and generates the release packages.
|
|
*
|
|
* @param string $releaseVersion Version of tiki that is being released.
|
|
*/
|
|
|
|
function build_packages($releaseVersion)
|
|
{
|
|
global $options;
|
|
|
|
$workDir = $_SERVER['HOME'] . "/tikipack";
|
|
$fileName = 'tiki-' . $releaseVersion;
|
|
$relDir = $workDir . '/' . $releaseVersion; // where the tiki dir and tarballs go
|
|
$sourceDir = $relDir . '/' . $fileName; // the svn export
|
|
|
|
echo "Seting up $workDir directory\n";
|
|
if (! is_dir($workDir)) {
|
|
if (! mkdir($workDir)) {
|
|
error('Cant make ' . $workDir . "\n");
|
|
die();
|
|
}
|
|
}
|
|
|
|
// remove previous files if they exist.
|
|
if (is_dir($relDir)) {
|
|
echo "Removing previous files\n";
|
|
$shellout = rrmdir($relDir);
|
|
if ($shellout) {
|
|
die($shellout . "\n");
|
|
}
|
|
}
|
|
if (! mkdir($relDir)) {
|
|
error('Cant make ' . $relDir . "\n");
|
|
die();
|
|
}
|
|
|
|
// create an export in tikipack to work with
|
|
echo "Exporting working copy into $sourceDir\n";
|
|
$shellout = export(ROOT, $sourceDir);
|
|
if ($options['debug-packaging']) {
|
|
echo $shellout . "\n";
|
|
}
|
|
|
|
|
|
if (! is_file($sourceDir . '/vendor_bundled/composer.json')) {
|
|
echo 'composer.json not found. Aborting.' . "\n";
|
|
die();
|
|
}
|
|
|
|
if (is_file($workDir . '/composer.phar')) {
|
|
if (! unlink($workDir . '/composer.phar')) {
|
|
echo "Can't delete tikipack/composer.phar. Aborting." . "\n";
|
|
die();
|
|
}
|
|
}
|
|
|
|
echo "Downloading composer.phar" . "\n";
|
|
$checksum = file_get_contents('https://composer.github.io/installer.sig');
|
|
$composerInstaller = $workDir . '/composer-setup.php';
|
|
if (! file_put_contents($composerInstaller, file_get_contents('http://getcomposer.org/installer'))) {
|
|
echo "Can't create tikipack/composer-setup.php. Aborting." . "\n";
|
|
die();
|
|
}
|
|
|
|
if ($checksum !== hash_file('sha384', $composerInstaller)) {
|
|
echo "Invalid composer installer checksum. Aborting." . "\n";
|
|
unlink($composerInstaller);
|
|
die();
|
|
}
|
|
|
|
$shellout = shell_exec('php ' . escapeshellarg($composerInstaller) . ' --quiet --2 --install-dir=' . $workDir . ' 2>&1');
|
|
|
|
if ($shellout) {
|
|
echo "Composer installer failed. Aborting." . "\n";
|
|
unlink($composerInstaller);
|
|
die();
|
|
}
|
|
|
|
// tidy up
|
|
unlink($composerInstaller);
|
|
|
|
echo 'Installing dependencies through composer' . "\n";
|
|
$shellout = shell_exec('php ' . escapeshellarg($workDir . '/composer.phar') . ' install -d ' . escapeshellarg($sourceDir . '/vendor_bundled') . ' --prefer-dist --no-dev 2>&1');
|
|
if ($options['debug-packaging']) {
|
|
echo $shellout . "\n";
|
|
}
|
|
|
|
if ($options['debug-packaging']) {
|
|
echo $shellout . "\n";
|
|
}
|
|
|
|
if (
|
|
strpos($shellout, 'Fatal error:') !== false ||
|
|
strpos($shellout, 'Installation failed,') !== false ||
|
|
// symfony/dependency-injection comes in quite late in the list and is required - sometimes no error is reported even though it didn't work
|
|
strpos($shellout, 'symfony/dependency-injection') === false
|
|
) {
|
|
echo 'Vendor bundled packages installation Failed. Exiting' . "\n";
|
|
die();
|
|
}
|
|
|
|
echo "Removing development files\n";
|
|
$shellout = rrmdir($sourceDir . '/tests');
|
|
if ($shellout) {
|
|
die($shellout . "\n");
|
|
}
|
|
|
|
$shellout = rrmdir($sourceDir . '/db/convertscripts');
|
|
if ($shellout) {
|
|
die($shellout . "\n");
|
|
}
|
|
|
|
$shellout = rrmdir($sourceDir . '/doc/devtools');
|
|
if ($shellout) {
|
|
die($shellout . "\n");
|
|
}
|
|
|
|
$shellout = rrmdir($sourceDir . '/bin');
|
|
if ($shellout) {
|
|
die($shellout . "\n");
|
|
}
|
|
|
|
removeFiles($sourceDir, ['.gitignore']);
|
|
|
|
echo "Removing language file comments\n";
|
|
foreach (scandir($sourceDir . '/lang') as $strip) {
|
|
if (is_file($sourceDir . '/lang/' . $strip . '/language.php')) {
|
|
$shellout = shell_exec('php ' . escapeshellarg(__DIR__ . '/stripcomments.php') . ' ' . escapeshellarg($sourceDir . '/lang/' . $strip . '/language.php') . ' 2>&1');
|
|
}
|
|
if ($shellout) {
|
|
die($shellout . "\n");
|
|
}
|
|
}
|
|
|
|
echo "Setting file permissions\n";
|
|
setPermissions($sourceDir);
|
|
|
|
$relDir = escapeshellarg($relDir);
|
|
|
|
echo "Creating $fileName.tar.gz\n";
|
|
$shellout = shell_exec("cd $relDir; tar -pczf " . escapeshellarg($fileName . ".tar.gz") . ' ' . escapeshellarg($fileName) . " --exclude '*.DS_Store' 2>&1");
|
|
if ($options['debug-packaging']) {
|
|
echo $shellout . "\n";
|
|
}
|
|
|
|
echo "Creating $fileName.tar.bz2\n";
|
|
$shellout = shell_exec("cd $relDir; tar -pcjf " . escapeshellarg($fileName . ".tar.bz2") . ' ' . escapeshellarg($fileName) . " --exclude '*.DS_Store' 2>&1");
|
|
if ($options['debug-packaging']) {
|
|
echo $shellout . "\n";
|
|
}
|
|
|
|
echo "Creating $fileName.tar.xz\n";
|
|
$shellout = shell_exec("cd $relDir; tar -pcJf " . escapeshellarg($fileName . ".tar.xz") . ' ' . escapeshellarg($fileName) . " --exclude '*.DS_Store' 2>&1");
|
|
if ($options['debug-packaging']) {
|
|
echo $shellout . "\n";
|
|
}
|
|
|
|
echo "Creating $fileName.zip\n";
|
|
$shellout = shell_exec("cd $relDir; zip -ry " . escapeshellarg($fileName . ".zip") . ' ' . escapeshellarg($fileName) . ' -x "*.DS_Store" -9 2>&1');
|
|
if ($options['debug-packaging']) {
|
|
echo $shellout . "\n";
|
|
}
|
|
|
|
echo "Creating $fileName.7z\n";
|
|
$shellout = shell_exec("cd $relDir; 7za a " . escapeshellarg($fileName . ".7z") . ' ' . escapeshellarg($fileName) . ' -xr!*.DS_Store -mx=9 2>&1');
|
|
if (strpos($shellout, 'command not found')) {
|
|
error("7za not installed. Archive creation failed.\n");
|
|
}
|
|
if ($options['debug-packaging']) {
|
|
echo $shellout . "\n";
|
|
}
|
|
|
|
echo color("\nTo upload the 'tarballs', copy-paste and execute the following line (and change '\$SF_LOGIN' by your SF.net login):\n", 'yellow');
|
|
echo color(" cd $relDir; scp $fileName.* \$SF_LOGIN@frs.sourceforge.net:/home/pfs/project/t/ti/tikiwiki/\$RELEASEFOLDER\$\n", 'yellow');
|
|
|
|
info(">> Packages files have been built in ~/tikipack/$releaseVersion\n");
|
|
}
|
|
|
|
/**
|
|
* @param $dir
|
|
* @param $entries
|
|
* @param $regexp_pattern
|
|
* @return bool
|
|
*/
|
|
function get_files_list($dir, &$entries, $regexp_pattern)
|
|
{
|
|
$d = dir($dir);
|
|
while (false !== ($e = $d->read())) {
|
|
$entry = $dir . '/' . $e;
|
|
if (is_dir($entry)) {
|
|
// do not descend and no CVS/Subversion files
|
|
if ($e != '..' && $e != '.' && $e != 'CVS' && $e != '.git' && $e != '.gitignore' && $e != '.svn' && $entry != './temp/templates_c' && $entry != './vendor_bundled/vendor') {
|
|
if (! get_files_list($entry, $entries, $regexp_pattern)) {
|
|
return false;
|
|
}
|
|
}
|
|
} elseif (preg_match($regexp_pattern, $e) && realpath($entry) != __FILE__) {
|
|
$entries[] = $entry;
|
|
}
|
|
}
|
|
$d->close();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param $alreadyDone
|
|
* @param $toDo
|
|
* @param $message
|
|
*/
|
|
function display_progress_percentage($alreadyDone, $toDo, $message)
|
|
{
|
|
$onePercent = ceil($toDo / 100);
|
|
if ($alreadyDone % $onePercent === 0 || $alreadyDone == $toDo) {
|
|
$percentage = ($alreadyDone >= $toDo - $onePercent) ? 100 : min(100, $alreadyDone / $onePercent);
|
|
printf("\r$message", $percentage);
|
|
}
|
|
}
|
|
|
|
function zone_is_empty()
|
|
{
|
|
// dummy function to keep smarty happy
|
|
}
|
|
|
|
/**
|
|
* @param $error_msg
|
|
*/
|
|
function check_smarty_syntax(&$error_msg)
|
|
{
|
|
global $tikidomain, $prefs;
|
|
$tikidomain = '';
|
|
// Initialize $prefs with some variables needed by the tra() function and smarty autosave plugin
|
|
$prefs = [
|
|
'lang_use_db' => 'n',
|
|
'language' => 'en',
|
|
'site_language' => 'en',
|
|
'feature_ajax' => 'n'
|
|
];
|
|
|
|
// Load Tiki Smarty
|
|
$prefs['smarty_compilation'] = 'always';
|
|
$prefs['smarty_security'] = 'y';
|
|
$prefs['maxRecords'] = 25;
|
|
$prefs['log_tpl'] = 'y';
|
|
$prefs['feature_sefurl_filter'] = 'y';
|
|
$prefs['site_layout'] = 'basic';
|
|
require_once 'vendor_bundled/vendor/smarty/smarty/libs/Smarty.class.php';
|
|
require_once 'lib/init/smarty.php';
|
|
require_once 'lib/init/initlib.php';
|
|
// needed in Smarty_Tiki
|
|
define('TIKI_PATH', getcwd());
|
|
require_once 'lib/smarty_tiki/prefilter.tr.php';
|
|
require_once 'lib/smarty_tiki/prefilter.jq.php';
|
|
require_once 'lib/smarty_tiki/prefilter.log_tpl.php';
|
|
$smarty = new Smarty_Tiki();
|
|
set_error_handler('check_smarty_syntax_error_handler');
|
|
|
|
$templates_dir = TIKI_PATH . '/templates';
|
|
|
|
// tell TikiDb we don't need the database
|
|
define('DB_TIKI_SETUP', 0);
|
|
|
|
$errors_found = false;
|
|
$entries = [];
|
|
get_files_list($templates_dir, $entries, '/\.tpl$/');
|
|
|
|
$nbEntries = count($entries);
|
|
for ($i = 0; $i < $nbEntries; $i++) {
|
|
display_progress_percentage($i, $nbEntries, '%d%% of files passed the Smarty syntax check');
|
|
|
|
if (strpos($entries[$i], 'tiki-mods.tpl') === false) {
|
|
$template_file = substr($entries[$i], strlen($templates_dir) + 1);
|
|
|
|
try {
|
|
$_tpl = $smarty->createTemplate($template_file, null, null, null, false);
|
|
$_tpl->compileTemplateSource();
|
|
} catch (Exception $e) {
|
|
echo color("\nError: " . $e->getMessage(), 'red') . "\n";
|
|
$errors_found = true;
|
|
}
|
|
}
|
|
}
|
|
restore_error_handler();
|
|
@define('DB_TIKI_SETUP', 1); // suppress notice about redefining a constant TODO better
|
|
|
|
echo "\n";
|
|
|
|
if ($errors_found) {
|
|
die('Fix the Smarty errors and try again please.');
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @param $errno
|
|
* @param $errstr
|
|
* @param string $errfile
|
|
* @param int $errline
|
|
* @param array $errcontext
|
|
*/
|
|
function check_smarty_syntax_error_handler($errno, $errstr, $errfile = '', $errline = 0, $errcontext = [])
|
|
{
|
|
if (strpos($errstr, 'filemtime(): stat failed for') === false) { // smarty seems to emit these for every file
|
|
echo "\n" . color($errstr, 'red') . "\n";
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param $dir
|
|
* @param $error_msg
|
|
* @param $hide_php_warnings
|
|
* @return bool
|
|
*/
|
|
function check_php_syntax(&$dir, &$error_msg, $hide_php_warnings)
|
|
{
|
|
global $phpCommand;
|
|
$checkPhpCommand = $phpCommand . (ERROR_REPORTING_LEVEL > 0 ? ' -d error_reporting=' . (int)ERROR_REPORTING_LEVEL : '');
|
|
|
|
$entries = [];
|
|
get_files_list($dir, $entries, '/\.php$/');
|
|
|
|
$nbEntries = count($entries);
|
|
for ($i = 0; $i < $nbEntries; $i++) {
|
|
display_progress_percentage($i, $nbEntries, '%d%% of files passed the PHP syntax check');
|
|
$return_var = 0;
|
|
$output = null;
|
|
exec("$checkPhpCommand -l {$entries[$i]} 2>&1", $output, $return_var);
|
|
$fullOutput = implode("\n", $output);
|
|
|
|
if (strpos($fullOutput, 'Segmentation fault') !== false) {
|
|
// If php -l command segfaults, wait and retry (it seems to happen quite often on some environments for this command)
|
|
echo "\r[Retrying due to a Segfault...]";
|
|
sleep(1);
|
|
$i--;
|
|
} elseif ($return_var !== 0) {
|
|
// Handle PHP errors
|
|
$fullOutput = trim($fullOutput);
|
|
$error_msg = ($fullOutput == '') ? "\nPHP Parsing error in '{$entries[$i]}' ($return_var)\n" : "\n$fullOutput";
|
|
return false;
|
|
} elseif (! $hide_php_warnings && ($nb_lines = count($output)) > 1 && ! preg_match(THIRD_PARTY_LIBS_PATTERN, $entries[$i])) {
|
|
// Handle PHP warnings / notices (this just displays a yellow warning, it doesn't return false or an error_msg)
|
|
// and exclude some third party libs when displaying warnings from the PHP syntax check, because we can't fix it directly by the way.
|
|
echo "\r";
|
|
foreach ($output as $k => $line) {
|
|
// Remove empty lines and last line (because in case of a simple warning, the last line simply says 'No syntax errors...')
|
|
if (trim($line) == '' || $k == $nb_lines - 1) {
|
|
continue;
|
|
}
|
|
echo color("$line\n", 'yellow');
|
|
}
|
|
display_progress_percentage($i, $nbEntries, '%d%% of files passed the PHP syntax check');
|
|
}
|
|
unset($output, $return_var);
|
|
}
|
|
|
|
echo "\n";
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check the CI Pipeline for the result of the specific checks regarding the DB structure
|
|
*
|
|
* @param string $mainversion
|
|
* @param string $error_msg
|
|
* @return bool
|
|
*/
|
|
function check_database_files_and_upgrade($mainversion, &$error_msg)
|
|
{
|
|
$gitlabUrl = 'https://gitlab.com';
|
|
$gitlabRepo = $gitlabUrl . '/tikiwiki/tiki';
|
|
|
|
$branchToCheck = $mainversion . '.x';
|
|
|
|
$pipeline = gitlabGetLastFinishedPipelineByBranch($gitlabRepo, $branchToCheck);
|
|
|
|
if (empty($pipeline)) {
|
|
echo color('Could not retrieve pipeline information for branch ' . $branchToCheck . "\n", 'red');
|
|
echo color(
|
|
'You can check manually using ' . $gitlabRepo . '/pipelines/?scope=branches&format=json&ref=' . $branchToCheck . "\n",
|
|
'yellow'
|
|
);
|
|
$error_msg .= 'Information about the CI pipeline could not be retrieved' . "\n";
|
|
return false;
|
|
}
|
|
|
|
echo color(
|
|
'Checking jobs for branch ' . $branchToCheck . ', pipeline: ' . $gitlabUrl . $pipeline['url'] . "\n",
|
|
'yellow'
|
|
);
|
|
|
|
$jobs = gitlabGetJobStatusByPipeline($gitlabRepo, $pipeline['id']);
|
|
|
|
if (empty($pipeline)) {
|
|
echo color('Could not retrieve jobs information for pipeline ' . $pipeline['id'] . "\n", 'red');
|
|
echo color('You can check manually using ' . $gitlabUrl . $pipeline['url'] . "\n", 'yellow');
|
|
$error_msg .= 'Information about jobs in the CI pipeline could not be retrieved' . "\n";
|
|
return false;
|
|
}
|
|
|
|
$checkList = [
|
|
'schema-naming-convention' => '1.1.2.1. Check _tiki.sql suffixes',
|
|
'db-upgrade-' => '1.1.2.2. Structure',
|
|
'schema-sql-drop' => '1.1.2.3. Drop Table',
|
|
'sql-engine' => '1.1.2.4. MyISAM',
|
|
'sql-engine-conversion' => '1.1.2.5. InnoDB',
|
|
];
|
|
|
|
$allOk = true;
|
|
|
|
foreach ($checkList as $checkPrefix => $checkName) {
|
|
foreach ($jobs['tiki-check'] as $jobName => $job) {
|
|
if (strpos($jobName, $checkPrefix) === 0) {
|
|
echo color(
|
|
$checkName . ': ' . $job['status'] . ', job: ' . $jobName . ', url: ' . $gitlabUrl . $job['url'] . "\n",
|
|
$job['status'] == PIPELINE_STATUS_PASSED ? 'green' : 'red'
|
|
);
|
|
if ($job['status'] != PIPELINE_STATUS_PASSED) {
|
|
$error_msg .= 'Issues with job ' . $jobName . ' in the CI Pipeline' . "\n";
|
|
$allOk = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $allOk;
|
|
}
|
|
|
|
/**
|
|
* Lookup the ID of the last finished pipeline run for
|
|
* a given branch with status 'passed' or 'failed'.
|
|
*
|
|
* @param string $repoUrl Url of the repo in gitlab
|
|
* @param string $branch Branch to use for filtering
|
|
* @param integer $page Page associated with the cycle (recursive lookup)
|
|
* @return array|bool The result or false if error
|
|
*/
|
|
function gitlabGetLastFinishedPipelineByBranch($repoUrl, $branch, $page = 1)
|
|
{
|
|
$lastPipelineByBranch = $repoUrl . '/pipelines/?scope=finished&format=json&per_page=' . PIPELINES_FETCH_AMOUNT . '&page=' . $page . '&ref=' . $branch;
|
|
|
|
if (getenv('TEST_GITLAB_PIPELINE')) {
|
|
$lastPipelineByBranch = getenv('TEST_GITLAB_PIPELINE'); // to allow fake the answer while testing
|
|
}
|
|
|
|
$content = file_get_contents($lastPipelineByBranch);
|
|
$jsonContent = json_decode($content, true);
|
|
if (! empty($jsonContent) && empty($jsonContent['pipelines'])) {
|
|
return [];
|
|
}
|
|
|
|
if (empty($jsonContent)) {
|
|
return false;
|
|
}
|
|
|
|
$pipeline = array_filter(
|
|
$jsonContent['pipelines'],
|
|
function ($pipeline) use ($branch) {
|
|
return $pipeline['ref']['name'] === $branch &&
|
|
($pipeline['details']['status']['text'] === PIPELINE_STATUS_PASSED ||
|
|
$pipeline['details']['status']['text'] === PIPELINE_STATUS_FAILED);
|
|
}
|
|
);
|
|
|
|
if (empty($pipeline)) {
|
|
if ($page >= 50) {
|
|
return false;
|
|
}
|
|
return gitlabGetLastFinishedPipelineByBranch($repoUrl, $branch, ++$page);
|
|
}
|
|
|
|
$pipeline = reset($pipeline);
|
|
return ['id' => $pipeline['id'], 'url' => $pipeline['path']];
|
|
}
|
|
|
|
/**
|
|
* Returns the list of stages and the jobs for each of the stages
|
|
*
|
|
* @param string $repoUrl Url of the repo in gitlab
|
|
* @param string $pipelineId Pipeline ID from where to retrieve the list of jobs
|
|
* @return array|bool The result or false if error
|
|
*/
|
|
function gitlabGetJobStatusByPipeline($repoUrl, $pipelineId)
|
|
{
|
|
$pipelineJobsUrl = $repoUrl . '/pipelines/' . $pipelineId . '?format=json';
|
|
|
|
if (getenv('TEST_GITLAB_JOBS')) {
|
|
$pipelineJobsUrl = getenv('TEST_GITLAB_JOBS'); // to allow fake the answer while testing
|
|
}
|
|
|
|
$content = file_get_contents($pipelineJobsUrl);
|
|
$jsonContent = json_decode($content, true);
|
|
if (empty($jsonContent)) {
|
|
return false;
|
|
}
|
|
|
|
$stages = [];
|
|
foreach ($jsonContent['details']['stages'] as $stage) {
|
|
$jobs = [];
|
|
foreach ($stage['groups'] as $group) {
|
|
foreach ($group['jobs'] as $job) {
|
|
$jobs[$job['name']] = ['status' => $job['status']['text'], 'url' => $job['status']['details_path']];
|
|
}
|
|
}
|
|
$stages[$stage['name']] = $jobs;
|
|
}
|
|
|
|
return $stages;
|
|
}
|
|
|
|
/**
|
|
* @return array|bool
|
|
*/
|
|
function get_options()
|
|
{
|
|
if ($_SERVER['argc'] <= 1) {
|
|
return false;
|
|
}
|
|
|
|
$argv = [];
|
|
$options = [
|
|
'howto' => false,
|
|
'help' => false,
|
|
'http-proxy' => false,
|
|
'mirror-uri' => false,
|
|
'no-commit' => false,
|
|
'no-check-vcs' => false,
|
|
'no-check-db' => false,
|
|
'no-check-php' => false,
|
|
'no-check-php-warnings' => false,
|
|
'no-check-smarty' => false,
|
|
'no-first-update' => false,
|
|
'no-readme-update' => false,
|
|
'no-lang-update' => false,
|
|
'no-changelog-update' => false,
|
|
'no-copyright-update' => false,
|
|
'no-secdb' => false,
|
|
'no-packaging' => false,
|
|
'no-tagging' => false,
|
|
'force-yes' => false,
|
|
'debug-packaging' => false,
|
|
'only-secdb' => false,
|
|
'devmode' => false,
|
|
'use-git' => false,
|
|
'skip' => 0,
|
|
];
|
|
|
|
// Environment variables provide default values for parameter options. e.g. export TIKI_NO_SECDB=true
|
|
$prefix = "TIKI-";
|
|
foreach ($options as $option => $optValue) {
|
|
$envOption = $prefix . $option;
|
|
$envOption = str_replace("-", "_", $envOption);
|
|
if (isset($_ENV[$envOption])) {
|
|
$envValue = $_ENV[$envOption];
|
|
$options[$option] = $envValue;
|
|
}
|
|
}
|
|
|
|
foreach ($_SERVER['argv'] as $arg) {
|
|
if (substr($arg, 0, 2) == '--') {
|
|
if (($opt = substr($arg, 2)) != '' && isset($options[$opt])) {
|
|
$options[$opt] = true;
|
|
} elseif (substr($arg, 2, 11) == 'http-proxy=') {
|
|
if (($proxy = substr($arg, 13)) != '') {
|
|
$options[substr($arg, 2, 10)] = stream_context_create(
|
|
[
|
|
'http' => [
|
|
'proxy' => 'tcp://' . $proxy,
|
|
'request_fulluri' => true
|
|
]
|
|
]
|
|
);
|
|
} else {
|
|
$options[substr($arg, 2, 10)] = true;
|
|
}
|
|
} elseif (substr($arg, 2, 15) == 'mirror-uri=') {
|
|
if (($uri = substr($arg, 17)) != '') {
|
|
$options[substr($arg, 2, 14)] = $uri;
|
|
}
|
|
} elseif (strpos($arg, '=') !== false) {
|
|
$parts = explode('=', substr($arg, 2));
|
|
if (isset($options[$parts[0]])) {
|
|
$options[$parts[0]] = $parts[1];
|
|
}
|
|
} else {
|
|
error("Unknown option $arg. Try using --help option.\n");
|
|
}
|
|
} else {
|
|
$argv[] = $arg;
|
|
}
|
|
}
|
|
$_SERVER['argv'] = $argv;
|
|
unset($argv);
|
|
|
|
if ($options['http-proxy'] === true) {
|
|
error("The --http-proxy option need a value. Use it this way: --http-proxy=HOST_DOMAIN:PORT_NUMBER");
|
|
}
|
|
|
|
if ($_SERVER['argc'] == 2) {
|
|
$_SERVER['argv'][] = '';
|
|
}
|
|
|
|
return $options;
|
|
}
|
|
|
|
/**
|
|
* @param $msg
|
|
* @param bool $increment_step
|
|
* @param bool $commit_msg
|
|
* @return bool
|
|
*/
|
|
function important_step($msg, $increment_step = true, $commit_msg = false)
|
|
{
|
|
global $options;
|
|
static $step = 0;
|
|
|
|
// Auto-Skip the step if this is a commit step and if there is nothing to commit
|
|
if ($commit_msg && ! has_uncommited_changes('.')) {
|
|
return false;
|
|
}
|
|
|
|
// Increment step number if needed
|
|
if ($increment_step) {
|
|
$step++;
|
|
}
|
|
|
|
if ($step <= $options['skip']) {
|
|
print "Skipping step $step\n";
|
|
return false;
|
|
}
|
|
|
|
if ($commit_msg && $options['no-commit']) {
|
|
print "Skipping actual commit ('$commit_msg') because no-commit = true\n";
|
|
return false;
|
|
}
|
|
|
|
if ($options['force-yes']) {
|
|
important("\n$step) $msg...");
|
|
$do_step = true;
|
|
} else {
|
|
important("\n$step) $msg?");
|
|
|
|
$prompt = '[Y/n/q/?] ';
|
|
if (function_exists('readline')) {
|
|
// readline function requires php readline extension...
|
|
$c = readline($prompt);
|
|
} else {
|
|
echo $prompt;
|
|
$c = rtrim(fgets(STDIN), "\n");
|
|
}
|
|
|
|
switch (strtolower($c)) {
|
|
case 'y':
|
|
case '':
|
|
$do_step = true;
|
|
break;
|
|
case 'n':
|
|
info(">> Skipping step $step.");
|
|
$do_step = false;
|
|
break;
|
|
case 'q':
|
|
die;
|
|
break;
|
|
default:
|
|
if ($c != '?') {
|
|
info(color(">> Unknown answer '$c'.", 'red'));
|
|
}
|
|
info(">> You have to type 'y' (Yes), 'n' (No) or 'q' (Quit) and press Enter.");
|
|
return important_step($msg, false);
|
|
}
|
|
}
|
|
|
|
if ($commit_msg && $do_step && ($revision = commit($commit_msg))) {
|
|
info(">> Commited revision $revision.");
|
|
}
|
|
|
|
return $do_step;
|
|
}
|
|
|
|
/**
|
|
* @param $newVersion
|
|
* @return array|bool
|
|
*/
|
|
function update_changelog_file($newVersion)
|
|
{
|
|
$handle = false;
|
|
if (! is_readable(CHANGELOG) || ! is_writable(CHANGELOG) || ! ($handle = @fopen(CHANGELOG, "r"))) {
|
|
error('The changelog file "' . CHANGELOG . '" is not readable or writable.');
|
|
}
|
|
|
|
$majorVersion = substr($newVersion, 0, strpos($newVersion, '.'));
|
|
$parseLogs = $sameFinalVersion = $skipBuffer = false;
|
|
$lastReleaseMajorNumber = -1;
|
|
$lastReleaseNumber = '';
|
|
$minRevision = $currentParsedRevision = 0;
|
|
$lastReleaseLogs = [];
|
|
$versionMatches = [];
|
|
$newChangelog = '';
|
|
$newChangelogEnd = '';
|
|
|
|
if ($handle) {
|
|
while (! feof($handle)) {
|
|
$buffer = fgets($handle);
|
|
if (empty($buffer)) {
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/^Version (\d+)\.(\d+)/', $buffer, $versionMatches)) {
|
|
$versionString = $versionMatches[1] . '.' . $versionMatches[2];
|
|
if ((float)$lastReleaseNumber < (float)$versionString) {
|
|
$lastReleaseNumber = $versionString;
|
|
if ($lastReleaseNumber === $newVersion) {
|
|
// The changelog file already contains log for the same final version
|
|
$sameFinalVersion = true;
|
|
$skipBuffer = true;
|
|
}
|
|
$parseLogs = true;
|
|
$lastReleaseMajorNumber = $versionMatches[1];
|
|
}
|
|
}
|
|
if ($parseLogs) {
|
|
$matches = [];
|
|
if (preg_match('/^(\d+ | ) \|/', $buffer, $matches)) {
|
|
$skipBuffer = false;
|
|
if ($minRevision == 0) {
|
|
$minRevision = (int)$matches[1];
|
|
}
|
|
$currentParsedRevision = (int)$matches[1];
|
|
} elseif (! $skipBuffer && $currentParsedRevision > 0 && $buffer[0] != '-') {
|
|
if (isset($lastReleaseLogs[$currentParsedRevision])) {
|
|
$lastReleaseLogs[$currentParsedRevision] .= $buffer;
|
|
} else {
|
|
$lastReleaseLogs[$currentParsedRevision] = $buffer;
|
|
}
|
|
}
|
|
}
|
|
if ($lastReleaseMajorNumber != -1 && $lastReleaseMajorNumber < $majorVersion) {
|
|
$newChangelogEnd .= generate_changelog_version_header($lastReleaseNumber);
|
|
$newChangelogEnd .= "Changelog for Tiki version " . $lastReleaseNumber . ", or older, available at:\n";
|
|
$newChangelogEnd .= "https://sourceforge.net/p/tikiwiki/code/HEAD/tree/tags/" . $lastReleaseNumber . "/changelog.txt\n\n";
|
|
break; // truncate the rest of the file
|
|
}
|
|
if (! $skipBuffer) {
|
|
if ($lastReleaseMajorNumber == -1) {
|
|
$newChangelog .= $buffer;
|
|
} else {
|
|
$newChangelogEnd .= $buffer;
|
|
}
|
|
}
|
|
}
|
|
fclose($handle);
|
|
}
|
|
|
|
$newChangelog .= generate_changelog_version_header($newVersion);
|
|
|
|
$return = ['nbCommits' => 0, 'sameFinalVersion' => $sameFinalVersion];
|
|
$matches = [];
|
|
|
|
if ($minRevision === 0) { // failed to get the last rev from the old file contents
|
|
$minRevision = get_tag_revision($lastReleaseNumber);
|
|
}
|
|
if ($minRevision != 0) {
|
|
if (preg_match_all('/^([A-Za-z0-9]+).\|.*\n\n(.*)\-{46}/Ums', get_logs('.', $minRevision), $matches, PREG_SET_ORDER)) {
|
|
foreach ($matches as $logEntry) {
|
|
// Do not keep merges and release-related logs
|
|
$commitFlag = substr(trim($logEntry[2]), 0, 5);
|
|
if ($commitFlag == '[MRG]' || $commitFlag == '[REL]') {
|
|
continue;
|
|
}
|
|
|
|
// Add log entries only if they were not already listed (same revision number or same log message) in the previous version
|
|
if (! isset($lastReleaseLogs[$logEntry[1]]) && ! in_array("\n" . $logEntry[2], $lastReleaseLogs)) {
|
|
$newChangelog .= str_replace("\n\n", "\n", $logEntry[0]) . "\n";
|
|
|
|
$lastReleaseLogs[] = "\n" . $logEntry[2];
|
|
if ($return['nbCommits'] == 0) {
|
|
$return['firstRevision'] = $logEntry[1];
|
|
}
|
|
$return['lastRevision'] = $logEntry[1];
|
|
$return['nbCommits']++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return file_put_contents(CHANGELOG, $newChangelog . $newChangelogEnd) ? $return : false;
|
|
}
|
|
|
|
/**
|
|
* Generate the header for a given version, used in the changelog
|
|
* @param string $version
|
|
* @return string
|
|
*/
|
|
function generate_changelog_version_header($version)
|
|
{
|
|
$majorVersion = substr($version, 0, strpos($version, '.'));
|
|
$releaseNotesURL = '<http://doc.tiki.org/Tiki' . $majorVersion . '>';
|
|
|
|
$versionHeader = <<<EOS
|
|
Version $version
|
|
$releaseNotesURL
|
|
------------------
|
|
|
|
----------------------------------------------
|
|
|
|
EOS;
|
|
return $versionHeader;
|
|
}
|
|
|
|
/**
|
|
* @param $newVersion
|
|
* @return array|bool
|
|
*/
|
|
function update_copyright_file($newVersion)
|
|
{
|
|
if (! is_readable(COPYRIGHTS) || ! is_writable(COPYRIGHTS)) {
|
|
error('The copyright file "' . COPYRIGHTS . '" is not readable or writable.');
|
|
}
|
|
global $nbCommiters, $options;
|
|
$nbCommiters = 0;
|
|
$contributors = [];
|
|
|
|
$repositoryUri = empty($options['mirror-uri']) ? TIKIVCS : $options['mirror-uri']; //
|
|
if (strpos($repositoryUri, '/') === 0) {
|
|
$repositoryUri = 'file://' . $repositoryUri;
|
|
}
|
|
$repositoryInfo = get_revision($repositoryUri);
|
|
|
|
$oldContributors = parse_copyrights();
|
|
get_contributors_data($repositoryUri, $contributors, 1, $repositoryInfo);
|
|
ksort($contributors);
|
|
|
|
$totalContributors = count($contributors);
|
|
$now = gmdate('Y-m-d');
|
|
|
|
$copyrights = <<<EOS
|
|
Tiki Copyright
|
|
----------------
|
|
|
|
The following list attempts to gather the copyright holders for Tiki
|
|
as of version $newVersion.
|
|
|
|
Accounts listed below with commits have contributed source code to CVS or SVN.
|
|
Please note that even more people contributed on various other aspects (documentation,
|
|
bug reporting, testing, etc.)
|
|
|
|
This is how we implement the Tiki Social Contract.
|
|
http://tiki.org/Social+Contract
|
|
|
|
List of members of the Community
|
|
As of $now, the community has:
|
|
* $totalContributors members on SourceForge.net,
|
|
* $nbCommiters of those people who made at least one code commit
|
|
|
|
This list is automatically generated and alphabetically sorted
|
|
from subversion repository by the following script:
|
|
doc/devtools/release.php
|
|
|
|
Counting the commits is not as trivial as it may sound. If your number of commits
|
|
seems incorrect, it could be that the script is not detecting them all. This
|
|
has been reported especially for commits early on in the project. Nonetheless,
|
|
the list provides a general idea.
|
|
|
|
====================================================================
|
|
|
|
EOS;
|
|
|
|
$return = ['newCommits' => 0, 'newContributors' => 0];
|
|
foreach ($contributors as $author => $infos) {
|
|
if (isset($oldContributors[$author])) {
|
|
if ($oldContributors[$author] != $infos) {
|
|
// Quickfix to keep old dates which may be different due to which time zone is used
|
|
if (isset($oldContributors[$author]['First Commit'])) {
|
|
$infos['First Commit'] = $oldContributors[$author]['First Commit'];
|
|
if (
|
|
isset($oldContributors[$author]['Number of Commits']) && isset($oldContributors[$author]['Number of Commits'])
|
|
&& isset($infos['Number of Commits']) && $oldContributors[$author]['Number of Commits'] == $infos['Number of Commits']
|
|
) {
|
|
$infos['Last Commit'] = $oldContributors[$author]['Last Commit'];
|
|
}
|
|
}
|
|
if (isset($infos['Number of Commits'])) {
|
|
if (isset($oldContributors[$author]['Number of Commits'])) {
|
|
$return['newCommits'] += ($infos['Number of Commits'] - $oldContributors[$author]['Number of Commits']);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
$return['newContributors']++;
|
|
}
|
|
$copyrights .= "\nNickname: $author";
|
|
$orderedKeys = ['Name', 'First Commit', 'Last Commit', 'Number of Commits', 'SF Role'];
|
|
foreach ($orderedKeys as $k) {
|
|
if (empty($infos[$k]) || ($k == 'Name' && $infos[$k] == $author)) {
|
|
continue;
|
|
}
|
|
$copyrights .= "\n$k: " . $infos[$k];
|
|
}
|
|
$copyrights .= "\n";
|
|
}
|
|
|
|
return file_put_contents(COPYRIGHTS, $copyrights) ? $return : false;
|
|
}
|
|
|
|
/**
|
|
* @return array|bool
|
|
*/
|
|
function parse_copyrights()
|
|
{
|
|
if (! $copyrights = @file(COPYRIGHTS)) {
|
|
return false;
|
|
}
|
|
|
|
$return = [];
|
|
$curNickname = '';
|
|
|
|
foreach ($copyrights as $line) {
|
|
if (empty($line)) {
|
|
continue;
|
|
}
|
|
if (substr($line, 0, 10) == 'Nickname: ') {
|
|
$curNickname = rtrim(substr($line, 10));
|
|
$return[$curNickname] = [];
|
|
} elseif ($curNickname != '' && ($pos = strpos($line, ':')) !== false) {
|
|
$return[$curNickname][substr($line, 0, $pos)] = rtrim(substr($line, $pos + 2));
|
|
}
|
|
}
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* @param $path
|
|
* @param $contributors
|
|
* @param $minRevision
|
|
* @param $maxRevision
|
|
* @param int $step
|
|
* @return mixed
|
|
*/
|
|
function get_contributors_data($path, &$contributors, $minRevision, $maxRevision, $step = 20000)
|
|
{
|
|
global $nbCommiters;
|
|
if (empty($contributors)) {
|
|
get_contributors_sf_data($contributors);
|
|
info(">> Retrieved members list from Sourceforge.");
|
|
}
|
|
|
|
get_contributors($path, $contributors, $minRevision, $maxRevision, $step);
|
|
$nbCommiters = array_filter($contributors, function ($contributor) {
|
|
// Get count contributors with commits
|
|
return count($contributor) > 2;
|
|
});
|
|
return $contributors;
|
|
}
|
|
|
|
/**
|
|
* @param $contributors
|
|
*/
|
|
function get_contributors_sf_data(&$contributors)
|
|
{
|
|
global $options;
|
|
$matches = [];
|
|
|
|
if (! function_exists('iconv')) {
|
|
error("PHP 'iconv' function is not available on this system. Impossible to get SF.net data.");
|
|
}
|
|
|
|
$html = $options['http-proxy'] ? file_get_contents(SF_TW_MEMBERS_URL, 0, $options['http-proxy']) : file_get_contents(SF_TW_MEMBERS_URL);
|
|
|
|
if (! empty($html) && preg_match('/(<table.*<\/\s*table>)/sim', $html, $matches)) {
|
|
$usersInfo = [];
|
|
if (preg_match_all('/<tr[^>]*>' . str_repeat('\s*<td[^>]*>(.*)<\/td>\s*', 3) . '<\/\s*tr>/Usim', $matches[0], $usersInfo, PREG_SET_ORDER)) {
|
|
foreach ($usersInfo as $k => $userInfo) {
|
|
$userInfo = array_map('trim', array_map('strip_tags', $userInfo));
|
|
$user = strtolower($userInfo['2']);
|
|
if (empty($user)) {
|
|
continue;
|
|
}
|
|
$contributors[$user] = [
|
|
'Name' => html_entity_decode(iconv("ISO-8859-15", "UTF-8", $userInfo['1']), ENT_COMPAT, 'UTF-8'),
|
|
'SF Role' => $userInfo['3']
|
|
];
|
|
}
|
|
}
|
|
} else {
|
|
error('Impossible to get SF.net users information. If you need to use a web proxy, try the --http-proxy option.');
|
|
die;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param $releaseVersion
|
|
* @param $mainVersion
|
|
* @return bool
|
|
*/
|
|
function update_readme_file($releaseVersion, $mainVersion)
|
|
{
|
|
if (! is_readable(README) || ! is_writable(README)) {
|
|
error('The README file "' . README . '" is not readable or writable.');
|
|
die;
|
|
}
|
|
|
|
$year = gmdate('Y');
|
|
$copyrights_file = COPYRIGHTS_FILENAME;
|
|
$license_file = LICENSE_FILENAME;
|
|
|
|
$majorVersion = substr($mainVersion, 0, strpos($mainVersion, '.'));
|
|
$release_notes_url = 'http://doc.tiki.org/Tiki' . $majorVersion;
|
|
// Changed from Tiki 12 to point to http://doc.tiki.org/Tiki12 instead of http://tiki.org/ReleaseNotes30
|
|
|
|
$readme = <<<EOF
|
|
Tiki! The wiki with a lot of features!
|
|
Version $releaseVersion
|
|
|
|
|
|
DOCUMENTATION
|
|
|
|
* The documentation for $mainVersion version is ever evolving at http://doc.tiki.org.
|
|
You're encouraged to contribute.
|
|
|
|
* It is highly recommended that you refer to the online documentation:
|
|
* http://doc.tiki.org/Installation for a setup guide
|
|
|
|
* Notes about this release are accessible from $release_notes_url
|
|
* Tiki has an active IRC channel, #tikiwiki on irc.freenode.net
|
|
|
|
INSTALLATION
|
|
|
|
* There is a file INSTALL in this directory with notes on how to setup and
|
|
configure Tiki. Again, see http://doc.tiki.org/Installation for the latest install help.
|
|
|
|
UPGRADES
|
|
|
|
* Read the online instructions if you want to upgrade your Tiki from a previous release http://doc.tiki.org/Upgrade
|
|
|
|
COPYRIGHT
|
|
|
|
Copyright (c) 2002-$year, Luis Argerich, Garland Foster, Eduardo Polidor, et. al.
|
|
Tiki was started under the name tikiwiki by Luis Argerich, Garland Foster, Eduardo Polidor, et. al.
|
|
All Rights Reserved. See $copyrights_file for details and a complete list of authors.
|
|
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See $license_file for details.
|
|
|
|
... Have fun!
|
|
|
|
Note to Tiki developers: update this text through release.php.
|
|
EOF;
|
|
|
|
return (bool)file_put_contents(README, $readme);
|
|
}
|
|
|
|
function display_usage()
|
|
{
|
|
echo "Usage: php doc/devtools/release.php [ Options ] <version-number> [ <subrelease> ]
|
|
Examples:
|
|
php doc/devtools/release.php 2.0 preRC3
|
|
php doc/devtools/release.php 2.0 RC3
|
|
php doc/devtools/release.php 2.0
|
|
|
|
Options:
|
|
--howto : display the Tiki release HOWTO
|
|
--help : display this help
|
|
--http-proxy=HOST:PORT : use a http proxy to get copyright data on sourceforge
|
|
--mirror-uri=URI : use another repository URI to update the copyrights file (to avoid retrieving data from sourceforge, which is usually slow)
|
|
--no-commit : do not commit any changes back to SVN or GIT
|
|
--no-check-vcs : do not check if there are uncommitted changes on the checkout used for the release
|
|
--no-check-db : do not check database scripts and database upgrades
|
|
--no-check-php : do not check syntax of all PHP files
|
|
--no-check-php-warnings : do not display PHP warnings and notices during the PHP syntax check
|
|
--no-check-smarty : do not check syntax of all Smarty templates
|
|
--no-first-update : do not vcs update the checkout used for the release as the first step
|
|
--no-readme-update : do not update the '" . README_FILENAME . "' file
|
|
--no-lang-update : do not update lang/*/language.php files
|
|
--no-changelog-update : do not update the '" . CHANGELOG_FILENAME . "' file
|
|
--no-copyright-update : do not update the '" . COPYRIGHTS_FILENAME . "' file
|
|
--no-secdb : do not update SecDB footprints
|
|
--only-secdb : only generate a secdb database
|
|
--no-packaging : do not build packages files
|
|
--no-tagging : do not tag the release on the remote vcs repository
|
|
--force-yes : disable the interactive mode (same as replying 'y' to all steps)
|
|
--debug-packaging : display debug output while in packaging step
|
|
--devmode : equivalent to no-commit + no-check-vcs + no-first-update
|
|
--skip=0 : number of steps to skip when debugging (for use with -- devmode)
|
|
--use-git : use git instead of snv (git working copy automatically detected)
|
|
Notes:
|
|
Subreleases begining with 'pre' will not be tagged.
|
|
";
|
|
die;
|
|
}
|
|
|
|
function display_howto()
|
|
{
|
|
echo <<<EOS
|
|
--------------------------
|
|
HOWTO release Tiki
|
|
--------------------------
|
|
|
|
Please see: https://dev.tiki.org/How+to+release
|
|
|
|
EOS;
|
|
shell_exec('open ' . escapeshellarg('https://dev.tiki.org/How+to+release'));
|
|
exit;
|
|
}
|