Advertisement
Guest User

texliveonfly.py

a guest
Oct 2nd, 2011
585
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 17.30 KB | None | 0 0
  1. #!/usr/bin/env python
  2.  
  3. # default options; feel free to change!
  4. defaultCompiler = "pdflatex"
  5. defaultArguments = "-synctex=1 -interaction=nonstopmode"
  6. defaultSpeechSetting = "never"
  7.  
  8. #
  9. # texliveonfly.py (formerly lualatexonfly.py) - "Downloading on the fly"
  10. #     (similar to miktex) for texlive.
  11. #
  12. # Given a .tex file, runs lualatex (by default) repeatedly, using error messages
  13. #     to install missing packages.
  14. #
  15. #
  16. # Version 1.2 ; October 4, 2011
  17. #
  18. # Written on Ubuntu 10.04 with TexLive 2011
  19. # Python 2.6+ or 3
  20. # Should work on Linux and OS X
  21. #
  22. # Copyright (C) 2011 Saitulaa Naranong
  23. #
  24. # This program is free software; you can redistribute it and/or modify
  25. # it under the terms of the GNU General Public License as published by
  26. # the Free Software Foundation; either version 3 of the License, or
  27. # (at your option) any later version.
  28. #
  29. # This program is distributed in the hope that it will be useful,
  30. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  31. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  32. # GNU General Public License for more details.
  33. #
  34. # You should have received a copy of the GNU General Public License
  35. # along with this program; if not, see <http://www.gnu.org/copyleft/gpl.html>.
  36.  
  37. import re, subprocess, os, time,  optparse, sys, shlex
  38.  
  39. scriptName = os.path.basename(__file__)     #the name of this script file
  40. py3 = sys.version_info[0]  >= 3
  41.  
  42. #functions to support python3's usage of bytes in some places where 2 uses strings
  43. tobytesifpy3 = lambda s = None  : s.encode() if py3 and s != None else s
  44. frombytesifpy3 = lambda b = None : b.decode("UTF-8") if py3 and b != None else b
  45.  
  46. #version of Popen.communicate that always takes and returns strings
  47. #regardless of py version
  48. def communicateStr ( process,  s = None ):
  49.     (a,b) = process.communicate( tobytesifpy3(s) )
  50.     return ( frombytesifpy3(a), frombytesifpy3(b) )
  51.  
  52. subprocess.Popen.communicateStr = communicateStr
  53.  
  54. #global variables (necessary in py2; for py3 should use nonlocal)
  55. installation_initialized = False
  56. installing = False
  57.  
  58. def generateSudoer(this_terminal_only = False,  tempDirectory = os.path.join(os.getenv("HOME"), ".texliveonfly") ):
  59.     lockfilePath = os.path.join(tempDirectory,  "newterminal_lock")
  60.     #NOTE: double-escaping \\ is neccessary for a slash to appear in the bash command
  61.     # in particular, double quotations in the command need to be written \\"
  62.     def spawnInNewTerminal(bashCommand):
  63.         #makes sure the temp directory exists
  64.         try:
  65.             os.mkdir(tempDirectory)
  66.         except OSError:
  67.             print("\n" + scriptName + ": Our temp directory " + tempDirectory +  " already exists; good.")
  68.  
  69.         #creates lock file
  70.         lockfile = open(lockfilePath, 'w')
  71.         lockfile.write( "Terminal privilege escalator running.")
  72.         lockfile.close()
  73.  
  74.         #adds intro and line to remove lock
  75.         bashCommand = '''echo \\"The graphical privilege escalator failed for some reason; we'll try asking for your administrator password here instead.\\n{0}\\n\\";{1}; rm \\"{2}\\"'''.format("-"*18,  bashCommand, lockfilePath)
  76.  
  77.         #runs the bash command in a new terminal
  78.         try:
  79.             subprocess.Popen ( ['x-terminal-emulator', '-e',  'sh -c "{0}"'.format(bashCommand) ]  )
  80.         except OSError:
  81.             try:
  82.                 subprocess.Popen ( ['xterm', '-e',  'sh -c "{0}"'.format(bashCommand) ]  )
  83.             except OSError:
  84.                 os.remove(lockfilePath)
  85.                 raise
  86.  
  87.         #doesn't let us proceed until the lock file has been removed by the bash command
  88.         while os.path.exists(lockfilePath):
  89.             time.sleep(0.1)
  90.  
  91.     def runSudoCommand(bashCommand):
  92.         if this_terminal_only:
  93.             process = subprocess.Popen( ['sudo'] + shlex.split(bashCommand) )
  94.             process.wait()
  95.         elif os.name == "mac":
  96.             process = subprocess.Popen(['osascript'], stdin=subprocess.PIPE )
  97.             process.communicateStr( '''do shell script "{0}" with administrator privileges'''.format(bashCommand) )
  98.         else:
  99.             #raises OSError if neither exist
  100.             try:
  101.                 process = subprocess.Popen( ['gksudo', bashCommand] )
  102.             except OSError:
  103.                 process = subprocess.Popen( ['kdesudo', bashCommand] )
  104.  
  105.             process.wait()
  106.  
  107.     # First tries one-liner graphical/terminal sudo, then opens extended command in new terminal
  108.     # raises OSError if both do
  109.     def attemptSudo(oneLiner, newTerminalCommand = ""):
  110.         try:
  111.             runSudoCommand(oneLiner)
  112.         except OSError:
  113.             if this_terminal_only:
  114.                 print("The sudo command has failed and we can't launch any more terminals.")
  115.                 raise
  116.             else:
  117.                 print("Default graphical priviledge escalator has failed for some reason.")
  118.                 print("A new terminal will open and you may be prompted for your sudo password.")
  119.                 spawnInNewTerminal(newTerminalCommand)
  120.  
  121.     return attemptSudo
  122.  
  123. #speech_setting = "never" prioritized over all others: "always", "install", "fail"
  124. def generateSpeakers(speech_setting):
  125.     speech_setting = speech_setting.lower()
  126.     doNothing = lambda x,  failure = None : None
  127.  
  128.     #most general inputs, always speaks
  129.     generalSpeaker = lambda expression,  failure = False : speakerFunc(expression)
  130.  
  131.     if "never" in speech_setting:
  132.         return (doNothing , doNothing)
  133.  
  134.     try:
  135.         if os.name == "mac":
  136.             speaker = subprocess.Popen(['say'], stdin=subprocess.PIPE )
  137.         else:
  138.             speaker = subprocess.Popen(['espeak'], stdin=subprocess.PIPE )
  139.     except:
  140.         return (doNothing , doNothing)
  141.  
  142.     def speakerFunc(expression):
  143.         if not expression.endswith("\n"):
  144.             expression += "\n"
  145.         try:
  146.             speaker.stdin.write(tobytesifpy3(expression))
  147.             speaker.stdin.flush()
  148.         except: #very tolerant of errors here
  149.             print("An error has occurred when using the speech synthesizer.")
  150.  
  151.     #if this is called, we're definitely installing.
  152.     def installationSpeaker(expression):
  153.         global installing
  154.         installing = True   #permanantly sets installing (for the endSpeaker)
  155.         if "install" in speech_setting:
  156.             speakerFunc(expression)
  157.  
  158.     def endSpeaker(expression,  failure = False):
  159.         if installing and "install" in speech_setting or failure and "fail" in speech_setting:
  160.             speakerFunc(expression)
  161.  
  162.     if "always" in speech_setting:
  163.         return (generalSpeaker, generalSpeaker)
  164.     else:
  165.         return (installationSpeaker,  endSpeaker)
  166.  
  167. #generates speaker for installing packages and an exit function
  168. def generateSpeakerFuncs(speech_setting):
  169.     (installspeaker,  exitspeaker) = generateSpeakers(speech_setting)
  170.  
  171.     def exiter(code = 0):
  172.         exitspeaker("Compilation{0}successful.".format(", un" if code != 0 else " "),  failure = code != 0 )
  173.         sys.exit(code)
  174.  
  175.     return (installspeaker, exiter)
  176.  
  177. def generateTLMGRFuncs(tlmgr, speaker, sudoFunc):
  178.     #checks that tlmgr is installed, raises OSError otherwise
  179.     #also checks whether we need to escalate permissions, using fake remove command
  180.     process = subprocess.Popen( [ tlmgr,  "remove" ], stdin=subprocess.PIPE, stdout = subprocess.PIPE,  stderr=subprocess.PIPE  )
  181.     (tlmgr_out,  tlmgr_err) = process.communicateStr()
  182.  
  183.     #does our default user have update permissions?
  184.     default_permission = "don't have permission" not in tlmgr_err
  185.  
  186.     #always call on first update; updates tlmgr and checks permissions
  187.     def initializeInstallation():
  188.         updateInfo = "Updating tlmgr prior to installing packages\n(this is necessary to avoid complaints from itself)."
  189.         print( scriptName + ": " + updateInfo)
  190.  
  191.         if default_permission:
  192.             process = subprocess.Popen( [tlmgr,  "update",  "--self" ] )
  193.             process.wait()
  194.         else:
  195.             print( "\n{0}: Default user doesn't have permission to modify the TeX Live distribution; upgrading to superuser for installation mode.\n".format(scriptName) )
  196.             basicCommand = ''''{0}' update --self'''.format(tlmgr)
  197.             sudoFunc( basicCommand, '''echo \\"This is {0}'s 'install packages on the fly' feature.\\n\\n{1}\\n\\" ; sudo {2}'''.format(scriptName, updateInfo, basicCommand ) )
  198.  
  199.     def installPackages(packages):
  200.         if len(packages) == 0:
  201.             return
  202.  
  203.         global installation_initialized
  204.         if not installation_initialized:
  205.             initializeInstallation()
  206.             installation_initialized = True
  207.  
  208.         packagesString = " ".join(packages)
  209.         print("{0}: Attempting to install LaTex package(s): {1}".format( scriptName, packagesString ) )
  210.  
  211.         if default_permission:
  212.             process = subprocess.Popen( [ tlmgr,  "install"] + packages , stdin=subprocess.PIPE )
  213.             process.wait()
  214.         else:
  215.             basicCommand = ''''{0}' install {1}'''.format(tlmgr,  packagesString)
  216.             bashCommand='''echo \\"This is {0}'s 'install packages on the fly' feature.\\n\\nAttempting to install LaTeX package(s): {1} \\"
  217. echo \\"(Some of them might not be real.)\\n\\"
  218. sudo {2}'''.format(scriptName, packagesString, basicCommand)
  219.  
  220.             sudoFunc(basicCommand, bashCommand)
  221.  
  222.     #strictmatch requires an entire /file match in the search results
  223.     def getSearchResults(preamble, term, strictMatch):
  224.         fontOrFile =  "font" if "font" in preamble else "file"
  225.         speaker("Searching for missing {0}: {1} ".format(fontOrFile, term))
  226.         print( "{0}: Searching repositories for missing {1} {2}".format(scriptName, fontOrFile,  term) )
  227.  
  228.         process = subprocess.Popen([ tlmgr, "search", "--global", "--file", term], stdin=subprocess.PIPE, stdout = subprocess.PIPE, stderr=subprocess.PIPE )
  229.         ( output ,  stderrdata ) = process.communicateStr()
  230.         outList = output.split("\n")
  231.  
  232.         results = ["latex"]    #latex 'result' for removal later
  233.  
  234.         for line in outList:
  235.             line = line.strip()
  236.             if line.startswith(preamble) and (not strictMatch or line.endswith("/" + term)):
  237.                 #filters out the package in:
  238.                 #   texmf-dist/.../package/file
  239.                 #and adds it to packages
  240.                 results.append(line.split("/")[-2].strip())
  241.                 results.append(line.split("/")[-3].strip()) #occasionally the package is one more slash before
  242.  
  243.         results = list(set(results))    #removes duplicates
  244.         results.remove("latex")     #removes most common fake result
  245.  
  246.         if len(results) == 0:
  247.             speaker("File not found.")
  248.             print("{0}: No results found for {1}".format( scriptName,  term ) )
  249.         else:
  250.             speaker("Installing.")
  251.  
  252.         return results
  253.  
  254.     def searchFilePackage(file):
  255.         return getSearchResults("texmf-dist/", file, True)
  256.  
  257.     def searchFontPackage(font):
  258.         font = re.sub(r"\((.*)\)", "", font)    #gets rid of parentheses
  259.         results = getSearchResults("texmf-dist/fonts/", font , False)
  260.  
  261.         #allow for possibility of lowercase
  262.         if len(results) == 0:
  263.             return [] if font.islower() else searchFontPackage(font.lower())
  264.         else:
  265.             return results
  266.  
  267.     def searchAndInstall(searcher,  entry):
  268.         installPackages(searcher(entry))
  269.         return entry    #returns the entry just installed
  270.  
  271.     return ( lambda entry : searchAndInstall(searchFilePackage,  entry),  lambda entry : searchAndInstall(searchFontPackage,  entry) )
  272.  
  273. def generateCompiler(compiler, arguments, texDoc, exiter):
  274.     def compileTexDoc():
  275.         try:
  276.             process = subprocess.Popen( [compiler] + shlex.split(arguments) + [texDoc], stdin=sys.stdin, stdout = subprocess.PIPE )
  277.             return readFromProcess(process)
  278.         except OSError:
  279.             print( "{0}: Unable to start {1}; are you sure it is installed?{2}".format(scriptName, compiler,
  280.                 "  \n\n(Or run " + scriptName + " --help for info on how to choose a different compiler.)" if compiler == defaultCompiler else "" )
  281.                 )
  282.             exiter(1)
  283.  
  284.     def readFromProcess(process):
  285.         getProcessLine = lambda : frombytesifpy3(process.stdout.readline())
  286.  
  287.         output = ""
  288.         line = getProcessLine()
  289.         while line != '':
  290.             output += line
  291.             sys.stdout.write(line)
  292.             line = getProcessLine()
  293.  
  294.         returnCode = None
  295.         while returnCode == None:
  296.             returnCode = process.poll()
  297.  
  298.         return (output, returnCode)
  299.  
  300.     return compileTexDoc
  301.  
  302. ### MAIN PROGRAM ###
  303.  
  304. if __name__ == '__main__':
  305.     # Parse command line
  306.     parser = optparse.OptionParser(
  307.         usage="\n\n\t%prog [options] file.tex\n\nUse option --help for more info.",
  308.         description = 'This program downloads TeX Live packages "on the fly" while compiling .tex documents.  ' +
  309.             'Some of its default options can be directly changed in {0}.  For example, the default compiler can be edited on line 4.'.format(scriptName) ,
  310.         version='1.2',
  311.         epilog = 'Copyright (C) 2011 Saitulaa Naranong.  This program comes with ABSOLUTELY NO WARRANTY; see the GNU General Public License v3 for more info.' ,
  312.         conflict_handler='resolve'
  313.     )
  314.  
  315.     parser.add_option('-h', '--help', action='help', help='print this help text and exit')
  316.     parser.add_option('-c', '--compiler', dest='compiler', metavar='COMPILER',
  317.         help='your LaTeX compiler; defaults to {0}'.format(defaultCompiler), default=defaultCompiler)
  318.     parser.add_option('-a', '--arguments', dest='arguments', metavar='ARGS',
  319.         help='arguments to pass to compiler; default is: "{0}"'.format(defaultArguments) , default=defaultArguments)
  320.     parser.add_option('--texlive_bin', dest='texlive_bin', metavar='LOCATION',
  321.         help='Custom location for the TeX Live bin folder', default="")
  322.     parser.add_option('--terminal_only', action = "store_true" , dest='terminal_only', default=False,
  323.         help="Forces us to assume we can run only in this terminal.  Permission escalators will appear here rather than graphically or in a new terminal.")
  324.     parser.add_option('-s',  '--speech_when' , dest='speech_setting', metavar="OPTION",  default=defaultSpeechSetting ,
  325.         help='Toggles speech-synthesized notifications (where supported).  OPTION can be "always", "never", "installing", "failed", or some combination.')
  326.     parser.add_option('-f', '--fail_silently', action = "store_true" , dest='fail_silently',
  327.         help="If tlmgr cannot be found, compile document anyway.", default=False)
  328.  
  329.     (options, args) = parser.parse_args()
  330.  
  331.     if len(args) == 0:
  332.         parser.error( "{0}: You must specify a .tex file to compile.".format(scriptName) )
  333.  
  334.     texDoc = args[0]
  335.     compiler_path = os.path.join( options.texlive_bin, options.compiler)
  336.  
  337.     (installSpeaker, exitScript) = generateSpeakerFuncs(options.speech_setting)
  338.     compileTex = generateCompiler( compiler_path, options.arguments, texDoc, exitScript)
  339.  
  340.     #initializes tlmgr, responds if the program not found
  341.     try:
  342.         tlmgr_path = os.path.join(options.texlive_bin, "tlmgr")
  343.         (installFile,  installFont) = generateTLMGRFuncs(tlmgr_path,  installSpeaker,  generateSudoer(options.terminal_only))
  344.     except OSError:
  345.         if options.fail_silently:
  346.             (output, returnCode)  = compileTex()
  347.             exitScript(returnCode)
  348.         else:
  349.             parser.error( "{0}: It appears {1} is not installed.  {2}".format(scriptName, tlmgr_path,
  350.                 "Are you sure you have TeX Live 2010 or later?" if tlmgr_path == "tlmgr" else "" ) )
  351.  
  352.     #loop constraints
  353.     done = False
  354.     previousFile = ""
  355.     previousFontFile = ""
  356.     previousFont =""
  357.  
  358.     #keeps running until all missing font/file errors are gone, or the same ones persist in all categories
  359.     while not done:
  360.         (output, returnCode)  = compileTex()
  361.  
  362.         #most reliable: searches for missing file
  363.         filesSearch = re.findall(r"! LaTeX Error: File `([^`']*)' not found" , output) + re.findall(r"! I can't find file `([^`']*)'." , output)
  364.         filesSearch = [ name for name in filesSearch if name != texDoc ]  #strips our .tex doc from list of files
  365.         #next most reliable: infers filename from font error
  366.         fontsFileSearch = [ name + ".tfm" for name in re.findall(r"! Font \\[^=]*=([^\s]*)\s", output) ]
  367.         #brute force search for font name in files
  368.         fontsSearch =  re.findall(r"! Font [^\n]*file\:([^\:\n]*)\:", output) + re.findall(r"! Font \\[^/]*/([^/]*)/", output)
  369.  
  370.         try:
  371.             if len(filesSearch) > 0 and filesSearch[0] != previousFile:
  372.                 previousFile = installFile(filesSearch[0] )
  373.             elif len(fontsFileSearch) > 0 and fontsFileSearch[0] != previousFontFile:
  374.                 previousFontFile = installFile(fontsFileSearch[0])
  375.             elif len(fontsSearch) > 0 and fontsSearch[0] != previousFont:
  376.                 previousFont = installFont(fontsSearch[0])
  377.             else:
  378.                 done = True
  379.         except OSError:
  380.             print("\n{0}: Unable to update; all privilege escalation attempts have failed!".format(scriptName) )
  381.             print("We've already compiled the .tex document, so there's nothing else to do.\n  Exiting..")
  382.             exitScript(returnCode)
  383.  
  384.     exitScript(returnCode)
  385.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement