Advertisement
Guest User

texliveonfly.py

a guest
Oct 1st, 2011
305
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 17.17 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.10 ; October 3, 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. class Sudoer(object):
  55.     def __init__(self, thisTerminalOnly = False,  tempDirectory = os.path.join(os.getenv("HOME"), ".texliveonfly") ):
  56.         self._tempDirectory = tempDirectory
  57.         self._lockfilePath = os.path.join(self._tempDirectory,  "newterminal_lock")
  58.         self._this_terminal_only = thisTerminalOnly
  59.  
  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(self,  bashCommand):
  63.         #makes sure the temp directory exists
  64.         try:
  65.             os.mkdir(self._tempDirectory)
  66.         except OSError:
  67.             print(scriptName + ": Our temp directory " + self._tempDirectory +  " already exists; good.")
  68.  
  69.         #creates lock file
  70.         lockfile = open(self._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, self._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(self._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(self._lockfilePath):
  89.             time.sleep(0.1)
  90.  
  91.     def runSudoCommand(self, bashCommand):
  92.         if self._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(self, oneLiner, newTerminalCommand):
  110.         try:
  111.             self.runSudoCommand(oneLiner)
  112.         except OSError:
  113.             if self._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.                 self.spawnInNewTerminal(newTerminalCommand)
  120.  
  121. class TLPackageManager(Sudoer):
  122.     #raises OSError if tlmgr_name is bad
  123.     def __init__(self, tlmgr_name, thisTerminalOnly = False, speechSetting = "",  tempDirectory = None ):
  124.         self.tlmgr = tlmgr_name
  125.         self._installation_initialized = False   #have we checked for updates yet?
  126.         self._search_performed = False       #have we tried to search yet?
  127.  
  128.         #checks that tlmgr is installed, raises OSError otherwise
  129.         #also checks whether we need to escalate permissions, using fake remove command
  130.         process = subprocess.Popen( [ self.tlmgr,  "remove" ], stdin=subprocess.PIPE, stdout = subprocess.PIPE,  stderr=subprocess.PIPE  )
  131.         (tlmgr_out,  tlmgr_err) = process.communicateStr()
  132.  
  133.         #does our default user have update permissions?
  134.         self._default_permission = "don't have permission" not in tlmgr_err
  135.  
  136.         if tempDirectory == None:
  137.             Sudoer.__init__(self, thisTerminalOnly)
  138.         else:
  139.             Sudoer.__init__(self, thisTerminalOnly,  tempDirectory)
  140.  
  141.         self._speech_setting = speechSetting.lower()
  142.         if self._speech_setting != "" and "never" not in self._speech_setting: #user entered nonempty value
  143.             try:
  144.                 if os.name == "mac":
  145.                     self.speaker = subprocess.Popen(['say'], stdin=subprocess.PIPE )
  146.                 else:
  147.                     self.speaker = subprocess.Popen(['espeak'], stdin=subprocess.PIPE )
  148.             except:
  149.                 self.speaker = None
  150.  
  151.     #always call on first update; updates tlmgr and checks permissions
  152.     def initializeInstallation(self):
  153.         if not self._installation_initialized:
  154.             self._installation_initialized = True
  155.             updateInfo = "Updating tlmgr prior to installing packages\n(this is necessary to avoid complaints from itself)."
  156.             print( scriptName + ": " + updateInfo)
  157.  
  158.             if self._default_permission:
  159.                 process = subprocess.Popen( [ self.tlmgr,  "update",  "--self" ] )
  160.                 process.wait()
  161.             else:
  162.                 print( "\n{0}: Default user doesn't have permission to modify the TeX Live distribution; upgrading to superuser for all future tasks.\n".format(scriptName) )
  163.                 basicCommand = ''''{0}' update --self'''.format(self.tlmgr)
  164.                 self.attemptSudo( basicCommand, '''echo \\"This is {0}'s 'install packages on the fly' feature.\\n\\n{1}\\n\\" ; sudo {2}'''.format(scriptName, updateInfo, basicCommand ) )
  165.  
  166.     #strictmatch requires an entire /file match in the search results
  167.     def getSearchResults(self,  preamble, term, strictMatch):
  168.         self._search_performed = True
  169.         fontOrFile =  "font" if "font" in preamble else "file"
  170.         self.speak("Searching for missing {0}: {1} ".format(fontOrFile, term))
  171.         print( "{0}: Searching repositories for missing {1} {2}".format(scriptName, fontOrFile,  term) )
  172.  
  173.         process = subprocess.Popen([ self.tlmgr, "search", "--global", "--file", term], stdin=subprocess.PIPE, stdout = subprocess.PIPE, stderr=subprocess.PIPE )
  174.         ( output ,  stderrdata ) = process.communicateStr()
  175.         outList = output.split("\n")
  176.  
  177.         results = ["latex"]    #latex 'result' for removal later
  178.  
  179.         for line in outList:
  180.             line = line.strip()
  181.             if line.startswith(preamble) and (not strictMatch or line.endswith("/" + term)):
  182.                 #filters out the package in:
  183.                 #   texmf-dist/.../package/file
  184.                 #and adds it to packages
  185.                 results.append(line.split("/")[-2].strip())
  186.                 results.append(line.split("/")[-3].strip()) #occasionally the package is one more slash before
  187.  
  188.         results = list(set(results))    #removes duplicates
  189.         results.remove("latex")     #removes most common fake result
  190.  
  191.         if len(results) == 0:
  192.             self.speak("File not found.")
  193.             print("{0}: No results found for {1}".format( scriptName,  term ) )
  194.         else:
  195.             self.speak("Installing.")
  196.  
  197.         return results
  198.  
  199.     def searchFilePackage(self,  file):
  200.         return self.getSearchResults("texmf-dist/", file, True)
  201.  
  202.     def searchFontPackage(self,  font):
  203.         font = re.sub(r"\((.*)\)", "", font)    #gets rid of parentheses
  204.         results = self.getSearchResults("texmf-dist/fonts/", font , False)
  205.  
  206.         #allow for possibility of lowercase
  207.         if len(results) == 0:
  208.             return [] if font.islower() else self.searchFontPackage(font.lower())
  209.         else:
  210.             return results
  211.  
  212.     def installPackages(self,  packages):
  213.         if len(packages) == 0:
  214.             return
  215.  
  216.         self.initializeInstallation()
  217.  
  218.         packagesString = " ".join(packages)
  219.         print("{0}: Attempting to install LaTex package(s): {1}".format( scriptName,  packagesString ) )
  220.  
  221.         if self._default_permission:
  222.             process = subprocess.Popen( [ self.tlmgr,  "install"] + packages , stdin=subprocess.PIPE )
  223.             process.wait()
  224.         else:
  225.             basicCommand = ''''{0}' install {1}'''.format(self.tlmgr,  packagesString)
  226.             bashCommand='''echo \\"This is {0}'s 'install packages on the fly' feature.\\n\\nAttempting to install LaTeX package(s): {1} \\"
  227. echo \\"(Some of them might not be real.)\\n\\"
  228. sudo {2}'''.format(scriptName, packagesString, basicCommand)
  229.  
  230.             self.attemptSudo(basicCommand, bashCommand)
  231.  
  232.     def searchAndInstall(self,  searcher,  entry):
  233.         self.installPackages(searcher(entry))
  234.  
  235.     def speak(self, expression, failure = False):
  236.         if  "never" in self._speech_setting or  "always" not in self._speech_setting and not \
  237.         ("install" in self._speech_setting and self._search_performed) and not (failure and "fail" in self._speech_setting ):
  238.             return
  239.  
  240.         if not expression.endswith("\n"):
  241.             expression += "\n"
  242.  
  243.         try:
  244.             self.speaker.stdin.write(tobytesifpy3(expression))
  245.             self.speaker.stdin.flush()
  246.         except: #very tolerant of errors here, including self.speaker being None or not being declared
  247.             print("An error has occurred when using the speech synthesizer.")
  248.  
  249. def readFromProcess(process):
  250.     getProcessLine = lambda : frombytesifpy3(process.stdout.readline())
  251.  
  252.     output = ""
  253.     line = getProcessLine()
  254.     while line != '':
  255.         output += line
  256.         sys.stdout.write(line)
  257.         line = getProcessLine()
  258.  
  259.     returnCode = None
  260.     while returnCode == None:
  261.         returnCode = process.poll()
  262.  
  263.     return (output, returnCode)
  264.  
  265. def compileTex(compiler, arguments, texDoc):
  266.     try:
  267.         process = subprocess.Popen( [compiler] + shlex.split(arguments) + [texDoc], stdin=sys.stdin, stdout = subprocess.PIPE )
  268.         return readFromProcess(process)
  269.     except OSError:
  270.         print( "{0}: Unable to start {1}; are you sure it is installed?{2}".format(scriptName, compiler,
  271.             "  \n\n(Or run " + scriptName + " --help for info on how to choose a different compiler.)" if compiler == defaultCompiler else "" )
  272.             )
  273.         exitScript(1)
  274.  
  275. def exitScript(code = 0,  speaker = None):
  276.     if speaker != None:
  277.         speaker("Compilation{0}successful.".format(", un" if code != 0 else " "),  failure = code != 0 )
  278.  
  279.     sys.exit(code)
  280.  
  281. ### MAIN PROGRAM ###
  282.  
  283. if __name__ == '__main__':
  284.     # Parse command line
  285.     parser = optparse.OptionParser(
  286.         usage="\n\n\t%prog [options] file.tex\n\nUse option --help for more info.",
  287.         description = 'This program downloads TeX Live packages "on the fly" while compiling .tex documents.  ' +
  288.             'Some of its default options can be directly changed in {0}.  For example, the default compiler can be edited on line 4.'.format(scriptName) ,
  289.         version='1.10',
  290.         epilog = 'Copyright (C) 2011 Saitulaa Naranong.  This program comes with ABSOLUTELY NO WARRANTY; see the GNU General Public License v3 for more info.' ,
  291.         conflict_handler='resolve'
  292.     )
  293.  
  294.     parser.add_option('-h', '--help', action='help', help='print this help text and exit')
  295.     parser.add_option('-c', '--compiler', dest='compiler', metavar='COMPILER',
  296.         help='your LaTeX compiler; defaults to {0}'.format(defaultCompiler), default=defaultCompiler)
  297.     parser.add_option('-a', '--arguments', dest='arguments', metavar='ARGS',
  298.         help='arguments to pass to compiler; default is: "{0}"'.format(defaultArguments) , default=defaultArguments)
  299.     parser.add_option('--texlive_bin', dest='texlive_bin', metavar='LOCATION',
  300.         help='Custom location for the TeX Live bin folder', default="")
  301.     parser.add_option('--terminal_only', action = "store_true" , dest='terminal_only', default=False,
  302.         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.")
  303.     parser.add_option('-s',  '--speech_when' , dest='speech_setting', metavar="OPTION",  default=defaultSpeechSetting ,
  304.         help='Toggles speech-synthesized notifications (where supported).  OPTION can be "always", "never", "installing", "failed", or some combination.')
  305.     parser.add_option('-f', '--fail_silently', action = "store_true" , dest='fail_silently',
  306.         help="If tlmgr cannot be found, compile document anyway.", default=False)
  307.  
  308.     (options, args) = parser.parse_args()
  309.  
  310.     if len(args) == 0:
  311.         parser.error( "{0}: You must specify a .tex file to compile.".format(scriptName) )
  312.  
  313.     texDoc = args[0]
  314.  
  315.     #initializes tlmgr, responds if the program not found
  316.     try:
  317.         tlmgr_path = os.path.join(options.texlive_bin, "tlmgr")
  318.         tlmgr = TLPackageManager( tlmgr_path ,  options.terminal_only, options.speech_setting )
  319.     except OSError:
  320.         if options.fail_silently:
  321.             (output, returnCode)  = compileTex( os.path.join( options.texlive_bin, options.compiler), options.arguments, texDoc)
  322.             exitScript(returnCode)
  323.         else:
  324.             parser.error( "{0}: It appears {1} is not installed.  {2}".format(scriptName, tlmgr_path,
  325.                 "Are you sure you have TeX Live 2010 or later?" if tlmgr_path == "tlmgr" else "" ) )
  326.  
  327.     #loop constraints
  328.     done = False
  329.     previousFile = ""
  330.     previousFontFile = ""
  331.     previousFont =""
  332.  
  333.     #keeps running until all missing font/file errors are gone, or the same ones persist in all categories
  334.     while not done:
  335.         (output, returnCode)  = compileTex( os.path.join( options.texlive_bin, options.compiler), options.arguments, texDoc)
  336.  
  337.         #most reliable: searches for missing file
  338.         filesSearch = re.findall(r"! LaTeX Error: File `([^`']*)' not found" , output) + re.findall(r"! I can't find file `([^`']*)'." , output)
  339.         filesSearch = [ name for name in filesSearch if name != texDoc ]  #strips our .tex doc from list of files
  340.         #next most reliable: infers filename from font error
  341.         fontsFileSearch = [ name + ".tfm" for name in re.findall(r"! Font \\[^=]*=([^\s]*)\s", output) ]
  342.         #brute force search for font name in files
  343.         fontsSearch =  re.findall(r"! Font [^\n]*file\:([^\:\n]*)\:", output) + re.findall(r"! Font \\[^/]*/([^/]*)/", output)
  344.  
  345.         try:
  346.             if len(filesSearch) > 0 and filesSearch[0] != previousFile:
  347.                 tlmgr.searchAndInstall(tlmgr.searchFilePackage,filesSearch[0] )
  348.                 previousFile = filesSearch[0]
  349.             elif len(fontsFileSearch) > 0 and fontsFileSearch[0] != previousFontFile:
  350.                 tlmgr.searchAndInstall(tlmgr.searchFilePackage,fontsFileSearch[0])
  351.                 previousFontFile = fontsFileSearch[0]
  352.             elif len(fontsSearch) > 0 and fontsSearch[0] != previousFont:
  353.                 tlmgr.searchAndInstall(tlmgr.searchFontPackage,fontsSearch[0])
  354.                 previousFont = fontsSearch[0]
  355.             else:
  356.                 done = True
  357.         except OSError:
  358.             print("\n{0}: Unable to update; all privilege escalation attempts have failed!".format(scriptName) )
  359.             print("We've already compiled the .tex document, so there's nothing else to do.\n  Exiting..")
  360.             exitScript(returnCode,  tlmgr.speak)
  361.  
  362.     exitScript(returnCode, tlmgr.speak)
  363.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement