coriolis/documentation/build.py

406 lines
15 KiB
Python
Executable File

#!/usr/bin/env python
import re
import os
import os.path
import sys
import time
import shutil
import socket
import subprocess
import logging
import optparse
def log ( message ):
print message
logging.info( message )
return
class ErrorMessage ( Exception ):
def __init__ ( self, code, *arguments ):
self._code = code
self._errors = [ 'Malformed call to ErrorMessage()'
, '%s' % str(arguments) ]
text = None
if len(arguments) == 1:
if isinstance(arguments[0],Exception): text = str(arguments[0]).split('\n')
else:
self._errors = arguments[0]
elif len(arguments) > 1:
text = list(arguments)
if text:
self._errors = []
while len(text[0]) == 0: del text[0]
lstrip = 0
if text[0].startswith('[ERROR]'): lstrip = 8
for line in text:
if line[0:lstrip ] == ' '*lstrip or \
line[0:lstrip-1] == '[ERROR]':
self._errors += [ line[lstrip:] ]
else:
self._errors += [ line.lstrip() ]
return
def __str__ ( self ):
if not isinstance(self._errors,list):
return "[ERROR] %s" % self._errors
formatted = "\n"
for i in range(len(self._errors)):
if i == 0: formatted += "[ERROR] %s" % self._errors[i]
else: formatted += " %s" % self._errors[i]
if i+1 < len(self._errors): formatted += "\n"
return formatted
def addMessage ( self, message ):
if not isinstance(self._errors,list):
self._errors = [ self._errors ]
if isinstance(message,list):
for line in message:
self._errors += [ line ]
else:
self._errors += [ message ]
return
def terminate ( self ):
print self
sys.exit(self._code)
def _getCode ( self ): return self._code
code = property(_getCode)
class Configuration ( object ):
def __init__ ( self ):
self.onSource = False
self.onLepka = False
self.onDocker = False
hostname = socket.gethostname()
if hostname.startswith('lepka'): self.onLepka = True
else: self.onDocker = True
scriptDir = os.path.abspath(os.getcwd())
if scriptDir.endswith( 'coriolis/documentation' ):
self.onLepka = False
self.onSource = True
if self.onDocker:
log( 'Using *Docker* configuration.' )
self.pelicanDir = '/data/git/coriolis.lip6.fr/pelican'
self.apacheDir = '/var/www/html'
if self.onLepka:
log( 'Using *Lepka* configuration.' )
self.pelicanDir = os.path.join( os.environ['HOME'], 'cms/coriolis.lip6.fr/pelican' )
self.apacheDir = '/dsk/l1/httpd/coriolis'
if self.onSource:
log( 'Using *Source* configuration.' )
self.pelicanDir = scriptDir
self.apacheDir = None
self.localDocDir = '/dsk/l1/jpc/coriolis-2.x/Linux.el7_64/Release.Shared/install/share/doc/coriolis2/en/html/doc'
self.remoteDocDir = '/data'
self.remoteGitDir = '/data/git'
self.remotePelicanDir = os.path.join( self.remoteGitDir, 'coriolis.lip6.fr/pelican' )
self.pluginsDir = os.path.join( self.remoteGitDir, 'pelican-plugins' )
self.logDir = os.path.join( self.pelicanDir, 'logs' )
self.target = 'coriolis-d'
return
class Command ( object ):
def __init__ ( self, command ):
self.command = command
self.outlog = None
self.errlog = None
return
def asString ( self ):
s = ''
for i in range(len(self.command)):
if i: s += ' '
if ' ' in self.command[i]: s += '"'+self.command[i]+'"'
else: s += self.command[i]
return s
def execute ( self ):
sys.stdout.flush()
sys.stderr.flush()
log( self.asString() )
child = subprocess.Popen( self.command, stdin =subprocess.PIPE
, stdout=subprocess.PIPE
, stderr=subprocess.PIPE )
self.outlog, self.errlog = child.communicate()
if len(self.outlog): logging.info ( self.outlog )
if len(self.errlog): logging.error( self.errlog )
status = child.returncode
status >>= 8
if status != 0:
ErrorMessage( status, "%s (status:%d)."%(error,status) ).terminate()
return status
@staticmethod
def rmtree ( path ):
command = 'rm -rf %s' % path
try:
log( command )
shutil.rmtree( path )
except shutil.Error, e:
logging.error( str(e) )
return 1
return 0
@staticmethod
def copytree ( src, dst ):
command = 'cp -r %s %s' % (src,dst)
try:
log( command )
shutil.copytree( src, dst )
except OSError, e:
logging.error( e )
return 1
except shutil.Error, errors:
for error in errors:
logging.error( 'cp %s %s: %s' % error )
return 1
return 0
class SshCommand ( object ):
def __init__ ( self, scriptlet ):
self.scriptlet = scriptlet
return
def execute ( self, target ):
command = [ 'ssh', '-x', target, self.scriptlet ]
Command( command ).execute()
return
class Document ( object ):
def __init__ ( self, conf, document ):
self.conf = conf
self.document = os.path.basename( document )
self.rdir = os.path.dirname ( document )
return
def toPdf ( self ):
pdfDir = '%s/content/pdfs' % self.conf.pelicanDir
stylesheet = '%s/etc/SoC-ReST.tex' % self.conf.pelicanDir
documentPdf = '%s.pdf' % self.document
documentRst = '%s.rst' % self.document
documentTex = '%s.tex' % self.document
documentRawTex = '%s-raw.tex' % self.document
documentTmps = [ documentTex
, documentRawTex
, '%s.log' % self.document
, '%s.out' % self.document
, '%s.aux' % self.document
, '%s.toc' % self.document
]
targetPdf = os.path.join( pdfDir, documentPdf )
cwd = os.getcwd()
os.chdir( os.path.join(self.conf.pelicanDir,self.rdir) )
os.environ[ 'TEXINPUTS' ] = '%s/etc/images//:./images//:' % self.conf.pelicanDir
Command( [ 'rst2latex', '--use-latex-toc'
, '--stylesheet=%s' % stylesheet
, documentRst
, documentRawTex
] ).execute()
pMulticol = re.compile( r' \\& \\\\multicolumn\{2\}\{l\|\}\{' )
fdi = open( documentRawTex, 'r' )
fdo = open( documentTex , 'w' )
for line in fdi.readlines():
fdo.write( pMulticol.sub(' \\& \\\\multicolumn{2}{p{0.6\\\\DUtablewidth}|}{',line) )
fdi.close()
fdo.close()
Command( [ 'pdflatex', '-halt-on-error', documentTex ] ).execute()
Command( [ 'pdflatex', '-halt-on-error', documentTex ] ).execute()
for file in documentTmps: os.unlink( file )
if not os.path.isdir( pdfDir ):
log( 'mkdir %s' % pdfDir )
os.mkdir( pdfDir )
if os.path.exists( targetPdf ): os.unlink( targetPdf )
log( 'mv %s %s' % (documentPdf,pdfDir) )
shutil.move( documentPdf, pdfDir )
os.chdir( cwd )
return
class Site ( object ):
def __init__ ( self, conf ):
self.conf = conf
return
def build ( self ):
print 'cd %s' % self.conf.pelicanDir
os.chdir( self.conf.pelicanDir )
status = Command( [ 'pelican', '-vD', '--ignore-cache', 'content' ] ).execute()
if status: return status
if self.conf.onLepka:
Command.rmtree ( self.conf.apacheDir )
Command.copytree( './output', self.conf.apacheDir )
Command.copytree( self.conf.localDocDir, os.path.join(self.conf.apacheDir,'doc') )
return
def gitPush ( self ):
print 'cd %s' % self.conf.pelicanDir
os.chdir( self.conf.pelicanDir )
lines = ''
cmd = Command( ['git', 'status', 'content', 'common', 'theme'] )
cmd.execute()
if cmd.outlog: lines = cmd.outlog.split('\n')
if lines[-2] != 'nothing to commit, working directory clean':
message = [ 'There are some uncommited changes in the site contents.' ] + lines
print ErrorMessage( 1, message )
#return False
Command( ['git', 'push'] ).execute()
return True
def gitCommitPdfs ( self ):
print 'cd %s' % self.conf.pelicanDir
os.chdir( self.conf.pelicanDir )
lines = ''
cmd = Command( ['git', 'status', 'content/pdfs'] )
cmd.execute()
if cmd.outlog: lines = cmd.outlog.split('\n')
if lines[-2] != 'nothing to commit, working directory clean':
message = 'Updated PDFs, %s.' % time.strftime( '%B %d, %Y (%H:%M)' )
Command( ['git', 'add' , 'content/pdfs'] ).execute()
Command( ['git', 'commit', '-m', message ] ).execute()
return True
def remoteBuild ( self ):
Command( [ 'ssh', '-x', '-o', 'StrictHostKeyChecking no'
, self.conf.target, "echo 'Force SSH key loading.'" ] ).execute()
Command( [ 'rsync', '--rsh=/usr/bin/ssh', '-avH'
, self.conf.localDocDir
, '%s:%s' % (self.conf.target,self.conf.remoteDocDir) ] ).execute()
remoteScript = \
' if [ ! -d %(remotePelicanDir)s ]; then' \
' cd %(remoteGitDir)s;' \
' git clone gitsoc@bop-t:coriolis.lip6.fr;' \
' sudo pelican-themes -s %(remotePelicanDir)s/themes/nest-coriolis;' \
' sudo chown -R pelican:pelican /var/www/html;' \
' fi;' \
' cd %(remotePelicanDir)s;' \
' git pull;' \
' if [ ! -d %(pluginsDir)s ]; then' \
' cd %(remoteGitDir)s;' \
' git clone https://github.com/getpelican/pelican-plugins;' \
' cd pelican-plugins;' \
' patch -p1 -i %(remotePelicanDir)s/patchs/0001-bootstrap-rst.no-math.patch;' \
' fi;' \
' cd %(remotePelicanDir)s;' \
' ./build.py -p;' \
% { 'pluginsDir' : self.conf.pluginsDir
, 'remoteGitDir' : self.conf.remoteGitDir
, 'remotePelicanDir' : self.conf.remotePelicanDir
}
SshCommand( remoteScript ).execute( self.conf.target )
return
if __name__ == '__main__':
usage = \
'\n' \
'\nThe "build.py" script rebuild the "coriolis.lip6.fr" website hosted' \
'\nat the Sorbonne Universite/LIP6. It can work in the following modes:' \
'\n' \
'\n1. Fully install the website on a freshly generated docker image.' \
'\n2. Update the "coriolis.lip6.fr" website.' \
'\n3. Generate a static website on my laptop ("lepka").' \
'\n4. Generate the static documentation shipped with coriolis (in git).' \
'\n' \
'\nNOTE: About the "<-C|--coriolis> remote rebuild option."' \
'\n Your ${HOME}/.ssh/config file must contain an alias for the' \
'\n "coriolis-d" host, as shown below:' \
'\n' \
'\n Host coriolis-d' \
'\n User pelican' \
'\n HostName localhost' \
'\n HostKeyAlias coriolis-d' \
'\n Port 2250' \
'\n CheckHostIP no' \
'\n ForwardAgent yes' \
'\n ForwardX11 no' \
'\n RemoteForward 2227 localhost:2227' \
'\n' \
'\n It logs as user "pelican" using the ssh port 2250 and creates' \
'\n a tunnel between port 2227 on the remote host (the web server)' \
'\n and port 2227 on the local host (your machine). You *must* have' \
'\n an ssh tunnel between your machine and "bop" on the local port' \
'\n 2227 for this to work. The "2227" tunnel between the web server' \
'\n and "bop" is used to perform "git pull".' \
'\n' \
'\n./build.py <options>'
parser = optparse.OptionParser( usage )
parser.add_option( '-p', '--pelican' , action='store_true', dest='doPelican' , help='Run pelican.' )
parser.add_option( '-P', '--pdfs' , action='store_true', dest='doPdfs' , help='Generate the PDFs.' )
parser.add_option( '-C', '--coriolis', action='store_true', dest='doCoriolis', help='Build/update the web site on the server (docker).' )
(options, args) = parser.parse_args()
conf = Configuration()
if not os.path.isdir( conf.logDir ):
os.mkdir( conf.logDir )
logging.basicConfig( filename='logs/build-%s.log' % time.strftime( '%Y%m%d-%H%M' )
, format='%(asctime)s:%(levelname)s| %(message)s'
, level=logging.INFO
)
documents = [ Document( conf, 'content/pages/users-guide/UsersGuide' )
, Document( conf, 'content/pages/python-tutorial/PythonTutorial' )
, Document( conf, 'content/pages/python-cpp/PythonCpp' )
, Document( conf, 'content/pages/stratus/Stratus' )
, Document( conf, 'content/pages/rds/RDS' )
]
coriolis = Site( conf )
if options.doPdfs:
for document in documents: document.toPdf()
if not conf.onSource:
coriolis.gitCommitPdfs()
if options.doPelican:
coriolis.build()
if options.doCoriolis:
coriolis.gitPush()
coriolis.remoteBuild()
sys.exit( 0 )