| // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file | |
| // for details. 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 'dart:convert'; | |
| import 'dart:io'; | |
| import 'dart:isolate'; | |
| import 'package:args/command_runner.dart'; | |
| import 'package:http/http.dart' as http; | |
| import 'package:path/path.dart' as p; | |
| import 'package:tar/tar.dart'; | |
| import 'package:yaml/yaml.dart'; | |
| import '../dartpad_config.dart'; | |
| import '../version_info.dart'; | |
| import 'dart.dart'; | |
| /// Command to build a DartPad SDK for Flutter. | |
| final class SetupFlutterCommand extends Command<void> { | |
| @override | |
| final String name = 'flutter'; | |
| @override | |
| final String description = 'Build a DartPad SDK for Flutter.'; | |
| SetupFlutterCommand() { | |
| argParser | |
| ..addOption( | |
| 'output', | |
| abbr: 'o', | |
| mandatory: true, | |
| valueHelp: 'dir', | |
| help: 'Output directory for the Flutter DartPad SDK.', | |
| ) | |
| ..addOption( | |
| 'flutter-root', | |
| valueHelp: 'dir', | |
| help: | |
| 'Path to an existing Flutter SDK checkout/installation ' | |
| '(defaults to \$FLUTTER_ROOT or `flutter` on PATH).', | |
| ) | |
| ..addOption( | |
| 'channel', | |
| abbr: 'c', | |
| allowed: ['master', 'main', 'beta', 'stable'], | |
| help: | |
| 'Clone Flutter (<channel>) into a temporary directory and build ' | |
| 'from that checkout (mutually exclusive with --flutter-root).', | |
| ) | |
| ..addFlag( | |
| 'use-cdn', | |
| defaultsTo: true, | |
| help: | |
| 'Use CanvasKit from Google CDN instead of bundling canvaskit/ locally.', | |
| ) | |
| ..addOption( | |
| 'dartpad-sdk', | |
| valueHelp: 'dir', | |
| help: | |
| 'Path to a locally built DartPad SDK for Dart (e.g. out/ReleaseX64/dartpad). ' | |
| 'When omitted, downloads the matching dartpad.zip from gs://dart-archive ' | |
| 'for <flutter>/bin/cache/dart-sdk/revision.', | |
| ) | |
| ..addOption( | |
| 'web-sdk', | |
| allowed: ['copy', 'build'], | |
| defaultsTo: 'copy', | |
| help: | |
| 'Copy or build `ddc_outline.dill` + `dart_sdk.js` from Flutter ' | |
| 'SDK.', | |
| ) | |
| ..addOption( | |
| 'bootstrap-code-path', | |
| valueHelp: 'file', | |
| help: 'Path to a file containing custom Flutter bootstrap code.', | |
| ); | |
| } | |
| @override | |
| Future<void> run() async { | |
| final results = argResults!; | |
| final outputDir = p.normalize(p.absolute(results.option('output')!)); | |
| var flutterRoot = switch (results.option('flutter-root')) { | |
| final path? => p.normalize(p.absolute(path)), | |
| null => null, | |
| }; | |
| final channel = results.option('channel'); | |
| final useCdn = results.flag('use-cdn'); | |
| final localDartPadSdk = switch (results.option('dartpad-sdk')) { | |
| final path? => p.normalize(p.absolute(path)), | |
| null => null, | |
| }; | |
| final webSdkOption = results.option('web-sdk')!; | |
| final bootstrapCodePath = switch (results.option('bootstrap-code-path')) { | |
| final path? => p.normalize(p.absolute(path)), | |
| null => null, | |
| }; | |
| final outDir = Directory(outputDir); | |
| if (outDir.existsSync() && outDir.listSync().isNotEmpty) { | |
| usageException( | |
| 'Output directory "$outputDir" already exists and is not empty.', | |
| ); | |
| } | |
| if (flutterRoot != null && channel != null) { | |
| usageException('Cannot specify both --flutter-root and --channel.'); | |
| } | |
| String? bootstrapCode; | |
| if (bootstrapCodePath != null) { | |
| final file = File(bootstrapCodePath); | |
| if (!file.existsSync()) { | |
| usageException('Bootstrap code file not found at $bootstrapCodePath'); | |
| } | |
| bootstrapCode = file.readAsStringSync(); | |
| } | |
| final tempDir = Directory.systemTemp.createTempSync( | |
| 'dartpad_flutter_setup_', | |
| ); | |
| try { | |
| if (channel != null) { | |
| flutterRoot = p.join(tempDir.path, 'flutter_sdk'); | |
| print('Cloning Flutter ($channel) into $flutterRoot...'); | |
| _runSync('git', [ | |
| 'clone', | |
| '-b', | |
| channel, | |
| 'https://github.com/flutter/flutter.git', | |
| flutterRoot, | |
| ], tempDir.path); | |
| } else { | |
| if (flutterRoot == null && | |
| Platform.environment['FLUTTER_ROOT'] != null) { | |
| flutterRoot = p.normalize( | |
| p.absolute(Platform.environment['FLUTTER_ROOT']!), | |
| ); | |
| } | |
| if (flutterRoot == null) { | |
| try { | |
| final flutterExecutable = await _resolveFlutterExecutable(); | |
| flutterRoot = p.normalize( | |
| Directory(flutterExecutable).parent.parent.path, | |
| ); | |
| } on Object { | |
| usageException( | |
| 'FLUTTER_ROOT not set, `flutter` not found in PATH, ' | |
| 'and neither --flutter-root nor --channel was provided.', | |
| ); | |
| } | |
| } | |
| } | |
| if (!Directory(flutterRoot).existsSync()) { | |
| usageException('Flutter SDK not found at $flutterRoot'); | |
| } | |
| outDir.createSync(recursive: true); | |
| print('Using Flutter SDK at: $flutterRoot'); | |
| final flutterBin = p.join( | |
| flutterRoot, | |
| 'bin', | |
| Platform.isWindows ? 'flutter.bat' : 'flutter', | |
| ); | |
| // 1. Create and build dummy app to ensure Flutter cache is populated | |
| // and harvest web assets + package_config.json. | |
| final myappDir = p.join(tempDir.path, 'myapp'); | |
| print('Creating dummy app...'); | |
| _runSync(flutterBin, [ | |
| 'create', | |
| 'myapp', | |
| '--empty', | |
| '--platforms', | |
| 'web', | |
| ], tempDir.path); | |
| print('Pruning pubspec.yaml...'); | |
| _runSync(flutterBin, ['pub', 'remove', 'flutter_lints'], myappDir); | |
| for (final sdkPkg in [ | |
| 'flutter_web_plugins', | |
| 'flutter_localizations', | |
| 'integration_test', | |
| ]) { | |
| print('Adding $sdkPkg dependency...'); | |
| _runSync(flutterBin, ['pub', 'add', sdkPkg, '--sdk=flutter'], myappDir); | |
| } | |
| print('Adding material_ui, cupertino_ui, cupertino_icons'); | |
| _runSync(flutterBin, [ | |
| 'pub', | |
| 'add', | |
| 'material_ui', | |
| 'cupertino_ui', | |
| 'cupertino_icons', | |
| ], myappDir); | |
| print('Running flutter pub get...'); | |
| _runSync(flutterBin, ['pub', 'get'], myappDir); | |
| print('Building dummy app for web (to harvest assets)...'); | |
| _runSync(flutterBin, ['build', 'web', '--debug'], myappDir); | |
| // Resolve Dart SDK and DartPad SDK for Dart (either local or downloaded | |
| // from gs://dart-archive matching <flutter>/bin/cache/dart-sdk/revision). | |
| final String dartSdkRoot; | |
| final String dartDartPadSdk; | |
| if (localDartPadSdk != null) { | |
| if (!Directory(localDartPadSdk).existsSync()) { | |
| throw StateError( | |
| 'Local DartPad SDK directory not found at $localDartPadSdk', | |
| ); | |
| } | |
| dartDartPadSdk = localDartPadSdk; | |
| final siblingDartSdk = p.normalize( | |
| p.join(localDartPadSdk, '..', 'dart-sdk'), | |
| ); | |
| dartSdkRoot = Directory(siblingDartSdk).existsSync() | |
| ? siblingDartSdk | |
| : p.dirname(p.dirname(Platform.resolvedExecutable)); | |
| } else { | |
| dartSdkRoot = p.join(flutterRoot, 'bin', 'cache', 'dart-sdk'); | |
| final revisionFile = File(p.join(dartSdkRoot, 'revision')); | |
| final versionFile = File(p.join(dartSdkRoot, 'version')); | |
| if (!revisionFile.existsSync() || !versionFile.existsSync()) { | |
| throw StateError( | |
| 'Expected ${revisionFile.path} and ${versionFile.path} to exist.', | |
| ); | |
| } | |
| final dartRevision = revisionFile.readAsStringSync().trim(); | |
| final dartVersion = versionFile.readAsStringSync().trim(); | |
| final dartChannel = inferDartChannelFromVersion(dartVersion); | |
| final zipUrl = resolveDartPadZipUrl( | |
| channel: dartChannel, | |
| revision: dartRevision, | |
| ); | |
| dartDartPadSdk = p.join(tempDir.path, 'dart_dartpad_sdk'); | |
| print( | |
| 'Downloading matching dartpad.zip for Flutter Dart SDK ' | |
| '$dartVersion ($dartRevision on channel $dartChannel)...', | |
| ); | |
| await downloadAndExtractDartPadZip(zipUrl, dartDartPadSdk); | |
| } | |
| final dartBin = p.join( | |
| dartSdkRoot, | |
| 'bin', | |
| Platform.isWindows ? 'dart.exe' : 'dart', | |
| ); | |
| final dartAotRuntimeBin = p.join( | |
| dartSdkRoot, | |
| 'bin', | |
| Platform.isWindows ? 'dartaotruntime.exe' : 'dartaotruntime', | |
| ); | |
| final packageRoot = await _resolveDartPadPackageRoot(); | |
| final assetDir = p.join(packageRoot, 'asset'); | |
| await _buildFlutterDartPadSdk( | |
| _BuildContext( | |
| dartSdkRoot: dartSdkRoot, | |
| dartDartPadSdk: dartDartPadSdk, | |
| flutterRoot: flutterRoot, | |
| tempDir: tempDir.path, | |
| myappDir: myappDir, | |
| flutterBin: flutterBin, | |
| dartBin: dartBin, | |
| dartAotRuntimeBin: dartAotRuntimeBin, | |
| flutterAssetDir: outputDir, | |
| assetDir: assetDir, | |
| bootstrapCode: bootstrapCode, | |
| buildWebSdk: webSdkOption == 'build', | |
| useCdn: useCdn, | |
| ), | |
| ); | |
| } finally { | |
| if (tempDir.existsSync()) { | |
| tempDir.deleteSync(recursive: true); | |
| } | |
| } | |
| } | |
| } | |
| /// Infers the `gs://dart-archive` channel (`main`, `dev`, `beta`, `stable`) | |
| /// from a Dart SDK version string (e.g. from `bin/cache/dart-sdk/version`). | |
| String inferDartChannelFromVersion(String version) { | |
| final v = version.trim(); | |
| if (v.contains('-edge.')) return 'main'; | |
| if (v.endsWith('.dev')) return 'dev'; | |
| if (v.endsWith('.beta')) return 'beta'; | |
| return 'stable'; | |
| } | |
| final class _BuildContext { | |
| final String dartSdkRoot; | |
| final String dartDartPadSdk; | |
| final String flutterRoot; | |
| final String tempDir; | |
| final String myappDir; | |
| final String flutterBin; | |
| final String dartBin; | |
| final String dartAotRuntimeBin; | |
| final String flutterAssetDir; | |
| final String assetDir; | |
| final String? bootstrapCode; | |
| final bool buildWebSdk; | |
| final bool useCdn; | |
| _BuildContext({ | |
| required this.dartSdkRoot, | |
| required this.dartDartPadSdk, | |
| required this.flutterRoot, | |
| required this.tempDir, | |
| required this.myappDir, | |
| required this.flutterBin, | |
| required this.dartBin, | |
| required this.dartAotRuntimeBin, | |
| required this.flutterAssetDir, | |
| required this.assetDir, | |
| required this.bootstrapCode, | |
| required this.buildWebSdk, | |
| required this.useCdn, | |
| }); | |
| } | |
| Future<void> _buildFlutterDartPadSdk(_BuildContext ctx) async { | |
| Directory(ctx.flutterAssetDir).createSync(recursive: true); | |
| // 2. Scrape Assets (CanvasKit, Fonts) | |
| print('Scraping assets...'); | |
| final sourceAssetsDir = p.join(ctx.myappDir, 'build', 'web', 'assets'); | |
| copyDirectoryContents(sourceAssetsDir, p.join(ctx.flutterAssetDir, 'assets')); | |
| final String canvasKitBaseUrl; | |
| if (ctx.useCdn) { | |
| canvasKitBaseUrl = await _resolveAndVerifyCanvasKitCdnUrl( | |
| ctx.flutterRoot, | |
| ctx.myappDir, | |
| ); | |
| } else { | |
| print('Scraping CanvasKit...'); | |
| final sourceCanvasKitDir = p.join( | |
| ctx.myappDir, | |
| 'build', | |
| 'web', | |
| 'canvaskit', | |
| ); | |
| final destCanvasKitDir = p.join(ctx.flutterAssetDir, 'canvaskit'); | |
| copyDirectoryContents(sourceCanvasKitDir, destCanvasKitDir); | |
| _verifyLocalCanvasKitFiles(destCanvasKitDir); | |
| canvasKitBaseUrl = './canvaskit/'; | |
| } | |
| // 3. Compile flutter_web.js and flutter_web.dill | |
| print('Compiling flutter_web.js and flutter_web.dill...'); | |
| final pkgConfigPath = p.join( | |
| ctx.myappDir, | |
| '.dart_tool', | |
| 'package_config.json', | |
| ); | |
| final pkgConfig = | |
| jsonDecode(File(pkgConfigPath).readAsStringSync()) | |
| as Map<String, dynamic>; | |
| final compileSources = <String>[]; | |
| final packageRootPaths = <String, String>{}; | |
| for (final pkgEntry in pkgConfig['packages'] as List<dynamic>) { | |
| final pkg = pkgEntry as Map<String, dynamic>; | |
| final name = pkg['name'] as String; | |
| if (name == 'sky_engine' || name == 'myapp') continue; | |
| final rootUriStr = pkg['rootUri'] as String; | |
| final rootUri = Uri.parse(rootUriStr); | |
| final rootPath = rootUri.scheme == 'file' | |
| ? rootUri.toFilePath() | |
| : p.normalize(p.join(ctx.myappDir, '.dart_tool', rootUriStr)); | |
| packageRootPaths[name] = rootPath; | |
| final libDir = Directory(p.join(rootPath, pkg['packageUri'] as String)); | |
| if (libDir.existsSync()) { | |
| final topLevelFiles = libDir | |
| .listSync(recursive: false) | |
| .whereType<File>() | |
| .where((f) => f.path.endsWith('.dart')); | |
| for (final file in topLevelFiles) { | |
| final relative = p.relative(file.path, from: libDir.path); | |
| if (name == 'matcher' && relative == 'mirror_matchers.dart') { | |
| continue; | |
| } | |
| compileSources.add('package:$name/${p.toUri(relative).path}'); | |
| } | |
| } | |
| } | |
| final webSdk = ctx.buildWebSdk | |
| ? _buildWebSdk(ctx, pkgConfigPath) | |
| : _copyWebSdk(ctx); | |
| final snapshotPath = p.join( | |
| ctx.dartSdkRoot, | |
| 'bin', | |
| 'snapshots', | |
| 'dartdevc.dart.snapshot', | |
| ); | |
| final outlinePath = webSdk.outlineDill; | |
| final outputJsPath = p.join(ctx.flutterAssetDir, 'flutter_web.js'); | |
| final outputDillPath = p.join(ctx.tempDir, 'flutter_web.dill'); | |
| _runSync(ctx.dartBin, [ | |
| snapshotPath, | |
| '-s', | |
| outlinePath, | |
| '--modules=ddc', | |
| '--canary', | |
| '--experimental-emit-debug-metadata', | |
| '--track-creation-locations', | |
| '--module-name=flutter_web', | |
| '--packages=$pkgConfigPath', | |
| '-o', | |
| outputJsPath, | |
| ...compileSources, | |
| ], ctx.myappDir); | |
| final metadataFile = File('$outputJsPath.metadata'); | |
| final metadataJson = | |
| jsonDecode(metadataFile.readAsStringSync()) as Map<String, Object?>; | |
| final flutterWebLibraries = [ | |
| for (final lib in metadataJson['libraries'] as List<Object?>) | |
| if (lib case {'importUri': final String uri}) uri, | |
| ]; | |
| // DevTools inspects `Isolate.libraries` for these libraries to detect that | |
| // the running isolate is a Flutter app and to enable the Widget Inspector. | |
| for (final requiredLib in const [ | |
| 'package:flutter/src/widgets/binding.dart', | |
| 'package:flutter/src/widgets/widget_inspector.dart', | |
| ]) { | |
| if (!flutterWebLibraries.contains(requiredLib)) { | |
| throw StateError( | |
| 'Expected "$requiredLib" in flutter_web.js.metadata libraries', | |
| ); | |
| } | |
| } | |
| metadataFile.deleteSync(); | |
| // We don't want the full dill generated by DDC. | |
| final fullDillPath = p.setExtension(outputJsPath, '.dill'); | |
| if (File(fullDillPath).existsSync()) { | |
| File(fullDillPath).deleteSync(); | |
| } | |
| final kernelWorkerPath = p.join( | |
| ctx.dartSdkRoot, | |
| 'bin', | |
| 'snapshots', | |
| 'kernel_worker_aot.dart.snapshot', | |
| ); | |
| _runSync(ctx.dartAotRuntimeBin, [ | |
| kernelWorkerPath, | |
| '--target', | |
| 'ddc', | |
| '--summary-only', | |
| '--track-creation-locations', | |
| '--packages-file', | |
| pkgConfigPath, | |
| '--dart-sdk-summary', | |
| outlinePath, | |
| '--output', | |
| outputDillPath, | |
| ...compileSources.expand((s) => ['--source', s]), | |
| ], ctx.myappDir); | |
| print('Copying dart_sdk.js...'); | |
| _copyFile(webSdk.dartSdkJs, p.join(ctx.flutterAssetDir, 'dart_sdk.js')); | |
| _copyFile( | |
| '${webSdk.dartSdkJs}.map', | |
| p.join(ctx.flutterAssetDir, 'dart_sdk.js.map'), | |
| ); | |
| // Synthesize sandbox.js | |
| print('Synthesizing sandbox.js...'); | |
| final sandboxJsPatch = File( | |
| p.join(ctx.assetDir, 'sandbox_flutter_patch.js'), | |
| ).readAsStringSync().replaceAll('{{canvasKitBaseUrl}}', canvasKitBaseUrl); | |
| final sandboxJs = File( | |
| p.join(ctx.dartDartPadSdk, 'sandbox.js'), | |
| ).readAsStringSync(); | |
| File( | |
| p.join(ctx.flutterAssetDir, 'sandbox.js'), | |
| ).writeAsStringSync('$sandboxJsPatch\n$sandboxJs'); | |
| // Synthesize sandbox_runtime.js (bundling flutter.js). | |
| print('Synthesizing sandbox_runtime.js...'); | |
| final flutterJs = File( | |
| p.join(ctx.myappDir, 'build', 'web', 'flutter.js'), | |
| ).readAsStringSync(); | |
| final sandboxRuntimeJs = File( | |
| p.join(ctx.dartDartPadSdk, 'sandbox_runtime.js'), | |
| ).readAsStringSync(); | |
| File( | |
| p.join(ctx.flutterAssetDir, 'sandbox_runtime.js'), | |
| ).writeAsStringSync('$flutterJs\n$sandboxRuntimeJs'); | |
| // Copy worker from Dart DartPad SDK. | |
| print('Copying worker...'); | |
| for (final f in [ | |
| 'devtools.html', | |
| 'worker.js', | |
| 'worker.wasm', | |
| 'worker.wasm.map', | |
| ]) { | |
| _copyFile(p.join(ctx.dartDartPadSdk, f), p.join(ctx.flutterAssetDir, f)); | |
| } | |
| copyDirectoryContents( | |
| p.join(ctx.dartDartPadSdk, 'devtools'), | |
| p.join(ctx.flutterAssetDir, 'devtools'), | |
| ); | |
| // Extract exact hosted package versions from `flutter pub deps --json` | |
| final depsJson = _runSync(ctx.flutterBin, [ | |
| 'pub', | |
| 'deps', | |
| '--json', | |
| ], ctx.myappDir); | |
| // 4. Create sdk.tar | |
| print('Building sdk.tar...'); | |
| final tar = tarWritingSink( | |
| File(p.join(ctx.flutterAssetDir, 'sdk.tar')).openWrite(), | |
| ); | |
| final defaultBootstrapCode = File( | |
| p.join(ctx.assetDir, 'bootstrap_flutter.dart.tmpl'), | |
| ).readAsStringSync(); | |
| tar.addJsonFile( | |
| target: DartPadConfig.defaultDartPadConfigPath, | |
| json: DartPadConfig( | |
| dartSdkPath: '/sdk/bin/cache/dart-sdk', | |
| flutterSdkPath: '/sdk', | |
| summaryModules: { | |
| '/sdk/bin/cache/flutter_web_sdk/kernel/flutter_web.dill': 'flutter_web', | |
| }, | |
| modes: [ | |
| DartPadRunMode(mode: 'console'), | |
| DartPadRunMode( | |
| mode: 'flutter', | |
| entrypointWrapperTemplate: ctx.bootstrapCode ?? defaultBootstrapCode, | |
| libraries: flutterWebLibraries, | |
| ), | |
| ], | |
| trackCreationLocations: true, | |
| ), | |
| ); | |
| print('Adding Dart SDK lib...'); | |
| // Note: `lib/_internal/js_runtime/` MUST be retained because | |
| // `sdk/lib/_internal/sdk_library_metadata/lib/libraries.dart` maps | |
| // `dart:_interceptors`, `dart:_native_typed_data`, and `dart:_js_helper` | |
| // (used by `dart:js_interop` and `dart:html` in `FolderBasedDartSdk`) | |
| // to `_internal/js_runtime/lib/...`. Conversely, `js_dev_runtime/` has | |
| // zero entries in `libraries.dart` and DDC uses `ddc_outline.dill`. | |
| const excludedInternalDirs = [ | |
| '_internal/vm/', | |
| '_internal/vm_shared/', | |
| '_internal/wasm/', | |
| '_internal/js_dev_runtime/', | |
| ]; | |
| tar.addDirectory( | |
| target: '/sdk/bin/cache/dart-sdk/lib', | |
| source: p.join(webSdk.dartSdkRoot, 'lib'), | |
| where: (f) { | |
| if (excludedInternalDirs.any(f.startsWith)) return false; | |
| return (f.endsWith('.dart') || | |
| f.endsWith('.json') || | |
| f.contains('_internal/')) && | |
| !f.endsWith('.dill'); | |
| }, | |
| ); | |
| print('Adding version and libraries'); | |
| tar.addFile( | |
| target: '/sdk/bin/cache/flutter.version.json', | |
| source: p.join(ctx.flutterRoot, 'bin/cache/flutter.version.json'), | |
| ); | |
| tar.addFile( | |
| target: '/sdk/bin/cache/libraries.json', | |
| source: p.join(webSdk.dartSdkRoot, 'lib', 'libraries.json'), | |
| ); | |
| tar.addFile( | |
| target: '/sdk/bin/cache/dart-sdk/version', | |
| source: p.join(webSdk.dartSdkRoot, 'version'), | |
| ); | |
| tar.addFile( | |
| target: '/sdk/bin/cache/dart-sdk/revision', | |
| source: p.join(webSdk.dartSdkRoot, 'revision'), | |
| ); | |
| // Add the ddc_outline.dill which contains dart:ui | |
| tar.addFile( | |
| target: '/sdk/bin/cache/dart-sdk/lib/_internal/ddc_outline.dill', | |
| source: webSdk.outlineDill, | |
| ); | |
| // Add the framework outline dill we just built | |
| tar.addFile( | |
| target: '/sdk/bin/cache/flutter_web_sdk/kernel/flutter_web.dill', | |
| source: outputDillPath, | |
| ); | |
| for (final pkg in [ | |
| 'flutter', | |
| 'flutter_web_plugins', | |
| 'flutter_localizations', | |
| 'flutter_test', | |
| 'integration_test', | |
| 'flutter_driver', | |
| 'fuchsia_remote_debug_protocol', | |
| ]) { | |
| print('Adding package:$pkg for analysis...'); | |
| tar.addDirectory( | |
| target: '/sdk/packages/$pkg', | |
| source: p.join(ctx.flutterRoot, 'packages', pkg), | |
| where: (f) => | |
| !f.startsWith('test/') && | |
| !f.endsWith('.arb') && | |
| ((pkg != 'flutter' && f == 'pubspec.yaml') || f.startsWith('lib/')), | |
| ); | |
| } | |
| // Pin all hosted dependencies baked into `flutter_web.dill` (including | |
| // `material_ui` and `cupertino_ui`) in `/sdk/packages/flutter/pubspec.yaml` | |
| // so `pub get` in the worker resolves the exact precompiled versions, and | |
| // pre-populate `/pub-cache` so `package:pub` does not need to download them. | |
| final hostedVersions = _extractHostedPackageVersions(depsJson); | |
| for (final MapEntry(key: name, value: version) in hostedVersions.entries) { | |
| final rootPath = packageRootPaths[name]; | |
| if (rootPath == null) { | |
| throw StateError( | |
| 'Missing package_config.json rootPath for hosted package $name', | |
| ); | |
| } | |
| final pubCacheRoot = p.dirname(p.dirname(p.dirname(rootPath))); | |
| final hashFile = p.join( | |
| pubCacheRoot, | |
| 'hosted-hashes', | |
| 'pub.dev', | |
| '$name-$version.sha256', | |
| ); | |
| if (!File(hashFile).existsSync()) { | |
| throw StateError('Missing pub cache hash file at $hashFile'); | |
| } | |
| print('Pre-populating /pub-cache with package:$name ($version)...'); | |
| tar.addDirectory( | |
| target: '/pub-cache/hosted/pub.dev/$name-$version', | |
| source: rootPath, | |
| where: (f) => | |
| f == 'pubspec.yaml' || (f.startsWith('lib/') && f.endsWith('.dart')), | |
| ); | |
| tar.addFile( | |
| target: '/pub-cache/hosted-hashes/pub.dev/$name-$version.sha256', | |
| source: hashFile, | |
| ); | |
| } | |
| final flutterPubspec = _yamlToJson( | |
| File( | |
| p.join(ctx.flutterRoot, 'packages', 'flutter', 'pubspec.yaml'), | |
| ).readAsStringSync(), | |
| ); | |
| flutterPubspec['dependencies'] = { | |
| ...(flutterPubspec['dependencies'] as Map<String, Object?>? ?? {}), | |
| ...hostedVersions, | |
| }; | |
| tar.addJsonFile( | |
| target: '/sdk/packages/flutter/pubspec.yaml', | |
| json: flutterPubspec, | |
| ); | |
| // Add sky_engine | |
| tar.addDirectory( | |
| target: '/sdk/bin/cache/pkg/sky_engine', | |
| source: p.join(ctx.flutterRoot, 'bin/cache/pkg/sky_engine'), | |
| where: (f) { | |
| if (excludedInternalDirs.any((d) => f.startsWith('lib/$d'))) { | |
| return false; | |
| } | |
| return f.endsWith('pubspec.yaml') || f.startsWith('lib/'); | |
| }, | |
| ); | |
| await tar.close(); | |
| final dartVersionInfo = VersionInfo.fromJson( | |
| jsonDecode( | |
| File(p.join(ctx.dartDartPadSdk, 'version.json')).readAsStringSync(), | |
| ) | |
| as Map<String, Object?>, | |
| ); | |
| final flutterVersionJson = | |
| jsonDecode( | |
| File( | |
| p.join(ctx.flutterRoot, 'bin', 'cache', 'flutter.version.json'), | |
| ).readAsStringSync(), | |
| ) | |
| as Map<String, Object?>; | |
| final versionInfo = createVersionInfo( | |
| workerProtocolMajor: dartVersionInfo.workerProtocolMajor, | |
| workerProtocolMinor: dartVersionInfo.workerProtocolMinor, | |
| modes: ['console', 'flutter'], | |
| dartVersion: File( | |
| p.join(webSdk.dartSdkRoot, 'version'), | |
| ).readAsStringSync().trim(), | |
| dartRevision: File( | |
| p.join(webSdk.dartSdkRoot, 'revision'), | |
| ).readAsStringSync().trim(), | |
| properties: extractFlutterVersionProperties(flutterVersionJson), | |
| ); | |
| File(p.join(ctx.flutterAssetDir, 'version.json')).writeAsStringSync( | |
| '${const JsonEncoder.withIndent(' ').convert(versionInfo.toJson())}\n', | |
| ); | |
| print('\nSuccessfully built Flutter DartPad SDK in ${ctx.flutterAssetDir}'); | |
| } | |
| final class _WebSdk { | |
| final String outlineDill; | |
| final String dartSdkJs; | |
| final String dartSdkRoot; | |
| _WebSdk({ | |
| required this.outlineDill, | |
| required this.dartSdkJs, | |
| required this.dartSdkRoot, | |
| }); | |
| } | |
| _WebSdk _copyWebSdk(_BuildContext ctx) { | |
| final kernel = p.join( | |
| ctx.flutterRoot, | |
| 'bin', | |
| 'cache', | |
| 'flutter_web_sdk', | |
| 'kernel', | |
| ); | |
| // Flutter 3.43+ (flutter/flutter#193193) moved the DDC library bundle SDK | |
| // from `ddcLibraryBundle-canvaskit/dart_sdk.js` to `ddc/canary/dart_sdk.js`. | |
| final canaryDartSdkJs = p.join(kernel, 'ddc', 'canary', 'dart_sdk.js'); | |
| final legacyDartSdkJs = p.join( | |
| kernel, | |
| 'ddcLibraryBundle-canvaskit', | |
| 'dart_sdk.js', | |
| ); | |
| return _WebSdk( | |
| outlineDill: p.join(kernel, 'ddc_outline.dill'), | |
| dartSdkJs: File(canaryDartSdkJs).existsSync() | |
| ? canaryDartSdkJs | |
| : legacyDartSdkJs, | |
| dartSdkRoot: p.join(ctx.flutterRoot, 'bin', 'cache', 'dart-sdk'), | |
| ); | |
| } | |
| _WebSdk _buildWebSdk(_BuildContext ctx, String pkgConfigPath) { | |
| print('Building web SDK...'); | |
| final flutterWebSdk = p.join( | |
| ctx.flutterRoot, | |
| 'bin', | |
| 'cache', | |
| 'flutter_web_sdk', | |
| ); | |
| final outputDir = p.join(ctx.tempDir, 'websdk'); | |
| Directory(outputDir).createSync(recursive: true); | |
| final flutterLibSpec = jsonDecode( | |
| File(p.join(flutterWebSdk, 'libraries.json')).readAsStringSync(), | |
| ); | |
| final flutterLibraries = | |
| ((flutterLibSpec as Map)['dartdevc'] as Map)['libraries'] as Map; | |
| File(p.join(outputDir, 'dartpad_libraries.json')).writeAsStringSync( | |
| jsonEncode({ | |
| 'dartdevc': { | |
| 'include': [ | |
| {'path': 'lib/libraries.json', 'target': 'dartdevc'}, | |
| ], | |
| 'libraries': flutterLibraries, | |
| }, | |
| }), | |
| ); | |
| final libraries = [ | |
| 'dart:core', | |
| ...flutterLibraries.keys.map((library) => 'dart:$library'), | |
| ]; | |
| final fileSystem = [ | |
| '--multi-root=$outputDir${p.separator}', | |
| '--multi-root=${ctx.dartSdkRoot}${p.separator}', | |
| '--multi-root=$flutterWebSdk${p.separator}', | |
| '--multi-root-scheme=org-dartlang-sdk', | |
| '--libraries-file=org-dartlang-sdk:///dartpad_libraries.json', | |
| ]; | |
| final outlineDill = p.join(outputDir, 'ddc_outline.dill'); | |
| _runSync(ctx.dartAotRuntimeBin, [ | |
| p.join( | |
| ctx.dartSdkRoot, | |
| 'bin', | |
| 'snapshots', | |
| 'kernel_worker_aot.dart.snapshot', | |
| ), | |
| '--target=ddc', | |
| '--summary-only', | |
| '--include-unsupported-platform-library-stubs', | |
| ...fileSystem, | |
| '--packages-file=$pkgConfigPath', | |
| '--output=$outlineDill', | |
| ...libraries.expand((library) => ['--source', library]), | |
| ], ctx.tempDir); | |
| final dartSdkJs = p.join(outputDir, 'dart_sdk.js'); | |
| _runSync(ctx.dartBin, [ | |
| p.join(ctx.dartSdkRoot, 'bin', 'snapshots', 'dartdevc.dart.snapshot'), | |
| '--compile-sdk', | |
| '--modules=ddc', | |
| '--canary', | |
| '--no-summarize', | |
| '-DFLUTTER_WEB_USE_SKIA=true', | |
| ...fileSystem, | |
| '--packages=$pkgConfigPath', | |
| '-o', | |
| dartSdkJs, | |
| ...libraries, | |
| ], ctx.tempDir); | |
| return _WebSdk( | |
| outlineDill: outlineDill, | |
| dartSdkJs: dartSdkJs, | |
| dartSdkRoot: ctx.dartSdkRoot, | |
| ); | |
| } | |
| String _runSync(String command, List<String> args, String workingDir) { | |
| final result = Process.runSync(command, args, workingDirectory: workingDir); | |
| if (result.exitCode != 0) { | |
| print('Command failed: $command ${args.join(' ')}'); | |
| print('stdout: ${result.stdout}'); | |
| print('stderr: ${result.stderr}'); | |
| throw StateError('Command failed: $command'); | |
| } | |
| return result.stdout.toString(); | |
| } | |
| void _copyFile(String source, String dest) => File(source).copySync(dest); | |
| Map<String, String> _extractHostedPackageVersions(String depsJson) { | |
| final data = jsonDecode(depsJson) as Map<String, Object?>; | |
| final packages = data['packages'] as List<Object?>; | |
| return { | |
| for (final pkg in packages) | |
| if (pkg is Map && pkg['source'] == 'hosted') | |
| pkg['name'] as String: pkg['version'] as String, | |
| }; | |
| } | |
| Map<String, Object?> _yamlToJson(String yamlString) => | |
| jsonDecode(jsonEncode(loadYaml(yamlString))) as Map<String, Object?>; | |
| extension on StreamSink<TarEntry> { | |
| void addFile({required String target, required String source}) => add( | |
| TarEntry.data( | |
| TarHeader(name: target, mode: 420), | |
| File(source).readAsBytesSync(), | |
| ), | |
| ); | |
| void addTextFile({required String target, required String text}) => | |
| add(TarEntry.data(TarHeader(name: target, mode: 420), utf8.encode(text))); | |
| void addJsonFile({required String target, required Object? json}) => | |
| addTextFile(target: target, text: jsonEncode(json)); | |
| void addDirectory({ | |
| required String source, | |
| required String target, | |
| bool Function(String path)? where, | |
| }) { | |
| final s = Directory(source); | |
| if (!s.existsSync()) return; | |
| for (final f in s.listSync(recursive: true).whereType<File>()) { | |
| final relative = p.posix.joinAll( | |
| p.split(p.relative(f.path, from: source)), | |
| ); | |
| if (where != null && !where(relative)) continue; | |
| add( | |
| TarEntry.data( | |
| TarHeader(name: p.posix.join(target, relative), mode: 420), | |
| f.readAsBytesSync(), | |
| ), | |
| ); | |
| } | |
| } | |
| } | |
| Future<String> _resolveFlutterExecutable() async { | |
| final command = Platform.isWindows ? 'where' : 'which'; | |
| final result = await Process.run(command, ['flutter']); | |
| if (result.exitCode != 0 || result.stdout.toString().trim().isEmpty) { | |
| throw StateError('Flutter not found in PATH'); | |
| } | |
| return result.stdout.toString().split('\n').first.trim(); | |
| } | |
| Future<String> _resolveDartPadPackageRoot() async { | |
| final uri = await Isolate.resolvePackageUri( | |
| Uri.parse('package:dartpad/dartpad.dart'), | |
| ); | |
| if (uri == null) { | |
| throw StateError('Unable to resolve package:dartpad.'); | |
| } | |
| return p.dirname(p.dirname(p.fromUri(uri))); | |
| } | |
| const _kRequiredCanvasKitFiles = [ | |
| 'canvaskit.js', | |
| 'canvaskit.wasm', | |
| 'chromium/canvaskit.js', | |
| 'chromium/canvaskit.wasm', | |
| ]; | |
| void _verifyLocalCanvasKitFiles(String canvasKitDir) { | |
| for (final relPath in _kRequiredCanvasKitFiles) { | |
| final file = File(p.joinAll([canvasKitDir, ...relPath.split('/')])); | |
| if (!file.existsSync() || file.lengthSync() == 0) { | |
| throw StateError('Missing or empty local CanvasKit file: ${file.path}'); | |
| } | |
| } | |
| } | |
| Future<String> _resolveAndVerifyCanvasKitCdnUrl( | |
| String flutterRoot, | |
| String myappDir, | |
| ) async { | |
| final versionFile = File( | |
| p.join(flutterRoot, 'bin', 'cache', 'flutter.version.json'), | |
| ); | |
| if (!versionFile.existsSync()) { | |
| throw StateError('Missing ${versionFile.path}'); | |
| } | |
| final versionJson = | |
| jsonDecode(versionFile.readAsStringSync()) as Map<String, Object?>; | |
| final engineRevision = versionJson['engineRevision'] as String?; | |
| if (engineRevision == null || | |
| !RegExp(r'^[0-9a-f]{40}$').hasMatch(engineRevision)) { | |
| throw StateError( | |
| 'Invalid or missing engineRevision in ${versionFile.path}: ' | |
| '$engineRevision', | |
| ); | |
| } | |
| // Cross-check against build/web/flutter_bootstrap.js generated by | |
| // `flutter build web` to ensure flutter_tools agrees on engineRevision. | |
| final bootstrapFile = File( | |
| p.join(myappDir, 'build', 'web', 'flutter_bootstrap.js'), | |
| ); | |
| if (bootstrapFile.existsSync()) { | |
| final bootstrapContent = bootstrapFile.readAsStringSync(); | |
| final match = RegExp( | |
| r'"engineRevision"\s*:\s*"([0-9a-f]{40})"', | |
| ).firstMatch(bootstrapContent); | |
| if (match != null && match.group(1) != engineRevision) { | |
| throw StateError( | |
| 'Engine revision mismatch between flutter.version.json ' | |
| '($engineRevision) and flutter_bootstrap.js (${match.group(1)})', | |
| ); | |
| } | |
| } | |
| final baseUrl = 'https://www.gstatic.com/flutter-canvaskit/$engineRevision/'; | |
| print('Verifying CanvasKit CDN resources at $baseUrl...'); | |
| final client = http.Client(); | |
| try { | |
| for (final relPath in _kRequiredCanvasKitFiles) { | |
| final uri = Uri.parse('$baseUrl$relPath'); | |
| final response = await client.head(uri); | |
| final contentLength = int.tryParse( | |
| response.headers['content-length'] ?? '', | |
| ); | |
| if (response.statusCode != 200 || | |
| (contentLength != null && contentLength <= 0)) { | |
| throw StateError( | |
| 'CanvasKit CDN verification failed for $uri ' | |
| '(HTTP ${response.statusCode}, content-length: $contentLength). ' | |
| 'Pass --no-use-cdn to bundle local CanvasKit files instead.', | |
| ); | |
| } | |
| } | |
| } finally { | |
| client.close(); | |
| } | |
| return baseUrl; | |
| } |