From 13e02bd7ee90abf2fb3ca3b208856698d5f32d11 Mon Sep 17 00:00:00 2001 From: Ortes Date: Fri, 12 Jun 2026 05:30:22 -0500 Subject: [PATCH 1/2] feat(package_info_plus): add opt-in compile-time package info accessor Adds an opt-in library, package_info_plus_environment.dart, exposing PackageInfoEnvironment.packageInfo. It returns the *running* app's PackageInfo: on web from compile-time PACKAGE_INFO_PLUS_* dart-defines, on every other platform by delegating to PackageInfo.fromPlatform(). On web there is no reliable runtime source for the running version: version.json is fetched from the server, so it reflects the deployed version rather than the executing (possibly cached) bundle, and the fetch can fail entirely (offline, CORS, hosting rewrites). The compile-time constants are embedded in the bundle and cannot diverge from it. A web build that omits PACKAGE_INFO_PLUS_VERSION fails to compile, so a misleading version can never ship silently. Native builds are unaffected. PackageInfo.fromPlatform() is unchanged and the new library is not exported by the package barrel, so this is purely additive and opt-in. Related: #2675, #225, #456, flutter/flutter#149031. --- .../package_info_plus/README.md | 43 ++++++++++ .../lib/package_info_plus_environment.dart | 78 +++++++++++++++++++ .../test/package_info_environment_test.dart | 44 +++++++++++ 3 files changed, 165 insertions(+) create mode 100644 packages/package_info_plus/package_info_plus/lib/package_info_plus_environment.dart create mode 100644 packages/package_info_plus/package_info_plus/test/package_info_environment_test.dart diff --git a/packages/package_info_plus/package_info_plus/README.md b/packages/package_info_plus/package_info_plus/README.md index 4aecbed5c8..4e2e979434 100644 --- a/packages/package_info_plus/package_info_plus/README.md +++ b/packages/package_info_plus/package_info_plus/README.md @@ -122,6 +122,49 @@ 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. + +`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..512d44750f --- /dev/null +++ b/packages/package_info_plus/package_info_plus/lib/package_info_plus_environment.dart @@ -0,0 +1,78 @@ +// 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'); + +/// 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 != '', + '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 => _version; + String get buildNumber => _buildNumber; + 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..72a8470f0c --- /dev/null +++ b/packages/package_info_plus/package_info_plus/test/package_info_environment_test.dart @@ -0,0 +1,44 @@ +// 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/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')}, + ); +} From fbb9e09abbf6db1da62066cdc8f318a6e9fc4b21 Mon Sep 17 00:00:00 2001 From: Ortes Date: Fri, 12 Jun 2026 12:22:39 -0500 Subject: [PATCH 2/2] feat(package_info_plus): recognize tool-provided version defines as fallbacks PackageInfoEnvironment now resolves the web version from, in order: PACKAGE_INFO_PLUS_VERSION (explicit), FLUTTER_BUILD_NAME (flutter/flutter#187935, injected by flutter_tools like FLUTTER_APP_FLAVOR), and dart.package.version (dart-lang/sdk#38855). Once either upstream proposal lands, apps need no configuration at all. --- .../package_info_plus/README.md | 6 +++ .../lib/package_info_plus_environment.dart | 37 +++++++++++++++++-- .../test/package_info_environment_test.dart | 28 ++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/packages/package_info_plus/package_info_plus/README.md b/packages/package_info_plus/package_info_plus/README.md index 4e2e979434..12eca80995 100644 --- a/packages/package_info_plus/package_info_plus/README.md +++ b/packages/package_info_plus/package_info_plus/README.md @@ -162,6 +162,12 @@ Behaviour per platform: - **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. 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 index 512d44750f..f4a7dba0f1 100644 --- 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 @@ -17,6 +17,21 @@ 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. /// @@ -28,7 +43,10 @@ const _packageName = String.fromEnvironment('PACKAGE_INFO_PLUS_PACKAGE_NAME'); class _CompileTimePackageInfo { const _CompileTimePackageInfo() : assert( - !kIsWeb || _version != '', + !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 ' @@ -38,8 +56,21 @@ class _CompileTimePackageInfo { '_PACKAGE_NAME) to your web build.', ); - String get version => _version; - String get buildNumber => _buildNumber; + 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; } 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 index 72a8470f0c..73b795ddaa 100644 --- 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 @@ -2,6 +2,7 @@ // 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'; @@ -41,4 +42,31 @@ void main() { }, 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, + ); }