[tool] Proposal to support dart define config from a json file (#108098)

diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index 6f7a2ab..7fcfe44 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -37,6 +37,7 @@
     List<String>? dartExperiments,
     required this.treeShakeIcons,
     this.performanceMeasurementFile,
+    this.dartDefineConfigJsonMap,
     this.packagesPath = '.dart_tool/package_config.json', // TODO(zanderso): make this required and remove the default.
     this.nullSafetyMode = NullSafetyMode.sound,
     this.codeSizeDirectory,
@@ -130,6 +131,17 @@
   /// rerun tasks.
   final String? performanceMeasurementFile;
 
+  /// Configure a constant pool file.
+  /// Additional constant values to be made available in the Dart program.
+  ///
+  /// These values can be used with the const `fromEnvironment` constructors of
+  ///  [String] the key and field are json values
+  /// json value
+  ///
+  /// An additional field `dartDefineConfigJsonMap` is provided to represent the native JSON value of the configuration file
+  ///
+  final Map<String, Object>? dartDefineConfigJsonMap;
+
   /// If provided, an output directory where one or more v8-style heap snapshots
   /// will be written for code size profiling.
   final String? codeSizeDirectory;
@@ -247,12 +259,17 @@
     };
   }
 
+
   /// Convert to a structured string encoded structure appropriate for usage as
   /// environment variables or to embed in other scripts.
   ///
   /// Fields that are `null` are excluded from this configuration.
   Map<String, String> toEnvironmentConfig() {
-    return <String, String>{
+    final Map<String, String> map = <String, String>{};
+    dartDefineConfigJsonMap?.forEach((String key, Object value) {
+      map[key] = '$value';
+    });
+    final Map<String, String> environmentMap = <String, String>{
       if (dartDefines.isNotEmpty)
         'DART_DEFINES': encodeDartDefines(dartDefines),
       if (dartObfuscation != null)
@@ -276,13 +293,23 @@
       if (codeSizeDirectory != null)
         'CODE_SIZE_DIRECTORY': codeSizeDirectory!,
     };
+    map.forEach((String key, String value) {
+      if (environmentMap.containsKey(key)) {
+        globals.printWarning(
+            'The key: [$key] already exists, you cannot use environment variables that have been used by the system!');
+      } else {
+        // System priority is greater than user priority
+        environmentMap[key] = value;
+      }
+    });
+    return environmentMap;
   }
 
   /// Convert this config to a series of project level arguments to be passed
   /// on the command line to gradle.
   List<String> toGradleConfig() {
     // PACKAGE_CONFIG not currently supported.
-    return <String>[
+    final List<String> result = <String>[
       if (dartDefines.isNotEmpty)
         '-Pdart-defines=${encodeDartDefines(dartDefines)}',
       if (dartObfuscation != null)
@@ -306,6 +333,20 @@
       for (String projectArg in androidProjectArgs)
         '-P$projectArg',
     ];
+    if(dartDefineConfigJsonMap != null) {
+      final List<String> items = <String>[];
+      for (final String gradleConf in result) {
+        final String key = gradleConf.split('=')[0].substring(2);
+        if (dartDefineConfigJsonMap!.containsKey(key)) {
+          globals.printWarning(
+              'The key: [$key] already exists, you cannot use gradle variables that have been used by the system!');
+        } else {
+          items.add('-P$key=${dartDefineConfigJsonMap?[key]}');
+        }
+      }
+      result.addAll(items);
+    }
+    return result;
   }
 }
 
diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart
index 081c381..41c3054 100644
--- a/packages/flutter_tools/lib/src/commands/assemble.dart
+++ b/packages/flutter_tools/lib/src/commands/assemble.dart
@@ -263,9 +263,29 @@
     if (argumentResults.wasParsed(FlutterOptions.kExtraGenSnapshotOptions)) {
       results[kExtraGenSnapshotOptions] = (argumentResults[FlutterOptions.kExtraGenSnapshotOptions] as List<String>).join(',');
     }
+
+    List<String> dartDefines = <String>[];
     if (argumentResults.wasParsed(FlutterOptions.kDartDefinesOption)) {
-      results[kDartDefines] = (argumentResults[FlutterOptions.kDartDefinesOption] as List<String>).join(',');
+      dartDefines = argumentResults[FlutterOptions.kDartDefinesOption] as List<String>;
     }
+    if (argumentResults.wasParsed(FlutterOptions.kDartDefineFromFileOption)) {
+      final String? configJsonPath = stringArg(FlutterOptions.kDartDefineFromFileOption);
+      if (configJsonPath != null && globals.fs.isFileSync(configJsonPath)) {
+        final String configJsonRaw = globals.fs.file(configJsonPath).readAsStringSync();
+        try {
+          (json.decode(configJsonRaw) as Map<String, dynamic>).forEach((String key, dynamic value) {
+            dartDefines.add('$key=$value');
+          });
+        } on FormatException catch (err) {
+          throwToolExit('Json config define file "--${FlutterOptions.kDartDefineFromFileOption}=$configJsonPath" format err, '
+              'please fix first! format err:\n$err');
+        }
+      }
+    }
+    if(dartDefines.isNotEmpty){
+      results[kDartDefines] = dartDefines.join(',');
+    }
+
     results[kDeferredComponents] = 'false';
     if (FlutterProject.current().manifest.deferredComponents != null && isDeferredComponentsTargets() && !isDebug()) {
       results[kDeferredComponents] = 'true';
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index b6c628b..a150911 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -19,6 +19,7 @@
 import '../build_system/build_system.dart';
 import '../bundle.dart' as bundle;
 import '../cache.dart';
+import '../convert.dart';
 import '../dart/generate_synthetic_packages.dart';
 import '../dart/language_version.dart';
 import '../dart/package_map.dart';
@@ -104,6 +105,7 @@
   static const String kSplitDebugInfoOption = 'split-debug-info';
   static const String kDartObfuscationOption = 'obfuscate';
   static const String kDartDefinesOption = 'dart-define';
+  static const String kDartDefineFromFileOption = 'dart-define-from-file';
   static const String kBundleSkSLPathOption = 'bundle-sksl-path';
   static const String kPerformanceMeasurementFile = 'performance-measurement-file';
   static const String kNullSafety = 'sound-null-safety';
@@ -591,6 +593,17 @@
       valueHelp: 'foo=bar',
       splitCommas: false,
     );
+    useDartDefineConfigJsonFileOption();
+  }
+
+  void useDartDefineConfigJsonFileOption() {
+    argParser.addOption(
+      FlutterOptions.kDartDefineFromFileOption,
+      help: 'The path of a json format file where flutter define a global constant pool. '
+          'Json entry will be available as constants from the String.fromEnvironment, bool.fromEnvironment, '
+          'int.fromEnvironment, and double.fromEnvironment constructors; the key and field are json values.',
+      valueHelp: 'use-define-config.json'
+    );
   }
 
   void usesWebRendererOption() {
@@ -1122,6 +1135,27 @@
       dartDefines = updateDartDefines(dartDefines, stringArgDeprecated('web-renderer')!);
     }
 
+    Map<String, Object>? defineConfigJsonMap;
+    if (argParser.options.containsKey(FlutterOptions.kDartDefineFromFileOption)) {
+      final String? configJsonPath = stringArg(FlutterOptions.kDartDefineFromFileOption);
+      if (configJsonPath != null && globals.fs.isFileSync(configJsonPath)) {
+        final String configJsonRaw = globals.fs.file(configJsonPath).readAsStringSync();
+        try {
+          defineConfigJsonMap = <String, Object>{};
+          // Fix json convert Object value :type '_InternalLinkedHashMap<String, dynamic>' is not a subtype of type 'Map<String, Object>' in type cast
+          (json.decode(configJsonRaw) as Map<String, dynamic>).forEach((String key, dynamic value) {
+            defineConfigJsonMap?[key]=value as Object;
+          });
+          defineConfigJsonMap.forEach((String key, Object value) {
+            dartDefines.add('$key=$value');
+          });
+        } on FormatException catch (err) {
+          throwToolExit('Json config define file "--${FlutterOptions.kDartDefineFromFileOption}=$configJsonPath" format err, '
+              'please fix first! format err:\n$err');
+        }
+      }
+    }
+
     return BuildInfo(buildMode,
       argParser.options.containsKey('flavor')
         ? stringArgDeprecated('flavor')
@@ -1146,6 +1180,7 @@
       bundleSkSLPath: bundleSkSLPath,
       dartExperiments: experiments,
       performanceMeasurementFile: performanceMeasurementFile,
+      dartDefineConfigJsonMap: defineConfigJsonMap,
       packagesPath: packagesPath ?? globals.fs.path.absolute('.dart_tool', 'package_config.json'),
       nullSafetyMode: nullSafetyMode,
       codeSizeDirectory: codeSizeDirectory,
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart
index 072015a..0137d7f 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart
@@ -290,4 +290,33 @@
       ],
     });
   });
+
+  testUsingContext('test --dart-define-from-file option with err json format', () async {
+    await globals.fs.file('config.json').writeAsString(
+        '''
+          {
+            "kInt": 1,
+            "kDouble": 1.1,
+            "name": "err json format,
+            "title": "this is title from config json file"
+          }
+        '''
+    );
+    final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand(
+      buildSystem: TestBuildSystem.all(BuildResult(success: true)),
+    ));
+
+    expect(commandRunner.run(<String>['assemble',
+      '-o Output',
+      'debug_macos_bundle_flutter_assets',
+      '--dart-define=k=v',
+      '--dart-define-from-file=config.json']),
+        throwsToolExit(message: 'Json config define file "--dart-define-from-file=config.json" format err, please fix first! format err:'));
+  }, overrides: <Type, Generator>{
+    Cache: () => Cache.test(processManager: FakeProcessManager.any()),
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
+  });
+
+
 }
diff --git a/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart
index 772afcc..ce01bf1 100644
--- a/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart
+++ b/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart
@@ -4,6 +4,7 @@
 
 import 'package:args/command_runner.dart';
 import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/common.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/build_system/build_system.dart';
@@ -463,6 +464,64 @@
     FileSystem: fsFactory,
     ProcessManager: () => FakeProcessManager.any(),
   });
+
+  testUsingContext('test --dart-define-from-file option', () async {
+    globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
+    globals.fs.file('pubspec.yaml').createSync();
+    globals.fs.file('.packages').createSync();
+    await globals.fs.file('config.json').writeAsString(
+      '''
+        {
+          "kInt": 1,
+          "kDouble": 1.1,
+          "name": "denghaizhu",
+          "title": "this is title from config json file"
+        }
+      '''
+    );
+    final CommandRunner<void> runner = createTestCommandRunner(BuildBundleCommand());
+
+    await runner.run(<String>[
+      'bundle',
+      '--no-pub',
+      '--dart-define-from-file=config.json',
+    ]);
+  }, overrides: <Type, Generator>{
+    BuildSystem: () => TestBuildSystem.all(BuildResult(success: true), (Target target, Environment environment) {
+      expect(environment.defines[kDartDefines], 'a0ludD0x,a0RvdWJsZT0xLjE=,bmFtZT1kZW5naGFpemh1,dGl0bGU9dGhpcyBpcyB0aXRsZSBmcm9tIGNvbmZpZyBqc29uIGZpbGU=');
+    }),
+    FileSystem: fsFactory,
+    ProcessManager: () => FakeProcessManager.any(),
+  });
+
+
+  testUsingContext('test --dart-define-from-file option by corrupted json', () async {
+    globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
+    globals.fs.file('pubspec.yaml').createSync();
+    globals.fs.file('.packages').createSync();
+    await globals.fs.file('config.json').writeAsString(
+        '''
+        {
+          "kInt": 1Error json format
+          "kDouble": 1.1,
+          "name": "denghaizhu",
+          "title": "this is title from config json file"
+        }
+      '''
+    );
+    final CommandRunner<void> runner = createTestCommandRunner(BuildBundleCommand());
+
+    expect(() => runner.run(<String>[
+      'bundle',
+      '--no-pub',
+      '--dart-define-from-file=config.json',
+    ]), throwsA(predicate<Exception>((Exception e) => e is ToolExit && e.message!.startsWith('Json config define file "--dart-define-from-file=config.json" format err'))));
+  }, overrides: <Type, Generator>{
+    FileSystem: fsFactory,
+    BuildSystem: () => TestBuildSystem.all(BuildResult(success: true)),
+    ProcessManager: () => FakeProcessManager.any(),
+  });
+
 }
 
 class FakeBundleBuilder extends Fake implements BundleBuilder {
diff --git a/packages/flutter_tools/test/general.shard/build_info_test.dart b/packages/flutter_tools/test/general.shard/build_info_test.dart
index e628b7b..0b59be0 100644
--- a/packages/flutter_tools/test/general.shard/build_info_test.dart
+++ b/packages/flutter_tools/test/general.shard/build_info_test.dart
@@ -7,6 +7,7 @@
 import 'package:flutter_tools/src/build_info.dart';
 
 import '../src/common.dart';
+import '../src/context.dart';
 
 void main() {
   late BufferLogger logger;
@@ -279,4 +280,70 @@
       kDartDefines: 'MTIzMiw0NTY=,Mg==',
     }, kDartDefines), <String>['1232,456', '2']);
   });
+
+  group('Check repeated buildInfo variables', () {
+    testUsingContext('toEnvironmentConfig repeated variable', () async {
+      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, '',
+          treeShakeIcons: true,
+          trackWidgetCreation: true,
+          dartDefines: <String>['foo=2', 'bar=2'],
+          dartDefineConfigJsonMap: <String,Object>{ 'DART_DEFINES' : 'Define a variable, but it occupies the variable name of the system'},
+          dartObfuscation: true,
+      );
+      buildInfo.toEnvironmentConfig();
+      expect(testLogger.warningText, contains('The key: [DART_DEFINES] already exists, you cannot use environment variables that have been used by the system'));
+    });
+
+    testUsingContext('toEnvironmentConfig repeated variable with DART_DEFINES not set', () async {
+      // Simulate operation flutterCommand.getBuildInfo  with `dart-define-from-file` set dartDefines
+      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, '',
+          treeShakeIcons: true,
+          dartDefines: <String>['DART_DEFINES=Define a variable, but it occupies the variable name of the system'],
+          trackWidgetCreation: true,
+          dartDefineConfigJsonMap: <String, Object>{ 'DART_DEFINES' : 'Define a variable, but it occupies the variable name of the system'},
+          dartObfuscation: true,
+      );
+      buildInfo.toEnvironmentConfig();
+      expect(testLogger.warningText, contains('The key: [DART_DEFINES] already exists, you cannot use environment variables that have been used by the system'));
+
+    });
+
+    testUsingContext('toGradleConfig repeated variable', () async {
+      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, '',
+          treeShakeIcons: true,
+          trackWidgetCreation: true,
+          dartDefines: <String>['foo=2', 'bar=2'],
+          dartDefineConfigJsonMap: <String,Object>{ 'dart-defines' : 'Define a variable, but it occupies the variable name of the system'},
+          dartObfuscation: true,
+      );
+      buildInfo.toGradleConfig();
+      expect(testLogger.warningText, contains('he key: [dart-defines] already exists, you cannot use gradle variables that have been used by the system'));
+    });
+
+    testUsingContext('toGradleConfig repeated variable with not set', () async {
+      // Simulate operation flutterCommand.getBuildInfo  with `dart-define-from-file` set dartDefines
+      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, '',
+          treeShakeIcons: true,
+          trackWidgetCreation: true,
+          dartDefines: <String>['dart-defines=Define a variable, but it occupies the variable name of the system'],
+          dartDefineConfigJsonMap: <String,Object>{ 'dart-defines' : 'Define a variable, but it occupies the variable name of the system'},
+          dartObfuscation: true,
+      );
+      buildInfo.toGradleConfig();
+      expect(testLogger.warningText, contains('he key: [dart-defines] already exists, you cannot use gradle variables that have been used by the system'));
+    });
+
+    testUsingContext('toGradleConfig with androidProjectArgs override gradle project variant', () async {
+      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, '',
+          treeShakeIcons: true,
+          trackWidgetCreation: true,
+          androidProjectArgs: <String>['applicationId=com.google'],
+          dartDefineConfigJsonMap: <String,Object>{ 'applicationId' : 'override applicationId'},
+          dartObfuscation: true,
+      );
+      buildInfo.toGradleConfig();
+      expect(testLogger.warningText, contains('The key: [applicationId] already exists, you cannot use gradle variables that have been used'));
+    });
+
+  });
 }