| // 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 'package:process/process.dart'; |
| import 'package:unified_analytics/unified_analytics.dart'; |
| |
| import '../artifacts.dart'; |
| import '../base/common.dart'; |
| import '../base/file_system.dart'; |
| import '../base/logger.dart'; |
| import '../base/project_migrator.dart'; |
| import '../base/terminal.dart'; |
| import '../base/utils.dart'; |
| import '../build_info.dart'; |
| import '../build_system/build_system.dart'; |
| import '../cache.dart'; |
| import '../flutter_plugins.dart'; |
| import '../globals.dart' as globals; |
| import '../platform_plugins.dart'; |
| import '../plugins.dart'; |
| import '../project.dart'; |
| import '../reporting/reporting.dart'; |
| import '../version.dart'; |
| import 'compiler_config.dart'; |
| import 'file_generators/flutter_service_worker_js.dart'; |
| import 'migrations/scrub_generated_plugin_registrant.dart'; |
| |
| export 'compiler_config.dart'; |
| |
| /// Whether the application has web plugins. |
| const String kHasWebPlugins = 'HasWebPlugins'; |
| |
| /// Base href to set in index.html in flutter build command |
| const String kBaseHref = 'baseHref'; |
| |
| /// The caching strategy to use for service worker generation. |
| const String kServiceWorkerStrategy = 'ServiceWorkerStrategy'; |
| |
| class WebBuilder { |
| WebBuilder({ |
| required Logger logger, |
| required ProcessManager processManager, |
| required BuildSystem buildSystem, |
| required Usage usage, |
| required Analytics analytics, |
| required FlutterVersion flutterVersion, |
| required FileSystem fileSystem, |
| }) : _logger = logger, |
| _processManager = processManager, |
| _buildSystem = buildSystem, |
| _flutterUsage = usage, |
| _analytics = analytics, |
| _flutterVersion = flutterVersion, |
| _fileSystem = fileSystem; |
| |
| final Logger _logger; |
| final ProcessManager _processManager; |
| final BuildSystem _buildSystem; |
| final Usage _flutterUsage; |
| final Analytics _analytics; |
| final FlutterVersion _flutterVersion; |
| final FileSystem _fileSystem; |
| |
| Future<void> buildWeb( |
| FlutterProject flutterProject, |
| String target, |
| BuildInfo buildInfo, |
| ServiceWorkerStrategy serviceWorkerStrategy, { |
| required List<WebCompilerConfig> compilerConfigs, |
| String? baseHref, |
| String? outputDirectoryPath, |
| }) async { |
| final bool hasWebPlugins = |
| (await findPlugins(flutterProject)).any((Plugin p) => p.platforms.containsKey(WebPlugin.kConfigKey)); |
| final Directory outputDirectory = outputDirectoryPath == null |
| ? _fileSystem.directory(getWebBuildDirectory()) |
| : _fileSystem.directory(outputDirectoryPath); |
| outputDirectory.createSync(recursive: true); |
| |
| // The migrators to apply to a Web project. |
| final List<ProjectMigrator> migrators = <ProjectMigrator>[ |
| ScrubGeneratedPluginRegistrant(flutterProject.web, _logger), |
| ]; |
| |
| final ProjectMigration migration = ProjectMigration(migrators); |
| migration.run(); |
| |
| final Status status = _logger.startProgress('Compiling $target for the Web...'); |
| final Stopwatch sw = Stopwatch()..start(); |
| try { |
| final BuildResult result = await _buildSystem.build( |
| globals.buildTargets.webServiceWorker(_fileSystem, compilerConfigs), |
| Environment( |
| projectDir: _fileSystem.currentDirectory, |
| outputDir: outputDirectory, |
| buildDir: flutterProject.directory.childDirectory('.dart_tool').childDirectory('flutter_build'), |
| defines: <String, String>{ |
| kTargetFile: target, |
| kHasWebPlugins: hasWebPlugins.toString(), |
| if (baseHref != null) kBaseHref: baseHref, |
| kServiceWorkerStrategy: serviceWorkerStrategy.cliName, |
| ...buildInfo.toBuildSystemEnvironment(), |
| }, |
| artifacts: globals.artifacts!, |
| fileSystem: _fileSystem, |
| logger: _logger, |
| processManager: _processManager, |
| platform: globals.platform, |
| usage: _flutterUsage, |
| analytics: _analytics, |
| cacheDir: globals.cache.getRoot(), |
| engineVersion: globals.artifacts!.isLocalEngine ? null : _flutterVersion.engineRevision, |
| flutterRootDir: _fileSystem.directory(Cache.flutterRoot), |
| // Web uses a different Dart plugin registry. |
| // https://github.com/flutter/flutter/issues/80406 |
| generateDartPluginRegistry: false, |
| )); |
| if (!result.success) { |
| for (final ExceptionMeasurement measurement in result.exceptions.values) { |
| _logger.printError( |
| 'Target ${measurement.target} failed: ${measurement.exception}', |
| stackTrace: measurement.fatal ? measurement.stackTrace : null, |
| ); |
| } |
| throwToolExit('Failed to compile application for the Web.'); |
| } |
| } on Exception catch (err) { |
| throwToolExit(err.toString()); |
| } finally { |
| status.stop(); |
| } |
| |
| // We don't print a size because the output directory can contain |
| // optional files not needed by the user. |
| globals.printStatus( |
| '${globals.terminal.successMark} ' |
| 'Built ${globals.fs.path.relative(outputDirectory.path)}', |
| color: TerminalColor.green, |
| ); |
| |
| final String buildSettingsString = _buildEventAnalyticsSettings( |
| configs: compilerConfigs, |
| ); |
| |
| BuildEvent( |
| 'web-compile', |
| type: 'web', |
| settings: buildSettingsString, |
| flutterUsage: _flutterUsage, |
| ).send(); |
| _analytics.send(Event.flutterBuildInfo( |
| label: 'web-compile', |
| buildType: 'web', |
| settings: buildSettingsString, |
| )); |
| |
| final Duration elapsedDuration = sw.elapsed; |
| final String variableName = compilerConfigs.length > 1 ? 'dual-compile' : 'dart2js'; |
| _flutterUsage.sendTiming( |
| 'build', |
| variableName, |
| elapsedDuration, |
| ); |
| _analytics.send(Event.timing( |
| workflow: 'build', |
| variableName: variableName, |
| elapsedMilliseconds: elapsedDuration.inMilliseconds, |
| )); |
| } |
| } |
| |
| /// Web rendering backend mode. |
| enum WebRendererMode implements CliEnum { |
| /// Auto detects which rendering backend to use. |
| auto, |
| |
| /// Always uses canvaskit. |
| canvaskit, |
| |
| /// Always uses html. |
| html, |
| |
| /// Always use skwasm. |
| skwasm; |
| |
| @override |
| String get cliName => snakeCase(name, '-'); |
| |
| @override |
| String get helpText => switch (this) { |
| auto => |
| 'Use the HTML renderer on mobile devices, and CanvasKit on desktop devices.', |
| canvaskit => |
| 'Always use the CanvasKit renderer. This renderer uses WebGL and WebAssembly to render graphics.', |
| html => |
| 'Always use the HTML renderer. This renderer uses a combination of HTML, CSS, SVG, 2D Canvas, and WebGL.', |
| skwasm => 'Always use the experimental skwasm renderer.' |
| }; |
| |
| Iterable<String> get dartDefines => switch (this) { |
| WebRendererMode.auto => <String>[ |
| 'FLUTTER_WEB_AUTO_DETECT=true', |
| ], |
| WebRendererMode.canvaskit => <String>[ |
| 'FLUTTER_WEB_AUTO_DETECT=false', |
| 'FLUTTER_WEB_USE_SKIA=true', |
| ], |
| WebRendererMode.html => <String>[ |
| 'FLUTTER_WEB_AUTO_DETECT=false', |
| 'FLUTTER_WEB_USE_SKIA=false', |
| ], |
| WebRendererMode.skwasm => <String>[ |
| 'FLUTTER_WEB_AUTO_DETECT=false', |
| 'FLUTTER_WEB_USE_SKIA=false', |
| 'FLUTTER_WEB_USE_SKWASM=true', |
| ] |
| }; |
| |
| List<String> updateDartDefines(List<String> inputDefines) { |
| final Set<String> dartDefinesSet = inputDefines.toSet(); |
| if (!inputDefines.any((String d) => d.startsWith('FLUTTER_WEB_AUTO_DETECT=')) |
| && inputDefines.any((String d) => d.startsWith('FLUTTER_WEB_USE_SKIA='))) { |
| dartDefinesSet.removeWhere((String d) => d.startsWith('FLUTTER_WEB_USE_SKIA=')); |
| } |
| dartDefinesSet.addAll(dartDefines); |
| return dartDefinesSet.toList(); |
| } |
| } |
| |
| /// The correct precompiled artifact to use for each build and render mode for DDC with AMD modules. |
| // TODO(markzipan): delete this when DDC's AMD module system is deprecated, https://github.com/flutter/flutter/issues/142060. |
| const Map<WebRendererMode, Map<NullSafetyMode, HostArtifact>> kAmdDartSdkJsArtifactMap = <WebRendererMode, Map<NullSafetyMode, HostArtifact>>{ |
| WebRendererMode.auto: <NullSafetyMode, HostArtifact> { |
| NullSafetyMode.sound: HostArtifact.webPrecompiledAmdCanvaskitAndHtmlSoundSdk, |
| NullSafetyMode.unsound: HostArtifact.webPrecompiledAmdCanvaskitAndHtmlSdk, |
| }, |
| WebRendererMode.canvaskit: <NullSafetyMode, HostArtifact> { |
| NullSafetyMode.sound: HostArtifact.webPrecompiledAmdCanvaskitSoundSdk, |
| NullSafetyMode.unsound: HostArtifact.webPrecompiledAmdCanvaskitSdk, |
| }, |
| WebRendererMode.html: <NullSafetyMode, HostArtifact> { |
| NullSafetyMode.sound: HostArtifact.webPrecompiledAmdSoundSdk, |
| NullSafetyMode.unsound: HostArtifact.webPrecompiledAmdSdk, |
| }, |
| }; |
| |
| /// The correct source map artifact to use for each build and render mode for DDC with AMD modules. |
| // TODO(markzipan): delete this when DDC's AMD module system is deprecated, https://github.com/flutter/flutter/issues/142060. |
| const Map<WebRendererMode, Map<NullSafetyMode, HostArtifact>> kAmdDartSdkJsMapArtifactMap = <WebRendererMode, Map<NullSafetyMode, HostArtifact>>{ |
| WebRendererMode.auto: <NullSafetyMode, HostArtifact> { |
| NullSafetyMode.sound: HostArtifact.webPrecompiledAmdCanvaskitAndHtmlSoundSdkSourcemaps, |
| NullSafetyMode.unsound: HostArtifact.webPrecompiledAmdCanvaskitAndHtmlSdkSourcemaps, |
| }, |
| WebRendererMode.canvaskit: <NullSafetyMode, HostArtifact> { |
| NullSafetyMode.sound: HostArtifact.webPrecompiledAmdCanvaskitSoundSdkSourcemaps, |
| NullSafetyMode.unsound: HostArtifact.webPrecompiledAmdCanvaskitSdkSourcemaps, |
| }, |
| WebRendererMode.html: <NullSafetyMode, HostArtifact> { |
| NullSafetyMode.sound: HostArtifact.webPrecompiledAmdSoundSdkSourcemaps, |
| NullSafetyMode.unsound: HostArtifact.webPrecompiledAmdSdkSourcemaps, |
| }, |
| }; |
| |
| /// The correct precompiled artifact to use for each build and render mode for DDC with DDC modules. |
| const Map<WebRendererMode, Map<NullSafetyMode, HostArtifact>> kDdcDartSdkJsArtifactMap = <WebRendererMode, Map<NullSafetyMode, HostArtifact>>{ |
| WebRendererMode.auto: <NullSafetyMode, HostArtifact> { |
| NullSafetyMode.sound: HostArtifact.webPrecompiledDdcCanvaskitAndHtmlSoundSdk, |
| NullSafetyMode.unsound: HostArtifact.webPrecompiledDdcCanvaskitAndHtmlSdk, |
| }, |
| WebRendererMode.canvaskit: <NullSafetyMode, HostArtifact> { |
| NullSafetyMode.sound: HostArtifact.webPrecompiledDdcCanvaskitSoundSdk, |
| NullSafetyMode.unsound: HostArtifact.webPrecompiledDdcCanvaskitSdk, |
| }, |
| WebRendererMode.html: <NullSafetyMode, HostArtifact> { |
| NullSafetyMode.sound: HostArtifact.webPrecompiledDdcSoundSdk, |
| NullSafetyMode.unsound: HostArtifact.webPrecompiledDdcSdk, |
| }, |
| }; |
| |
| /// The correct source map artifact to use for each build and render mode for DDC with DDC modules. |
| const Map<WebRendererMode, Map<NullSafetyMode, HostArtifact>> kDdcDartSdkJsMapArtifactMap = <WebRendererMode, Map<NullSafetyMode, HostArtifact>>{ |
| WebRendererMode.auto: <NullSafetyMode, HostArtifact> { |
| NullSafetyMode.sound: HostArtifact.webPrecompiledDdcCanvaskitAndHtmlSoundSdkSourcemaps, |
| NullSafetyMode.unsound: HostArtifact.webPrecompiledDdcCanvaskitAndHtmlSdkSourcemaps, |
| }, |
| WebRendererMode.canvaskit: <NullSafetyMode, HostArtifact> { |
| NullSafetyMode.sound: HostArtifact.webPrecompiledDdcCanvaskitSoundSdkSourcemaps, |
| NullSafetyMode.unsound: HostArtifact.webPrecompiledDdcCanvaskitSdkSourcemaps, |
| }, |
| WebRendererMode.html: <NullSafetyMode, HostArtifact> { |
| NullSafetyMode.sound: HostArtifact.webPrecompiledDdcSoundSdkSourcemaps, |
| NullSafetyMode.unsound: HostArtifact.webPrecompiledDdcSdkSourcemaps, |
| }, |
| }; |
| |
| String _buildEventAnalyticsSettings({ |
| required List<WebCompilerConfig> configs, |
| }) { |
| final Map<String, Object> values = <String, Object>{}; |
| final List<String> renderers = <String>[]; |
| final List<String> targets = <String>[]; |
| for (final WebCompilerConfig config in configs) { |
| values.addAll(config.buildEventAnalyticsValues); |
| renderers.add(config.renderer.name); |
| targets.add(config.compileTarget.name); |
| } |
| values['web-renderer'] = renderers.join(','); |
| values['web-target'] = targets.join(','); |
| |
| final List<String> sortedList = values.entries |
| .map((MapEntry<String, Object> e) => '${e.key}: ${e.value};') |
| .toList() |
| ..sort(); |
| |
| return sortedList.join(' '); |
| } |