// Copyright 2014 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 '../base/file_system.dart'; import '../base/logger.dart'; import '../convert.dart'; /// Represents a configured deferred component as defined in /// the app's pubspec.yaml. class DeferredComponent { DeferredComponent({ required this.name, this.libraries = const <String>[], this.assets = const <Uri>[], }) : _assigned = false; /// The name of the deferred component. There should be a matching /// android dynamic feature module with the same name. final String name; /// The dart libraries this component includes as listed in pubspec.yaml. /// /// This list is only of dart libraries manually configured to be in this component. /// Valid libraries that are listed here will always be guaranteed to be /// packaged in this component. However, libraries that are not listed here /// may also be included if the loading units that are needed also contain /// libraries that are not listed here. final List<String> libraries; /// Assets that are part of this component as a Uri relative to the project directory. final List<Uri> assets; /// The minimal set of [LoadingUnit]s needed that contain all of the dart libraries in /// [libraries]. /// /// Each [LoadingUnit] contains the compiled code for a set of dart libraries. Each /// [DeferredComponent] contains a list of dart libraries that must be included in the /// component. The set [loadingUnits] is all of the [LoadingUnit]s needed such that /// all required dart libs in [libraries] are in the union of the [LoadingUnit.libraries] /// included by the loading units in [loadingUnits]. /// /// When [loadingUnits] is non-null, then the component is considered [assigned] and the /// field [assigned] will be true. When [loadingUnits] is null, then the component is /// unassigned and should not be used for any tasks that require loading unit information. /// When using [loadingUnits], [assigned] should be checked first. Loading units can be /// assigned with [assignLoadingUnits]. Set<LoadingUnit>? get loadingUnits => _loadingUnits; Set<LoadingUnit>? _loadingUnits; /// Indicates if the component has loading units assigned. /// /// Unassigned components reflect the pubspec.yaml configuration directly, /// contain no loading unit data, and [loadingUnits] is null. Once assigned, the component /// will contain a set of [loadingUnits] which contains the [LoadingUnit]s that the /// component needs to include. Loading units can be assigned with the [assignLoadingUnits] /// call. bool get assigned => _assigned; bool _assigned; /// Selects the [LoadingUnit]s that contain this component's dart libraries. /// /// After calling this method, this [DeferredComponent] will be considered [assigned], /// and [loadingUnits] will return a non-null result. /// /// [LoadingUnit]s in `allLoadingUnits` that contain libraries that are in [libraries] /// are added to the set [loadingUnits]. /// /// Providing null or empty list of `allLoadingUnits` will still change the assigned /// status, but will result in [loadingUnits] returning an empty set. void assignLoadingUnits(List<LoadingUnit> allLoadingUnits) { _assigned = true; _loadingUnits = <LoadingUnit>{}; for (final String lib in libraries) { for (final LoadingUnit loadingUnit in allLoadingUnits) { if (loadingUnit.libraries.contains(lib)) { _loadingUnits!.add(loadingUnit); } } } } /// Provides a human readable string representation of the /// configuration. @override String toString() { final StringBuffer out = StringBuffer('\nDeferredComponent: $name\n Libraries:'); for (final String lib in libraries) { out.write('\n - $lib'); } if (loadingUnits != null && _assigned) { out.write('\n LoadingUnits:'); for (final LoadingUnit loadingUnit in loadingUnits!) { out.write('\n - ${loadingUnit.id}'); } } out.write('\n Assets:'); for (final Uri asset in assets) { out.write('\n - ${asset.path}'); } return out.toString(); } } /// Represents a single loading unit and holds information regarding it's id, /// shared library path, and dart libraries in it. class LoadingUnit { /// Constructs a [LoadingUnit]. /// /// Loading units must include an [id] and [libraries]. The [path] is only present when /// parsing the loading unit from a loading unit manifest produced by gen_snapshot. LoadingUnit({ required this.id, required this.libraries, this.path, }); /// The unique loading unit id that is used to identify the loading unit within dart. final int id; /// A list of dart libraries that the loading unit contains. final List<String> libraries; /// The output path of the shared library .so file created by gen_snapshot. /// /// This value may be null when the loading unit is parsed from a /// `deferred_components_golden.yaml` file, which does not store the path. final String? path; /// Returns a human readable string representation of this LoadingUnit, ignoring /// the [path] field. The [path] is not included as it is not relevant when the @override String toString() { final StringBuffer out = StringBuffer('\nLoadingUnit $id\n Libraries:'); for (final String lib in libraries) { out.write('\n - $lib'); } return out.toString(); } /// Returns true if the other loading unit has the same [id] and the same set of [libraries], /// ignoring order. bool equalsIgnoringPath(LoadingUnit other) { return other.id == id && other.libraries.toSet().containsAll(libraries); } /// Parses the loading unit manifests from the [outputDir] of the latest /// gen_snapshot/assemble run. /// /// This will read all existing loading units for every provided abi. If no abis are /// provided, loading units for all abis will be parsed. static List<LoadingUnit> parseGeneratedLoadingUnits(Directory outputDir, Logger logger, {List<String>? abis}) { final List<LoadingUnit> loadingUnits = <LoadingUnit>[]; final List<FileSystemEntity> files = outputDir.listSync(recursive: true); for (final FileSystemEntity fileEntity in files) { if (fileEntity is File) { final File file = fileEntity; // Determine if the abi is one we build. bool matchingAbi = abis == null; if (abis != null) { for (final String abi in abis) { if (file.parent.path.endsWith(abi)) { matchingAbi = true; break; } } } if (!file.path.endsWith('manifest.json') || !matchingAbi) { continue; } loadingUnits.addAll(parseLoadingUnitManifest(file, logger)); } } return loadingUnits; } /// Parses loading units from a single loading unit manifest json file. /// /// Returns an empty list if the manifestFile does not exist or is invalid. static List<LoadingUnit> parseLoadingUnitManifest(File manifestFile, Logger logger) { if (!manifestFile.existsSync()) { return <LoadingUnit>[]; } // Read gen_snapshot manifest final String fileString = manifestFile.readAsStringSync(); Map<String, dynamic>? manifest; try { manifest = jsonDecode(fileString) as Map<String, dynamic>; } on FormatException catch (e) { logger.printError('Loading unit manifest at `${manifestFile.path}` was invalid JSON:\n$e'); } final List<LoadingUnit> loadingUnits = <LoadingUnit>[]; // Setup android source directory if (manifest != null) { for (final dynamic loadingUnitMetadata in manifest['loadingUnits'] as List<dynamic>) { final Map<String, dynamic> loadingUnitMap = loadingUnitMetadata as Map<String, dynamic>; if (loadingUnitMap['id'] == 1) { continue; // Skip base unit } loadingUnits.add(LoadingUnit( id: loadingUnitMap['id'] as int, path: loadingUnitMap['path'] as String, libraries: List<String>.from(loadingUnitMap['libraries'] as List<dynamic>)), ); } } return loadingUnits; } }