#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 1998-2026 Stephane Galland # # This program is free library; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or any later version. # # This library is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; see the file COPYING. If not, # write to the Free Software Foundation, Inc., 59 Temple Place - Suite # 330, Boston, MA 02111-1307, USA. from dataclasses import dataclass import logging import os import re import sys import textwrap from collections import deque from typing import Any, override, Type from autolatex2.config.configobj import Config from autolatex2.make.abstractmaker import TeXMaker from autolatex2.make.abstractbuilder import Builder from autolatex2.make.filedescription import FileDescription from autolatex2.make.make_enums import TeXTools, TeXCompiler, BibCompiler, IndexCompiler, GlossaryCompiler from autolatex2.make.stamps import StampManager from autolatex2.tex.biber import BiberErrorParser from autolatex2.tex.bibtex import BibTeXErrorParser from autolatex2.tex.citationanalyzer import AuxiliaryCitationAnalyzer from autolatex2.tex.dependencyanalyzer import DependencyAnalyzer import autolatex2.tex.utils as texutils from autolatex2.tex.texlogparser import TeXLogParser, TeXWarnings, DetailedTeXWarning from autolatex2.tex.utils import FileType from autolatex2.translator.translatorrepository import TranslatorRepository from autolatex2.translator.translatorrunner import TranslatorRunner import autolatex2.utils.extlogging as extlogging from autolatex2.utils.extlogging import LogLevel from autolatex2.utils.runner import Runner, ScriptOutput from autolatex2.utils import extprint import autolatex2.utils.utilfunctions as genutils from autolatex2.utils.i18n import T @dataclass class BuilderFactory: """ Class for keeping track of all information about a builder. """ builder_output : FileType builder_type: Type[Builder] builder_instance: Builder|None = None def builder(self, configuration : Config) -> Builder: """ Replies the builder. :param configuration: the configuration to pass to the builder. :type configuration: Config :return: the builder :rtype: Builder """ if self.builder_instance is None: self.builder_instance = self.builder_type(configuration) assert self.builder_instance is not None return self.builder_instance # noinspection DuplicatedCode class AutoLaTeXMaker(TeXMaker): """ The maker for the program. """ __EXTENDED_WARNING_CODE : str = textwrap.dedent("""\ %************************************************************* % CODE ADDED BY AUTOLATEX TO CHANGE THE OUPUT OF THE WARNINGS %************************************************************* \\makeatletter \\newcount\\autolatex@@@lineno \\newcount\\autolatex@@@lineno@delta \\xdef\\autolatex@@@mainfile@real{::::REALFILENAME::::} \\def\\autolatex@@@mainfile{autolatex_autogenerated.tex} \\xdef\\autolatex@@@filename@stack{{\\autolatex@@@mainfile}{\\autolatex@@@mainfile}} \\global\\let\\autolatex@@@currentfile\\autolatex@@@mainfile \\def\\autolatex@@@filename@stack@push#1{% \\xdef\\autolatex@@@filename@stack{{#1}\\autolatex@@@filename@stack}% } \\def\\autolatex@@@filename@stack@pop@split#1#2\\@nil{% \\gdef\\autolatex@@@currentfile{#1}% \\gdef\\autolatex@@@filename@stack{#2}% } \\def\\autolatex@@@filename@stack@pop{% \\expandafter\\autolatex@@@filename@stack@pop@split\\autolatex@@@filename@stack\\@nil} \\def\\autolatex@@@update@filename{% \\ifx\\autolatex@@@mainfile\\autolatex@@@currentfile% \\edef\\autolatex@@@warning@filename{\\autolatex@@@mainfile@real}% \\global\\autolatex@@@lineno@delta=::::AUTOLATEXHEADERSIZE::::\\relax% \\else% \\edef\\autolatex@@@warning@filename{\\autolatex@@@currentfile}% \\global\\autolatex@@@lineno@delta=0\\relax% \\fi% {\\filename@parse{\\autolatex@@@warning@filename}\\global\\let\\autolatex@@@filename@ext\\filename@ext}% \\xdef\\autolatex@@@generic@warning@beginmessage{!!!![BeginWarning]\\autolatex@@@warning@filename:\\ifx\\autolatex@@@filename@ext\\relax.tex\\fi:}% \\xdef\\autolatex@@@generic@warning@endmessage{!!!![EndWarning]\\autolatex@@@warning@filename}% } \\def\\autolatex@@@openfile#1{% \\expandafter\\autolatex@@@filename@stack@push{\\autolatex@@@currentfile}% \\xdef\\autolatex@@@currentfile{#1}% \\autolatex@@@update@filename% } \\def\\autolatex@@@closefile{% \\autolatex@@@filename@stack@pop% \\autolatex@@@update@filename% } \\let\\autolatex@@@InputIfFileExists\\InputIfFileExists \\long\\def\\InputIfFileExists#1#2#3{% \\autolatex@@@openfile{#1}% \\autolatex@@@InputIfFileExists{#1}{#2}{#3}% \\autolatex@@@closefile% } \\let\\autolatex@@@input\\@input \\long\\def\\@input#1{% \\autolatex@@@openfile{#1}% \\autolatex@@@input{#1}% \\autolatex@@@closefile% } \\global\\DeclareRobustCommand{\\GenericWarning}[2]{% \\global\\autolatex@@@lineno\\inputlineno\\relax% \\global\\advance\\autolatex@@@lineno\\autolatex@@@lineno@delta\\relax% \\begingroup \\def\\MessageBreak{^^J#1}% \\set@display@protect \\immediate\\write\\@unused{^^J\\autolatex@@@generic@warning@beginmessage\\the\\autolatex@@@lineno: #2\\on@line.^^J\\autolatex@@@generic@warning@endmessage^^J}% \\endgroup } \\autolatex@@@update@filename \\makeatother %************************************************************* """) __COMMAND_DEFINITIONS : dict[int,dict[str,str|list[str]|None]] = { TeXTools.pdflatex.value: { 'cmd': 'pdflatex', 'flags': ['-halt-on-error', '-interaction', 'batchmode', '-file-line-error'], 'to_dvi': ['-output-format=dvi'], 'to_ps': None, 'to_pdf': ['-output-format=pdf'], 'synctex': '-synctex=1', 'jobname': '-jobname', 'output_dir': '-output-directory', 'ewarnings': __EXTENDED_WARNING_CODE, 'utf8': [], }, TeXTools.latex.value: { 'cmd': 'latex', 'flags': ['-halt-on-error', '-interaction', 'batchmode', '-file-line-error'], 'to_dvi': ['-output-format=dvi'], 'to_ps': None, 'to_pdf': ['-output-format=pdf'], 'synctex': '-synctex=1', 'jobname': '-jobname', 'output_dir': '-output-directory', 'ewarnings': __EXTENDED_WARNING_CODE, 'utf8': [], }, TeXTools.xelatex.value: { 'cmd': 'xelatex', 'flags': ['-halt-on-error', '-interaction', 'batchmode', '-file-line-error'], 'to_dvi': ['-no-pdf'], 'to_ps': None, 'to_pdf': [], 'synctex': '-synctex=1', 'jobname': '-jobname', 'output_dir': '-output-directory', 'ewarnings': __EXTENDED_WARNING_CODE, 'utf8': [], }, TeXTools.lualatex.value: { 'cmd': 'luatex', 'flags': ['-halt-on-error', '-interaction', 'batchmode', '-file-line-error'], 'to_dvi': ['-output-format=dvi'], 'to_ps': None, 'to_pdf': ['-output-format=pdf'], 'synctex': '-synctex=1', 'jobname': '-jobname', 'output_dir': '-output-directory', 'ewarnings': __EXTENDED_WARNING_CODE, 'utf8': [], }, TeXTools.bibtex.value: { 'cmd': 'bibtex', 'flags': [], 'utf8': [], }, TeXTools.biber.value: { 'cmd': 'biber', 'flags': [], 'utf8': [], }, TeXTools.makeindex.value: { 'cmd': 'makeindex', 'flags': [], 'index_style_flag': '-s', 'utf8': [], }, TeXTools.texindy.value: { 'cmd': 'texindy', 'flags': [], 'index_style_flag': '', 'utf8': ['-C', 'utf8'], }, TeXTools.makeglossaries.value: { 'cmd': 'makeglossaries', 'flags': [], 'glossary_style_flag': '-s', 'utf8': [], }, TeXTools.dvips.value: { 'cmd': 'dvips', 'flags': [], 'output':'-o', 'utf8': [], }, } def __init__(self, translator_runner : TranslatorRunner): """ Construct the make of translators. :param translator_runner: The runner of translators. :type translator_runner: TranslatorRunner """ self.__root_files : set[str] = set() self.__files : dict[str,FileDescription] = dict() self.__stamps : StampManager = StampManager() self.__standards_warnings : set[TeXWarnings] = set() self.__detailed_warnings : list[DetailedTeXWarning] = list() self.translator_runner : TranslatorRunner = translator_runner if self.translator_runner is None: self.__configuration : Config = Config() else: self.__configuration : Config = translator_runner.configuration # Initialization of the compiler definitions and the command-line options are # differed to the "__internal_register_commands" method factory self.__instance_compiler_definition : dict[str,str|list[str]|None] | None = None # Initialization of the builders self.__registered_builders : dict[FileType,BuilderFactory] = AutoLaTeXMaker.build_builder_dict('autolatex2.make.builders') # Initialize fields by resetting them self.reset() @property def registered_builders(self) -> dict[FileType,BuilderFactory]: """ Replies the registered builders. :return: the mapping from the output file type to the lambda expression that permits to create a builder for this file type. :rtype: dict[FileType,BuilderFactory] """ return self.__registered_builders @property @override def configuration(self) -> Config: return self.__configuration @property @override def stamp_manager(self) -> StampManager: """ Replies the manager of internal stamps. :rtype: StampManager """ return self.__stamps @staticmethod def create(configuration : Config) -> 'AutoLaTeXMaker': """ Static factory method for creating an instance of AutoLaTeXMaker with the "standard" building method. :param configuration: the configuration to use. :param configuration: Config :return: the instance of the maker :rtype: AutoaTeXMaker """ # Create the translator repository repository = TranslatorRepository(configuration) # Create the runner of translators runner = TranslatorRunner(repository) # Create the general maker maker = AutoLaTeXMaker(runner) # Set the maker from the configuration ddir = configuration.document_directory document_file = configuration.document_filename if ddir: fn = os.path.join(ddir, document_file) else: fn = document_file maker.add_root_file(fn) return maker def reset_commands(self): """ Reset the external tool commands and rebuild them from the current configuration. """ self.__instance_compiler_definition = None # noinspection PyTypeChecker def __internal_register_commands(self): """ Build the different commands according to the current configuration. This method should not be called from outside the class. It is based on the method factory design pattern. """ if self.__instance_compiler_definition is None: encoding = sys.getdefaultencoding() is_utf8_system = encoding.lower() == 'utf-8' compiler_num = -1 compiler = self.configuration.generation.latex_compiler if compiler is None: compiler_num = TeXTools.pdflatex.value if self.configuration.generation.pdf_mode else TeXTools.latex.value compiler = AutoLaTeXMaker.__COMMAND_DEFINITIONS[compiler_num]['cmd'] elif TeXCompiler[compiler]: compiler_definition = TeXCompiler[compiler] if compiler_definition is not None: compiler_num = TeXCompiler[compiler].value if compiler_num in AutoLaTeXMaker.__COMMAND_DEFINITIONS: self.__instance_compiler_definition = AutoLaTeXMaker.__COMMAND_DEFINITIONS[compiler_num].copy() else: self.__instance_compiler_definition = None if not self.__instance_compiler_definition: raise Exception(T("Cannot find a definition of the command line for the LaTeX compiler '%s'") % compiler) out_type = 'pdf' if self.configuration.generation.pdf_mode else 'ps' # LaTeX self.__latex_cli : list[str] = list() if self.configuration.generation.latex_cli: self.__latex_cli.extend(self.configuration.generation.latex_cli) else: self.__latex_cli.append(self.__instance_compiler_definition['cmd']) self.__latex_cli.extend(self.__instance_compiler_definition['flags']) if is_utf8_system and 'utf8' in self.__instance_compiler_definition and self.__instance_compiler_definition['utf8']: self.__latex_cli.extend(self.__instance_compiler_definition['utf8']) if ('to_%s' % out_type) not in self.__instance_compiler_definition: raise Exception(T("No command definition for '%s/%s'") % (compiler, out_type)) # Support of SyncTeX if self.configuration.generation.synctex and self.__instance_compiler_definition['synctex']: if isinstance(self.__instance_compiler_definition['synctex'], list): self.__latex_cli.extend(self.__instance_compiler_definition['synctex']) else: self.__latex_cli.append(self.__instance_compiler_definition['synctex']) target = self.__instance_compiler_definition['to_%s' % out_type] if target: if isinstance(target, list): self.__latex_cli.extend(target) else: self.__latex_cli.append(target) elif out_type == 'ps': if isinstance(self.__instance_compiler_definition['to_dvi'], list): self.__latex_cli.extend(self.__instance_compiler_definition['to_dvi']) else: self.__latex_cli.append(self.__instance_compiler_definition['to_dvi']) else: raise Exception(T('Invalid maker state: cannot find the command line to compile TeX files.')) if self.configuration.generation.latex_flags: self.__latex_cli.extend(self.configuration.generation.latex_flags) # BibTeX self.__bibtex_cli = list() if self.configuration.generation.bibtex_cli: self.__bibtex_cli.extend(self.configuration.generation.bibtex_cli) else: cmd = AutoLaTeXMaker.__COMMAND_DEFINITIONS[BibCompiler.bibtex.value] if not cmd: raise Exception(T("No command definition for 'bibtex'")) self.__bibtex_cli.append(cmd['cmd']) self.__bibtex_cli.extend(cmd['flags']) if is_utf8_system and 'utf8' in cmd and cmd['utf8']: self.__bibtex_cli.extend(cmd['utf8']) if self.configuration.generation.bibtex_flags: self.__bibtex_cli.extend(self.configuration.generation.bibtex_flags) # Biber self.__biber_cli = list() if self.configuration.generation.biber_cli: self.__biber_cli.extend(self.configuration.generation.biber_cli) else: cmd = AutoLaTeXMaker.__COMMAND_DEFINITIONS[BibCompiler.biber.value] if not cmd: raise Exception(T("No command definition for 'biber'")) self.__biber_cli.append(cmd['cmd']) self.__biber_cli.extend(cmd['flags']) if is_utf8_system and 'utf8' in cmd and cmd['utf8']: self.__biber_cli.extend(cmd['utf8']) if self.configuration.generation.biber_flags: self.__biber_cli.extend(self.configuration.generation.biber_flags) # MakeIndex self.__makeindex_cli = list() if self.configuration.generation.makeindex_cli: self.__makeindex_cli.extend(self.configuration.generation.makeindex_cli) else: cmd = AutoLaTeXMaker.__COMMAND_DEFINITIONS[IndexCompiler.makeindex.value] if not cmd: raise Exception(T("No command definition for 'makeindex'")) self.__makeindex_cli.append(cmd['cmd']) self.__makeindex_cli.extend(cmd['flags']) if is_utf8_system and 'utf8' in cmd and cmd['utf8']: self.__makeindex_cli.extend(cmd['utf8']) if self.configuration.generation.makeindex_flags: self.__makeindex_cli.extend(self.configuration.generation.makeindex_flags) # texindy self.__texindy_cli = list() if self.configuration.generation.texindy_cli: self.__texindy_cli.extend(self.configuration.generation.texindy_cli) else: cmd = AutoLaTeXMaker.__COMMAND_DEFINITIONS[IndexCompiler.texindy.value] if not cmd: raise Exception(T("No command definition for 'texindy'")) self.__texindy_cli.append(cmd['cmd']) self.__texindy_cli.extend(cmd['flags']) if is_utf8_system and 'utf8' in cmd and cmd['utf8']: self.__texindy_cli.extend(cmd['utf8']) if self.configuration.generation.texindy_flags: self.__texindy_cli.extend(self.configuration.generation.texindy_flags) # MakeGlossaries self.__makeglossaries_cli = list() if self.configuration.generation.makeglossary_cli: self.__makeglossaries_cli.extend(self.configuration.generation.makeglossary_cli) else: cmd = AutoLaTeXMaker.__COMMAND_DEFINITIONS[GlossaryCompiler.makeglossaries.value] if not cmd: raise Exception(T("No command definition for 'makeglossaries'")) self.__makeglossaries_cli.append(cmd['cmd']) self.__makeglossaries_cli.extend(cmd['flags']) if is_utf8_system and 'utf8' in cmd and cmd['utf8']: self.__makeglossaries_cli.extend(cmd['utf8']) if self.configuration.generation.makeglossary_flags: self.__makeglossaries_cli.extend(self.configuration.generation.makeglossary_flags) # dvips self.__dvips_cli = list() if self.configuration.generation.dvips_cli: self.__dvips_cli.extend(self.configuration.generation.dvips_cli) else: cmd = AutoLaTeXMaker.__COMMAND_DEFINITIONS[TeXTools.dvips.value] if not cmd: raise Exception(T("No command definition for 'dvips'")) self.__dvips_cli.append(cmd['cmd']) self.__dvips_cli.extend(cmd['flags']) if is_utf8_system and 'utf8' in cmd and cmd['utf8']: self.__dvips_cli.extend(cmd['utf8']) if self.configuration.generation.dvips_flags: self.__dvips_cli.extend(self.configuration.generation.dvips_flags) # Support of extended warnings if self.configuration.generation.extended_warnings and 'ewarnings' in self.__instance_compiler_definition and self.__instance_compiler_definition['ewarnings']: code = str(self.__instance_compiler_definition['ewarnings']).strip() s = str(-(code.count('\n') + 1)) code = code.replace('::::AUTOLATEXHEADERSIZE::::', s) self.__latex_warning_code = code self.__is_extended_warning_enable = True else: self.__latex_warning_code = '' self.__is_extended_warning_enable = False def reset(self): """ Reset the maker. """ self.__root_files = set() self.__reset_warnings() self.__reset_process_data() def __reset_process_data(self): """ Reset the processing data. """ self.__files = dict() self.stamp_manager.reset() def __reset_warnings(self): """ Reset the lists of warnings. """ self.__standards_warnings : set[TeXWarnings] = set() self.__detailed_warnings : list[DetailedTeXWarning] = list() @property def compiler_definition(self) -> dict[str,Any]: """ The definition of the LaTeX compiler that must be used by this maker. :rtype: dict[str,Any] """ self.__internal_register_commands() assert self.__instance_compiler_definition is not None return self.__instance_compiler_definition @property def detailed_warnings_enabled(self) -> bool: """ Replies if the detailed warnings are supported by the TeX compiler. :rtype: bool """ self.__internal_register_commands() return self.__is_extended_warning_enable @property def extended_warnings_code(self) -> str: """ Replies the TeX code that permits to output the extended warnings. :rtype: str """ self.__internal_register_commands() return self.__latex_warning_code @property def latex_cli(self) -> list[str]: """ The command-line that is used for running the LaTeX tool. :rtype: list[str] """ self.__internal_register_commands() return self.__latex_cli @property def bibtex_cli(self) -> list[str]: """ The command-line that is used for running the BibTeX tool. :rtype: list[str] """ self.__internal_register_commands() return self.__bibtex_cli @property def biber_cli(self) -> list[str]: """ The command-line that is used for running the Biber tool. :rtype: list[str] """ self.__internal_register_commands() return self.__biber_cli @property def makeindex_cli(self) -> list[str]: """ The command-line that is used for running the makeindex tool. :rtype: list[str] """ self.__internal_register_commands() return self.__makeindex_cli @property def texindy_cli(self) -> list[str]: """ The command-line that is used for running the texindy tool. :rtype: list[str] """ self.__internal_register_commands() return self.__texindy_cli @property def makeglossaries_cli(self) -> list[str]: """ The command-line that is used for running the makeglossaries tool. :rtype: list[str] """ self.__internal_register_commands() return self.__makeglossaries_cli @property def dvips_cli(self) -> list[str]: """ The command-line that is used for running the dvips tool. :rtype: list[str] """ self.__internal_register_commands() return self.__dvips_cli @property def root_files(self) -> set[str]: """ The root files that are involved within the lastest compilation process. :rtype: set[str] """ return self.__root_files def add_root_file(self, filename : str): """ Add root file. :param filename: The name of the root file. :type filename: str """ self.__root_files.add(filename) @property def files(self) -> dict[str,FileDescription]: """ The files that are involved within the lastest compilation process. :rtype: dict[str,FileDescription] """ return self.__files @property def standard_warnings(self) -> set[TeXWarnings]: """ The standard LaTeX warnings that are discovered during the lastest compilation process. :rtype: set[str] """ return self.__standards_warnings @property def detailed_warnings(self) -> list[DetailedTeXWarning]: """ The detailed warnings that are discovered during the lastest compilation process. :rtype: list[DetailedTeXWarning] """ return self.__detailed_warnings @override def run_latex(self, filename : str, loop : bool = False, extra_run_support : bool = False) -> int: """ Launch the LaTeX tool and return the number of times the tool was launched. :param filename: The name TeX file to compile. :type filename: str :param loop: Indicates if this function may loop on the LaTeX compilation when it is requested by the LaTeX tool. Default value: False. :type loop: bool :param extra_run_support: Indicates if this function may apply an additional run of LaTeX tool because a tool used in TeX file does not provide accurate log message, and needs to have an extra LaTeX run to solves the problem. This is the case of Multibib for example. This argument is considered only if the argument "loop" is True; Otherwise, it is ignored. Default is False. :type extra_run_support: bool :return: The number of times the latex tool was run. :rtype: int """ self.__internal_register_commands() if filename in self.__files: mfn = self.__files[filename].main_filename if mfn is not None and mfn != '': filename = mfn log_file = FileType.log.ensure_extension(filename) log_parser = TeXLogParser(log_file=log_file) nb_runs = 0 # This is a do-while implementation while True: logging.info(T('LATEX: %s') % os.path.basename(filename)) self.__reset_warnings() if os.path.isfile(log_file): os.remove(log_file) command_output = None continue_to_compile = False if self.detailed_warnings_enabled: with open(filename, "r") as f: content = f.readlines() autofile = texutils.create_extended_tex_filename(filename) with open(autofile, "w") as f: code = self.__latex_warning_code.replace('::::REALFILENAME::::', filename) f.write(code) f.write("\n") f.write(''.join(content)) f.write("\n") try: cmd = self.__latex_cli.copy() assert self.__instance_compiler_definition is not None if 'jobname' in self.__instance_compiler_definition and self.__instance_compiler_definition['jobname']: cmd.append(str(self.__instance_compiler_definition['jobname'])) cmd.append(genutils.simple_basename(filename, *FileType.tex_extensions())) if 'output_dir' in self.__instance_compiler_definition and self.__instance_compiler_definition['output_dir'] is not None and self.__instance_compiler_definition['output_dir']: cmd.append(str(self.__instance_compiler_definition['output_dir'])) cmd.append(os.path.dirname(filename)) else: logging.warning(T('LATEX: no command-line option provided for changing the output directory')) cmd.append(autofile) cmd = Runner.normalize_command(*cmd) nb_runs += 1 logging.debug(T('Running: %s') % repr(cmd)) command_output = self.__run_cmd(*cmd) logging.debug(T('Run finished')) finally: genutils.unlink(autofile) else: cmd = self.__latex_cli.copy() cmd.append(os.path.relpath(filename)) cmd = Runner.normalize_command(*cmd) nb_runs += 1 logging.debug(T('Running: %s') % repr(cmd)) command_output = self.__run_cmd(*cmd) logging.debug(T('Run finished')) if command_output is not None and command_output.return_code != 0: logging.debug(T("LATEX: Error when processing %s") % os.path.basename(filename)) # Parse the log to extract the blocks of messages if os.path.isfile(log_file): fatal_error, log_blocks = log_parser.extract_failure() else: raise Exception(T("Log file not found: %s" % log_file)) # Display the message if fatal_error: logging.debug(T("LATEX: The first error found in the log file is:")) extlogging.multiline_error(fatal_error) logging.debug(T("LATEX: End of error log.")) else: logging.error(T("LATEX: Unable to extract the error from the log. Please read the log file.")) raise Exception(T("LaTeX compiler fail. See log file for details")) elif not os.path.isfile(log_file): raise Exception(T("Log file not found: %s" % log_file)) else: continue_to_compile = log_parser.extract_warnings(enable_loop=loop, enable_detailed_warnings=self.detailed_warnings_enabled, standards_warnings=self.__standards_warnings, detailed_warnings=self.__detailed_warnings) logging.debug(T('Detection of rebuild: %s') % (str(continue_to_compile))) # Stoping condition for the do-while loop if not continue_to_compile: # Special case of Multibib that may not output the "Re-run" warning message when it has missed citations. if loop and extra_run_support and (TeXWarnings.undefined_reference in self.standard_warnings or TeXWarnings.undefined_citation in self.standard_warnings): # Disable the Multibib support because it is needed to compile only once for solving # the missed citations from Multibib extra_run_support = False continue # Stop the do-while loop break return nb_runs # noinspection PyMethodMayBeStatic def __run_cmd(self, *cmd) -> ScriptOutput: """ Run the given command and show up the standard output if it is in debug mode. :param cmd: The command to run. :type cmd: str array :return: An output containing the standard output, the error output, and the exception, the return code. :rtype: ScriptOutput """ if logging.getLogger().isEnabledFor(logging.DEBUG): exit_code = 0 sex = None sout = '' serr = '' try: exit_code = Runner.run_command_without_redirect(*cmd) except Exception as ex: sex = ex return ScriptOutput(standard_output=sout, error_output=serr, exception=sex, return_code=exit_code) else: return Runner.run_command(*cmd) # noinspection PyMethodMayBeStatic,PyBroadException def __select_aux_file(self, filename : str) -> bool: try: with open(filename, 'r') as f: line = f.readline() expr = re.compile(r'\\(?:abx@aux@cite|citation|bibcite)', re.S) while line: if expr.search(line): return True line = f.readline() except: pass return False def detect_aux_files_with_biliography(self, filename : str, check_aux_content : bool = True) -> list[str]: """ Explore the document folder and subfolders for finding auxiliary files that contains bibliographical citations. :param filename: The name TeX file to compile. :type filename: str :param check_aux_content: Indicates if this function has to read the auxiliary files to determine if a citation is inside. Then, if it is the case, the auxiliary file is passed to the bibliography tool; Otherwise it is ignored. Default is: True. :type check_aux_content: bool :return: The list of auxiliary files :rtype: list[str] """ if check_aux_content: aux_file_list = texutils.find_aux_files(filename, self.__select_aux_file) else: aux_file_list = texutils.find_aux_files(filename) if not aux_file_list: aux_file = FileType.aux.ensure_extension(filename) aux_file_list.append(aux_file) return aux_file_list @override def run_bibtex(self, filename : str, check_aux_content : bool = True) -> dict[str,Any] | None: """ Launch the BibTeX tool (BibTeX, Biber, etc.) once time and replies a dictionary that describes any error. The returned dictionary has the keys: filename, lineno and message. This function also supports the document with zro, one or more bibliography sections, such a those introduced by the LaTeX package 'bibunits'. :param filename: The name of the auxiliary file or the root TeX file to use as input for the bibliography tool. :type filename: str :param check_aux_content: Indicates if this function has to read the auxiliary files to determine if a citation is inside. Then, if it is the case, the auxiliary file is passed to the bibliography tool; Otherwise it is ignored. Default is: True. :type check_aux_content: bool :return: the error result, or None if there is no error. :rtype: dict[str,Any] | None """ self.__internal_register_commands() self.__reset_warnings() if FileType.aux.is_file(filename): # The input filename is an auxiliary file, that is the standard type of file for BibTeX. aux_file_list = [ filename ] else: if filename in self.__files: mfn = self.__files[filename].main_filename if mfn is not None and mfn != '': filename = mfn aux_file_list = self.detect_aux_files_with_biliography(filename, check_aux_content) for aux_file in aux_file_list: if self.configuration.generation.is_biber: # Remove the file extension because Biber does not support it properly from the CLI aux_file = genutils.basename_with_path(aux_file, *FileType.aux.extensions()) logging.info(T('BIBER: %s') % os.path.basename(aux_file)) cmd = self.__biber_cli.copy() else: logging.info(T('BIBTEX: %s') % os.path.basename(aux_file)) cmd = self.__bibtex_cli.copy() cmd.append(os.path.relpath(aux_file)) cmd = Runner.normalize_command(*cmd) if self.configuration.generation.is_biber: logging.debug(T('BIBER: Command line is: %s') % ' '.join(cmd)) else: logging.debug(T('BIBTEX: Command line is: %s') % ' '.join(cmd)) command_output = Runner.run_command(*cmd) if command_output.return_code != 0: if self.configuration.generation.is_biber: logging.debug(T('BIBER: error when processing %s') % os.path.basename(aux_file)) else: logging.debug(T('BIBTEX: error when processing %s') % os.path.basename(aux_file)) log = command_output.standard_output if not log: log = command_output.error_output if log: if self.configuration.generation.is_biber: log_parser = BiberErrorParser() else: log_parser = BibTeXErrorParser() current_error = log_parser.parse_log(aux_file, log) if current_error: return current_error current_error = {'filename': aux_file, 'lineno': 0, 'message': command_output.standard_output + "\n" + command_output.error_output} return current_error return None @override def run_makeindex(self, filename : str) -> ScriptOutput | None: """ Launch the MakeIndex tool once time. The success status if the run of MakeIndex is replied. :param filename: The filename of the index file to compile. :type filename: str :return: None on success; Otherwise a tuple with the exit code and the standard and error outputs from the Makeindex tool. :rtype: tuple[int,str,str] | None """ self.__internal_register_commands() idx_file = FileType.idx.ensure_extension(filename) logging.info(T('MAKEINDEX: %s') % os.path.basename(idx_file)) self.__reset_warnings() if self.configuration.generation.is_xindy_index: cmd = self.__texindy_cli.copy() cmd_def = AutoLaTeXMaker.__COMMAND_DEFINITIONS[IndexCompiler.texindy.value] else: cmd = self.__makeindex_cli.copy() cmd_def = AutoLaTeXMaker.__COMMAND_DEFINITIONS[IndexCompiler.makeindex.value] if cmd_def and 'index_style_flag' in cmd_def and cmd_def['index_style_flag']: ist_file = self.configuration.generation.makeindex_style_filename if ist_file: cmd.append(cmd_def['index_style_flag']) cmd.append(os.path.relpath(ist_file)) cmd.append(os.path.relpath(idx_file)) cmd = Runner.normalize_command(*cmd) command_output = Runner.run_command(*cmd) if command_output.return_code != 0: logging.error(T("%s\n%s") % (command_output.standard_output, command_output.error_output)) return command_output return None @override def run_makeglossaries(self, filename : str) -> bool: """ Launch the MakeGlossaries tool once time. The success status if the run of MakeGlossaries is replied. :param filename: The filename of the TeX file to compile. :type filename: str :return: True to continue the process. False to stop. :rtype: bool """ self.__internal_register_commands() tex_wo_ext = genutils.basename_with_path(filename, *FileType.tex_extensions()) gls_file = FileType.glo.ensure_extension(filename) logging.info(T('MAKEGLOSSARIES: %s') % (os.path.basename(gls_file))) self.__reset_warnings() cmd = self.__makeglossaries_cli.copy() ist_file = self.configuration.generation.makeindex_style_filename if ist_file: cmd_def = AutoLaTeXMaker.__COMMAND_DEFINITIONS[GlossaryCompiler.makeglossaries.value] if not cmd_def: raise Exception(T("No command definition for 'makeglossaries'")) cmd.append(cmd_def['glossary_style_flag']) cmd.append(os.path.relpath(ist_file)) cmd.append(os.path.relpath(tex_wo_ext)) cmd = Runner.normalize_command(*cmd) command_output = Runner.run_command(*cmd) return command_output.return_code == 0 def run_dvips(self, filename : str) -> dict[str,Any] | None: """ Launch the tool for converting a DVI file to a Postscript-based file. Replies the description of the current error. :param filename: The name dvi file to convert. :type filename: str :return: None on success; Otherwise a dict with the exit code and the standard and error outputs from the dvips tool. :rtype: dict[str,Any] | None """ self.__internal_register_commands() logging.info(T('DVIPS: %s') % os.path.basename(filename)) if filename in self.__files: mfn = self.__files[filename].main_filename if mfn is not None and mfn != '': filename = mfn output = FileType.ps.ensure_extension(filename) cmd_def = AutoLaTeXMaker.__COMMAND_DEFINITIONS[TeXTools.dvips.value] self.__reset_warnings() cmd = self.__dvips_cli.copy() cmd.append(cmd_def['output']) cmd.append(os.path.relpath(output)) cmd.append(os.path.relpath(filename)) cmd = Runner.normalize_command(*cmd) command_output = Runner.run_command(*cmd) if command_output.return_code != 0: logging.debug(T('DVIPS: error when processing %s') % (os.path.basename(filename))) current_error = {'filename': filename, 'lineno': 0, 'message': command_output.standard_output + "\n" + command_output.error_output} return current_error return None def __create_file_description(self, output_file : str, output_type : FileType, input_filename : str, main_filename : str | None, use_xindy: bool = False, use_biber: bool = False, use_multibib: bool = False, use_bibunits: bool = False) -> bool: """ Create an entry into the list of files involved into the execution process. :param output_file: The name of the output file. :type output_file: str :param output_type: The type of the file, 'pdf' or 'ps'. :type output_type: str :param input_filename: The name of the input file. :type input_filename: str :param main_filename: The name of the main file associated to this file in the process. If it is None, the current file is the main file itself. :type main_filename: str | None :param use_xindy: Indicates if Xindy must be used for building the index. Default is False. :type use_xindy: bool :param use_biber: Indicates if Biber must be used for building the bibliography. Default is False. :type use_biber: bool :param use_multibib: Indicates if Multibib must be used for building the bibliography. Default is False. :type use_multibib: bool :param use_bibunits: Indicates if Bibunits must be used for building the bibliography. Default is False. :type use_bibunits: bool :return: True if the list of known dependencies has been changed :rtype: bool """ if output_file not in self.__files: desc = FileDescription(output_filename = output_file, file_type = output_type, input_filename = input_filename, main_filename = main_filename) self.__files[output_file] = desc self.__files[output_file].use_xindy = use_xindy self.__files[output_file].use_biber = use_biber self.__files[output_file].use_multibib = use_multibib self.__files[output_file].use_bibunits = use_bibunits return True return False # noinspection DuplicatedCode def __compute_tex_dependencies(self, tex_root_filename : str, root_dir : str, pdf_filename : str) -> bool: """ Build the dependency tree for the given TeX file. Replies if the dependencies have been changed. :param tex_root_filename: The root TeX filename. :type tex_root_filename: str :param root_dir: The name of the root directory. :type root_dir: str :param pdf_filename: The name of the PDF file to generate. :type pdf_filename: str :return: True if the dependency tree has changed. :rtype: bool """ tex_files : deque[str] = deque() tex_files.append(tex_root_filename) changed = False while tex_files: tex_file = tex_files.popleft() assert tex_file is not None if os.path.isfile(tex_file): logging.debug(T("Computing dependencies for %s") % tex_file) analyzer = DependencyAnalyzer(tex_file, root_dir, tex_root_filename, self.configuration.generation.include_extra_macros) analyzer.run() chg = self.__create_file_description(tex_file, FileType.tex, tex_file, main_filename=None if tex_file == tex_root_filename else tex_root_filename, use_xindy=analyzer.is_xindy_index, use_biber=analyzer.is_biber, use_bibunits=analyzer.is_bibunits, use_multibib=analyzer.is_multibib) # The bibliography and index flags must be propagated to the root tex file in order to be # detected outside this function if tex_file != tex_root_filename and tex_root_filename in self.__files: root_description = self.__files[tex_root_filename] if analyzer.is_bibunits: root_description.use_bibunits = True if analyzer.is_multibib: root_description.use_multibib = True if analyzer.is_biber: root_description.use_biber = True if analyzer.is_xindy_index: root_description.use_xindy = True changed = changed or chg all_dep_types = analyzer.get_dependency_types() # Treat the pure TeX files for dep_type in all_dep_types.intersection(FileType.tex_types()): deps = analyzer.get_dependencies_for_type(dep_type) for dep in deps: self.__create_file_description(dep.file_name, dep_type, dep.file_name, None if dep_type != FileType.tex or dep == tex_root_filename else tex_root_filename, use_xindy=analyzer.is_xindy_index, use_biber=analyzer.is_biber, use_bibunits=analyzer.is_bibunits, use_multibib=analyzer.is_multibib) self.__files[tex_file].dependencies.add(dep.file_name) changed = True if dep_type == FileType.tex: tex_files.append(dep.file_name) # Treat the bibliography files that are referred from the TeX code all_bbl_files = set() bibliography_dep_types = all_dep_types.intersection(FileType.bibliography_types()) for dep_type in bibliography_dep_types: deps = analyzer.get_dependencies_for_type(dep_type) if dep_type == FileType.bib: for description in deps: bib_file = description.file_name self.__create_file_description(bib_file, FileType.bib, bib_file, main_filename=tex_root_filename, use_xindy=analyzer.is_xindy_index, use_biber=analyzer.is_biber, use_bibunits=analyzer.is_bibunits, use_multibib=analyzer.is_multibib) dep_bbl_files = description.output_files if not dep_bbl_files: # The name of the BBL file is from the basename of the AUX file, # that is based on the basename of the TeX file. bbl_file = FileType.bbl.ensure_extension(tex_root_filename) dep_bbl_files = [bbl_file] for bbl_file in dep_bbl_files: bbl_file = genutils.abs_path(FileType.bbl.ensure_extension(bbl_file), os.path.dirname(bib_file)) aux_file = FileType.aux.ensure_extension(bbl_file) self.__create_file_description(bbl_file, FileType.bbl, aux_file, main_filename=tex_root_filename, use_xindy=analyzer.is_xindy_index, use_biber=analyzer.is_biber, use_bibunits=analyzer.is_bibunits, use_multibib=analyzer.is_multibib) self.__files[pdf_filename].dependencies.add(bbl_file) self.__files[bbl_file].dependencies.add(bib_file) self.__files[bbl_file].dependencies.add(tex_file) self.__files[bbl_file].use_xindy = analyzer.is_xindy_index self.__files[bbl_file].use_biber = analyzer.is_biber self.__files[bbl_file].use_multibib = analyzer.is_multibib self.__files[bbl_file].use_bibunits = analyzer.is_bibunits all_bbl_files.add(bbl_file) changed = True for dep_type in bibliography_dep_types: deps = analyzer.get_dependencies_for_type(dep_type) if dep_type != FileType.bib: for description in deps: chg = self.__create_file_description(description.filename, description.file_type, tex_root_filename, main_filename=tex_root_filename, use_xindy=analyzer.is_xindy_index, use_biber=analyzer.is_biber, use_bibunits=analyzer.is_bibunits, use_multibib=analyzer.is_multibib) changed = changed or chg for bbl_file in all_bbl_files: self.__files[bbl_file].dependencies.add(description.file_name) changed = True # Treat the index files that are referred from the TeX code if analyzer.is_makeindex: idx_file = FileType.idx.ensure_extension(tex_root_filename) self.__create_file_description(idx_file, FileType.idx, tex_root_filename, main_filename=tex_root_filename, use_xindy=analyzer.is_xindy_index, use_biber=analyzer.is_biber, use_bibunits=analyzer.is_bibunits, use_multibib=analyzer.is_multibib) self.__files[idx_file].use_xindy = analyzer.is_xindy_index self.__files[idx_file].dependencies.add(tex_file) ind_file = FileType.ind.ensure_extension(idx_file) self.__create_file_description(ind_file, FileType.ind, idx_file, main_filename=tex_root_filename, use_xindy=analyzer.is_xindy_index, use_biber=analyzer.is_biber, use_bibunits=analyzer.is_bibunits, use_multibib=analyzer.is_multibib) self.__files[ind_file].use_xindy = analyzer.is_xindy_index self.__files[ind_file].dependencies.add(idx_file) self.__files[pdf_filename].dependencies.add(ind_file) changed = True # Treat the glossaries files that are referred from the TeX code if analyzer.is_glossary: glo_file = FileType.glo.ensure_extension(tex_root_filename) self.__create_file_description( glo_file, FileType.glo, tex_root_filename, main_filename=tex_root_filename, use_xindy=analyzer.is_xindy_index, use_biber=analyzer.is_biber, use_bibunits=analyzer.is_bibunits, use_multibib=analyzer.is_multibib) self.__files[glo_file].dependencies.add(tex_file) gls_file = FileType.gls.ensure_extension(glo_file) self.__create_file_description(gls_file, FileType.gls, tex_root_filename, main_filename=tex_root_filename, use_xindy=analyzer.is_xindy_index, use_biber=analyzer.is_biber, use_bibunits=analyzer.is_bibunits, use_multibib=analyzer.is_multibib) self.__files[gls_file].dependencies.add(glo_file) self.__files[pdf_filename].dependencies.add(gls_file) changed = True return changed def __compute_aux_dependencies(self, tex_root_filename : str, root_dir : str, pdf_filename : str) -> bool: """ Build the dependency tree for the given Aux file. Replies if the dependencies have been changed. The references in the auxiliary file is related to specific bibliography systems, e.g., multibib. :param tex_root_filename: The TeX root filename. :type tex_root_filename: str :param root_dir: The name of the root directory. :type root_dir: str :param pdf_filename: The PDF root filename. :type pdf_filename: str :rtype: bool """ onlyfiles = [os.path.join(root_dir, f) for f in os.listdir(root_dir) if f.lower().endswith(FileType.aux.extension()) and os.path.isfile(os.path.join(root_dir, f))] changed = False for aux_file in onlyfiles: analyzer = AuxiliaryCitationAnalyzer(aux_file) analyzer.run() styles = analyzer.styles databases = analyzer.databases if styles: for style in styles: bst_file = os.path.abspath(style + FileType.bst.extension()) if os.path.isfile(bst_file): chg = self.__create_file_description(bst_file, FileType.bst, tex_root_filename, main_filename=tex_root_filename) changed = changed or chg for db in databases: bib_file = os.path.abspath(db) if os.path.isfile(bib_file): self.__create_file_description(bib_file, FileType.bib, db, main_filename=tex_root_filename) bbl_file = os.path.abspath(FileType.bbl.ensure_extension(bib_file)) self.__create_file_description(bbl_file, FileType.bbl, tex_root_filename, main_filename=tex_root_filename) self.__files[bbl_file].dependencies.add(bst_file) self.__files[bbl_file].dependencies.add(bib_file) self.__files[pdf_filename].dependencies.add(bbl_file) changed = True if databases: for db in databases: bib_file = os.path.abspath(db) if os.path.isfile(bib_file): self.__create_file_description(bib_file, FileType.bib, tex_root_filename, main_filename=tex_root_filename) bbl_file = FileType.bbl.ensure_extension(bib_file) self.__create_file_description(bbl_file, FileType.bbl, tex_root_filename, main_filename=tex_root_filename) self.__files[bbl_file].dependencies.add(bib_file) self.__files[pdf_filename].dependencies.add(bbl_file) changed = True return changed def compute_dependencies(self, tex_filename : str, read_aux_file : bool = True) -> tuple[str,dict[str,FileDescription]]: """ Build the dependency tree for the given TeX file. :param tex_filename: The TeX filename. :type tex_filename: str :param read_aux_file: Indicates if the auxiliary files must be read too. Default is True. :type read_aux_file: bool :return: The tuple with the root dependency file and the description of a file. :rtype: tuple[str,dict[str,FileDescription]] """ root_dir = os.path.dirname(tex_filename) out_type = FileType.pdf if self.configuration.generation.pdf_mode else FileType.ps # Add dependency for the final PDF file out_file = out_type.ensure_extension(tex_filename) self.__create_file_description(out_file, out_type, tex_filename, tex_filename) self.__files[out_file].dependencies.add(tex_filename) # TeX files self.__compute_tex_dependencies(tex_filename, root_dir, out_file) if tex_filename in self.__files: in_file = self.__files[tex_filename] self.__files[out_file].use_xindy = in_file.use_xindy self.__files[out_file].use_multibib = in_file.use_multibib self.__files[out_file].use_bibunits = in_file.use_bibunits self.__files[out_file].use_biber = in_file.use_biber # Aux files if read_aux_file: self.__compute_aux_dependencies(tex_filename, root_dir, out_file) return out_file, self.__files # noinspection PyMethodMayBeStatic @override def is_obsolete_timestamp(self, parent_timestamp : float | None, child_timestamp : float | None) -> bool: """ Detemrine if the two given timestamps correspond to an obsolete parent time stamp compared to the child one. A parent time stamp is obsolete when it is undefined or its value is older than the one of the child. :param parent_timestamp: The timestamp of the parent file. :type parent_timestamp: float | None :param child_timestamp: The timestamp of the child file. :type child_timestamp: float | None :return: True if the parent file is considered as obsolete. :rtype: bool """ if parent_timestamp is None or child_timestamp is None: return True assert parent_timestamp is not None and child_timestamp is not None return parent_timestamp < child_timestamp def __build_internal_execution_list_rec(self, root_tex_file: str, current_filename: str, dependencies: dict[str, FileDescription], builds : list[FileDescription], seen_files : set[str]) -> FileDescription: """ Build the list of files that needs to be generated in the best order. This function is checking if a builder was defined for the file. This function does not use the lasted change date of each file to determine if a build is needed. The test of recent changes is done by the builders themselves. This function must be invoked after a call to compute_dependencies(). :param root_tex_file: The LaTeX file to compile. :type root_tex_file: str :param current_filename: The current file to generate. :type current_filename: str :param dependencies: The tree of the dependencies for the root file. :type dependencies: dict[str,FileDescription] :param builds: The list of files to be built. This list is filled up by this function. :type builds: list[FileDescription] :param seen_files: Set of filenames that have been already treated by the function in another recursive call. :return: The file description of the current file. :rtype: FileDescription """ assert current_filename in dependencies and dependencies[current_filename] current_file = dependencies[current_filename] if current_filename not in seen_files: # First time the first is found seen_files.add(current_filename) if current_file.dependencies: is_source_type = current_file.file_type.is_source_type() is_buildable = False for dependency in current_file.dependencies: dependency_file = self.__build_internal_execution_list_rec(root_tex_file=root_tex_file, current_filename=dependency, dependencies=dependencies, builds=builds, seen_files=seen_files) # Propagate the timestamp if parent and child are source types if is_source_type: if dependency_file.file_type.is_source_type() \ and self.is_obsolete_timestamp(current_file.change, dependency_file.change): current_file.change = dependency_file.change elif not is_buildable: is_buildable = True if is_buildable and current_file.file_type in self.registered_builders: builds.append(current_file) return current_file def build_internal_execution_list(self, root_file : str, root_pdf_file : str, dependencies : dict[str,FileDescription], enable_initial_latex_run : bool = True) -> list[FileDescription]: """ Build the list of files that needs to be generated in the best order. For each file, a builder is defined and must be invoked. This function does not use the lasted change date of each file to determine if a build is needed. It is delegated to the builders themselves. This function must be invoked after a call to compute_dependencies(). :param root_file: The LaTeX file to compile. :type root_file: str :param root_pdf_file: The root PDF file to generate. :type root_pdf_file: str :param dependencies: The tree of the dependencies for the root file. :type dependencies: dict[str,FileDescription] :param enable_initial_latex_run: Indicates if the build list could contain the initial (La)TeX call for generating the first auxiliary files. Default is True. :type enable_initial_latex_run: bool :return: the list of files to be built. :rtype: list[FileDescription] """ builds = list() if enable_initial_latex_run: # Launch one LaTeX compilation to be sure that every auxiliary file that is expected is generated main_aux_file = FileType.aux.ensure_extension(root_file) description = FileDescription( output_filename = main_aux_file, file_type = FileType.aux, input_filename = root_file, main_filename = root_file) description.dependencies.add(root_file) builds.append(description) seen_files = set() self.__build_internal_execution_list_rec(root_file, root_pdf_file, dependencies, builds, seen_files) return builds def run_translators(self, force_generation : bool = False, detect_conflicts : bool = True) -> dict[str,str]: """ Run the image translators. Replies the list of images that is detected. The replied dict associates each source image (keys) to the generated image's filename (values) or None if no file was generated by the call to this function. :param force_generation: Indicates if the image generation is forced to be run on all the images (True) or if only the changed source images are considered for the image generation (False). Default is: False. :type force_generation: bool :param detect_conflicts: Indicates if the conflicts in translator loading is run. Default is True. :type detect_conflicts: bool :return: the dictionary that maps the source image's filename to the generated image's filename. Only the mapping for the images that are generated during this invocation of run_translators() are included in the dictionary. The images that are up-to-date are not put in the dictionary. :rtype: dict[str,str] """ self.translator_runner.sync(detect_conflicts = detect_conflicts) images = self.translator_runner.get_source_images() generated_images = dict() for img in images: generated_image = self.translator_runner.generate_image(in_file = img, only_more_recent = not force_generation) if generated_image: generated_images[img] = generated_image return generated_images # noinspection PyMethodMayBeStatic def __need_rebuild(self, root_file : str, file : FileDescription, dependencies : dict[str,FileDescription], builder : Builder) -> bool: """ Determines if a rebuild is needed for the file according to the behavior of the given builder. The builder is invoked for each registered dependencies of the file. If the builder replies that a rebuild is needed for at least one of these dependencies, then this function returns True. :param root_file: the name of the root TeX file. :type root_file: str :param file: the description of the file to be build up. :type file: FileDescription :param dependencies: list of known files in the dependency list. :type dependencies: dict[str,FileDescription] :param builder: The builder to consider. :type builder: Builder :return: True if a building is needed according to the behavior of the builder. :rtype: bool """ if builder.need_rebuild_without_dependency(current_file=file, root_tex_file=root_file, maker=self): return True if builder.consider_dependencies(): if file.dependencies: for dependency in file.dependencies: dependency_file = dependencies[dependency] if dependency in dependencies else None if dependency_file: if builder.need_rebuild_with_dependency(current_file=file, dependency_file=dependency_file, root_tex_file=root_file, maker=self): return True else: return True return False # noinspection PyMethodMayBeStatic def __launch_file_build(self, root_file : str, file : FileDescription, force_change : bool, dependencies : dict[str,FileDescription]) -> bool: """ Launch the builder for the given file. If the builder does not exist, the function returns with True. If a builder is defined, the function tests with the builder is a build is needed. If a build is not needed, it means that the output file is up-to-date and nothing appends. If a build is needed, the builder is invoked for building. :param root_file: the name of the root TeX file. :type root_file: str :param file: the description of the file to be build up. :type file: FileDescription :param force_change: Indicates if the file needs to be changed or not. :type force_change: bool :param dependencies: list of known files in the dependency list. :type dependencies: dict[str,FileDescription] :return: the continuation status of the building process. If True is returned, it means that the building process could continue; Otherwise, the building process has to stop because of an abnormal condition from the builder. :rtype: bool """ builders = self.registered_builders if builders and file.file_type in builders: builder_factory = builders[file.file_type] if builder_factory: builder = builder_factory.builder(self.configuration) if builder and (force_change or self.__need_rebuild(root_file, file, dependencies, builder)): # Build is needed continuation = builder.build(root_file=root_file, input_file=file, maker=self) # Reset the last change date of the output file to force the next builders to use # the last updated date file.reset_change() return continuation else: logging.error(T("A builder is defined for type '%s' without the definition of its internal factory" % file.file_type.name)) return False return True # noinspection PyBroadException def build(self, force_change : bool = False) -> bool: """ Launch the building process (latex*, BibTeX, Makeindex, Makeglossaries). Caution: this function does not generate the images (See run_translators function). Caution: this function may invoke multiple times the latex tool. :param force_change: Indicates if the file needs to be changed or not. :type force_change: bool :return: True to continue process. False to stop the process. """ self.__reset_process_data() for root_file in self.__root_files: root_dir = os.path.dirname(root_file) # 1. Read building stamps logging.debug(T("Reading the saved stamps")) self.stamp_manager.read_build_stamps(root_dir) # 2. Compute the dependencies of the files logging.log(LogLevel.FINE_INFO, T("Building the file dependencies")) root_dep_file, dependencies = self.compute_dependencies(root_file) extlogging.multiline_debug(T("Dependency List = %s") % repr(dependencies)) # 3. Construct the build list and launch the required builds logging.log(LogLevel.FINE_INFO, T("Building the execution list")) builds = self.build_internal_execution_list(root_file, root_dep_file, dependencies) if logging.getLogger().isEnabledFor(logging.DEBUG): if builds: logging.debug(T("Build list:")) idx = 1 for b in builds: logging.debug(T("%d) %s") % (idx, b.output_filename)) idx = idx + 1 else: logging.debug(T("Empty build list")) # 4. Build the files if builds: for file in builds: continuation = self.__launch_file_build(root_file=root_file, file=file, force_change=force_change, dependencies=dependencies) if not continuation: return False # 5. Write building stamps logging.debug(T("Saving the file stamps")) self.stamp_manager.write_build_stamps(root_dir) # 6. Output the warnings from the last TeX builds main_tex_file = self.__files[root_file].main_filename or root_file log_file = FileType.log.ensure_extension(main_tex_file) log_parser = TeXLogParser(log_file=log_file) detailed_warnings = self.detailed_warnings if detailed_warnings: if logging.getLogger().isEnabledFor(LogLevel.FINE_INFO): for w in detailed_warnings: extlogging.multiline_fine_info(textwrap.wrap( T("%s:%d: %s") % (w.filename, w.lineno, w.message), width=80)) self.__reset_warnings() # 7. Generate the Postscript-based file when requested if not self.configuration.generation.pdf_mode: dvi_file = FileType.dvi.ensure_extension(root_file) dvi_date = genutils.get_file_last_change(dvi_file) if dvi_date is not None: ps_file = FileType.ps.ensure_extension(root_file) ps_date = genutils.get_file_last_change(ps_file) if ps_date is None or dvi_date >= ps_date: logging.log(LogLevel.FINE_INFO, T("Converting the DVI file: %s") % dvi_file) self.run_dvips(dvi_file) # 8. Detect warnings from the log file if not already done if not self.standard_warnings: log_parser.extract_warnings(enable_loop=False, enable_detailed_warnings=self.detailed_warnings_enabled, standards_warnings=self.__standards_warnings, detailed_warnings=self.__detailed_warnings) # 9. Output the last LaTeX warning indicators. if logging.getLogger().isEnabledFor(logging.WARNING): if TeXWarnings.multiple_definition in self.standard_warnings: s = T("LaTeX Warning: There were multiply-defined labels.") logging.warning(s) if self.detailed_warnings_enabled: extprint.eprint("!!" + log_file + ":W1: " + s + "\n") if TeXWarnings.undefined_reference in self.standard_warnings: s = T("LaTeX Warning: There were undefined references.") logging.warning(s) if self.detailed_warnings_enabled: extprint.eprint("!!" + log_file + ":W2: " + s + "\n") if TeXWarnings.undefined_citation in self.standard_warnings: s = T("LaTeX Warning: There were undefined citations.") logging.warning(s) if self.detailed_warnings_enabled: extprint.eprint("!!" + log_file + ":W3: " + s + "\n") if TeXWarnings.other_warning in self.standard_warnings: bn = os.path.basename(log_file) logging.warning((T("LaTeX Warning: Please look inside %s for the other the warning messages.") % bn) + "\n") return True @staticmethod def build_builder_dict(package_name : str) -> dict[FileType,BuilderFactory]: """ Build the dictionary that maps the builder id to AutoLaTeX dynamic builders. :param package_name: The name of the package to explore. :type package_name: str :return: the dict of the factories of builders. :rtype: dict[FileType,BuilderFactory] """ execution_environment : dict[str,Any] = { 'modules': None, } exec("import " + package_name + "\nmodules = " + package_name + ".__all__", None, execution_environment) modules = execution_environment['modules'] ids : dict[FileType,BuilderFactory] = dict() for module in modules: execution_environment = { 'type': None, 'output': None, } cmd = textwrap.dedent("""\ from %s.%s import DynamicBuilder type = DynamicBuilder output = DynamicBuilder.output """) % (package_name, module) exec(cmd, None, execution_environment) builder_type : Type[Builder] = execution_environment['type'] builder_output : FileType = execution_environment['output'] if builder_output and builder_type: ids[builder_output] = BuilderFactory( builder_output = builder_output, builder_type = builder_type) return ids @override def reset_file_change_for(self, filename : str): """ Reset the buffered last-changed date for the given filename, if it is known. If the filename is not known, this function does nothing. :param filename: The name of the file for which the last-change date should be reset. :type filename: str """ if filename in self.__files: self.__files[filename].reset_change() # noinspection PyBroadException def remove_output_file(self): """ Remove the output file from the TeX process if it exists. """ for root_file in self.__root_files: out_type = FileType.pdf if self.configuration.generation.pdf_mode else FileType.dvi out_file = out_type.ensure_extension(root_file) if os.path.isfile(out_file): try: os.unlink(out_file) except: pass