Skip to content

Commit

Permalink
refactor: Simplify build (#492)
Browse files Browse the repository at this point in the history
* refactor: Simplify build

* chore: correctly rebuild
  • Loading branch information
ci010 authored Oct 10, 2023
1 parent 83d9067 commit 4cba34e
Show file tree
Hide file tree
Showing 9 changed files with 330 additions and 187 deletions.
262 changes: 252 additions & 10 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

215 changes: 52 additions & 163 deletions xmcl-electron-app/build.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
import asar from '@electron/asar'
import { rebuild } from '@electron/rebuild'
import chalk from 'chalk'
import { createHash } from 'crypto'
import { Arch, build as electronBuilder, Configuration } from 'electron-builder'
import { build as esbuild, BuildOptions } from 'esbuild'
import { createReadStream, createWriteStream, existsSync } from 'fs'
import { Configuration, build as electronBuilder } from 'electron-builder'
import { BuildOptions, build as esbuild } from 'esbuild'
import { createReadStream, createWriteStream } from 'fs'
import { copy, ensureFile } from 'fs-extra'
import { copyFile, mkdir, readdir, rename, rm, stat, unlink, writeFile } from 'fs/promises'
import { platform } from 'os'
import path, { basename, dirname, join, resolve } from 'path'
import { pipeline, Writable } from 'stream'
import { copyFile, readdir, rm, stat } from 'fs/promises'
import path, { resolve } from 'path'
import { pipeline } from 'stream'
import { promisify } from 'util'
import { buildAppInstaller } from './build/appinstaller-builder'
import { config as electronBuilderConfig } from './build/electron-builder.config'
import esbuildConfig from './esbuild.config'
import { version } from './package.json'
// @ts-ignore
import pump from 'pump'
// @ts-ignore
import tfs from 'tar-fs'
import { stream } from 'undici'
import { createGunzip } from 'zlib'
import { ensureFileSync } from 'fs-extra/esm'
import createPrintPlugin from 'plugins/esbuild.print.plugin'

/**
* @returns Hash string
Expand All @@ -33,20 +26,21 @@ async function writeHash(algorithm: string, path: string, destination: string) {
/**
* Use esbuild to build main process
*/
async function buildMain(options: BuildOptions) {
async function buildMain(options: BuildOptions, slient = false) {
await rm(path.join(__dirname, './dist'), { recursive: true, force: true })
console.log(chalk.bold.underline('Build main process & preload'))
if (!slient) console.log(chalk.bold.underline('Build main process & preload'))
const startTime = Date.now()
if (!slient) options.plugins?.push(createPrintPlugin())
await esbuild({
...options,
outdir: resolve(__dirname, './dist'),
entryPoints: [path.join(__dirname, './main/index.ts')],
})
console.log(
`Build completed in ${((Date.now() - startTime) / 1000).toFixed(2)}s.\n`,
)
const time = ((Date.now() - startTime) / 1000).toFixed(2)
if (!slient) console.log(`Build completed in ${time}s.`)
await copy(path.join(__dirname, '../xmcl-keystone-ui/dist'), path.join(__dirname, './dist/renderer'))
console.log()
if (!slient) console.log('\n')
return time
}

/**
Expand Down Expand Up @@ -82,153 +76,48 @@ async function buildElectron(config: Configuration, dir: boolean) {
)
}

const currentPlatform = platform()
async function installArm64Dependencies() {
const unpack = async (tarPath: string) => {
const options = {
readable: true,
writable: true,
hardlinkAsFilesFallback: true,
}
let binaryName = ''
function updateName(entry: any) {
if (/\.node$/i.test(entry.name)) binaryName = entry.name
}

const dir = dirname(tarPath)
await ensureFile(tarPath)
await new Promise<void>((resolve, reject) => {
pump(
createReadStream(tarPath),
createGunzip(),
tfs.extract(dir, options).on('entry', updateName), (err: any) => {
if (err) return reject(err)
else resolve()
})
})

const unpackTo = resolve(__dirname, 'dist', basename(binaryName))
await unlink(unpackTo).catch(() => undefined)
await rename(join(dir, binaryName), unpackTo)
console.log(`Download and unpack to ${unpackTo}`)
async function start() {
if (!process.env.BUILD_TARGET) {
await buildMain(esbuildConfig)
return
}

const download = async (url: string, dest: string) => await stream(url, {
method: 'GET',
throwOnError: true,
maxRedirections: 2,
opaque: createWriteStream(dest),
}, ({ opaque }) => opaque as Writable)

const urls = {
darwin: {
keytar: 'https://github.com/atom/node-keytar/releases/download/v7.9.0/keytar-v7.9.0-napi-v3-darwin-arm64.tar.gz',
nodeDataChannel: 'https://github.com/murat-dogan/node-datachannel/releases/download/v0.4.1/node-datachannel-v0.4.1-node-v93-darwin-arm64.tar.gz',
classicLevel: 'https://github.com/Level/classic-level/releases/download/v1.2.0/darwin-x64+arm64.tar.gz',
const dir = process.env.BUILD_TARGET === 'dir'
const config: Configuration = {
...electronBuilderConfig,
async beforeBuild(context) {
await rebuild({
buildPath: context.appDir,
electronVersion: context.electronVersion,
arch: context.arch,
types: ['dev'],
})
console.log(` ${chalk.blue('•')} rebuilt native modules ${chalk.blue('electron')}=${context.electronVersion} ${chalk.blue('arch')}=${context.arch}`)
const time = await buildMain(esbuildConfig, true)
console.log(` ${chalk.blue('•')} compiled main process & preload in ${chalk.blue('time')}=${time}s`)
},
linux: {
keytar: 'https://github.com/atom/node-keytar/releases/download/v7.9.0/keytar-v7.9.0-napi-v3-linux-arm64.tar.gz',
nodeDataChannel: 'https://github.com/murat-dogan/node-datachannel/releases/download/v0.4.1/node-datachannel-v0.4.1-node-v93-linux-arm64.tar.gz',
async artifactBuildStarted(context) {
if (context.targetPresentableName.toLowerCase() === 'appx') {
console.log(` ${chalk.blue('•')} copy appx icons`)
const files = await readdir(path.join(__dirname, './icons'))
const storeFiles = files.filter(f => f.endsWith('.png') &&
!f.endsWith('256x256.png') &&
!f.endsWith('tray.png'))
.map((f) => [
path.join(__dirname, 'icons', f),
path.join(__dirname, 'build', 'appx', f.substring(f.indexOf('@') + 1)),
] as const)
await Promise.all(storeFiles.map(v => ensureFile(v[1]).then(() => copyFile(v[0], v[1]))))
}
},
async artifactBuildCompleted(context) {
if (!context.arch) return
if (context.target && context.target.name === 'appx') {
await buildAppInstaller(version, path.join(__dirname, './build/output/xmcl.appinstaller'), electronBuilderConfig.appx!.publisher!)
}
},
}

if (currentPlatform === 'darwin' || currentPlatform === 'linux') {
const result = await Promise.all(Object.entries(urls[currentPlatform]).map(async ([name, url]) => {
const tarGz = resolve(__dirname, `cache/${name}.tar.gz`)
await ensureFile(tarGz)
await download(url, tarGz)
return tarGz
}))
await Promise.all(result.map(unpack))
}

if (currentPlatform === 'linux') {
// copy classic-level dependencies
const src = resolve(__dirname, '../xmcl-runtime/node_modules/classic-level/prebuilds/linux-arm64/node.napi.armv8.node')
const dest = resolve(__dirname, 'dist/node.napi.glibc.node')
await unlink(dest).catch(() => { })
await copyFile(src, dest)
console.log(`copy ${src} -> ${dest}`)
}
}

async function start() {
await buildMain(esbuildConfig)

if (process.env.BUILD_TARGET) {
const dir = process.env.BUILD_TARGET === 'dir'
const config: Configuration = {
...electronBuilderConfig,
async beforePack(context) {
const asarFile = join(context.appOutDir, 'resources', 'app.asar')
const distDir = join(context.packager.projectDir, 'dist')

const isArm = context.arch === 3
// Install arm64 dependencies
if (isArm) {
await installArm64Dependencies()
}

console.log('overwrite asar', asarFile)

await asar.createPackage(distDir, asarFile)

const dest = isArm ? 'build/output/app-arm64.asar' : 'build/output/app.asar'
const destSha256 = dest + '.sha256'
if (existsSync('build/output/win-unpacked/resources/app.asar')) {
await copyFile('build/output/win-unpacked/resources/app.asar', dest)
await writeHash('sha256', dest, destSha256)
}
if (existsSync('build/output/linux-unpacked/resources/app.asar')) {
await copyFile('build/output/linux-unpacked/resources/app.asar', dest)
await writeHash('sha256', dest, destSha256)
}
if (existsSync('build/output/mac/X Minecraft Launcher.app/Contents/Resources/app.asar')) {
await copyFile('build/output/mac/X Minecraft Launcher.app/Contents/Resources/app.asar', dest)
await writeHash('sha256', dest, destSha256)
}
},
async artifactBuildStarted(context) {
if (context.targetPresentableName.toLowerCase() === 'appx') {
const files = await readdir(path.join(__dirname, './icons'))
const storeFiles = files.filter(f => f.endsWith('.png') &&
!f.endsWith('256x256.png') &&
!f.endsWith('tray.png'))
.map((f) => [
path.join(__dirname, 'icons', f),
path.join(__dirname, 'build', 'appx', f.substring(f.indexOf('@') + 1)),
] as const)
await Promise.all(storeFiles.map(v => ensureFile(v[1]).then(() => copyFile(v[0], v[1]))))
}
},
async artifactBuildCompleted(context) {
if (!context.arch) return
if (context.target && context.target.name === 'appx') {
await buildAppInstaller(version, path.join(__dirname, './build/output/xmcl.appinstaller'), electronBuilderConfig.appx!.publisher!)
}
},
}

await buildElectron(config, dir)

const currentPlatform = platform()
const runtime = currentPlatform === 'win32'
? 'appx'
: currentPlatform === 'linux'
? 'appimage'
: undefined

if (!dir && runtime) {
// Build appx and appImage additionally
(config.win as any).target = ['appx'];
(config.linux as any).target = [{ target: 'AppImage', arch: ['x64', 'arm64'] }]

esbuildConfig.define['process.env.RUNTIME'] = `"${runtime}"`
await buildMain(esbuildConfig)

await buildElectron(config, dir)
}
}
await buildElectron(config, dir)
}

start().catch((e) => {
Expand Down
1 change: 1 addition & 0 deletions xmcl-electron-app/build/electron-builder.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export const config = {
'ia32',
],
},
'appx',
],
},
linux: {
Expand Down
1 change: 0 additions & 1 deletion xmcl-electron-app/esbuild.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ const config = {
},
plugins: [
pluginRenderer(),
pluginPrint(),
pluginStatic(),
pluginPreload(path.resolve(__dirname, './preload')),
pluginVueDevtools(path.resolve(__dirname, '../extensions')),
Expand Down
23 changes: 19 additions & 4 deletions xmcl-electron-app/main/ElectronLauncherApp.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { LauncherApp } from '@xmcl/runtime'
import { Shell } from '@xmcl/runtime/lib/app/Shell'
import { LAUNCHER_NAME } from '@xmcl/runtime/lib/constant'
import { app, shell } from 'electron'
import { join } from 'path'
import { URL } from 'url'
import { ElectronController } from './ElectronController'
import { ElectronSecretStorage } from './ElectronSecretStorage'
import defaultApp from './defaultApp'
import { definedServices } from './definedServices'
import { pluginAutoUpdate } from './pluginAutoUpdate'
import { isDirectory } from './utils/fs'
import { ElectronUpdater } from './utils/updater'
import { getWindowsUtils } from './utils/windowsUtils'
import { ElectronSecretStorage } from './ElectronSecretStorage'
import { join } from 'path'
import { LAUNCHER_NAME } from '@xmcl/runtime/lib/constant'
import { pluginAutoUpdate } from './pluginAutoUpdate'

class ElectronShell implements Shell {
showItemInFolder = shell.showItemInFolder
Expand Down Expand Up @@ -76,6 +76,20 @@ class ElectronShell implements Shell {
}
}

const getEnv = () => {
const isAppImage = !!process.env.APPIMAGE
if (isAppImage) {
return 'appimage'
} else {
const currentPath = app.getPath('exe')
if (currentPath.includes('WindowsApps')) {
return 'appx'
} else {
return 'raw'
}
}
}

export default class ElectronLauncherApp extends LauncherApp {
constructor() {
super(app,
Expand All @@ -84,6 +98,7 @@ export default class ElectronLauncherApp extends LauncherApp {
(app) => new ElectronController(app as ElectronLauncherApp),
(app) => new ElectronUpdater(app as ElectronLauncherApp),
defaultApp,
getEnv(),
definedServices,
[pluginAutoUpdate],
)
Expand Down
9 changes: 4 additions & 5 deletions xmcl-electron-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
"url": "https://github.com/voxelum/x-minecraft-launcher"
},
"scripts": {
"build": "cross-env NODE_ENV=development esbuild build.ts --bundle --platform=node --external:electron --external:electron-builder --external:esbuild --format=cjs | node",
"build": "cross-env NODE_ENV=development tsx build.ts",
"build:appx": "cross-env NODE_ENV=production env BUILD_TARGET=appx npm run build",
"build:all": "cross-env NODE_ENV=production cross-env BUILD_TARGET=production npm run build",
"build:dir": "cross-env BUILD_TARGET=dir npm run build",
"postinstall": "electron-rebuild --only better-sqlite3",
"check": "tsc --noEmit --project tsconfig.json && tsc --noEmit --project preload/tsconfig.json",
"dev": "cross-env NODE_ENV=development esbuild dev.ts --bundle --platform=node --external:electron --external:esbuild --format=cjs | node",
"dev": "cross-env NODE_ENV=development tsx dev.ts",
"lint": "eslint --ext .ts .",
"lint:fix": "npm run lint -- --fix"
},
Expand All @@ -45,6 +45,7 @@
"@xmcl/runtime-api": "workspace:*",
"@xmcl/task": "workspace:*",
"@xmcl/windows-utils": "^0.0.15",
"better-sqlite3": "^8.5.1",
"builtin-modules": "^3.3.0",
"chalk": "^4.1.2",
"create-desktop-shortcuts": "^1.10.1",
Expand All @@ -66,12 +67,10 @@
"node-datachannel": "0.5.0-dev",
"node-disk-info": "^1.3.0",
"original-fs": "^1.2.0",
"pump": "^3.0.0",
"semver": "^7.3.8",
"source-map-support": "^0.5.21",
"tar-fs": "^2.1.1",
"better-sqlite3": "^8.5.1",
"tslib": "^2.5.0",
"tsx": "^3.13.0",
"typescript": "^5.2.2",
"undici": "5.22.1"
}
Expand Down
1 change: 0 additions & 1 deletion xmcl-electron-app/plugins/esbuild.jschardet.plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export default function createJschardetPlugin(): Plugin {
if (build.initialOptions) {
build.onLoad({ filter: /universaldetectors?\.js/g }, async ({ path }) => {
const content = await readFile(path, 'utf-8')
console.log('replaced!!!')
return {
contents: content.replace('denormalizedEncodings = [];', 'const denormalizedEncodings = [];'),
loader: 'js',
Expand Down
2 changes: 1 addition & 1 deletion xmcl-runtime-api/src/services/BaseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface Environment extends Platform {
/**
* The container of the launcher. Will be raw if the launcher is just installed on system. Will be appx if it's appx.
*/
env: 'raw' | 'appx' | 'appimage'
env: 'raw' | 'appx' | 'appimage' | string
/**
* The version of the launcher
*/
Expand Down
3 changes: 1 addition & 2 deletions xmcl-runtime/lib/app/LauncherApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,6 @@ export class LauncherApp extends EventEmitter {

readonly build: number = Number.parseInt(process.env.BUILD_NUMBER ?? '0', 10)

readonly env: 'raw' | 'appx' | 'appimage' = process.env.RUNTIME as any || 'raw'

get version() { return this.host.getVersion() }

protected managers: Manager[]
Expand Down Expand Up @@ -143,6 +141,7 @@ export class LauncherApp extends EventEmitter {
getController: (app: LauncherApp) => LauncherAppController,
getUpdater: (app: LauncherApp) => LauncherAppUpdater,
readonly builtinAppManifest: InstalledAppManifest,
readonly env: string,
services: ServiceConstructor[],
_plugins: LauncherAppPlugin[],
) {
Expand Down

0 comments on commit 4cba34e

Please sign in to comment.