From ec26008dd1c81b886a8ac5ceae2323f7516fa6e3 Mon Sep 17 00:00:00 2001 From: deathaxe Date: Wed, 10 Sep 2025 19:22:58 +0200 Subject: [PATCH] Use aux-directory as working directory when building PDF This commit... 1. adds main TeX document's location to TEXINPUTS,BIBINTPUTS and BSTINPUTS 2. uses aux-directory as current working directory for executed commands 3. removes `--output-directory` arguments from pdflatex and latexmk These changes enable all builders - traditional (latexmk & texify), basic builder, script builder - to make use of aux-directory the same way. It actually also simplifies support for bibtex, as it doesn't need special environment variables being set. --- docs/buildsystem.md | 43 +++++--------- docs/settings.md | 2 +- latextools/make_pdf.py | 2 +- plugins/builder/basic_builder.py | 41 +++----------- plugins/builder/pdf_builder.py | 78 ++++++++++++++++---------- plugins/builder/script_builder.py | 2 + plugins/builder/traditional_builder.py | 16 ++++-- 7 files changed, 84 insertions(+), 100 deletions(-) diff --git a/docs/buildsystem.md b/docs/buildsystem.md index c93aae8e..356c5235 100644 --- a/docs/buildsystem.md +++ b/docs/buildsystem.md @@ -109,20 +109,26 @@ Available default builders are: To learn, how to create custom builder plugins, refer to [Custom Builder](#custom-builder) section. +Breaking Changes of LaTeXTools v4.5.2: + +1. All commands are executed with working directory set to `aux_directory`, which enables support for custom auxiliary directories in all builders and compilers, without the need to explicitly specify `--aux-directory` or `--output-directory` command line arguments. + +2. Main TeX document's location is automatically prepended to `$TEXINPUTS`, `$BIBINPUTS` and `$BSTINPUTS`. Hence no special action is required for commands like `bibtex` or `bibtex8`, which don't support `--output-directory` arguments. + ## Traditional Builder The `traditional` builder is designed to work in most circumstances and for most setups. It supports all [builder features](features.md#default-builder) discussed elsewhere in the documentation, including multi-document support, the ability to set LaTeX flags via the [TeX Options](features.md#tex-options) settings, etc. If available, [latexmk][] is used to generate the document. Otherwise [texify][] is used as fallback. -**Note:** [texify][] doesn't support features, such as specifying output directory, auxiliary directory, or jobname. - Default commands: ``` -latexmk -cd -f -%E -interaction=nonstopmode -synctex=1 +latexmk -f -%E -interaction=nonstopmode -synctex=1 ``` +**Note:** `-cd` argument is no longer required as of LaTeXTools v4.5.2 + ``` texify -b -p --engine=%E --tex-option="--synctex=1" ``` @@ -253,34 +259,10 @@ Commands are executed in the same path as `$file_path`, i.e. the folder containi ### Output and Auxiliary Directories -If [auxiliary or output directory settings](settings.md#output-directory-settings) are specified, script builder creates them before batch execution. They are provided by [variables](#variables) and need to be passed to relevant commands in `script_commands`. +If [auxiliary or output directory settings](settings.md#output-directory-settings) are specified, script builder creates them before batch execution. They are provided by [variables](#variables) for use in `script_commands`. `pdflatex` and friends do not create (sub-)directories as needed. If `\include` is used (or anything attempts to `\@openout` a file in a subfolder), they must be created manually by script commands like `"mkdir $output_directory\chapters"` (Windows) or `"mkdir -p $output_directory/chapters"` (Linux/MacOS). -**BibTeX** - -Unlike biber, bibtex (and bibtex8) does not support an output directory parameter. -The following workaround can be used to run BibTeX _inside_ the output / auxiliary directory while making the directory containing your main file available to the `BIBINPUTS` environment variable. - -```json -{ - "builder_settings": { - "linux": { - "script_commands": [ - "cd $output_directory; BIBINPUTS=\"$file_path;$BIBINPUTS\" bibtex \"$file_base_name\"", - ] - }, - "windows": { - "script_commands": [ - "cd $output_directory & set BIBINPUTS=\"$file_path:%BIBINPUTS%\" & bibtex \"$file_base_name\"" - ] - } - } -} -``` - -**Note:** If a custom style file is used in the same directory, a similar work-around for `BSTINPUTS` environment variable needs to be applied. - ### Job Name If jobname behaviour is used, `$jobname` is to be passed to relevant commands. In particular, a standard build cycle might look something like this: @@ -434,9 +416,10 @@ class MySecondBuilder(PdfBuilder): yield (["pdflatex", f"-synctex={self.sync_id}"], "running pdflatex...") # By default commands are executed with current working directory (`cwd`) - # set to main tex document's location. To call commands with custom parameters + # set to specified aux_directory if it differs from tex document's location. + # To call commands with custom working directory or parameters, # use `PdfBuilder.command()` method. - yield (self.command(["bibtex"], cwd=self.aux_directory_full), "running bibtex"...) + yield (self.command(["bibtex"], cwd="/my/custom/working/dir"), "running bibtex"...) # Prevent aborting workflow if command(s) return with non-zero exit status self.abort_on_error = False diff --git a/docs/settings.md b/docs/settings.md index 2709432e..86364d10 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -91,7 +91,7 @@ This section refers to setting that can be found in a platform-specific block fo ## Output Directory Settings -* `aux_directory` (`""`): specifies the auxiliary directory to store any auxiliary files generated during a LaTeX build. Note that the auxiliary directory option is only useful if you are using MiKTeX. Path can be specified using either an absolute path or a relative path. If `aux_directory` is set from the project file, a relative path will be interpreted as relative to the project file. If it is set in the settings file, it will be interpreted relative to the main tex file. In addition, the following special values are honored: +* `aux_directory` (`".aux"`): specifies the auxiliary directory to store any auxiliary files generated during a LaTeX build. Path can be specified absolute or relative to `tex_root` or, if `aux_directory` set in a project file, relative to project file's location. In addition, the following special values are honored: * `<>`: uses a temporary directory in the system temp directory instead of a specified path; this directory will be unique to each main file, but does not persist across restarts. * `<>`: uses the ST cache directory (or a suitable directory on ST2) to store the output files; unlike the `<>` option, this directory can persist across restarts. * `<>`: uses a sub-directory in the same folder as the main tex file with what should be a unique name; note, this is probably not all that useful and you're better off using one of the other two options or a named relative path diff --git a/latextools/make_pdf.py b/latextools/make_pdf.py index a6ea2740..2a145791 100644 --- a/latextools/make_pdf.py +++ b/latextools/make_pdf.py @@ -201,7 +201,7 @@ def worker(self, activity_indicator): try: ws = re.compile(r"\s+") - (errors, warnings, badboxes) = parse_tex_log(data, self.caller.builder.tex_dir) + (errors, warnings, badboxes) = parse_tex_log(data, self.caller.builder.aux_directory_full) content = [""] if errors: content.append("Errors:") diff --git a/plugins/builder/basic_builder.py b/plugins/builder/basic_builder.py index 1d2a0e5d..127f5dbd 100644 --- a/plugins/builder/basic_builder.py +++ b/plugins/builder/basic_builder.py @@ -57,12 +57,6 @@ def commands(self) -> CommandGenerator: latex = [engine, "-interaction=nonstopmode", "-shell-escape", "-synctex=1"] biber = ["biber"] - if self.aux_directory: - # No supported engine supports --aux-directory, use --output-directory - # and move final documents later. - biber.append(f"--output-directory={self.aux_directory}") - latex.append(f"--output-directory={self.aux_directory}") - if self.job_name and self.job_name != self.base_name: latex.append(f"--jobname={self.job_name}") @@ -78,7 +72,7 @@ def commands(self) -> CommandGenerator: # Create required directories and retry while matches := FILE_WRITE_ERROR_REGEX.findall(self.out): for path, _ in matches: - abspath = os.path.join(self.aux_directory_full or self.tex_dir, path) + abspath = os.path.join(self.aux_directory_full, path) os.makedirs(abspath, exist_ok=True) logger.debug(f"Created directory {abspath}") @@ -105,10 +99,14 @@ def commands(self) -> CommandGenerator: if run_bibtex: if use_bibtex: - yield ( - self.run_bibtex(bibtex), - f"running {bibtex or 'bibtex'}...", - ) + # set-up bibtex cmd line + if bibtex is None: + bibtex = [self.builder_settings.get("bibtex", "bibtex")] + elif isinstance(bibtex, str): + bibtex = [bibtex] + bibtex.append(self.job_name) + yield (bibtex, f"running {bibtex[0]}...") + else: yield (biber + [self.job_name], "running biber...") @@ -122,24 +120,3 @@ def commands(self) -> CommandGenerator: yield (latex, f"running {engine}...") self.copy_assets_to_output() - - def run_bibtex(self, cmd: CommandLine | None=None) -> Command: - # set-up bibtex cmd line - if cmd is None: - cmd = [self.builder_settings.get("bibtex", "bibtex")] - elif isinstance(cmd, str): - cmd = [cmd] - cmd.append(self.job_name) - - # return default command line, if build output is tex_dir. - if not self.aux_directory: - return cmd - - # to get bibtex to work with the output directory, we change the - # cwd to the output directory and add the main directory to - # BIBINPUTS and BSTINPUTS - env = self.env.copy() - # cwd is, at the point, the path to the main tex file - env["BIBINPUTS"] = self.tex_dir + os.pathsep + env.get("BIBINPUTS", "") - env["BSTINPUTS"] = self.tex_dir + os.pathsep + env.get("BSTINPUTS", "") - return self.command(cmd, cwd=self.aux_directory_full, env=env) diff --git a/plugins/builder/pdf_builder.py b/plugins/builder/pdf_builder.py index 96f4fdc1..c4275bff 100644 --- a/plugins/builder/pdf_builder.py +++ b/plugins/builder/pdf_builder.py @@ -112,8 +112,8 @@ def __init__( """ self.display = output - self.tex_root = tex_root - self.tex_dir, self.tex_name = os.path.split(tex_root) + self.tex_root = os.path.normpath(tex_root) + self.tex_dir, self.tex_name = os.path.split(self.tex_root) self.base_name, self.tex_ext = os.path.splitext(self.tex_name) self.engine = engine self.options = options @@ -126,29 +126,35 @@ def __init__( # relative to self.tex_dir, we use that instead of the absolute path # note that the full path for both is available as # self.output_directory_full and self.aux_directory_full - self.aux_directory_full = aux_directory - self.aux_directory = ( - os.path.relpath(aux_directory, self.tex_dir) - if aux_directory and aux_directory.startswith(self.tex_dir) - else aux_directory - ) - if self.aux_directory: - os.makedirs(self.aux_directory_full, exist_ok=True) + if aux_directory: + self.aux_directory_full = os.path.normpath(aux_directory) try: - gitignore = os.path.join(self.aux_directory_full, ".gitignore") - with open(gitignore, "w+", encoding="utf-8") as fobj: - fobj.write("*\n") - except FileExistsError: - pass - - self.output_directory_full = output_directory - self.output_directory = ( - os.path.relpath(output_directory, self.tex_dir) - if output_directory and output_directory.startswith(self.tex_dir) - else output_directory - ) - if self.output_directory: - os.makedirs(self.output_directory_full, exist_ok=True) + self.aux_directory = os.path.relpath(self.aux_directory_full, self.tex_dir) + except Exception: + self.aux_directory = self.aux_directory_full + if self.aux_directory: + os.makedirs(self.aux_directory_full, exist_ok=True) + try: + gitignore = os.path.join(self.aux_directory_full, ".gitignore") + with open(gitignore, "w+", encoding="utf-8") as fobj: + fobj.write("*\n") + except FileExistsError: + pass + else: + self.aux_directory_full = self.tex_dir + self.aux_directory = "" + + if output_directory: + self.output_directory_full = os.path.normpath(output_directory) + try: + self.output_directory = os.path.relpath(self.output_directory_full, self.tex_dir) + except Exception: + self.output_directory = self.output_directory_full + if self.output_directory: + os.makedirs(self.output_directory_full, exist_ok=True) + else: + self.output_directory_full = self.tex_dir + self.output_directory = "" self.display_log = self.builder_settings.get("display_log", False) """ @@ -157,9 +163,22 @@ def __init__( Value of `builder_settings: { display_log: ... }` setting """ + # ensure main TeX document's location is available in environment + # it enables builders such as latexmk or texify to run with different working directory. + for key in ("TEXINPUTS", "BIBINPUTS", "BSTINPUTS"): + env[key] = ( + self.tex_dir + os.pathsep + env[key] + if key in env + else self.tex_dir + ) + # finally expand variables in custom environment self.env = {k: self.expandvars(v) for k, v in env.items()} if env else None + logger.debug("tex directory: %s", self.tex_dir) + logger.debug("aux directory: %s", self.aux_directory_full) + logger.debug("out directory: %s", self.output_directory_full) + def set_output(self, out: str) -> None: """ Save command output. @@ -233,7 +252,7 @@ def command( Process object representing invoked command. """ if cwd is None: - cwd = self.tex_dir + cwd = self.aux_directory_full if env is None: env = self.env @@ -271,8 +290,8 @@ def expandvars(self, text: str, **custom_vars: str) -> str: file_name=self.tex_name, file_ext=self.tex_ext, file_base_name=self.base_name, - output_directory=self.output_directory_full or self.tex_dir, - aux_directory=self.aux_directory_full or self.tex_dir, + output_directory=self.output_directory_full, + aux_directory=self.aux_directory_full, jobname=self.job_name, engine=self.engine, **custom_vars @@ -297,12 +316,11 @@ def copy_assets_to_output(self) -> None: Original PDF file is kept in place for e.g. latexmk to be able to skip build if nothing was changed. """ - dst_dir = self.output_directory_full or self.tex_dir - if self.aux_directory and self.aux_directory_full != dst_dir: + if self.aux_directory_full != self.output_directory_full: for ext in (".synctex.gz", ".pdf"): asset_name = self.base_name + ext src_file = os.path.join(self.aux_directory_full, asset_name) - dst_file = os.path.join(dst_dir, asset_name) + dst_file = os.path.join(self.output_directory_full, asset_name) src_st = os.stat(src_file) diff --git a/plugins/builder/script_builder.py b/plugins/builder/script_builder.py index a319fd5e..6b8d5bc9 100644 --- a/plugins/builder/script_builder.py +++ b/plugins/builder/script_builder.py @@ -67,3 +67,5 @@ def commands(self) -> CommandGenerator: raise ValueError(f"Invalid command type! '{cmd}' must be a 'str' or 'list'!") yield (cmd, f"Running '{cmd}'...") + + self.copy_assets_to_output() diff --git a/plugins/builder/traditional_builder.py b/plugins/builder/traditional_builder.py index 3be2685b..aac17eea 100644 --- a/plugins/builder/traditional_builder.py +++ b/plugins/builder/traditional_builder.py @@ -15,7 +15,6 @@ DEFAULT_COMMAND_LATEXMK = [ "latexmk", - "-cd", "-f", "-%E", "-interaction=nonstopmode", @@ -81,7 +80,9 @@ def commands(self) -> CommandGenerator: cmd[i] = c.replace("-%E", flag).replace("%E", engine) if latexmk: - if self.aux_directory: + # if `-cd` is specified, `-output-directory` is required to prevent + # latexmk changing working directory to tex root. Note, that's slower. + if self.aux_directory and "-cd" in cmd: # Don't use --aux-directory as the way latexmk moves # final documents to a possibly defined --output-directory # prevents files reloading in SumatraPDF or even fails @@ -94,11 +95,14 @@ def commands(self) -> CommandGenerator: cmd += map(lambda o: f"-latexoption={o}", self.options) elif texify: + if self.job_name != self.base_name: + cmd.append(f'--job-name="{self.job_name}"') + cmd += map(lambda o: f'--tex-option="{o}"', self.options) - # texify wants the .tex extension; latexmk doesn't care either way - yield (cmd + [self.tex_name], f"running {cmd[0]}...") + # texify requires absolute path if aux-directory differs; + # latexmk doesn't care either way + yield (cmd + [self.tex_root], f"running {cmd[0]}...") # Sync compiled documents with output directory. - if latexmk and self.aux_directory: - self.copy_assets_to_output() + self.copy_assets_to_output()