UNPKG

node-nim

Version:

NetEase IM nodejs wrapper based on NetEase IM C++ SDK

574 lines (515 loc) 20 kB
const childProcess = require('child_process') const fs = require('fs') const os = require('os') const path = require('path') const repoRoot = path.resolve(__dirname, '../..') const nodeNimRoot = path.resolve(__dirname, '..') function parseArgs(argv) { const options = {} for (let i = 0; i < argv.length; i++) { const arg = argv[i] if (arg === '--ohos-install-dir') { const value = argv[++i] if (!value || value.startsWith('--')) { throw new Error('--ohos-install-dir requires a value') } options.ohosInstallDir = value } else if (arg === '--output') { const value = argv[++i] if (!value || value.startsWith('--')) { throw new Error('--output requires a value') } options.output = value } else if (arg === '--sdk-version') { const value = argv[++i] if (!value || value.startsWith('--')) { throw new Error('--sdk-version requires a value') } options.sdkVersion = value } else if (arg === '--skip-npm-install') { options.skipNpmInstall = true } else { throw new Error(`Unknown argument: ${arg}`) } } return options } function resolvePath(value) { return path.isAbsolute(value) ? value : path.join(repoRoot, value) } function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true }) } function removeDir(dir) { fs.rmSync(dir, { recursive: true, force: true }) } function removeDirContents(dir, preserveNames = new Set()) { if (!fs.existsSync(dir)) { return } for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (preserveNames.has(entry.name)) { continue } fs.rmSync(path.join(dir, entry.name), { recursive: true, force: true }) } } function copyDir(src, dest, options = {}) { const stat = fs.statSync(src) if (!stat.isDirectory()) { throw new Error(`${src} is not a directory`) } ensureDir(dest) for (const entry of fs.readdirSync(src, { withFileTypes: true })) { if (entry.name === '.DS_Store') { continue } const srcPath = path.join(src, entry.name) const destPath = path.join(dest, entry.name) if (options.filter && !options.filter(srcPath, entry)) { continue } if (entry.isDirectory()) { copyDir(srcPath, destPath, options) } else if (entry.isFile()) { fs.copyFileSync(srcPath, destPath) } } } function replaceInFiles(root, predicate, replacer) { if (!fs.existsSync(root)) { return } for (const entry of fs.readdirSync(root, { withFileTypes: true })) { const entryPath = path.join(root, entry.name) if (entry.isDirectory()) { replaceInFiles(entryPath, predicate, replacer) } else if (entry.isFile() && predicate(entryPath)) { const before = fs.readFileSync(entryPath, 'utf8') const after = replacer(before, entryPath) if (after !== before) { fs.writeFileSync(entryPath, after) } } } } function replaceInFilesWithRestore(root, predicate, replacer) { const originals = new Map() replaceInFiles(root, predicate, (content, file) => { const replaced = replacer(content, file) if (replaced !== content) { originals.set(file, content) } return replaced }) return () => { for (const [file, content] of originals) { fs.writeFileSync(file, content) } } } function unique(values) { return values.filter(Boolean).filter((value, index, array) => array.indexOf(value) === index) } function isFile(file) { try { return fs.statSync(file).isFile() } catch (error) { return false } } function isDir(dir) { try { return fs.statSync(dir).isDirectory() } catch (error) { return false } } function isExecutable(file) { try { fs.accessSync(file, fs.constants.X_OK) return true } catch (error) { return false } } function prependPath(dirs) { const existing = (process.env.PATH || '').split(path.delimiter).filter(Boolean) process.env.PATH = unique([...dirs.filter(isDir), ...existing]).join(path.delimiter) } function hasPathSeparator(command) { return command.includes('/') || command.includes('\\') } function resolveExecutable(command, extraDirs = []) { if (hasPathSeparator(command)) { const executable = path.isAbsolute(command) ? command : path.resolve(repoRoot, command) return isExecutable(executable) ? executable : '' } const pathDirs = (process.env.PATH || '').split(path.delimiter).filter(Boolean) for (const dir of unique([...extraDirs, ...pathDirs])) { const candidate = path.join(dir, command) if (isExecutable(candidate)) { return candidate } } return '' } function getMacOhosEnvironment(sdkVersion) { if (process.platform !== 'darwin') { return null } const home = os.homedir() const commandLineToolHomes = unique([ process.env.COMMAND_LINE_TOOLS_HOME, process.env.DEVECO_TOOLS_HOME, path.join(home, 'Downloads/command-line-tools') ]) const toolHomeCandidates = unique([ process.env.DEVECO_TOOLS_HOME, process.env.DEVECO_STUDIO_HOME ? path.join(process.env.DEVECO_STUDIO_HOME, 'Contents/tools') : '', '/Applications/DevEco-Studio.app/Contents/tools', '/Applications/DevEco Studio.app/Contents/tools', ...commandLineToolHomes ]) const openHarmonyHomes = unique([ process.env.OPENHARMONY_HOME, path.join(home, 'Library/OpenHarmony') ]) const baseSdkHomes = unique([ process.env.OHOS_BASE_SDK_HOME, ...openHarmonyHomes.map((dir) => path.join(dir, 'Sdk')), ...commandLineToolHomes.map((dir) => path.join(dir, 'sdk/default/openharmony')), path.join(home, 'Library/Huawei/Sdk') ]) const nativeHomeCandidates = unique([ process.env.OHOS_NATIVE_HOME, ...baseSdkHomes.map((dir) => path.basename(dir) === 'openharmony' ? path.join(dir, 'native') : path.join(dir, sdkVersion, 'native')) ]) const nativeHome = nativeHomeCandidates.find((dir) => isFile(path.join(dir, 'build/cmake/ohos.toolchain.cmake'))) || '' const sdkHome = nativeHome ? path.dirname(nativeHome) : '' const baseSdkHome = nativeHome && path.basename(sdkHome) === sdkVersion ? path.dirname(sdkHome) : sdkHome const toolHome = toolHomeCandidates.find((dir) => { return isExecutable(path.join(dir, 'bin/ohpm')) || isExecutable(path.join(dir, 'bin/hvigorw')) || isExecutable(path.join(dir, 'ohpm/bin/ohpm')) || isExecutable(path.join(dir, 'hvigor/bin/hvigorw')) }) || '' if (baseSdkHome) { process.env.OHOS_BASE_SDK_HOME = baseSdkHome if (baseSdkHome.endsWith('/Sdk')) { process.env.OPENHARMONY_HOME = path.dirname(baseSdkHome) } } if (nativeHome) { process.env.OHOS_NATIVE_HOME = nativeHome } if (toolHome) { process.env.COMMAND_LINE_TOOLS_HOME = toolHome process.env.DEVECO_TOOLS_HOME = toolHome } prependPath([ nativeHome ? path.join(nativeHome, 'llvm/bin') : '', sdkHome ? path.join(sdkHome, 'toolchains') : '', toolHome ? path.join(toolHome, 'bin') : '', toolHome ? path.join(toolHome, 'node/bin') : '', toolHome ? path.join(toolHome, 'hvigor/bin') : '', toolHome ? path.join(toolHome, 'ohpm/bin') : '' ]) return { nativeHome, nativeHomeCandidates, toolHome, toolHomeCandidates } } function formatMissingDependencies(missing) { return [ 'Missing required tools for OHPM HAR packaging:', ...missing.map((item) => { const details = item.details && item.details.length > 0 ? `\n Checked:\n${item.details.map((detail) => ` - ${detail}`).join('\n')}` : '' return `- ${item.name}${details}` }) ].join('\n') } function resolvePackagingTools(options) { const macEnv = getMacOhosEnvironment(options.sdkVersion) const missing = [] if (process.platform === 'darwin' && !macEnv.nativeHome) { missing.push({ name: `OpenHarmony native SDK ${options.sdkVersion}`, details: macEnv.nativeHomeCandidates.map((dir) => path.join(dir, 'build/cmake/ohos.toolchain.cmake')) }) } const npx = resolveExecutable('npx') if (!npx) { missing.push({ name: 'npx', details: ['Install Node.js with npm, or add npx to PATH'] }) } if (!options.skipNpmInstall && !resolveExecutable('npm')) { missing.push({ name: 'npm', details: ['Install Node.js with npm, or pass --skip-npm-install after dependencies are installed'] }) } const ohpm = resolveExecutable('ohpm') if (!ohpm) { missing.push({ name: 'ohpm', details: process.platform === 'darwin' && macEnv.toolHomeCandidates ? macEnv.toolHomeCandidates.flatMap((dir) => [ path.join(dir, 'bin/ohpm'), path.join(dir, 'ohpm/bin/ohpm') ]) : [ '/home/conan/command-line-tools/bin/ohpm', '/home/conan/command-line-tools/ohpm/bin/ohpm', 'Add ohpm to PATH' ] }) } const hvigorwCommand = process.env.HVIGORW || 'hvigorw' const hvigorw = resolveExecutable(hvigorwCommand) if (!hvigorw) { missing.push({ name: hvigorwCommand, details: process.platform === 'darwin' && macEnv.toolHomeCandidates ? macEnv.toolHomeCandidates.flatMap((dir) => [ path.join(dir, 'bin/hvigorw'), path.join(dir, 'hvigor/bin/hvigorw') ]) : [ '/home/conan/command-line-tools/bin/hvigorw', '/home/conan/command-line-tools/hvigor/bin/hvigorw', 'Set HVIGORW or add hvigorw to PATH' ] }) } if (missing.length > 0) { throw new Error(formatMissingDependencies(missing)) } return { ohpm, hvigorw } } function run(command, args, cwd) { console.log(`[node-nim][ohpm] ${command} ${args.join(' ')}`) childProcess.execFileSync(command, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' }) } function runGit(args) { try { return childProcess.execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() } catch (error) { return '' } } function isSemverCore(version) { return /^\d+\.\d+\.\d+$/.test(version) } function resolveSdkDisplayVersion(sdkVersion) { const sdkDisplayVersions = { 17: '5.0.5(17)' } return sdkDisplayVersions[sdkVersion] || sdkVersion } function resolveSdkApiVersion(sdkVersion) { const value = String(sdkVersion).trim() const displayMatch = value.match(/\((\d+)\)$/) if (displayMatch) { return displayMatch[1] } if (/^\d+$/.test(value)) { return value } if (!value) { throw new Error(`Invalid OHOS SDK version: ${sdkVersion}`) } throw new Error(`OHOS SDK version must be an API level, got: ${sdkVersion}`) } function getPackageVersion() { const gitTag = runGit(['describe', '--abbrev=0', '--tags', '--exact-match']) const gitBranch = runGit(['rev-parse', '--abbrev-ref', 'HEAD']) const latestTag = runGit(['describe', '--tags', '--abbrev=0']) const gitCommitCount = runGit(['rev-list', '--count', 'HEAD', `^${latestTag}~1`]) console.log(`[node-nim][ohpm] Git tag: ${gitTag}`) console.log(`[node-nim][ohpm] Git branch: ${gitBranch}`) console.log(`[node-nim][ohpm] Git commit count: ${gitCommitCount}`) console.log(`[node-nim][ohpm] Latest tag: ${latestTag}`) if (gitTag) { return gitTag } if (!gitBranch) { throw new Error('Unable to derive package version: missing git branch') } if (!gitCommitCount) { throw new Error('Unable to derive package version: missing git commit count') } let channel = 'latest' if (gitBranch.startsWith('release') || gitBranch.startsWith('hotfix')) { channel = 'beta' } else if (gitBranch.startsWith('feature')) { channel = 'alpha' } const branchVersion = gitBranch.replace(/.*\//, '').replace(/-.*/, '') const baseVersion = isSemverCore(branchVersion) ? branchVersion : latestTag if (!baseVersion) { throw new Error(`Unable to derive package version from git branch or latest git tag: ${gitBranch}`) } return `${baseVersion}-${channel}.${gitCommitCount}` } function copyNativeLibraries(nativeLibDir, abiDir, expectedBridge) { if (!fs.existsSync(nativeLibDir)) { throw new Error(`Missing native lib dir: ${nativeLibDir}`) } const soFiles = fs.readdirSync(nativeLibDir).filter((name) => name.endsWith('.so')) if (soFiles.length === 0) { throw new Error(`No .so files found in ${nativeLibDir}`) } const legacyBridge = expectedBridge.replace(/-/g, '_') if (soFiles.includes(legacyBridge)) { throw new Error(`Unexpected OHOS native bridge ${legacyBridge}; rebuild native output as ${expectedBridge}`) } if (!soFiles.includes(expectedBridge)) { throw new Error(`Missing OHOS native bridge ${path.join(nativeLibDir, expectedBridge)}`) } ensureDir(abiDir) for (const file of soFiles) { fs.copyFileSync(path.join(nativeLibDir, file), path.join(abiDir, file)) } } function main() { const args = parseArgs(process.argv.slice(2)) if (!args.ohosInstallDir) { throw new Error('Missing required argument: --ohos-install-dir') } const version = getPackageVersion() const sdkVersion = args.sdkVersion || process.env.OHPM_SDK_VERSION || '17' const sdkApiVersion = resolveSdkApiVersion(sdkVersion) const sdkDisplayVersion = resolveSdkDisplayVersion(sdkVersion) const tools = resolvePackagingTools({ sdkVersion: sdkApiVersion, skipNpmInstall: args.skipNpmInstall }) const projectDir = resolvePath(process.env.OHPM_PROJECT_DIR || 'node-nim/ohpm') const distDir = resolvePath(process.env.OHPM_DIST_DIR || 'node-nim/ohpm/dist') const moduleDir = path.join(projectDir, 'node-nim') const tsDir = path.join(moduleDir, 'ts') const typesDir = path.join(moduleDir, 'types') const abiDir = path.join(moduleDir, 'libs/arm64-v8a') const legacyAbiDir = path.join(moduleDir, 'src/main/libs/arm64-v8a') const legacyLibRootDir = path.join(moduleDir, 'src/main/libs') const loaderFile = path.join(tsDir, 'loader.ts') const ohosInstallDir = resolvePath(args.ohosInstallDir) const nativeLibDir = path.join(ohosInstallDir, 'lib') const harOutput = resolvePath(args.output || process.env.HAR_OUTPUT || `node-nim/ohpm/dist/node-nim-${version}.har`) const tscOutDir = path.join(distDir, 'tsc-out') const nativeTypePackage = 'libnode-nim.so' console.log(`[node-nim][ohpm] Package version: ${version}`) console.log(`[node-nim][ohpm] OHOS SDK version: ${sdkApiVersion}`) console.log(`[node-nim][ohpm] OHOS SDK display version: ${sdkDisplayVersion}`) console.log(`[node-nim][ohpm] OHOS install directory: ${ohosInstallDir}`) if (process.env.OHOS_NATIVE_HOME) { console.log(`[node-nim][ohpm] OHOS_NATIVE_HOME: ${process.env.OHOS_NATIVE_HOME}`) } if (process.env.DEVECO_TOOLS_HOME) { console.log(`[node-nim][ohpm] DEVECO_TOOLS_HOME: ${process.env.DEVECO_TOOLS_HOME}`) } removeDir(distDir) removeDir(path.join(projectDir, 'package')) removeDir(path.join(projectDir, '.hvigor')) removeDir(path.join(projectDir, 'oh_modules')) removeDir(path.join(projectDir, 'build')) removeDir(path.join(moduleDir, 'build')) removeDir(path.join(moduleDir, 'oh_modules')) removeDir(path.join(moduleDir, 'BuildProfile.ets')) removeDir(path.join(projectDir, 'oh-package-lock.json5')) removeDir(path.join(moduleDir, 'oh-package-lock.json5')) ensureDir(distDir) ensureDir(tsDir) if (!fs.existsSync(loaderFile)) { throw new Error(`Missing OHPM loader: ${loaderFile}`) } removeDirContents(tsDir, new Set(['loader.ts'])) removeDir(typesDir) removeDir(abiDir) removeDir(legacyAbiDir) removeDir(legacyLibRootDir) ensureDir(abiDir) copyDir(path.join(nodeNimRoot, 'ts'), tsDir, { filter: (srcPath, entry) => !(entry.isFile() && path.relative(path.join(nodeNimRoot, 'ts'), srcPath) === 'loader.ts') }) if (!args.skipNpmInstall) { run('npm', ['install', '--ignore-scripts', '--no-package-lock', '--include=dev'], nodeNimRoot) } run('npx', ['tsc', '--emitDeclarationOnly', '--declaration', '--declarationDir', typesDir, '--outDir', tscOutDir], nodeNimRoot) const tsExt = (file) => file.endsWith('.ts') || file.endsWith('.d.ts') replaceInFiles(tsDir, tsExt, (content) => content.replace(/from 'ts\/node-nim'/g, "from '../node-nim'")) replaceInFiles(typesDir, tsExt, (content) => content.replace(/from 'ts\/node-nim'/g, "from '../node-nim'")) fs.writeFileSync(path.join(typesDir, 'loader.d.ts'), [ `import type { NodeNimNativeSdk } from '${nativeTypePackage}'`, '', 'declare const sdk: NodeNimNativeSdk', '', 'export { sdk }', 'export default sdk', '' ].join('\n')) const placeholderExt = (file) => ['.json5', '.ets', '.ts', '.json'].includes(path.extname(file)) copyNativeLibraries(nativeLibDir, abiDir, nativeTypePackage) const nativeBridge = path.join(abiDir, nativeTypePackage) if (!fs.existsSync(nativeBridge)) { throw new Error(`Missing OHOS native bridge ${nativeBridge}`) } const nodeAddon = path.join(abiDir, 'node-nim.node') if (fs.existsSync(nodeAddon)) { throw new Error('node-nim.node must not enter the HAR package') } const restoreProjectFiles = replaceInFilesWithRestore(projectDir, placeholderExt, (content) => { return content .replace(/\{\{VERSION\}\}/g, version) .replace(/"\{\{OHOS_SDK_VERSION\}\}"/g, sdkApiVersion) .replace(/\{\{OHOS_SDK_VERSION\}\}/g, sdkApiVersion) }) try { run(tools.ohpm, ['install', '--all'], projectDir) run(tools.hvigorw, ['assembleHar', '--no-daemon'], projectDir) const outputDir = path.join(moduleDir, 'build/default/outputs/default') const harFiles = fs.existsSync(outputDir) ? fs.readdirSync(outputDir).filter((name) => name.endsWith('.har')) : [] if (harFiles.length !== 1) { throw new Error(`Expected one HAR file in ${outputDir}, found ${harFiles.length}`) } const harFile = path.join(outputDir, harFiles[0]) if (!fs.existsSync(harFile)) { throw new Error(`No HAR file generated at ${harFile}`) } ensureDir(path.dirname(harOutput)) fs.copyFileSync(harFile, harOutput) if (fs.statSync(harOutput).size === 0) { throw new Error(`Generated HAR is empty: ${harOutput}`) } } finally { restoreProjectFiles() } console.log(`[node-nim][ohpm] HAR output: ${harOutput}`) } try { main() } catch (error) { console.error(`[node-nim][ohpm] ${error.message}`) process.exit(1) }