arm64 support

This commit is contained in:
Natsumi
2025-09-02 22:30:46 +12:00
parent d4bf9ef0f6
commit f1c8c0fa65
9 changed files with 174 additions and 146 deletions

View File

@@ -13,8 +13,10 @@
"test:coverage": "jest --coverage",
"prod": "cross-env PLATFORM=windows webpack --config webpack.config.js --mode production",
"prod-linux": "cross-env PLATFORM=linux webpack --config webpack.config.js --mode production",
"build-electron": "node ./src-electron/download-dotnet-runtime.js && node ./src-electron/patch-package-version.js && electron-builder --publish never",
"postbuild-electron": "node ./src-electron/patch-node-api-dotnet.js && node ./src-electron/rename-builds.js",
"build-electron": "node ./src-electron/download-dotnet-runtime.js --arch=x64 && node ./src-electron/patch-package-version.js && electron-builder --x64 --publish never",
"build-electron-arm64": "node ./src-electron/download-dotnet-runtime.js --arch=arm64 && node ./src-electron/patch-package-version.js && electron-builder --arm64 --publish never",
"postbuild-electron": "node ./src-electron/patch-node-api-dotnet.js --arch=x64 && node ./src-electron/rename-builds.js --arch=x64",
"postbuild-electron-arm64": "node ./src-electron/patch-node-api-dotnet.js --arch=arm64 && node ./src-electron/rename-builds.js --arch=arm64",
"start-electron": "electron ."
},
"repository": {

View File

@@ -2,30 +2,9 @@ const fs = require('fs');
const path = require('path');
const https = require('https');
const { spawnSync } = require('child_process');
let runnerArch = process.arch.toString();
let runnerPlatform = process.platform.toString();
const args = process.argv.slice(2);
for (let i = 0; i < args.length; i++) {
if (args[i] === '--arch' && i + 1 < args.length) {
runnerArch = args[i + 1];
} else if (args[i] === '--platform' && i + 1 < args.length) {
runnerPlatform = args[i + 1];
}
}
let platform = '';
if (runnerPlatform === 'linux') {
platform = 'linux';
} else if (runnerPlatform === 'darwin') {
platform = 'osx';
} else if (runnerPlatform === 'win32') {
platform = 'win';
} else {
throw new Error(`Unsupported platform: ${runnerPlatform}`);
}
const { getArchAndPlatform } = require('./utils');
const DOTNET_VERSION = '9.0.8';
const DOTNET_RUNTIME_URL = `https://builds.dotnet.microsoft.com/dotnet/Runtime/${DOTNET_VERSION}/dotnet-runtime-${DOTNET_VERSION}-${platform}-${runnerArch}.tar.gz`;
const DOTNET_RUNTIME_DIR = path.join(
__dirname,
'..',
@@ -78,78 +57,80 @@ async function extractTarGz(tarGzPath, extractDir) {
});
}
async function main() {
if (platform !== 'linux' && platform !== 'darwin') {
console.log('Skipping .NET runtime download on non supported platform');
return;
async function downloadDotnetRuntime(arch, platform) {
if (!arch || !platform) {
throw new Error('Architecture and platform must be specified');
}
console.log(
`Downloading .NET ${DOTNET_VERSION}-${platform}-${runnerArch} runtime...`
);
let dotnetPlatform = '';
if (platform === 'linux') {
dotnetPlatform = 'linux';
} else if (platform === 'darwin') {
dotnetPlatform = 'osx';
} else if (platform === 'win32') {
dotnetPlatform = 'win';
} else {
throw new Error(`Unsupported platform: ${platform}`);
}
if (!fs.existsSync(DOTNET_RUNTIME_DIR)) {
fs.mkdirSync(DOTNET_RUNTIME_DIR, { recursive: true });
}
console.log(
`Downloading .NET ${DOTNET_VERSION}-${dotnetPlatform}-${arch} runtime...`
);
const tarGzPath = path.join(DOTNET_RUNTIME_DIR, 'dotnet-runtime.tar.gz');
const dotnetRuntimeUrl = `https://builds.dotnet.microsoft.com/dotnet/Runtime/${DOTNET_VERSION}/dotnet-runtime-${DOTNET_VERSION}-${dotnetPlatform}-${arch}.tar.gz`;
try {
// Download .NET runtime
await downloadFile(DOTNET_RUNTIME_URL, tarGzPath);
console.log('Download completed');
// Download .NET runtime
await downloadFile(dotnetRuntimeUrl, tarGzPath);
console.log('Download completed');
// Extract .NET runtime to a temporary directory first
const tempExtractDir = path.join(DOTNET_RUNTIME_DIR, 'temp');
if (!fs.existsSync(tempExtractDir)) {
fs.mkdirSync(tempExtractDir, { recursive: true });
}
console.log('Extracting .NET runtime...');
await extractTarGz(tarGzPath, tempExtractDir);
console.log('Extraction completed');
// Clean up tar.gz file
fs.unlinkSync(tarGzPath);
console.log('Cleanup completed');
// Ensure the dotnet executable is executable
const extractedDotnet = path.join(tempExtractDir, 'dotnet');
fs.chmodSync(extractedDotnet, 0o755);
// Move all other files to the root of dotnet-runtime
const files = fs.readdirSync(tempExtractDir);
for (const file of files) {
const sourcePath = path.join(tempExtractDir, file);
const targetPath = path.join(DOTNET_RUNTIME_DIR, file);
if (fs.existsSync(targetPath)) {
if (fs.lstatSync(sourcePath).isDirectory()) {
// Remove existing directory and move new one
fs.rmSync(targetPath, { recursive: true, force: true });
} else {
// Remove existing file
fs.unlinkSync(targetPath);
}
}
fs.renameSync(sourcePath, targetPath);
}
// Clean up temp directory
fs.rmSync(tempExtractDir, { recursive: true, force: true });
console.log(
`.NET runtime downloaded and extracted to: ${DOTNET_RUNTIME_DIR}`
);
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
// Extract .NET runtime to a temporary directory first
const tempExtractDir = path.join(DOTNET_RUNTIME_DIR, 'temp');
if (!fs.existsSync(tempExtractDir)) {
fs.mkdirSync(tempExtractDir, { recursive: true });
}
console.log('Extracting .NET runtime...');
await extractTarGz(tarGzPath, tempExtractDir);
console.log('Extraction completed');
// Clean up tar.gz file
fs.unlinkSync(tarGzPath);
console.log('Cleanup completed');
// Ensure the dotnet executable is executable
const extractedDotnet = path.join(tempExtractDir, 'dotnet');
fs.chmodSync(extractedDotnet, 0o755);
// Move all other files to the root of dotnet-runtime
const files = fs.readdirSync(tempExtractDir);
for (const file of files) {
const sourcePath = path.join(tempExtractDir, file);
const targetPath = path.join(DOTNET_RUNTIME_DIR, file);
if (fs.existsSync(targetPath)) {
if (fs.lstatSync(sourcePath).isDirectory()) {
// Remove existing directory and move new one
fs.rmSync(targetPath, { recursive: true, force: true });
} else {
// Remove existing file
fs.unlinkSync(targetPath);
}
}
fs.renameSync(sourcePath, targetPath);
}
// Clean up temp directory
fs.rmSync(tempExtractDir, { recursive: true, force: true });
console.log(
`.NET runtime downloaded and extracted to: ${DOTNET_RUNTIME_DIR}`
);
}
if (require.main === module) {
main();
}
module.exports = { downloadFile, extractTarGz };
const { arch, platform } = getArchAndPlatform();
downloadDotnetRuntime(arch, platform);

View File

@@ -16,16 +16,11 @@ const https = require('https');
//app.disableHardwareAcceleration();
if (process.platform === 'linux') {
const bundledDotNetPath = path.join(process.resourcesPath, 'dotnet-runtime');
if (fs.existsSync(bundledDotNetPath)) {
// Include bundled .NET runtime
const bundledDotNetPath = path.join(
process.resourcesPath,
'dotnet-runtime'
);
if (fs.existsSync(bundledDotNetPath)) {
process.env.DOTNET_ROOT = bundledDotNetPath;
process.env.PATH = `${bundledDotNetPath}:${process.env.PATH}`;
}
process.env.DOTNET_ROOT = bundledDotNetPath;
process.env.PATH = `${bundledDotNetPath}:${process.env.PATH}`;
} else if (process.platform === 'darwin') {
const dotnetPath = path.join('/usr/local/share/dotnet');
const dotnetPathArm = path.join('/usr/local/share/dotnet/x64');
@@ -154,6 +149,10 @@ if (!gotTheLock) {
});
}
ipcMain.handle('getArch', () => {
return process.arch.toString();
});
ipcMain.handle('applyWindowSettings', (event, position, size, state) => {
if (position) {
mainWindow.setPosition(parseInt(position.x), parseInt(position.y));
@@ -812,6 +811,10 @@ function isDotNetInstalled() {
const result = spawnSync(dotnetPath, ['--list-runtimes'], {
encoding: 'utf-8'
});
if (result.error) {
console.error('Error checking .NET runtimes:', result.error);
return false;
}
return result.stdout?.includes('.NETCore.App 9.0');
}

View File

@@ -1,5 +1,6 @@
const fs = require('fs');
const path = require('path');
const { getArchAndPlatform } = require('./utils');
function patchFile(filePath) {
if (!fs.existsSync(filePath)) {
@@ -30,25 +31,30 @@ managedHostPath = managedHostPath.indexOf('app.asar.unpacked') < 0 ?
return false;
}
// Paths to patch
let platformName = '';
switch (process.platform) {
case 'win32':
platformName = 'win';
break;
case 'darwin':
platformName = 'mac';
break;
case 'linux':
platformName = 'linux';
break;
function patchNodeApiDotNet(arch, platform) {
let platformName = '';
switch (platform) {
case 'win32':
platformName = 'win';
break;
case 'darwin':
platformName = 'mac';
break;
case 'linux':
platformName = 'linux';
break;
}
if (arch === 'arm64') {
platformName += '-arm64';
}
const postBuildPath = path.join(
__dirname,
`./../build/${platformName}-unpacked/resources/app.asar.unpacked/node_modules/node-api-dotnet/init.js`
);
console.log('Patching post-build init.js...');
patchFile(postBuildPath);
}
if (process.arch === 'arm64') {
platformName += '-arm64';
}
const postBuildPath = path.join(
__dirname,
`./../build/${platformName}-unpacked/resources/app.asar.unpacked/node_modules/node-api-dotnet/init.js`
);
console.log('Patching post-build init.js...');
patchFile(postBuildPath);
const { arch, platform } = getArchAndPlatform();
patchNodeApiDotNet(arch, platform);

View File

@@ -22,6 +22,7 @@ contextBridge.exposeInMainWorld('interopApi', {
const validChannels = ['launch-command'];
contextBridge.exposeInMainWorld('electron', {
getArch: () => ipcRenderer.invoke('getArch'),
openFileDialog: () => ipcRenderer.invoke('dialog:openFile'),
openDirectoryDialog: () => ipcRenderer.invoke('dialog:openDirectory'),
onWindowPositionChanged: (callback) =>
@@ -48,7 +49,6 @@ contextBridge.exposeInMainWorld('electron', {
ipcRenderer: {
on(channel, func) {
if (validChannels.includes(channel)) {
console.log('contextBridge', channel, func);
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
}

View File

@@ -1,5 +1,6 @@
const fs = require('fs');
const path = require('path');
const { getArchAndPlatform } = require('./utils');
const rootDir = path.join(__dirname, '..');
const versionFilePath = path.join(rootDir, 'Version');
@@ -17,34 +18,42 @@ try {
process.exit(1);
}
if (process.platform === 'linux') {
const oldAppImage = path.join(buildDir, `VRCX_Version.AppImage`);
const newAppImage = path.join(buildDir, `VRCX_${version}.AppImage`);
try {
if (fs.existsSync(oldAppImage)) {
fs.renameSync(oldAppImage, newAppImage);
console.log(`Renamed: ${oldAppImage} -> ${newAppImage}`);
} else {
console.log(`File not found: ${oldAppImage}`);
function renameBuild(arch, platform) {
if (platform === 'linux') {
const oldAppImage = path.join(buildDir, `VRCX_Version.AppImage`);
const newAppImage = path.join(
buildDir,
`VRCX_${version}_${arch}.AppImage`
);
try {
if (fs.existsSync(oldAppImage)) {
fs.renameSync(oldAppImage, newAppImage);
console.log(`Renamed: ${oldAppImage} -> ${newAppImage}`);
} else {
console.log(`File not found: ${oldAppImage}`);
}
} catch (err) {
console.error('Error renaming files:', err);
process.exit(1);
}
} catch (err) {
console.error('Error renaming files:', err);
process.exit(1);
}
} else if (process.platform === 'darwin') {
const oldDmg = path.join(buildDir, `VRCX_Version.dmg`);
const newDmg = path.join(buildDir, `VRCX_${version}.dmg`);
try {
if (fs.existsSync(oldDmg)) {
fs.renameSync(oldDmg, newDmg);
console.log(`Renamed: ${oldDmg} -> ${newDmg}`);
} else {
console.log(`File not found: ${oldDmg}`);
} else if (platform === 'darwin') {
const oldDmg = path.join(buildDir, `VRCX_Version.dmg`);
const newDmg = path.join(buildDir, `VRCX_${version}_${arch}.dmg`);
try {
if (fs.existsSync(oldDmg)) {
fs.renameSync(oldDmg, newDmg);
console.log(`Renamed: ${oldDmg} -> ${newDmg}`);
} else {
console.log(`File not found: ${oldDmg}`);
}
} catch (err) {
console.error('Error renaming files:', err);
process.exit(1);
}
} catch (err) {
console.error('Error renaming files:', err);
process.exit(1);
} else {
console.log('No renaming needed for this platform.');
}
} else {
console.log('No renaming needed for this platform.');
}
const { arch, platform } = getArchAndPlatform();
renameBuild(arch, platform);

19
src-electron/utils.js Normal file
View File

@@ -0,0 +1,19 @@
function getArchAndPlatform() {
// --arch= win32, darwin, linux
// --platform= x64, arm64
const args = process.argv.slice(2);
let arch = process.arch.toString();
let platform = process.platform.toString();
for (let i = 0; i < args.length; i++) {
if (args[i].startsWith('--arch=')) {
arch = args[i].split('=')[1];
}
if (args[i].startsWith('--platform=')) {
platform = args[i].split('=')[1];
}
}
console.log(`Using arch: ${arch}, platform: ${platform}`);
return { arch, platform };
}
module.exports = { getArchAndPlatform };

View File

@@ -14,6 +14,7 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
const { t } = useI18n();
const state = reactive({
arch: 'x64',
appVersion: '',
autoUpdateVRCX: 'Auto Download',
latestAppVersion: '',
@@ -40,6 +41,12 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
});
async function initVRCXUpdaterSettings() {
if (!WINDOWS) {
const arch = await window.electron.getArch();
console.log('Architecture:', arch);
state.arch = arch;
}
const [autoUpdateVRCX, vrcxId] = await Promise.all([
configRepository.getString('VRCX_autoUpdateVRCX', 'Auto Download'),
configRepository.getString('VRCX_id', '')
@@ -200,7 +207,7 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
}
if (
LINUX &&
asset.name.endsWith('.AppImage') &&
asset.name.endsWith(`${state.arch}.AppImage`) &&
asset.content_type === 'application/octet-stream'
) {
downloadUrl = asset.browser_download_url;

View File

@@ -30,6 +30,7 @@ declare global {
) => Promise<any>;
};
electron: {
getArch: () => Promise<string>;
openFileDialog: () => Promise<string>;
openDirectoryDialog: () => Promise<string>;
desktopNotification: (
@@ -63,7 +64,7 @@ declare global {
overlayHand: int
) => Promise<void>;
ipcRenderer: {
on(channel: String, func: (...args: unknown[]) => void)
on(channel: String, func: (...args: unknown[]) => void);
};
};
__APP_GLOBALS__: AppGlobals;