Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/tools/impl/verapdf.ts
12921 views
1
/*
2
* verapdf.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { existsSync, safeRemoveSync } from "../../deno_ral/fs.ts";
8
import { basename, join } from "../../deno_ral/path.ts";
9
import { warning } from "../../deno_ral/log.ts";
10
11
import { unzip } from "../../core/zip.ts";
12
import { execProcess } from "../../core/process.ts";
13
import { quartoDataDir } from "../../core/appdirs.ts";
14
15
import {
16
InstallableTool,
17
InstallContext,
18
kUpdatePath,
19
PackageInfo,
20
RemotePackageInfo,
21
} from "../types.ts";
22
import { createToolSymlink, removeToolSymlink } from "../tools.ts";
23
import { isWindows } from "../../deno_ral/platform.ts";
24
25
// Download base URL (quarto.org redirects to S3 bucket)
26
const kDownloadBaseUrl = "https://quarto.org/download";
27
const kDefaultVersion = "1.28.2";
28
29
// Minimum Java version required
30
const kMinJavaVersion = 8;
31
32
// Highest Java LTS version officially supported by this veraPDF version.
33
// Update this when bumping kDefaultVersion to a release that supports a newer LTS.
34
const kMaxSupportedLtsVersion = 21;
35
36
// The name of the file that we use to store the installed version
37
const kVersionFileName = "version";
38
39
// Check if a Java version is a Long-Term Support (LTS) release.
40
// LTS versions: 8, 11, then every 2 years starting from 17 (17, 21, 25, 29, ...)
41
function isLtsJavaVersion(version: number): boolean {
42
if (version === 8 || version === 11) return true;
43
if (version >= 17 && (version - 17) % 4 === 0) return true;
44
return false;
45
}
46
47
export const verapdfInstallable: InstallableTool = {
48
name: "VeraPDF",
49
prereqs: [{
50
check: async (context) => {
51
const javaVersion = await getJavaVersion();
52
context.props.javaVersion = javaVersion;
53
54
// Block installation if Java is not installed or version is too old
55
if (javaVersion === undefined || javaVersion < kMinJavaVersion) {
56
return false;
57
}
58
59
// Warn but allow installation for non-LTS or too-new LTS Java versions
60
if (!isLtsJavaVersion(javaVersion)) {
61
const supportedVersions = Array.from(
62
{ length: kMaxSupportedLtsVersion - 8 + 1 },
63
(_, i) => i + 8,
64
).filter(isLtsJavaVersion).join(", ");
65
warning(
66
`Java ${javaVersion} is not a Long-Term Support (LTS) version. ` +
67
`veraPDF ${kDefaultVersion} officially supports Java ${supportedVersions}. ` +
68
`Installation will proceed, but you may encounter issues.`,
69
);
70
} else if (javaVersion > kMaxSupportedLtsVersion) {
71
warning(
72
`Java ${javaVersion} is newer than veraPDF ${kDefaultVersion} officially supports. ` +
73
`Installation will proceed, but you may encounter issues.`,
74
);
75
}
76
77
return true;
78
},
79
os: ["darwin", "linux", "windows"],
80
message: (context) => {
81
const javaVersion = context.props.javaVersion as number | undefined;
82
if (javaVersion === undefined) {
83
return `Java is not installed. veraPDF requires Java ${kMinJavaVersion} or later.`;
84
} else {
85
return `Java ${javaVersion} is too old. veraPDF requires Java ${kMinJavaVersion} or later.`;
86
}
87
},
88
}],
89
installed,
90
installDir,
91
binDir,
92
installedVersion,
93
latestRelease,
94
preparePackage,
95
install,
96
afterInstall,
97
uninstall,
98
};
99
100
async function getJavaVersion(): Promise<number | undefined> {
101
try {
102
const result = await execProcess({
103
cmd: "java",
104
args: ["-version"],
105
stderr: "piped",
106
});
107
if (!result.success) {
108
return undefined;
109
}
110
// Java outputs version to stderr
111
// Parse: openjdk version "17.0.1" or java version "1.8.0_301"
112
const match = result.stderr?.match(/version "(\d+)(?:\.(\d+))?/);
113
if (match) {
114
const major = parseInt(match[1]);
115
// Java 8 reports as "1.8", newer versions report as "11", "17", etc.
116
return major === 1 ? parseInt(match[2]) : major;
117
}
118
} catch {
119
return undefined;
120
}
121
return undefined;
122
}
123
124
function verapdfInstallDir(): string {
125
return quartoDataDir("verapdf");
126
}
127
128
async function installed(): Promise<boolean> {
129
const dir = verapdfInstallDir();
130
const verapdfBin = isWindows
131
? join(dir, "verapdf.bat")
132
: join(dir, "verapdf");
133
return existsSync(verapdfBin);
134
}
135
136
async function installDir(): Promise<string | undefined> {
137
if (await installed()) {
138
return verapdfInstallDir();
139
}
140
return undefined;
141
}
142
143
async function binDir(): Promise<string | undefined> {
144
if (await installed()) {
145
return verapdfInstallDir();
146
}
147
return undefined;
148
}
149
150
async function installedVersion(): Promise<string | undefined> {
151
const dir = verapdfInstallDir();
152
const versionFile = join(dir, kVersionFileName);
153
if (existsSync(versionFile)) {
154
return await Deno.readTextFile(versionFile);
155
}
156
return undefined;
157
}
158
159
function noteInstalledVersion(version: string): void {
160
const dir = verapdfInstallDir();
161
const versionFile = join(dir, kVersionFileName);
162
Deno.writeTextFileSync(versionFile, version);
163
}
164
165
async function latestRelease(): Promise<RemotePackageInfo> {
166
// Use pinned version from configuration or default
167
const version = Deno.env.get("VERAPDF") || kDefaultVersion;
168
const filename = `verapdf-greenfield-${version}-installer.zip`;
169
// Use quarto.org redirect with version in path (points to S3 bucket)
170
const downloadUrl = `${kDownloadBaseUrl}/verapdf/${version}/${filename}`;
171
172
return {
173
url: downloadUrl,
174
version,
175
assets: [{ name: filename, url: downloadUrl }],
176
};
177
}
178
179
async function preparePackage(context: InstallContext): Promise<PackageInfo> {
180
const pkgInfo = await latestRelease();
181
const version = pkgInfo.version;
182
const asset = pkgInfo.assets[0];
183
const filePath = join(context.workingDir, asset.name);
184
185
await context.download(`VeraPDF ${version}`, asset.url, filePath);
186
return { filePath, version };
187
}
188
189
async function install(
190
pkgInfo: PackageInfo,
191
context: InstallContext,
192
): Promise<void> {
193
const targetDir = verapdfInstallDir();
194
195
// Extract the downloaded ZIP
196
await context.withSpinner(
197
{ message: `Extracting ${basename(pkgInfo.filePath)}` },
198
async () => {
199
await unzip(pkgInfo.filePath);
200
},
201
);
202
203
// Find the installer JAR in the extracted files
204
// The ZIP extracts to a subdirectory like "verapdf-greenfield-1.28.2/"
205
const extractedDir = context.workingDir;
206
let installerJar: string | undefined;
207
208
for await (const entry of Deno.readDir(extractedDir)) {
209
if (entry.isDirectory && entry.name.startsWith("verapdf-")) {
210
// Look inside the extracted subdirectory for the JAR
211
const subDir = join(extractedDir, entry.name);
212
for await (const subEntry of Deno.readDir(subDir)) {
213
if (subEntry.isFile && subEntry.name.endsWith(".jar")) {
214
installerJar = join(subDir, subEntry.name);
215
break;
216
}
217
}
218
if (installerJar) break;
219
} else if (entry.isFile && entry.name.endsWith(".jar")) {
220
// JAR might be at the root level
221
installerJar = join(extractedDir, entry.name);
222
break;
223
}
224
}
225
226
if (!installerJar) {
227
context.error(
228
"Could not find veraPDF installer JAR in the downloaded package",
229
);
230
return Promise.reject();
231
}
232
233
// Create auto-install.xml for headless installation
234
// Panel IDs and pack names must match the IzPack installer configuration
235
const autoInstallXml = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
236
<AutomatedInstallation langpack="eng">
237
<com.izforge.izpack.panels.htmlhello.HTMLHelloPanel id="welcome"/>
238
<com.izforge.izpack.panels.target.TargetPanel id="install_dir">
239
<installpath>${targetDir}</installpath>
240
</com.izforge.izpack.panels.target.TargetPanel>
241
<com.izforge.izpack.panels.packs.PacksPanel id="sdk_pack_select">
242
<pack index="0" name="veraPDF GUI" selected="false"/>
243
<pack index="1" name="veraPDF Mac and *nix Scripts" selected="true"/>
244
<pack index="2" name="veraPDF Batch files" selected="true"/>
245
<pack index="3" name="veraPDF Validation model" selected="true"/>
246
<pack index="4" name="veraPDF Documentation" selected="false"/>
247
<pack index="5" name="veraPDF Sample Plugins" selected="false"/>
248
</com.izforge.izpack.panels.packs.PacksPanel>
249
<com.izforge.izpack.panels.install.InstallPanel id="install"/>
250
<com.izforge.izpack.panels.finish.FinishPanel id="finish"/>
251
</AutomatedInstallation>`;
252
253
const autoInstallPath = join(extractedDir, "auto-install.xml");
254
await Deno.writeTextFile(autoInstallPath, autoInstallXml);
255
256
// Run the installer in headless mode
257
// Pass the auto-install XML path directly to the installer
258
await context.withSpinner(
259
{ message: "Installing veraPDF" },
260
async () => {
261
const result = await execProcess({
262
cmd: "java",
263
args: ["-jar", installerJar!, autoInstallPath],
264
stdout: "piped",
265
stderr: "piped",
266
});
267
268
if (!result.success) {
269
const errorMsg = result.stderr || "Unknown error";
270
throw new Error(`veraPDF installation failed: ${errorMsg}`);
271
}
272
},
273
);
274
275
// Note the installed version
276
noteInstalledVersion(pkgInfo.version);
277
}
278
279
async function afterInstall(context: InstallContext): Promise<boolean> {
280
if (context.flags[kUpdatePath]) {
281
const dir = verapdfInstallDir();
282
const verapdfBin = isWindows
283
? join(dir, "verapdf.bat")
284
: join(dir, "verapdf");
285
286
await context.withSpinner(
287
{ message: "Updating PATH" },
288
async () => {
289
await createToolSymlink(verapdfBin, "verapdf", context);
290
},
291
);
292
293
// On Windows, a restart may be needed
294
return isWindows;
295
}
296
297
return false;
298
}
299
300
async function uninstall(context: InstallContext): Promise<void> {
301
// Remove symlinks if they exist
302
if (context.flags[kUpdatePath]) {
303
await removeToolSymlink("verapdf");
304
}
305
306
await context.withSpinner(
307
{ message: "Removing veraPDF" },
308
async () => {
309
const dir = verapdfInstallDir();
310
if (existsSync(dir)) {
311
safeRemoveSync(dir, { recursive: true });
312
}
313
},
314
);
315
}
316
317