Rename app to EasyView and make APK build cross-platform

- App display name -> EasyView (Android label, PWA manifest, page titles,
  about/guide text, APK artifact name)
- scripts/build-apk.mjs: auto-detect JDK/SDK/Gradle toolchain instead of
  hardcoded Windows paths; pick highest build-tools; platform 34 fallback;
  path.delimiter classpaths; npm run build without cmd.exe on POSIX
- Add build-apk.sh one-shot Linux build wrapper (loads ven env + keystore)
- Signing keystore renamed to easyview.jks (alias easyview),
  env vars EASYVIEW_KEYSTORE_PASS / EASYVIEW_KEY_PASS
This commit is contained in:
2026-10-26 19:38:42 +08:00
parent e01c7eaecb
commit f70913481e
12 changed files with 170 additions and 96 deletions
+116 -33
View File
@@ -4,19 +4,92 @@ 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 isWin = process.platform === 'win32'
const exeSuffix = isWin ? '.exe' : ''
const devRoot =
process.env.ANDROID_DEV_ROOT ||
(isWin ? 'E:\\AiCodeGitea\\ven\\AndroidDev' : path.resolve(root, '..', 'ven'))
const isFile = (p) => {
try {
return fs.statSync(p).isFile()
} catch {
return false
}
}
const isDir = (p) => {
try {
return fs.statSync(p).isDirectory()
} catch {
return false
}
}
const jdkHome = (() => {
const candidates = [
process.env.JAVA_HOME,
path.join(devRoot, 'jdk21'),
path.join(devRoot, 'jdk17'),
path.join(devRoot, 'AndroidDev', 'jdk17')
].filter(Boolean)
const found = candidates.find((dir) => isFile(path.join(dir, 'bin', `java${exeSuffix}`)))
if (!found) throw new Error('JDK not found: set JAVA_HOME (workspace: source ../ven/*-env.sh)')
return found
})()
const sdkHome = (() => {
const candidates = [
process.env.ANDROID_HOME,
process.env.ANDROID_SDK_ROOT,
path.join(devRoot, 'android-sdk'),
path.join(devRoot, 'Sdk')
].filter(Boolean)
const found = candidates.find((dir) => isDir(path.join(dir, 'platforms')))
if (!found) throw new Error('Android SDK not found: set ANDROID_HOME')
return found
})()
function findGradleHome() {
const explicit = [process.env.GRADLE_HOME, path.join(devRoot, 'gradle-8.7')].filter(Boolean)
const found = explicit.find((dir) => isFile(path.join(dir, 'lib', 'kotlin-stdlib-1.9.23.jar')))
if (found) return found
const distRoot = path.join(devRoot, 'gradle-dist')
const candidates = isDir(distRoot)
? fs
.readdirSync(distRoot)
.filter((name) => name.startsWith('gradle-'))
.sort()
.reverse()
.map((name) => path.join(distRoot, name))
: []
const hit = candidates.find((dir) => isDir(path.join(dir, 'lib')))
if (!hit) throw new Error(`Gradle distribution with Kotlin compiler not found under ${distRoot}`)
return hit
}
const gradleHome = findGradleHome()
const buildToolsVersion =
process.env.BUILD_TOOLS_VERSION ||
fs
.readdirSync(path.join(sdkHome, 'build-tools'))
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
.pop()
const buildTools = path.join(sdkHome, 'build-tools', buildToolsVersion)
const platformId =
process.env.COMPILE_SDK ||
(isDir(path.join(sdkHome, 'platforms', 'android-34'))
? 'android-34'
: fs
.readdirSync(path.join(sdkHome, 'platforms'))
.filter((name) => name.startsWith('android-'))
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
.pop())
const androidJar = path.join(sdkHome, 'platforms', platformId, 'android.jar')
const java = path.join(jdkHome, 'bin', `java${exeSuffix}`)
const jar = path.join(jdkHome, 'bin', `jar${exeSuffix}`)
const keytool = path.join(jdkHome, 'bin', `keytool${exeSuffix}`)
const kotlincJars = path.join(gradleHome, 'lib', '*')
const kotlinStdlib = fs
.readdirSync(path.join(gradleHome, 'lib'))
@@ -25,6 +98,9 @@ const kotlinStdlib = fs
.pop()
if (!kotlinStdlib) throw new Error('kotlin-stdlib not found in Gradle lib')
const appVersion = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version
const versionCode = Number(process.env.APP_VERSION_CODE || 7)
const env = {
...process.env,
JAVA_HOME: jdkHome,
@@ -35,9 +111,13 @@ const env = {
buildTools,
path.join(sdkHome, 'platform-tools'),
process.env.PATH || ''
].join(';')
].join(path.delimiter)
}
console.log(`JDK : ${jdkHome}`)
console.log(`Android SDK: ${sdkHome} (build-tools ${buildToolsVersion}, platform ${platformId})`)
console.log(`Gradle lib : ${gradleHome}`)
const appDir = path.join(root, 'android', 'app', 'src', 'main')
const manifest = path.join(appDir, 'AndroidManifest.xml')
const resDir = path.join(appDir, 'res')
@@ -47,15 +127,17 @@ 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')
const apkName = 'PhoneViewer-0.5.0.apk'
const apkName = `EasyView-${appVersion}.apk`
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)
// Android build-tools ship .bat wrappers on Windows and plain scripts elsewhere.
function runTool(name, args, options = {}) {
const tool = path.join(buildTools, `${name}${isWin ? '.bat' : ''}`)
return isWin ? run('cmd.exe', ['/c', tool, ...args], options) : run(tool, args, options)
}
fs.rmSync(outDir, { recursive: true, force: true })
@@ -64,7 +146,8 @@ fs.mkdirSync(classesDir, { recursive: true })
fs.mkdirSync(dexDir, { recursive: true })
console.log('== building web assets ==')
run('cmd.exe', ['/c', 'npm run build'], { cwd: root })
if (isWin) run('cmd.exe', ['/c', 'npm run build'], { cwd: root })
else run(process.env.NPM_BIN || 'npm', ['run', 'build'], { cwd: root, shell: false })
console.log('== copying web assets into Android assets ==')
fs.rmSync(wwwDir, { recursive: true, force: true })
@@ -93,7 +176,7 @@ run(java, [
'-no-stdlib',
'-no-reflect',
'-classpath',
`${androidJar};${path.join(gradleHome, 'lib', kotlinStdlib)}`,
[androidJar, path.join(gradleHome, 'lib', kotlinStdlib)].join(path.delimiter),
'-jvm-target',
'17',
'-d',
@@ -106,7 +189,7 @@ const classesJar = path.join(outDir, 'classes.jar')
run(jar, ['cf', classesJar, '-C', classesDir, '.'])
console.log('== dexing with d8 ==')
runBat(path.join(buildTools, 'd8.bat'), [
runTool('d8', [
'--release',
'--lib',
androidJar,
@@ -120,11 +203,11 @@ runBat(path.join(buildTools, 'd8.bat'), [
console.log('== compiling resources ==')
const resZip = path.join(outDir, 'resources.zip')
run(path.join(buildTools, 'aapt2.exe'), ['compile', '-o', resZip, '--dir', resDir])
run(path.join(buildTools, `aapt2${exeSuffix}`), ['compile', '-o', resZip, '--dir', resDir])
console.log('== linking base apk ==')
const baseApk = path.join(outDir, 'base.apk')
run(path.join(buildTools, 'aapt2.exe'), [
run(path.join(buildTools, `aapt2${exeSuffix}`), [
'link',
'-o',
baseApk,
@@ -139,9 +222,9 @@ run(path.join(buildTools, 'aapt2.exe'), [
'--target-sdk-version',
'34',
'--version-code',
'6',
String(versionCode),
'--version-name',
'0.5.0',
appVersion,
'--auto-add-overlay'
])
@@ -167,17 +250,17 @@ zip.writeZip(unsigned)
console.log('== zipalign ==')
const aligned = path.join(outDir, 'aligned.apk')
run(path.join(buildTools, 'zipalign.exe'), ['-p', '-f', '4', unsigned, aligned])
run(path.join(buildTools, `zipalign${exeSuffix}`), ['-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
const keystore = path.join(keystoreDir, 'easyview.jks')
const storePass = process.env.EASYVIEW_KEYSTORE_PASS ?? process.env.PHONEVIEWER_KEYSTORE_PASS
const keyPass = process.env.EASYVIEW_KEY_PASS ?? process.env.PHONEVIEWER_KEY_PASS ?? storePass
if (!storePass) {
throw new Error('Set PHONEVIEWER_KEYSTORE_PASS before building the signed APK.')
throw new Error('Set EASYVIEW_KEYSTORE_PASS before building the signed APK.')
}
const alias = 'phoneviewer'
const alias = 'easyview'
if (!fs.existsSync(keystore)) {
fs.mkdirSync(keystoreDir, { recursive: true })
run(keytool, [
@@ -200,12 +283,12 @@ if (!fs.existsSync(keystore)) {
'-keypass',
keyPass,
'-dname',
'CN=PhoneViewer, OU=Android, O=PhoneViewer, L=Beijing, ST=Beijing, C=CN',
'CN=EasyView, OU=Android, O=EasyView, L=Beijing, ST=Beijing, C=CN',
'-noprompt'
])
}
const finalApk = path.join(outDir, apkName)
runBat(path.join(buildTools, 'apksigner.bat'), [
runTool('apksigner', [
'sign',
'--ks',
keystore,
@@ -219,7 +302,7 @@ runBat(path.join(buildTools, 'apksigner.bat'), [
finalApk,
aligned
])
runBat(path.join(buildTools, 'apksigner.bat'), ['verify', '--verbose', '--print-certs', finalApk])
runTool('apksigner', ['verify', '--verbose', '--print-certs', finalApk])
const rootApk = path.join(root, apkName)
fs.copyFileSync(finalApk, rootApk)