Skip to content
Open
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 @@ -61,3 +61,4 @@ All changes included in 1.9:
- ([#13402](https://github.com/quarto-dev/quarto-cli/issues/13402)): `nfpm` (<https://nfpm.goreleaser.com/>) is now used to create the `.deb` package, and new `.rpm` package. Both Linux packages are also now built for `x86_64` (`amd64`) and `aarch64` (`arm64`) architectures.
- ([#13528](https://github.com/quarto-dev/quarto-cli/pull/13528)): Adds support for table specification using nested lists and the `list-table` class.
- ([#13575](https://github.com/quarto-dev/quarto-cli/pull/13575)): Improve CPU architecture detection/reporting in macOS to allow quarto to run in virtualized environments such as OpenAI's `codex`.
- ([#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.
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
Loading