X-Git-Url: https://git.lyx.org/gitweb/?a=blobdiff_plain;f=lib%2Fconfigure.py;h=872e2ac50d2d064837ea07c4459770bcc6f310f0;hb=653376087d4107e127d577d6a4f29351a9d46b05;hp=d124d06a1274ea991c352788d09eb8f34f61b244;hpb=cb7ecd7fee806d6387eb475ec112c9d047541b10;p=lyx.git diff --git a/lib/configure.py b/lib/configure.py index d124d06a12..872e2ac50d 100644 --- a/lib/configure.py +++ b/lib/configure.py @@ -102,7 +102,15 @@ def checkTeXPaths(): from tempfile import mkstemp fd, tmpfname = mkstemp(suffix='.ltx') if os.name == 'nt': - inpname = tmpfname.replace('\\', '/') + from ctypes import windll, create_unicode_buffer + GetShortPathName = windll.kernel32.GetShortPathNameW + longname = unicode(tmpfname) + shortlen = GetShortPathName(longname, 0, 0) + shortname = create_unicode_buffer(shortlen) + if GetShortPathName(longname, shortname, shortlen): + inpname = shortname.value.replace('\\', '/') + else: + inpname = tmpfname.replace('\\', '/') else: inpname = cmdOutput('cygpath -m ' + tmpfname) logname = os.path.basename(inpname.replace('.ltx', '.log')) @@ -110,6 +118,8 @@ def checkTeXPaths(): os.write(fd, r'\relax') os.close(fd) latex_out = cmdOutput(r'latex "\nonstopmode\input{%s}"' % inpname) + if 'Error' in latex_out: + latex_out = cmdOutput(r'latex "\nonstopmode\input{\"%s\"}"' % inpname) if 'Error' in latex_out: logger.warning("configure: TeX engine needs posix-style paths in latex files") windows_style_tex_paths = 'false' @@ -138,9 +148,9 @@ def checkProg(description, progs, rc_entry = [], path = [], not_found = ''): 1. emtpy: no rc entry will be added 2. one pattern: %% will be replaced by the first found program, or '' if no program is found. - 3. several patterns for each prog and not_found. This is used - when different programs have different usages. If you do not - want not_found entry to be added to the RC file, you can specify + 3. several patterns for each prog and not_found. This is used + when different programs have different usages. If you do not + want not_found entry to be added to the RC file, you can specify an entry for each prog and use '' for the not_found entry. not_found: the value that should be used instead of '' if no program @@ -181,7 +191,7 @@ def checkProg(description, progs, rc_entry = [], path = [], not_found = ''): def checkProgAlternatives(description, progs, rc_entry = [], alt_rc_entry = [], path = [], not_found = ''): - ''' + ''' The same as checkProg, but additionally, all found programs will be added as alt_rc_entries ''' @@ -250,44 +260,38 @@ def checkProgAlternatives(description, progs, rc_entry = [], alt_rc_entry = [], return ['', not_found] -def addViewerAlternatives(rcs): - r = re.compile(r'\\Format (\S+).*$') - m = None - alt = '' - for idxx in range(len(rcs)): - if len(rcs) == 1: - m = r.match(rcs[0]) - if m: - alt = r'\viewer_alternatives ' + m.group(1) + " %%" - elif len(rcs) > 1: - m = r.match(rcs[idxx]) - if m: - if idxx > 0: - alt += '\n' - alt += r'\viewer_alternatives ' + m.group(1) + " %%" - return alt - - -def addEditorAlternatives(rcs): +def addAlternatives(rcs, alt_type): + ''' + Returns a \\prog_alternatives string to be used as an alternative + rc entry. alt_type can be a string or a list of strings. + ''' r = re.compile(r'\\Format (\S+).*$') m = None alt = '' + alt_token = '\\%s_alternatives ' + if isinstance(alt_type, str): + alt_tokens = [alt_token % alt_type] + else: + alt_tokens = map(lambda s: alt_token % s, alt_type) for idxx in range(len(rcs)): if len(rcs) == 1: m = r.match(rcs[0]) if m: - alt = r'\editor_alternatives ' + m.group(1) + " %%" + alt = '\n'.join([s + m.group(1) + " %%" for s in alt_tokens]) elif len(rcs) > 1: m = r.match(rcs[idxx]) if m: if idxx > 0: alt += '\n' - alt += r'\editor_alternatives ' + m.group(1) + " %%" + alt += '\n'.join([s + m.group(1) + " %%" for s in alt_tokens]) return alt -def checkViewer(description, progs, rc_entry = [], path = []): - ''' The same as checkProgAlternatives, but for viewers ''' +def listAlternatives(progs, alt_type, rc_entry = []): + ''' + Returns a list of \\prog_alternatives strings to be used as alternative + rc entries. alt_type can be a string or a list of strings. + ''' if len(rc_entry) > 1 and len(rc_entry) != len(progs) + 1: logger.error("rc entry should have one item or item for each prog and not_found.") sys.exit(2) @@ -295,75 +299,45 @@ def checkViewer(description, progs, rc_entry = [], path = []): for idx in range(len(progs)): if len(rc_entry) == 1: rcs = rc_entry[0].split('\n') - alt = addViewerAlternatives(rcs) + alt = addAlternatives(rcs, alt_type) alt_rc_entry.insert(0, alt) elif len(rc_entry) > 1: rcs = rc_entry[idx].split('\n') - alt = addViewerAlternatives(rcs) + alt = addAlternatives(rcs, alt_type) alt_rc_entry.insert(idx, alt) + return alt_rc_entry + + +def checkViewer(description, progs, rc_entry = [], path = []): + ''' The same as checkProgAlternatives, but for viewers ''' + alt_rc_entry = listAlternatives(progs, 'viewer', rc_entry) return checkProgAlternatives(description, progs, rc_entry, alt_rc_entry, path, not_found = 'auto') def checkEditor(description, progs, rc_entry = [], path = []): ''' The same as checkProgAlternatives, but for editors ''' - if len(rc_entry) > 1 and len(rc_entry) != len(progs) + 1: - logger.error("rc entry should have one item or item for each prog and not_found.") - sys.exit(2) - alt_rc_entry = [] - for idx in range(len(progs)): - if len(rc_entry) == 1: - rcs = rc_entry[0].split('\n') - alt = addEditorAlternatives(rcs) - alt_rc_entry.insert(0, alt) - elif len(rc_entry) > 1: - rcs = rc_entry[idx].split('\n') - alt = addEditorAlternatives(rcs) - alt_rc_entry.insert(idx, alt) + alt_rc_entry = listAlternatives(progs, 'editor', rc_entry) return checkProgAlternatives(description, progs, rc_entry, alt_rc_entry, path, not_found = 'auto') def checkViewerNoRC(description, progs, rc_entry = [], path = []): ''' The same as checkViewer, but do not add rc entry ''' - if len(rc_entry) > 1 and len(rc_entry) != len(progs) + 1: - logger.error("rc entry should have one item or item for each prog and not_found.") - sys.exit(2) - alt_rc_entry = [] - for idx in range(len(progs)): - if len(rc_entry) == 1: - rcs = rc_entry[0].split('\n') - alt = addViewerAlternatives(rcs) - alt_rc_entry.insert(0, alt) - elif len(rc_entry) > 1: - rcs = rc_entry[idx].split('\n') - alt = addViewerAlternatives(rcs) - alt_rc_entry.insert(idx, alt) + alt_rc_entry = listAlternatives(progs, 'viewer', rc_entry) rc_entry = [] return checkProgAlternatives(description, progs, rc_entry, alt_rc_entry, path, not_found = 'auto') def checkEditorNoRC(description, progs, rc_entry = [], path = []): ''' The same as checkViewer, but do not add rc entry ''' - if len(rc_entry) > 1 and len(rc_entry) != len(progs) + 1: - logger.error("rc entry should have one item or item for each prog and not_found.") - sys.exit(2) - alt_rc_entry = [] - for idx in range(len(progs)): - if len(rc_entry) == 1: - rcs = rc_entry[0].split('\n') - alt = addEditorAlternatives(rcs) - alt_rc_entry.insert(0, alt) - elif len(rc_entry) > 1: - rcs = rc_entry[idx].split('\n') - alt = addEditorAlternatives(rcs) - alt_rc_entry.insert(idx, alt) + alt_rc_entry = listAlternatives(progs, 'editor', rc_entry) rc_entry = [] return checkProgAlternatives(description, progs, rc_entry, alt_rc_entry, path, not_found = 'auto') def checkViewerEditor(description, progs, rc_entry = [], path = []): ''' The same as checkProgAlternatives, but for viewers and editors ''' - checkEditorNoRC(description, progs, rc_entry, path) - return checkViewer(description, progs, rc_entry, path) + alt_rc_entry = listAlternatives(progs, ['editor', 'viewer'], rc_entry) + return checkProgAlternatives(description, progs, rc_entry, alt_rc_entry, path, not_found = 'auto') def checkDTLtools(): @@ -398,7 +372,7 @@ def checkLatex(dtl_tools): PLATEX = '' removeFiles(['chklatex.ltx', 'chklatex.log']) #----------------------------------------------------------------- - # use LATEX to convert from latex to dvi if PPLATEX is not available + # use LATEX to convert from latex to dvi if PPLATEX is not available if PPLATEX == '': PPLATEX = LATEX if dtl_tools: @@ -456,7 +430,7 @@ def checkLuatex(): logger.info(msg + ' yes') addToRC(r'\converter luatex pdf5 "%s" "latex=lualatex"' % LUATEX) if DVILUATEX != '': - addToRC(r'\converter luatex dvi3 "%s" "latex=lualatex"' % DVILUATEX) + addToRC(r'\converter luatex dvi3 "%s" "latex=dvilualatex"' % DVILUATEX) # remove temporary files removeFiles(['luatest.tex', 'luatest.log', 'luatest.aux', 'luatest.pdf']) @@ -473,7 +447,7 @@ def checkModule(module): return False -def checkFormatEntries(dtl_tools): +def checkFormatEntries(dtl_tools): ''' Check all formats (\Format entries) ''' checkViewerEditor('a Tgif viewer and editor', ['tgif'], rc_entry = [r'\Format tgif obj Tgif "" "%%" "%%" "vector"']) @@ -534,7 +508,7 @@ def checkFormatEntries(dtl_tools): \Format asciixfig asc "Plain text (Xfig output)" "" "" "%%" "" \Format dateout tmp "date (output)" "" "" "%%" "" \Format docbook sgml DocBook B "" "%%" "document,menu=export" -\Format docbook-xml xml "Docbook (XML)" "" "" "%%" "document,menu=export" +\Format docbook-xml xml "DocBook (XML)" "" "" "%%" "document,menu=export" \Format dot dot "Graphviz Dot" "" "" "%%" "vector" \Format platex tex "LaTeX (pLaTeX)" "" "" "%%" "document,menu=export" \Format literate nw NoWeb N "" "%%" "document,menu=export" @@ -551,10 +525,10 @@ def checkFormatEntries(dtl_tools): \Format text4 txt "Plain text (catdvi)" "" "" "%%" "document" \Format textparagraph txt "Plain Text, Join Lines" "" "" "%%" "document"''' ]) #Spreadsheets using ssconvert from gnumeric - checkViewer('gnumeric spreadsheet software', ['gnumeric'], - rc_entry = [r'''\Format gnumeric gnumeric "Gnumeric spreadsheet" "" "" "%%" "document" -\Format excel xls "Excel spreadsheet" "" "" "%%" "document" -\Format oocalc ods "OpenOffice spreadsheet" "" "" "%%" "document"''']) + checkViewer('gnumeric spreadsheet software', ['gnumeric'], + rc_entry = [r'''\Format gnumeric gnumeric "Gnumeric spreadsheet" "" "" "%%" "document" +\Format excel xls "Excel spreadsheet" "" "" "%%" "document" +\Format oocalc ods "OpenOffice spreadsheet" "" "" "%%" "document"''']) # path, xhtmlview = checkViewer('an HTML previewer', ['firefox', 'mozilla file://$$p$$i', 'netscape'], rc_entry = [r'\Format xhtml xhtml "LyXHTML" y "%%" "" "document,menu=export"']) @@ -572,7 +546,7 @@ def checkFormatEntries(dtl_tools): # rc_entry = [ r'\ps_command "%%"' ]) checkViewer('a Postscript previewer', ['kghostview', 'okular', 'evince', 'gv', 'ghostview -swap'], rc_entry = [r'''\Format eps eps EPS "" "%%" "" "vector" -\Format ps ps Postscript t "%%" "" "document,vector"''']) +\Format ps ps Postscript t "%%" "" "document,vector,menu=export"''']) # for xdg-open issues look here: http://www.mail-archive.com/lyx-devel@lists.lyx.org/msg151818.html checkViewer('a PDF previewer', ['kpdf', 'okular', 'evince', 'kghostview', 'xpdf', 'acrobat', 'acroread', \ 'gv', 'ghostview'], @@ -598,7 +572,7 @@ def checkFormatEntries(dtl_tools): checkViewerEditor('an OpenDocument/OpenOffice viewer', ['swriter', 'oowriter', 'abiword'], rc_entry = [r'''\Format odt odt OpenDocument "" "%%" "%%" "document,vector,menu=export" \Format sxw sxw "OpenOffice.Org (sxw)" "" "" "" "document,vector"''']) - # + # checkViewerEditor('a Rich Text and Word viewer', ['swriter', 'oowriter', 'abiword'], rc_entry = [r'''\Format rtf rtf "Rich Text Format" "" "%%" "%%" "document,vector,menu=export" \Format word doc "MS Word" W "%%" "%%" "document,vector,menu=export"''']) @@ -612,6 +586,7 @@ def checkFormatEntries(dtl_tools): \Format lyx14x 14.lyx "LyX 1.4.x" "" "" "" "document" \Format lyx15x 15.lyx "LyX 1.5.x" "" "" "" "document" \Format lyx16x 16.lyx "LyX 1.6.x" "" "" "" "document,menu=export" +\Format lyx20x 20.lyx "LyX 2.0.x" "" "" "" "document,menu=export" \Format clyx cjklyx "CJK LyX 1.4.x (big5)" "" "" "" "document" \Format jlyx cjklyx "CJK LyX 1.4.x (euc-jp)" "" "" "" "document" \Format klyx cjklyx "CJK LyX 1.4.x (euc-kr)" "" "" "" "document" @@ -636,7 +611,7 @@ def checkConverterEntries(): rc_entry = [ r'\converter xetex pdf4 "%%" "latex=xelatex"' ]) checkLuatex() - + ''' If we're running LyX in-place then tex2lyx will be found in ../src/tex2lyx. Add this directory to the PATH temporarily and search for tex2lyx. @@ -770,7 +745,7 @@ def checkConverterEntries(): else: # set empty converter to override the default imagemagick addToRC(r'\converter lyxpreview ppm "" ""') - # + # checkProg('a fax program', ['kdeprintfax $$i', 'ksendfax $$i', 'hylapex $$i'], rc_entry = [ r'\converter ps fax "%%" ""']) # @@ -825,22 +800,22 @@ def checkConverterEntries(): checkProg('a Dia -> EPS converter', ['dia -e $$o -t eps $$i'], rc_entry = [ r'\converter dia eps "%%" ""']) # - checkProg('a SVG -> PDF converter', ['rsvg-convert -f pdf -o $$o $$i', 'inkscape --file=$$p/$$i --export-area-drawing --without-gui --export-pdf=$$p/$$o'], + checkProg('a SVG -> PDF converter', ['rsvg-convert -f pdf -o $$o $$i', 'inkscape --file=$$i --export-area-drawing --without-gui --export-pdf=$$o'], rc_entry = [ r'\converter svg pdf "%%" ""']) # - checkProg('a SVG -> EPS converter', ['rsvg-convert -f ps -o $$o $$i', 'inkscape --file=$$p/$$i --export-area-drawing --without-gui --export-eps=$$p/$$o'], + checkProg('a SVG -> EPS converter', ['rsvg-convert -f ps -o $$o $$i', 'inkscape --file=$$i --export-area-drawing --without-gui --export-eps=$$o'], rc_entry = [ r'\converter svg eps "%%" ""']) - # the PNG export via Inkscape must not have the full path ($$p) for the file + # checkProg('a SVG -> PNG converter', ['rsvg-convert -f png -o $$o $$i', 'inkscape --without-gui --file=$$i --export-png=$$o'], rc_entry = [ r'\converter svg png "%%" ""']) - + # # gnumeric/xls/ods to tex - checkProg('a spreadsheet -> latex converter', ['ssconvert'], - rc_entry = [ r'''\converter gnumeric latex "ssconvert --export-type=Gnumeric_html:latex $$i $$o" "" -\converter ods latex "ssconvert --export-type=Gnumeric_html:latex $$i $$o" "" -\converter xls latex "ssconvert --export-type=Gnumeric_html:latex $$i $$o" ""''', -'']) + checkProg('a spreadsheet -> latex converter', ['ssconvert'], + rc_entry = [ r'''\converter gnumeric latex "ssconvert --export-type=Gnumeric_html:latex $$i $$o" "" +\converter ods latex "ssconvert --export-type=Gnumeric_html:latex $$i $$o" "" +\converter xls latex "ssconvert --export-type=Gnumeric_html:latex $$i $$o" ""''', +'']) path, lilypond = checkProg('a LilyPond -> EPS/PDF/PNG converter', ['lilypond']) if (lilypond != ''): @@ -913,7 +888,7 @@ def checkConverterEntries(): else: addToRC(r'\Format lyxgz gz "LyX Archive (tar.gz)" "" "" "" "document,menu=export"') addToRC(cmd % "lyxgz") - + # # FIXME: no rc_entry? comment it out # checkProg('Image converter', ['convert $$i $$o']) @@ -928,6 +903,7 @@ def checkConverterEntries(): \converter lyx lyx14x "python -tt $$s/lyx2lyx/lyx2lyx -t 245 $$i > $$o" "" \converter lyx lyx15x "python -tt $$s/lyx2lyx/lyx2lyx -t 276 $$i > $$o" "" \converter lyx lyx16x "python -tt $$s/lyx2lyx/lyx2lyx -t 345 $$i > $$o" "" +\converter lyx lyx20x "python -tt $$s/lyx2lyx/lyx2lyx -t 413 $$i > $$o" "" \converter lyx clyx "python -tt $$s/lyx2lyx/lyx2lyx -c big5 -t 245 $$i > $$o" "" \converter lyx jlyx "python -tt $$s/lyx2lyx/lyx2lyx -c euc_jp -t 245 $$i > $$o" "" \converter lyx klyx "python -tt $$s/lyx2lyx/lyx2lyx -c euc_kr -t 245 $$i > $$o" "" @@ -998,15 +974,15 @@ def checkOtherEntries(): def processLayoutFile(file, bool_docbook): ''' process layout file and get a line of result - + Declare lines look like this: (article.layout, scrbook.layout, svjog.layout) - + \DeclareLaTeXClass{article} \DeclareLaTeXClass[scrbook]{book (koma-script)} \DeclareLaTeXClass[svjour,svjog.clo]{article (Springer - svjour/jog)} we expect output: - + "article" "article" "article" "false" "article.cls" "scrbook" "scrbook" "book (koma-script)" "false" "scrbook.cls" "svjog" "svjour" "article (Springer - svjour/jog)" "false" "svjour.cls,svjog.clo" @@ -1041,11 +1017,11 @@ def processLayoutFile(file, bool_docbook): def checkLatexConfig(check_config, bool_docbook): - ''' Explore the LaTeX configuration + ''' Explore the LaTeX configuration Return None (will be passed to sys.exit()) for success. ''' msg = 'checking LaTeX configuration... ' - # if --without-latex-config is forced, or if there is no previous + # if --without-latex-config is forced, or if there is no previous # version of textclass.lst, re-generate a default file. if not os.path.isfile('textclass.lst') or not check_config: # remove the files only if we want to regenerate @@ -1069,7 +1045,7 @@ def checkLatexConfig(check_config, bool_docbook): for file in glob.glob( os.path.join('layouts', '*.layout') ) + \ glob.glob( os.path.join(srcdir, 'layouts', '*.layout' ) ) : # valid file? - if not os.path.isfile(file): + if not os.path.isfile(file): continue # get stuff between /xxxx.layout . classname = file.split(os.sep)[-1].split('.')[0] @@ -1088,8 +1064,8 @@ def checkLatexConfig(check_config, bool_docbook): return None # the following will generate textclass.lst.tmp, and packages.lst.tmp logger.info(msg + '\tauto') - removeFiles(['wrap_chkconfig.ltx', 'chkconfig.vars', \ - 'chkconfig.classes', 'chklayouts.tex']) + removeFiles(['chkconfig.classes', 'chkconfig.vars', 'chklayouts.tex', + 'wrap_chkconfig.ltx']) rmcopy = False if not os.path.isfile( 'chkconfig.ltx' ): shutil.copyfile( os.path.join(srcdir, 'chkconfig.ltx'), 'chkconfig.ltx' ) @@ -1142,7 +1118,7 @@ def checkLatexConfig(check_config, bool_docbook): key, val = re.sub('-', '_', line).split('=') val = val.strip() values[key] = val.strip("'") - # chk_fontenc may not exist + # chk_fontenc may not exist try: addToRC(r'\font_encoding "%s"' % values["chk_fontenc"]) except: @@ -1166,25 +1142,35 @@ def checkModulesConfig(): tx.write('''## This file declares modules and their associated definition files. ## It has been automatically generated by configure ## Use "Options/Reconfigure" if you need to update it after a -## configuration change. +## configuration change. ## "ModuleName" "filename" "Description" "Packages" "Requires" "Excludes" "Category" ''') + # build the list of available modules - foundClasses = [] + seen = [] + # note that this searches the local directory first, then the + # system directory. that way, we pick up the user's version first. for file in glob.glob( os.path.join('layouts', '*.module') ) + \ glob.glob( os.path.join(srcdir, 'layouts', '*.module' ) ) : # valid file? logger.info(file) - if not os.path.isfile(file): + if not os.path.isfile(file): + continue + + filename = file.split(os.sep)[-1] + filename = filename[:-7] + if seen.count(filename): continue - retval = processModuleFile(file, bool_docbook) + + seen.append(filename) + retval = processModuleFile(file, filename, bool_docbook) if retval != "": tx.write(retval) tx.close() logger.info('\tdone') -def processModuleFile(file, bool_docbook): +def processModuleFile(file, filename, bool_docbook): ''' process module file and get a line of result The top of a module file should look like this: @@ -1209,8 +1195,6 @@ def processModuleFile(file, bool_docbook): modname = desc = pkgs = req = excl = catgy = "" readingDescription = False descLines = [] - filename = file.split(os.sep)[-1] - filename = filename[:-7] for line in open(file).readlines(): if readingDescription: @@ -1275,7 +1259,6 @@ def processModuleFile(file, bool_docbook): cm.close() return '"%s" "%s" "%s" "%s" "%s" "%s" "%s"\n' % (modname, filename, desc, pkgs, req, excl, catgy) - def checkTeXAllowSpaces(): @@ -1285,7 +1268,7 @@ def checkTeXAllowSpaces(): msg = "Checking whether TeX allows spaces in file names... " writeToFile('a b.tex', r'\message{working^^J}' ) if LATEX != '': - if os.name == 'nt': + if os.name == 'nt' or sys.platform == 'cygwin': latex_out = cmdOutput(LATEX + r""" "\nonstopmode\input{\"a b\"}" """) else: latex_out = cmdOutput(LATEX + r""" '\nonstopmode\input{"a b"}' """) @@ -1304,15 +1287,15 @@ def checkTeXAllowSpaces(): def removeTempFiles(): # Final clean-up if not lyx_keep_temps: - removeFiles(['chkconfig.vars', \ - 'wrap_chkconfig.ltx', 'wrap_chkconfig.log', \ - 'chklayouts.tex', 'chkmodules.tex', 'missfont.log', - 'chklatex.ltx', 'chklatex.log']) + removeFiles(['chkconfig.vars', 'chklatex.ltx', 'chklatex.log', + 'chklayouts.tex', 'chkmodules.tex', 'missfont.log', + 'wrap_chkconfig.ltx', 'wrap_chkconfig.log']) if __name__ == '__main__': lyx_check_config = True outfile = 'lyxrc.defaults' + lyxrc_fileformat = 1 rc_entries = '' lyx_keep_temps = False version_suffix = '' @@ -1351,10 +1334,13 @@ Options: writeToFile(outfile, '''# This file has been automatically generated by LyX' lib/configure.py # script. It contains default settings that have been determined by # examining your system. PLEASE DO NOT MODIFY ANYTHING HERE! If you -# want to customize LyX, use LyX' Preferences dialog or modify directly +# want to customize LyX, use LyX' Preferences dialog or modify directly # the "preferences" file instead. Any setting in that file will # override the values given here. -''') + +Format %i + +''' % lyxrc_fileformat) # check latex LATEX = checkLatex(dtl_tools) checkFormatEntries(dtl_tools)