Skip to content

Commit f7aedbc

Browse files
committed
Add installer for windows setup.exe
1 parent a96660c commit f7aedbc

File tree

3 files changed

+130
-0
lines changed

3 files changed

+130
-0
lines changed

packages/compass-smoke-tests/src/installers/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { installMacDMG } from './mac-dmg';
33
import { installMacZIP } from './mac-zip';
44
import { installWindowsZIP } from './windows-zip';
55
import { installWindowsMSI } from './windows-msi';
6+
import { installWindowsSetup } from './windows-setup';
67

78
export function getInstaller(kind: PackageKind) {
89
if (kind === 'osx_dmg') {
@@ -13,6 +14,8 @@ export function getInstaller(kind: PackageKind) {
1314
return installWindowsZIP;
1415
} else if (kind === 'windows_msi') {
1516
return installWindowsMSI;
17+
} else if (kind === 'windows_setup') {
18+
return installWindowsSetup;
1619
} else {
1720
throw new Error(`Installer for '${kind}' is not yet implemented`);
1821
}
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: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
}: InstallablePackage): InstalledAppInfo {
24+
const { LOCALAPPDATA: LOCAL_APPDATA_PATH } = process.env;
25+
assert(
26+
typeof LOCAL_APPDATA_PATH === 'string',
27+
'Expected a LOCALAPPDATA environment injected by the shell'
28+
);
29+
const installDirectory = path.resolve(LOCAL_APPDATA_PATH, appName);
30+
31+
function uninstall({ expectMissing = false }: UninstallOptions = {}) {
32+
const registryEntry = windowsRegistry.query(
33+
`HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${appName}`
34+
);
35+
if (registryEntry) {
36+
if (expectMissing) {
37+
console.warn(
38+
'Found an existing registry entry (likely from a previous run)'
39+
);
40+
}
41+
const { UninstallString: uninstallCommand } = registryEntry;
42+
assert(
43+
typeof uninstallCommand === 'string',
44+
'Expected an UninstallString in the registry entry'
45+
);
46+
console.log(`Running command to uninstall: ${uninstallCommand}`);
47+
cp.execSync(uninstallCommand, { stdio: 'inherit' });
48+
}
49+
// Removing the any remaining files
50+
fs.rmSync(installDirectory, { recursive: true, force: true });
51+
}
52+
53+
uninstall({ expectMissing: true });
54+
55+
// Assert the app is not on the filesystem at the expected location
56+
assert(
57+
!fs.existsSync(installDirectory),
58+
`Delete any existing installations first (found ${installDirectory})`
59+
);
60+
61+
// Run the installer
62+
console.warn(
63+
"Installing globally, since we haven't discovered a way to specify an install path"
64+
);
65+
execute(filepath, []);
66+
67+
const appPath = path.resolve(installDirectory, `${appName}.exe`);
68+
execute(appPath, ['--version']);
69+
70+
return {
71+
appPath: installDirectory,
72+
uninstall,
73+
};
74+
}

0 commit comments

Comments
 (0)