/
magnusroot
/
flutter
Обзор
Документация
Войти
/
magnusroot
/
flutter
Код
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
packages/flutter_tools/lib/src/devfs.dart
889 строк
31 KB
Ben Konyi
[flutter_tools] Watch transitive #include headers for FragmentProgram hot reload (#187945)
08 июл 2026, 19:47
Не верифицирован
08 июл 2026, 19:47
9977864
Код
Авторство
О чём код?
// 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 'dart:async'; import 'package:package_config/package_config.dart'; import 'package:process/process.dart'; import 'package:vm_service/vm_service.dart' as vm_service; import 'artifacts.dart'; import 'asset.dart'; import 'base/config.dart'; import 'base/context.dart'; import 'base/file_system.dart'; import 'base/io.dart'; import 'base/logger.dart'; import 'base/net.dart'; import 'base/os.dart'; import 'build_info.dart'; import 'build_system/tools/asset_transformer.dart'; import 'build_system/tools/shader_compiler.dart'; import 'compile.dart'; import 'convert.dart' show base64, utf8; import 'vmservice.dart'; class DevFSConfig { /// Should DevFS assume that symlink targets are stable? bool cacheSymlinks = false; /// Should DevFS assume that there are no symlinks to directories? bool noDirectorySymlinks = false; } DevFSConfig? get devFSConfig => context.get<DevFSConfig>(); /// Common superclass for content copied to the device. abstract class DevFSContent { /// Return true if the entry has been modified since it was last marked clean. bool get isModified; /// Mark the content as clean, resetting the [isModified] state. void markClean(); /// Return true if the entry has been modified after the given time. bool isModifiedAfter(DateTime time); int get size; Future<List<int>> contentsAsBytes(); Stream<List<int>> contentsAsStream(); Stream<List<int>> contentsAsCompressedStream(OperatingSystemUtils osUtils) { return osUtils.gzipLevel1Stream(contentsAsStream()); } } // File content to be copied to the device. class DevFSFileContent extends DevFSContent { DevFSFileContent(this.file); final FileSystemEntity file; File? _linkTarget; FileStat? _fileStat; File _getFile() { final File? linkTarget = _linkTarget; if (linkTarget != null) { return linkTarget; } if (file is Link) { // The link target. return file.fileSystem.file(file.resolveSymbolicLinksSync()); } return file as File; } (FileStat?, File?) _statFile() { File? linkTarget = _linkTarget; if (linkTarget != null) { // Stat the cached symlink target. final FileStat fileStat = linkTarget.statSync(); if (fileStat.type != FileSystemEntityType.notFound) { return (fileStat, linkTarget); } linkTarget = null; } final FileStat fileStat = file.statSync(); if (fileStat.type == FileSystemEntityType.notFound) { return (null, null); } if (fileStat.type == FileSystemEntityType.link) { try { final String resolved = file.resolveSymbolicLinksSync(); final File resolvedFile = file.fileSystem.file(resolved); // Stat the link target. final FileStat fileStat = resolvedFile.statSync(); if (fileStat.type == FileSystemEntityType.notFound) { return (null, null); } return (fileStat, resolvedFile); } on FileSystemException { return (null, null); } } return (fileStat, null); } @override bool get isModified { final (FileStat? currentStat, _) = _statFile(); if (_fileStat == null && currentStat == null) { return false; } return _fileStat == null || currentStat == null || currentStat.modified.isAfter(_fileStat!.modified); } @override void markClean() { final (FileStat? fileStat, File? linkTarget) = _statFile(); _fileStat = fileStat; if (linkTarget != null && (devFSConfig?.cacheSymlinks ?? false)) { _linkTarget = linkTarget; } else { _linkTarget = null; } } @override bool isModifiedAfter(DateTime time) { // Whether the file changed after [time] only depends on the file's current // modification time, not on the cached [_fileStat]. Falling back to the // cached stat (e.g. `_fileStat == null`) would report a freshly created // DevFSFileContent as modified even when its file is older than [time]. final (FileStat? currentStat, _) = _statFile(); return currentStat != null && currentStat.modified.isAfter(time); } @override int get size { final FileStat? stat = _fileStat ?? _statFile().$1; return stat?.size ?? 0; } @override Future<List<int>> contentsAsBytes() async => _getFile().readAsBytes(); @override Stream<List<int>> contentsAsStream() => _getFile().openRead(); } /// Byte content to be copied to the device. class DevFSByteContent extends DevFSContent { DevFSByteContent(this._bytes); final List<int> _bytes; final _creationTime = DateTime.now(); var _isModified = true; List<int> get bytes => _bytes; @override bool get isModified => _isModified; @override void markClean() { _isModified = false; } @override bool isModifiedAfter(DateTime time) { return _creationTime.isAfter(time); } @override int get size => _bytes.length; @override Future<List<int>> contentsAsBytes() async => _bytes; @override Stream<List<int>> contentsAsStream() => Stream<List<int>>.fromIterable(<List<int>>[_bytes]); } /// String content to be copied to the device. class DevFSStringContent extends DevFSByteContent { DevFSStringContent(String string) : _string = string, super(utf8.encode(string)); final String _string; String get string => _string; } /// A string compressing DevFSContent. /// /// A specialized DevFSContent similar to DevFSByteContent where the contents /// are the compressed bytes of a string. Its difference is that the original /// uncompressed string can be compared with directly without the indirection /// of a compute-expensive uncompress/decode and compress/encode to compare /// the strings. /// /// The `hintString` parameter is a zlib dictionary hinting mechanism to suggest /// the most common string occurrences to potentially assist with compression. class DevFSStringCompressingBytesContent extends DevFSContent { DevFSStringCompressingBytesContent(this._string, {String? hintString}) : _compressor = ZLibEncoder( dictionary: hintString == null ? null : utf8.encode(hintString), gzip: true, level: 9, ); final String _string; final ZLibEncoder _compressor; final _creationTime = DateTime.now(); var _isModified = true; late final List<int> bytes = _compressor.convert(utf8.encode(_string)); @override bool get isModified => _isModified; @override void markClean() { _isModified = false; } @override bool isModifiedAfter(DateTime time) { return _creationTime.isAfter(time); } @override int get size => bytes.length; @override Future<List<int>> contentsAsBytes() async => bytes; @override Stream<List<int>> contentsAsStream() => Stream<List<int>>.value(bytes); /// This checks the source string with another string. bool equals(String string) => _string == string; } class DevFSException implements Exception { DevFSException(this.message, [this.error, this.stackTrace]); final String message; final dynamic error; final StackTrace? stackTrace; @override String toString() => 'DevFSException($message, $error, $stackTrace)'; } /// Interface responsible for syncing asset files to a development device. abstract class DevFSWriter { /// Write the assets in [entries] to the target device. /// /// The keys of the map are relative from the [baseUri]. /// /// Throws a [DevFSException] if the process fails to complete. Future<void> write(Map<Uri, DevFSContent> entries, Uri baseUri, DevFSWriter parent); } class _DevFSHttpWriter implements DevFSWriter { _DevFSHttpWriter( this.fsName, FlutterVmService serviceProtocol, { required OperatingSystemUtils osUtils, required HttpClient httpClient, required Logger logger, Duration? uploadRetryThrottle, }) : httpAddress = serviceProtocol.httpAddress, _client = httpClient, _osUtils = osUtils, _uploadRetryThrottle = uploadRetryThrottle, _logger = logger; final HttpClient _client; final OperatingSystemUtils _osUtils; final Logger _logger; final Duration? _uploadRetryThrottle; final String fsName; final Uri? httpAddress; // 3 was chosen to try to limit the variance in the time it takes to execute // `await request.close()` since there is a known bug in Dart where it doesn't // always return a status code in response to a PUT request: // https://github.com/dart-lang/sdk/issues/43525. static const kMaxInFlight = 3; var _inFlight = 0; late Map<Uri, DevFSContent> _outstanding; late Completer<void> _completer; @override Future<void> write(Map<Uri, DevFSContent> entries, Uri devFSBase, [DevFSWriter? parent]) async { try { _client.maxConnectionsPerHost = kMaxInFlight; _completer = Completer<void>(); _outstanding = Map<Uri, DevFSContent>.of(entries); _scheduleWrites(); await _completer.future; } on SocketException catch (socketException, stackTrace) { _logger.printTrace('DevFS sync failed. Lost connection to device: $socketException'); throw DevFSException('Lost connection to device.', socketException, stackTrace); } on Exception catch (exception, stackTrace) { _logger.printError('Could not update files on device: $exception'); throw DevFSException('Sync failed', exception, stackTrace); } } void _scheduleWrites() { while ((_inFlight < kMaxInFlight) && (!_completer.isCompleted) && _outstanding.isNotEmpty) { final Uri deviceUri = _outstanding.keys.first; final DevFSContent content = _outstanding.remove(deviceUri)!; _startWrite(deviceUri, content, retry: 10); _inFlight += 1; } if ((_inFlight == 0) && (!_completer.isCompleted) && _outstanding.isEmpty) { _completer.complete(); } } Future<void> _startWrite(Uri deviceUri, DevFSContent content, {int retry = 0}) async { while (true) { try { final HttpClientRequest request = await _client.putUrl(httpAddress!); request.headers.removeAll(HttpHeaders.acceptEncodingHeader); request.headers.add('dev_fs_name', fsName); request.headers.add('dev_fs_uri_b64', base64.encode(utf8.encode('$deviceUri'))); final Stream<List<int>> contents = content.contentsAsCompressedStream(_osUtils); await request.addStream(contents); // Once the bug in Dart is solved we can remove the timeout // (https://github.com/dart-lang/sdk/issues/43525). try { final HttpClientResponse response = await request.close().timeout( const Duration(seconds: 60), ); response.listen( (_) {}, onError: (dynamic error) { _logger.printTrace('error: $error'); }, cancelOnError: true, ); } on TimeoutException { request.abort(); // This should throw "HttpException: Request has been aborted". await request.done; // Just to be safe we rethrow the TimeoutException. rethrow; } break; } on Exception catch (error, trace) { if (!_completer.isCompleted) { _logger.printTrace('Error writing "$deviceUri" to DevFS: $error'); if (retry > 0) { retry--; _logger.printTrace('trying again in a few - $retry more attempts left'); await Future<void>.delayed(_uploadRetryThrottle ?? const Duration(milliseconds: 500)); continue; } _completer.completeError(error, trace); } } } _inFlight -= 1; _scheduleWrites(); } } // Basic statistics for DevFS update operation. class UpdateFSReport { UpdateFSReport({ bool success = false, int invalidatedSourcesCount = 0, int syncedBytes = 0, int scannedSourcesCount = 0, Duration compileDuration = Duration.zero, Duration transferDuration = Duration.zero, Duration findInvalidatedDuration = Duration.zero, bool hotReloadRejected = false, }) : _success = success, _invalidatedSourcesCount = invalidatedSourcesCount, _syncedBytes = syncedBytes, _scannedSourcesCount = scannedSourcesCount, _compileDuration = compileDuration, _transferDuration = transferDuration, _findInvalidatedDuration = findInvalidatedDuration, _hotReloadRejected = hotReloadRejected; bool get success => _success; int get invalidatedSourcesCount => _invalidatedSourcesCount; int get syncedBytes => _syncedBytes; int get scannedSourcesCount => _scannedSourcesCount; Duration get compileDuration => _compileDuration; Duration get transferDuration => _transferDuration; Duration get findInvalidatedDuration => _findInvalidatedDuration; /// Whether there was a hot reload rejection in this compile. /// /// On the web, hot reload can be rejected during compile time instead of at /// runtime. bool get hotReloadRejected => _hotReloadRejected; bool _success; int _invalidatedSourcesCount; int _syncedBytes; int _scannedSourcesCount; Duration _compileDuration; Duration _transferDuration; Duration _findInvalidatedDuration; bool _hotReloadRejected; void incorporateResults(UpdateFSReport report) { if (!report._success) { _success = false; } _invalidatedSourcesCount += report._invalidatedSourcesCount; _syncedBytes += report._syncedBytes; _scannedSourcesCount += report._scannedSourcesCount; _compileDuration += report._compileDuration; _transferDuration += report._transferDuration; _findInvalidatedDuration += report._findInvalidatedDuration; if (report._hotReloadRejected) { _hotReloadRejected = true; } } } class DevFS { /// Create a [DevFS] named [fsName] for the local files in [rootDirectory]. /// /// Failed uploads are retried after [uploadRetryThrottle] duration, defaults to 500ms. DevFS( FlutterVmService serviceProtocol, this.fsName, this.rootDirectory, { required OperatingSystemUtils osUtils, required Logger logger, required FileSystem fileSystem, required ProcessManager processManager, required Artifacts artifacts, required BuildMode buildMode, HttpClient? httpClient, Duration? uploadRetryThrottle, StopwatchFactory stopwatchFactory = const StopwatchFactory(), Config? config, }) : _vmService = serviceProtocol, _logger = logger, _fileSystem = fileSystem, _httpWriter = _DevFSHttpWriter( fsName, serviceProtocol, osUtils: osUtils, logger: logger, uploadRetryThrottle: uploadRetryThrottle, httpClient: httpClient ?? ((context.get<HttpClientFactory>() == null) ? HttpClient() : context.get<HttpClientFactory>()!()), ), _stopwatchFactory = stopwatchFactory, _config = config, _assetTransformer = DevelopmentAssetTransformer( transformer: AssetTransformer( processManager: processManager, fileSystem: fileSystem, dartBinaryPath: artifacts.getArtifactPath(Artifact.engineDartBinary), buildMode: buildMode, ), fileSystem: fileSystem, logger: logger, ); final FlutterVmService _vmService; final _DevFSHttpWriter _httpWriter; final Logger _logger; final FileSystem _fileSystem; final StopwatchFactory _stopwatchFactory; final Config? _config; final DevelopmentAssetTransformer _assetTransformer; final String fsName; final Directory rootDirectory; final assetPathsToEvict = <String>{}; final shaderPathsToEvict = <String>{}; // A flag to indicate whether we have called `setAssetDirectory` on the target device. bool hasSetAssetDirectory = false; /// Whether the font manifest was uploaded during [update]. bool didUpdateFontManifest = false; List<Uri> sources = <Uri>[]; DateTime? lastCompiled; DateTime? _previousCompiled; PackageConfig? lastPackageConfig; Uri? _baseUri; Uri? get baseUri => _baseUri; Uri deviceUriToHostUri(Uri deviceUri) { final deviceUriString = deviceUri.toString(); final baseUriString = baseUri.toString(); if (deviceUriString.startsWith(baseUriString)) { final String deviceUriSuffix = deviceUriString.substring(baseUriString.length); return rootDirectory.uri.resolve(deviceUriSuffix); } return deviceUri; } Future<Uri> create() async { _logger.printTrace('DevFS: Creating new filesystem on the device ($_baseUri)'); try { final vm_service.Response response = await _vmService.createDevFS(fsName); _baseUri = Uri.parse(response.json!['uri'] as String); } on vm_service.RPCError catch (rpcException) { if (rpcException.code == vm_service.RPCErrorKind.kServiceDisappeared.code || rpcException.code == vm_service.RPCErrorKind.kConnectionDisposed.code || rpcException.message.contains('Service connection disposed')) { // This can happen if the device has been disconnected, so translate to // a DevFSException, which the caller will handle. throw DevFSException('Service disconnected', rpcException); } // 1001 is kFileSystemAlreadyExists in //dart/runtime/vm/json_stream.h if (rpcException.code != 1001) { // Other RPCErrors are unexpected. Rethrow so it will hit crash // logging. rethrow; } _logger.printTrace('DevFS: Creating failed. Destroying and trying again'); await destroy(); final vm_service.Response response = await _vmService.createDevFS(fsName); _baseUri = Uri.parse(response.json!['uri'] as String); } _logger.printTrace('DevFS: Created new filesystem on the device ($_baseUri)'); return _baseUri!; } Future<void> destroy() async { _logger.printTrace('DevFS: Deleting filesystem on the device ($_baseUri)'); await _vmService.deleteDevFS(fsName); _logger.printTrace('DevFS: Deleted filesystem on the device ($_baseUri)'); } /// Mark the [lastCompiled] time to the previous successful compile. /// /// Sometimes a hot reload will be rejected by the VM due to a change in the /// structure of the code not supporting the hot reload. In these cases, /// the best resolution is a hot restart. However, the resident runner /// will not recognize this file as having been changed since the delta /// will already have been accepted. Instead, reset the compile time so /// that the last updated files are included in subsequent compilations until /// a reload is accepted. void resetLastCompiled() { lastCompiled = _previousCompiled; } /// Updates files on the device. /// /// Returns the number of bytes synced. /// /// If [fullRestart] is true, assumes this is a hot restart instead of a hot /// reload. If [resetCompiler] is true, sends a `reset` instruction to the /// frontend server. Future<UpdateFSReport> update({ required Uri mainUri, required ResidentCompiler generator, required bool trackWidgetCreation, required String pathToReload, required List<Uri> invalidatedFiles, required PackageConfig packageConfig, required String dillOutputPath, required DevelopmentShaderCompiler shaderCompiler, DevFSWriter? devFSWriter, String? target, AssetBundle? bundle, bool bundleFirstUpload = false, bool fullRestart = false, bool resetCompiler = false, File? dartPluginRegistrant, }) async { final candidateCompileTime = DateTime.now(); lastPackageConfig = packageConfig; // Update modified files final dirtyEntries = <Uri, DevFSContent>{}; var assetBuildFailed = false; var syncedBytes = 0; if (resetCompiler) { generator.reset(); } // On a full restart, or on an initial compile for the attach based workflow, // this will produce a full dill. Subsequent invocations will produce incremental // dill files that depend on the invalidated files. _logger.printTrace('Compiling dart to kernel with ${invalidatedFiles.length} updated files'); // Await the compiler response after checking if the bundle is updated. This allows the file // stating to be done while waiting for the frontend_server response. final Stopwatch compileTimer = _stopwatchFactory.createStopwatch('compile')..start(); final Future<CompilerOutput?> pendingCompilerOutput = generator .recompile( mainUri, invalidatedFiles, outputPath: dillOutputPath, fs: _fileSystem, projectRootPath: rootDirectory.path, packageConfig: packageConfig, checkDartPluginRegistry: true, // The entry point is assumed not to have changed. dartPluginRegistrant: dartPluginRegistrant, ) .then((CompilerOutput? result) { compileTimer.stop(); return result; }); if (bundle != null) { // Mark processing of bundle started for testability of starting the compile // before processing bundle. _logger.printTrace('Processing bundle.'); // await null to give time for telling the compiler to compile. await null; final String assetDirectory = getAssetBuildDirectory(_config, _fileSystem); try { final int bundleSyncedBytes = await updateBundle( bundle: bundle, dirtyEntries: dirtyEntries, assetDirectory: assetDirectory, assetTransformer: _assetTransformer, shaderCompiler: shaderCompiler, fileSystem: _fileSystem, rootDirectoryPath: rootDirectory.path, assetPathsToEvict: assetPathsToEvict, shaderPathsToEvict: shaderPathsToEvict, bundleFirstUpload: bundleFirstUpload, invalidatedFiles: invalidatedFiles, onFontManifestUpdated: () => didUpdateFontManifest = true, ); syncedBytes += bundleSyncedBytes; _assetTransformer.pruneDependencies(bundle.entries.keys.toSet()); } on Exception catch (err, stackTrace) { _logger.printError('Error updating bundle: $err'); _logger.printTrace('$stackTrace'); assetBuildFailed = true; } // Mark processing of bundle done for testability of starting the compile // before processing bundle. _logger.printTrace('Bundle processing done.'); } final CompilerOutput? compilerOutput = await pendingCompilerOutput; if (compilerOutput == null || compilerOutput.errorCount > 0) { return UpdateFSReport(); } // Only update the last compiled time if we successfully compiled. _previousCompiled = lastCompiled; lastCompiled = candidateCompileTime; // list of sources that needs to be monitored are in [compilerOutput.sources] sources = <Uri>{ ...compilerOutput.sources, ..._assetTransformer.dependencies.values.expand((Set<Uri> uris) => uris), }.toList(); // // Don't send full kernel file that would overwrite what VM already // started loading from. if (!bundleFirstUpload) { final String compiledBinary = compilerOutput.outputFilename; if (compiledBinary.isNotEmpty) { final Uri entryUri = _fileSystem.path.toUri(pathToReload); final content = DevFSFileContent(_fileSystem.file(compiledBinary)); syncedBytes += content.size; dirtyEntries[entryUri] = content; } } _logger.printTrace('Updating files.'); final Stopwatch transferTimer = _stopwatchFactory.createStopwatch('transfer')..start(); if (assetBuildFailed) { return UpdateFSReport(); } _logger.printTrace('Pending asset builds completed. Writing dirty entries.'); if (dirtyEntries.isNotEmpty) { await (devFSWriter ?? _httpWriter).write(dirtyEntries, _baseUri!, _httpWriter); } transferTimer.stop(); _logger.printTrace('DevFS: Sync finished'); return UpdateFSReport( success: true, syncedBytes: syncedBytes, invalidatedSourcesCount: invalidatedFiles.length, compileDuration: compileTimer.elapsed, transferDuration: transferTimer.elapsed, ); } /// Converts a platform-specific file path to a platform-independent URL path. static String _asUriPath(FileSystem fileSystem, String filePath) => '${fileSystem.path.toUri(filePath).path}/'; /// Process and sync [bundle] assets to [dirtyEntries]. /// /// Returns the total number of bytes processed. /// Throws an [Exception] if an asset build failed. static Future<int> updateBundle({ required AssetBundle bundle, required Map<Uri, DevFSContent> dirtyEntries, required String assetDirectory, required DevelopmentAssetTransformer assetTransformer, required DevelopmentShaderCompiler shaderCompiler, required FileSystem fileSystem, required String rootDirectoryPath, required Set<String> assetPathsToEvict, required Set<String> shaderPathsToEvict, required bool bundleFirstUpload, List<Uri> invalidatedFiles = const <Uri>[], bool syncAllAssetsOnFirstUpload = false, void Function()? onFontManifestUpdated, }) async { final String assetBuildDirPrefix = _asUriPath(fileSystem, assetDirectory); final pendingAssetBuilds = <Future<void>>[]; var syncedBytes = 0; final Set<Uri> invalidatedSet = invalidatedFiles.toSet(); final syncedEntries = <AssetBundleEntry>[]; bundle.entries.forEach((String archivePath, AssetBundleEntry entry) { final bool hasTransformers = entry.transformers.isNotEmpty; final bool skipSync = bundleFirstUpload && !syncAllAssetsOnFirstUpload && !hasTransformers; if (skipSync) { entry.content.markClean(); return; } final isShader = entry.kind == AssetKind.shader; final bool isEntryModified = entry.content.isModified || (isShader && shaderCompiler.areDependenciesModified(entry.content)); final Set<Uri>? deps = assetTransformer.dependencies[archivePath]; final bool hasInvalidatedDependencies = deps != null && deps.any(invalidatedSet.contains); if (!bundleFirstUpload && !isEntryModified && !hasInvalidatedDependencies) { return; } syncedEntries.add(entry); // Modified shaders must be recompiled per-target platform. final Uri deviceUri = fileSystem.path.toUri( fileSystem.path.join(assetDirectory, archivePath), ); if (deviceUri.path.startsWith(assetBuildDirPrefix)) { archivePath = deviceUri.path.substring(assetBuildDirPrefix.length); } // If the font manifest is updated, invoke the callback so the hot runner // can invoke a service extension to force the engine to reload fonts. if (archivePath == kFontManifestJson) { onFontManifestUpdated?.call(); } switch (entry.kind) { case AssetKind.shader: pendingAssetBuilds.add(() async { DevFSContent content = entry.content; if (entry.transformers.isNotEmpty) { final DevFSContent? transformed = await assetTransformer.retransformAsset( inputAssetKey: archivePath, inputAssetContent: content, transformerEntries: entry.transformers, workingDirectory: rootDirectoryPath, ); if (transformed == null) { throw AssetTransformationException(archivePath, 'Failed to transform shader'); } content = transformed; } if (!bundleFirstUpload || syncAllAssetsOnFirstUpload) { final DevFSContent? compiled = await shaderCompiler.recompileShader(content); if (compiled == null) { throw DevFSShaderCompilationException(archivePath, 'Failed to compile shader'); } dirtyEntries[deviceUri] = compiled; syncedBytes += compiled.size; } if (!bundleFirstUpload) { shaderPathsToEvict.add(archivePath); } }()); case AssetKind.regular: case AssetKind.font: pendingAssetBuilds.add(() async { DevFSContent? content; if (entry.transformers.isEmpty || entry.kind != AssetKind.regular) { content = entry.content; } else { content = await assetTransformer.retransformAsset( inputAssetKey: archivePath, inputAssetContent: entry.content, transformerEntries: entry.transformers, workingDirectory: rootDirectoryPath, ); } if (content == null) { throw AssetTransformationException(archivePath, 'Failed to transform asset'); } if (!bundleFirstUpload || syncAllAssetsOnFirstUpload) { dirtyEntries[deviceUri] = content; syncedBytes += content.size; } if (!bundleFirstUpload) { assetPathsToEvict.add(archivePath); } }()); } }); await Future.wait(pendingAssetBuilds); for (final entry in syncedEntries) { entry.content.markClean(); } return syncedBytes; } } /// An implementation of a devFS writer which copies physical files for devices /// running on the same host. /// /// DevFS entries which correspond to physical files are copied using [File.copySync], /// while entries that correspond to arbitrary string/byte values are written from /// memory. /// /// Requires that the file system is the same for both the tool and application. class LocalDevFSWriter implements DevFSWriter { LocalDevFSWriter({required FileSystem fileSystem}) : _fileSystem = fileSystem; final FileSystem _fileSystem; @override Future<void> write(Map<Uri, DevFSContent> entries, Uri baseUri, [DevFSWriter? parent]) async { try { for (final MapEntry<Uri, DevFSContent> entry in entries.entries) { final Uri uri = entry.key; final DevFSContent devFSContent = entry.value; final File destination = _fileSystem.file(baseUri.resolveUri(uri)); if (!destination.parent.existsSync()) { destination.parent.createSync(recursive: true); } if (devFSContent is DevFSFileContent) { final content = devFSContent.file as File; content.copySync(destination.path); continue; } destination.writeAsBytesSync(await devFSContent.contentsAsBytes()); } } on FileSystemException catch (err) { throw DevFSException(err.toString()); } } } /// Exception thrown when development-time asset transformation fails. final class AssetTransformationException implements Exception { AssetTransformationException(this.archivePath, this.message); final String archivePath; final String message; @override String toString() => 'AssetTransformationException: $message (Asset: $archivePath)'; } /// Exception thrown when development-time shader compilation fails. final class DevFSShaderCompilationException implements Exception { DevFSShaderCompilationException(this.archivePath, this.message); final String archivePath; final String message; @override String toString() => 'DevFSShaderCompilationException: $message (Shader: $archivePath)'; }