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.
 
 
 

800 lines
27 KiB

<?php
/*
* Commande d'execution …
*/
namespace autodoc\Helpers;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use autodoc\Application;
/**
* Exécuter l'application…
*/
class Generator
{
private $input;
private $output;
private $dirs = array();
private $files = array();
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)
{
$this->input = $input;
$this->output = $output;
$this->setApplicationDirectories();
}
/**
* 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);
}
$ok = $ok
&& $this->retrouverInfoPaquetXml()
&& $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) {
// 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';
if (file_exists( $this->files['autodoc.txt.bak'] )) {
unlink($this->files['autodoc.txt.bak']);
}
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");
}
}
$this->output->writeln("\n* Lecture des informations du fichier");
$anciens = $this->parseFile($this->files['autodoc.txt.bak'], false);
$presents = $this->parseFile($this->files['autodoc.txt']);
$output_base = $this->input->getOption('sorties');
if (!$output_base) $output_base = $this->dirs['work'] . '/output';
if ($anciens) {
$absents = array_diff_key($anciens, $presents);
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);
}
}
}
$nb_erreur = 0;
foreach ($presents as $prefixe => $present) {
$this->output->writeln("\n\n");
$this->output->writeln("<comment> Générer la documentation de $prefixe</comment>");
$this->output->writeln("<comment> ------------------------------------------------</comment>");
$this->output->writeln("\n");
$ok = $this->createDirectories($prefixe)
&& $this->getSvnSource($present['source'])
&& $this->retrouverInfoPaquetXml()
&& $this->prepareConfigXml()
&& $this->clearLogs()
;
if (!$ok) {
$nb_erreur++;
$this->output->writeln("<error> * La documentation de $prefixe est ignorée à cause d'une erreur.</error>");
continue;
}
$this->execute();
}
$this->output->writeln("\n\n\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>Toutes les documentations ("
. count($presents) . ") ont été générées.</comment>\n");
}
// 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);
$content = $twig->render('index.html', array(
'titre' => 'Documentation automatique des plugins SPIP',
'plugins' => $plugins,
));
file_put_contents($index, $content);
$this->output->writeln("Fin");
}
/**
* 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>");
// 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</error>]");
return false;
}
$last = array_pop($res);
$this->output->writeln("[<info>OK</info>] ($last)");
return true;
}
}
// erreur, donc pas un dossier svn ou pas le bon
// 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]</error>");
return false;
}
$last = array_pop($res);
if ($last) $last = "($last)";
$this->output->writeln("<info>[OK]</info> $last");
return true;
}
/**
* 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'];
}
$svn = '/usr/bin/svn';
$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 http://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')) {
$is_spip = true;
if (!file_exists($paquet = $source . '/ecrire/paquet.xml')) {
return false;
}
}
$paquet = simplexml_load_file($paquet);
if (!$paquet) return false;
$this->output->writeln("<comment> > Paquet.xml trouvé.</comment>");
$nom = (string)$paquet->nom;
$version = (string)$paquet['version'];
$prefixe = (string)$paquet['prefix'];
$this->infos_plugins[$prefixe] = array(
'nom' => $nom,
'prefixe' => $prefixe,
'slogan' => '',
'description' => '',
'annuaire' => '',
'documentation' => '',
'developpement' => '',
);
$documentation = $developpement = $lien_plugins_spip = '';
if (isset($paquet['documentation'])) {
$documentation = $paquet['documentation'];
$this->infos_plugins[$prefixe]['documentation'] = $documentation;
}
if (isset($paquet['developpement'])) {
$developpement = $paquet['developpement'];
$this->infos_plugins[$prefixe]['developpement'] = $developpement;
}
if (!$is_spip) {
$lien_plugins_spip = "http://plugins.spip.net/$prefixe.html";
$this->infos_plugins[$prefixe]['annuaire'] = $lien_plugins_spip;
// récupérer les infos dans les chaînes de langue.
$langue = $source . "/lang/paquet-$prefixe"."_fr.php";
if (file_exists($langue)) {
$GLOBALS['idx_lang'] = 'pour_autodoc';
defined('_ECRIRE_INC_VERSION') || define('_ECRIRE_INC_VERSION', 1);
@include $langue;
$infos = $GLOBALS['pour_autodoc'];
if (is_array($infos)) {
if (!$nom and isset($infos["$prefixe" . "_nom"])) {
$nom = $infos["$prefixe" . "_nom"];
$this->infos_plugins[$prefixe]['nom'] = $nom;
}
if (isset($infos["$prefixe" . "_slogan"])) {
$this->infos_plugins[$prefixe]['slogan'] = $infos["$prefixe" . "_slogan"];
}
if (isset($infos["$prefixe" . "_descriptif"])) {
$this->infos_plugins[$prefixe]['descriptif'] = $infos["$prefixe" . "_descriptif"];
}
}
}
}
if (!$this->getOption('titre') and $nom) {
$this->setOption('titre', $nom);
}
if (!$this->getOption('presentation')) {
if ($is_spip) {
$presentation = "Cette documentation est issue du code source PHP de SPIP $version.&#13;&#10;&#13;&#10;";
} else {
$presentation = "Cette documentation est issue du code source PHP du plugin « ".$nom." », version $version.&#13;&#10;&#13;&#10;";
}
if (!$is_spip OR $documentation OR $developpement) {
if (!$is_spip) {
$presentation .= "- [Description dans l'annuaire des plugins]($lien_plugins_spip)&#13;&#10;";
}
if ($documentation) {
$presentation .= "- [Documentation]($documentation)&#13;&#10;";
}
if ($developpement) {
$presentation .= "- [Outil de développement]($developpement)&#13;&#10;";
}
}
$this->setOption('presentation', $presentation);
}
return true;
}
/**
* Utilise le template de configuration phpdoc.xml en modifiant ses variables
* et l'enregistre pour qu'il soit utilisé par l'application
*
* @return bool true si réussi.
**/
private function prepareConfigXml() {
$this->output->write("* Préparer le fichier phpdoc.xml ");
$template = $this->dirs['helper'] . '/phpdoc_helper.xml';
$template = file_get_contents($template);
$substitutions = array(
'@DIR_CACHE@' => $this->dirs['cache'],
'@DIR_OUTPUT@' => $this->dirs['output'],
'@DIR_INPUT@' => $this->dirs['input'],
'@DIR_LOG@' => $this->dirs['log'],
'@DIR_ROOT@' => $this->dirs['root'],
'@OPT_TITRE@' => $this->getOption('titre', ''),
'@OPT_DESCRIPTION@' => $this->getOption('description', ''),
'@OPT_PRESENTATION@' => $this->getOption('presentation', ''),
);
$template = str_replace(array_keys($substitutions), array_values($substitutions), $template);
$destination = $this->dirs['work'] . '/phpdoc.xml';
@unlink($destination);
if (!@file_put_contents($this->files['phpdoc.xml'], $template)) {
$this->output->writeln("<error>[Echec]</error>");
return false;
}
$this->output->writeln("<info>[OK]</info>");
return true;
}
/**
* Exécute l'application phpDocumentor avec le phpdoc.xml qui a été créé
*
* @return bool true si réussi.
**/
private function execute() {
$conf = $this->files['phpdoc.xml'];
$command = "project:run --config=$conf --parseprivate";
if ($title = $this->getCommand('title')) {
$command .= $title;
}
$app = new Application();
$app->run_with_command($command);
}
}