1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:file/memory.dart';
6import 'package:flutter_tools/src/artifacts.dart';
7import 'package:flutter_tools/src/base/file_system.dart';
8import 'package:flutter_tools/src/base/logger.dart';
9import 'package:flutter_tools/src/build_info.dart';
10import 'package:flutter_tools/src/build_system/build_system.dart';
11import 'package:flutter_tools/src/build_system/targets/icon_tree_shaker.dart';
12import 'package:flutter_tools/src/devfs.dart';
13
14import '../../../src/common.dart';
15import '../../../src/fake_process_manager.dart';
16import '../../../src/fakes.dart';
17
18const List<int> _kTtfHeaderBytes = <int>[0, 1, 0, 0, 0, 15, 0, 128, 0, 3, 0, 112];
19
20const String inputPath = '/input/fonts/MaterialIcons-Regular.otf';
21const String outputPath = '/output/fonts/MaterialIcons-Regular.otf';
22const String relativePath = 'fonts/MaterialIcons-Regular.otf';
23
24final RegExp whitespace = RegExp(r'\s+');
25
26void main() {
27 late BufferLogger logger;
28 late MemoryFileSystem fileSystem;
29 late FakeProcessManager processManager;
30 late Artifacts artifacts;
31 late DevFSStringContent fontManifestContent;
32
33 late String dartPath;
34 late String constFinderPath;
35 late String fontSubsetPath;
36 late List<String> fontSubsetArgs;
37
38 List<String> getConstFinderArgs(String appDillPath) => <String>[
39 dartPath,
40 constFinderPath,
41 '--kernel-file', appDillPath,
42 '--class-library-uri', 'package:flutter/src/widgets/icon_data.dart',
43 '--class-name', 'IconData',
44 '--annotation-class-name', '_StaticIconProvider',
45 '--annotation-class-library-uri', 'package:flutter/src/widgets/icon_data.dart',
46 ];
47
48 void addConstFinderInvocation(
49 String appDillPath, {
50 int exitCode = 0,
51 String stdout = '',
52 String stderr = '',
53 }) {
54 processManager.addCommand(FakeCommand(
55 command: getConstFinderArgs(appDillPath),
56 exitCode: exitCode,
57 stdout: stdout,
58 stderr: stderr,
59 ));
60 }
61
62 void resetFontSubsetInvocation({
63 int exitCode = 0,
64 String stdout = '',
65 String stderr = '',
66 required CompleterIOSink stdinSink,
67 }) {
68 stdinSink.clear();
69 processManager.addCommand(FakeCommand(
70 command: fontSubsetArgs,
71 exitCode: exitCode,
72 stdout: stdout,
73 stderr: stderr,
74 stdin: stdinSink,
75 ));
76 }
77
78 setUp(() {
79 processManager = FakeProcessManager.empty();
80 fontManifestContent = DevFSStringContent(validFontManifestJson);
81 artifacts = Artifacts.test();
82 fileSystem = MemoryFileSystem.test();
83 logger = BufferLogger.test();
84 dartPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
85 constFinderPath = artifacts.getArtifactPath(Artifact.constFinder);
86 fontSubsetPath = artifacts.getArtifactPath(Artifact.fontSubset);
87
88 fontSubsetArgs = <String>[
89 fontSubsetPath,
90 outputPath,
91 inputPath,
92 ];
93
94 fileSystem.file(constFinderPath).createSync(recursive: true);
95 fileSystem.file(dartPath).createSync(recursive: true);
96 fileSystem.file(fontSubsetPath).createSync(recursive: true);
97 fileSystem.file(inputPath)
98 ..createSync(recursive: true)
99 ..writeAsBytesSync(_kTtfHeaderBytes);
100 });
101
102 Environment createEnvironment(Map<String, String> defines) {
103 return Environment.test(
104 fileSystem.directory('/icon_test')..createSync(recursive: true),
105 defines: defines,
106 artifacts: artifacts,
107 processManager: FakeProcessManager.any(),
108 fileSystem: fileSystem,
109 logger: BufferLogger.test(),
110 );
111 }
112
113 testWithoutContext('Prints error in debug mode environment', () async {
114 final Environment environment = createEnvironment(<String, String>{
115 kIconTreeShakerFlag: 'true',
116 kBuildMode: 'debug',
117 });
118
119 final IconTreeShaker iconTreeShaker = IconTreeShaker(
120 environment,
121 fontManifestContent,
122 logger: logger,
123 processManager: processManager,
124 fileSystem: fileSystem,
125 artifacts: artifacts,
126 targetPlatform: TargetPlatform.android,
127 );
128
129 expect(
130 logger.errorText,
131 'Font subsetting is not supported in debug mode. The --tree-shake-icons'
132 ' flag will be ignored.\n',
133 );
134 expect(iconTreeShaker.enabled, false);
135
136 final bool subsets = await iconTreeShaker.subsetFont(
137 input: fileSystem.file(inputPath),
138 outputPath: outputPath,
139 relativePath: relativePath,
140 );
141 expect(subsets, false);
142 expect(processManager, hasNoRemainingExpectations);
143 });
144
145 testWithoutContext('Does not get enabled without font manifest', () {
146 final Environment environment = createEnvironment(<String, String>{
147 kIconTreeShakerFlag: 'true',
148 kBuildMode: 'release',
149 });
150
151 final IconTreeShaker iconTreeShaker = IconTreeShaker(
152 environment,
153 null,
154 logger: logger,
155 processManager: processManager,
156 fileSystem: fileSystem,
157 artifacts: artifacts,
158 targetPlatform: TargetPlatform.android,
159 );
160
161 expect(
162 logger.errorText,
163 isEmpty,
164 );
165 expect(iconTreeShaker.enabled, false);
166 expect(processManager, hasNoRemainingExpectations);
167 });
168
169 testWithoutContext('Gets enabled', () {
170 final Environment environment = createEnvironment(<String, String>{
171 kIconTreeShakerFlag: 'true',
172 kBuildMode: 'release',
173 });
174
175 final IconTreeShaker iconTreeShaker = IconTreeShaker(
176 environment,
177 fontManifestContent,
178 logger: logger,
179 processManager: processManager,
180 fileSystem: fileSystem,
181 artifacts: artifacts,
182 targetPlatform: TargetPlatform.android,
183 );
184
185 expect(
186 logger.errorText,
187 isEmpty,
188 );
189 expect(iconTreeShaker.enabled, true);
190 expect(processManager, hasNoRemainingExpectations);
191 });
192
193 test('No app.dill throws exception', () async {
194 final Environment environment = createEnvironment(<String, String>{
195 kIconTreeShakerFlag: 'true',
196 kBuildMode: 'release',
197 });
198
199 final IconTreeShaker iconTreeShaker = IconTreeShaker(
200 environment,
201 fontManifestContent,
202 logger: logger,
203 processManager: processManager,
204 fileSystem: fileSystem,
205 artifacts: artifacts,
206 targetPlatform: TargetPlatform.android,
207 );
208
209 expect(
210 () async => iconTreeShaker.subsetFont(
211 input: fileSystem.file(inputPath),
212 outputPath: outputPath,
213 relativePath: relativePath,
214 ),
215 throwsA(isA<IconTreeShakerException>()),
216 );
217 expect(processManager, hasNoRemainingExpectations);
218 });
219
220 testWithoutContext('Can subset a font', () async {
221 final Environment environment = createEnvironment(<String, String>{
222 kIconTreeShakerFlag: 'true',
223 kBuildMode: 'release',
224 });
225 final File appDill = environment.buildDir.childFile('app.dill')
226 ..createSync(recursive: true);
227
228 final IconTreeShaker iconTreeShaker = IconTreeShaker(
229 environment,
230 fontManifestContent,
231 logger: logger,
232 processManager: processManager,
233 fileSystem: fileSystem,
234 artifacts: artifacts,
235 targetPlatform: TargetPlatform.android,
236 );
237 final CompleterIOSink stdinSink = CompleterIOSink();
238 addConstFinderInvocation(appDill.path, stdout: validConstFinderResult);
239 resetFontSubsetInvocation(stdinSink: stdinSink);
240 // Font starts out 2500 bytes long
241 final File inputFont = fileSystem.file(inputPath)
242 ..writeAsBytesSync(List<int>.filled(2500, 0));
243 // after subsetting, font is 1200 bytes long
244 fileSystem.file(outputPath)
245 ..createSync(recursive: true)
246 ..writeAsBytesSync(List<int>.filled(1200, 0));
247 bool subsetted = await iconTreeShaker.subsetFont(
248 input: inputFont,
249 outputPath: outputPath,
250 relativePath: relativePath,
251 );
252 expect(stdinSink.getAndClear(), '59470\n');
253 resetFontSubsetInvocation(stdinSink: stdinSink);
254
255 expect(subsetted, true);
256 subsetted = await iconTreeShaker.subsetFont(
257 input: fileSystem.file(inputPath),
258 outputPath: outputPath,
259 relativePath: relativePath,
260 );
261 expect(subsetted, true);
262 expect(stdinSink.getAndClear(), '59470\n');
263 expect(processManager, hasNoRemainingExpectations);
264 expect(
265 logger.statusText,
266 contains('Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 2500 to 1200 bytes (52.0% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.'),
267 );
268 });
269
270 testWithoutContext('Does not subset a non-supported font', () async {
271 final Environment environment = createEnvironment(<String, String>{
272 kIconTreeShakerFlag: 'true',
273 kBuildMode: 'release',
274 });
275 final File appDill = environment.buildDir.childFile('app.dill')
276 ..createSync(recursive: true);
277
278 final IconTreeShaker iconTreeShaker = IconTreeShaker(
279 environment,
280 fontManifestContent,
281 logger: logger,
282 processManager: processManager,
283 fileSystem: fileSystem,
284 artifacts: artifacts,
285 targetPlatform: TargetPlatform.android,
286 );
287
288 final CompleterIOSink stdinSink = CompleterIOSink();
289 addConstFinderInvocation(appDill.path, stdout: validConstFinderResult);
290 resetFontSubsetInvocation(stdinSink: stdinSink);
291
292 final File notAFont = fileSystem.file('input/foo/bar.txt')
293 ..createSync(recursive: true)
294 ..writeAsStringSync('I could not think of a better string');
295 final bool subsetted = await iconTreeShaker.subsetFont(
296 input: notAFont,
297 outputPath: outputPath,
298 relativePath: relativePath,
299 );
300 expect(subsetted, false);
301 });
302
303 testWithoutContext('Does not subset an invalid ttf font', () async {
304 final Environment environment = createEnvironment(<String, String>{
305 kIconTreeShakerFlag: 'true',
306 kBuildMode: 'release',
307 });
308 final File appDill = environment.buildDir.childFile('app.dill')
309 ..createSync(recursive: true);
310
311 final IconTreeShaker iconTreeShaker = IconTreeShaker(
312 environment,
313 fontManifestContent,
314 logger: logger,
315 processManager: processManager,
316 fileSystem: fileSystem,
317 artifacts: artifacts,
318 targetPlatform: TargetPlatform.android,
319 );
320
321 final CompleterIOSink stdinSink = CompleterIOSink();
322 addConstFinderInvocation(appDill.path, stdout: validConstFinderResult);
323 resetFontSubsetInvocation(stdinSink: stdinSink);
324
325 final File notAFont = fileSystem.file(inputPath)
326 ..writeAsBytesSync(<int>[0, 1, 2]);
327 final bool subsetted = await iconTreeShaker.subsetFont(
328 input: notAFont,
329 outputPath: outputPath,
330 relativePath: relativePath,
331 );
332
333 expect(subsetted, false);
334 });
335
336 for (final TargetPlatform platform in <TargetPlatform>[TargetPlatform.android_arm, TargetPlatform.web_javascript]) {
337 testWithoutContext('Non-constant instances $platform', () async {
338 final Environment environment = createEnvironment(<String, String>{
339 kIconTreeShakerFlag: 'true',
340 kBuildMode: 'release',
341 });
342 final File appDill = environment.buildDir.childFile('app.dill')
343 ..createSync(recursive: true);
344
345 final IconTreeShaker iconTreeShaker = IconTreeShaker(
346 environment,
347 fontManifestContent,
348 logger: logger,
349 processManager: processManager,
350 fileSystem: fileSystem,
351 artifacts: artifacts,
352 targetPlatform: platform,
353 );
354
355 addConstFinderInvocation(appDill.path, stdout: constFinderResultWithInvalid);
356
357 await expectLater(
358 () => iconTreeShaker.subsetFont(
359 input: fileSystem.file(inputPath),
360 outputPath: outputPath,
361 relativePath: relativePath,
362 ),
363 throwsToolExit(
364 message:
365 'Avoid non-constant invocations of IconData or try to build'
366 ' again with --no-tree-shake-icons.',
367 ),
368 );
369 expect(processManager, hasNoRemainingExpectations);
370 });
371 }
372
373 testWithoutContext('Does not add 0x32 for non-web builds', () async {
374 final Environment environment = createEnvironment(<String, String>{
375 kIconTreeShakerFlag: 'true',
376 kBuildMode: 'release',
377 });
378 final File appDill = environment.buildDir.childFile('app.dill')
379 ..createSync(recursive: true);
380
381 final IconTreeShaker iconTreeShaker = IconTreeShaker(
382 environment,
383 fontManifestContent,
384 logger: logger,
385 processManager: processManager,
386 fileSystem: fileSystem,
387 artifacts: artifacts,
388 targetPlatform: TargetPlatform.android_arm64,
389 );
390
391 addConstFinderInvocation(
392 appDill.path,
393 // Does not contain space char
394 stdout: validConstFinderResult,
395 );
396 final CompleterIOSink stdinSink = CompleterIOSink();
397 resetFontSubsetInvocation(stdinSink: stdinSink);
398 expect(processManager.hasRemainingExpectations, isTrue);
399 final File inputFont = fileSystem.file(inputPath)
400 ..writeAsBytesSync(List<int>.filled(2500, 0));
401 fileSystem.file(outputPath)
402 ..createSync(recursive: true)
403 ..writeAsBytesSync(List<int>.filled(1200, 0));
404
405 final bool result = await iconTreeShaker.subsetFont(
406 input: inputFont,
407 outputPath: outputPath,
408 relativePath: relativePath,
409 );
410
411 expect(result, isTrue);
412 final List<String> codePoints = stdinSink.getAndClear().trim().split(whitespace);
413 expect(codePoints, isNot(contains('optional:32')));
414
415 expect(processManager, hasNoRemainingExpectations);
416 });
417
418 testWithoutContext('Ensures 0x32 is included for web builds', () async {
419 final Environment environment = createEnvironment(<String, String>{
420 kIconTreeShakerFlag: 'true',
421 kBuildMode: 'release',
422 });
423 final File appDill = environment.buildDir.childFile('app.dill')
424 ..createSync(recursive: true);
425
426 final IconTreeShaker iconTreeShaker = IconTreeShaker(
427 environment,
428 fontManifestContent,
429 logger: logger,
430 processManager: processManager,
431 fileSystem: fileSystem,
432 artifacts: artifacts,
433 targetPlatform: TargetPlatform.web_javascript,
434 );
435
436 addConstFinderInvocation(
437 appDill.path,
438 // Does not contain space char
439 stdout: validConstFinderResult,
440 );
441 final CompleterIOSink stdinSink = CompleterIOSink();
442 resetFontSubsetInvocation(stdinSink: stdinSink);
443 expect(processManager.hasRemainingExpectations, isTrue);
444 final File inputFont = fileSystem.file(inputPath)
445 ..writeAsBytesSync(List<int>.filled(2500, 0));
446 fileSystem.file(outputPath)
447 ..createSync(recursive: true)
448 ..writeAsBytesSync(List<int>.filled(1200, 0));
449
450 final bool result = await iconTreeShaker.subsetFont(
451 input: inputFont,
452 outputPath: outputPath,
453 relativePath: relativePath,
454 );
455
456 expect(result, isTrue);
457 final List<String> codePoints = stdinSink.getAndClear().trim().split(whitespace);
458 expect(codePoints, containsAllInOrder(const <String>['59470', 'optional:32']));
459
460 expect(processManager, hasNoRemainingExpectations);
461 });
462
463 testWithoutContext('Non-zero font-subset exit code', () async {
464 final Environment environment = createEnvironment(<String, String>{
465 kIconTreeShakerFlag: 'true',
466 kBuildMode: 'release',
467 });
468 final File appDill = environment.buildDir.childFile('app.dill')
469 ..createSync(recursive: true);
470 fileSystem.file(inputPath).createSync(recursive: true);
471
472 final IconTreeShaker iconTreeShaker = IconTreeShaker(
473 environment,
474 fontManifestContent,
475 logger: logger,
476 processManager: processManager,
477 fileSystem: fileSystem,
478 artifacts: artifacts,
479 targetPlatform: TargetPlatform.android,
480 );
481
482 final CompleterIOSink stdinSink = CompleterIOSink();
483 addConstFinderInvocation(appDill.path, stdout: validConstFinderResult);
484 resetFontSubsetInvocation(exitCode: -1, stdinSink: stdinSink);
485
486 await expectLater(
487 () => iconTreeShaker.subsetFont(
488 input: fileSystem.file(inputPath),
489 outputPath: outputPath,
490 relativePath: relativePath,
491 ),
492 throwsA(isA<IconTreeShakerException>()),
493 );
494 expect(processManager, hasNoRemainingExpectations);
495 });
496
497 testWithoutContext('font-subset throws on write to sdtin', () async {
498 final Environment environment = createEnvironment(<String, String>{
499 kIconTreeShakerFlag: 'true',
500 kBuildMode: 'release',
501 });
502 final File appDill = environment.buildDir.childFile('app.dill')
503 ..createSync(recursive: true);
504
505 final IconTreeShaker iconTreeShaker = IconTreeShaker(
506 environment,
507 fontManifestContent,
508 logger: logger,
509 processManager: processManager,
510 fileSystem: fileSystem,
511 artifacts: artifacts,
512 targetPlatform: TargetPlatform.android,
513 );
514
515 final CompleterIOSink stdinSink = CompleterIOSink(throwOnAdd: true);
516 addConstFinderInvocation(appDill.path, stdout: validConstFinderResult);
517 resetFontSubsetInvocation(exitCode: -1, stdinSink: stdinSink);
518
519 await expectLater(
520 () => iconTreeShaker.subsetFont(
521 input: fileSystem.file(inputPath),
522 outputPath: outputPath,
523 relativePath: relativePath,
524 ),
525 throwsA(isA<IconTreeShakerException>()),
526 );
527 expect(processManager, hasNoRemainingExpectations);
528 });
529
530 testWithoutContext('Invalid font manifest', () async {
531 final Environment environment = createEnvironment(<String, String>{
532 kIconTreeShakerFlag: 'true',
533 kBuildMode: 'release',
534 });
535 final File appDill = environment.buildDir.childFile('app.dill')
536 ..createSync(recursive: true);
537
538 fontManifestContent = DevFSStringContent(invalidFontManifestJson);
539
540 final IconTreeShaker iconTreeShaker = IconTreeShaker(
541 environment,
542 fontManifestContent,
543 logger: logger,
544 processManager: processManager,
545 fileSystem: fileSystem,
546 artifacts: artifacts,
547 targetPlatform: TargetPlatform.android,
548 );
549
550 addConstFinderInvocation(appDill.path, stdout: validConstFinderResult);
551
552 await expectLater(
553 () => iconTreeShaker.subsetFont(
554 input: fileSystem.file(inputPath),
555 outputPath: outputPath,
556 relativePath: relativePath,
557 ),
558 throwsA(isA<IconTreeShakerException>()),
559 );
560 expect(processManager, hasNoRemainingExpectations);
561 });
562
563 testWithoutContext('Allow system font fallback when fontFamily is null', () async {
564 final Environment environment = createEnvironment(<String, String>{
565 kIconTreeShakerFlag: 'true',
566 kBuildMode: 'release',
567 });
568 final File appDill = environment.buildDir.childFile('app.dill')
569 ..createSync(recursive: true);
570
571 // Valid manifest, just not using it.
572 fontManifestContent = DevFSStringContent(validFontManifestJson);
573
574 final IconTreeShaker iconTreeShaker = IconTreeShaker(
575 environment,
576 fontManifestContent,
577 logger: logger,
578 processManager: processManager,
579 fileSystem: fileSystem,
580 artifacts: artifacts,
581 targetPlatform: TargetPlatform.android,
582 );
583
584 addConstFinderInvocation(appDill.path, stdout: emptyConstFinderResult);
585 // Does not throw
586 await iconTreeShaker.subsetFont(
587 input: fileSystem.file(inputPath),
588 outputPath: outputPath,
589 relativePath: relativePath,
590 );
591
592 expect(
593 logger.traceText,
594 contains(
595 'Expected to find fontFamily for constant IconData with codepoint: '
596 '59470, but found fontFamily: null. This usually means '
597 'you are relying on the system font. Alternatively, font families in '
598 'an IconData class can be provided in the assets section of your '
599 'pubspec.yaml, or you are missing "uses-material-design: true".\n'
600 ),
601 );
602 expect(processManager, hasNoRemainingExpectations);
603 });
604
605 testWithoutContext('Allow system font fallback when fontFamily is null and manifest is empty', () async {
606 final Environment environment = createEnvironment(<String, String>{
607 kIconTreeShakerFlag: 'true',
608 kBuildMode: 'release',
609 });
610 final File appDill = environment.buildDir.childFile('app.dill')
611 ..createSync(recursive: true);
612
613 // Nothing in font manifest
614 fontManifestContent = DevFSStringContent(emptyFontManifestJson);
615
616 final IconTreeShaker iconTreeShaker = IconTreeShaker(
617 environment,
618 fontManifestContent,
619 logger: logger,
620 processManager: processManager,
621 fileSystem: fileSystem,
622 artifacts: artifacts,
623 targetPlatform: TargetPlatform.android,
624 );
625
626 addConstFinderInvocation(appDill.path, stdout: emptyConstFinderResult);
627 // Does not throw
628 await iconTreeShaker.subsetFont(
629 input: fileSystem.file(inputPath),
630 outputPath: outputPath,
631 relativePath: relativePath,
632 );
633
634 expect(
635 logger.traceText,
636 contains(
637 'Expected to find fontFamily for constant IconData with codepoint: '
638 '59470, but found fontFamily: null. This usually means '
639 'you are relying on the system font. Alternatively, font families in '
640 'an IconData class can be provided in the assets section of your '
641 'pubspec.yaml, or you are missing "uses-material-design: true".\n'
642 ),
643 );
644 expect(processManager, hasNoRemainingExpectations);
645 });
646
647 testWithoutContext('ConstFinder non-zero exit', () async {
648 final Environment environment = createEnvironment(<String, String>{
649 kIconTreeShakerFlag: 'true',
650 kBuildMode: 'release',
651 });
652 final File appDill = environment.buildDir.childFile('app.dill')
653 ..createSync(recursive: true);
654
655 fontManifestContent = DevFSStringContent(invalidFontManifestJson);
656
657 final IconTreeShaker iconTreeShaker = IconTreeShaker(
658 environment,
659 fontManifestContent,
660 logger: logger,
661 processManager: processManager,
662 fileSystem: fileSystem,
663 artifacts: artifacts,
664 targetPlatform: TargetPlatform.android,
665 );
666
667 addConstFinderInvocation(appDill.path, exitCode: -1);
668
669 await expectLater(
670 () async => iconTreeShaker.subsetFont(
671 input: fileSystem.file(inputPath),
672 outputPath: outputPath,
673 relativePath: relativePath,
674 ),
675 throwsA(isA<IconTreeShakerException>()),
676 );
677 expect(processManager, hasNoRemainingExpectations);
678 });
679}
680
681const String validConstFinderResult = '''
682{
683 "constantInstances": [
684 {
685 "codePoint": 59470,
686 "fontFamily": "MaterialIcons",
687 "fontPackage": null,
688 "matchTextDirection": false
689 }
690 ],
691 "nonConstantLocations": []
692}
693''';
694
695const String emptyConstFinderResult = '''
696{
697 "constantInstances": [
698 {
699 "codePoint": 59470,
700 "fontFamily": null,
701 "fontPackage": null,
702 "matchTextDirection": false
703 }
704 ],
705 "nonConstantLocations": []
706}
707''';
708
709const String constFinderResultWithInvalid = '''
710{
711 "constantInstances": [
712 {
713 "codePoint": 59470,
714 "fontFamily": "MaterialIcons",
715 "fontPackage": null,
716 "matchTextDirection": false
717 }
718 ],
719 "nonConstantLocations": [
720 {
721 "file": "file:///Path/to/hello_world/lib/file.dart",
722 "line": 19,
723 "column": 11
724 }
725 ]
726}
727''';
728
729const String validFontManifestJson = '''
730[
731 {
732 "family": "MaterialIcons",
733 "fonts": [
734 {
735 "asset": "fonts/MaterialIcons-Regular.otf"
736 }
737 ]
738 },
739 {
740 "family": "GalleryIcons",
741 "fonts": [
742 {
743 "asset": "packages/flutter_gallery_assets/fonts/private/gallery_icons/GalleryIcons.ttf"
744 }
745 ]
746 },
747 {
748 "family": "packages/cupertino_icons/CupertinoIcons",
749 "fonts": [
750 {
751 "asset": "packages/cupertino_icons/assets/CupertinoIcons.ttf"
752 }
753 ]
754 }
755]
756''';
757
758const String invalidFontManifestJson = '''
759{
760 "famly": "MaterialIcons",
761 "fonts": [
762 {
763 "asset": "fonts/MaterialIcons-Regular.otf"
764 }
765 ]
766}
767''';
768
769const String emptyFontManifestJson = '[]';
770

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com