From f1c8c0fa65ef934e96edda36432a55568f32d07c Mon Sep 17 00:00:00 2001 From: Natsumi Date: Tue, 2 Sep 2025 22:30:46 +1200 Subject: [PATCH] arm64 support --- package.json | 6 +- src-electron/download-dotnet-runtime.js | 149 +++++++++++------------- src-electron/main.js | 21 ++-- src-electron/patch-node-api-dotnet.js | 48 ++++---- src-electron/preload.js | 2 +- src-electron/rename-builds.js | 63 +++++----- src-electron/utils.js | 19 +++ src/stores/vrcxUpdater.js | 9 +- src/types/globals.d.ts | 3 +- 9 files changed, 174 insertions(+), 146 deletions(-) create mode 100644 src-electron/utils.js diff --git a/package.json b/package.json index c7351d52..fe51078d 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src-electron/download-dotnet-runtime.js b/src-electron/download-dotnet-runtime.js index e0d1da09..2a807d91 100644 --- a/src-electron/download-dotnet-runtime.js +++ b/src-electron/download-dotnet-runtime.js @@ -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); diff --git a/src-electron/main.js b/src-electron/main.js index eb863f80..d8292188 100644 --- a/src-electron/main.js +++ b/src-electron/main.js @@ -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'); } diff --git a/src-electron/patch-node-api-dotnet.js b/src-electron/patch-node-api-dotnet.js index 0c247e43..b20f0c4b 100644 --- a/src-electron/patch-node-api-dotnet.js +++ b/src-electron/patch-node-api-dotnet.js @@ -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); diff --git a/src-electron/preload.js b/src-electron/preload.js index 279c3d88..8c3ee126 100644 --- a/src-electron/preload.js +++ b/src-electron/preload.js @@ -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)); } } diff --git a/src-electron/rename-builds.js b/src-electron/rename-builds.js index eb8490af..7c59c0c1 100644 --- a/src-electron/rename-builds.js +++ b/src-electron/rename-builds.js @@ -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); diff --git a/src-electron/utils.js b/src-electron/utils.js new file mode 100644 index 00000000..1be2dfaf --- /dev/null +++ b/src-electron/utils.js @@ -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 }; diff --git a/src/stores/vrcxUpdater.js b/src/stores/vrcxUpdater.js index 189388e3..bfef61ca 100644 --- a/src/stores/vrcxUpdater.js +++ b/src/stores/vrcxUpdater.js @@ -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; diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts index 6c4d94dd..653c2cdb 100644 --- a/src/types/globals.d.ts +++ b/src/types/globals.d.ts @@ -30,6 +30,7 @@ declare global { ) => Promise; }; electron: { + getArch: () => Promise; openFileDialog: () => Promise; openDirectoryDialog: () => Promise; desktopNotification: ( @@ -63,7 +64,7 @@ declare global { overlayHand: int ) => Promise; ipcRenderer: { - on(channel: String, func: (...args: unknown[]) => void) + on(channel: String, func: (...args: unknown[]) => void); }; }; __APP_GLOBALS__: AppGlobals;