Skip to content

Commit

Permalink
Fix #4068 Respect latexmk+subfile when creating output subfolders
Browse files Browse the repository at this point in the history
  • Loading branch information
James-Yu committed Nov 22, 2023
1 parent a8818da commit 070a8b2
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 51 deletions.
20 changes: 14 additions & 6 deletions src/compile/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
64 changes: 49 additions & 15 deletions src/compile/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -49,28 +51,53 @@ 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) {
stepString = `${step.recipeName}: ${step.index + 1}/${stepQueue.steps[stepQueue.steps.length - 1].index + 1} (${step.name})`
} 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)
Expand All @@ -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) {
Expand Down
92 changes: 68 additions & 24 deletions src/compile/recipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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[] = []
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))

Expand All @@ -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))
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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) {
Expand Down
17 changes: 11 additions & 6 deletions src/compile/terminate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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}`)
}
Expand Down

0 comments on commit 070a8b2

Please sign in to comment.