Advertisement
Guest User

texliveonfly.py

a guest
Sep 25th, 2011
164
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 10.65 KB | None | 0 0
  1. #!/usr/bin/env python
  2.  
  3. # texliveonfly.py (formerly lualatexonfly.py) - "Downloading on the fly"
  4. #     (similar to miktex) for texlive.
  5. #
  6. # Given a .tex file, runs lualatex (by default) repeatedly, using error messages
  7. #     to install missing packages.
  8. #
  9. #
  10. # September 27, 2011 Release
  11. #
  12. # Written on Ubuntu 10.04 with TexLive 2011
  13. # Python 2.6+ or 3 (might work on low as 2.4)
  14. # Other systems may have not been tested.
  15. #
  16. # Copyright (C) 2011 Saitulaa Naranong
  17. #
  18. # This program is free software; you can redistribute it and/or modify
  19. # it under the terms of the GNU General Public License as published by
  20. # the Free Software Foundation; either version 3 of the License, or
  21. # (at your option) any later version.
  22. #
  23. # This program is distributed in the hope that it will be useful,
  24. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  25. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  26. # GNU General Public License for more details.
  27. #
  28. # You should have received a copy of the GNU General Public License
  29. # along with this program; if not, see <http://www.gnu.org/copyleft/gpl.html>.
  30.  
  31. import re, subprocess, os, time,  optparse, sys, shlex
  32.  
  33. scriptName = os.path.basename(__file__)     #the name of this script file
  34. py3 = sys.version_info[0]  >= 3
  35.  
  36. #functions to support python3's usage of bytes in some places where 2 uses strings
  37. tobytesifpy3 = lambda s : s.encode() if py3 else s
  38. frombytesifpy3 = lambda b : b.decode("UTF-8") if py3 else b
  39.  
  40. #sets up temp directory and paths
  41. tempDirectory =  os.path.join(os.getenv("HOME"), ".texliveonfly")
  42. lockfilePath = os.path.join(tempDirectory,  "newterminal_lock")
  43.  
  44. #makes sure the temp directory exists
  45. try:
  46.     os.mkdir(tempDirectory)
  47. except OSError:
  48.     print(scriptName + ": Our temp directory " + tempDirectory +  " already exists; good.")
  49.  
  50. checkedForUpdates = False   #have we checked for updates yet?
  51.  
  52. #NOTE: double-escaping \\ is neccessary for a slash to appear in the bash command
  53. # in particular, double quotations in the command need to be written \\"
  54. #NOTE: This function assumes it's only called after doc is compiled; otherwise remove sys.exit
  55. def spawnInNewTerminal(bashCommand):
  56.     #creates lock file
  57.     lockfile = open(lockfilePath, 'w')
  58.     lockfile.write( scriptName + " currently performing task in separate terminal.")
  59.     lockfile.close()
  60.  
  61.     #adds intro and line to remove lock
  62.     bashCommand = '''echo \\"This is {0}'s 'install packages on the fly' feature.\\n{1}\\n\\";{2}; rm \\"{3}\\"'''.format(scriptName, "-"*18,  bashCommand, lockfilePath)
  63.  
  64.     #runs the bash command in a new terminal
  65.     try:
  66.         if os.name == "mac":
  67.             #possible OS X bash implementation (needs testing)
  68.             process = subprocess.Popen(['osascript'], stdin=subprocess.PIPE, stderr=subprocess.PIPE)
  69.             process.communicate( tobytesifpy3( '''tell application "Terminal"\nactivate\ndo script with command "{0} ; exit"\nend tell'''.format(bashCommand) ) )
  70.             process.wait()
  71.         else:
  72.             process = subprocess.Popen ( ['x-terminal-emulator', '-e',  'sh -c "{0}"'.format(bashCommand) ]  )
  73.             process.wait()
  74.     except OSError:
  75.         print("\n{0}: Unable to update because we can't spawn new terminal:\n  {1}".format(scriptName,
  76.             "osascript does not seem to be working!" if os.name == "mac" else "there is no x-terminal-emulator!" ) )
  77.         print("We've already compiled the .tex document, so there's nothing else to do.\n  Exiting..")
  78.         os.remove(lockfilePath)
  79.         sys.exit(returnCode)
  80.  
  81.     #doesn't let us proceed until the lock file has been removed by the bash command
  82.     while os.path.exists(lockfilePath):
  83.         time.sleep(0.1)
  84.  
  85. def updateTLMGR():
  86.     global checkedForUpdates
  87.     if not checkedForUpdates:
  88.         spawnInNewTerminal('''echo \\"Updating tlmgr prior to installing packages\n(this is necessary to avoid complaints from itself).\\n\\" ; sudo tlmgr update --self''')
  89.         checkedForUpdates = True
  90.  
  91. #strictmatch requires an entire /file match in the search results
  92. def getSearchResults(preamble, term, strictMatch):
  93.     print( "{0}: Searching repositories for missing {1} {2}".format(scriptName, "font" if "font" in preamble else "file",  term) )
  94.  
  95.     process = subprocess.Popen(["tlmgr", "search", "--global", "--file", term], stdin=subprocess.PIPE, stdout = subprocess.PIPE, stderr=subprocess.PIPE )
  96.     ( output ,  stderrdata ) = process.communicate()
  97.     outList = frombytesifpy3(output).split("\n")
  98.  
  99.     results = ["latex"]    #latex 'result' for removal later
  100.  
  101.     for line in outList:
  102.         line = line.strip()
  103.         if line.startswith(preamble) and (not strictMatch or line.endswith("/" + term)):
  104.             #filters out the package in:
  105.             #   texmf-dist/.../package/file
  106.             #and adds it to packages
  107.             results.append(line.split("/")[-2].strip())
  108.             results.append(line.split("/")[-3].strip()) #occasionally the package is one more slash before
  109.  
  110.     results = list(set(results))    #removes duplicates
  111.     results.remove("latex")     #removes most common fake result
  112.  
  113.     if len(results) == 0:
  114.         print("No results found for " + term)
  115.  
  116.     return results
  117.  
  118. def getFilePackage(file):
  119.     return " ".join( getSearchResults("texmf-dist/", file, True) )
  120.  
  121. def getFontPackage(font):
  122.     font = re.sub(r"\((.*)\)", "", font)    #gets rid of parentheses
  123.     results = getSearchResults("texmf-dist/fonts/", font , False)
  124.  
  125.     #allow for possibility of lowercase
  126.     if len(results) == 0:
  127.         return "" if font.islower() else getFontPackage(font.lower())
  128.     else:
  129.         return " ".join(results)
  130.  
  131. #string can contain more than one package
  132. def installPackages(packagesString):
  133.     if packagesString.strip() == "":
  134.         return
  135.  
  136.     #New terminal is required: we're not guaranteed user can input sudo password into editor
  137.     print("Attempting to install LaTex package(s): " + packagesString )
  138.     print("A new terminal will open and you may be prompted for your sudo password.")
  139.  
  140.     updateTLMGR()  #avoids complaints about tlmgr not being updated
  141.  
  142.     #bash command to download
  143.     bashCommand='''echo \\"Attempting to install LaTeX package(s): {0} \\"
  144. echo \\"(Some of them might not be real.)\\n\\"
  145. sudo tlmgr install {0}'''.format(packagesString)
  146.  
  147.     spawnInNewTerminal(bashCommand)
  148.  
  149. def readFromProcess(process):
  150.     getProcessLine = lambda : frombytesifpy3(process.stdout.readline())
  151.  
  152.     output = ""
  153.     line = getProcessLine()
  154.     while line != '':
  155.         output += line
  156.         sys.stdout.write(line)
  157.         line = getProcessLine()
  158.  
  159.     returnCode = None
  160.     while returnCode == None:
  161.         returnCode = process.poll()
  162.  
  163.     return (output, returnCode)
  164.  
  165. def compileTex(compiler, arguments, texDoc):
  166.     try:
  167.         process = subprocess.Popen( [compiler] + shlex.split(arguments) + [texDoc], stdin=sys.stdin, stdout = subprocess.PIPE )
  168.         return readFromProcess(process)
  169.     except OSError:
  170.         print( "{0}: Unable to start {1}; are you sure it is installed?{2}".format(scriptName, compiler,
  171.             "  \n\n(Or run " + scriptName + " --help for info on how to choose a different compiler.)" if compiler == defaultCompiler else "" )
  172.             )
  173.         sys.exit(1)
  174.  
  175. ### MAIN PROGRAM ###
  176. licenseinfo = """texliveonfly.py Copyright (C) 2011 Saitulaa Naranong
  177. This program comes with ABSOLUTELY NO WARRANTY;
  178. See the GNU General Public License v3 for more info."""
  179.  
  180. defaultArgs = "-synctex=1 -interaction=nonstopmode"
  181. defaultCompiler = "lualatex"
  182.  
  183. if __name__ == '__main__':
  184.     # Parse command line
  185.     parser = optparse.OptionParser(
  186.         usage="\n\n\t%prog [options] file.tex\n\nUse option --help for more info.\n\n" + licenseinfo ,
  187.         version='2011.27.9',
  188.         conflict_handler='resolve'
  189.     )
  190.  
  191.     parser.add_option('-h', '--help', action='help', help='print this help text and exit')
  192.     parser.add_option('-c', '--compiler', dest='compiler', metavar='COMPILER',
  193.         help='your LaTeX compiler; defaults to {0}'.format(defaultCompiler), default=defaultCompiler)
  194.     parser.add_option('-a', '--arguments', dest='arguments', metavar='ARGS',
  195.         help='arguments to pass to compiler; default is: "{0}"'.format(defaultArgs) , default=defaultArgs)
  196.     parser.add_option('-f', '--fail_silently', action = "store_true" , dest='fail_silently',
  197.         help="If tlmgr cannot be found, compile document anyway.", default=False)
  198.  
  199.     (options, args) = parser.parse_args()
  200.  
  201.     if len(args) == 0:
  202.         parser.error( "{0}: You must specify a .tex file to compile.".format(scriptName) )
  203.  
  204.     texDoc = args[0]
  205.  
  206.     #checks that tlmgr is installed, responds based on --fail_silently flag
  207.     try:
  208.         process = subprocess.Popen( ["tlmgr"] , stdout = subprocess.PIPE,  stderr=subprocess.PIPE )
  209.     except OSError:
  210.         if options.fail_silently:
  211.             (output, returnCode)  = compileTex(options.compiler, options.arguments, texDoc)
  212.             sys.exit(returnCode)
  213.         else:
  214.             parser.error( "{0}: It appears tlmgr is not installed.  Are you sure you have TeX Live 2010 or later?".format(scriptName) )
  215.  
  216.     #loop constraints
  217.     done = False
  218.     previousFile = ""
  219.     previousFontFile = ""
  220.     previousFont =""
  221.  
  222.     #keeps running until all missing font/file errors are gone, or the same ones persist in all categories
  223.     while not done:
  224.         (output, returnCode)  = compileTex(options.compiler, options.arguments, texDoc)
  225.  
  226.         #most reliable: searches for missing file
  227.         filesSearch = re.findall(r"! LaTeX Error: File `([^`']*)' not found" , output) + re.findall(r"! I can't find file `([^`']*)'." , output)
  228.         filesSearch = [ name for name in filesSearch if name != texDoc ]  #strips our .tex doc from list of files
  229.         #next most reliable: infers filename from font error
  230.         fontsFileSearch = [ name + ".tfm" for name in re.findall(r"! Font \\[^=]*=([^\s]*)\s", output) ]
  231.         #brute force search for font name in files
  232.         fontsSearch =  re.findall(r"! Font [^\n]*file\:([^\:\n]*)\:", output) + re.findall(r"! Font \\[^/]*/([^/]*)/", output)
  233.  
  234.         if len(filesSearch) > 0 and filesSearch[0] != previousFile:
  235.             installPackages(getFilePackage(filesSearch[0]))
  236.             previousFile = filesSearch[0]
  237.         elif len(fontsFileSearch) > 0 and fontsFileSearch[0] != previousFontFile:
  238.             installPackages(getFilePackage(fontsFileSearch[0]))
  239.             previousFontFile = fontsFileSearch[0]
  240.         elif len(fontsSearch) > 0 and fontsSearch[0] != previousFont:
  241.             installPackages(getFontPackage(fontsSearch[0]))
  242.             previousFont = fontsSearch[0]
  243.         else:
  244.             done = True
  245.  
  246.     sys.exit(returnCode)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement