Files
PhoneViewer/scripts/build-apk.mjs
T

229 lines
6.5 KiB
JavaScript
Raw Normal View History

2026-10-26 17:44:44 +08:00
import fs from 'node:fs'
import path from 'node:path'
import { execFileSync } from 'node:child_process'
import AdmZip from 'adm-zip'
const root = process.cwd()
const devRoot = process.env.ANDROID_DEV_ROOT || 'E:\\AiCodeGitea\\ven\\AndroidDev'
const jdkHome = fs.existsSync(path.join(process.env.JAVA_HOME || '', 'bin', 'java.exe'))
? process.env.JAVA_HOME
: path.join(devRoot, 'jdk17')
const sdkHome = fs.existsSync(path.join(process.env.ANDROID_HOME || '', 'platforms'))
? process.env.ANDROID_HOME
: path.join(devRoot, 'Sdk')
const gradleHome = path.join(devRoot, 'gradle-8.7')
const buildTools = path.join(sdkHome, 'build-tools', '34.0.0')
const androidJar = path.join(sdkHome, 'platforms', 'android-34', 'android.jar')
const java = path.join(jdkHome, 'bin', 'java.exe')
const jar = path.join(jdkHome, 'bin', 'jar.exe')
const keytool = path.join(jdkHome, 'bin', 'keytool.exe')
const kotlincJars = path.join(gradleHome, 'lib', '*')
const kotlinStdlib = fs
.readdirSync(path.join(gradleHome, 'lib'))
.filter((name) => /^kotlin-stdlib-[\d.]+\.jar$/.test(name))
.sort()
.pop()
if (!kotlinStdlib) throw new Error('kotlin-stdlib not found in Gradle lib')
const env = {
...process.env,
JAVA_HOME: jdkHome,
ANDROID_HOME: sdkHome,
ANDROID_SDK_ROOT: sdkHome,
PATH: [
path.join(jdkHome, 'bin'),
buildTools,
path.join(sdkHome, 'platform-tools'),
process.env.PATH || ''
].join(';')
}
const appDir = path.join(root, 'android', 'app', 'src', 'main')
const manifest = path.join(appDir, 'AndroidManifest.xml')
const resDir = path.join(appDir, 'res')
const assetsDir = path.join(appDir, 'assets')
const wwwDir = path.join(assetsDir, 'www')
const sourceDir = path.join(appDir, 'java')
const outDir = path.join(root, 'android', 'build')
const classesDir = path.join(outDir, 'classes')
const dexDir = path.join(outDir, 'dex')
2026-10-26 18:07:00 +08:00
const apkName = 'PhoneViewer-0.5.0.apk'
2026-10-26 17:44:44 +08:00
function run(command, args, options = {}) {
console.log('>', path.basename(command), args.join(' '))
execFileSync(command, args, { stdio: 'inherit', env, ...options })
}
function runBat(batFile, args, options = {}) {
run('cmd.exe', ['/c', batFile, ...args], options)
}
fs.rmSync(outDir, { recursive: true, force: true })
fs.mkdirSync(outDir, { recursive: true })
fs.mkdirSync(classesDir, { recursive: true })
fs.mkdirSync(dexDir, { recursive: true })
console.log('== building web assets ==')
run('cmd.exe', ['/c', 'npm run build'], { cwd: root })
console.log('== copying web assets into Android assets ==')
fs.rmSync(wwwDir, { recursive: true, force: true })
fs.mkdirSync(wwwDir, { recursive: true })
fs.cpSync(path.join(root, 'dist'), wwwDir, { recursive: true })
for (const name of fs.readdirSync(wwwDir)) {
if (name === 'sw.js' || name.startsWith('workbox-')) {
fs.rmSync(path.join(wwwDir, name), { force: true })
}
}
console.log('== compiling Kotlin ==')
const kotlinFiles = []
function walk(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name)
if (entry.isDirectory()) walk(full)
else if (entry.name.endsWith('.kt')) kotlinFiles.push(full)
}
}
walk(sourceDir)
run(java, [
'-cp',
kotlincJars,
'org.jetbrains.kotlin.cli.jvm.K2JVMCompiler',
'-no-stdlib',
'-no-reflect',
'-classpath',
`${androidJar};${path.join(gradleHome, 'lib', kotlinStdlib)}`,
'-jvm-target',
'17',
'-d',
classesDir,
...kotlinFiles
])
console.log('== creating classes.jar ==')
const classesJar = path.join(outDir, 'classes.jar')
run(jar, ['cf', classesJar, '-C', classesDir, '.'])
console.log('== dexing with d8 ==')
runBat(path.join(buildTools, 'd8.bat'), [
'--release',
'--lib',
androidJar,
'--min-api',
'26',
'--output',
dexDir,
classesJar,
path.join(gradleHome, 'lib', kotlinStdlib)
])
console.log('== compiling resources ==')
const resZip = path.join(outDir, 'resources.zip')
run(path.join(buildTools, 'aapt2.exe'), ['compile', '-o', resZip, '--dir', resDir])
console.log('== linking base apk ==')
const baseApk = path.join(outDir, 'base.apk')
run(path.join(buildTools, 'aapt2.exe'), [
'link',
'-o',
baseApk,
'-I',
androidJar,
'--manifest',
manifest,
'-R',
resZip,
'--min-sdk-version',
'26',
'--target-sdk-version',
'34',
'--version-code',
2026-10-26 18:07:00 +08:00
'6',
2026-10-26 17:44:44 +08:00
'--version-name',
2026-10-26 18:07:00 +08:00
'0.5.0',
2026-10-26 17:44:44 +08:00
'--auto-add-overlay'
])
console.log('== adding assets and classes.dex ==')
const unsigned = path.join(outDir, 'unsigned.apk')
fs.rmSync(unsigned, { force: true })
const zip = new AdmZip(baseApk)
function addDirectoryToZip(directory, prefix) {
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const full = path.join(directory, entry.name)
const name = `${prefix}/${entry.name}`
if (entry.isDirectory()) addDirectoryToZip(full, name)
else zip.addFile(name, fs.readFileSync(full))
}
}
addDirectoryToZip(wwwDir, 'assets/www')
for (const dex of fs.readdirSync(dexDir).filter((name) => name.endsWith('.dex'))) {
zip.addFile(dex, fs.readFileSync(path.join(dexDir, dex)))
}
zip.writeZip(unsigned)
console.log('== zipalign ==')
const aligned = path.join(outDir, 'aligned.apk')
run(path.join(buildTools, 'zipalign.exe'), ['-p', '-f', '4', unsigned, aligned])
console.log('== signing ==')
const keystoreDir = path.join(root, 'android', 'keystore')
const keystore = path.join(keystoreDir, 'phoneviewer.jks')
const storePass = process.env.PHONEVIEWER_KEYSTORE_PASS
const keyPass = process.env.PHONEVIEWER_KEY_PASS ?? storePass
if (!storePass) {
throw new Error('Set PHONEVIEWER_KEYSTORE_PASS before building the signed APK.')
}
const alias = 'phoneviewer'
if (!fs.existsSync(keystore)) {
fs.mkdirSync(keystoreDir, { recursive: true })
run(keytool, [
'-genkeypair',
'-v',
'-keystore',
keystore,
'-storetype',
'JKS',
'-alias',
alias,
'-keyalg',
'RSA',
'-keysize',
'2048',
'-validity',
'10000',
'-storepass',
storePass,
'-keypass',
keyPass,
'-dname',
'CN=PhoneViewer, OU=Android, O=PhoneViewer, L=Beijing, ST=Beijing, C=CN',
'-noprompt'
])
}
const finalApk = path.join(outDir, apkName)
runBat(path.join(buildTools, 'apksigner.bat'), [
'sign',
'--ks',
keystore,
'--ks-key-alias',
alias,
'--ks-pass',
`pass:${storePass}`,
'--key-pass',
`pass:${keyPass}`,
'--out',
finalApk,
aligned
])
runBat(path.join(buildTools, 'apksigner.bat'), ['verify', '--verbose', '--print-certs', finalApk])
const rootApk = path.join(root, apkName)
fs.copyFileSync(finalApk, rootApk)
console.log('')
console.log('APK ready:', rootApk)
console.log('APK size:', (fs.statSync(rootApk).size / 1024 / 1024).toFixed(2), 'MB')