- 安卓构建成功

This commit is contained in:
Yudi Xiao
2026-04-08 19:08:08 +08:00
parent f9dfde0ace
commit 920de25f96
5 changed files with 371 additions and 13 deletions

View File

@@ -41,6 +41,14 @@
],
"ios": {
"bundleIdentifier": "com.hixcc.duooomi-ble-sdk"
},
"android": {
"permissions": [
"android.permission.BLUETOOTH",
"android.permission.BLUETOOTH_ADMIN",
"android.permission.BLUETOOTH_CONNECT"
],
"package": "com.hixcc.duooomiblesdk"
}
}
}

View File

@@ -29,6 +29,7 @@
"devDependencies": {
"@react-native-community/cli": "^20.1.3",
"@types/react": "~19.2.2",
"hermes-compiler": "0.14.1",
"typescript": "~5.9.2"
},
"private": true

7
pnpm-lock.yaml generated
View File

@@ -45,6 +45,9 @@ importers:
'@types/react':
specifier: ~19.2.2
version: 19.2.14
hermes-compiler:
specifier: 0.14.1
version: 0.14.1
typescript:
specifier: ~5.9.2
version: 5.9.3
@@ -2322,28 +2325,24 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}

View File

@@ -1,7 +1,8 @@
import { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs'
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import net from 'node:net'
import { fileURLToPath, pathToFileURL } from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
@@ -10,23 +11,368 @@ 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) {
const executable = process.platform === 'win32' ? `${command}.cmd` : command
const result = spawnSync(executable, args, {
cwd: rootDir,
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 })
run('pnpx', ['expo-brownfield', 'build:android', '--release'])
ensureWindowsShortPathWorkspace()
ensureAndroidProject()
ensureGradleWrapperTimeout()
ensureAndroidPublishPath()
ensureAndroidLocalProperties()
run(gradleWrapper, [gradleTask], buildEnv, commandAndroidProjectDir)
if (!existsSync(mavenDir)) {
throw new Error(`Missing Android Maven output: ${mavenDir}`)

View File

@@ -13,12 +13,16 @@ const frameworksDir = path.join(packageDir, 'Frameworks')
const wrapperTemplateDir = path.join(rootDir, 'wrappers', 'ios')
function run(command, args) {
const executable = process.platform === 'win32' ? `${command}.cmd` : command
const result = spawnSync(executable, args, {
const result = spawnSync(command, args, {
cwd: rootDir,
shell: process.platform === 'win32',
stdio: 'inherit',
})
if (result.error) {
throw result.error
}
if (result.status !== 0) {
process.exit(result.status ?? 1)
}