|
| 1 | +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file |
| 2 | +// for details. All rights reserved. Use of this source code is governed by a |
| 3 | +// BSD-style license that can be found in the LICENSE file. |
| 4 | + |
| 5 | +// This script builds a local Dart SDK and copies the contents to the cache of |
| 6 | +// a local Flutter SDK. This script is helpful for testing local SDK changes, |
| 7 | +// such as Dart Analysis Server changes for example, against a Flutter project. |
| 8 | +// |
| 9 | +// Note: this script will not be sufficient if the local Dart SDK changes also |
| 10 | +// need to be included in the build of the Flutter enginge. There are no |
| 11 | +// guarantees you will be able to run a Flutter app with these changes applied. |
| 12 | +// This script is mainly useful for testing static features, like static |
| 13 | +// analysis. |
| 14 | +// |
| 15 | +// For ease of use, consider setting the LOCAL_DART_SDK and LOCAL_FLUTTER_SDK |
| 16 | +// environment variables. Otherwise, you will need to specify these paths via |
| 17 | +// the -d and -f options when running this script. You can add the following to |
| 18 | +// your .bash_profile or .zshrc file to set the environment variables: |
| 19 | +// |
| 20 | +// export LOCAL_DART_SDK='/Users/me/path/to/dart-sdk/sdk' |
| 21 | +// export LOCAL_FLUTTER_SDK='/Users/me/path/to/flutter' |
| 22 | + |
| 23 | +import 'dart:async'; |
| 24 | +import 'dart:convert'; |
| 25 | +import 'dart:io'; |
| 26 | + |
| 27 | +import 'package:args/args.dart'; |
| 28 | +import 'package:path/path.dart' as path; |
| 29 | + |
| 30 | +const _architecture = 'arch'; |
| 31 | +const _buildSdk = 'build-sdk'; |
| 32 | +const _help = 'help'; |
| 33 | +const _localDart = 'local-dart'; |
| 34 | +const _localFlutter = 'local-flutter'; |
| 35 | +const _reset = 'reset'; |
| 36 | +const _verbose = 'verbose'; |
| 37 | + |
| 38 | +final _parser = |
| 39 | + ArgParser() |
| 40 | + ..addOption( |
| 41 | + _architecture, |
| 42 | + abbr: 'a', |
| 43 | + help: 'Specify your machine\'s architecture.', |
| 44 | + allowed: ['ARM64', 'X64'], |
| 45 | + defaultsTo: 'ARM64', |
| 46 | + ) |
| 47 | + ..addFlag( |
| 48 | + _buildSdk, |
| 49 | + negatable: true, |
| 50 | + defaultsTo: true, |
| 51 | + help: |
| 52 | + 'Whether to build the Dart SDK as part of running this script. ' |
| 53 | + 'Negate with --no-$_buildSdk if you have already built the Dart ' |
| 54 | + 'SDK locally and want to skip this step.', |
| 55 | + ) |
| 56 | + ..addOption( |
| 57 | + _localDart, |
| 58 | + abbr: 'd', |
| 59 | + help: |
| 60 | + 'Path to your local Dart SDK directory. If unspecified, this value ' |
| 61 | + 'will default to the value of the LOCAL_DART_SDK environment ' |
| 62 | + 'variable.', |
| 63 | + valueHelp: '/Users/me/path/to/dart-sdk/sdk', |
| 64 | + ) |
| 65 | + ..addOption( |
| 66 | + _localFlutter, |
| 67 | + abbr: 'f', |
| 68 | + help: |
| 69 | + 'Path to your local Flutter SDK directory. If unspecified, this ' |
| 70 | + 'value will default to the value of the LOCAL_FLUTTER_SDK ' |
| 71 | + 'environment variable.', |
| 72 | + valueHelp: '/Users/me/path/to/flutter', |
| 73 | + ) |
| 74 | + ..addFlag( |
| 75 | + _verbose, |
| 76 | + negatable: false, |
| 77 | + abbr: 'v', |
| 78 | + help: |
| 79 | + 'Run the script with verbose output, which will forward the stdout ' |
| 80 | + 'and stderr of all sub-processes.', |
| 81 | + ) |
| 82 | + ..addFlag( |
| 83 | + _help, |
| 84 | + negatable: false, |
| 85 | + abbr: 'h', |
| 86 | + help: 'Show the program usage.', |
| 87 | + ) |
| 88 | + ..addSeparator('Additional commands') |
| 89 | + ..addFlag( |
| 90 | + _reset, |
| 91 | + negatable: false, |
| 92 | + help: |
| 93 | + 'Reset your local Flutter SDK cache to undo the effects of running ' |
| 94 | + 'this script.', |
| 95 | + ); |
| 96 | + |
| 97 | +void main(List<String> args) async { |
| 98 | + if (Platform.isWindows) { |
| 99 | + throw Exception('This script is not currently supported for Windows.'); |
| 100 | + } |
| 101 | + |
| 102 | + final options = _parser.parse(args); |
| 103 | + if (options.flag(_help)) { |
| 104 | + print(_parser.usage); |
| 105 | + exit(0); |
| 106 | + } |
| 107 | + |
| 108 | + _verboseOutput = options.flag(_verbose); |
| 109 | + |
| 110 | + final reset = options.flag(_reset); |
| 111 | + if (reset) { |
| 112 | + await _resetLocalFlutterSdk(options); |
| 113 | + exit(0); |
| 114 | + } |
| 115 | + |
| 116 | + var localDartSdk = |
| 117 | + options.option(_localDart) ?? Platform.environment['LOCAL_DART_SDK']; |
| 118 | + var localFlutterSdk = |
| 119 | + options.option(_localFlutter) ?? |
| 120 | + Platform.environment['LOCAL_FLUTTER_SDK']; |
| 121 | + if (localDartSdk == null || localFlutterSdk == null) { |
| 122 | + stderr.writeln( |
| 123 | + 'Error: either the --$_localDart and --$_localFlutter arguments must be ' |
| 124 | + 'passed or the LOCAL_DART_SDK and LOCAL_FLUTTER_SDK environment ' |
| 125 | + 'variables must be set.', |
| 126 | + ); |
| 127 | + exit(1); |
| 128 | + } |
| 129 | + localDartSdk = _maybeRemoveTrailingSlash(localDartSdk); |
| 130 | + localFlutterSdk = _maybeRemoveTrailingSlash(localFlutterSdk); |
| 131 | + |
| 132 | + if (options.flag(_buildSdk)) { |
| 133 | + stdout.writeln('Building the Dart SDK...'); |
| 134 | + await _runCommand('./tools/build.py', [ |
| 135 | + '-mrelease', |
| 136 | + 'create_sdk', |
| 137 | + ], workingDirectory: localDartSdk); |
| 138 | + } |
| 139 | + |
| 140 | + await _deleteDartSdkInFlutterCache(localFlutterSdk); |
| 141 | + |
| 142 | + // Copy the built Dart SDK to the Flutter SDK cache. |
| 143 | + String outDirectory; |
| 144 | + if (Platform.isMacOS) { |
| 145 | + outDirectory = 'xcodebuild'; |
| 146 | + } else if (Platform.isLinux) { |
| 147 | + outDirectory = 'out'; |
| 148 | + } else { |
| 149 | + outDirectory = 'unsupported'; |
| 150 | + } |
| 151 | + final builtDartSdkPath = path.join( |
| 152 | + localDartSdk, |
| 153 | + outDirectory, |
| 154 | + 'Release${options.option(_architecture)}', |
| 155 | + 'dart-sdk', |
| 156 | + ); |
| 157 | + final flutterCacheDartSdkPath = _flutterCachePrefix( |
| 158 | + 'dart-sdk', |
| 159 | + localFlutterSdk: localFlutterSdk, |
| 160 | + ); |
| 161 | + stdout.writeln( |
| 162 | + 'Copying the built Dart SDK at $builtDartSdkPath to the Flutter SDK cache ' |
| 163 | + 'at $flutterCacheDartSdkPath...', |
| 164 | + ); |
| 165 | + await _runCommand('cp', ['-R', builtDartSdkPath, flutterCacheDartSdkPath]); |
| 166 | + |
| 167 | + // Delete and regenerate the Flutter tools snapshot file so that Flutter tools |
| 168 | + // will rebuild the snapshot with your local Dart SDK changes. |
| 169 | + final flutterToolsSnapshotPath = _flutterCachePrefix( |
| 170 | + 'flutter_tools.snapshot', |
| 171 | + localFlutterSdk: localFlutterSdk, |
| 172 | + ); |
| 173 | + await _runCommand('rm', [flutterToolsSnapshotPath]); |
| 174 | + stdout.writeln('Regenerating the $flutterToolsSnapshotPath file...'); |
| 175 | + await _runCommand(path.join(localFlutterSdk, 'bin', 'flutter'), [ |
| 176 | + '--version', |
| 177 | + ]); |
| 178 | + |
| 179 | + stdout.writeln( |
| 180 | + 'Finished copying local Dart SDK build to the local Flutter SDK.\n\nTo ' |
| 181 | + 'reset your local Flutter SDK state, run: ' |
| 182 | + 'dart tools/copy_dart_to_flutter.dart --reset.', |
| 183 | + ); |
| 184 | +} |
| 185 | + |
| 186 | +Future<void> _resetLocalFlutterSdk(ArgResults options) async { |
| 187 | + var localFlutterSdk = |
| 188 | + options.option(_localFlutter) ?? |
| 189 | + Platform.environment['LOCAL_FLUTTER_SDK']; |
| 190 | + if (localFlutterSdk == null) { |
| 191 | + stderr.writeln( |
| 192 | + 'Error: either the --$_localFlutter argument must be passed or the ' |
| 193 | + 'LOCAL_FLUTTER_SDK environment variable must be set.', |
| 194 | + ); |
| 195 | + exit(1); |
| 196 | + } |
| 197 | + |
| 198 | + await _deleteDartSdkInFlutterCache(localFlutterSdk); |
| 199 | + final flutterToolsSnapshotPath = _flutterCachePrefix( |
| 200 | + 'flutter_tools.snapshot', |
| 201 | + localFlutterSdk: localFlutterSdk, |
| 202 | + ); |
| 203 | + final flutterToolsStampPath = _flutterCachePrefix( |
| 204 | + 'flutter_tools.stamp', |
| 205 | + localFlutterSdk: localFlutterSdk, |
| 206 | + ); |
| 207 | + final engineStampPath = _flutterCachePrefix( |
| 208 | + 'engine-dart-sdk.stamp', |
| 209 | + localFlutterSdk: localFlutterSdk, |
| 210 | + ); |
| 211 | + await _runCommand('rm', [ |
| 212 | + flutterToolsSnapshotPath, |
| 213 | + flutterToolsStampPath, |
| 214 | + engineStampPath, |
| 215 | + ]); |
| 216 | + // Regenerate the local Flutter cache with the original values. |
| 217 | + await _runCommand(path.join(localFlutterSdk, 'bin', 'flutter'), [ |
| 218 | + '--version', |
| 219 | + ]); |
| 220 | + |
| 221 | + stdout.writeln('Finished restting your local Flutter SDK cache.'); |
| 222 | +} |
| 223 | + |
| 224 | +Future<void> _deleteDartSdkInFlutterCache(String localFlutterSdk) async { |
| 225 | + final flutterCacheDartSdkPath = _flutterCachePrefix( |
| 226 | + 'dart-sdk', |
| 227 | + localFlutterSdk: localFlutterSdk, |
| 228 | + ); |
| 229 | + stdout.writeln( |
| 230 | + 'Deleting the Dart SDK in the Flutter SDK cache at ' |
| 231 | + '$flutterCacheDartSdkPath...', |
| 232 | + ); |
| 233 | + await _runCommand('rm', ['-rf', flutterCacheDartSdkPath]); |
| 234 | +} |
| 235 | + |
| 236 | +String _flutterCachePrefix(String value, {required String localFlutterSdk}) => |
| 237 | + path.join(localFlutterSdk, 'bin', 'cache', value); |
| 238 | + |
| 239 | +String _maybeRemoveTrailingSlash(String path) { |
| 240 | + if (path.endsWith('/')) { |
| 241 | + return path.substring(0, path.length - 1); |
| 242 | + } else { |
| 243 | + return path; |
| 244 | + } |
| 245 | +} |
| 246 | + |
| 247 | +/// Top level variable to track whether the --verbose flag was specified. |
| 248 | +/// |
| 249 | +/// Tracked as a top level variable so that it does not have to be passed as a |
| 250 | +/// parameter everywhere. |
| 251 | +var _verboseOutput = false; |
| 252 | + |
| 253 | +/// Runs a command in a sub-process and optionally forwards stdout and stderr to |
| 254 | +/// the main process running this script. |
| 255 | +/// |
| 256 | +/// If a sub-process exits with a non-zero exit code, the main process will |
| 257 | +/// exit. |
| 258 | +Future<void> _runCommand( |
| 259 | + String executable, |
| 260 | + List<String> arguments, { |
| 261 | + String? workingDirectory, |
| 262 | +}) async { |
| 263 | + stdout.writeln( |
| 264 | + '${workingDirectory != null ? '$workingDirectory ' : ''}' |
| 265 | + '> $executable ${arguments.join(' ')}', |
| 266 | + ); |
| 267 | + final process = await Process.start( |
| 268 | + executable, |
| 269 | + arguments, |
| 270 | + workingDirectory: workingDirectory, |
| 271 | + ); |
| 272 | + if (_verboseOutput) { |
| 273 | + process.stdout.transform(utf8.decoder).listen(stdout.write); |
| 274 | + process.stderr.transform(utf8.decoder).listen(stderr.write); |
| 275 | + } |
| 276 | + final exitCode = await process.exitCode; |
| 277 | + if (exitCode != 0) { |
| 278 | + exit(exitCode); |
| 279 | + } |
| 280 | +} |
0 commit comments