From 070a8b2f0a56b83b2b70272b92b1d88a24eb836d Mon Sep 17 00:00:00 2001 From: James Yu <yujianqiaojames@gmail.com> Date: Wed, 22 Nov 2023 17:24:22 +0000 Subject: [PATCH] Fix #4068 Respect latexmk+subfile when creating output subfolders --- src/compile/external.ts | 20 ++++++--- src/compile/queue.ts | 64 +++++++++++++++++++++------- src/compile/recipe.ts | 92 +++++++++++++++++++++++++++++----------- src/compile/terminate.ts | 17 +++++--- 4 files changed, 142 insertions(+), 51 deletions(-) diff --git a/src/compile/external.ts b/src/compile/external.ts index 4e43128e6..bac9d6243 100644 --- a/src/compile/external.ts +++ b/src/compile/external.ts @@ -13,29 +13,37 @@ const logger = lw.log('Build', 'External') * queue. After that, this function tries to initiate a {@link buildLoop} if * there is no one running. * - * @param command The external command to be executed. - * @param args The arguments to {@link command}. - * @param pwd The current working directory. This argument will be overrided - * if there are workspace folders. If so, the root of the first workspace - * folder is used as the current working directory. - * @param rootFile Path to the root LaTeX file. + * @param {string} command - The command to execute for building the project. + * @param {string[]} args - The arguments to pass to the build command. + * @param {string} pwd - The current working directory for the build. + * @param {() => Promise<void>} buildLoop - A function that represents the build loop. + * @param {string} [rootFile] - Optional. The root file for the build. */ export async function build(command: string, args: string[], pwd: string, buildLoop: () => Promise<void>, rootFile?: string) { + // Check if a build is already in progress if (lw.compile.compiling) { void logger.showErrorMessageWithCompilerLogButton('Please wait for the current build to finish.') return } + // Save all open files in the workspace await vscode.workspace.saveAll() + // Determine the current working directory for the build const workspaceFolder = vscode.workspace.workspaceFolders?.[0] const cwd = workspaceFolder?.uri.fsPath || pwd + + // Replace argument placeholders if a root file is provided if (rootFile !== undefined) { args = args.map(replaceArgumentPlaceholders(rootFile, lw.file.tmpDirPath)) } + + // Create a Tool object representing the build command and arguments const tool: Tool = { name: command, command, args } + // Add the build tool to the queue for execution queue.add(tool, rootFile, 'External', Date.now(), true, cwd) + // Execute the build loop await buildLoop() } diff --git a/src/compile/queue.ts b/src/compile/queue.ts index 0d523069c..be8071260 100644 --- a/src/compile/queue.ts +++ b/src/compile/queue.ts @@ -4,23 +4,23 @@ import type { ExternalStep, RecipeStep, Step, StepQueue, Tool } from '../types' const stepQueue: StepQueue = { steps: [], nextSteps: [] } /** - * Add a {@link Tool} to the queue. The input {@link tool} is first wrapped - * to be a {@link RecipeStep} or {@link ExternalStep} with additional - * information, according to the nature {@link isExternal}. Then the wrapped - * {@link Step} is added to the current {@link steps} if they belongs to the - * same recipe, determined by the same {@link timestamp}, or added to the - * {@link nextSteps} for later execution. + * Add a Tool to the queue, either as a RecipeStep or ExternalStep, based on + * isExternal flag. If the tool belongs to the same recipe (determined by + * timestamp), it is added to the current steps; otherwise, it is added to the + * next steps for later execution. * - * @param tool The {@link Tool} to be added to the queue. - * @param rootFile Path to the root LaTeX file. - * @param recipeName The name of the recipe which the {@link tool} belongs - * to. - * @param timestamp The timestamp when the recipe is called. - * @param isExternal Whether the {@link tool} is an external command. - * @param cwd The current working directory if the {@link tool} is an + * @param {Tool} tool - The Tool to be added to the queue. + * @param {string | undefined} rootFile - Path to the root LaTeX file. + * @param {string} recipeName - The name of the recipe to which the tool + * belongs. + * @param {number} timestamp - The timestamp when the recipe is called. + * @param {boolean} [isExternal=false] - Whether the tool is an external + * command. + * @param {string} [cwd] - The current working directory if the tool is an * external command. */ function add(tool: Tool, rootFile: string | undefined, recipeName: string, timestamp: number, isExternal: boolean = false, cwd?: string) { + // Wrap the tool as a RecipeStep or ExternalStep let step: Step if (!isExternal && rootFile !== undefined) { step = tool as RecipeStep @@ -37,6 +37,8 @@ function add(tool: Tool, rootFile: string | undefined, recipeName: string, times step.isExternal = true step.cwd = cwd || '' } + + // Add the step to the appropriate queue (steps or nextSteps) if (stepQueue.steps.length === 0 || step.timestamp === stepQueue.steps[0].timestamp) { step.index = (stepQueue.steps[stepQueue.steps.length - 1]?.index ?? -1) + 1 stepQueue.steps.push(step) @@ -49,21 +51,44 @@ function add(tool: Tool, rootFile: string | undefined, recipeName: string, times } } +/** + * Add a step to the beginning of the current steps queue. + * + * @param {Step} step - The Step to be added to the front of the current steps + * queue. + */ function prepend(step: Step) { stepQueue.steps.unshift(step) } +/** + * Clear both the current steps and next steps queues. + */ function clear() { - stepQueue.nextSteps = [] - stepQueue.steps = [] + stepQueue.nextSteps.length = 0 + stepQueue.steps.length = 0 } +/** + * Check if the given step is the last one in the current steps queue. + * + * @param {Step} step - The Step to check. + * @returns {boolean} - True if the step is the last one; otherwise, false. + */ function isLastStep(step: Step) { return stepQueue.steps.length === 0 || stepQueue.steps[0].timestamp !== step.timestamp } +/** + * Get a formatted string representation of the given step. + * + * @param {Step} step - The Step to get the string representation for. + * @returns {string} - The formatted string representation of the step. + */ function getStepString(step: Step): string { let stepString: string + + // Determine the format of the stepString based on timestamp and index if (step.timestamp !== stepQueue.steps[0]?.timestamp && step.index === 0) { stepString = step.recipeName } else if (step.timestamp === stepQueue.steps[0]?.timestamp) { @@ -71,6 +96,8 @@ function getStepString(step: Step): string { } else { stepString = `${step.recipeName}: ${step.index + 1}/${step.index + 1} (${step.name})` } + + // Determine the format of the stepString based on timestamp and index if(step.rootFile) { const rootFileUri = vscode.Uri.file(step.rootFile) const configuration = vscode.workspace.getConfiguration('latex-workshop', rootFileUri) @@ -83,6 +110,13 @@ function getStepString(step: Step): string { return stepString } +/** + * Get the next step from the queue, either from the current steps or next + * steps. + * + * @returns {Step | undefined} - The next step from the queue, or undefined if + * the queue is empty. + */ function getStep(): Step | undefined { let step: Step | undefined if (stepQueue.steps.length > 0) { diff --git a/src/compile/recipe.ts b/src/compile/recipe.ts index 3987c63d5..7d6931ceb 100644 --- a/src/compile/recipe.ts +++ b/src/compile/recipe.ts @@ -14,45 +14,54 @@ let prevRecipe: Recipe | undefined = undefined let prevLangId = '' /** - * Build LaTeX project using the recipe system. This function creates - * {@link Tool}s containing the tool info and adds them to the queue. After - * that, this function tries to initiate a {@link buildLoop} if there is no - * one running. + * Build LaTeX project using the recipe system. Creates Tools containing the + * tool info and adds them to the queue. Initiates a buildLoop if there is no + * running one. * - * @param rootFile Path to the root LaTeX file. - * @param langId The language ID of the root file. This argument is used to - * determine whether the previous recipe can be applied to this root file. - * @param recipeName The name of recipe to be used. If `undefined`, the - * builder tries to determine on its own, in {@link createBuildTools}. This - * parameter is given only when RECIPE command is invoked. For all other - * cases, it should be `undefined`. + * @param {string} rootFile - Path to the root LaTeX file. + * @param {string} langId - The language ID of the root file. Used to determine + * whether the previous recipe can be applied. + * @param {Function} buildLoop - A function that represents the build loop. + * @param {string} [recipeName] - Optional. The name of the recipe to be used. + * If undefined, the builder tries to determine on its own. */ export async function build(rootFile: string, langId: string, buildLoop: () => Promise<void>, recipeName?: string) { logger.log(`Build root file ${rootFile}`) + // Save all open files in the workspace await vscode.workspace.saveAll() - createOutputSubFolders(rootFile) - + // Create build tools based on the recipe system const tools = createBuildTools(rootFile, langId, recipeName) + // Create output subdirectories for included files + if (tools?.map(tool => tool.command).includes('latexmk') && rootFile === lw.root.subfiles.path && lw.root.file.path) { + createOutputSubFolders(lw.root.file.path) + } else { + createOutputSubFolders(rootFile) + } + + // Check for invalid toolchain if (tools === undefined) { logger.log('Invalid toolchain.') - + // Set compiling status to false lw.compile.compiling = false return } + + // Add tools to the queue with timestamp const timestamp = Date.now() tools.forEach(tool => queue.add(tool, rootFile, recipeName || 'Build', timestamp)) + // Execute the build loop await buildLoop() } /** - * Create sub directories of output directory This was supposed to create - * the outputDir as latexmk does not take care of it (neither does any of - * latex command). If the output directory does not exist, the latex - * commands simply fail. + * Create subdirectories of the output directory. This is necessary as some + * LaTeX commands do not create the output directory themselves. + * + * @param {string} rootFile - Path to the root LaTeX file. */ function createOutputSubFolders(rootFile: string) { const rootDir = path.dirname(rootFile) @@ -83,8 +92,15 @@ function createOutputSubFolders(rootFile: string) { }) } + /** * Given an optional recipe, create the corresponding {@link Tool}s. + * + * @param {string} rootFile - Path to the root LaTeX file. + * @param {string} langId - The language ID of the root file. + * @param {string} [recipeName] - Optional. The name of the recipe to be used. + * @returns {Tool[] | undefined} - An array of Tool objects representing the + * build tools. */ function createBuildTools(rootFile: string, langId: string, recipeName?: string): Tool[] | undefined { let buildTools: Tool[] = [] @@ -130,6 +146,14 @@ function createBuildTools(rootFile: string, langId: string, recipeName?: string) return buildTools } +/** + * Find magic comments in the root file, including TeX and BibTeX programs, and + * the LW recipe name. + * + * @param {string} rootFile - Path to the root LaTeX file. + * @returns {{tex?: Tool, bib?: Tool, recipe?: string}} - An object containing + * the TeX and BibTeX tools and the LW recipe name. + */ function findMagicComments(rootFile: string): {tex?: Tool, bib?: Tool, recipe?: string} { const regexTex = /^(?:%\s*!\s*T[Ee]X\s(?:TS-)?program\s*=\s*([^\s]*)$)/m const regexBib = /^(?:%\s*!\s*BIB\s(?:TS-)?program\s*=\s*([^\s]*)$)/m @@ -183,6 +207,16 @@ function findMagicComments(rootFile: string): {tex?: Tool, bib?: Tool, recipe?: return {tex: texCommand, bib: bibCommand, recipe: recipe?.[1]} } +/** + * Create build tools based on magic comments in the root file. + * + * @param {string} rootFile - Path to the root LaTeX file. + * @param {Tool} magicTex - Tool object representing the TeX command from magic + * comments. + * @param {Tool} [magicBib] - Optional. Tool object representing the BibTeX + * command from magic comments. + * @returns {Tool[]} - An array of Tool objects representing the build tools. + */ function createBuildMagic(rootFile: string, magicTex: Tool, magicBib?: Tool): Tool[] { const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) @@ -203,8 +237,13 @@ function createBuildMagic(rootFile: string, magicTex: Tool, magicBib?: Tool): To /** - * @param recipeName This recipe name may come from user selection of RECIPE - * command, or from the %! LW recipe magic command. + * Find a recipe based on the provided recipe name, language ID, and root file. + * + * @param {string} rootFile - Path to the root LaTeX file. + * @param {string} langId - The language ID of the root file. + * @param {string} [recipeName] - Optional. The name of the recipe to be used. + * @returns {Recipe | undefined} - The Recipe object corresponding to the + * provided parameters. */ function findRecipe(rootFile: string, langId: string, recipeName?: string): Recipe | undefined { const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) @@ -257,8 +296,11 @@ function findRecipe(rootFile: string, langId: string, recipeName?: string): Reci } /** - * Expand the bare {@link Tool} with docker and argument placeholder - * strings. + * Expand the bare {@link Tool} with Docker and argument placeholder strings. + * + * @param {string} rootFile - Path to the root LaTeX file. + * @param {Tool[]} buildTools - An array of Tool objects to be populated. + * @returns {Tool[]} - An array of Tool objects with expanded values. */ function populateTools(rootFile: string, buildTools: Tool[]): Tool[] { const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) @@ -304,8 +346,10 @@ function populateTools(rootFile: string, buildTools: Tool[]): Tool[] { let _isMikTeX: boolean /** - * Whether latex toolchain compilers are provided by MikTeX. This function uses - * a cache variable `_isMikTeX`. + * Check whether the LaTeX toolchain compilers are provided by MikTeX. + * + * @returns {boolean} - True if the LaTeX toolchain is provided by MikTeX; + * otherwise, false. */ function isMikTeX(): boolean { if (_isMikTeX === undefined) { diff --git a/src/compile/terminate.ts b/src/compile/terminate.ts index c689f4a18..0d7af7f65 100644 --- a/src/compile/terminate.ts +++ b/src/compile/terminate.ts @@ -5,12 +5,12 @@ import { queue } from './queue' const logger = lw.log('Build', 'Recipe') /** - * Terminate current process of LaTeX building. OS-specific (pkill for linux - * and macos, taskkill for win) kill command is first called with process - * pid. No matter whether it succeeded, `kill()` of `child_process` is later - * called to "double kill". Also, all subsequent tools in queue are cleared, - * including ones in the current recipe and (if available) those from the - * cached recipe to be executed. + * Terminate the current process of LaTeX building. This OS-specific function + * uses a kill command (pkill for Linux and macOS, taskkill for Windows) with + * the process PID. Regardless of success, `kill()` from the `child_process` + * module is later called for a "double kill." Subsequent tools in the queue, + * including those from the current recipe and (if available) those from the + * cached recipe to be executed, are cleared. */ export function terminate() { if (lw.compile.process === undefined) { @@ -21,14 +21,19 @@ export function terminate() { try { logger.log(`Kill child processes of the current process with PID ${pid}.`) if (process.platform === 'linux' || process.platform === 'darwin') { + // Use pkill to kill child processes cp.execSync(`pkill -P ${pid}`, { timeout: 1000 }) } else if (process.platform === 'win32') { + // Use taskkill on Windows to forcefully terminate child processes cp.execSync(`taskkill /F /T /PID ${pid}`, { timeout: 1000 }) } } catch (e) { logger.logError('Failed killing child processes of the current process.', e) } finally { + // Clear all subsequent tools in the queue queue.clear() + + // Perform a "double kill" using kill() from child_process lw.compile.process.kill() logger.log(`Killed the current process with PID ${pid}`) }