mirror of
https://github.com/codexu/note-gen.git
synced 2026-09-11 02:19:00 +08:00
256 lines
9.2 KiB
JavaScript
256 lines
9.2 KiB
JavaScript
import { spawn } from 'node:child_process'
|
|
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
|
|
const projectRoot = process.cwd()
|
|
const pnpmCommand = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
|
|
const packageIdentifier = 'com.codexu.NoteGen'
|
|
const mainActivity = `${packageIdentifier}/.MainActivity`
|
|
const requestedDevice = process.env.ANDROID_DEVICE?.trim() || ''
|
|
const androidMinSdk = '24'
|
|
|
|
function resolveAdbCommand() {
|
|
if (process.env.ANDROID_ADB?.trim()) return process.env.ANDROID_ADB.trim()
|
|
|
|
const executable = process.platform === 'win32' ? 'adb.exe' : 'adb'
|
|
for (const sdkRoot of [process.env.ANDROID_HOME, process.env.ANDROID_SDK_ROOT]) {
|
|
if (!sdkRoot?.trim()) continue
|
|
const candidate = resolve(sdkRoot, 'platform-tools', executable)
|
|
if (existsSync(candidate)) return candidate
|
|
}
|
|
return executable
|
|
}
|
|
|
|
const adbCommand = resolveAdbCommand()
|
|
|
|
function run(command, args, options = {}) {
|
|
return new Promise((resolvePromise, reject) => {
|
|
const child = spawn(command, args, {
|
|
cwd: projectRoot,
|
|
stdio: options.capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
env: options.env || process.env,
|
|
})
|
|
let stdout = ''
|
|
let stderr = ''
|
|
|
|
if (options.capture) {
|
|
child.stdout.on('data', chunk => { stdout += chunk })
|
|
child.stderr.on('data', chunk => { stderr += chunk })
|
|
}
|
|
|
|
child.on('error', error => {
|
|
const pathHint = command === adbCommand
|
|
? ' Ensure Android SDK platform-tools is on PATH, or set ANDROID_ADB to the adb executable.'
|
|
: ''
|
|
reject(new Error(`Failed to start ${command}.${pathHint} ${error.message}`))
|
|
})
|
|
child.on('exit', (code, signal) => {
|
|
if (signal) {
|
|
reject(new Error(`${command} was stopped by signal ${signal}.`))
|
|
return
|
|
}
|
|
if (code !== 0) {
|
|
reject(new Error(`${command} exited with code ${code ?? 1}.${stderr.trim() ? `\n${stderr.trim()}` : ''}`))
|
|
return
|
|
}
|
|
resolvePromise(stdout.trim())
|
|
})
|
|
})
|
|
}
|
|
|
|
function resolveNdkRoot() {
|
|
const configuredRoots = [
|
|
process.env.ANDROID_NDK_ROOT,
|
|
process.env.ANDROID_NDK,
|
|
process.env.ANDROID_NDK_HOME,
|
|
process.env.NDK_HOME,
|
|
].map(path => path?.trim()).filter(Boolean)
|
|
|
|
for (const path of configuredRoots) {
|
|
if (existsSync(resolve(path, 'toolchains', 'llvm', 'prebuilt'))) return path
|
|
}
|
|
|
|
for (const sdkRoot of [process.env.ANDROID_HOME, process.env.ANDROID_SDK_ROOT]) {
|
|
if (!sdkRoot?.trim()) continue
|
|
const ndkDirectory = resolve(sdkRoot, 'ndk')
|
|
if (!existsSync(ndkDirectory)) continue
|
|
const installed = readdirSync(ndkDirectory)
|
|
.map(name => resolve(ndkDirectory, name))
|
|
.filter(path => statSync(path).isDirectory())
|
|
.sort((left, right) => right.localeCompare(left, undefined, { numeric: true }))
|
|
const detected = installed.find(path => existsSync(resolve(path, 'toolchains', 'llvm', 'prebuilt')))
|
|
if (detected) return detected
|
|
}
|
|
|
|
throw new Error('Android NDK was not found. Install it with Android Studio, or set ANDROID_NDK_ROOT.')
|
|
}
|
|
|
|
function createAndroidBuildEnv(target) {
|
|
const ndkRoot = resolveNdkRoot()
|
|
const prebuiltRoot = resolve(ndkRoot, 'toolchains', 'llvm', 'prebuilt')
|
|
const hostDirectory = readdirSync(prebuiltRoot)
|
|
.map(name => resolve(prebuiltRoot, name))
|
|
.find(path => statSync(path).isDirectory())
|
|
if (!hostDirectory) throw new Error(`No NDK LLVM toolchain was found under ${prebuiltRoot}.`)
|
|
|
|
const binDirectory = resolve(hostDirectory, 'bin')
|
|
const targetTools = {
|
|
aarch64: {
|
|
cargoTriple: 'aarch64_linux_android',
|
|
compiler: `aarch64-linux-android${androidMinSdk}-clang`,
|
|
},
|
|
armv7: {
|
|
cargoTriple: 'armv7_linux_androideabi',
|
|
compiler: `armv7a-linux-androideabi${androidMinSdk}-clang`,
|
|
},
|
|
i686: {
|
|
cargoTriple: 'i686_linux_android',
|
|
compiler: `i686-linux-android${androidMinSdk}-clang`,
|
|
},
|
|
x86_64: {
|
|
cargoTriple: 'x86_64_linux_android',
|
|
compiler: `x86_64-linux-android${androidMinSdk}-clang`,
|
|
},
|
|
}
|
|
const tools = targetTools[target]
|
|
const compiler = resolve(binDirectory, tools.compiler)
|
|
const archiver = resolve(binDirectory, 'llvm-ar')
|
|
const ranlib = resolve(binDirectory, 'llvm-ranlib')
|
|
for (const path of [compiler, archiver, ranlib]) {
|
|
if (!existsSync(path)) throw new Error(`Required Android NDK tool was not found: ${path}`)
|
|
}
|
|
|
|
return {
|
|
...process.env,
|
|
ANDROID_NDK_ROOT: ndkRoot,
|
|
ANDROID_NDK: ndkRoot,
|
|
ANDROID_NDK_HOME: ndkRoot,
|
|
NDK_HOME: ndkRoot,
|
|
PATH: `${binDirectory}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH || ''}`,
|
|
[`CC_${tools.cargoTriple}`]: compiler,
|
|
[`AR_${tools.cargoTriple}`]: archiver,
|
|
[`RANLIB_${tools.cargoTriple}`]: ranlib,
|
|
}
|
|
}
|
|
|
|
async function ensureAndroidProject(buildEnv) {
|
|
const wrapperName = process.platform === 'win32' ? 'gradlew.bat' : 'gradlew'
|
|
const wrapperPath = resolve(projectRoot, 'src-tauri', 'gen', 'android', wrapperName)
|
|
if (existsSync(wrapperPath)) return
|
|
|
|
console.log('[deploy:android] Android project is incomplete; initializing the Gradle wrapper...')
|
|
await run(pnpmCommand, [
|
|
'tauri',
|
|
'android',
|
|
'init',
|
|
'--ci',
|
|
'--skip-targets-install',
|
|
], { env: buildEnv })
|
|
|
|
if (!existsSync(wrapperPath)) {
|
|
throw new Error(`Tauri Android initialization completed, but ${wrapperPath} was not created.`)
|
|
}
|
|
}
|
|
|
|
async function adb(args, options) {
|
|
return await run(adbCommand, args, options)
|
|
}
|
|
|
|
async function selectDevice() {
|
|
const output = await adb(['devices', '-l'], { capture: true })
|
|
const devices = output.split(/\r?\n/).slice(1).map(line => line.trim()).filter(Boolean).map(line => {
|
|
const [serial, state = 'unknown'] = line.split(/\s+/, 3)
|
|
return { serial, state, line }
|
|
})
|
|
|
|
if (requestedDevice) {
|
|
const selected = devices.find(device => device.serial === requestedDevice)
|
|
if (!selected) throw new Error(`Android device "${requestedDevice}" was not found by adb.`)
|
|
if (selected.state !== 'device') {
|
|
throw new Error(`Android device "${requestedDevice}" is ${selected.state}; unlock it and authorize USB debugging.`)
|
|
}
|
|
return selected.serial
|
|
}
|
|
|
|
const ready = devices.filter(device => device.state === 'device')
|
|
if (ready.length === 0) {
|
|
const unauthorized = devices.find(device => device.state === 'unauthorized')
|
|
if (unauthorized) {
|
|
throw new Error(`Android device "${unauthorized.serial}" is unauthorized; unlock it and accept the USB debugging prompt.`)
|
|
}
|
|
throw new Error('No authorized Android device was found. Connect the phone and enable USB debugging.')
|
|
}
|
|
if (ready.length > 1) {
|
|
throw new Error(`Multiple Android devices are connected (${ready.map(device => device.serial).join(', ')}). Set ANDROID_DEVICE to the target serial.`)
|
|
}
|
|
return ready[0].serial
|
|
}
|
|
|
|
function tauriTargetForAbi(abi) {
|
|
const targets = {
|
|
'arm64-v8a': 'aarch64',
|
|
'armeabi-v7a': 'armv7',
|
|
x86: 'i686',
|
|
x86_64: 'x86_64',
|
|
}
|
|
const target = targets[abi]
|
|
if (!target) throw new Error(`Unsupported Android ABI "${abi}".`)
|
|
return target
|
|
}
|
|
|
|
function collectApks(directory) {
|
|
if (!existsSync(directory)) return []
|
|
return readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
|
|
const path = resolve(directory, entry.name)
|
|
if (entry.isDirectory()) return collectApks(path)
|
|
return entry.isFile() && entry.name.endsWith('.apk') ? [path] : []
|
|
})
|
|
}
|
|
|
|
function findBuiltApk(target) {
|
|
const outputRoot = resolve(projectRoot, 'src-tauri/gen/android/app/build/outputs/apk')
|
|
const candidates = collectApks(outputRoot)
|
|
.filter(path => path.toLowerCase().includes('debug'))
|
|
.filter(path => !path.toLowerCase().includes('androidtest'))
|
|
.sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs)
|
|
return candidates.find(path => path.includes(target)) || candidates[0]
|
|
}
|
|
|
|
try {
|
|
console.log('[deploy:android] Looking for an authorized Android phone...')
|
|
const serial = await selectDevice()
|
|
const [model, abi] = await Promise.all([
|
|
adb(['-s', serial, 'shell', 'getprop', 'ro.product.model'], { capture: true }),
|
|
adb(['-s', serial, 'shell', 'getprop', 'ro.product.cpu.abi'], { capture: true }),
|
|
])
|
|
const target = tauriTargetForAbi(abi)
|
|
const buildEnv = createAndroidBuildEnv(target)
|
|
console.log(`[deploy:android] Target: ${model || 'Android device'} (${serial}, ${abi}).`)
|
|
console.log(`[deploy:android] Android NDK: ${buildEnv.ANDROID_NDK_ROOT}.`)
|
|
await ensureAndroidProject(buildEnv)
|
|
console.log(`[deploy:android] Building the ${target} debug APK...`)
|
|
|
|
await run(pnpmCommand, [
|
|
'tauri',
|
|
'android',
|
|
'build',
|
|
'--debug',
|
|
'--apk',
|
|
'--target',
|
|
target,
|
|
], { env: buildEnv })
|
|
|
|
const apkPath = findBuiltApk(target)
|
|
if (!apkPath) throw new Error('The Android build completed, but a debug APK was not found.')
|
|
|
|
console.log(`[deploy:android] Installing ${apkPath}...`)
|
|
await adb(['-s', serial, 'install', '-r', '-d', apkPath])
|
|
|
|
console.log(`[deploy:android] Launching NoteGen on ${model || serial}...`)
|
|
await adb(['-s', serial, 'shell', 'am', 'start', '-S', '-n', mainActivity])
|
|
console.log(`[deploy:android] NoteGen was deployed to ${model || serial}.`)
|
|
} catch (error) {
|
|
console.error(`[deploy:android] ${error instanceof Error ? error.message : String(error)}`)
|
|
process.exit(1)
|
|
}
|