Files
duooomi-ble-sdk/scripts/package-android.mjs
2026-04-08 19:08:08 +08:00

385 lines
10 KiB
JavaScript

import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { spawnSync } from 'node:child_process'
import net from 'node:net'
import { fileURLToPath, pathToFileURL } from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const rootDir = path.resolve(__dirname, '..')
const distDir = path.join(rootDir, 'dist', 'android')
const mavenDir = path.join(distDir, 'maven')
const wrapperDir = path.join(distDir, 'wrapper')
const wrapperTemplateDir = path.join(rootDir, 'wrappers', 'android')
const androidProjectDir = path.join(rootDir, 'android')
const gradleWrapperPropertiesPath = path.join(androidProjectDir, 'gradle', 'wrapper', 'gradle-wrapper.properties')
const androidBuildGradlePath = path.join(androidProjectDir, 'build.gradle')
const androidLocalPropertiesPath = path.join(androidProjectDir, 'local.properties')
const gradleWrapper = process.platform === 'win32' ? 'gradlew.bat' : './gradlew'
const gradleTask = 'publishBrownfieldReleasePublicationTodistRepository'
let commandRootDir = rootDir
let commandAndroidProjectDir = androidProjectDir
const commonProxyCandidates = [
{ protocol: 'http', host: '127.0.0.1', port: 7890 },
{ protocol: 'http', host: '127.0.0.1', port: 7897 },
{ protocol: 'http', host: '127.0.0.1', port: 8080 },
{ protocol: 'http', host: '127.0.0.1', port: 10809 },
{ protocol: 'socks', host: '127.0.0.1', port: 7891 },
{ protocol: 'socks', host: '127.0.0.1', port: 10808 },
]
function run(command, args, env = process.env, cwd = commandRootDir) {
const result = spawnSync(command, args, {
cwd,
env,
shell: process.platform === 'win32',
stdio: 'inherit',
})
if (result.error) {
throw result.error
}
if (result.status !== 0) {
process.exit(result.status ?? 1)
}
}
function ensureAndroidProject() {
if (existsSync(androidProjectDir)) {
return
}
run('pnpx', ['expo', 'prebuild', '--platform', 'android', '--pnpm'], buildEnv)
}
function ensureGradleWrapperTimeout() {
if (!existsSync(gradleWrapperPropertiesPath)) {
return
}
const properties = readFileSync(gradleWrapperPropertiesPath, 'utf8')
if (properties.includes('networkTimeout=120000')) {
return
}
const updatedProperties = properties.includes('networkTimeout=')
? properties.replace(/networkTimeout=\d+/g, 'networkTimeout=120000')
: `${properties.trimEnd()}\nnetworkTimeout=120000\n`
writeFileSync(gradleWrapperPropertiesPath, updatedProperties)
}
function ensureAndroidPublishPath() {
if (!existsSync(androidBuildGradlePath)) {
return
}
const buildGradle = readFileSync(androidBuildGradlePath, 'utf8')
const expectedUrl = pathToFileURL(mavenDir).toString()
const updatedBuildGradle = buildGradle.replace(
/url\.set\("file:\/\/[^"]*"\)/g,
`url.set("${expectedUrl}")`
)
if (updatedBuildGradle !== buildGradle) {
writeFileSync(androidBuildGradlePath, updatedBuildGradle)
}
}
function escapePropertiesPath(value) {
return value.replace(/\\/g, '\\\\')
}
function resolveAndroidSdkPath() {
const candidates = [
process.env.ANDROID_HOME,
process.env.ANDROID_SDK_ROOT,
path.join(process.env.LOCALAPPDATA || '', 'Android', 'Sdk'),
path.join(process.env.USERPROFILE || '', 'AppData', 'Local', 'Android', 'Sdk'),
].filter(Boolean)
return candidates.find((candidate) => existsSync(candidate)) || null
}
function resolveAndroidSdkCmakeDir() {
const sdkPath = resolveAndroidSdkPath()
if (!sdkPath) {
return null
}
const preferredVersionDir = path.join(sdkPath, 'cmake', '4.1.2')
if (existsSync(preferredVersionDir)) {
return preferredVersionDir
}
return null
}
function ensureAndroidLocalProperties() {
const sdkPath = resolveAndroidSdkPath()
const cmakeDir = resolveAndroidSdkCmakeDir()
if (!sdkPath && !cmakeDir) {
return
}
const lines = []
if (sdkPath) {
lines.push(`sdk.dir=${escapePropertiesPath(sdkPath)}`)
}
if (cmakeDir) {
lines.push(`cmake.dir=${escapePropertiesPath(cmakeDir)}`)
}
const content = `${lines.join('\n')}\n`
writeFileSync(androidLocalPropertiesPath, content)
}
function normalizeProxyUrl(rawValue) {
if (!rawValue) {
return null
}
const normalized = rawValue.includes('://') ? rawValue : `http://${rawValue}`
try {
const url = new URL(normalized)
return {
protocol: url.protocol.replace(':', ''),
host: url.hostname,
port: Number(url.port || (url.protocol === 'https:' ? '443' : '80')),
}
} catch {
return null
}
}
function runAndCapture(command, args) {
const result = spawnSync(command, args, {
cwd: commandRootDir,
shell: process.platform === 'win32',
encoding: 'utf8',
})
if (result.error || result.status !== 0) {
return null
}
return (result.stdout || '').trim()
}
function parseWindowsProxyServer(proxyServer) {
if (!proxyServer) {
return null
}
const normalized = proxyServer.trim()
const segments = normalized.split(';').map((segment) => segment.trim()).filter(Boolean)
if (segments.length === 0) {
return null
}
const preferredSegment =
segments.find((segment) => segment.startsWith('https=')) ||
segments.find((segment) => segment.startsWith('http=')) ||
segments.find((segment) => segment.startsWith('socks=')) ||
segments[0]
const [scheme, value] = preferredSegment.includes('=')
? preferredSegment.split('=', 2)
: ['http', preferredSegment]
return normalizeProxyUrl(`${scheme}://${value}`)
}
function resolveWindowsSystemProxy() {
if (process.platform !== 'win32') {
return null
}
const registryJson = runAndCapture('powershell', [
'-NoProfile',
'-Command',
"$settings = Get-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings'; [PSCustomObject]@{ enabled = [bool]$settings.ProxyEnable; server = $settings.ProxyServer } | ConvertTo-Json -Compress",
])
if (registryJson) {
try {
const registrySettings = JSON.parse(registryJson)
if (registrySettings?.enabled && registrySettings?.server) {
const proxy = parseWindowsProxyServer(registrySettings.server)
if (proxy) {
return proxy
}
}
} catch {
// Ignore parsing errors and fall through to the next strategy.
}
}
const winHttpOutput = runAndCapture('netsh', ['winhttp', 'show', 'proxy'])
if (!winHttpOutput) {
return null
}
const proxyLine = winHttpOutput
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => line.toLowerCase().startsWith('proxy server'))
if (!proxyLine) {
return null
}
const server = proxyLine.split(':').slice(1).join(':').trim()
return parseWindowsProxyServer(server)
}
function canConnect({ host, port }) {
return new Promise((resolve) => {
const socket = new net.Socket()
const done = (value) => {
socket.destroy()
resolve(value)
}
socket.setTimeout(300)
socket.once('connect', () => done(true))
socket.once('timeout', () => done(false))
socket.once('error', () => done(false))
socket.connect(port, host)
})
}
async function resolveProxy() {
const envProxy = normalizeProxyUrl(
process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy
)
if (envProxy) {
return envProxy
}
const systemProxy = resolveWindowsSystemProxy()
if (systemProxy) {
return systemProxy
}
for (const candidate of commonProxyCandidates) {
if (await canConnect(candidate)) {
return candidate
}
}
return null
}
function buildJavaToolOptions(proxy) {
const current = process.env.JAVA_TOOL_OPTIONS?.trim()
if (!proxy) {
return current || undefined
}
const proxyArgs = proxy.protocol === 'socks'
? [
`-DsocksProxyHost=${proxy.host}`,
`-DsocksProxyPort=${proxy.port}`,
]
: [
`-Dhttp.proxyHost=${proxy.host}`,
`-Dhttp.proxyPort=${proxy.port}`,
`-Dhttps.proxyHost=${proxy.host}`,
`-Dhttps.proxyPort=${proxy.port}`,
]
return [current, ...proxyArgs].filter(Boolean).join(' ')
}
function buildCommandEnv(proxy) {
const env = { ...process.env }
const javaToolOptions = buildJavaToolOptions(proxy)
if (javaToolOptions) {
env.JAVA_TOOL_OPTIONS = javaToolOptions
}
if (proxy && proxy.protocol !== 'socks') {
const proxyUrl = `http://${proxy.host}:${proxy.port}`
env.HTTP_PROXY = env.HTTP_PROXY || env.http_proxy || proxyUrl
env.HTTPS_PROXY = env.HTTPS_PROXY || env.https_proxy || proxyUrl
env.http_proxy = env.http_proxy || env.HTTP_PROXY
env.https_proxy = env.https_proxy || env.HTTPS_PROXY
}
env.NODE_ENV = env.NODE_ENV || 'production'
env.ORG_GRADLE_PROJECT_newArchEnabled = 'false'
return env
}
function ensureWindowsShortPathWorkspace() {
if (process.platform !== 'win32') {
return
}
const currentDrive = path.parse(rootDir).root.replace(/\\/g, '').replace(':', '').toUpperCase()
const candidateDrives = ['X', 'Y', 'Z', 'W', 'V']
const driveLetter = candidateDrives.find((candidate) => candidate !== currentDrive)
if (!driveLetter) {
return
}
const mappedRoot = `${driveLetter}:\\`
const substList = runAndCapture('subst', []) || ''
if (!substList.includes(`${driveLetter}: =>`)) {
const result = spawnSync('subst', [`${driveLetter}:`, rootDir], {
cwd: rootDir,
shell: true,
stdio: 'inherit',
})
if (result.error || result.status !== 0) {
return
}
}
commandRootDir = mappedRoot
commandAndroidProjectDir = path.join(mappedRoot, 'android')
console.log(`Using short workspace path: ${mappedRoot}`)
}
const proxy = await resolveProxy()
const buildEnv = buildCommandEnv(proxy)
if (proxy) {
console.log(`Using local ${proxy.protocol.toUpperCase()} proxy: ${proxy.host}:${proxy.port}`)
} else {
console.log('No local proxy detected, using direct network access')
}
rmSync(distDir, { recursive: true, force: true })
mkdirSync(distDir, { recursive: true })
ensureWindowsShortPathWorkspace()
ensureAndroidProject()
ensureGradleWrapperTimeout()
ensureAndroidPublishPath()
ensureAndroidLocalProperties()
run(gradleWrapper, [gradleTask], buildEnv, commandAndroidProjectDir)
if (!existsSync(mavenDir)) {
throw new Error(`Missing Android Maven output: ${mavenDir}`)
}
cpSync(wrapperTemplateDir, wrapperDir, { recursive: true })
console.log(`Android wrapper package ready: ${wrapperDir}`)
console.log(`Android Maven repo ready: ${mavenDir}`)