diff --git a/app.json b/app.json index b6fbe85..980ddc7 100644 --- a/app.json +++ b/app.json @@ -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" } } } diff --git a/package.json b/package.json index b3b5b1a..d22f9ef 100644 --- a/package.json +++ b/package.json @@ -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 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55ca62e..aa25326 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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==} diff --git a/scripts/package-android.mjs b/scripts/package-android.mjs index 9195046..e43e1ab 100644 --- a/scripts/package-android.mjs +++ b/scripts/package-android.mjs @@ -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}`) diff --git a/scripts/package-ios.mjs b/scripts/package-ios.mjs index 8e4901f..84cc508 100644 --- a/scripts/package-ios.mjs +++ b/scripts/package-ios.mjs @@ -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) }