Skip to content

Commit d6bb0d2

Browse files
committed
Add installer for windows setup.exe
1 parent 139fa5d commit d6bb0d2

File tree

3 files changed

+131
-0
lines changed

3 files changed

+131
-0
lines changed

packages/compass-smoke-tests/src/cli.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { installMacDMG } from './installers/mac-dmg';
2222
import { installMacZIP } from './installers/mac-zip';
2323
import { installWindowsZIP } from './installers/windows-zip';
2424
import { installWindowsMSI } from './installers/windows-msi';
25+
import { installWindowsSetup } from './installers/windows-setup';
2526

2627
const SUPPORTED_PLATFORMS = ['win32', 'darwin', 'linux'] as const;
2728
const SUPPORTED_ARCHS = ['x64', 'arm64'] as const;
@@ -172,6 +173,8 @@ function getInstaller(kind: PackageKind) {
172173
return installWindowsZIP;
173174
} else if (kind === 'windows_msi') {
174175
return installWindowsMSI;
176+
} else if (kind === 'windows_setup') {
177+
return installWindowsSetup;
175178
} else {
176179
throw new Error(`Installer for '${kind}' is not yet implemented`);
177180
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import cp from 'node:child_process';
2+
3+
type RegistryEntry = {
4+
key: string;
5+
type: string;
6+
value: string;
7+
};
8+
9+
function isRegistryEntry(value: unknown): value is RegistryEntry {
10+
return (
11+
typeof value === 'object' &&
12+
value !== null &&
13+
'key' in value &&
14+
typeof value.key === 'string' &&
15+
'type' in value &&
16+
typeof value.type === 'string' &&
17+
'value' in value &&
18+
typeof value.value === 'string'
19+
);
20+
}
21+
22+
/**
23+
* Parse the putput of a "reg query" call.
24+
*/
25+
function parseQueryRegistryOutput(output: string): RegistryEntry[] {
26+
const result = output.matchAll(
27+
/^\s*(?<key>\w+) +(?<type>\w+) *(?<value>.*)$/gm
28+
);
29+
return [...result].map(({ groups }) => groups).filter(isRegistryEntry);
30+
}
31+
32+
/**
33+
* Query the Windows registry by key.
34+
*/
35+
export function query(key: string) {
36+
const result = cp.spawnSync('reg', ['query', key], { encoding: 'utf8' });
37+
if (result.status === 0) {
38+
const entries = parseQueryRegistryOutput(result.stdout);
39+
return Object.fromEntries(entries.map(({ key, value }) => [key, value]));
40+
} else if (
41+
result.status === 1 &&
42+
result.stderr.trim() ===
43+
'ERROR: The system was unable to find the specified registry key or value.'
44+
) {
45+
return null;
46+
} else {
47+
throw Error(
48+
`Expected either an entry or status code 1, got status ${
49+
result.status ?? '?'
50+
}: ${result.stderr}`
51+
);
52+
}
53+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import path from 'node:path';
2+
import assert from 'node:assert/strict';
3+
import fs from 'node:fs';
4+
import cp from 'node:child_process';
5+
6+
import type { InstalledAppInfo, InstallablePackage } from './types';
7+
import { execute } from '../execute';
8+
import * as windowsRegistry from './windows-registry';
9+
10+
type UninstallOptions = {
11+
/**
12+
* Expect the app to already be uninstalled, warn if that's not the case.
13+
*/
14+
expectMissing?: boolean;
15+
};
16+
17+
/**
18+
* Install using the Windows installer.
19+
*/
20+
export function installWindowsSetup({
21+
appName,
22+
filepath,
23+
destinationPath,
24+
}: InstallablePackage): InstalledAppInfo {
25+
const { LOCALAPPDATA: LOCAL_APPDATA_PATH } = process.env;
26+
assert(
27+
typeof LOCAL_APPDATA_PATH === 'string',
28+
'Expected a LOCALAPPDATA environment injected by the shell'
29+
);
30+
const installDirectory = path.resolve(LOCAL_APPDATA_PATH, appName);
31+
32+
function uninstall({ expectMissing = false }: UninstallOptions = {}) {
33+
const registryEntry = windowsRegistry.query(
34+
`HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${appName}`
35+
);
36+
if (registryEntry) {
37+
if (expectMissing) {
38+
console.warn(
39+
'Found an existing registry entry (likely from a previous run)'
40+
);
41+
}
42+
const { UninstallString: uninstallCommand } = registryEntry;
43+
assert(
44+
typeof uninstallCommand === 'string',
45+
'Expected an UninstallString in the registry entry'
46+
);
47+
console.log(`Running command to uninstall: ${uninstallCommand}`);
48+
cp.execSync(uninstallCommand, { stdio: 'inherit' });
49+
}
50+
// Removing the any remaining files
51+
fs.rmSync(installDirectory, { recursive: true, force: true });
52+
}
53+
54+
uninstall({ expectMissing: true });
55+
56+
// Assert the app is not on the filesystem at the expected location
57+
assert(
58+
!fs.existsSync(installDirectory),
59+
`Delete any existing installations first (found ${installDirectory})`
60+
);
61+
62+
// Run the installer
63+
console.warn(
64+
"Installing globally, since we haven't discovered a way to specify an install path"
65+
);
66+
execute(filepath, []);
67+
68+
const appPath = path.resolve(installDirectory, `${appName}.exe`);
69+
execute(appPath, ['--version']);
70+
71+
return {
72+
appPath: installDirectory,
73+
uninstall,
74+
};
75+
}

0 commit comments

Comments
 (0)