Skip to content

Commit 16fda9d

Browse files
ycliu613meta-codesync[bot]
authored andcommitted
Add MSE detection and version validation
Summary: Add platform detection and version validation utilities for Meta Spatial Editor (MSE). These functions detect if MSE is installed and validate version compatibility. Functions added: - `normalizeVersion()` - Normalizes version strings to major.minor.patch format - `detectPlatform()` - Detects macOS, Windows, or Linux - `detectMSEVersion()` - Finds installed MSE version via plist (macOS), registry (Windows), or CLI (Linux) - `isVersionSufficient()` - Validates installed version meets minimum requirements Also includes comprehensive unit tests for all detection functions. This is part 2 of 4 for the MSE auto-install feature. Reviewed By: wangpingsx Differential Revision: D91470650 fbshipit-source-id: 4bad43d7b47570da076346de9a3ebe7e2f2e33a7
1 parent 8a9e1db commit 16fda9d

File tree

2 files changed

+236
-0
lines changed

2 files changed

+236
-0
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import { execSync } from 'child_process';
9+
import fs from 'fs';
10+
import path from 'path';
11+
import semver from 'semver';
12+
import {
13+
MSE_MIN_VERSION,
14+
MSE_INSTALL_PATHS,
15+
MSE_LINUX_CLI_NAME,
16+
} from './mse-config.js';
17+
import type { Platform } from './types.js';
18+
19+
/** Normalize version to major.minor.patch for comparison */
20+
export function normalizeVersion(v: string): string {
21+
const parts = v.split('.');
22+
while (parts.length < 3) parts.push('0');
23+
return parts.slice(0, 3).join('.');
24+
}
25+
26+
export function detectPlatform(): Platform {
27+
const p = process.platform;
28+
return p === 'darwin' || p === 'win32' ? p : 'linux';
29+
}
30+
31+
/** Get highest version directory (e.g., v20) from Windows install path */
32+
function getHighestVersion(directoryPath: string): string | null {
33+
try {
34+
const entries = fs.readdirSync(directoryPath, { withFileTypes: true });
35+
const versions = entries
36+
.filter((e) => e.isDirectory() && /^v\d+$/.test(e.name))
37+
.map((e) => parseInt(e.name.slice(1), 10));
38+
return versions.length ? `v${Math.max(...versions)}` : null;
39+
} catch {
40+
return null;
41+
}
42+
}
43+
44+
function getMacOSAppVersion(appPath: string): string | null {
45+
const plistPath = path.join(appPath, 'Contents', 'Info.plist');
46+
try {
47+
const output = execSync(
48+
`plutil -extract CFBundleShortVersionString raw "${plistPath}"`,
49+
{ encoding: 'utf-8', timeout: 5000 },
50+
);
51+
return output.trim();
52+
} catch {
53+
return null;
54+
}
55+
}
56+
57+
function getWindowsAppVersion(basePath: string): string | null {
58+
const highestVersion = getHighestVersion(basePath);
59+
if (!highestVersion) return null;
60+
const vNum = parseInt(highestVersion.substring(1), 10);
61+
return `${vNum}.0.0`;
62+
}
63+
64+
function parseVersionFromOutput(output: string): string | null {
65+
const match = output.match(/v?(\d+\.\d+\.\d+)/);
66+
return match ? match[1] : null;
67+
}
68+
69+
export async function detectMSEVersion(platform: Platform): Promise<string | null> {
70+
if (platform === 'linux') {
71+
const cliPath = path.join(MSE_INSTALL_PATHS.linux, MSE_LINUX_CLI_NAME);
72+
73+
if (fs.existsSync(cliPath)) {
74+
try {
75+
const output = execSync(`"${cliPath}" --version`, { encoding: 'utf-8', timeout: 5000 });
76+
return parseVersionFromOutput(output);
77+
} catch {
78+
return null;
79+
}
80+
}
81+
82+
try {
83+
const output = execSync('MetaSpatialEditorCLI --version', { encoding: 'utf-8', timeout: 5000 });
84+
return parseVersionFromOutput(output);
85+
} catch {
86+
return null;
87+
}
88+
}
89+
90+
const installPath = MSE_INSTALL_PATHS[platform];
91+
if (!installPath || !fs.existsSync(installPath)) return null;
92+
93+
return platform === 'darwin' ? getMacOSAppVersion(installPath) : getWindowsAppVersion(installPath);
94+
}
95+
96+
export function isVersionSufficient(installed: string, required: string = MSE_MIN_VERSION): boolean {
97+
try {
98+
return semver.gte(installed, required);
99+
} catch {
100+
return false;
101+
}
102+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
9+
import {
10+
detectPlatform,
11+
isVersionSufficient,
12+
normalizeVersion,
13+
} from '../src/mse-installer.js';
14+
import {
15+
MSE_MIN_VERSION,
16+
MSE_APP_NAMES,
17+
getAppcastUrl,
18+
} from '../src/mse-config.js';
19+
20+
describe('MSE Installer', () => {
21+
let originalPlatform: string;
22+
23+
beforeEach(() => {
24+
originalPlatform = process.platform;
25+
});
26+
27+
afterEach(() => {
28+
Object.defineProperty(process, 'platform', { value: originalPlatform });
29+
});
30+
31+
describe('normalizeVersion', () => {
32+
it('should pad version to 3 parts', () => {
33+
expect(normalizeVersion('11')).toBe('11.0.0');
34+
expect(normalizeVersion('11.0')).toBe('11.0.0');
35+
expect(normalizeVersion('11.0.0')).toBe('11.0.0');
36+
});
37+
38+
it('should truncate version to 3 parts', () => {
39+
expect(normalizeVersion('11.0.0.10.576')).toBe('11.0.0');
40+
expect(normalizeVersion('9.1.2.3.4.5')).toBe('9.1.2');
41+
});
42+
43+
it('should handle single digit versions', () => {
44+
expect(normalizeVersion('9')).toBe('9.0.0');
45+
});
46+
47+
it('should preserve full 3-part versions', () => {
48+
expect(normalizeVersion('9.1.2')).toBe('9.1.2');
49+
expect(normalizeVersion('12.5.3')).toBe('12.5.3');
50+
});
51+
});
52+
53+
describe('detectPlatform', () => {
54+
it('should return darwin for macOS', () => {
55+
Object.defineProperty(process, 'platform', { value: 'darwin' });
56+
expect(detectPlatform()).toBe('darwin');
57+
});
58+
59+
it('should return win32 for Windows', () => {
60+
Object.defineProperty(process, 'platform', { value: 'win32' });
61+
expect(detectPlatform()).toBe('win32');
62+
});
63+
64+
it('should return linux for Linux', () => {
65+
Object.defineProperty(process, 'platform', { value: 'linux' });
66+
expect(detectPlatform()).toBe('linux');
67+
});
68+
69+
it('should return linux for unknown platforms', () => {
70+
Object.defineProperty(process, 'platform', { value: 'freebsd' });
71+
expect(detectPlatform()).toBe('linux');
72+
});
73+
});
74+
75+
describe('isVersionSufficient', () => {
76+
it('should return true for versions >= minimum', () => {
77+
expect(isVersionSufficient('9.0.0')).toBe(true);
78+
expect(isVersionSufficient('9.0.1')).toBe(true);
79+
expect(isVersionSufficient('10.0.0')).toBe(true);
80+
expect(isVersionSufficient('11.0.0')).toBe(true);
81+
});
82+
83+
it('should return false for versions < minimum', () => {
84+
expect(isVersionSufficient('8.0.0')).toBe(false);
85+
expect(isVersionSufficient('8.9.9')).toBe(false);
86+
expect(isVersionSufficient('1.0.0')).toBe(false);
87+
});
88+
89+
it('should accept custom minimum version', () => {
90+
expect(isVersionSufficient('10.0.0', '10.0.0')).toBe(true);
91+
expect(isVersionSufficient('9.9.9', '10.0.0')).toBe(false);
92+
});
93+
94+
it('should return false for invalid versions', () => {
95+
expect(isVersionSufficient('invalid')).toBe(false);
96+
expect(isVersionSufficient('')).toBe(false);
97+
});
98+
});
99+
100+
describe('MSE Config', () => {
101+
it('should have correct app names for all platforms', () => {
102+
expect(MSE_APP_NAMES.darwin).toBe('cosmo_studio_for_macos');
103+
expect(MSE_APP_NAMES.win32).toBe('cosmo_studio_for_windows');
104+
expect(MSE_APP_NAMES.linux).toBe('cosmo_studio_cli_for_linux');
105+
});
106+
107+
it('should generate correct appcast URLs', () => {
108+
expect(getAppcastUrl('darwin')).toContain('cosmo_studio_for_macos');
109+
expect(getAppcastUrl('darwin')).toContain('appcast.xml');
110+
expect(getAppcastUrl('win32')).toContain('cosmo_studio_for_windows');
111+
expect(getAppcastUrl('linux')).toContain('cosmo_studio_cli_for_linux');
112+
});
113+
114+
it('should have minimum version set', () => {
115+
expect(MSE_MIN_VERSION).toBe('9.0.0');
116+
});
117+
});
118+
119+
describe('Version comparison edge cases', () => {
120+
it('should correctly compare normalized versions', () => {
121+
// These should be considered equal after normalization
122+
const v1 = normalizeVersion('11.0');
123+
const v2 = normalizeVersion('11.0.0.10.576');
124+
expect(v1).toBe(v2);
125+
});
126+
127+
it('should handle version upgrade detection', () => {
128+
const installed = normalizeVersion('10.0');
129+
const latest = normalizeVersion('11.0.0.10.576');
130+
expect(installed).not.toBe(latest);
131+
expect(isVersionSufficient(installed, latest)).toBe(false);
132+
});
133+
});
134+
});

0 commit comments

Comments
 (0)