Skip to content

Commit c457836

Browse files
committed
Fix tinytex install silently ignoring extraction failures
Check the return value of unzip() during TinyTeX installation. When extraction fails (e.g., tar xf on a .tar.xz without xz-utils), the installer now throws a clear error instead of proceeding to move non-existent files and producing a confusing lstat/NotFound error. For .tar.xz archives, the error message includes a hint to install xz-utils. Also add xz-utils to .deb Recommends and xz to .rpm Recommends since TinyTeX now ships .tar.xz as the preferred format. Fixes #14304
1 parent 01754ec commit c457836

3 files changed

Lines changed: 99 additions & 5 deletions

File tree

package/src/linux/installer.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,15 @@ async function createNfpmConfig(
9292
// Format-specific configuration
9393
if (format === 'deb') {
9494
config.overrides.deb = {
95-
recommends: ["unzip"],
95+
recommends: ["unzip", "xz-utils"],
9696
};
9797
// Add Debian-specific metadata
9898
config.section = "user/text";
9999
config.priority = "optional";
100+
} else if (format === 'rpm') {
101+
config.overrides.rpm = {
102+
recommends: ["xz"],
103+
};
100104
}
101105
return config;
102106
}

src/tools/impl/tinytex.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,15 @@ async function install(
190190
await context.withSpinner(
191191
{ message: `Unzipping ${basename(pkgInfo.filePath)}` },
192192
async () => {
193-
await unzip(pkgInfo.filePath);
193+
const result = await unzip(pkgInfo.filePath);
194+
if (!result.success) {
195+
const hint = pkgInfo.filePath.endsWith(".tar.xz")
196+
? "\nOn Linux, you may need to install xz-utils (e.g., apt install xz-utils)."
197+
: "";
198+
throw new Error(
199+
`Failed to extract ${basename(pkgInfo.filePath)}.${hint}\n${result.stderr}`,
200+
);
201+
}
194202
},
195203
);
196204

tests/unit/tools/tinytex.test.ts

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
*/
66

77
import { unitTest } from "../../test.ts";
8-
import { assert, assertEquals } from "testing/asserts";
9-
import { tinyTexPkgName } from "../../../src/tools/impl/tinytex.ts";
8+
import { assert, assertEquals, assertRejects } from "testing/asserts";
9+
import {
10+
tinyTexInstallable,
11+
tinyTexPkgName,
12+
} from "../../../src/tools/impl/tinytex.ts";
1013
import { getLatestRelease } from "../../../src/tools/github.ts";
11-
import { GitHubRelease } from "../../../src/tools/types.ts";
14+
import { GitHubRelease, InstallContext, PackageInfo } from "../../../src/tools/types.ts";
15+
import { join } from "../../../src/deno_ral/path.ts";
1216

1317
// ---- Pure logic tests for tinyTexPkgName ----
1418

@@ -158,3 +162,81 @@ unitTest(
158162
assertAssetExists(candidates, assetNames, "Windows");
159163
},
160164
);
165+
166+
// ---- Extraction failure tests ----
167+
168+
function createMockContext(workingDir: string): InstallContext {
169+
return {
170+
workingDir,
171+
info: (_msg: string) => {},
172+
withSpinner: async (_options, op) => {
173+
await op();
174+
},
175+
error: (_msg: string) => {},
176+
confirm: async (_msg: string) => true,
177+
download: async (_name: string, _url: string, _target: string) => {},
178+
props: {},
179+
flags: {},
180+
};
181+
}
182+
183+
unitTest(
184+
"install - throws on extraction failure for corrupt archive",
185+
async () => {
186+
const workingDir = Deno.makeTempDirSync({ prefix: "quarto-tinytex-test" });
187+
try {
188+
// Create a corrupt .tar.xz file that tar cannot extract
189+
const badArchive = join(
190+
workingDir,
191+
"TinyTeX-linux-x86_64-v2026.04.tar.xz",
192+
);
193+
Deno.writeTextFileSync(badArchive, "this is not a valid archive");
194+
195+
const pkgInfo: PackageInfo = {
196+
filePath: badArchive,
197+
version: "v2026.04",
198+
};
199+
const context = createMockContext(workingDir);
200+
201+
await assertRejects(
202+
() => tinyTexInstallable.install(pkgInfo, context),
203+
Error,
204+
"Failed to extract",
205+
);
206+
} finally {
207+
Deno.removeSync(workingDir, { recursive: true });
208+
}
209+
},
210+
);
211+
212+
unitTest(
213+
"install - extraction failure for .tar.xz includes xz-utils hint",
214+
async () => {
215+
const workingDir = Deno.makeTempDirSync({ prefix: "quarto-tinytex-test" });
216+
try {
217+
const badArchive = join(
218+
workingDir,
219+
"TinyTeX-linux-x86_64-v2026.04.tar.xz",
220+
);
221+
Deno.writeTextFileSync(badArchive, "this is not a valid archive");
222+
223+
const pkgInfo: PackageInfo = {
224+
filePath: badArchive,
225+
version: "v2026.04",
226+
};
227+
const context = createMockContext(workingDir);
228+
229+
try {
230+
await tinyTexInstallable.install(pkgInfo, context);
231+
throw new Error("Expected install to throw");
232+
} catch (e) {
233+
assert(
234+
e instanceof Error && e.message.includes("xz-utils"),
235+
`Error message should mention xz-utils, got: ${e}`,
236+
);
237+
}
238+
} finally {
239+
Deno.removeSync(workingDir, { recursive: true });
240+
}
241+
},
242+
);

0 commit comments

Comments
 (0)