Skip to content

Commit 168d807

Browse files
authored
Add .env file support for option --dart-define-from-file (#128668)
# Proposal I suggest to make possible to specify .env files to the --dart-define-from-file in addition to the Json format. # Issue Close #128667
1 parent 35085c3 commit 168d807

File tree

2 files changed

+254
-10
lines changed

2 files changed

+254
-10
lines changed

packages/flutter_tools/lib/src/runner/flutter_command.dart

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -612,17 +612,17 @@ abstract class FlutterCommand extends Command<void> {
612612
valueHelp: 'foo=bar',
613613
splitCommas: false,
614614
);
615-
useDartDefineConfigJsonFileOption();
615+
useDartDefineFromFileOption();
616616
}
617617

618-
void useDartDefineConfigJsonFileOption() {
618+
void useDartDefineFromFileOption() {
619619
argParser.addMultiOption(
620620
FlutterOptions.kDartDefineFromFileOption,
621-
help: 'The path of a json format file where flutter define a global constant pool. '
622-
'Json entry will be available as constants from the String.fromEnvironment, bool.fromEnvironment, '
623-
'and int.fromEnvironment constructors; the key and field are json values.\n'
621+
help:
622+
'The path of a .json or .env file containing key-value pairs that will be available as environment variables.\n'
623+
'These can be accessed using the String.fromEnvironment, bool.fromEnvironment, and int.fromEnvironment constructors.\n'
624624
'Multiple defines can be passed by repeating "--${FlutterOptions.kDartDefineFromFileOption}" multiple times.',
625-
valueHelp: 'use-define-config.json',
625+
valueHelp: 'use-define-config.json|.env',
626626
splitCommas: false,
627627
);
628628
}
@@ -1341,18 +1341,29 @@ abstract class FlutterCommand extends Command<void> {
13411341
final Map<String, Object?> dartDefineConfigJsonMap = <String, Object?>{};
13421342

13431343
if (argParser.options.containsKey(FlutterOptions.kDartDefineFromFileOption)) {
1344-
final List<String> configJsonPaths = stringsArg(
1344+
final List<String> configFilePaths = stringsArg(
13451345
FlutterOptions.kDartDefineFromFileOption,
13461346
);
13471347

1348-
for (final String path in configJsonPaths) {
1348+
for (final String path in configFilePaths) {
13491349
if (!globals.fs.isFileSync(path)) {
13501350
throwToolExit('Json config define file "--${FlutterOptions
13511351
.kDartDefineFromFileOption}=$path" is not a file, '
13521352
'please fix first!');
13531353
}
13541354

1355-
final String configJsonRaw = globals.fs.file(path).readAsStringSync();
1355+
final String configRaw = globals.fs.file(path).readAsStringSync();
1356+
1357+
// Determine whether the file content is JSON or .env format.
1358+
String configJsonRaw;
1359+
if (configRaw.trim().startsWith('{')) {
1360+
configJsonRaw = configRaw;
1361+
} else {
1362+
1363+
// Convert env file to JSON.
1364+
configJsonRaw = convertEnvFileToJsonRaw(configRaw);
1365+
}
1366+
13561367
try {
13571368
// Fix json convert Object value :type '_InternalLinkedHashMap<String, dynamic>' is not a subtype of type 'Map<String, Object>' in type cast
13581369
(json.decode(configJsonRaw) as Map<String, dynamic>)
@@ -1370,6 +1381,88 @@ abstract class FlutterCommand extends Command<void> {
13701381
return dartDefineConfigJsonMap;
13711382
}
13721383

1384+
/// Parse a property line from an env file.
1385+
/// Supposed property structure should be:
1386+
/// key=value
1387+
///
1388+
/// Where: key is a string without spaces and value is a string.
1389+
/// Value can also contain '=' char.
1390+
///
1391+
/// Returns a record of key and value as strings.
1392+
MapEntry<String, String> _parseProperty(String line) {
1393+
final RegExp blockRegExp = RegExp(r'^\s*([a-zA-Z_]+[a-zA-Z0-9_]*)\s*=\s*"""\s*(.*)$');
1394+
if (blockRegExp.hasMatch(line)) {
1395+
throwToolExit('Multi-line value is not supported: $line');
1396+
}
1397+
1398+
final RegExp propertyRegExp = RegExp(r'^\s*([a-zA-Z_]+[a-zA-Z0-9_]*)\s*=\s*(.*)?$');
1399+
final Match? match = propertyRegExp.firstMatch(line);
1400+
if (match == null) {
1401+
throwToolExit('Unable to parse file provided for '
1402+
'--${FlutterOptions.kDartDefineFromFileOption}.\n'
1403+
'Invalid property line: $line');
1404+
}
1405+
1406+
final String key = match.group(1)!;
1407+
final String value = match.group(2) ?? '';
1408+
1409+
// Remove wrapping quotes and trailing line comment.
1410+
final RegExp doubleQuoteValueRegExp = RegExp(r'^"(.*)"\s*(\#\s*.*)?$');
1411+
final Match? doubleQuoteValue = doubleQuoteValueRegExp.firstMatch(value);
1412+
if (doubleQuoteValue != null) {
1413+
return MapEntry<String, String>(key, doubleQuoteValue.group(1)!);
1414+
}
1415+
1416+
final RegExp quoteValueRegExp = RegExp(r"^'(.*)'\s*(\#\s*.*)?$");
1417+
final Match? quoteValue = quoteValueRegExp.firstMatch(value);
1418+
if (quoteValue != null) {
1419+
return MapEntry<String, String>(key, quoteValue.group(1)!);
1420+
}
1421+
1422+
final RegExp backQuoteValueRegExp = RegExp(r'^`(.*)`\s*(\#\s*.*)?$');
1423+
final Match? backQuoteValue = backQuoteValueRegExp.firstMatch(value);
1424+
if (backQuoteValue != null) {
1425+
return MapEntry<String, String>(key, backQuoteValue.group(1)!);
1426+
}
1427+
1428+
final RegExp noQuoteValueRegExp = RegExp(r'^([^#\n\s]*)\s*(?:\s*#\s*(.*))?$');
1429+
final Match? noQuoteValue = noQuoteValueRegExp.firstMatch(value);
1430+
if (noQuoteValue != null) {
1431+
return MapEntry<String, String>(key, noQuoteValue.group(1)!);
1432+
}
1433+
1434+
return MapEntry<String, String>(key, value);
1435+
}
1436+
1437+
/// Converts an .env file string to its equivalent JSON string.
1438+
///
1439+
/// For example, the .env file string
1440+
/// key=value # comment
1441+
/// complexKey="foo#bar=baz"
1442+
/// would be converted to a JSON string equivalent to:
1443+
/// {
1444+
/// "key": "value",
1445+
/// "complexKey": "foo#bar=baz"
1446+
/// }
1447+
///
1448+
/// Multiline values are not supported.
1449+
String convertEnvFileToJsonRaw(String configRaw) {
1450+
final List<String> lines = configRaw
1451+
.split('\n')
1452+
.map((String line) => line.trim())
1453+
.where((String line) => line.isNotEmpty)
1454+
.where((String line) => !line.startsWith('#')) // Remove comment lines.
1455+
.toList();
1456+
1457+
final Map<String, String> propertyMap = <String, String>{};
1458+
for (final String line in lines) {
1459+
final MapEntry<String, String> property = _parseProperty(line);
1460+
propertyMap[property.key] = property.value;
1461+
}
1462+
1463+
return jsonEncode(propertyMap);
1464+
}
1465+
13731466
/// Updates dart-defines based on [webRenderer].
13741467
@visibleForTesting
13751468
static List<String> updateDartDefines(List<String> dartDefines, WebRendererMode webRenderer) {

packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,8 @@ void main() {
518518
"kDouble": 1.1,
519519
"name": "denghaizhu",
520520
"title": "this is title from config json file",
521-
"nullValue": null
521+
"nullValue": null,
522+
"containEqual": "sfadsfv=432f"
522523
}
523524
'''
524525
);
@@ -549,6 +550,7 @@ void main() {
549550
'name=denghaizhu',
550551
'title=this is title from config json file',
551552
'nullValue=null',
553+
'containEqual=sfadsfv=432f',
552554
'body=this is body from config json file',
553555
]),
554556
);
@@ -557,6 +559,155 @@ void main() {
557559
ProcessManager: () => FakeProcessManager.any(),
558560
});
559561

562+
testUsingContext('--dart-define-from-file correctly parses a valid env file', () async {
563+
globals.fs
564+
.file(globals.fs.path.join('lib', 'main.dart'))
565+
.createSync(recursive: true);
566+
globals.fs.file('pubspec.yaml').createSync();
567+
globals.fs.file('.packages').createSync();
568+
await globals.fs.file('.env').writeAsString('''
569+
# comment
570+
kInt=1
571+
kDouble=1.1 # should be double
572+
573+
name=piotrfleury
574+
title=this is title from config env file
575+
empty=
576+
577+
doubleQuotes="double quotes 'value'#=" # double quotes
578+
singleQuotes='single quotes "value"#=' # single quotes
579+
backQuotes=`back quotes "value" '#=` # back quotes
580+
581+
hashString="some-#-hash-string-value"
582+
583+
# Play around with spaces around the equals sign.
584+
spaceBeforeEqual =value
585+
spaceAroundEqual = value
586+
spaceAfterEqual= value
587+
588+
''');
589+
await globals.fs.file('.env2').writeAsString('''
590+
# second comment
591+
592+
body=this is body from config env file
593+
''');
594+
final CommandRunner<void> runner =
595+
createTestCommandRunner(BuildBundleCommand(
596+
logger: BufferLogger.test(),
597+
));
598+
599+
await runner.run(<String>[
600+
'bundle',
601+
'--no-pub',
602+
'--dart-define-from-file=.env',
603+
'--dart-define-from-file=.env2',
604+
]);
605+
}, overrides: <Type, Generator>{
606+
BuildSystem: () => TestBuildSystem.all(BuildResult(success: true),
607+
(Target target, Environment environment) {
608+
expect(
609+
_decodeDartDefines(environment),
610+
containsAllInOrder(const <String>[
611+
'kInt=1',
612+
'kDouble=1.1',
613+
'name=piotrfleury',
614+
'title=this is title from config env file',
615+
'empty=',
616+
"doubleQuotes=double quotes 'value'#=",
617+
'singleQuotes=single quotes "value"#=',
618+
'backQuotes=back quotes "value" \'#=',
619+
'hashString=some-#-hash-string-value',
620+
'spaceBeforeEqual=value',
621+
'spaceAroundEqual=value',
622+
'spaceAfterEqual=value',
623+
'body=this is body from config env file'
624+
]),
625+
);
626+
}),
627+
FileSystem: fsFactory,
628+
ProcessManager: () => FakeProcessManager.any(),
629+
});
630+
631+
testUsingContext('--dart-define-from-file option env file throws a ToolExit when .env file contains a multiline value', () async {
632+
globals.fs
633+
.file(globals.fs.path.join('lib', 'main.dart'))
634+
.createSync(recursive: true);
635+
globals.fs.file('pubspec.yaml').createSync();
636+
globals.fs.file('.packages').createSync();
637+
await globals.fs.file('.env').writeAsString('''
638+
# single line value
639+
name=piotrfleury
640+
641+
# multi-line value
642+
multiline = """ Welcome to .env demo
643+
a simple counter app with .env file support
644+
for more info, check out the README.md file
645+
Thanks! """ # This is the welcome message that will be displayed on the counter app
646+
647+
''');
648+
final CommandRunner<void> runner =
649+
createTestCommandRunner(BuildBundleCommand(
650+
logger: BufferLogger.test(),
651+
));
652+
653+
expect(() => runner.run(<String>[
654+
'bundle',
655+
'--no-pub',
656+
'--dart-define-from-file=.env',
657+
]), throwsToolExit(message: 'Multi-line value is not supported: multiline = """ Welcome to .env demo'));
658+
}, overrides: <Type, Generator>{
659+
BuildSystem: () => TestBuildSystem.all(BuildResult(success: true)),
660+
FileSystem: fsFactory,
661+
ProcessManager: () => FakeProcessManager.any(),
662+
});
663+
664+
testUsingContext('--dart-define-from-file option works with mixed file formats',
665+
() async {
666+
globals.fs
667+
.file(globals.fs.path.join('lib', 'main.dart'))
668+
.createSync(recursive: true);
669+
globals.fs.file('pubspec.yaml').createSync();
670+
globals.fs.file('.packages').createSync();
671+
await globals.fs.file('.env').writeAsString('''
672+
kInt=1
673+
kDouble=1.1
674+
name=piotrfleury
675+
title=this is title from config env file
676+
''');
677+
await globals.fs.file('config.json').writeAsString('''
678+
{
679+
"body": "this is body from config json file"
680+
}
681+
''');
682+
final CommandRunner<void> runner =
683+
createTestCommandRunner(BuildBundleCommand(
684+
logger: BufferLogger.test(),
685+
));
686+
687+
await runner.run(<String>[
688+
'bundle',
689+
'--no-pub',
690+
'--dart-define-from-file=.env',
691+
'--dart-define-from-file=config.json',
692+
]);
693+
}, overrides: <Type, Generator>{
694+
BuildSystem: () => TestBuildSystem.all(BuildResult(success: true),
695+
(Target target, Environment environment) {
696+
expect(
697+
_decodeDartDefines(environment),
698+
containsAllInOrder(const <String>[
699+
'kInt=1',
700+
'kDouble=1.1',
701+
'name=piotrfleury',
702+
'title=this is title from config env file',
703+
'body=this is body from config json file',
704+
]),
705+
);
706+
}),
707+
FileSystem: fsFactory,
708+
ProcessManager: () => FakeProcessManager.any(),
709+
});
710+
560711
testUsingContext('test --dart-define-from-file option if conflict', () async {
561712
globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true);
562713
globals.fs.file('pubspec.yaml').createSync();

0 commit comments

Comments
 (0)