From 3b7209800664f1126099d65ae7406331b1272577 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Sat, 14 Mar 2026 21:15:05 +0800 Subject: [PATCH 1/5] har --- .gitignore | 2 + Example/harmony_use_pushy/bun.lock | 10 +- .../harmony/build-profile.json5 | 4 - .../harmony/entry/oh-package-lock.json5 | 6 +- .../harmony/entry/oh-package.json5 | 2 +- .../harmony/entry/src/main/cpp/CMakeLists.txt | 13 +- .../entry/src/main/cpp/PackageProvider.cpp | 13 +- Example/harmony_use_pushy/package.json | 6 +- harmony/har-wrapper/.gitignore | 5 + harmony/har-wrapper/AppScope/app.json5 | 8 + harmony/har-wrapper/build-profile.json5 | 35 ++ .../har-wrapper/hvigor/hvigor-config.json5 | 5 + harmony/har-wrapper/hvigorfile.ts | 6 + harmony/har-wrapper/oh-package.json5 | 4 + harmony/pushy/build-profile.json5 | 9 +- harmony/pushy/src/main/cpp/CMakeLists.txt | 72 +-- package.json | 1 + scripts/build-harmony-har.js | 427 ++++++++++++++++++ 18 files changed, 578 insertions(+), 50 deletions(-) create mode 100644 harmony/har-wrapper/.gitignore create mode 100644 harmony/har-wrapper/AppScope/app.json5 create mode 100644 harmony/har-wrapper/build-profile.json5 create mode 100644 harmony/har-wrapper/hvigor/hvigor-config.json5 create mode 100644 harmony/har-wrapper/hvigorfile.ts create mode 100644 harmony/har-wrapper/oh-package.json5 create mode 100644 scripts/build-harmony-har.js diff --git a/.gitignore b/.gitignore index 2aaee467..32cecff4 100644 --- a/.gitignore +++ b/.gitignore @@ -55,8 +55,10 @@ Example/harmony_use_pushy/libs harmony/package +harmony/pushy.har **/oh_modules harmony/pushy/.preview +harmony/pushy/.cxx Example/harmony_use_pushy/harmony/entry/src/main/resources/rawfile/meta.json **/.hvigor Example/harmony_use_pushy/harmony/entry/src/main/cpp/generated diff --git a/Example/harmony_use_pushy/bun.lock b/Example/harmony_use_pushy/bun.lock index cfe4e16d..a8c145f2 100644 --- a/Example/harmony_use_pushy/bun.lock +++ b/Example/harmony_use_pushy/bun.lock @@ -5,10 +5,10 @@ "": { "name": "harmony_use_pushy", "dependencies": { - "@react-native-oh/react-native-harmony": "^0.72.59", + "@react-native-oh/react-native-harmony": "0.72.96", "react": "18.2.0", "react-native": "0.72.5", - "react-native-update": "^10.37.1", + "react-native-update": "^10.37.15", }, "devDependencies": { "@babel/core": "^7.20.0", @@ -25,7 +25,7 @@ "metro-react-native-babel-preset": "0.76.8", "prettier": "^2.4.1", "react-test-renderer": "18.2.0", - "typescript": "4.8.4", + "typescript": "5.9.3", }, }, }, @@ -1400,7 +1400,7 @@ "react-native": ["react-native@0.72.5", "", { "dependencies": { "@jest/create-cache-key-function": "^29.2.1", "@react-native-community/cli": "11.3.7", "@react-native-community/cli-platform-android": "11.3.7", "@react-native-community/cli-platform-ios": "11.3.7", "@react-native/assets-registry": "^0.72.0", "@react-native/codegen": "^0.72.7", "@react-native/gradle-plugin": "^0.72.11", "@react-native/js-polyfills": "^0.72.1", "@react-native/normalize-colors": "^0.72.0", "@react-native/virtualized-lists": "^0.72.8", "abort-controller": "^3.0.0", "anser": "^1.4.9", "base64-js": "^1.1.2", "deprecated-react-native-prop-types": "4.1.0", "event-target-shim": "^5.0.1", "flow-enums-runtime": "^0.0.5", "invariant": "^2.2.4", "jest-environment-node": "^29.2.1", "jsc-android": "^250231.0.0", "memoize-one": "^5.0.0", "metro-runtime": "0.76.8", "metro-source-map": "0.76.8", "mkdirp": "^0.5.1", "nullthrows": "^1.1.1", "pretty-format": "^26.5.2", "promise": "^8.3.0", "react-devtools-core": "^4.27.2", "react-refresh": "^0.4.0", "react-shallow-renderer": "^16.15.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.24.0-canary-efb381bbf-20230505", "stacktrace-parser": "^0.1.10", "use-sync-external-store": "^1.0.0", "whatwg-fetch": "^3.0.0", "ws": "^6.2.2", "yargs": "^17.6.2" }, "peerDependencies": { "react": "18.2.0" }, "bin": { "react-native": "cli.js" } }, "sha512-oIewslu5DBwOmo7x5rdzZlZXCqDIna0R4dUwVpfmVteORYLr4yaZo5wQnMeR+H7x54GaMhmgeqp0ZpULtulJFg=="], - "react-native-update": ["react-native-update@10.37.1", "", { "dependencies": { "nanoid": "^3.3.3", "react-native-url-polyfill": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-native": ">=0.59.0" } }, "sha512-it7NGlU7WRoOctpt9B39DE5TB881m2EpScLZUVRH98NieWcdxSVK5POWbgTXRwoaOm287LgS6f39TlGbWVuD+A=="], + "react-native-update": ["react-native-update@10.37.15", "", { "dependencies": { "nanoid": "^3.3.3", "react-native-url-polyfill": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-native": ">=0.59.0" } }, "sha512-bEqCjducc+co6gMlDrAKtRVyzkxdObKLoy9iE9fz4F7pCncpr9qp2eUyM15VmOkeYD3V2OZRfhQRQiNByTViaw=="], "react-native-url-polyfill": ["react-native-url-polyfill@2.0.0", "", { "dependencies": { "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "react-native": "*" } }, "sha512-My330Do7/DvKnEvwQc0WdcBnFPploYKp9CYlefDXzIdEaA+PAhDYllkvGeEroEzvc4Kzzj2O4yVdz8v6fjRvhA=="], @@ -1610,7 +1610,7 @@ "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], - "typescript": ["typescript@4.8.4", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "uglify-es": ["uglify-es@3.3.9", "", { "dependencies": { "commander": "~2.13.0", "source-map": "~0.6.1" }, "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ=="], diff --git a/Example/harmony_use_pushy/harmony/build-profile.json5 b/Example/harmony_use_pushy/harmony/build-profile.json5 index dca14ff9..017af333 100644 --- a/Example/harmony_use_pushy/harmony/build-profile.json5 +++ b/Example/harmony_use_pushy/harmony/build-profile.json5 @@ -36,9 +36,5 @@ }, ], }, - { - name: 'pushy', - srcPath: '../node_modules/react-native-update/harmony/pushy', - }, ], } diff --git a/Example/harmony_use_pushy/harmony/entry/oh-package-lock.json5 b/Example/harmony_use_pushy/harmony/entry/oh-package-lock.json5 index 83ae3378..410020cf 100644 --- a/Example/harmony_use_pushy/harmony/entry/oh-package-lock.json5 +++ b/Example/harmony_use_pushy/harmony/entry/oh-package-lock.json5 @@ -7,7 +7,7 @@ "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", "specifiers": { "@rnoh/react-native-openharmony@0.72.96": "@rnoh/react-native-openharmony@0.72.96", - "pushy@../../node_modules/react-native-update/harmony/pushy": "pushy@../../node_modules/react-native-update/harmony/pushy" + "pushy@../../../../harmony/pushy.har": "pushy@../../../../harmony/pushy.har" }, "packages": { "@rnoh/react-native-openharmony@0.72.96": { @@ -17,10 +17,10 @@ "resolved": "https://ohpm.openharmony.cn/ohpm/@rnoh/react-native-openharmony/-/react-native-openharmony-0.72.96.har", "registryType": "ohpm" }, - "pushy@../../node_modules/react-native-update/harmony/pushy": { + "pushy@../../../../harmony/pushy.har": { "name": "pushy", "version": "10.35.1", - "resolved": "../../node_modules/react-native-update/harmony/pushy", + "resolved": "../../../../harmony/pushy.har", "registryType": "local", "dependencies": { "@rnoh/react-native-openharmony": "^0.72.96" diff --git a/Example/harmony_use_pushy/harmony/entry/oh-package.json5 b/Example/harmony_use_pushy/harmony/entry/oh-package.json5 index da130568..84ef2f6c 100644 --- a/Example/harmony_use_pushy/harmony/entry/oh-package.json5 +++ b/Example/harmony_use_pushy/harmony/entry/oh-package.json5 @@ -7,6 +7,6 @@ license: '', dependencies: { '@rnoh/react-native-openharmony': '0.72.96', - pushy: 'file:../../node_modules/react-native-update/harmony/pushy', + pushy: 'file:../../../../harmony/pushy.har', }, } diff --git a/Example/harmony_use_pushy/harmony/entry/src/main/cpp/CMakeLists.txt b/Example/harmony_use_pushy/harmony/entry/src/main/cpp/CMakeLists.txt index 20fd1d11..6536efb4 100644 --- a/Example/harmony_use_pushy/harmony/entry/src/main/cpp/CMakeLists.txt +++ b/Example/harmony_use_pushy/harmony/entry/src/main/cpp/CMakeLists.txt @@ -12,9 +12,17 @@ set(OH_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules") set(WITH_HITRACE_SYSTRACE 1) # for other CMakeLists.txt files to use add_compile_definitions(WITH_HITRACE_SYSTRACE) -add_subdirectory("${OH_MODULES}/pushy/src/main/cpp" ./pushy) +set(PUSHY_CPP_DIR "${NODE_MODULES}/react-native-update/harmony/pushy/src/main/cpp") +if(NOT EXISTS "${PUSHY_CPP_DIR}/PushyTurboModule.cpp") + message(FATAL_ERROR "Cannot find Pushy glue sources in node_modules: ${PUSHY_CPP_DIR}") +endif() add_subdirectory("${RNOH_CPP_DIR}" ./rn) +file(GLOB PUSHY_GLUE_CPP CONFIGURE_DEPENDS "${PUSHY_CPP_DIR}/*.cpp") +add_library(rnoh_pushy SHARED ${PUSHY_GLUE_CPP}) +target_include_directories(rnoh_pushy PUBLIC "${PUSHY_CPP_DIR}") +target_link_libraries(rnoh_pushy PUBLIC rnoh) + file(GLOB GENERATED_CPP_FILES "${CMAKE_CURRENT_SOURCE_DIR}/generated/*.cpp") # this line is needed by codegen v1 add_library(rnoh_app SHARED @@ -22,5 +30,6 @@ add_library(rnoh_app SHARED "./PackageProvider.cpp" "${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp" ) +target_include_directories(rnoh_app PRIVATE "${PUSHY_CPP_DIR}") target_link_libraries(rnoh_app PUBLIC rnoh) -target_link_libraries(rnoh_app PUBLIC rnoh_pushy) \ No newline at end of file +target_link_libraries(rnoh_app PUBLIC rnoh_pushy) diff --git a/Example/harmony_use_pushy/harmony/entry/src/main/cpp/PackageProvider.cpp b/Example/harmony_use_pushy/harmony/entry/src/main/cpp/PackageProvider.cpp index e184e15e..0018d51e 100644 --- a/Example/harmony_use_pushy/harmony/entry/src/main/cpp/PackageProvider.cpp +++ b/Example/harmony_use_pushy/harmony/entry/src/main/cpp/PackageProvider.cpp @@ -1,9 +1,20 @@ #include "RNOH/PackageProvider.h" + +#if __has_include("PushyPackage.h") #include "PushyPackage.h" +#define HAS_PUSHY_PACKAGE 1 +#else +#define HAS_PUSHY_PACKAGE 0 +#endif + using namespace rnoh; std::vector> PackageProvider::getPackages(Package::Context ctx) { +#if HAS_PUSHY_PACKAGE return { std::make_shared(ctx) }; -} \ No newline at end of file +#else + return {}; +#endif +} diff --git a/Example/harmony_use_pushy/package.json b/Example/harmony_use_pushy/package.json index 25128b35..9f45a247 100644 --- a/Example/harmony_use_pushy/package.json +++ b/Example/harmony_use_pushy/package.json @@ -12,10 +12,10 @@ "test": "jest" }, "dependencies": { - "@react-native-oh/react-native-harmony": "^0.72.59", + "@react-native-oh/react-native-harmony": "0.72.96", "react": "18.2.0", "react-native": "0.72.5", - "react-native-update": "^10.37.1" + "react-native-update": "^10.37.15" }, "devDependencies": { "@babel/core": "^7.20.0", @@ -32,7 +32,7 @@ "metro-react-native-babel-preset": "0.76.8", "prettier": "^2.4.1", "react-test-renderer": "18.2.0", - "typescript": "4.8.4" + "typescript": "5.9.3" }, "engines": { "node": ">=16" diff --git a/harmony/har-wrapper/.gitignore b/harmony/har-wrapper/.gitignore new file mode 100644 index 00000000..1fe9e1dc --- /dev/null +++ b/harmony/har-wrapper/.gitignore @@ -0,0 +1,5 @@ +.hvigor +.idea +build +oh_modules +oh-package-lock.json5 diff --git a/harmony/har-wrapper/AppScope/app.json5 b/harmony/har-wrapper/AppScope/app.json5 new file mode 100644 index 00000000..3ba0b4fa --- /dev/null +++ b/harmony/har-wrapper/AppScope/app.json5 @@ -0,0 +1,8 @@ +{ + "app": { + "bundleName": "com.reactnativecn.pushy.har", + "vendor": "reactnativecn", + "versionCode": 1, + "versionName": "1.0.0" + } +} diff --git a/harmony/har-wrapper/build-profile.json5 b/harmony/har-wrapper/build-profile.json5 new file mode 100644 index 00000000..7732a217 --- /dev/null +++ b/harmony/har-wrapper/build-profile.json5 @@ -0,0 +1,35 @@ +{ + "app": { + "signingConfigs": [], + "products": [ + { + "name": "default", + "compatibleSdkVersion": "5.0.0(12)", + "targetSdkVersion": "5.0.0(12)", + "runtimeOS": "HarmonyOS" + } + ], + "buildModeSet": [ + { + "name": "debug" + }, + { + "name": "release" + } + ] + }, + "modules": [ + { + "name": "pushy", + "srcPath": "../pushy", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + } + ] +} diff --git a/harmony/har-wrapper/hvigor/hvigor-config.json5 b/harmony/har-wrapper/hvigor/hvigor-config.json5 new file mode 100644 index 00000000..941ccf5d --- /dev/null +++ b/harmony/har-wrapper/hvigor/hvigor-config.json5 @@ -0,0 +1,5 @@ +{ + "modelVersion": "5.0.0", + "dependencies": { + } +} diff --git a/harmony/har-wrapper/hvigorfile.ts b/harmony/har-wrapper/hvigorfile.ts new file mode 100644 index 00000000..60c195a0 --- /dev/null +++ b/harmony/har-wrapper/hvigorfile.ts @@ -0,0 +1,6 @@ +import {appTasks} from '@ohos/hvigor-ohos-plugin'; + +export default { + system: appTasks, + plugins: [], +}; diff --git a/harmony/har-wrapper/oh-package.json5 b/harmony/har-wrapper/oh-package.json5 new file mode 100644 index 00000000..99251907 --- /dev/null +++ b/harmony/har-wrapper/oh-package.json5 @@ -0,0 +1,4 @@ +{ + modelVersion: '5.0.0', + dependencies: {}, +} diff --git a/harmony/pushy/build-profile.json5 b/harmony/pushy/build-profile.json5 index 65d48627..151a46ee 100644 --- a/harmony/pushy/build-profile.json5 +++ b/harmony/pushy/build-profile.json5 @@ -1,8 +1,15 @@ { "apiType": "stageMode", + "buildOption": { + "externalNativeOptions": { + "path": "./src/main/cpp/CMakeLists.txt", + "arguments": "", + "cppFlags": "" + } + }, "targets": [ { "name": "default", } ] -} \ No newline at end of file +} diff --git a/harmony/pushy/src/main/cpp/CMakeLists.txt b/harmony/pushy/src/main/cpp/CMakeLists.txt index a62ea21e..ad529824 100644 --- a/harmony/pushy/src/main/cpp/CMakeLists.txt +++ b/harmony/pushy/src/main/cpp/CMakeLists.txt @@ -1,39 +1,51 @@ cmake_minimum_required(VERSION 3.13) project(rnupdate) -# Point to android/jni directory for shared source code -set(ANDROID_JNI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../node_modules/react-native-update/android/jni) -set(HDIFFPATCH_DIR ${ANDROID_JNI_DIR}/HDiffPatch) -set(LZMA_DIR ${ANDROID_JNI_DIR}/lzma) -set(HDP_SOURCES - ${CMAKE_CURRENT_SOURCE_DIR}/pushy.c - ${ANDROID_JNI_DIR}/hpatch.c - ${HDIFFPATCH_DIR}/libHDiffPatch/HPatch/patch.c - ${HDIFFPATCH_DIR}/file_for_patch.c - ${LZMA_DIR}/C/LzmaDec.c - ${LZMA_DIR}/C/Lzma2Dec.c -) +set(PUSHY_MODULE_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../../..) +set(PUSHY_ABI ${CMAKE_OHOS_ARCH_ABI}) +if(NOT PUSHY_ABI) + set(PUSHY_ABI ${OHOS_ARCH}) +endif() +set(PREBUILT_RNUPDATE_PATH ${PUSHY_MODULE_ROOT}/libs/${PUSHY_ABI}/librnupdate.so) set(CMAKE_VERBOSE_MAKEFILE on) +if(EXISTS ${PREBUILT_RNUPDATE_PATH}) + add_library(rnupdate SHARED IMPORTED GLOBAL) + set_target_properties(rnupdate PROPERTIES IMPORTED_LOCATION ${PREBUILT_RNUPDATE_PATH}) +else() + # When building the HAR itself, native sources are compiled from the repo checkout. + set(ANDROID_JNI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../android/jni) + set(HDIFFPATCH_DIR ${ANDROID_JNI_DIR}/HDiffPatch) + set(LZMA_DIR ${ANDROID_JNI_DIR}/lzma) + set(HDP_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/pushy.c + ${ANDROID_JNI_DIR}/hpatch.c + ${HDIFFPATCH_DIR}/libHDiffPatch/HPatch/patch.c + ${HDIFFPATCH_DIR}/file_for_patch.c + ${LZMA_DIR}/C/LzmaDec.c + ${LZMA_DIR}/C/Lzma2Dec.c + ) -add_library(rnupdate SHARED - ${HDP_SOURCES} -) + add_library(rnupdate SHARED + ${HDP_SOURCES} + ) -target_include_directories(rnupdate PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR} - ${ANDROID_JNI_DIR} - ${HDIFFPATCH_DIR} - ${HDIFFPATCH_DIR}/libHDiffPatch/HPatch - ${LZMA_DIR}/C -) + target_include_directories(rnupdate PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${ANDROID_JNI_DIR} + ${HDIFFPATCH_DIR} + ${HDIFFPATCH_DIR}/libHDiffPatch/HPatch + ${LZMA_DIR}/C + ) -target_link_libraries(rnupdate PUBLIC - libace_napi.z.so -) - -file(GLOB rnoh_pushy_SRC CONFIGURE_DEPENDS *.cpp) -add_library(rnoh_pushy SHARED ${rnoh_pushy_SRC}) -target_include_directories(rnoh_pushy PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(rnoh_pushy PUBLIC rnoh) + target_link_libraries(rnupdate PUBLIC + libace_napi.z.so + ) +endif() +if(TARGET rnoh) + file(GLOB rnoh_pushy_SRC CONFIGURE_DEPENDS *.cpp) + add_library(rnoh_pushy SHARED ${rnoh_pushy_SRC}) + target_include_directories(rnoh_pushy PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(rnoh_pushy PUBLIC rnoh) +endif() diff --git a/package.json b/package.json index da39954c..7e084a23 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "lint": "eslint \"src/*.@(ts|tsx|js|jsx)\" && tsc --noEmit", "submodule": "git submodule update --init --recursive", "test": "echo \"Error: no test specified\" && exit 1", + "build:harmony-har": "node scripts/build-harmony-har.js", "build:so": "bun submodule && $ANDROID_HOME/ndk/28.2.13676358/ndk-build NDK_PROJECT_PATH=android APP_BUILD_SCRIPT=android/jni/Android.mk NDK_APPLICATION_MK=android/jni/Application.mk NDK_LIBS_OUT=android/lib", "build:ios-debug": "cd Example/testHotUpdate && bun && detox build --configuration ios.sim.debug", "build:ios-release": "cd Example/testHotUpdate && bun && detox build --configuration ios.sim.release", diff --git a/scripts/build-harmony-har.js b/scripts/build-harmony-har.js new file mode 100644 index 00000000..1a589542 --- /dev/null +++ b/scripts/build-harmony-har.js @@ -0,0 +1,427 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const projectRoot = path.resolve(__dirname, '..'); +const androidJniDir = path.join(projectRoot, 'android', 'jni'); +const harmonyModuleDir = path.join(projectRoot, 'harmony', 'pushy'); +const harmonyBuildDir = path.join(harmonyModuleDir, 'build'); +const harmonyNativeStageDir = path.join( + harmonyModuleDir, + 'src', + 'main', + 'cpp', + 'android-generated', +); +const harmonyNativeStageJniDir = path.join(harmonyNativeStageDir, 'jni'); +const wrapperProjectDir = path.join(projectRoot, 'harmony', 'har-wrapper'); +const defaultOutputPath = path.join(projectRoot, 'harmony', 'pushy.har'); +const wrapperProjectFiles = [ + 'hvigorfile.ts', + path.join('hvigor', 'hvigor-config.json5'), + 'oh-package.json5', + path.join('AppScope', 'app.json5'), + 'build-profile.json5', +]; + +const args = parseArgs(process.argv.slice(2)); +const buildMode = normalizeBuildMode( + args['build-mode'] || process.env.HARMONY_BUILD_MODE || 'debug', +); +const skipInstall = + args['skip-install'] || process.env.HARMONY_SKIP_INSTALL === '1'; +const outputDir = args['out-dir'] + ? path.resolve(projectRoot, args['out-dir']) + : process.env.HARMONY_HAR_OUTPUT_DIR + ? path.resolve(projectRoot, process.env.HARMONY_HAR_OUTPUT_DIR) + : null; +const outputPath = args['out-file'] + ? path.resolve(projectRoot, args['out-file']) + : process.env.HARMONY_HAR_OUTPUT_PATH + ? path.resolve(projectRoot, process.env.HARMONY_HAR_OUTPUT_PATH) + : outputDir + ? null + : defaultOutputPath; + +try { + main(); +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +} + +function main() { + syncHarmonyNativeSources(); + let buildError = null; + + try { + buildHar(); + } catch (error) { + buildError = error; + } + + try { + cleanupHarmonyNativeSources(); + } catch (error) { + if (!buildError) { + buildError = error; + } else { + console.warn( + `Warning: failed to clean staged Harmony native sources: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + } + + if (buildError) { + throw buildError; + } +} + +function buildHar() { + ensureWrapperProject(); + + const devecoRoots = getDevEcoRoots(); + const hvigorwPath = resolveBinary('hvigorw', [ + process.env.HVIGORW_PATH, + ...devecoRoots.map((root) => + path.join(root, 'tools', 'hvigor', 'bin', 'hvigorw'), + ), + ]); + const ohpmPath = resolveBinary('ohpm', [ + process.env.OHPM_PATH, + ...devecoRoots.map((root) => + path.join(root, 'tools', 'ohpm', 'bin', 'ohpm'), + ), + ]); + + if (!hvigorwPath) { + fail( + 'Cannot find hvigorw. Set HVIGORW_PATH or install DevEco Studio.', + ); + } + + if (!ohpmPath) { + fail('Cannot find ohpm. Set OHPM_PATH or install DevEco Studio.'); + } + + const env = { + ...process.env, + }; + + if (!env.DEVECO_SDK_HOME) { + const devecoSdkHome = findExistingPath( + devecoRoots.map((root) => path.join(root, 'sdk')), + ); + if (devecoSdkHome) { + env.DEVECO_SDK_HOME = devecoSdkHome; + } + } + + if (!env.DEVECO_STUDIO_HOME) { + const devecoStudioHome = findExistingPath(devecoRoots); + if (devecoStudioHome) { + env.DEVECO_STUDIO_HOME = devecoStudioHome; + } + } + + if (!skipInstall) { + runCommand(ohpmPath, ['install'], { + cwd: harmonyModuleDir, + env, + label: 'Install Harmony dependencies', + }); + + runCommand(ohpmPath, ['install'], { + cwd: wrapperProjectDir, + env, + label: 'Install wrapper project dependencies', + }); + } + + const hvigorArgs = ['assembleHar']; + if (buildMode !== 'debug') { + hvigorArgs.push('-p', `buildMode=${buildMode}`); + } + + runCommand(hvigorwPath, hvigorArgs, { + cwd: wrapperProjectDir, + env, + label: `Build Harmony HAR (${buildMode})`, + }); + + const harPath = findNewestHar(harmonyBuildDir); + if (!harPath) { + fail( + `Build finished but no .har artifact was found under ${relativeToProject( + harmonyBuildDir, + )}`, + ); + } + + let finalPath = harPath; + if (outputDir || outputPath) { + finalPath = outputPath || path.join(outputDir, path.basename(harPath)); + fs.mkdirSync(path.dirname(finalPath), { recursive: true }); + fs.copyFileSync(harPath, finalPath); + } + + console.log(`HAR package ready: ${finalPath}`); +} + +function syncHarmonyNativeSources() { + ensureFileExists( + path.join(androidJniDir, 'hpatch.c'), + `Missing Android native source: ${relativeToProject( + path.join(androidJniDir, 'hpatch.c'), + )}`, + ); + ensureFileExists( + path.join(androidJniDir, 'hpatch.h'), + `Missing Android native source: ${relativeToProject( + path.join(androidJniDir, 'hpatch.h'), + )}`, + ); + ensureFileExists( + path.join(androidJniDir, 'HDiffPatch'), + `Missing Android native source directory: ${relativeToProject( + path.join(androidJniDir, 'HDiffPatch'), + )}`, + ); + ensureFileExists( + path.join(androidJniDir, 'lzma', 'C'), + `Missing Android native source directory: ${relativeToProject( + path.join(androidJniDir, 'lzma', 'C'), + )}`, + ); + + fs.rmSync(harmonyNativeStageDir, { recursive: true, force: true }); + fs.mkdirSync(path.join(harmonyNativeStageJniDir, 'lzma'), { + recursive: true, + }); + + copyPath( + path.join(androidJniDir, 'hpatch.c'), + path.join(harmonyNativeStageJniDir, 'hpatch.c'), + ); + copyPath( + path.join(androidJniDir, 'hpatch.h'), + path.join(harmonyNativeStageJniDir, 'hpatch.h'), + ); + copyPath( + path.join(androidJniDir, 'HDiffPatch'), + path.join(harmonyNativeStageJniDir, 'HDiffPatch'), + ); + copyPath( + path.join(androidJniDir, 'lzma', 'C'), + path.join(harmonyNativeStageJniDir, 'lzma', 'C'), + ); +} + +function cleanupHarmonyNativeSources() { + fs.rmSync(harmonyNativeStageDir, { recursive: true, force: true }); +} + +function copyPath(sourcePath, targetPath) { + const stats = fs.statSync(sourcePath); + if (stats.isDirectory()) { + fs.cpSync(sourcePath, targetPath, { + recursive: true, + force: true, + filter: (entry) => path.basename(entry) !== '.git', + }); + return; + } + + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + fs.copyFileSync(sourcePath, targetPath); +} + +function ensureWrapperProject() { + wrapperProjectFiles.forEach((relativePath) => { + const fullPath = path.join(wrapperProjectDir, relativePath); + ensureFileExists( + fullPath, + `Missing Harmony wrapper file: ${relativeToProject(fullPath)}`, + ); + }); +} + +function parseArgs(argv) { + const parsed = {}; + + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (!token.startsWith('--')) { + fail(`Unsupported argument: ${token}`); + } + + const keyValue = token.slice(2).split('='); + const key = keyValue[0]; + const inlineValue = keyValue.length > 1 ? keyValue.slice(1).join('=') : ''; + + if (key === 'skip-install') { + parsed[key] = true; + continue; + } + + const value = inlineValue || argv[index + 1]; + if (!value || value.startsWith('--')) { + fail(`Missing value for --${key}`); + } + + parsed[key] = value; + if (!inlineValue) { + index += 1; + } + } + + return parsed; +} + +function normalizeBuildMode(value) { + const mode = String(value).toLowerCase(); + if (mode === 'debug' || mode === 'release') { + return mode; + } + + fail(`Unsupported build mode: ${value}. Use debug or release.`); +} + +function getDevEcoRoots() { + const roots = new Set(); + const envCandidates = [ + process.env.DEVECO_STUDIO_HOME, + process.env.DEVECO_SDK_HOME, + ].filter(Boolean); + + envCandidates.forEach((candidate) => { + const normalized = normalizeDevEcoRoot(candidate); + if (normalized) { + roots.add(normalized); + } + }); + + roots.add('/Applications/DevEco-Studio.app/Contents'); + return Array.from(roots); +} + +function normalizeDevEcoRoot(value) { + const resolved = path.resolve(value); + const basename = path.basename(resolved); + + if (basename === 'sdk') { + return path.dirname(resolved); + } + + if (basename === 'Contents') { + return resolved; + } + + if (resolved.endsWith('.app')) { + return path.join(resolved, 'Contents'); + } + + if (fs.existsSync(path.join(resolved, 'Contents', 'tools'))) { + return path.join(resolved, 'Contents'); + } + + return resolved; +} + +function resolveBinary(name, candidates) { + const explicitPath = findExistingPath(candidates); + if (explicitPath) { + return explicitPath; + } + + const whichResult = spawnSync('bash', ['-lc', `command -v ${name}`], { + encoding: 'utf8', + }); + if (whichResult.status === 0) { + const resolved = whichResult.stdout.trim(); + if (resolved) { + return resolved; + } + } + + return null; +} + +function findExistingPath(candidates) { + for (const candidate of candidates) { + if (candidate && fs.existsSync(candidate)) { + return candidate; + } + } + + return null; +} + +function runCommand(command, commandArgs, options) { + const { cwd, env, label } = options; + console.log(`> ${label}`); + console.log(` ${[command, ...commandArgs].join(' ')}`); + + const result = spawnSync(command, commandArgs, { + cwd, + env, + stdio: 'inherit', + }); + + if (result.status !== 0) { + fail(`${label} failed with exit code ${result.status || 1}.`); + } +} + +function findNewestHar(rootDir) { + if (!fs.existsSync(rootDir)) { + return null; + } + + let latestFile = null; + let latestMtime = 0; + const queue = [rootDir]; + + while (queue.length > 0) { + const currentDir = queue.pop(); + const entries = fs.readdirSync(currentDir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(currentDir, entry.name); + if (entry.isDirectory()) { + queue.push(fullPath); + continue; + } + + if (!entry.isFile() || !entry.name.endsWith('.har')) { + continue; + } + + const stat = fs.statSync(fullPath); + if (!latestFile || stat.mtimeMs > latestMtime) { + latestFile = fullPath; + latestMtime = stat.mtimeMs; + } + } + } + + return latestFile; +} + +function ensureFileExists(filePath, message) { + if (!fs.existsSync(filePath)) { + fail(message); + } +} + +function relativeToProject(filePath) { + return path.relative(projectRoot, filePath) || '.'; +} + +function fail(message) { + throw new Error(message); +} From 56a635934ad49a16a64d09533f191c82486dcb40 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 16 Mar 2026 14:39:30 +0800 Subject: [PATCH 2/5] cpp --- .../harmony/entry/src/main/cpp/CMakeLists.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Example/harmony_use_pushy/harmony/entry/src/main/cpp/CMakeLists.txt b/Example/harmony_use_pushy/harmony/entry/src/main/cpp/CMakeLists.txt index 6536efb4..328e9a98 100644 --- a/Example/harmony_use_pushy/harmony/entry/src/main/cpp/CMakeLists.txt +++ b/Example/harmony_use_pushy/harmony/entry/src/main/cpp/CMakeLists.txt @@ -13,13 +13,14 @@ set(WITH_HITRACE_SYSTRACE 1) # for other CMakeLists.txt files to use add_compile_definitions(WITH_HITRACE_SYSTRACE) set(PUSHY_CPP_DIR "${NODE_MODULES}/react-native-update/harmony/pushy/src/main/cpp") -if(NOT EXISTS "${PUSHY_CPP_DIR}/PushyTurboModule.cpp") - message(FATAL_ERROR "Cannot find Pushy glue sources in node_modules: ${PUSHY_CPP_DIR}") +set(PUSHY_TURBO_MODULE_CPP "${PUSHY_CPP_DIR}/PushyTurboModule.cpp") +if(NOT EXISTS "${PUSHY_TURBO_MODULE_CPP}") + message(FATAL_ERROR "Cannot find Pushy glue source in node_modules: ${PUSHY_TURBO_MODULE_CPP}") endif() add_subdirectory("${RNOH_CPP_DIR}" ./rn) -file(GLOB PUSHY_GLUE_CPP CONFIGURE_DEPENDS "${PUSHY_CPP_DIR}/*.cpp") -add_library(rnoh_pushy SHARED ${PUSHY_GLUE_CPP}) +# Build only the RNOH glue here. librnupdate.so is provided by pushy.har. +add_library(rnoh_pushy SHARED "${PUSHY_TURBO_MODULE_CPP}") target_include_directories(rnoh_pushy PUBLIC "${PUSHY_CPP_DIR}") target_link_libraries(rnoh_pushy PUBLIC rnoh) From 5801728a43cea3da4655c8aaf666472e4af2a55b Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 16 Mar 2026 20:55:39 +0800 Subject: [PATCH 3/5] fix ci --- .github/workflows/publish.yml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f853443e..6ca68950 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,14 +10,25 @@ permissions: jobs: build: runs-on: ubuntu-latest + container: ghcr.io/sanchuanhehe/harmony-next-pipeline-docker/harmonyos-ci-image:latest timeout-minutes: 10 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 + with: + submodules: recursive - uses: oven-sh/setup-bun@v2 # Setup .npmrc file to publish to npm - - uses: actions/setup-node@v6 - with: - node-version: 24 - registry-url: 'https://registry.npmjs.org' + # - uses: actions/setup-node@v6 + # with: + # node-version: 24 + # registry-url: 'https://registry.npmjs.org' - run: bun install --frozen-lockfile + - name: Verify Harmony build tools + run: | + command -v hvigorw >/dev/null 2>&1 || (echo "Missing hvigorw in the Harmony container image." >&2; exit 1) + command -v ohpm >/dev/null 2>&1 || (echo "Missing ohpm in the Harmony container image." >&2; exit 1) + - name: Build Harmony HAR + run: npm run build:harmony-har -- --build-mode release + - name: Verify Harmony HAR artifact + run: test -f harmony/pushy.har - run: npm publish --provenance --access public From 933bfd4a41d3bd9288f532c26ba0787d8efc3afb Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 16 Mar 2026 21:13:08 +0800 Subject: [PATCH 4/5] refactor: Implement UpdateContext as a singleton and centralize preference and launch state management. --- harmony/pushy/src/main/ets/DownloadTask.ts | 251 +++++++++------- harmony/pushy/src/main/ets/Logger.ts | 31 +- .../main/ets/PushyFileJSBundleProvider.ets | 2 +- .../pushy/src/main/ets/PushyTurboModule.ts | 82 +----- harmony/pushy/src/main/ets/UpdateContext.ts | 276 +++++++++++++----- 5 files changed, 385 insertions(+), 257 deletions(-) diff --git a/harmony/pushy/src/main/ets/DownloadTask.ts b/harmony/pushy/src/main/ets/DownloadTask.ts index 72905c88..90aa88c5 100644 --- a/harmony/pushy/src/main/ets/DownloadTask.ts +++ b/harmony/pushy/src/main/ets/DownloadTask.ts @@ -51,6 +51,108 @@ export class DownloadTask { } } + private async ensureDirectory(path: string): Promise { + if (!path || fileIo.accessSync(path)) { + return; + } + + const parentPath = path.substring(0, path.lastIndexOf('/')); + if (parentPath && parentPath !== path) { + await this.ensureDirectory(parentPath); + } + + if (!fileIo.accessSync(path)) { + await fileIo.mkdir(path); + } + } + + private async ensureParentDirectory(filePath: string): Promise { + const parentPath = filePath.substring(0, filePath.lastIndexOf('/')); + if (!parentPath) { + return; + } + await this.ensureDirectory(parentPath); + } + + private async recreateDirectory(path: string): Promise { + await this.removeDirectory(path); + await this.ensureDirectory(path); + } + + private async readFileContent(filePath: string): Promise { + const stat = await fileIo.stat(filePath); + const reader = await fileIo.open(filePath, fileIo.OpenMode.READ_ONLY); + const content = new ArrayBuffer(stat.size); + + try { + await fileIo.read(reader.fd, content); + return content; + } finally { + await fileIo.close(reader); + } + } + + private toUint8Array(content: ArrayBuffer | Uint8Array): Uint8Array { + if (content instanceof Uint8Array) { + return content; + } + return new Uint8Array(content); + } + + private async writeFileContent( + targetFile: string, + content: ArrayBuffer | Uint8Array, + ): Promise { + const payload = this.toUint8Array(content); + await this.ensureParentDirectory(targetFile); + if (fileIo.accessSync(targetFile)) { + await fileIo.unlink(targetFile); + } + + let writer: fileIo.File | null = null; + try { + writer = await fileIo.open( + targetFile, + fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY, + ); + const chunkSize = 4096; + let bytesWritten = 0; + + while (bytesWritten < payload.byteLength) { + const chunk = payload.slice(bytesWritten, bytesWritten + chunkSize); + await fileIo.write(writer.fd, chunk); + bytesWritten += chunk.byteLength; + } + } finally { + if (writer) { + await fileIo.close(writer); + } + } + } + + private parseJsonEntry(content: ArrayBuffer): Record { + return JSON.parse( + new util.TextDecoder().decodeToString(new Uint8Array(content)), + ) as Record; + } + + private async applyBundlePatch( + originContent: ArrayBuffer | Uint8Array, + patchContent: ArrayBuffer | Uint8Array, + outputFile: string, + ): Promise { + try { + const patched = await Pushy.hdiffPatch( + this.toUint8Array(originContent), + this.toUint8Array(patchContent), + ); + await this.writeFileContent(outputFile, patched); + } catch (error) { + error.message = `Failed to process bundle patch: ${error.message}`; + throw error; + } + } + private async downloadFile(params: DownloadTaskParams): Promise { const httpRequest = http.createHttp(); this.hash = params.hash; @@ -89,14 +191,7 @@ export class DownloadTask { if (exists) { await fileIo.unlink(params.targetFile); } else { - const targetDir = params.targetFile.substring( - 0, - params.targetFile.lastIndexOf('/'), - ); - exists = fileIo.accessSync(targetDir); - if (!exists) { - await fileIo.mkdir(targetDir); - } + await this.ensureParentDirectory(params.targetFile); } writer = await fileIo.open( @@ -198,9 +293,7 @@ export class DownloadTask { private async doFullPatch(params: DownloadTaskParams): Promise { await this.downloadFile(params); - await this.removeDirectory(params.unzipDirectory); - await fileIo.mkdir(params.unzipDirectory); - + await this.recreateDirectory(params.unzipDirectory); await zlib.decompressFile(params.targetFile, params.unzipDirectory); } @@ -243,12 +336,11 @@ export class DownloadTask { private async doPatchFromApp(params: DownloadTaskParams): Promise { await this.downloadFile(params); - await this.removeDirectory(params.unzipDirectory); - await fileIo.mkdir(params.unzipDirectory); + await this.recreateDirectory(params.unzipDirectory); let foundDiff = false; let foundBundlePatch = false; - const copyList: Map> = new Map(); + const copyList: Map = new Map(); await zlib.decompressFile(params.targetFile, params.unzipDirectory); const zipFile = await this.processUnzippedFiles(params.unzipDirectory); for (const entry of zipFile.entries) { @@ -256,11 +348,7 @@ export class DownloadTask { if (fn === '__diff.json') { foundDiff = true; - const bufferArray = new Uint8Array(entry.content); - const obj = JSON.parse( - new util.TextDecoder().decodeToString(bufferArray), - ); - + const obj = this.parseJsonEntry(entry.content); const copies = obj.copies as Record; for (const [to, rawPath] of Object.entries(copies)) { let from = rawPath.replace('resources/rawfile/', ''); @@ -282,35 +370,16 @@ export class DownloadTask { } if (fn === 'bundle.harmony.js.patch') { foundBundlePatch = true; - try { - const resourceManager = this.context.resourceManager; - const originContent = await resourceManager.getRawFileContent( - 'bundle.harmony.js', - ); - const patched = await Pushy.hdiffPatch( - new Uint8Array(originContent.buffer), - new Uint8Array(entry.content), - ); - const outputFile = `${params.unzipDirectory}/bundle.harmony.js`; - const writer = await fileIo.open( - outputFile, - fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY, - ); - const chunkSize = 4096; - let bytesWritten = 0; - const totalLength = patched.byteLength; - - while (bytesWritten < totalLength) { - const chunk = patched.slice(bytesWritten, bytesWritten + chunkSize); - await fileIo.write(writer.fd, chunk); - bytesWritten += chunk.byteLength; - } - await fileIo.close(writer); - continue; - } catch (error) { - error.message = 'Failed to process bundle patch:' + error.message; - throw error; - } + const resourceManager = this.context.resourceManager; + const originContent = await resourceManager.getRawFileContent( + 'bundle.harmony.js', + ); + await this.applyBundlePatch( + originContent, + entry.content, + `${params.unzipDirectory}/bundle.harmony.js`, + ); + continue; } } @@ -325,8 +394,7 @@ export class DownloadTask { private async doPatchFromPpk(params: DownloadTaskParams): Promise { await this.downloadFile(params); - await this.removeDirectory(params.unzipDirectory); - await fileIo.mkdir(params.unzipDirectory); + await this.recreateDirectory(params.unzipDirectory); let foundDiff = false; let foundBundlePatch = false; @@ -338,74 +406,43 @@ export class DownloadTask { if (fn === '__diff.json') { foundDiff = true; - await fileIo - .copyDir(params.originDirectory + '/', params.unzipDirectory + '/') - .catch(error => { - console.error('copy error:', error); - }); - - const bufferArray = new Uint8Array(entry.content); - const obj = JSON.parse( - new util.TextDecoder().decodeToString(bufferArray), + await fileIo.copyDir( + `${params.originDirectory}/`, + `${params.unzipDirectory}/`, ); + const obj = this.parseJsonEntry(entry.content); + const { copies, deletes } = obj; for (const [to, from] of Object.entries(copies)) { - await fileIo - .copyFile( - `${params.originDirectory}/${from}`, - `${params.unzipDirectory}/${to}`, - ) - .catch(error => { - console.error('copy error:', error); - }); + const targetFile = `${params.unzipDirectory}/${to}`; + await this.ensureParentDirectory(targetFile); + await fileIo.copyFile( + `${params.originDirectory}/${from}`, + targetFile, + ); } for (const fileToDelete of Object.keys(deletes)) { - await fileIo - .unlink(`${params.unzipDirectory}/${fileToDelete}`) - .catch(error => { - console.error('delete error:', error); - }); + const targetFile = `${params.unzipDirectory}/${fileToDelete}`; + if (fileIo.accessSync(targetFile)) { + await fileIo.unlink(targetFile); + } } continue; } if (fn === 'bundle.harmony.js.patch') { foundBundlePatch = true; - const filePath = params.originDirectory + '/bundle.harmony.js'; - const res = fileIo.accessSync(filePath); - if (res) { - const stat = await fileIo.stat(filePath); - const reader = await fileIo.open(filePath, fileIo.OpenMode.READ_ONLY); - const fileSize = stat.size; - const originContent = new ArrayBuffer(fileSize); - try { - await fileIo.read(reader.fd, originContent); - const patched = await Pushy.hdiffPatch( - new Uint8Array(originContent), - new Uint8Array(entry.content), - ); - const outputFile = `${params.unzipDirectory}/bundle.harmony.js`; - const writer = await fileIo.open( - outputFile, - fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY, - ); - const chunkSize = 4096; - let bytesWritten = 0; - const totalLength = patched.byteLength; - while (bytesWritten < totalLength) { - const chunk = patched.slice( - bytesWritten, - bytesWritten + chunkSize, - ); - await fileIo.write(writer.fd, chunk); - bytesWritten += chunk.byteLength; - } - await fileIo.close(writer); - continue; - } finally { - await fileIo.close(reader); - } + const bundlePath = `${params.originDirectory}/bundle.harmony.js`; + if (!fileIo.accessSync(bundlePath)) { + throw Error(`Origin bundle not found: ${bundlePath}`); } + const originContent = await this.readFileContent(bundlePath); + await this.applyBundlePatch( + originContent, + entry.content, + `${params.unzipDirectory}/bundle.harmony.js`, + ); + continue; } } @@ -433,6 +470,7 @@ export class DownloadTask { .split('.')[0]; const mediaBuffer = await resourceManager.getMediaByName(mediaName); for (const target of targets) { + await this.ensureParentDirectory(target); const fileStream = fileIo.createStreamSync(target, 'w+'); fileStream.writeSync(mediaBuffer.buffer); fileStream.close(); @@ -441,6 +479,7 @@ export class DownloadTask { } const fromContent = await resourceManager.getRawFd(from); for (const target of targets) { + await this.ensureParentDirectory(target); saveFileToSandbox(fromContent, target); } } diff --git a/harmony/pushy/src/main/ets/Logger.ts b/harmony/pushy/src/main/ets/Logger.ts index 394241cf..0946e087 100644 --- a/harmony/pushy/src/main/ets/Logger.ts +++ b/harmony/pushy/src/main/ets/Logger.ts @@ -1,4 +1,3 @@ - import hilog from '@ohos.hilog'; class Logger { @@ -7,29 +6,47 @@ class Logger { private format: string = '%{public}s,%{public}s'; private isDebug: boolean; - constructor(prefix: string = 'MyApp', domain: number = 0xFF00, isDebug = false) { + constructor( + prefix: string = 'MyApp', + domain: number = 0xff00, + isDebug = false, + ) { this.prefix = prefix; this.domain = domain; this.isDebug = isDebug; } + private normalizeArgs(args: string[]): [string, string] { + if (args.length === 0) { + return ['', '']; + } + if (args.length === 1) { + return [args[0], '']; + } + return [args[0], args.slice(1).join(' ')]; + } + debug(...args: string[]): void { if (this.isDebug) { - hilog.debug(this.domain, this.prefix, this.format, args); + const [tag, message] = this.normalizeArgs(args); + hilog.debug(this.domain, this.prefix, this.format, tag, message); } } info(...args: string[]): void { - hilog.info(this.domain, this.prefix, this.format, args); + const [tag, message] = this.normalizeArgs(args); + hilog.info(this.domain, this.prefix, this.format, tag, message); } warn(...args: string[]): void { - hilog.warn(this.domain, this.prefix, this.format, args); + const [tag, message] = this.normalizeArgs(args); + hilog.warn(this.domain, this.prefix, this.format, tag, message); } error(...args: string[]): void { - hilog.error(this.domain, this.prefix, this.format, args); + const [tag, message] = this.normalizeArgs(args); + hilog.error(this.domain, this.prefix, this.format, tag, message); } } -export default new Logger('geolocation', 0xFF00, false) \ No newline at end of file +export default new Logger('pushy', 0xff00, false); diff --git a/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets b/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets index 06c9a172..5c03e052 100644 --- a/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets +++ b/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets @@ -13,7 +13,7 @@ export class PushyFileJSBundleProvider extends JSBundleProvider { constructor(context: common.UIAbilityContext) { super(); - this.updateContext = new UpdateContext(context); + this.updateContext = UpdateContext.getInstance(context); this.path = this.updateContext.getBundleUrl(); } diff --git a/harmony/pushy/src/main/ets/PushyTurboModule.ts b/harmony/pushy/src/main/ets/PushyTurboModule.ts index e8b78efd..577e9385 100644 --- a/harmony/pushy/src/main/ets/PushyTurboModule.ts +++ b/harmony/pushy/src/main/ets/PushyTurboModule.ts @@ -3,13 +3,10 @@ import { TurboModuleContext, } from '@rnoh/react-native-openharmony/ts'; import common from '@ohos.app.ability.common'; -import dataPreferences from '@ohos.data.preferences'; -import { bundleManager } from '@kit.AbilityKit'; import logger from './Logger'; import { UpdateModuleImpl } from './UpdateModuleImpl'; import { UpdateContext } from './UpdateContext'; import { EventHub } from './EventHub'; -import { util } from '@kit.ArkTS'; const TAG = 'PushyTurboModule'; @@ -21,86 +18,25 @@ export class PushyTurboModule extends TurboModule { super(ctx); logger.debug(TAG, ',PushyTurboModule constructor'); this.mUiCtx = ctx.uiAbilityContext; - this.context = new UpdateContext(this.mUiCtx); + this.context = UpdateContext.getInstance(this.mUiCtx); EventHub.getInstance().setRNInstance(ctx.rnInstance); } getConstants(): Object { logger.debug(TAG, ',call getConstants'); - const context = this.mUiCtx; - const preferencesManager = dataPreferences.getPreferencesSync(context, { - name: 'update', - }); - const isFirstTime = preferencesManager.getSync( - 'isFirstTime', - false, - ) as boolean; - const rolledBackVersion = preferencesManager.getSync( - 'rolledBackVersion', - '', - ) as string; - const uuid = preferencesManager.getSync('uuid', '') as string; - const currentVersion = preferencesManager.getSync( - 'currentVersion', - '', - ) as string; + const { isFirstTime, rolledBackVersion } = + this.context.consumeLaunchMarks(); + const uuid = this.context.getKv('uuid'); + const currentVersion = this.context.getCurrentVersion(); const currentVersionInfo = this.context.getKv(`hash_${currentVersion}`); - const isUsingBundleUrl = this.context.getIsUsingBundleUrl(); - let bundleFlags = - bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION; - let packageVersion = ''; - try { - const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleFlags); - packageVersion = bundleInfo?.versionName || 'Unknown'; - } catch (error) { - console.error('Failed to get bundle info:', error); - } - const storedPackageVersion = preferencesManager.getSync( - 'packageVersion', - '', - ) as string; - const storedBuildTime = preferencesManager.getSync( - 'buildTime', - '', - ) as string; - let buildTime = ''; - try { - const resourceManager = this.mUiCtx.resourceManager; - const content = resourceManager.getRawFileContentSync('meta.json'); - const metaData = JSON.parse( - new util.TextDecoder().decodeToString(content), - ); - if (metaData.pushy_build_time) { - buildTime = String(metaData.pushy_build_time); - } - } catch {} - - const packageVersionChanged = - !storedPackageVersion || packageVersion !== storedPackageVersion; - const buildTimeChanged = !storedBuildTime || buildTime !== storedBuildTime; - - if (packageVersionChanged || buildTimeChanged) { - this.context.cleanUp(); - preferencesManager.putSync('packageVersion', packageVersion); - preferencesManager.putSync('buildTime', buildTime); - } - - if (isFirstTime) { - preferencesManager.deleteSync('isFirstTime'); - } - - if (rolledBackVersion) { - preferencesManager.deleteSync('rolledBackVersion'); - } - return { - downloadRootDir: `${context.filesDir}/_update`, + downloadRootDir: this.context.getRootDir(), currentVersionInfo, - packageVersion, + packageVersion: this.context.getPackageVersion(), currentVersion, - buildTime, - isUsingBundleUrl, + buildTime: this.context.getBuildTime(), + isUsingBundleUrl: this.context.getIsUsingBundleUrl(), isFirstTime, rolledBackVersion, uuid, diff --git a/harmony/pushy/src/main/ets/UpdateContext.ts b/harmony/pushy/src/main/ets/UpdateContext.ts index d9fee124..dd8d413f 100644 --- a/harmony/pushy/src/main/ets/UpdateContext.ts +++ b/harmony/pushy/src/main/ets/UpdateContext.ts @@ -1,50 +1,135 @@ import preferences from '@ohos.data.preferences'; import bundleManager from '@ohos.bundle.bundleManager'; import fileIo from '@ohos.file.fs'; -import { DownloadTask } from './DownloadTask'; import common from '@ohos.app.ability.common'; +import { util } from '@kit.ArkTS'; +import { DownloadTask } from './DownloadTask'; import { DownloadTaskParams } from './DownloadTaskParams'; +type LaunchMarks = { + isFirstTime: boolean; + rolledBackVersion: string; +}; + export class UpdateContext { + private static instances: Map = new Map(); + private static isUsingBundleUrl: boolean = false; + private static ignoreRollbackInCurrentProcess: boolean = false; + + static getInstance(context: common.UIAbilityContext): UpdateContext { + const key = context.filesDir; + const cached = UpdateContext.instances.get(key); + if (cached) { + return cached; + } + + const instance = new UpdateContext(context); + UpdateContext.instances.set(key, instance); + return instance; + } + private context: common.UIAbilityContext; private rootDir: string; private preferences: preferences.Preferences; - private static DEBUG: boolean = false; - private static isUsingBundleUrl: boolean = false; - constructor(context: common.UIAbilityContext) { + private constructor(context: common.UIAbilityContext) { this.context = context; - this.rootDir = context.filesDir + '/_update'; + this.rootDir = `${context.filesDir}/_update`; + this.ensureRootDir(); + this.initPreferences(); + } + private ensureRootDir(): void { try { if (!fileIo.accessSync(this.rootDir)) { fileIo.mkdirSync(this.rootDir); } - } catch (e) { - console.error('Failed to create root directory:', e); + } catch (error) { + console.error('Failed to create root directory:', error); } - this.initPreferences(); } - private initPreferences() { + private initPreferences(): void { try { this.preferences = preferences.getPreferencesSync(this.context, { name: 'update', }); + const packageVersion = this.getPackageVersion(); - const storedVersion = this.preferences.getSync('packageVersion', ''); - if (!storedVersion) { - this.preferences.putSync('packageVersion', packageVersion); - this.preferences.flush(); - } else if (storedVersion && packageVersion !== storedVersion) { - this.cleanUp(); + const buildTime = this.getBuildTime(); + const storedPackageVersion = this.getStringPreference('packageVersion'); + const storedBuildTime = this.getStringPreference('buildTime'); + const packageVersionChanged = + !!storedPackageVersion && packageVersion !== storedPackageVersion; + const buildTimeChanged = + !!storedBuildTime && buildTime !== storedBuildTime; + + if (packageVersionChanged || buildTimeChanged) { + this.scheduleCleanUp(); this.preferences.clear(); + UpdateContext.ignoreRollbackInCurrentProcess = false; + } + + let shouldFlush = packageVersionChanged || buildTimeChanged; + if (this.getStringPreference('packageVersion') !== packageVersion) { this.preferences.putSync('packageVersion', packageVersion); + shouldFlush = true; + } + if (this.getStringPreference('buildTime') !== buildTime) { + this.preferences.putSync('buildTime', buildTime); + shouldFlush = true; + } + + if (shouldFlush) { this.preferences.flush(); } - } catch (e) { - console.error('Failed to init preferences:', e); + } catch (error) { + console.error('Failed to init preferences:', error); + } + } + + private getStringPreference(key: string, fallback: string = ''): string { + const value = this.preferences.getSync(key, fallback); + if (typeof value === 'string') { + return value; + } + if (value === null || value === undefined) { + return fallback; + } + return String(value); + } + + private getBooleanPreference( + key: string, + fallback: boolean = false, + ): boolean { + const value = this.preferences.getSync(key, fallback); + if (typeof value === 'boolean') { + return value; + } + if (typeof value === 'string') { + const normalized = value.trim().toLowerCase(); + if (normalized === 'true') { + return true; + } + if (normalized === 'false' || normalized === '') { + return false; + } } + if (typeof value === 'number') { + return value !== 0; + } + return fallback; + } + + private scheduleCleanUp(): void { + void this.cleanUp().catch(error => { + console.error('Failed to clean up updates:', error); + }); + } + + public getRootDir(): string { + return this.rootDir; } public setKv(key: string, value: string): void { @@ -53,40 +138,62 @@ export class UpdateContext { } public getKv(key: string): string { - return this.preferences.getSync(key, '') as string; + return this.getStringPreference(key); } public isFirstTime(): boolean { - return this.preferences.getSync('firstTime', false) as boolean; + return this.getBooleanPreference('firstTime', false); } public rolledBackVersion(): string { - return this.preferences.getSync('rolledBackVersion', '') as string; + return this.getStringPreference('rolledBackVersion'); + } + + public consumeLaunchMarks(): LaunchMarks { + const marks = { + isFirstTime: this.getBooleanPreference('firstTimeMarked', false), + rolledBackVersion: this.rolledBackVersion(), + }; + + if (marks.isFirstTime) { + this.preferences.deleteSync('firstTimeMarked'); + } + if (marks.rolledBackVersion) { + this.preferences.deleteSync('rolledBackVersion'); + } + if (marks.isFirstTime || marks.rolledBackVersion) { + this.preferences.flush(); + this.scheduleCleanUp(); + } + + return marks; } public markSuccess(): void { this.preferences.putSync('firstTimeOk', true); - const lastVersion = this.preferences.getSync('lastVersion', '') as string; - const curVersion = this.preferences.getSync('currentVersion', '') as string; + const lastVersion = this.getStringPreference('lastVersion'); + const currentVersion = this.getStringPreference('currentVersion'); - if (lastVersion && lastVersion !== curVersion) { + if (lastVersion && lastVersion !== currentVersion) { this.preferences.deleteSync('lastVersion'); this.preferences.deleteSync(`hash_${lastVersion}`); } + this.preferences.flush(); - this.cleanUp(); + this.scheduleCleanUp(); } public clearFirstTime(): void { this.preferences.putSync('firstTime', false); + this.preferences.deleteSync('firstTimeMarked'); this.preferences.flush(); - this.cleanUp(); + this.scheduleCleanUp(); } public clearRollbackMark(): void { - this.preferences.putSync('rolledBackVersion', null); + this.preferences.deleteSync('rolledBackVersion'); this.preferences.flush(); - this.cleanUp(); + this.scheduleCleanUp(); } public async downloadFullUpdate(url: string, hash: string): Promise { @@ -99,9 +206,9 @@ export class UpdateContext { params.unzipDirectory = `${this.rootDir}/${hash}`; const downloadTask = new DownloadTask(this.context); await downloadTask.execute(params); - } catch (e) { - console.error('Failed to download full update:', e); - throw e; + } catch (error) { + console.error('Failed to download full update:', error); + throw error; } } @@ -114,7 +221,7 @@ export class UpdateContext { params.type = DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD; params.url = url; params.hash = hash; - params.targetFile = this.rootDir + '/' + fileName; + params.targetFile = `${this.rootDir}/${fileName}`; const downloadTask = new DownloadTask(this.context); await downloadTask.execute(params); @@ -151,10 +258,10 @@ export class UpdateContext { params.unzipDirectory = `${this.rootDir}/${hash}`; const downloadTask = new DownloadTask(this.context); - return await downloadTask.execute(params); - } catch (e) { - console.error('Failed to download package patch:', e); - throw e; + await downloadTask.execute(params); + } catch (error) { + console.error('Failed to download package patch:', error); + throw error; } } @@ -165,33 +272,47 @@ export class UpdateContext { throw Error(`Bundle version ${hash} not found.`); } - const lastVersion = this.getKv('currentVersion'); - this.setKv('currentVersion', hash); + const lastVersion = this.getCurrentVersion(); + this.preferences.putSync('currentVersion', hash); if (lastVersion && lastVersion !== hash) { - this.setKv('lastVersion', lastVersion); + this.preferences.putSync('lastVersion', lastVersion); + } else { + this.preferences.deleteSync('lastVersion'); } - - this.setKv('firstTime', 'true'); - this.setKv('firstTimeOk', 'false'); - this.setKv('rolledBackVersion', ''); - } catch (e) { - console.error('Failed to switch version:', e); - throw e; + this.preferences.putSync('firstTime', true); + this.preferences.putSync('firstTimeOk', false); + this.preferences.deleteSync('firstTimeMarked'); + this.preferences.deleteSync('rolledBackVersion'); + this.preferences.flush(); + UpdateContext.ignoreRollbackInCurrentProcess = false; + } catch (error) { + console.error('Failed to switch version:', error); + throw error; } } - public getBundleUrl() { + public getBundleUrl(): string { UpdateContext.isUsingBundleUrl = true; - const currentVersion = this.getCurrentVersion(); - if (!currentVersion) { + let version = this.getCurrentVersion(); + if (!version) { return ''; } - if (!this.isFirstTime()) { - if (!this.preferences.getSync('firstTimeOk', true)) { - return this.rollBack(); - } + + const isFirstTime = this.isFirstTime(); + const isFirstTimeOk = this.getBooleanPreference('firstTimeOk', true); + if ( + !UpdateContext.ignoreRollbackInCurrentProcess && + !isFirstTime && + !isFirstTimeOk + ) { + version = this.rollBack(); + } else if (isFirstTime && !UpdateContext.ignoreRollbackInCurrentProcess) { + UpdateContext.ignoreRollbackInCurrentProcess = true; + this.preferences.putSync('firstTime', false); + this.preferences.putSync('firstTimeMarked', true); + this.preferences.flush(); } - let version = currentVersion; + while (version) { const bundleFile = `${this.rootDir}/${version}/bundle.harmony.js`; try { @@ -201,38 +322,52 @@ export class UpdateContext { continue; } return bundleFile; - } catch (e) { - console.error('Failed to access bundle file:', e); + } catch (error) { + console.error('Failed to access bundle file:', error); version = this.rollBack(); } } + return ''; } - getPackageVersion(): string { - let bundleFlags = - bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION; + public getPackageVersion(): string { let packageVersion = ''; try { - const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleFlags); + const bundleInfo = bundleManager.getBundleInfoForSelfSync( + bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION, + ); packageVersion = bundleInfo?.versionName || 'Unknown'; } catch (error) { - console.error('获取包信息失败:', error); + console.error('Failed to get bundle info:', error); } return packageVersion; } + public getBuildTime(): string { + try { + const content = + this.context.resourceManager.getRawFileContentSync('meta.json'); + const metaData = JSON.parse( + new util.TextDecoder().decodeToString(content), + ) as { + pushy_build_time?: string | number; + }; + if (metaData.pushy_build_time !== undefined) { + return String(metaData.pushy_build_time); + } + } catch {} + return ''; + } + public getCurrentVersion(): string { - const currentVersion = this.getKv('currentVersion'); - return currentVersion; + return this.getStringPreference('currentVersion'); } private rollBack(): string { - const lastVersion = this.preferences.getSync('lastVersion', '') as string; - const currentVersion = this.preferences.getSync( - 'currentVersion', - '', - ) as string; + const lastVersion = this.getStringPreference('lastVersion'); + const currentVersion = this.getCurrentVersion(); + if (!lastVersion) { this.preferences.deleteSync('currentVersion'); } else { @@ -241,16 +376,17 @@ export class UpdateContext { } this.preferences.putSync('firstTimeOk', true); this.preferences.putSync('firstTime', false); + this.preferences.deleteSync('firstTimeMarked'); this.preferences.putSync('rolledBackVersion', currentVersion); this.preferences.flush(); return lastVersion; } - public async cleanUp() { + public async cleanUp(): Promise { const params = new DownloadTaskParams(); params.type = DownloadTaskParams.TASK_TYPE_CLEANUP; - params.hash = this.preferences.getSync('currentVersion', '') as string; - params.originHash = this.preferences.getSync('lastVersion', '') as string; + params.hash = this.getCurrentVersion(); + params.originHash = this.getStringPreference('lastVersion'); params.unzipDirectory = this.rootDir; const downloadTask = new DownloadTask(this.context); await downloadTask.execute(params); From c1f94a71d093d9e3df8b75b01365b3a47f99510c Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 16 Mar 2026 21:27:02 +0800 Subject: [PATCH 5/5] cleanup --- .../harmony/entry/src/main/cpp/PackageProvider.cpp | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Example/harmony_use_pushy/harmony/entry/src/main/cpp/PackageProvider.cpp b/Example/harmony_use_pushy/harmony/entry/src/main/cpp/PackageProvider.cpp index 0018d51e..e184e15e 100644 --- a/Example/harmony_use_pushy/harmony/entry/src/main/cpp/PackageProvider.cpp +++ b/Example/harmony_use_pushy/harmony/entry/src/main/cpp/PackageProvider.cpp @@ -1,20 +1,9 @@ #include "RNOH/PackageProvider.h" - -#if __has_include("PushyPackage.h") #include "PushyPackage.h" -#define HAS_PUSHY_PACKAGE 1 -#else -#define HAS_PUSHY_PACKAGE 0 -#endif - using namespace rnoh; std::vector> PackageProvider::getPackages(Package::Context ctx) { -#if HAS_PUSHY_PACKAGE return { std::make_shared(ctx) }; -#else - return {}; -#endif -} +} \ No newline at end of file