diff --git a/packages/package_info_plus/package_info_plus/README.md b/packages/package_info_plus/package_info_plus/README.md index 4aecbed5c8..12eca80995 100644 --- a/packages/package_info_plus/package_info_plus/README.md +++ b/packages/package_info_plus/package_info_plus/README.md @@ -122,6 +122,55 @@ If your project was created before Flutter 3.3 you need to migrate the project a In a web environment, the package uses the `version.json` file that it is generated in the build process. +#### `version.json` reflects the deployed version, not the running one + +`PackageInfo.fromPlatform()` resolves the version on web by fetching `version.json` from the server +at runtime (with a cache buster). That file reflects the **currently deployed** version, not the +version of the bundle actually executing in the browser. If a user is running a stale, cached bundle +while a newer version has been deployed, `fromPlatform()` reports the newly deployed version. +The fetch can also fail entirely (offline, CORS, hosting rewrites), leaving every field empty. + +If you need the version of the *running* bundle — e.g. to display it to the user or to gate +outdated clients — use the compile-time accessor below instead. + +#### Compile-time package information (`PackageInfoEnvironment`) + +Import the opt-in `package_info_plus_environment.dart` library and read +`PackageInfoEnvironment.packageInfo`: + +```dart +import 'package:package_info_plus/package_info_plus_environment.dart'; + +final info = await PackageInfoEnvironment.packageInfo; +``` + +Behaviour per platform: + +- **Web** — returns a `PackageInfo` built from the compile-time `PACKAGE_INFO_PLUS_*` defines. + These are embedded in the running bundle and cannot diverge from it. Provide them at build time: + + ```sh + flutter build web \ + --dart-define=PACKAGE_INFO_PLUS_VERSION=1.2.3 \ + --dart-define=PACKAGE_INFO_PLUS_BUILD_NUMBER=45 + ``` + + A **web build that omits `PACKAGE_INFO_PLUS_VERSION` fails to compile**, so a misleading version + can never ship silently. `PACKAGE_INFO_PLUS_BUILD_NUMBER`, `PACKAGE_INFO_PLUS_APP_NAME` and + `PACKAGE_INFO_PLUS_PACKAGE_NAME` are optional (empty when omitted). + +- **All other platforms** — delegates to `PackageInfo.fromPlatform()`, which reads the installed + binary and is already reliable. The defines are not required there. + +The accessor also recognizes tool-provided defines as fallbacks, so apps need no configuration at +all once their build front-end injects the version itself: `FLUTTER_BUILD_NAME` / +`FLUTTER_BUILD_NUMBER` (proposed in [flutter/flutter#187935](https://github.com/flutter/flutter/pull/187935)) +and `dart.package.version` ([dart-lang/sdk#38855](https://github.com/dart-lang/sdk/issues/38855)). +The explicit `PACKAGE_INFO_PLUS_*` defines take precedence over both. + +`PackageInfo.fromPlatform()` is unchanged; this accessor lives in a separate library that you import +only when you opt in, so existing builds are unaffected. + #### Accessing the `version.json` The package tries to locate the `version.json` using three methods: diff --git a/packages/package_info_plus/package_info_plus/lib/package_info_plus_environment.dart b/packages/package_info_plus/package_info_plus/lib/package_info_plus_environment.dart new file mode 100644 index 0000000000..f4a7dba0f1 --- /dev/null +++ b/packages/package_info_plus/package_info_plus/lib/package_info_plus_environment.dart @@ -0,0 +1,109 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:package_info_plus/package_info_plus.dart'; + +/// Compile-time package metadata, provided at build time with `--dart-define`: +/// +/// ```sh +/// flutter build web \ +/// --dart-define=PACKAGE_INFO_PLUS_VERSION=1.2.3 \ +/// --dart-define=PACKAGE_INFO_PLUS_BUILD_NUMBER=45 +/// ``` +const _version = String.fromEnvironment('PACKAGE_INFO_PLUS_VERSION'); +const _buildNumber = String.fromEnvironment('PACKAGE_INFO_PLUS_BUILD_NUMBER'); +const _appName = String.fromEnvironment('PACKAGE_INFO_PLUS_APP_NAME'); +const _packageName = String.fromEnvironment('PACKAGE_INFO_PLUS_PACKAGE_NAME'); + +/// Tool-provided fallbacks, recognized so that apps need no configuration at +/// all once their build front-end injects the version itself: +/// +/// * `FLUTTER_BUILD_NAME` / `FLUTTER_BUILD_NUMBER` — proposed in +/// flutter/flutter#187935 (injected by flutter_tools from the pubspec +/// `version`, like `FLUTTER_APP_FLAVOR` already is). +/// * `dart.package.version` — companion proposal for the `dart` CLI +/// (dart-lang/sdk#38855); carries the verbatim pubspec `version`, +/// including any `+buildNumber` suffix. +/// +/// The explicit `PACKAGE_INFO_PLUS_*` defines take precedence over both. +const _flutterBuildName = String.fromEnvironment('FLUTTER_BUILD_NAME'); +const _flutterBuildNumber = String.fromEnvironment('FLUTTER_BUILD_NUMBER'); +const _dartPackageVersion = String.fromEnvironment('dart.package.version'); + +/// Holds the compile-time package info and enforces, at compile time, that a +/// version was provided on web builds. +/// +/// The `assert` runs during constant evaluation: importing this library into a +/// **web** build that does not pass `--dart-define=PACKAGE_INFO_PLUS_VERSION` +/// is a compile error. Native builds are unaffected (`kIsWeb` is `false` +/// there), because they read the real version from the installed binary via +/// [PackageInfo.fromPlatform]. +class _CompileTimePackageInfo { + const _CompileTimePackageInfo() + : assert( + !kIsWeb || + _version != '' || + _flutterBuildName != '' || + _dartPackageVersion != '', + 'PACKAGE_INFO_PLUS_VERSION must be provided via --dart-define on web ' + 'builds. On the web there is no reliable runtime source for the ' + 'version of the *running* bundle (version.json is fetched from the ' + 'server and reflects the deployed version, not the executing one). ' + 'Pass --dart-define=PACKAGE_INFO_PLUS_VERSION= ' + '(and optionally PACKAGE_INFO_PLUS_BUILD_NUMBER / _APP_NAME / ' + '_PACKAGE_NAME) to your web build.', + ); + + String get version { + if (_version != '') return _version; + if (_flutterBuildName != '') return _flutterBuildName; + return _dartPackageVersion.split('+').first; + } + + String get buildNumber { + if (_buildNumber != '') return _buildNumber; + if (_flutterBuildNumber != '') return _flutterBuildNumber; + if (_dartPackageVersion.contains('+')) { + return _dartPackageVersion.split('+').last; + } + return ''; + } + + String get appName => _appName; + String get packageName => _packageName; +} + +/// Opt-in accessor for the **running** application's [PackageInfo]. +/// +/// Import this library explicitly (it is intentionally not exported by +/// `package_info_plus.dart`) when you need a version you can trust on the web — +/// for example to display it to the user or to gate outdated clients. +/// +/// Behaviour per platform: +/// +/// * **Web** — returns a [PackageInfo] built from the compile-time +/// `PACKAGE_INFO_PLUS_*` defines. These are embedded in the running bundle, +/// so they cannot diverge from it the way the server-fetched `version.json` +/// used by [PackageInfo.fromPlatform] can. A web build that omits +/// `PACKAGE_INFO_PLUS_VERSION` **fails to compile** — a misleading version +/// can never ship silently. +/// * **All other platforms** — delegates to [PackageInfo.fromPlatform], which +/// reads the installed binary's metadata and is already reliable. The defines +/// are not required there. +abstract final class PackageInfoEnvironment { + /// The running application's [PackageInfo]. See [PackageInfoEnvironment]. + static Future get packageInfo async { + if (kIsWeb) { + const env = _CompileTimePackageInfo(); + return PackageInfo( + appName: env.appName, + packageName: env.packageName, + version: env.version, + buildNumber: env.buildNumber, + ); + } + return PackageInfo.fromPlatform(); + } +} diff --git a/packages/package_info_plus/package_info_plus/test/package_info_environment_test.dart b/packages/package_info_plus/package_info_plus/test/package_info_environment_test.dart new file mode 100644 index 0000000000..73b795ddaa --- /dev/null +++ b/packages/package_info_plus/package_info_plus/test/package_info_environment_test.dart @@ -0,0 +1,72 @@ +// Copyright 2019 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:package_info_plus/package_info_plus_environment.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const channel = MethodChannel('dev.fluttercommunity.plus/package_info'); + final log = []; + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + log.add(methodCall); + return { + 'appName': 'package_info_example', + 'buildNumber': '1', + 'packageName': 'io.flutter.plugins.packageinfoexample', + 'version': '1.0', + }; + }); + + tearDown(log.clear); + + // Off the web (these tests run on the Dart VM), the accessor delegates to + // PackageInfo.fromPlatform(). The web path is enforced at compile time and is + // covered by a compile test rather than a runtime test — a web build that + // omits PACKAGE_INFO_PLUS_VERSION does not compile. + test( + 'packageInfo delegates to fromPlatform on non-web platforms', + () async { + final info = await PackageInfoEnvironment.packageInfo; + expect(info.version, '1.0'); + expect(info.appName, 'package_info_example'); + expect(info.packageName, 'io.flutter.plugins.packageinfoexample'); + expect(info.buildNumber, '1'); + expect(log, [isMethodCall('getAll', arguments: null)]); + }, + onPlatform: {'browser': const Skip('Web path is compile-time enforced')}, + ); + + // Exercises the tool-provided fallback chain on the web path. Run with: + // + // flutter test test/package_info_environment_test.dart --platform chrome \ + // --dart-define=FLUTTER_BUILD_NAME=9.9.9 --dart-define=FLUTTER_BUILD_NUMBER=7 + // + // (No PACKAGE_INFO_PLUS_VERSION: the explicit define would take precedence.) + // Note: once flutter/flutter#187935 lands, FLUTTER_BUILD_NAME becomes + // tool-reserved and is injected automatically instead of being passed by hand. + test( + 'web path falls back to the tool-provided FLUTTER_BUILD_NAME defines', + () async { + if (!kIsWeb) return; + final info = await PackageInfoEnvironment.packageInfo; + expect(info.version, const String.fromEnvironment('FLUTTER_BUILD_NAME')); + expect( + info.buildNumber, + const String.fromEnvironment('FLUTTER_BUILD_NUMBER'), + ); + }, + skip: + const bool.hasEnvironment('PACKAGE_INFO_PLUS_VERSION') || + !const bool.hasEnvironment('FLUTTER_BUILD_NAME') + ? 'Requires --dart-define=FLUTTER_BUILD_NAME and no ' + 'PACKAGE_INFO_PLUS_VERSION (see comment above)' + : false, + ); +}