Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/changelog-1.9.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All changes included in 1.9:
- ([#13441](https://github.com/quarto-dev/quarto-cli/pull/13441)): Catch `undefined` exceptions in Pandoc failure to avoid spurious error message.
- ([#13046](https://github.com/quarto-dev/quarto-cli/issues/13046)): Use new url for multiplex socket.io server <https://multiplex.up.railway.app/> as default for `format: revealjs` and `revealjs.multiplex: true`.
- ([#13506](https://github.com/quarto-dev/quarto-cli/issues/13506)): Fix navbar active state detection when sidebar has no logo configured. Prevents empty logo links from interfering with navigation highlighting.
- ([#13625](https://github.com/quarto-dev/quarto-cli/issues/13625)): Fix Windows file locking error (os error 32) when rendering with `--output-dir` flag. Context cleanup now happens before removing the temporary `.quarto` directory, ensuring file handles are properly closed.
- ([#13633](https://github.com/quarto-dev/quarto-cli/issues/13633)): Fix detection and auto-installation of babel language packages from newer error format that doesn't explicitly mention `.ldf` filename.

## Dependencies
Expand Down
17 changes: 12 additions & 5 deletions src/command/render/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -886,13 +886,20 @@ export async function renderProject(
);
}

// in addition to the cleanup above, if forceClean is set, we need to clean up the project scratch dir
// entirely. See options.forceClean in render-shared.ts
// .quarto is really a fiction created because of `--output-dir` being set on non-project
// renders
// Clean up synthetic project created for --output-dir
// When --output-dir is used without a project file, we create a temporary
// project context with a .quarto directory (see render-shared.ts).
// After rendering completes, we must remove this directory to avoid leaving
// debris in non-project directories (#9745).
//
// cf https://github.com/quarto-dev/quarto-cli/issues/9745#issuecomment-2125951545
// Critical ordering for Windows: Close file handles BEFORE removing directory
// to avoid "The process cannot access the file because it is being used by
// another process" (os error 32) (#13625).
if (projectRenderConfig.options.forceClean) {
// 1. Close all file handles (KV database, temp context, etc.)
context.cleanup();

// 2. Remove the temporary .quarto directory
const scratchDir = join(projDir, kQuartoScratch);
if (existsSync(scratchDir)) {
safeRemoveSync(scratchDir, { recursive: true });
Expand Down
8 changes: 5 additions & 3 deletions src/command/render/render-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ export async function render(
// determine target context/files
let context = await projectContext(path, nbContext, options);

// if there is no project parent and an output-dir was passed, then force a project
// Create a synthetic project when --output-dir is used without a project file
// This creates a temporary .quarto directory to manage the render, which must
// be fully cleaned up afterward to avoid leaving debris (see #9745)
if (!context && options.flags?.outputDir) {
// recompute context
context = await projectContextForDirectory(path, nbContext, options);

// force clean as --output-dir implies fully overwrite the target
// forceClean signals this is a synthetic project that needs full cleanup
// including removing the .quarto scratch directory after rendering (#13625)
options.forceClean = options.flags.clean !== false;
}

Expand Down
2 changes: 2 additions & 0 deletions tests/docs/render-output-dir/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/.quarto/
**/*.quarto_ipynb
6 changes: 6 additions & 0 deletions tests/docs/render-output-dir/test.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: "Test Output Dir"
format: html
---

This is a simple document to test rendering with --output-dir flag.
54 changes: 54 additions & 0 deletions tests/smoke/render/render-output-dir.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* render-output-dir.test.ts
*
* Test for Windows file locking issue with --output-dir flag
* Regression test for: https://github.com/quarto-dev/quarto-cli/issues/13625
*
* Copyright (C) 2020-2025 Posit Software, PBC
*
*/
import { existsSync, safeRemoveSync } from "../../../src/deno_ral/fs.ts";
import { docs } from "../../utils.ts";
import { isWindows } from "../../../src/deno_ral/platform.ts";
import { fileExists, pathDoNotExists } from "../../verify.ts";
import { testRender } from "./render.ts";
import type { Verify } from "../../test.ts";


const inputDir = docs("render-output-dir/");
const quartoDir = ".quarto";
const outputDir = "output-test-dir";

const cleanupDirs = async () => {
if (existsSync(outputDir)) {
safeRemoveSync(outputDir, { recursive: true });
}
if (existsSync(quartoDir)) {
safeRemoveSync(quartoDir, { recursive: true });
}
};

const testOutputDirRender = (
quartoVerify: Verify,
extraArgs: string[] = [],
) => {
testRender(
"test.qmd",
"html",
false,
[quartoVerify],
{
cwd: () => inputDir,
setup: cleanupDirs,
teardown: cleanupDirs,
},
["--output-dir", outputDir, ...extraArgs],
outputDir,
);
};

// Test 1: Default behavior (clean=true) - .quarto should be removed
testOutputDirRender(pathDoNotExists(quartoDir));

// Test 2: With --no-clean flag - .quarto should be preserved
testOutputDirRender(fileExists(quartoDir), ["--no-clean"]);
4 changes: 4 additions & 0 deletions tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,13 @@ export function outputForInput(

const outputPath: string = projectRoot && projectOutDir !== undefined
? join(projectRoot, projectOutDir, dir, `${stem}.${outputExt}`)
: projectOutDir !== undefined
? join(projectOutDir, dir, `${stem}.${outputExt}`)
: join(dir, `${stem}.${outputExt}`);
const supportPath: string = projectRoot && projectOutDir !== undefined
? join(projectRoot, projectOutDir, dir, `${stem}_files`)
: projectOutDir !== undefined
? join(projectOutDir, dir, `${stem}_files`)
: join(dir, `${stem}_files`);

return {
Expand Down
2 changes: 1 addition & 1 deletion tests/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export const fileExists = (file: string): Verify => {

export const pathDoNotExists = (path: string): Verify => {
return {
name: `path ${path} exists`,
name: `path ${path} do not exists`,
verify: (_output: ExecuteOutput[]) => {
verifyNoPath(path);
return Promise.resolve();
Expand Down