Skip to content

Bundled engines leak into output metadata in installed Quarto (filter substring matches source tree only) #14529

@cderv

Description

@cderv

Follow-up to #13819 / #13824 — the filter that strips bundled engines from output metadata only matches paths in the source tree. In an installed Quarto distribution, bundled engines live under share/extension-subtrees/, not resources/extension-subtrees/, so the filter is a no-op and the bundled julia-engine.js absolute path leaks into the rendered YAML metadata.

Reproduce

Minimal document:

---
title: Basic doc
format: markdown
---

It should be markdown engine.

With Quarto installed via scoop on Windows (any package-manager install with the standard share/ layout reproduces it):

quarto render index.qmd

Output index.md contains a leaked engines: field:

---
engines:
- path: "C:\\Users\\chris\\scoop\\apps\\quarto-prerelease\\current\\share\\extension-subtrees\\julia-engine_extensions\\julia-engine\\julia-engine.js"
title: Basic doc
---

(When the output format is markdown, the markdown writer escapes each \ as a `{=tex}` raw block, so the leaked path also looks visually mangled in the file. Cosmetic side effect of the same leak.)

The same engines block also appears in the metadata logged for other formats (html, etc.), same as the original #13819 report.

Root cause

Two filter sites both check the substring resources/extension-subtrees/:

// Filter out bundled engines from the engines array
if (Array.isArray(metadata.engines)) {
const filteredEngines = metadata.engines.filter((engine) => {
const enginePath = typeof engine === "string" ? engine : engine.path;
// Keep user engines, filter out bundled ones
return !enginePath?.replace(/\\/g, "/").includes(
"resources/extension-subtrees/",
);
});
// Remove the engines key entirely if empty, otherwise assign filtered array
if (filteredEngines.length === 0) {
delete metadata.engines;
} else {
metadata.engines = filteredEngines;
}
}

// Filter out bundled engines from metadata passed to Pandoc
if (Array.isArray(pandocPassedMetadata.engines)) {
const filteredEngines = pandocPassedMetadata.engines.filter((engine) => {
const enginePath = typeof engine === "string" ? engine : engine.path;
if (!enginePath) return true;
return !enginePath.replace(/\\/g, "/").includes(
"resources/extension-subtrees/",
);
});
if (filteredEngines.length === 0) {
delete pandocPassedMetadata.engines;
} else {
pandocPassedMetadata.engines = filteredEngines;
}
}

At runtime, builtinSubtreeExtensions() resolves engine paths through resourcePath("extension-subtrees"), which joins sharePath() with extension-subtrees:

// Path for built-in extensions
function builtinExtensions() {
return resourcePath("extensions");
}
// Path for built-in subtree extensions
export function builtinSubtreeExtensions() {
return resourcePath("extension-subtrees");
}

export function resourcePath(resource?: string): string {
const sharePath = quartoConfig.sharePath();
if (resource) {
return join(sharePath, resource);
} else {
return sharePath;
}
}

So in an installed distro, bundled engine paths look like <install>/share/extension-subtrees/... and never contain the substring resources/extension-subtrees/. Filter is a no-op.

This is a regression of #13819 — PR #13824 fixed the leak when running from the source tree but anchored the matcher to the dev-tree path. Commit 7c0e332 ("cross-plat") later normalized backslashes but kept the same substring.

The existing smoke test https://github.com/quarto-dev/quarto-cli/blob/36e2232273cb9de9af82f2bbb946a0a36808b425/tests/docs/smoke-all/2025/12/22/markdown-engine-no-extension-subtrees.qmd doesn't catch this because CI runs from the source tree, where the dev-tree path does match the filter.

Suggested fix

We could match the unique directory name extension-subtrees/, or better, compare engine paths against builtinSubtreeExtensions() directly so the check is anchored to the actual bundled-extension root regardless of source vs. installed layout.

Related: #14527, #14528 (other facets of internal extensions being treated like user extensions).

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingextensionsrelates to Quarto extensions mechanismmetadataIssues involving metadata resolution in quarto-cli cells, documents, and projects.

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions