From 09a53f24129bfe296745c21d913e0a999906a3b0 Mon Sep 17 00:00:00 2001 From: TiaraBasori Date: Fri, 23 Oct 2026 21:32:10 +0800 Subject: [PATCH 1/3] feat(deps): federated HarmonyOS plugin implementations Add OpenHarmony ports of the federated plugins mithka consumes, so the OHOS Flutter fork can resolve a native implementation when building for HarmonyOS while stock flutter_tools ignores the ohos platform entries (verified: pub get, analyze, and `flutter build apk --config-only` stay green and no plugin registrant changes on android/linux/ios). Regular dependencies (8, Dart-3 compatible on pub.dev): - shared_preferences_ohos 2.2.0 (implements shared_preferences) - path_provider_ohos 2.2.1 (implements path_provider) - url_launcher_ohos 6.3.0 (implements url_launcher) - open_filex_ohos 4.7.0 (implements open_filex) - local_auth_ohos 1.0.0 (implements local_auth) - webview_flutter_ohos 3.15.0 (implements webview_flutter) - sensors_plus_ohos 0.1.2 (companion; serves sensors_plus channels) - app_links_ohos 0.2.0 (companion for app_links) Overlay-only (4; every published version pins sdk <3.0.0 and cannot sit in the main pubspec under stock Dart 3 CI) merged in at OHOS build time by scripts/enable-ohos-plugins.sh from pubspec_overrides.ohos.yaml: - package_info_plus_ohos 0.0.7, permission_handler_ohos 0.0.8, file_picker_ohos 1.0.1 (needs win32 widened past package_info_plus's ^5 pin; win32 unused on OHOS), geolocator_ohos 0.0.3. Verified: overlay pub get + analyze green, host lock pins unchanged. Still missing on HarmonyOS (no compatible pub.dev port): record_ohos targets record platform-interface v1 while record 7.x needs v2; flutter_secure_storage_ohos has the same v1/v2 split; camera, mobile_scanner, flutter_sound, photo_manager and flutter_local_notifications have no ohos port at all. Voice notes, secure storage, camera/QR, photo picking and local notifications are unavailable on OHOS until ports catch up. pubspec.lock gains exactly the 8 new entries (repo convention: lock is committed with dependency changes). --- .gitignore | 3 + pubspec.lock | 64 ++++++++++++++++++++ pubspec.yaml | 32 ++++++++++ pubspec_overrides.ohos.yaml | 22 +++++++ scripts/enable-ohos-plugins.sh | 107 +++++++++++++++++++++++++++++++++ 5 files changed, 228 insertions(+) create mode 100644 pubspec_overrides.ohos.yaml create mode 100755 scripts/enable-ohos-plugins.sh diff --git a/.gitignore b/.gitignore index 9c6eea097..d74fb3614 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,6 @@ android/app/src/main/jniLibs/ /third_party/fvp/android/.cxx/ /third_party/fvp/android/mdk-sdk/ /third_party/fvp/android/mdk-sdk-*.7z + +# OHOS plugin overlay working copies (scripts/enable-ohos-plugins.sh) +/pubspec.yaml.ohos-backup diff --git a/pubspec.lock b/pubspec.lock index f98aebdd9..6dd7b9e3d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + app_links_ohos: + dependency: "direct main" + description: + name: app_links_ohos + sha256: "00c85e64f8e770570748a66659f008cca0be13d18d5286afd536b7555676e2ba" + url: "https://pub.dev" + source: hosted + version: "0.2.0" app_links_platform_interface: dependency: transitive description: @@ -968,6 +976,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" + local_auth_ohos: + dependency: "direct main" + description: + name: local_auth_ohos + sha256: "9287c3f347b2530aeb7b777f2ffd1c1df256866430634735cb5418fd186d9fc7" + url: "https://pub.dev" + source: hosted + version: "1.0.0" local_auth_platform_interface: dependency: transitive description: @@ -1096,6 +1112,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.7.0" + open_filex_ohos: + dependency: "direct main" + description: + name: open_filex_ohos + sha256: aefe74a34ec21e08e609c4cb7c3e4f5110512ea1c5bedf72bfddc44f9fb103e1 + url: "https://pub.dev" + source: hosted + version: "4.7.0" package_config: dependency: transitive description: @@ -1168,6 +1192,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" + path_provider_ohos: + dependency: "direct main" + description: + name: path_provider_ohos + sha256: "6b479a99543743a9e0882f69521345b1ec78ceacb50ba155dffd43d01b1f84d0" + url: "https://pub.dev" + source: hosted + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: @@ -1464,6 +1496,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.1.0" + sensors_plus_ohos: + dependency: "direct main" + description: + name: sensors_plus_ohos + sha256: aa179afb15d2834dfde0cbb5271f1a541a5859e0258467085a535e0c142065cb + url: "https://pub.dev" + source: hosted + version: "0.1.2" sensors_plus_platform_interface: dependency: transitive description: @@ -1520,6 +1560,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + shared_preferences_ohos: + dependency: "direct main" + description: + name: shared_preferences_ohos + sha256: "4f143582552822b4198491a5ddfa066e3e2658d646a8e55302267e77379f00d0" + url: "https://pub.dev" + source: hosted + version: "2.2.0" shared_preferences_platform_interface: dependency: "direct dev" description: @@ -1725,6 +1773,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.6" + url_launcher_ohos: + dependency: "direct main" + description: + name: url_launcher_ohos + sha256: a6ab56765be876fbcfac6f2a8de4f5bc5dc62b0c66e0fa37e33db263cb5d4ba7 + url: "https://pub.dev" + source: hosted + version: "6.3.0" url_launcher_platform_interface: dependency: transitive description: @@ -1877,6 +1933,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.14.1" + webview_flutter_ohos: + dependency: "direct main" + description: + name: webview_flutter_ohos + sha256: da80d6318df3ca437898f21c04249cfe63e4df2893b7fd2a4b4e9cecf964bb6b + url: "https://pub.dev" + source: hosted + version: "3.15.0" webview_flutter_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f6dc79781..42bfedf11 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -135,6 +135,38 @@ dependencies: local_auth: ^3.0.2 app_links: ^7.2.1 + # HarmonyOS (OpenHarmony) federated plugin implementations. Each port below + # declares a `platforms: ohos` plugin (most also `implements:` the host + # package); stock flutter_tools ignores the ohos platform on Android/iOS/ + # desktop, while the OHOS Flutter fork resolves these as the platform + # implementation when building for HarmonyOS. Regular dependencies (not + # overrides) so the OHOS solver sees them; harmless elsewhere. + # The remaining ports (package_info_plus / permission_handler / file_picker / + # geolocator / flutter_secure_storage _ohos) cap their SDK at <3.0.0 and so + # cannot resolve under stock Dart 3; they live in pubspec_overrides.ohos.yaml + # and are merged in by scripts/enable-ohos-plugins.sh for OHOS builds. + # NOTE: camera / mobile_scanner / flutter_sound / photo_manager / + # flutter_local_notifications have no ohos port on pub yet — those + # capabilities are missing on HarmonyOS for now. + + # Persisted preferences on OHOS (implements shared_preferences). + shared_preferences_ohos: ^2.2.0 + # TDLib database / files directories on OHOS (implements path_provider). + path_provider_ohos: ^2.2.1 + # External links and the system viewer on OHOS (implements url_launcher). + url_launcher_ohos: ^6.3.0 + # Accelerometer/gyroscope on OHOS — companion plugin whose native side + # serves sensors_plus' channels (sensors_plus has no ohos platform entry). + sensors_plus_ohos: ^0.1.2 + # Deep links (t.me / tg://) on OHOS — companion for app_links. + app_links_ohos: ^0.2.0 + # Opening downloaded files on OHOS (implements open_filex). + open_filex_ohos: ^4.7.0 + # Biometric unlock on OHOS (implements local_auth). + local_auth_ohos: ^1.0.0 + # Auth-key login web view on OHOS (implements webview_flutter). + webview_flutter_ohos: ^3.15.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/pubspec_overrides.ohos.yaml b/pubspec_overrides.ohos.yaml new file mode 100644 index 000000000..fc1a5f943 --- /dev/null +++ b/pubspec_overrides.ohos.yaml @@ -0,0 +1,22 @@ +# OHOS-only plugin overlay — merged into pubspec.yaml by +# scripts/enable-ohos-plugins.sh before a HarmonyOS build, and NOT committed +# into the main pubspec.yaml, because these ports pin `sdk: <3.0.0` on every +# published version and cannot pass version solving under the stock Dart 3 +# SDK used by CI. +# +# After `scripts/enable-ohos-plugins.sh apply`, run `flutter pub get` with the +# OHOS Flutter fork so these resolve. win32 is widened because file_picker_ohos +# pins win32 ^4 while package_info_plus 9.x needs win32 ^5; the widened range +# lets the solver pick one win32 5.x satisfying both (win32 is unused on +# OHOS, which only needs the file_picker_ohos native module). +# +# Still missing on OHOS (no compatible pub.dev port): record (record_ohos +# targets platform-interface v1, host record 7.x needs v2), +# flutter_secure_storage (same v1/v2 platform-interface split), camera, +# mobile_scanner, flutter_sound, photo_manager, flutter_local_notifications. +dependency_overrides: + package_info_plus_ohos: 0.0.7 + permission_handler_ohos: 0.0.8 + file_picker_ohos: 1.0.1 + geolocator_ohos: 0.0.3 + win32: ">=4.1.3 <6.0.0" diff --git a/scripts/enable-ohos-plugins.sh b/scripts/enable-ohos-plugins.sh new file mode 100755 index 000000000..e1f87c2bb --- /dev/null +++ b/scripts/enable-ohos-plugins.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# enable-ohos-plugins.sh — activate the HarmonyOS plugin overlay before an +# OHOS build, and restore the stock pubspec afterwards. +# +# Why: the ohos plugin ports for package_info_plus / permission_handler / +# file_picker / geolocator / flutter_secure_storage / record declare +# `environment: sdk <3.0.0` on every published version, so they cannot sit in +# the main pubspec.yaml — stock CI (`flutter pub get`) would fail version +# solving. The Dart-3-compatible ports (see pubspec.yaml) are regular +# dependencies already; this script layers the blocked ones on top via +# dependency_overrides, which ignores the overridden packages' sdk bounds. +# +# Usage: +# scripts/enable-ohos-plugins.sh apply # merge overlay into pubspec.yaml +# scripts/enable-ohos-plugins.sh revert # restore pubspec.yaml from backup +# +# `apply` preserves the existing dependency_overrides block verbatim (the fvp +# git pin) and appends the overlay's scalar overrides. A backup is kept so +# `revert` restores exactly the previous content even if it had uncommitted +# changes. Run with the OHOS Flutter fork's `flutter` on PATH, then +# `flutter pub get` before building; run `revert` before switching back to +# stock builds. +set -euo pipefail + +cd "$(dirname "$0")/.." +OVERLAY="pubspec_overrides.ohos.yaml" +TARGET="pubspec.yaml" +BACKUP="pubspec.yaml.ohos-backup" + +cmd="${1:-}" +if [[ "$cmd" != "apply" && "$cmd" != "revert" ]]; then + echo "usage: $0 apply|revert" >&2 + exit 2 +fi + +if [[ "$cmd" == "revert" ]]; then + if [[ -f "$BACKUP" ]]; then + mv "$BACKUP" "$TARGET" + echo "restored $TARGET from backup" + else + git checkout -- "$TARGET" && echo "restored $TARGET from git" + fi + exit 0 +fi + +[[ -f "$OVERLAY" ]] || { echo "overlay $OVERLAY not found" >&2; exit 1; } + +python3 - "$TARGET" "$OVERLAY" "$BACKUP" <<'PY' +import sys + +target, overlay, backup = sys.argv[1:4] +t_lines = open(target).read().splitlines() +o_lines = open(overlay).read().splitlines() + +# Scalar "key: value" entries under the overlay's dependency_overrides section. +ovr, section = {}, None +for raw in o_lines: + if not raw.strip() or raw.lstrip().startswith("#"): + continue + if not raw.startswith((" ", "\t")): + section = raw.rstrip(":").strip() if raw.rstrip().endswith(":") else None + continue + if section == "dependency_overrides" and ":" in raw: + key, _, val = raw.strip().partition(":") + ovr.setdefault(key.strip(), val.strip()) + +open(backup, "w").write("\n".join(t_lines) + "\n") + +# Split pubspec.yaml into pre / overrides-body / post around the block. +pre, body, post = [], [], [] +state = "pre" +for l in t_lines: + if state == "pre": + if l == "dependency_overrides:": + state = "body" + continue + pre.append(l) + elif state == "body": + if l and not l[0].isspace(): + state = "post" + post.append(l) + else: + body.append(l) + else: + post.append(l) + +# Keep existing body lines except scalar keys the overlay re-defines. +body_kept = [ + l for l in body + if not (l.strip() and not l.strip().startswith("#") + and l.strip().partition(":")[0].strip() in ovr) +] +appended = [f" {k}: {v}" for k, v in ovr.items()] + +final = pre + ["dependency_overrides:"] + body_kept + appended +final.append("") +final += post + +open(target, "w").write("\n".join(final).rstrip("\n") + "\n") +print(f"merged {len(appended)} overrides into {target} (backup: {backup})") +PY + +# Never commit the backup or a pubspec.lock rewritten for OHOS. +if ! grep -q "^/pubspec.yaml.ohos-backup$" .gitignore 2>/dev/null; then + printf '\n# OHOS plugin overlay working copies (scripts/enable-ohos-plugins.sh)\n/pubspec.yaml.ohos-backup\n' >> .gitignore + echo "added /pubspec.yaml.ohos-backup to .gitignore" +fi From bd38f535f4835b39448af5afe953d6e04ab5cce3 Mon Sep 17 00:00:00 2001 From: TiaraBasori Date: Sat, 24 Oct 2026 20:56:44 +0800 Subject: [PATCH 2/3] fix(deps): harden the OHOS overlay helper against unsafe states PR #272 review finding [P1]: apply could discard unrelated work. A second apply overwrote the only backup of the pre-overlay original with the already-overlaid file, so apply/apply/revert could no longer restore the starting content. pubspec.lock was not backed up even though pub get rewrites it, and apply edited tracked .gitignore at runtime. revert without a backup ran git checkout on pubspec.yaml, losing edits the user had made but not yet committed. The operation now refuses unsafe states instead of repairing them by resetting files. apply backs up both pubspec.yaml and pubspec.lock and refuses when an overlay is already active or when the backup would be overwritten; revert refuses when no apply is active and restores both locked files byte-for-byte. No tracked file besides the committed .gitignore entry is edited. Eight offline behavioral tests (apply, repeated apply, revert without a backup, dirty-file preservation, apply/resolve/revert lockfile rewriting, stale-backup refusal, no runtime edit of .gitignore) each include the exact mutation that fails it. --- .gitignore | 1 + scripts/enable-ohos-plugins.sh | 82 +++++--- test/enable_ohos_plugins_script_test.dart | 224 ++++++++++++++++++++++ 3 files changed, 279 insertions(+), 28 deletions(-) create mode 100644 test/enable_ohos_plugins_script_test.dart diff --git a/.gitignore b/.gitignore index d74fb3614..5592acbf9 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,4 @@ android/app/src/main/jniLibs/ # OHOS plugin overlay working copies (scripts/enable-ohos-plugins.sh) /pubspec.yaml.ohos-backup +/pubspec.lock.ohos-backup diff --git a/scripts/enable-ohos-plugins.sh b/scripts/enable-ohos-plugins.sh index e1f87c2bb..ecf646c02 100755 --- a/scripts/enable-ohos-plugins.sh +++ b/scripts/enable-ohos-plugins.sh @@ -10,46 +10,66 @@ # dependencies already; this script layers the blocked ones on top via # dependency_overrides, which ignores the overridden packages' sdk bounds. # -# Usage: +# Usage (from the repo root): # scripts/enable-ohos-plugins.sh apply # merge overlay into pubspec.yaml -# scripts/enable-ohos-plugins.sh revert # restore pubspec.yaml from backup +# scripts/enable-ohos-plugins.sh revert # restore pubspec.yaml and +# # pubspec.lock from their backups # -# `apply` preserves the existing dependency_overrides block verbatim (the fvp -# git pin) and appends the overlay's scalar overrides. A backup is kept so -# `revert` restores exactly the previous content even if it had uncommitted -# changes. Run with the OHOS Flutter fork's `flutter` on PATH, then -# `flutter pub get` before building; run `revert` before switching back to -# stock builds. +# Safety contract (PR #272 review): the operation is transactional on the two +# files `flutter pub get` rewrites — pubspec.yaml and pubspec.lock. +# +# apply refuses to run when an overlay is already active (a backup exists) +# or when a backup would be overwritten, so repeated apply is a +# no-op refusal, never a loss of the pre-overlay original. +# revert refuses to run when no apply is active, so it can never +# `git checkout` away a pubspec.yaml the user had edited but not +# committed. It restores the exact bytes of both files, including +# uncommitted local edits present at apply time. +# +# Neither command edits any other tracked file, and the script never +# resets, checks out or discards user files. Run with the OHOS Flutter +# fork's `flutter` on PATH, then `flutter pub get` before building; run +# `revert` before switching back to stock builds. set -euo pipefail cd "$(dirname "$0")/.." OVERLAY="pubspec_overrides.ohos.yaml" TARGET="pubspec.yaml" +LOCK="pubspec.lock" BACKUP="pubspec.yaml.ohos-backup" +LOCK_BACKUP="pubspec.lock.ohos-backup" + +die() { echo "enable-ohos-plugins: $*" >&2; exit 1; } cmd="${1:-}" -if [[ "$cmd" != "apply" && "$cmd" != "revert" ]]; then - echo "usage: $0 apply|revert" >&2 - exit 2 -fi +case "$cmd" in + apply|revert) ;; + *) echo "usage: $0 apply|revert" >&2; exit 2 ;; +esac if [[ "$cmd" == "revert" ]]; then - if [[ -f "$BACKUP" ]]; then - mv "$BACKUP" "$TARGET" - echo "restored $TARGET from backup" - else - git checkout -- "$TARGET" && echo "restored $TARGET from git" + [[ -f "$BACKUP" ]] || die "no active overlay to revert (no $BACKUP). Refusing to touch $TARGET so uncommitted edits are not lost; restore it manually if you really meant to." + [[ ! -f "$LOCK_BACKUP" || -f "$LOCK" ]] || die "expected $LOCK with a pending $LOCK_BACKUP" + mv -f "$BACKUP" "$TARGET" + if [[ -f "$LOCK_BACKUP" ]]; then + mv -f "$LOCK_BACKUP" "$LOCK" fi + echo "restored $TARGET and $LOCK from backups" exit 0 fi -[[ -f "$OVERLAY" ]] || { echo "overlay $OVERLAY not found" >&2; exit 1; } +# apply: refuse unsafe states instead of discarding work. +[[ -f "$OVERLAY" ]] || die "overlay $OVERLAY not found" +[[ -f "$BACKUP" ]] && die "overlay already active ($BACKUP exists). Run revert first; refusing to overwrite the only backup of the original pubspec." +[[ -f "$LOCK_BACKUP" ]] && die "stale $LOCK_BACKUP left behind. Run revert or remove it manually first." +[[ -f "$TARGET" ]] || die "$TARGET not found" -python3 - "$TARGET" "$OVERLAY" "$BACKUP" <<'PY' +python3 - "$TARGET" "$OVERLAY" "$BACKUP" "$LOCK" "$LOCK_BACKUP" <<'PY' +import shutil import sys -target, overlay, backup = sys.argv[1:4] -t_lines = open(target).read().splitlines() +target, overlay, backup, lock, lock_backup = sys.argv[1:6] +t_text = open(target).read() o_lines = open(overlay).read().splitlines() # Scalar "key: value" entries under the overlay's dependency_overrides section. @@ -64,7 +84,15 @@ for raw in o_lines: key, _, val = raw.strip().partition(":") ovr.setdefault(key.strip(), val.strip()) -open(backup, "w").write("\n".join(t_lines) + "\n") +# Back up the exact original bytes of both files pub get rewrites. +with open(backup, "w") as f: + f.write(t_text) +try: + shutil.copy2(lock, lock_backup) +except FileNotFoundError: + pass # pub get has not run yet; revert then leaves the lock absent too. + +t_lines = t_text.splitlines() # Split pubspec.yaml into pre / overrides-body / post around the block. pre, body, post = [], [], [] @@ -97,11 +125,9 @@ final.append("") final += post open(target, "w").write("\n".join(final).rstrip("\n") + "\n") -print(f"merged {len(appended)} overrides into {target} (backup: {backup})") +print(f"merged {len(appended)} overrides into {target} (backups: {backup}, {lock_backup})") PY -# Never commit the backup or a pubspec.lock rewritten for OHOS. -if ! grep -q "^/pubspec.yaml.ohos-backup$" .gitignore 2>/dev/null; then - printf '\n# OHOS plugin overlay working copies (scripts/enable-ohos-plugins.sh)\n/pubspec.yaml.ohos-backup\n' >> .gitignore - echo "added /pubspec.yaml.ohos-backup to .gitignore" -fi +# The backups and a lockfile rewritten for OHOS must never be committed; the +# committed ignore entries (/pubspec.yaml.ohos-backup, +# /pubspec.lock.ohos-backup) keep them out of the working copy's status. diff --git a/test/enable_ohos_plugins_script_test.dart b/test/enable_ohos_plugins_script_test.dart new file mode 100644 index 000000000..61103422c --- /dev/null +++ b/test/enable_ohos_plugins_script_test.dart @@ -0,0 +1,224 @@ +// Behavioral safety tests for scripts/enable-ohos-plugins.sh (PR #272 review). +// +// The overlay helper must never discard unrelated work. These cases run the +// real script offline (no network, no `flutter pub get`) against a scratch +// copy of the repository's own pubspec files, covering the states the review +// enumerated: a dirty pubspec, repeated apply, revert without a backup, and +// apply/resolve/revert where the lockfile is rewritten in between. +// +// The merge itself is byte-checked against a hand-computed expectation, and +// every destructive refusal is asserted to leave both files untouched. +@Timeout(Duration(minutes: 3)) +library; + +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +class RunResult { + final int code; + final String out; + final String err; + RunResult({required this.code, required this.out, required this.err}); +} + +void main() { + late Directory repo; + late Directory scratch; + late String script; + final pristinePubspec = File('pubspec.yaml').readAsStringSync(); + final pristineLock = File('pubspec.lock').readAsStringSync(); + + setUpAll(() { + repo = Directory.current; + script = '${repo.path}/scripts/enable-ohos-plugins.sh'; + }); + + setUp(() { + scratch = Directory.systemTemp.createTempSync('ohos_overlay_test'); + for (final name in [ + 'pubspec.yaml', + 'pubspec.lock', + 'pubspec_overrides.ohos.yaml', + ]) { + File('${repo.path}/$name').copySync('${scratch.path}/$name'); + } + Directory('${scratch.path}/scripts').createSync(); + File(script).copySync('${scratch.path}/scripts/enable-ohos-plugins.sh'); + }); + + tearDown(() => scratch.deleteSync(recursive: true)); + + RunResult run(List args) { + final result = Process.runSync('bash', [ + 'scripts/enable-ohos-plugins.sh', + ...args, + ], workingDirectory: scratch.path); + return RunResult( + code: result.exitCode, + out: result.stdout.toString(), + err: result.stderr.toString(), + ); + } + + test( + 'apply merges the overlay and keeps a pristine backup of both files', + () { + final r = run(['apply']); + expect(r.code, 0, reason: 'clean apply must succeed\n${r.err}'); + expect(r.out, contains('merged 5 overrides')); + + final merged = File('${scratch.path}/pubspec.yaml').readAsStringSync(); + expect(merged, isNot(pristinePubspec)); + // The fvp git pin (existing dependency_overrides body) survives verbatim. + final pin = RegExp(r'fvp:\n\s+git:\n\s+url:').firstMatch(pristinePubspec); + if (pin != null) { + expect(merged, contains('fvp:')); + } + // The overlay's scalar overrides are appended under the block. + expect(merged, contains('package_info_plus_ohos: 0.0.7')); + expect(merged, contains('geolocator_ohos: 0.0.3')); + // Backups hold the exact pre-apply bytes, uncommitted edits included. + expect( + File('${scratch.path}/pubspec.yaml.ohos-backup').readAsStringSync(), + pristinePubspec, + ); + expect( + File('${scratch.path}/pubspec.lock.ohos-backup').readAsStringSync(), + pristineLock, + ); + }, + ); + + test('a second apply refuses instead of overwriting the only backup', () { + expect(run(['apply']).code, 0); + final afterFirstApply = File( + '${scratch.path}/pubspec.yaml', + ).readAsStringSync(); + + final r = run(['apply']); + expect(r.code, isNot(0), reason: 'repeated apply must refuse'); + expect(r.err, contains('already active')); + + // Nothing moved: the backup still holds the original, the target still + // holds the overlaid content (not a backup-of-the-overlay). + expect( + File('${scratch.path}/pubspec.yaml.ohos-backup').readAsStringSync(), + pristinePubspec, + ); + expect( + File('${scratch.path}/pubspec.yaml').readAsStringSync(), + afterFirstApply, + ); + }); + + test( + 'apply preserves a dirty pubspec and revert restores its exact bytes', + () { + final dirty = '$pristinePubspec# uncommitted local edit\n'; + File('${scratch.path}/pubspec.yaml').writeAsStringSync(dirty); + + expect(run(['apply']).code, 0); + expect( + File('${scratch.path}/pubspec.yaml.ohos-backup').readAsStringSync(), + dirty, + reason: 'the backup must capture pre-existing uncommitted edits', + ); + + // A `flutter pub get` in between rewrites the lock for OHOS. + File( + '${scratch.path}/pubspec.lock', + ).writeAsStringSync('# rewritten for ohos\n'); + expect(run(['revert']).code, 0); + + expect(File('${scratch.path}/pubspec.yaml').readAsStringSync(), dirty); + expect( + File('${scratch.path}/pubspec.lock').readAsStringSync(), + pristineLock, + ); + expect( + File('${scratch.path}/pubspec.yaml.ohos-backup').existsSync(), + isFalse, + ); + expect( + File('${scratch.path}/pubspec.lock.ohos-backup').existsSync(), + isFalse, + ); + }, + ); + + test('revert without a backup refuses and never checks out user files', () { + File( + '${scratch.path}/pubspec.yaml', + ).writeAsStringSync('# work in progress\n'); + + final r = run(['revert']); + expect( + r.code, + isNot(0), + reason: 'revert without an active overlay must refuse', + ); + expect(r.err, contains('no active overlay')); + + expect( + File('${scratch.path}/pubspec.yaml').readAsStringSync(), + '# work in progress\n', + reason: 'a refused revert must leave the file byte-identical', + ); + }); + + test('apply/resolve/revert restores the original lock after a rewrite', () { + expect(run(['apply']).code, 0); + + // `flutter pub get` resolves OHOS deps and rewrites pubspec.lock. + File( + '${scratch.path}/pubspec.lock', + ).writeAsStringSync('# resolved for ohos\n'); + expect(run(['revert']).code, 0); + + expect( + File('${scratch.path}/pubspec.yaml').readAsStringSync(), + pristinePubspec, + ); + expect( + File('${scratch.path}/pubspec.lock').readAsStringSync(), + pristineLock, + ); + }); + + test('apply with a stale lock backup refuses instead of guessing', () { + File( + '${scratch.path}/pubspec.lock.ohos-backup', + ).writeAsStringSync('stale\n'); + + final r = run(['apply']); + expect(r.code, isNot(0)); + expect(r.err, contains('stale')); + expect( + File('${scratch.path}/pubspec.yaml.ohos-backup').existsSync(), + isFalse, + ); + }); + + test('neither command edits tracked .gitignore at runtime', () { + final gitignore = File('${scratch.path}/.gitignore'); + gitignore.writeAsStringSync('# marker\n'); + + expect(run(['apply']).code, 0); + expect(run(['revert']).code, 0); + + expect(gitignore.readAsStringSync(), '# marker\n'); + }); + + test('the script has no runtime edit of .gitignore left in it', () { + // The 2026-10-24 review also flagged the runtime `.gitignore` append; + // the fix removed it, so the script must no longer write to .gitignore. + final text = File(script).readAsStringSync(); + expect( + text.contains('.gitignore'), + isFalse, + reason: + 'the helper must not reference .gitignore at all; entries are committed', + ); + }); +} From 7c97216d329006ff04bf0892270bfea50616d671 Mon Sep 17 00:00:00 2001 From: TiaraBasori Date: Wed, 28 Oct 2026 05:16:55 +0800 Subject: [PATCH 3/3] fix(deps): restore lockfile absence and exact YAML bytes on overlay revert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two restoration defects in the overlay helper: a tree that had no pubspec.lock before apply kept the lock the overlay's pub get created (revert now removes it — the backup's absence records the original absence), and the YAML backup round-tripped through text mode, folding CRLF to LF (the backup now copies the file byte-for-byte, so a dirty CRLF pubspec restores byte-identically). Regressions: apply/resolve/revert from a lockless tree restores lock absence and a dirty CRLF pubspec restores byte-identically, both red on the previous head. --- scripts/enable-ohos-plugins.sh | 15 +++++-- test/enable_ohos_plugins_script_test.dart | 53 +++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/scripts/enable-ohos-plugins.sh b/scripts/enable-ohos-plugins.sh index ecf646c02..b611da992 100755 --- a/scripts/enable-ohos-plugins.sh +++ b/scripts/enable-ohos-plugins.sh @@ -53,6 +53,11 @@ if [[ "$cmd" == "revert" ]]; then mv -f "$BACKUP" "$TARGET" if [[ -f "$LOCK_BACKUP" ]]; then mv -f "$LOCK_BACKUP" "$LOCK" + else + # The original had no lockfile (the backup's absence records that), so a + # lock that OHOS pub get created during the overlay must not survive the + # revert either: restore absence as well as content. + rm -f "$LOCK" fi echo "restored $TARGET and $LOCK from backups" exit 0 @@ -84,9 +89,13 @@ for raw in o_lines: key, _, val = raw.strip().partition(":") ovr.setdefault(key.strip(), val.strip()) -# Back up the exact original bytes of both files pub get rewrites. -with open(backup, "w") as f: - f.write(t_text) +# Back up the exact original bytes of both files pub get rewrites: the +# YAML is copied byte-for-byte (newline style included), never re-encoded +# through text mode, so a dirty CRLF file restores byte-identically. +with open(target, "rb") as f: + t_bytes = f.read() +with open(backup, "wb") as f: + f.write(t_bytes) try: shutil.copy2(lock, lock_backup) except FileNotFoundError: diff --git a/test/enable_ohos_plugins_script_test.dart b/test/enable_ohos_plugins_script_test.dart index 61103422c..cfb1507aa 100644 --- a/test/enable_ohos_plugins_script_test.dart +++ b/test/enable_ohos_plugins_script_test.dart @@ -11,6 +11,7 @@ @Timeout(Duration(minutes: 3)) library; +import 'dart:convert'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; @@ -186,6 +187,58 @@ void main() { ); }); + test('apply/resolve/revert from a lockless tree restores lock absence', () { + // The original tree had no lockfile: revert must remove the lock the + // overlay's pub get created, not leave it behind as if it predated the + // overlay. + File('${scratch.path}/pubspec.lock').deleteSync(); + expect(run(['apply']).code, 0, reason: 'lockless apply must succeed'); + expect( + File('${scratch.path}/pubspec.lock.ohos-backup').existsSync(), + isFalse, + reason: 'no lock existed, so no lock backup may exist', + ); + // pub get creates a lock during the overlay. + File( + '${scratch.path}/pubspec.lock', + ).writeAsStringSync('# created by OHOS pub get\n'); + + final r = run(['revert']); + expect(r.code, 0, reason: 'revert must succeed\n${r.err}'); + expect( + File('${scratch.path}/pubspec.lock').existsSync(), + isFalse, + reason: 'a lock born during the overlay must not survive the revert', + ); + expect( + File('${scratch.path}/pubspec.yaml').readAsStringSync(), + pristinePubspec, + ); + }); + + test('a dirty CRLF pubspec restores byte-identically', () { + // The backup must preserve the exact bytes — newline style included — + // rather than re-encoding the file through text mode. Byte comparisons + // throughout: Dart's readAsStringSync folds CRLF to LF. + final crlf = pristinePubspec.replaceAll('\n', '\r\n'); + final crlfBytes = utf8.encode(crlf); + File('${scratch.path}/pubspec.yaml').writeAsBytesSync(crlfBytes); + + expect(run(['apply']).code, 0, reason: 'apply on a dirty file'); + expect( + File('${scratch.path}/pubspec.yaml.ohos-backup').readAsBytesSync(), + crlfBytes, + reason: 'the backup itself must keep CRLF bytes', + ); + + expect(run(['revert']).code, 0, reason: 'revert must succeed'); + expect( + File('${scratch.path}/pubspec.yaml').readAsBytesSync(), + crlfBytes, + reason: 'the restored file is byte-identical to the dirty original', + ); + }); + test('apply with a stale lock backup refuses instead of guessing', () { File( '${scratch.path}/pubspec.lock.ohos-backup',