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
+4 -2
View File
@@ -13,8 +13,10 @@
"test:coverage": "jest --coverage", "test:coverage": "jest --coverage",
"prod": "cross-env PLATFORM=windows webpack --config webpack.config.js --mode production", "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", "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", "build-electron": "node ./src-electron/download-dotnet-runtime.js --arch=x64 && node ./src-electron/patch-package-version.js && electron-builder --x64 --publish never",
"postbuild-electron": "node ./src-electron/patch-node-api-dotnet.js && node ./src-electron/rename-builds.js", "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 ." "start-electron": "electron ."
}, },
"repository": { "repository": {
+65 -84
View File
@@ -2,30 +2,9 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const https = require('https'); const https = require('https');
const { spawnSync } = require('child_process'); const { spawnSync } = require('child_process');
const { getArchAndPlatform } = require('./utils');
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 DOTNET_VERSION = '9.0.8'; 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( const DOTNET_RUNTIME_DIR = path.join(
__dirname, __dirname,
'..', '..',
@@ -78,78 +57,80 @@ async function extractTarGz(tarGzPath, extractDir) {
}); });
} }
async function main() { async function downloadDotnetRuntime(arch, platform) {
if (platform !== 'linux' && platform !== 'darwin') { if (!arch || !platform) {
console.log('Skipping .NET runtime download on non supported platform'); throw new Error('Architecture and platform must be specified');
return;
} }
console.log( let dotnetPlatform = '';
`Downloading .NET ${DOTNET_VERSION}-${platform}-${runnerArch} runtime...` 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)) { if (!fs.existsSync(DOTNET_RUNTIME_DIR)) {
fs.mkdirSync(DOTNET_RUNTIME_DIR, { recursive: true }); 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 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
// Download .NET runtime await downloadFile(dotnetRuntimeUrl, tarGzPath);
await downloadFile(DOTNET_RUNTIME_URL, tarGzPath); console.log('Download completed');
console.log('Download completed');
// Extract .NET runtime to a temporary directory first // Extract .NET runtime to a temporary directory first
const tempExtractDir = path.join(DOTNET_RUNTIME_DIR, 'temp'); const tempExtractDir = path.join(DOTNET_RUNTIME_DIR, 'temp');
if (!fs.existsSync(tempExtractDir)) { if (!fs.existsSync(tempExtractDir)) {
fs.mkdirSync(tempExtractDir, { recursive: true }); 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);
} }
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) { const { arch, platform } = getArchAndPlatform();
main(); downloadDotnetRuntime(arch, platform);
}
module.exports = { downloadFile, extractTarGz };
+12 -9
View File
@@ -16,16 +16,11 @@ const https = require('https');
//app.disableHardwareAcceleration(); //app.disableHardwareAcceleration();
if (process.platform === 'linux') { const bundledDotNetPath = path.join(process.resourcesPath, 'dotnet-runtime');
if (fs.existsSync(bundledDotNetPath)) {
// Include bundled .NET runtime // Include bundled .NET runtime
const bundledDotNetPath = path.join( process.env.DOTNET_ROOT = bundledDotNetPath;
process.resourcesPath, process.env.PATH = `${bundledDotNetPath}:${process.env.PATH}`;
'dotnet-runtime'
);
if (fs.existsSync(bundledDotNetPath)) {
process.env.DOTNET_ROOT = bundledDotNetPath;
process.env.PATH = `${bundledDotNetPath}:${process.env.PATH}`;
}
} else if (process.platform === 'darwin') { } else if (process.platform === 'darwin') {
const dotnetPath = path.join('/usr/local/share/dotnet'); const dotnetPath = path.join('/usr/local/share/dotnet');
const dotnetPathArm = path.join('/usr/local/share/dotnet/x64'); 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) => { ipcMain.handle('applyWindowSettings', (event, position, size, state) => {
if (position) { if (position) {
mainWindow.setPosition(parseInt(position.x), parseInt(position.y)); mainWindow.setPosition(parseInt(position.x), parseInt(position.y));
@@ -812,6 +811,10 @@ function isDotNetInstalled() {
const result = spawnSync(dotnetPath, ['--list-runtimes'], { const result = spawnSync(dotnetPath, ['--list-runtimes'], {
encoding: 'utf-8' encoding: 'utf-8'
}); });
if (result.error) {
console.error('Error checking .NET runtimes:', result.error);
return false;
}
return result.stdout?.includes('.NETCore.App 9.0'); return result.stdout?.includes('.NETCore.App 9.0');
} }
+27 -21
View File
@@ -1,5 +1,6 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { getArchAndPlatform } = require('./utils');
function patchFile(filePath) { function patchFile(filePath) {
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
@@ -30,25 +31,30 @@ managedHostPath = managedHostPath.indexOf('app.asar.unpacked') < 0 ?
return false; return false;
} }
// Paths to patch function patchNodeApiDotNet(arch, platform) {
let platformName = ''; let platformName = '';
switch (process.platform) { switch (platform) {
case 'win32': case 'win32':
platformName = 'win'; platformName = 'win';
break; break;
case 'darwin': case 'darwin':
platformName = 'mac'; platformName = 'mac';
break; break;
case 'linux': case 'linux':
platformName = 'linux'; platformName = 'linux';
break; 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 { arch, platform } = getArchAndPlatform();
} patchNodeApiDotNet(arch, platform);
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);
+1 -1
View File
@@ -22,6 +22,7 @@ contextBridge.exposeInMainWorld('interopApi', {
const validChannels = ['launch-command']; const validChannels = ['launch-command'];
contextBridge.exposeInMainWorld('electron', { contextBridge.exposeInMainWorld('electron', {
getArch: () => ipcRenderer.invoke('getArch'),
openFileDialog: () => ipcRenderer.invoke('dialog:openFile'), openFileDialog: () => ipcRenderer.invoke('dialog:openFile'),
openDirectoryDialog: () => ipcRenderer.invoke('dialog:openDirectory'), openDirectoryDialog: () => ipcRenderer.invoke('dialog:openDirectory'),
onWindowPositionChanged: (callback) => onWindowPositionChanged: (callback) =>
@@ -48,7 +49,6 @@ contextBridge.exposeInMainWorld('electron', {
ipcRenderer: { ipcRenderer: {
on(channel, func) { on(channel, func) {
if (validChannels.includes(channel)) { if (validChannels.includes(channel)) {
console.log('contextBridge', channel, func);
ipcRenderer.on(channel, (event, ...args) => func(...args)); ipcRenderer.on(channel, (event, ...args) => func(...args));
} }
} }
+36 -27
View File
@@ -1,5 +1,6 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { getArchAndPlatform } = require('./utils');
const rootDir = path.join(__dirname, '..'); const rootDir = path.join(__dirname, '..');
const versionFilePath = path.join(rootDir, 'Version'); const versionFilePath = path.join(rootDir, 'Version');
@@ -17,34 +18,42 @@ try {
process.exit(1); process.exit(1);
} }
if (process.platform === 'linux') { function renameBuild(arch, platform) {
const oldAppImage = path.join(buildDir, `VRCX_Version.AppImage`); if (platform === 'linux') {
const newAppImage = path.join(buildDir, `VRCX_${version}.AppImage`); const oldAppImage = path.join(buildDir, `VRCX_Version.AppImage`);
try { const newAppImage = path.join(
if (fs.existsSync(oldAppImage)) { buildDir,
fs.renameSync(oldAppImage, newAppImage); `VRCX_${version}_${arch}.AppImage`
console.log(`Renamed: ${oldAppImage} -> ${newAppImage}`); );
} else { try {
console.log(`File not found: ${oldAppImage}`); 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) { } else if (platform === 'darwin') {
console.error('Error renaming files:', err); const oldDmg = path.join(buildDir, `VRCX_Version.dmg`);
process.exit(1); const newDmg = path.join(buildDir, `VRCX_${version}_${arch}.dmg`);
} try {
} else if (process.platform === 'darwin') { if (fs.existsSync(oldDmg)) {
const oldDmg = path.join(buildDir, `VRCX_Version.dmg`); fs.renameSync(oldDmg, newDmg);
const newDmg = path.join(buildDir, `VRCX_${version}.dmg`); console.log(`Renamed: ${oldDmg} -> ${newDmg}`);
try { } else {
if (fs.existsSync(oldDmg)) { console.log(`File not found: ${oldDmg}`);
fs.renameSync(oldDmg, newDmg); }
console.log(`Renamed: ${oldDmg} -> ${newDmg}`); } catch (err) {
} else { console.error('Error renaming files:', err);
console.log(`File not found: ${oldDmg}`); process.exit(1);
} }
} catch (err) { } else {
console.error('Error renaming files:', err); console.log('No renaming needed for this platform.');
process.exit(1);
} }
} else {
console.log('No renaming needed for this platform.');
} }
const { arch, platform } = getArchAndPlatform();
renameBuild(arch, platform);
+19
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 };
+8 -1
View File
@@ -14,6 +14,7 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
const { t } = useI18n(); const { t } = useI18n();
const state = reactive({ const state = reactive({
arch: 'x64',
appVersion: '', appVersion: '',
autoUpdateVRCX: 'Auto Download', autoUpdateVRCX: 'Auto Download',
latestAppVersion: '', latestAppVersion: '',
@@ -40,6 +41,12 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
}); });
async function initVRCXUpdaterSettings() { async function initVRCXUpdaterSettings() {
if (!WINDOWS) {
const arch = await window.electron.getArch();
console.log('Architecture:', arch);
state.arch = arch;
}
const [autoUpdateVRCX, vrcxId] = await Promise.all([ const [autoUpdateVRCX, vrcxId] = await Promise.all([
configRepository.getString('VRCX_autoUpdateVRCX', 'Auto Download'), configRepository.getString('VRCX_autoUpdateVRCX', 'Auto Download'),
configRepository.getString('VRCX_id', '') configRepository.getString('VRCX_id', '')
@@ -200,7 +207,7 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
} }
if ( if (
LINUX && LINUX &&
asset.name.endsWith('.AppImage') && asset.name.endsWith(`${state.arch}.AppImage`) &&
asset.content_type === 'application/octet-stream' asset.content_type === 'application/octet-stream'
) { ) {
downloadUrl = asset.browser_download_url; downloadUrl = asset.browser_download_url;
+2 -1
View File
@@ -30,6 +30,7 @@ declare global {
) => Promise<any>; ) => Promise<any>;
}; };
electron: { electron: {
getArch: () => Promise<string>;
openFileDialog: () => Promise<string>; openFileDialog: () => Promise<string>;
openDirectoryDialog: () => Promise<string>; openDirectoryDialog: () => Promise<string>;
desktopNotification: ( desktopNotification: (
@@ -63,7 +64,7 @@ declare global {
overlayHand: int overlayHand: int
) => Promise<void>; ) => Promise<void>;
ipcRenderer: { ipcRenderer: {
on(channel: String, func: (...args: unknown[]) => void) on(channel: String, func: (...args: unknown[]) => void);
}; };
}; };
__APP_GLOBALS__: AppGlobals; __APP_GLOBALS__: AppGlobals;