| // Copyright (c) 2015, 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. |
| |
| library services.grind; |
| |
| import 'dart:async'; |
| import 'dart:convert' show jsonDecode, JsonEncoder; |
| import 'dart:io'; |
| |
| import 'package:dart_services/src/project.dart'; |
| import 'package:dart_services/src/pub.dart'; |
| import 'package:dart_services/src/sdk.dart'; |
| import 'package:grinder/grinder.dart'; |
| import 'package:grinder/src/run_utils.dart' show mergeWorkingDirectory; |
| import 'package:http/http.dart' as http; |
| import 'package:path/path.dart' as path; |
| |
| Future<void> main(List<String> args) async { |
| return grind(args); |
| } |
| |
| @Task('Make sure SDKs are appropriately initialized') |
| @Depends(setupFlutterSdk) |
| void sdkInit() {} |
| |
| @Task() |
| @Depends(buildProjectTemplates) |
| void analyze() async { |
| await runWithLogging('dart', arguments: ['analyze']); |
| } |
| |
| @Task() |
| @Depends(buildStorageArtifacts) |
| Future<dynamic> test() => |
| runWithLogging(Platform.executable, arguments: ['test']); |
| |
| @DefaultTask() |
| @Depends(analyze, test) |
| void analyzeTest() {} |
| |
| @Task() |
| @Depends(buildStorageArtifacts) |
| Future<void> serve() async { |
| await runWithLogging(Platform.executable, arguments: [ |
| 'bin/server_dev.dart', |
| '--channel', |
| _channel, |
| '--port', |
| '8082', |
| ]); |
| } |
| |
| @Task() |
| @Depends(buildStorageArtifacts) |
| Future<void> serveNullSafety() async { |
| await runWithLogging(Platform.executable, arguments: [ |
| 'bin/server_dev.dart', |
| '--channel', |
| _channel, |
| '--port', |
| '8084', |
| '--null-safety', |
| ]); |
| } |
| |
| const _dartImageName = 'dart'; |
| final _dockerVersionMatcher = RegExp('^FROM $_dartImageName:(.*)\$'); |
| const _dockerFileNames = [ |
| 'cloud_run.Dockerfile', |
| 'cloud_run_null_safety.Dockerfile' |
| ]; |
| |
| /// Returns the Flutter channel provided in environment variables. |
| late final String _channel = () { |
| final channel = Platform.environment['FLUTTER_CHANNEL']; |
| if (channel == null) { |
| throw StateError('Must provide FLUTTER_CHANNEL'); |
| } |
| return channel; |
| }(); |
| |
| /// Returns the appropriate SDK for the given Flutter channel. |
| /// |
| /// The Flutter SDK directory must be already created by [sdkInit]. |
| Sdk _getSdk() => Sdk.create(_channel); |
| |
| @Task('Update the docker and SDK versions') |
| void updateDockerVersion() { |
| final platformVersion = Platform.version.split(' ').first; |
| for (final _dockerFileName in _dockerFileNames) { |
| final dockerFile = File(_dockerFileName); |
| final dockerImageLines = dockerFile.readAsLinesSync().map((String s) { |
| if (s.contains(_dockerVersionMatcher)) { |
| return 'FROM $_dartImageName:$platformVersion'; |
| } |
| return s; |
| }).toList(); |
| dockerImageLines.add(''); |
| |
| dockerFile.writeAsStringSync(dockerImageLines.join('\n')); |
| } |
| } |
| |
| final List<String> compilationArtifacts = [ |
| 'dart_sdk.js', |
| 'flutter_web.js', |
| ]; |
| |
| @Task('validate that we have the correct compilation artifacts available in ' |
| 'google storage') |
| @Depends(sdkInit) |
| void validateStorageArtifacts() async { |
| final sdk = _getSdk(); |
| print('validate-storage-artifacts version: ${sdk.version}'); |
| final version = sdk.versionFull; |
| |
| const nullUnsafeUrlBase = |
| 'https://storage.googleapis.com/compilation_artifacts/'; |
| const nullSafeUrlBase = 'https://storage.googleapis.com/nnbd_artifacts/'; |
| |
| for (final urlBase in [nullUnsafeUrlBase, nullSafeUrlBase]) { |
| for (final artifact in compilationArtifacts) { |
| await _validateExists(Uri.parse('$urlBase$version/$artifact')); |
| } |
| } |
| } |
| |
| Future<void> _validateExists(Uri url) async { |
| log('checking $url...'); |
| |
| final response = await http.head(url); |
| if (response.statusCode != 200) { |
| fail( |
| 'compilation artifact not found: $url ' |
| '(${response.statusCode} ${response.reasonPhrase})', |
| ); |
| } |
| } |
| |
| /// Builds the SIX project templates: |
| /// |
| /// * the Dart project template (both null safe and pre-null safe), |
| /// * the Flutter project template (both null safe and pre-null safe), |
| /// * the Firebase project template (both null safe and pre-null safe). |
| @Task('build the project templates') |
| @Depends(sdkInit, updatePubDependencies) |
| void buildProjectTemplates() async { |
| final templatesPath = |
| Directory(path.join(Directory.current.path, 'project_templates')); |
| final exists = await templatesPath.exists(); |
| if (exists) { |
| await templatesPath.delete(recursive: true); |
| } |
| |
| final sdk = _getSdk(); |
| for (final nullSafety in [true, false]) { |
| await _buildDartProjectTemplate( |
| dartSdkPath: sdk.dartSdkPath, |
| nullSafety: nullSafety, |
| channel: _channel, |
| templatePath: templatesPath.path, |
| ); |
| |
| await _buildFlutterProjectTemplate( |
| sdk: sdk, |
| nullSafety: nullSafety, |
| channel: _channel, |
| templatePath: templatesPath.path, |
| includeFirebase: false, |
| ); |
| |
| await _buildFlutterProjectTemplate( |
| sdk: sdk, |
| nullSafety: nullSafety, |
| channel: _channel, |
| templatePath: templatesPath.path, |
| includeFirebase: true, |
| ); |
| } |
| } |
| |
| Map<String, String> _dependencyVersions(Iterable<String> packages, |
| {required String channel}) { |
| final allVersions = _parsePubDependenciesFile(channel: channel); |
| return { |
| for (var package in packages) package: allVersions[package]!, |
| }; |
| } |
| |
| /// Builds a basic Dart project template directory, complete with `pubspec.yaml` |
| /// and `analysis_options.yaml`. |
| Future<void> _buildDartProjectTemplate({ |
| required String dartSdkPath, |
| required bool nullSafety, |
| required String channel, |
| required String templatePath, |
| }) async { |
| final projectPath = Directory(path.join( |
| templatePath, nullSafety ? 'null-safe' : 'null-unsafe', 'dart_project')); |
| final projectDir = await projectPath.create(recursive: true); |
| final dependencies = |
| _dependencyVersions(supportedBasicDartPackages, channel: channel); |
| joinFile(projectDir, ['pubspec.yaml']).writeAsStringSync(createPubspec( |
| includeFlutterWeb: false, |
| nullSafety: nullSafety, |
| dependencies: dependencies, |
| )); |
| await _runDartPubGet(dartSdkPath, projectDir); |
| joinFile(projectDir, ['analysis_options.yaml']).writeAsStringSync(''' |
| include: package:lints/recommended.yaml |
| linter: |
| rules: |
| avoid_print: false |
| '''); |
| } |
| |
| /// Builds a Flutter project template directory, complete with `pubspec.yaml`, |
| /// `analysis_options.yaml`, and `web/index.html`. |
| /// |
| /// Depending on [includeFirebase], Firebase packages are included in |
| /// `pubspec.yaml` which affects how `flutter packages get` will register |
| /// plugins. |
| Future<void> _buildFlutterProjectTemplate({ |
| required Sdk sdk, |
| required bool nullSafety, |
| required String channel, |
| required String templatePath, |
| required bool includeFirebase, |
| }) async { |
| final projectPath = path.join( |
| templatePath, |
| nullSafety ? 'null-safe' : 'null-unsafe', |
| includeFirebase ? 'firebase_project' : 'flutter_project', |
| ); |
| final projectDir = await Directory(projectPath).create(recursive: true); |
| await Directory(path.join(projectPath, 'lib')).create(); |
| await Directory(path.join(projectPath, 'web')).create(); |
| await File(path.join(projectPath, 'web', 'index.html')).create(); |
| var packages = { |
| ...supportedBasicDartPackages, |
| ...supportedFlutterPackages, |
| if (includeFirebase) ...registerableFirebasePackages, |
| }; |
| final dependencies = _dependencyVersions(packages, channel: channel); |
| joinFile(projectDir, ['pubspec.yaml']).writeAsStringSync(createPubspec( |
| includeFlutterWeb: true, |
| nullSafety: nullSafety, |
| dependencies: dependencies, |
| )); |
| await _runFlutterPackagesGet(sdk.flutterToolPath, projectDir); |
| if (includeFirebase) { |
| // `flutter packages get` has been run with a _subset_ of all supported |
| // Firebase packages, the ones that don't require a Firebase app to be |
| // configured in JavaScript, before executing Dart. Now add the full set of |
| // supported Firebase pacakges. This workaround is a very fragile hack. |
| packages = { |
| ...supportedBasicDartPackages, |
| ...supportedFlutterPackages, |
| if (includeFirebase) ...firebasePackages, |
| }; |
| final dependencies = _dependencyVersions(packages, channel: channel); |
| joinFile(projectDir, ['pubspec.yaml']).writeAsStringSync(createPubspec( |
| includeFlutterWeb: true, |
| nullSafety: nullSafety, |
| dependencies: dependencies, |
| )); |
| await _runDartPubGet(sdk.dartSdkPath, projectDir); |
| } |
| joinFile(projectDir, ['analysis_options.yaml']).writeAsStringSync(''' |
| include: package:flutter_lints/flutter.yaml |
| linter: |
| rules: |
| avoid_print: false |
| use_key_in_widget_constructors: false |
| '''); |
| } |
| |
| Future<void> _runDartPubGet(String dartSdkPath, Directory dir) async { |
| log('running dart pub get (${dir.path})'); |
| |
| await runWithLogging( |
| path.join(dartSdkPath, 'bin', 'dart'), |
| arguments: ['pub', 'get'], |
| workingDirectory: dir.path, |
| ); |
| } |
| |
| Future<void> _runFlutterPackagesGet( |
| String flutterToolPath, Directory dir) async { |
| log('running flutter packages get (${dir.path})'); |
| |
| await runWithLogging( |
| flutterToolPath, |
| arguments: ['packages', 'get'], |
| workingDirectory: dir.path, |
| ); |
| } |
| |
| @Task('build the sdk compilation artifacts for upload to google storage') |
| @Depends(sdkInit, buildProjectTemplates) |
| void buildStorageArtifacts() async { |
| final sdk = _getSdk(); |
| delete(getDir('artifacts')); |
| final instructions = <String>[]; |
| |
| for (final nullSafe in [false, true]) { |
| // build and copy dart_sdk.js, flutter_web.js, and flutter_web.dill |
| final temp = Directory.systemTemp.createTempSync('flutter_web_sample'); |
| |
| try { |
| instructions.add(await _buildStorageArtifacts(temp, sdk, |
| nullSafety: nullSafe, channel: _channel)); |
| } finally { |
| temp.deleteSync(recursive: true); |
| } |
| } |
| log('\nFrom the dart-services project root dir, run:'); |
| for (final instruction in instructions) { |
| log(instruction); |
| } |
| } |
| |
| Future<String> _buildStorageArtifacts(Directory dir, Sdk sdk, |
| {required bool nullSafety, required String channel}) async { |
| final pubspec = createPubspec( |
| includeFlutterWeb: true, |
| nullSafety: nullSafety, |
| dependencies: _parsePubDependenciesFile(channel: channel)); |
| joinFile(dir, ['pubspec.yaml']).writeAsStringSync(pubspec); |
| |
| // Run `flutter packages get`. |
| await runWithLogging( |
| sdk.flutterToolPath, |
| arguments: ['packages', 'get'], |
| workingDirectory: dir.path, |
| ); |
| |
| // locate the artifacts |
| final flutterPackages = ['flutter', 'flutter_test']; |
| |
| final flutterLibraries = <String>[]; |
| final packageLines = joinFile(dir, ['.packages']).readAsLinesSync(); |
| for (var line in packageLines) { |
| line = line.trim(); |
| if (line.startsWith('#') || line.isEmpty) { |
| continue; |
| } |
| final index = line.indexOf(':'); |
| if (index == -1) { |
| continue; |
| } |
| final packageName = line.substring(0, index); |
| final url = line.substring(index + 1); |
| if (flutterPackages.contains(packageName)) { |
| // This is a package we're interested in - add all the public libraries to |
| // the list. |
| final libPath = Uri.parse(url).toFilePath(); |
| for (final entity in getDir(libPath).listSync()) { |
| if (entity is File && entity.path.endsWith('.dart')) { |
| flutterLibraries.add('package:$packageName/${fileName(entity)}'); |
| } |
| } |
| } |
| } |
| |
| // Make sure flutter-sdk/bin/cache/flutter_web_sdk/flutter_web_sdk/kernel/flutter_ddc_sdk.dill |
| // is installed. |
| await runWithLogging( |
| sdk.flutterToolPath, |
| arguments: ['precache', '--web'], |
| workingDirectory: dir.path, |
| ); |
| |
| // Build the artifacts using DDC: |
| // dart-sdk/bin/dartdevc -s kernel/flutter_ddc_sdk.dill |
| // --modules=amd package:flutter/animation.dart ... |
| final compilerPath = path.join(sdk.dartSdkPath, 'bin', 'dartdevc'); |
| final dillPath = path.join( |
| sdk.flutterWebSdkPath, |
| nullSafety ? 'flutter_ddc_sdk_sound.dill' : 'flutter_ddc_sdk.dill', |
| ); |
| |
| final args = <String>[ |
| '-s', |
| dillPath, |
| if (nullSafety) ...[ |
| '--sound-null-safety', |
| '--enable-experiment=non-nullable' |
| ], |
| '--modules=amd', |
| '--source-map', |
| '-o', |
| 'flutter_web.js', |
| ...flutterLibraries |
| ]; |
| |
| await runWithLogging( |
| compilerPath, |
| arguments: args, |
| workingDirectory: dir.path, |
| ); |
| |
| // Copy both to the project directory. |
| final artifactsDir = |
| getDir(path.join('artifacts', nullSafety ? 'null-safe' : 'null-unsafe')); |
| artifactsDir.createSync(recursive: true); |
| |
| final sdkJsPath = path.join( |
| sdk.flutterWebSdkPath, |
| nullSafety |
| ? 'amd-canvaskit-html-sound/dart_sdk.js' |
| : 'amd-canvaskit-html/dart_sdk.js'); |
| |
| copy(getFile(sdkJsPath), artifactsDir); |
| copy(getFile('$sdkJsPath.map'), artifactsDir); |
| copy(joinFile(dir, ['flutter_web.js']), artifactsDir); |
| copy(joinFile(dir, ['flutter_web.js.map']), artifactsDir); |
| copy(joinFile(dir, ['flutter_web.dill']), artifactsDir); |
| |
| // Emit some good Google Storage upload instructions. |
| final version = sdk.versionFull; |
| return (' gsutil -h "Cache-Control: public, max-age=604800, immutable" cp -z js ${artifactsDir.path}/*.js*' |
| ' gs://${nullSafety ? 'nnbd_artifacts' : 'compilation_artifacts'}/$version/'); |
| } |
| |
| @Task('Reinitialize the Flutter submodule.') |
| void setupFlutterSdk() async { |
| print('setup-flutter-sdk channel: $_channel'); |
| |
| // Download the SDK into ./flutter-sdks/ |
| final sdkManager = DownloadingSdkManager(_channel); |
| print('Flutter version: ${sdkManager.flutterVersion}'); |
| final flutterSdkPath = await sdkManager.createFromConfigFile(); |
| |
| // Set up the Flutter SDK the way dart-services needs it. |
| |
| final flutterBinFlutter = path.join(flutterSdkPath, 'bin', 'flutter'); |
| await runWithLogging( |
| flutterBinFlutter, |
| arguments: ['doctor'], |
| ); |
| |
| await runWithLogging( |
| flutterBinFlutter, |
| arguments: ['config', '--enable-web'], |
| ); |
| |
| await runWithLogging( |
| flutterBinFlutter, |
| arguments: [ |
| 'precache', |
| '--web', |
| '--no-android', |
| '--no-ios', |
| '--no-linux', |
| '--no-windows', |
| '--no-macos', |
| '--no-fuchsia', |
| ], |
| ); |
| } |
| |
| @Task() |
| void fuzz() { |
| log('warning: fuzz testing is a noop, see #301'); |
| } |
| |
| @Task('Update generated files and run all checks prior to deployment') |
| @Depends(sdkInit, updateDockerVersion, generateProtos, updatePubDependencies, |
| analyze, test, validateStorageArtifacts) |
| void deploy() { |
| log('Deploy via Google Cloud Console'); |
| } |
| |
| @Task() |
| @Depends(generateProtos, analyze, fuzz, buildStorageArtifacts) |
| void buildbot() {} |
| |
| @Task('Generate Protobuf classes') |
| void generateProtos() async { |
| await runWithLogging( |
| 'protoc', |
| arguments: ['--dart_out=lib/src', 'protos/dart_services.proto'], |
| onErrorMessage: |
| 'Error running "protoc"; make sure the Protocol Buffer compiler is ' |
| 'installed (see README.md)', |
| ); |
| |
| // reformat generated classes so travis dart format test doesn't fail |
| await runWithLogging( |
| 'dart', |
| arguments: ['format', '--fix', 'lib/src/protos'], |
| ); |
| |
| // And reformat again, for $REASONS |
| await runWithLogging( |
| 'dart', |
| arguments: ['format', '--fix', 'lib/src/protos'], |
| ); |
| |
| // generate common_server_proto.g.dart |
| Pub.run('build_runner', arguments: ['build', '--delete-conflicting-outputs']); |
| } |
| |
| Future<void> runWithLogging(String executable, |
| {List<String> arguments = const [], |
| RunOptions? runOptions, |
| String? workingDirectory, |
| String? onErrorMessage}) async { |
| runOptions = mergeWorkingDirectory(workingDirectory, runOptions); |
| log("$executable ${arguments.join(' ')}"); |
| |
| Process proc; |
| try { |
| proc = await Process.start(executable, arguments, |
| workingDirectory: runOptions.workingDirectory, |
| environment: runOptions.environment, |
| includeParentEnvironment: runOptions.includeParentEnvironment, |
| runInShell: runOptions.runInShell); |
| } catch (e) { |
| if (onErrorMessage != null) { |
| print(onErrorMessage); |
| } |
| rethrow; |
| } |
| |
| proc.stdout.listen((out) => log(runOptions!.stdoutEncoding.decode(out))); |
| proc.stderr.listen((err) => log(runOptions!.stdoutEncoding.decode(err))); |
| final exitCode = await proc.exitCode; |
| |
| if (exitCode != 0) { |
| fail('Unable to exec $executable, failed with code $exitCode'); |
| } |
| } |
| |
| const String _samplePackageName = 'dartpad_sample'; |
| |
| String createPubspec({ |
| required bool includeFlutterWeb, |
| required bool nullSafety, |
| Map<String, String> dependencies = const {}, |
| }) { |
| var content = ''' |
| name: $_samplePackageName |
| environment: |
| sdk: '>=${nullSafety ? '2.14.0' : '2.10.0'} <3.0.0' |
| dependencies: |
| '''; |
| |
| if (includeFlutterWeb) { |
| content += ''' |
| flutter: |
| sdk: flutter |
| flutter_test: |
| sdk: flutter |
| '''; |
| } |
| dependencies.forEach((name, version) { |
| content += ' $name: $version\n'; |
| }); |
| |
| return content; |
| } |
| |
| @Task('Update pubspec dependency versions') |
| @Depends(sdkInit) |
| void updatePubDependencies() async { |
| final sdk = _getSdk(); |
| _updateDependenciesFile( |
| flutterToolPath: sdk.flutterToolPath, channel: _channel); |
| } |
| |
| /// Updates the "dependencies file". |
| /// |
| /// The new set of dependency packages, and their version numbers, is determined |
| /// by resolving versions of direct and indirect dependencies of a Flutter web |
| /// app with Firebase plugins in a scratch pub package. |
| /// |
| /// See [_pubDependenciesFile] for the location of the dependencies files. |
| void _updateDependenciesFile({ |
| required String flutterToolPath, |
| required String channel, |
| }) async { |
| final tempDir = Directory.systemTemp.createTempSync('pubspec-scratch'); |
| final pubspec = createPubspec( |
| includeFlutterWeb: true, |
| nullSafety: true, |
| dependencies: { |
| // pkg:lints and pkg:flutter_lints |
| 'lints': 'any', |
| 'flutter_lints': 'any', |
| for (var package in firebasePackages) package: 'any', |
| for (var package in supportedFlutterPackages) package: 'any', |
| for (var package in supportedBasicDartPackages) package: 'any', |
| }, |
| ); |
| joinFile(tempDir, ['pubspec.yaml']).writeAsStringSync(pubspec); |
| await _runFlutterPackagesGet(flutterToolPath, tempDir); |
| final packageVersions = packageVersionsFromPubspecLock(tempDir.path); |
| |
| _pubDependenciesFile(channel: channel) |
| .writeAsStringSync(_jsonEncoder.convert(packageVersions)); |
| } |
| |
| /// An encoder which indents nested elements by two spaces. |
| const JsonEncoder _jsonEncoder = JsonEncoder.withIndent(' '); |
| |
| /// Returns the File containing the pub dependencies and their version numbers. |
| /// |
| /// The file is at `tool/pub_dependencies_{channel}.json`, for the Flutter |
| /// channels: stable, beta, dev, old. |
| File _pubDependenciesFile({required String channel}) { |
| final versionsFileName = 'pub_dependencies_$channel.json'; |
| return File(path.join(Directory.current.path, 'tool', versionsFileName)); |
| } |
| |
| /// Parses [_pubDependenciesFile] as a JSON Map of Strings. |
| Map<String, String> _parsePubDependenciesFile({required String channel}) { |
| final packageVersions = |
| jsonDecode(_pubDependenciesFile(channel: channel).readAsStringSync()) |
| as Map; |
| return packageVersions.cast<String, String>(); |
| } |