You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1044 lines
36 KiB

<?php
/*
* Commande d'execution …
*/
namespace autodoc\Helpers;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\OutputInterface;
use autodoc\Application as Autodoc;
/**
* Exécuter l'application…
*/
class Generator
{
private $input;
private $output;
private $autoloader;
private $dirs = array();
private $files = array();
private $svn = array(); // informations sur le dépot SVN
private $options = array('dirs' => array()); // forcer des répertoires en dehors des options de ligne de commande
private $commands = array(); // forcer des commandes spécifiques à phpdocumentor
// les infos de plugins sont enregistrées dedans pour l'index.html de la commande generateFromFile()
private $infos_plugins = array();
/**
* Description de la documentation à générer
* @var string $description */
private $description = "";
/**
* Présentation de la documentation à générer
* @var string $presentation */
private $presentation = "";
/**
* Constructeur
*
* @param InputInterface $input
* @param OutputInterface $output
**/
public function __construct(InputInterface $input, OutputInterface $output, $autoloader = null)
{
$this->input = $input;
$this->output = $output;
$this->autoloader = $autoloader;
$this->setApplicationDirectories();
}
/**
* Obtient le chemin d'un executable sur le serveur.
*
* @example
* ```
* $cmd = $this->obtenir_commande_serveur('svn');
* if ($cmd)
* exec("$cmd ... ... ");
* }
* ```
* @param string $command
* Nom de la commande
* @return string
* Chemin de la commande
**/
public function obtenir_commande_serveur($command) {
static $commands = array();
if (array_key_exists($command, $commands)) {
return $commands[$command];
}
exec("command -v $command", $output, $err);
if (!$err and count($output) and $cmd = trim($output[0])) {
return $commands[$command] = $cmd;
}
$this->output->writeln("<error>Commande '$command' introuvable sur ce serveur…</error>");
return $commands[$command] = '';
}
/**
* Générer la documentation à partir du SVN de SPIP
*
* Retrouve le nom de la version utilisée et s'en sert de titre.
*
* @param string $chemin Chemin dans le svn
**/
public function generateFromSpip($chemin) {
$core = "svn://trac.rezo.net/spip/$chemin";
$prefixe = $this->input->getOption('prefixe');
$ok = $this->createDirectories($prefixe)
&& $this->getSvnSource($core);
if (!$ok) return false;
$titre = "";
if ($this->input->hasOption('titre')) {
$titre = $this->input->getOption('titre');
}
// retrouver la version de SPIP
if (!$titre and file_exists($inc_version = $this->dirs['input'] . '/ecrire/inc_version.php')) {
$inc_version = file_get_contents($inc_version);
if (preg_match('/spip_version_branche = "([^"]+)";/', $inc_version, $res)) {
$titre = "SPIP " . $res[1];
}
}
if ($titre) {
$this->setCommand('title', $titre);
$this->setOption('titre', $titre);
$this->setOption('description', "Documentation du code PHP de " . $titre);
}
return $this->generateFromDirectory($this->dirs['input'], true);
}
/**
* Générer la documentation à partir du SVN de la Zone de SPIP
*
* @param string $chemin Chemin dans le svn
**/
public function generateFromZone($chemin) {
$zone = "svn://zone.spip.org/spip-zone/$chemin";
return $this->generateFromSvn($zone);
}
/**
* Générer la documentation à partir d'une url SVN
*
* @param string $source URL SVN
**/
public function generateFromSvn($source) {
$prefixe = $this->input->getOption('prefixe');
$ok = $this->createDirectories($prefixe)
&& $this->getSvnSource($source)
&& $this->generateFromDirectory($this->dirs['input'], true);
return $ok;
}
/**
* Générer la documentation à partir d'un répertoire indiqué
*
* @param string $dir Chemin du répertoire source
* @param bool $is_ready
* Indique si les préparatifs (création des répertoires) sont déjà faits
**/
public function generateFromDirectory($dir, $is_ready = false) {
$prefixe = $this->input->getOption('prefixe');
$ok = true;
if (!$is_ready) {
# forcer le chemin de la source spécifique
$this->options['dirs']['input'] = $dir;
$ok = $this->createDirectories($prefixe);
if (!$ok) return false;
}
$this->retrouverInfoPaquetXml();
$ok = $this->prepareConfigXml()
&& $this->clearLogs()
;
if (!$ok) {
return false;
}
$this->execute();
return true;
}
/**
* Générer la documentation à partir d'un fichier listant les documentations à exécuter
*
* On duplique le fichier chez soi et on gère une rotation de ce fichier à chaque lancement
* afin de supprimer les documentations devenues inutiles (disparues du nouveau fichier).
*
* @param string $file Chemin du fichier ou URL SVN
**/
public function generateFromFile($file) {
$timerStart = microtime(true);
// Option boussole SPIP => topnav.
if ($this->getOption('avec_boussole_spip')) {
$this->setOption('topnav', '//boussole.spip.net/?page=spipnav.js&lang=fr');
}
// si un prefixe spécifique de plugin est indiqué,
// seul lui sera actualisé dans la liste des plugins.
// et on considère que le fichier autodoc.txt n'a pas bougé dans le cas.
$prefixe = $this->getOption('prefixe', null);
// définir les chemins et faire tourner le backup précédent
$this->files['autodoc.txt'] = $this->dirs['work'] . '/autodoc.txt';
$this->files['autodoc.txt.bak'] = $this->files['autodoc.txt'] . '.bak';
// à quel endroit sont générées les documentation des fichiers
$output_base = $this->input->getOption('sorties');
if (!$output_base) {
$output_base = $this->dirs['work'] . '/output';
}
$this->setOption('site', '../index.html');
if ($prefixe) {
// autodoc.txt est censé être déjà là
$this->output->writeln("* Lecture des informations du fichier (sans l'actualiser)");
$presents = $this->parseFile($this->files['autodoc.txt']);
// ne conserver que ce prefixe
$presents = array_intersect_key($presents, array($prefixe => ''));
} else {
// autodoc.txt doit être téléchargé ou mis à jour
$this->actualiser_liste_source($file);
$this->output->writeln("* Lecture des informations du fichier");
$anciens = $this->parseFile($this->files['autodoc.txt.bak'], false);
$presents = $this->parseFile($this->files['autodoc.txt']);
if ($anciens) {
$absents = array_diff_key($anciens, $presents);
$this->supprimer_vieux_plugins($absents);
}
}
$nb_erreur = 0;
$nb_update = 0;
foreach ($presents as $_prefixe => $description) {
$ok = $this->obtenir_fichiers_a_documenter($_prefixe, $description);
if ($ok) {
$nb_update++;
$this->execute();
} elseif ($ok === false) {
$nb_erreur++;
}
}
$this->output->writeln("\n");
if ($nb_erreur) {
$this->output->writeln("<comment>$nb_erreur documentation(s) non générée(s) sur " . count($presents) . ".</comment>");
} else {
$this->output->writeln("<comment>Documentations générées : " . $nb_update . " / " . count($presents) . ".</comment>");
}
// générer la page sommaire (si pas uniquement 1 seul prefixe actualisé
if (!$prefixe) {
$this->generer_sommaire_documentations($presents, $output_base);
}
$this->output->writeln(sprintf("%-'-81s", ""));
$this->output->write(sprintf('%-68.68s .. ', "Temps total"));
$this->output->writeln(sprintf('%8.3fs', microtime(true) - $timerStart));
$this->output->writeln("Fin");
}
/**
* Télécharge ou actualise les fichiers d'une documentation
*
* Si la mise à jour ne modifie pas les fichiers,
* la documentation n'est pas à actualiser (retourne null).
*
* @param string $prefixe
* Préfixe de cette documentation
* @param array $description
* Description. La clé 'source' a l'url SVN
* @return bool|null
* - True si OK et mise à jour à faire.
* - null : ok, mais mise à jour inutile
* - False : erreur
**/
public function obtenir_fichiers_a_documenter($prefixe, $description) {
$this->output->writeln("\n");
$titre = "Générer la documentation de $prefixe";
$this->output->writeln("<comment>$titre</comment>");
$this->output->writeln("<comment>" . str_repeat("-", strlen($titre)) . "</comment>");
if (!$this->createDirectories($prefixe)) {
$this->output->writeln("<error>* Documentation ignorée à cause d'une erreur.</error>");
return false;
}
$revision_actuelle = $this->recuperer_revision_svn();
if (!$this->getSvnSource($description['source'])) {
$this->output->writeln("<error>* Documentation ignorée à cause d'une erreur.</error>");
return false;
}
// ces options sont créées par retrouverInfoPaquetXml() SI elles n'existent pas.
// il faut les nettoyer à chaque passage !
$this->setOption('titre', null);
$this->setOption('description', null);
$this->setOption('presentation', null);
// Retrouver les informations de paquet.xml
if (!$this->retrouverInfoPaquetXml()) {
$this->output->writeln("<error>* Documentation ignorée : paquet.xml introuvable.</error>");
return false;
}
// pas besoin de mettre à jour, si l'update n'a pas augmenté la révision
$revision_nouvelle = $this->recuperer_revision_svn();
if ($revision_nouvelle <= $revision_actuelle) {
$this->output->writeln("* Documentation déjà à jour.");
// sauf si on le force explicitement
if (!$this->getOption('force')) {
return null;
}
}
$ok = $this->prepareConfigXml() && $this->clearLogs();
if (!$ok) {
$this->output->writeln("<error>* Documentation ignorée à cause d'une erreur.</error>");
return false;
}
return true;
}
/**
* Générer le sommaire des documentations réalisés à partir d'un fichier source d'url
*
* @param array $presents
* Liste des plugins du fichier
* @param string $outut_base
* Répertoire où on été enregistré les documentations
* @return
**/
public function generer_sommaire_documentations($presents, $output_base) {
// générer un sommaire de toutes ces documentations
$this->output->writeln("Création du sommaire des documentations");
// ajouter les dépendances du fichier html
if (!is_dir($data = $output_base . '/__data')) {
mkdir($data);
$template = $this->dirs['template'];
foreach (array('bootstrap', 'images', 'css', 'js') as $dir) {
exec("cp -r '$template/$dir' '$data/$dir'");
}
copy("$template/favicon.png", "$output_base/favicon.png");
}
if (file_exists($index = "$output_base/index.html")) {
unlink($index);
}
// obtenir les description des plugins
$plugins = array();
foreach ($presents as $prefixe => $present) {
if (isset($this->infos_plugins[$prefixe])) {
$plugins[$prefixe] = $this->infos_plugins[$prefixe];
} else {
// ce n'était pas un plugin ou erreur
$plugins[$prefixe] = array(
'prefixe' => $prefixe,
'nom' => $prefixe,
'slogan' => '',
'description' => '',
'annuaire' => '',
'documentation' => '',
'developpement' => '',
);
}
}
usort($plugins, function($a, $b) {
return ($a['nom'] < $b['nom']) ? -1 : 1;
});
// charger Twig, générer la page et l'enregitrer
$loader = new \Twig_Loader_Filesystem( $this->dirs['helper'] . '/Template' );
$twig = new \Twig_Environment($loader);
$topnav = $this->getOption('topnav', '');
$content = $twig->render('index.html', array(
'titre' => 'Documentation automatique des plugins SPIP',
'plugins' => $plugins,
'topnav' => $topnav,
));
file_put_contents($index, $content);
}
/**
* Télécharge la source de liste des documentations à faire dans autodoc.txt
* et fait une rotation avec l'ancienne liste
*
* @param strig $file
* URL du fichier source (svn) de la liste des docémentations à générer
* @return bool True si ok.
**/
public function actualiser_liste_source($file) {
// supprimer l'ancienne rotation
if (file_exists( $this->files['autodoc.txt.bak'] )) {
unlink($this->files['autodoc.txt.bak']);
}
// rotation
if (file_exists( $this->files['autodoc.txt'] )) {
copy($this->files['autodoc.txt'], $this->files['autodoc.txt.bak']);
unlink($this->files['autodoc.txt']);
}
// copier le fichier de liste chez soi
if (0 === strpos($file, 'svn://')) {
$this->output->write("* Obtenir <info>$file</info> ");
// si c'est en svn, on l'exporte simplement.
if ($res = $this->makeSvnCommand("export $file autodoc.txt", true, $this->dirs['work'])) {
$this->output->writeln("[<info>OK</info>]");
} else {
$this->output->writeln("[<info>Error</info>]");
throw new \Exception("Impossible de récupérer le fichier SVN : $file");
}
} else {
if (file_exists($file)) {
copy($file, $this->files['autodoc.txt']);
} else {
throw new \Exception("Impossible de trouver le fichier : $file");
}
}
return true;
}
/**
* Effacte une ou plusieurs documentations qui ne sont plus listés
* dans la description du fichier autodoc.txt
*
* @param array $absents
* Liste [ prefixe => url ]
* @return bool
* True si au moins une documentation, et ses caches, ont été effacés.
**/
public function supprimer_vieux_plugins($absents) {
if ($absents) {
$this->output->writeln("<comment>Certaines documentations ne sont plus à générer.</comment>");
foreach ($absents as $prefixe=>$absent) {
$this->output->writeln("<comment>- Effacement de $prefixe.</comment>");
$this->deleteDirectoryContent($output_base . '/$prefixe', true);
$this->deleteDirectoryContent($this->dirs['work'] . 'log/$prefixe', true);
$this->deleteDirectoryContent($this->dirs['work'] . 'input/$prefixe', true);
$this->deleteDirectoryContent($this->dirs['work'] . 'cache/$prefixe', true);
}
return true;
}
return false;
}
/**
* Analyse un fichier listant les documentations à générer.
*
* @param string $file Chemin du fichier
* @param bool $write_errors Ecrire les erreurs dans la console ?
* @return array|bool
* - false si echec,
* - couples (prefixe => array) sinon. Le tableau de description a les clés suivantes :
* - ligne : numéro de ligne
* - type : 'svn'
* - source : url du svn
**/
private function parseFile($file, $write_errors = true) {
if (!file_exists($file)) return false;
if (!$lines = file($file)) return false;
$liste = array();
foreach ($lines as $lineno => $line) {
if (!$line) continue;
$line = trim($line);
if (!$line OR $line[0] == '#') continue;
$couples = explode(';', $line);
$lineno++;
if (count($couples) != 2) {
if (count($couples) == 1) {
$this->output->writeln("<error>Ligne $lineno omise. Le préfixe ne semble pas défini. Contenu : $line</error>");
} else {
$this->output->writeln("<error>Ligne $lineno omise. Trop de paramètres indiqués. Contenu : $line</error>");
}
continue;
}
list ($url, $prefixe) = $couples;
if (!$url) {
$this->output->writeln("<error>Ligne $lineno omise. L'URL ne semble pas définie. Contenu : $line</error>");
continue;
}
if (!$prefixe) {
$this->output->writeln("<error>Ligne $lineno omise. Le préfixe ne semble pas défini. Contenu : $line</error>");
continue;
}
if (isset($liste[$prefixe])) {
$this->output->writeln("<error>Ligne $lineno omise. Le prefixe $prefixe est déjà déclaré ligne : " . $liste[$prefixe]['ligne'] . "</error>");
continue;
}
// pas d'erreur !
$liste[$prefixe] = array(
'type' => 'svn',
'ligne' => $lineno,
'source' => $url
);
}
return $liste;
}
/**
* Effacer le contenu du répertoire log.
*
* On ne gardera du coup que les logs de la prochaine exécution.
*
* @return bool true si réussi
**/
private function clearLogs() {
return $this->deleteDirectoryContent($this->dirs['log']);
}
/**
* Définit les répertoires utiles à l'application
**/
private function setApplicationDirectories() {
# ce répertoire
$this->dirs['helper'] = realpath(__DIR__);
# executable php de l'application autodoc (extension de l'application phpdocumentor)
$this->dirs['bin'] = realpath( $this->dirs['helper'] . '/../../../bin');
# répertoire racine (celui depuis lequel on execute ce script).
exec('pwd', $output);
$this->dirs['root'] = $output[0];
# répertoire de travail (celui où on écrira tout).
$this->dirs['work'] = $this->dirs['root'] . '/work';
# répertoire du template zora
$this->dirs['template'] = realpath( $this->dirs['helper'] . '/../../../templates/zora');
# fichier de config xml pour phpdocumentor
$this->files['phpdoc.xml'] = $this->dirs['work'] . '/phpdoc.xml';
}
/**
* Définit et crée les répertoires nécessaires au fonctionnement de ce programme
*
* @param string $prefixe Préfixe utilisé pour cette génération
* @return bool true si réussi
**/
private function createDirectories($prefixe) {
$_work = $this->dirs['work'];
$this->output->writeln("* Vérifier/créer les répertoires de travail dans <info>$_work</info>");
# si un répertoire pour toutes les sorties est indiqué, forcer son utilisation.
if ($this->input->hasOption('sorties') and $dir_output = $this->input->getOption('sorties')) {
$this->setOption('dirs/output', $dir_output . "/$prefixe");
}
# si un répertoire de sortie est indiqué, forcer son utilisation.
if ($this->input->hasOption('sortie') and $dir_output = $this->input->getOption('sortie')) {
$this->setOption('dirs/output', $dir_output);
}
foreach (array('output', 'input', 'log', 'cache') as $dir) {
// valeur par défaut, en fonction du préfixe
$this->dirs[$dir] = "$_work/$dir/$prefixe";
// valeur forcée dans certains cas
if ($path = $this->getOption("dirs/$dir")) {
$this->dirs[$dir] = $path;
}
}
foreach (array('output', 'input', 'log', 'cache') as $dir) {
if (!$this->createDirectory( $this->dirs[$dir] )) {
return false;
}
}
return true;
}
/**
* Créer un des répertoires d'utilisation
*
* Vérifie également que le répertoire est utilisable en écriture.
*
* @param string $chemin Répertoire à créer
* @return bool true si réussi
**/
private function createDirectory($dir) {
if (!is_dir($dir)) {
$this->output->writeln(" - Création du répertoire <info>$dir</info>");
if (!@mkdir($dir, 0755, true)) {
$this->output->writeln("<error>Impossible de créer le répertoire : $dir</error>");
$error = error_get_last();
$this->output->writeln($error['message']);
return false;
}
}
if (!is_writable($dir)) {
$this->output->writeln("<error>Le répertoire $dir n'est pas accessible en écriture</error>");
return false;
}
return true;
}
/**
* Télécharge ou met à jour la source SVN indiquée
*
* @param string $source URL de la source SVN
* @return bool true si réussi
**/
private function getSvnSource($source) {
$this->output->writeln("* Obtenir <info>$source</info>");
$this->svn = array();
// si c'est en svn et le bon, on fait svn up simplement.
if ($res = $this->makeSvnCommand('info --xml')) {
// nous avons un svn… verifions que c'est le bon !
$xml = simplexml_load_string(implode("", $res));
$source_actuelle = (string)$xml->entry->url;
if ($source_actuelle == $source) {
#$this->output->write("<comment> - Update </comment>");
if (!$res = $this->makeSvnCommand("update")) {
$this->output->writeln("<error>[Echec Update]</error>");
return false;
}
$last = array_pop($res);
# $this->output->writeln("[<info>OK</info>] ($last)");
}
}
// erreur, donc pas un dossier svn ou pas le bon
else {
// on nettoie le contenu pour svn co
$this->deleteDirectoryContent($this->dirs['input']);
#$this->output->write("<comment> - Checkout </comment>");
if (!$res = $this->makeSvnCommand("checkout $source .")) {
$this->output->writeln("<error>[Echec Checkout]</error>");
return false;
}
$last = array_pop($res);
if ($last) $last = "($last)";
# $this->output->writeln("<info>[OK]</info> $last");
}
// On récupère le numéro de dernière révision, ça peut servir
if ($revision = $this->recuperer_revision_svn()) {
$this->svn['revision'] = $revision;
}
return true;
}
/**
* Retrouver le numéro de la dernière révision du dossier de travail
*
* @return string Numéro de révision (si connu)
**/
function recuperer_revision_svn() {
$revision = '';
// Ici, on a les bons fichiers SVN à jour
// on récupère le numéro de dernière révision
if ($res = $this->makeSvnCommand('info --xml')) {
// nous avons un svn… verifions que c'est le bon !
$xml = simplexml_load_string(implode("", $res));
$revision = (string)$xml->entry->commit['revision'];
}
return $revision;
}
/**
* Exécuter la commande svn indiquée
*
* @param string $cmd Commande SVN
* @param bool $no_error true : Envoie les erreurs dans /dev/null
* @return mixed|bool false en cas d'erreur
**/
private function makeSvnCommand($cmd, $no_error = true, $dir = null) {
if (is_null($dir)) {
$dir = $this->dirs['input'];
}
if (!$svn = $this->obtenir_commande_serveur('svn')) {
if (!$svn = $this->obtenir_commande_serveur('svnlite')) {
return false;
} else {
$this->output->writeln("<info>Commande 'svnlite' trouvée</info>");
}
}
$no_error = $no_error ? '2> /dev/null' : '';
exec("cd $dir && $svn $cmd $no_error", $res, $error);
if ($error) return false;
return $res;
}
/**
* Suppression du contenu d'un repertoire.
*
* @link https://www.php.net/manual/en/function.rmdir.php#92050
*
* @param string $dir Chemin du repertoire
* @param string $delete_me Supprimer aussi le répertoire ?
* @return bool Suppression reussie.
*/
function deleteDirectoryContent($dir, $delete_me = false) {
if (!file_exists($dir)) return true;
if (!is_dir($dir) || is_link($dir)) return @unlink($dir);
foreach (scandir($dir) as $item) {
if ($item == '.' || $item == '..') continue;
if (!$this->deleteDirectoryContent($dir . "/" . $item, true)) {
@chmod($dir . "/" . $item, 0777);
if (!$this->deleteDirectoryContent($dir . "/" . $item, true)) return false;
};
}
if ($delete_me) {
return @rmdir($dir);
}
return true;
}
/**
* Obtient une option, soit de la ligne de commande, soit forcée par le générateur
*
* @param string $name Nom de l'option
* @return mixed
**/
private function getOption($name, $default = null) {
if (strpos($name, '/') === false) {
if ($this->input->hasOption($name) and $valeur = $this->input->getOption($name)) {
return $valeur;
}
if (isset($this->options[$name]) and $this->options[$name]) {
return $this->options[$name];
}
return $default;
}
$name = explode('/', $name);
$pointeur = &$this->options;
while (true) {
$n = array_shift($name);
if (!isset($pointeur[$n])) {
return $default;
}
if (!count($name)) {
return $pointeur[$n];
}
if (!is_array($pointeur[$n])) {
return $default;
}
$pointeur = &$pointeur[$n];
}
}
/**
* Enregistre une option (forcée par le générateur)
*
* Permet d'enregistrer avec des chemins par /
*
* @example
* $this->setOption('dirs/output', $dir)
*
* @param string $name Nom de l'option
* @param string $value Valeur de l'option
**/
private function setOption($name, $value) {
$name = explode('/', $name);
$n = array_shift($name);
$pointeur = &$this->options;
while ($name) {
if (!is_array($pointeur[$n])) {
$pointeur[$n] = array();
}
$pointeur = &$pointeur[$n];
$n = array_shift($name);
}
$pointeur[$n] = $value;
}
/**
* Récupère une option de commande pour phpDocumentor
*
* @param string $command Nom de la commande
* @return mixed
**/
private function getCommand($command) {
if (isset($this->commands[$command])) {
$val = $this->commands[$command];
return " --$command=\"$val\"";
}
return null;
}
/**
* Enregistre une option de commande pour phpDocumentor
*
* @param string $command Nom de la commande
* @param string $value Valeur de la commande
**/
private function setCommand($command, $value) {
$this->commands[$command] = $value;
}
/**
* Retrouver des infos du paquet.xml si on le trouve
*
* Définir avec le titre, et une présentation (si ce n'est déjà fait).
*
* @return bool true si on a trouvé un paquet.xml
**/
private function retrouverInfoPaquetXml() {
$source = $this->dirs['input'];
$is_spip = false;
if (!file_exists($paquet = $source . '/paquet.xml')) {