Arsse/RoboFile.php

404 lines
18 KiB
PHP
Raw Normal View History

<?php
use Robo\Result;
2019-08-14 12:21:08 -04:00
const BASE = __DIR__.\DIRECTORY_SEPARATOR;
const BASE_TEST = BASE."tests".\DIRECTORY_SEPARATOR;
define("IS_WIN", defined("PHP_WINDOWS_VERSION_MAJOR"));
2019-10-17 13:00:56 -04:00
define("IS_MAC", php_uname("s") === "Darwin");
2021-03-01 19:02:15 -05:00
error_reporting(0);
2019-08-14 12:21:08 -04:00
function norm(string $path): string {
$out = realpath($path);
if (!$out) {
$out = str_replace(["/", "\\"], \DIRECTORY_SEPARATOR, $path);
}
return $out;
}
class RoboFile extends \Robo\Tasks {
2019-01-25 16:56:05 -05:00
/** Runs the typical test suite
2017-12-22 11:41:54 -05:00
*
* Arguments passed to the task are passed on to PHPUnit. Thus one may, for
2017-12-17 11:37:07 -05:00
* example, run the following command and get the expected results:
2017-12-22 11:41:54 -05:00
*
* ./robo test --testsuite TTRSS --exclude-group slow --testdox
*
* Please see the PHPUnit documentation for available options.
2020-03-01 15:16:50 -05:00
*/
public function test(array $args): Result {
return $this->runTests(escapeshellarg(\PHP_BINARY), "typical", $args);
}
2019-01-25 16:56:05 -05:00
/** Runs the full test suite
2017-12-22 11:41:54 -05:00
*
2018-08-15 09:59:07 -04:00
* This includes pedantic tests which may help to identify problems.
* See help for the "test" task for more details.
2020-03-01 15:16:50 -05:00
*/
2017-12-17 11:37:07 -05:00
public function testFull(array $args): Result {
return $this->runTests(escapeshellarg(\PHP_BINARY), "full", $args);
2017-12-17 11:37:07 -05:00
}
2017-12-22 11:41:54 -05:00
/**
* Runs a quick subset of the test suite
*
* See help for the "test" task for more details.
2020-03-01 15:16:50 -05:00
*/
public function testQuick(array $args): Result {
return $this->runTests(escapeshellarg(\PHP_BINARY), "quick", $args);
}
2017-12-22 11:41:54 -05:00
/** Produces a code coverage report
*
2017-12-17 11:37:07 -05:00
* By default this task produces an HTML-format coverage report in
2018-08-15 09:59:07 -04:00
* tests/coverage/. Additional reports may be produced by passing
2017-12-17 11:37:07 -05:00
* arguments to this task as one would to PHPUnit.
2017-12-22 11:41:54 -05:00
*
2020-02-01 23:43:46 -05:00
* Robo first tries to use pcov and will fall back first to xdebug then
2020-03-01 10:17:16 -05:00
* phpdbg. Neither pcov nor xdebug need to be enabled to be used; they
2020-02-01 23:43:46 -05:00
* only need to be present in the extension load path to be used.
2020-03-01 15:16:50 -05:00
*/
public function coverage(array $args): Result {
// run tests with code coverage reporting enabled
$exec = $this->findCoverageEngine();
2019-08-14 12:21:08 -04:00
return $this->runTests($exec, "coverage", array_merge(["--coverage-html", BASE_TEST."coverage"], $args));
}
/** Produces a code coverage report, with redundant tests
*
* Depending on the environment, some tests that normally provide
* coverage may be skipped, while working alternatives are normally
* suppressed for reasons of time. This coverage report will try to
* run all tests which may cover code.
2018-12-05 17:28:11 -05:00
*
* See also help for the "coverage" task for more details.
2020-03-01 15:16:50 -05:00
*/
public function coverageFull(array $args): Result {
// run tests with code coverage reporting enabled
$exec = $this->findCoverageEngine();
2019-08-14 12:21:08 -04:00
return $this->runTests($exec, "typical", array_merge(["--coverage-html", BASE_TEST."coverage"], $args));
2018-08-15 09:59:07 -04:00
}
/** Runs the coding standards fixer */
public function clean($opts = ['demo|d' => false]): Result {
2019-08-14 12:21:08 -04:00
$t = $this->taskExec(norm(BASE."vendor/bin/php-cs-fixer"));
2018-08-15 09:59:07 -04:00
$t->arg("fix");
if ($opts['demo']) {
$t->args("--dry-run", "--diff")->option("--diff-format", "udiff");
}
return $t->run();
}
protected function findCoverageEngine(): string {
2020-02-01 23:43:46 -05:00
$dir = rtrim(ini_get("extension_dir"), "/").\DIRECTORY_SEPARATOR;
$ext = IS_WIN ? "dll" : (IS_MAC ? "dylib" : "so");
$php = escapeshellarg(\PHP_BINARY);
$code = escapeshellarg(BASE."lib");
if (extension_loaded("pcov")) {
return "$php -d pcov.enabled=1 -d pcov.directory=$code";
} elseif (extension_loaded("xdebug")) {
2021-01-03 16:51:25 -05:00
return "$php -d xdebug.mode=coverage";
2020-02-01 23:43:46 -05:00
} elseif (file_exists($dir."pcov.$ext")) {
return "$php -d extension=pcov.$ext -d pcov.enabled=1 -d pcov.directory=$code";
2020-11-06 19:56:32 -05:00
} elseif (file_exists($dir."xdebug.$ext")) {
2021-01-03 16:51:25 -05:00
return "$php -d zend_extension=xdebug.$ext -d xdebug.mode=coverage";
} else {
2020-02-01 23:43:46 -05:00
if (IS_WIN) {
$dbg = dirname(\PHP_BINARY)."\\phpdbg.exe";
$dbg = file_exists($dbg) ? $dbg : "";
} else {
$dbg = trim(`which phpdbg 2>/dev/null`);
}
if ($dbg) {
return escapeshellarg($dbg)." -qrr";
} else {
return $php;
}
}
}
2019-03-25 09:53:06 -04:00
protected function blackhole(bool $all = false): string {
2019-08-14 12:21:08 -04:00
$hole = IS_WIN ? "nul" : "/dev/null";
2019-03-25 09:53:06 -04:00
return $all ? ">$hole 2>&1" : "2>$hole";
}
2020-03-01 15:16:50 -05:00
protected function runTests(string $executor, string $set, array $args): Result {
2018-08-15 09:59:07 -04:00
switch ($set) {
case "typical":
$set = ["--exclude-group", "optional"];
break;
case "quick":
$set = ["--exclude-group", "optional,slow"];
break;
case "coverage":
2018-12-10 12:39:09 -05:00
$set = ["--exclude-group", "optional,coverageOptional"];
break;
2018-08-15 09:59:07 -04:00
case "full":
$set = [];
break;
default:
throw new \Exception;
}
2019-08-14 12:21:08 -04:00
$execpath = norm(BASE."vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit");
$confpath = realpath(BASE_TEST."phpunit.dist.xml") ?: norm(BASE_TEST."phpunit.xml");
$this->taskServer(8000)->host("localhost")->dir(BASE_TEST."docroot")->rawArg("-n")->arg(BASE_TEST."server.php")->rawArg($this->blackhole())->background()->run();
2019-09-12 08:32:40 -04:00
return $this->taskExec($executor)->option("-d", "zend.assertions=1")->arg($execpath)->option("-c", $confpath)->args(array_merge($set, $args))->run();
2018-08-15 09:59:07 -04:00
}
2017-12-22 11:41:54 -05:00
/** Packages a given commit of the software into a release tarball
*
2017-12-17 11:37:07 -05:00
* The version to package may be any Git tree-ish identifier: a tag, a branch,
* or any commit hash. If none is provided on the command line, Robo will prompt
* for a commit to package; the default is "HEAD".
2017-12-22 11:41:54 -05:00
*
2017-12-17 11:37:07 -05:00
* Note that while it is possible to re-package old versions, the resultant tarball
2017-12-22 11:41:54 -05:00
* may not be equivalent due to subsequent changes in the exclude list, or because
2017-12-17 11:37:07 -05:00
* of new tooling.
2020-03-01 15:16:50 -05:00
*/
2017-12-17 11:37:07 -05:00
public function package(string $version = null): Result {
// establish which commit to package
$commit = $version ?? $this->askDefault("Commit to package:", "HEAD");
// start a collection
$t = $this->collectionBuilder();
// create a temporary directory
$dir = $t->tmpDir().\DIRECTORY_SEPARATOR;
// create a Git worktree for the selected commit in the temp location
2021-05-21 06:43:17 -04:00
$result = $this->taskExec("git worktree add ".escapeshellarg($dir)." ".escapeshellarg($commit))->dir(BASE)->run();
if ($result->getExitCode() > 0) {
return $result;
}
2021-05-21 06:43:17 -04:00
try {
// get useable version strings from Git
$version = trim(`git -C "$dir" describe --tags`);
$archVersion = preg_replace('/^([^-]+)-(\d+)-(\w+)$/', "$1.r$2.$3", $version);
// name the generic release tarball
$tarball = "arsse-$version.tar.gz";
// generate the Debian changelog; this also validates our original changelog
$debianChangelog = $this->changelogDebian($this->changelogParse(file_get_contents($dir."CHANGELOG"), $version), $version);
// save commit description to VERSION file for use by packaging
$t->addTask($this->taskWriteToFile($dir."VERSION")->text($version));
// save the Debian changelog
$t->addTask($this->taskWriteToFile($dir."dist/debian/changelog")->text($debianChangelog));
// patch the Arch PKGBUILD file with the correct version string
$t->addTask($this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^pkgver=.*$/m')->to("pkgver=$archVersion"));
// patch the Arch PKGBUILD file with the correct source file
$t->addTask($this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^source=\("arsse-[^"]+"\)$/m')->to('source=("'.basename($tarball).'")'));
// perform Composer installation in the temp location with dev dependencies
$t->addTask($this->taskComposerInstall()->arg("-q")->dir($dir));
// generate the manual
$t->addCode(function() {
return $this->manual(["-q"]);
});
// perform Composer installation in the temp location for final output
$t->addTask($this->taskComposerInstall()->dir($dir)->noDev()->optimizeAutoloader()->arg("--no-scripts")->arg("-q"));
// delete unwanted files
$t->addTask($this->taskFilesystemStack()->remove([
$dir.".git",
$dir.".gitignore",
$dir.".gitattributes",
$dir."composer.json",
$dir."composer.lock",
$dir.".php_cs.dist",
$dir."phpdoc.dist.xml",
$dir."build.xml",
$dir."RoboFile.php",
$dir."CONTRIBUTING.md",
$dir."docs",
$dir."tests",
$dir."vendor-bin",
$dir."vendor/bin",
$dir."robo",
$dir."robo.bat",
$dir."package.json",
$dir."yarn.lock",
$dir."postcss.config.js",
]));
// generate a sample configuration file
$t->addTask($this->taskExec(escapeshellarg(\PHP_BINARY)." arsse.php conf save-defaults config.defaults.php")->dir($dir));
// remove any existing archive
$t->addTask($this->taskFilesystemStack()->remove($tarball));
// package it all up
$t->addTask($this->taskPack($tarball)->addDir("arsse", $dir));
// execute the collection
$result = $t->run();
} finally {
// remove the Git worktree
$this->taskFilesystemStack()->remove($dir)->run();
$this->taskExec("git worktree prune")->dir(BASE)->run();
}
return $result;
}
/** Packages a release tarball into an Arch package */
public function packageArch(string $tarball): Result {
$dir = dirname($tarball);
// start a collection
$t = $this->collectionBuilder();
// extract the PKGBUILD from the tarball
$t->addCode(function() use ($tarball, $dir) {
2021-05-19 11:27:21 -04:00
// because Robo doesn't support extracting a single file we have to do it ourselves
(new \Archive_Tar($tarball))->extractList("arsse/dist/arch/PKGBUILD", $dir,"arsse/dist/arch/", false);
2021-05-19 11:34:54 -04:00
// perform a do-nothing filesystem operation since we need a Robo task result
return $this->taskFilesystemStack()->chmod("PKGBUILD", 0644)->dir($dir)->run();
})->completion($this->taskFilesystemStack()->remove("PKGBUILD")->dir($dir));
// build the package
$t->taskExec("makepkg -Ccf")->dir($dir);
return $t->run();
}
2019-01-25 16:56:05 -05:00
/** Generates static manual pages in the "manual" directory
2019-09-05 10:21:36 -04:00
*
2019-01-25 16:56:05 -05:00
* The resultant files are suitable for offline viewing and inclusion into release builds
*/
public function manual(array $args): Result {
2019-08-14 12:21:08 -04:00
$execpath = escapeshellarg(norm(BASE."vendor/bin/daux"));
2019-08-04 16:21:58 -04:00
$t = $this->collectionBuilder();
2019-08-14 12:21:08 -04:00
$t->taskExec($execpath)->arg("generate")->option("-d", BASE."manual")->args($args);
2019-10-11 12:51:32 -04:00
$t->taskDeleteDir(BASE."manual/daux_libraries");
2019-08-14 12:21:08 -04:00
$t->taskDeleteDir(BASE."manual/theme");
$t->taskDeleteDir(BASE."manual/themes/src");
2019-08-04 16:21:58 -04:00
return $t->run();
}
2019-01-25 16:56:05 -05:00
/** Serves a live view of the manual using the built-in Web server */
2019-01-25 16:56:05 -05:00
public function manualLive(array $args): Result {
2019-08-14 12:21:08 -04:00
$execpath = escapeshellarg(norm(BASE."vendor/bin/daux"));
2019-01-25 16:56:05 -05:00
return $this->taskExec($execpath)->arg("serve")->args($args)->run();
}
2019-08-02 11:15:48 -04:00
/** Rebuilds the entire manual theme
2019-09-05 10:21:36 -04:00
*
2019-08-02 11:15:48 -04:00
* This requires Node and Yarn to be installed, and only needs to be done when
* Daux's theme changes
*/
public function manualTheme(array $args): Result {
2021-04-09 19:18:42 -04:00
$postcss = escapeshellarg(norm(BASE."node_modules/.bin/postcss"));
2019-10-11 12:00:48 -04:00
$themesrc = norm(BASE."docs/theme/src/").\DIRECTORY_SEPARATOR;
2019-08-14 12:21:08 -04:00
$themeout = norm(BASE."docs/theme/arsse/").\DIRECTORY_SEPARATOR;
$dauxjs = norm(BASE."vendor-bin/daux/vendor/daux/daux.io/themes/daux/js/").\DIRECTORY_SEPARATOR;
// start a collection; this stops after the first failure
$t = $this->collectionBuilder();
// install dependencies via Yarn
$t->taskExec("yarn install");
// compile the stylesheet
2021-04-09 19:18:42 -04:00
$t->taskExec($postcss)->arg($themesrc."arsse.scss")->option("-o", $themeout."arsse.css");
2019-10-11 12:00:48 -04:00
// copy JavaScript files from the Daux theme
foreach (glob($dauxjs."daux*.js") as $file) {
$t->taskFilesystemStack()->copy($file, $themeout.basename($file), true);
}
// execute the collection
return $t->run();
}
2021-05-20 23:53:25 -04:00
protected function changelogParse(string $text, string $targetVersion): array {
$lines = preg_split('/\r?\n/', $text);
$version = "";
$section = "";
$out = [];
$entry = [];
$expected = ["version"];
for ($a = 0; $a < sizeof($lines);) {
$l = rtrim($lines[$a++]);
if (in_array("version", $expected) && preg_match('/^Version (\d+(?:\.\d+)*) \(([\d\?]{4}-[\d\?]{2}-[\d\?]{2})\)\s*$/', $l, $m)) {
$version = $m[1];
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $m[2])) {
// uncertain dates are allowed only for the top version, and only if it does not match the target version (otherwise we have forgotten to set the correct date before tagging)
if (!$out && $targetVersion !== $version) {
// use today's date; local time is fine
$date = date("Y-m-d");
} else {
throw new \Exception("CHANGELOG: Date at line $a is incomplete");
}
} else {
2021-05-20 23:53:25 -04:00
$date = $m[2];
}
2021-05-20 23:53:25 -04:00
if ($entry) {
$out[] = $entry;
}
$entry = ['version' => $version, 'date' => $date, 'features' => [], 'fixes' => [], 'changes' => []];
$expected = ["separator"];
} elseif (in_array("separator", $expected) && preg_match('/^=+/', $l)) {
$length = strlen($lines[$a - 2]);
if (strlen($l) !== $length) {
throw new \Exception("CHANGELOG: Separator at line $a is of incorrect length");
}
$expected = ["blank line"];
$section = "";
} elseif (in_array("blank line", $expected) && $l === "") {
$expected = [
'' => ["features section", "fixes section", "changes section"],
'features' => ["fixes section", "changes section", "version"],
'fixes' => ["changes section", "version"],
'changes' => ["version"],
][$section];
$expected[] = "end-of-file";
} elseif (in_array("features section", $expected) && $l === "New features:") {
$section = "features";
$expected = ["item"];
} elseif (in_array("fixes section", $expected) && $l === "Bug fixes:") {
$section = "fixes";
$expected = ["item"];
} elseif (in_array("changes section", $expected) && $l === "Changes:") {
$section = "changes";
$expected = ["item"];
} elseif (in_array("item", $expected) && preg_match('/^- (\w.*)$/', $l, $m)) {
$entry[$section][] = $m[1];
$expected = ["item", "continuation", "blank line"];
} elseif (in_array("continuation", $expected) && preg_match('/^ (\w.*)$/', $l, $m)) {
$last = sizeof($entry[$section]) - 1;
$entry[$section][$last] .= "\n".$m[1];
} else {
2021-05-20 23:53:25 -04:00
if (sizeof($expected) > 1) {
throw new \Exception("CHANGELOG: Expected one of [".implode(", ", $expected)."] at line $a");
} else {
throw new \Exception("CHANGELOG: Expected ".$expected[0]." at line $a");
}
}
2021-05-20 23:53:25 -04:00
}
if (!in_array("end-of-file", $expected)) {
if (sizeof($expected) > 1) {
2021-05-20 23:53:25 -04:00
throw new \Exception("CHANGELOG: Expected one of [".implode(", ", $expected)."] at end of file");
} else {
2021-05-20 23:53:25 -04:00
throw new \Exception("CHANGELOG: Expected ".$expected[0]." at end of file");
}
}
2021-05-20 23:53:25 -04:00
$out[] = $entry;
return $out;
}
2021-05-20 23:53:25 -04:00
protected function changelogDebian(array $log, string $targetVersion): string {
$latest = $log[0]['version'];
$baseVersion = preg_replace('/^(\d+(?:\.\d+)*).*/', "$1", $targetVersion);
if ($baseVersion !== $targetVersion && version_compare($latest, $baseVersion, ">")) {
// if the changelog contains an entry for a future version, change its version number to match the target version instead of using the future version
$log[0]['version'] = $targetVersion;
} else {
2021-05-20 23:53:25 -04:00
// otherwise synthesize a changelog entry for the changes since the last tag
array_unshift($log, ['version' => $targetVersion, 'date' => date("Y-m-d"), 'features' => [], 'fixes' => [], 'changes' => ["Unspecified changes"]]);
}
2021-05-20 23:53:25 -04:00
$out = "";
foreach ($log as $entry) {
$out .= "arsse (".$entry['version']."-1) unstable; urgency=low\n";
if ($entry['features']) {
$out .= "\n [ New features ]\n";
foreach ($entry['features'] as $item) {
$out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n";
}
}
2021-05-20 23:53:25 -04:00
if ($entry['fixes']) {
$out .= "\n [ Bug fixes ]\n";
foreach ($entry['fixes'] as $item) {
$out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n";
}
}
2021-05-20 23:53:25 -04:00
if ($entry['changes']) {
$out .= "\n [ Other changes ]\n";
foreach ($entry['changes'] as $item) {
$out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n";
}
}
2021-05-20 23:53:25 -04:00
$out .= "\n -- The Arsse team <no-contact@invalid> ".\DateTimeImmutable::createFromFormat("Y-m-d", $entry['date'], new \DateTimeZone("UTC"))->format("D, d M Y")." 00:00:00 +0000\n\n";
}
2021-05-20 23:53:25 -04:00
return $out;
}
}