From 7a095316dc13bf1999ff62cb2fb2a0df3a5bdfca Mon Sep 17 00:00:00 2001 From: Patrick Stewart Date: Tue, 3 Sep 2024 13:19:16 -0700 Subject: [PATCH] add(conduit): refactoring conduit --- packages/runtime/.gitignore | 7 + packages/runtime/CHANGELOG.md | 3 + packages/runtime/README.md | 39 +++ packages/runtime/analysis_options.yaml | 30 +++ packages/runtime/lib/runtime.dart | 64 +++++ packages/runtime/lib/slow_coerce.dart | 86 ++++++ packages/runtime/lib/src/analyzer.dart | 143 ++++++++++ packages/runtime/lib/src/build.dart | 214 +++++++++++++++ packages/runtime/lib/src/build_context.dart | 252 ++++++++++++++++++ packages/runtime/lib/src/build_manager.dart | 89 +++++++ packages/runtime/lib/src/compiler.dart | 40 +++ packages/runtime/lib/src/context.dart | 76 ++++++ packages/runtime/lib/src/exceptions.dart | 20 ++ packages/runtime/lib/src/generator.dart | 123 +++++++++ packages/runtime/lib/src/mirror_coerce.dart | 79 ++++++ packages/runtime/lib/src/mirror_context.dart | 90 +++++++ packages/runtime/pubspec.yaml | 24 ++ .../lib/src => runtime/test}/.gitkeep | 0 18 files changed, 1379 insertions(+) create mode 100644 packages/runtime/.gitignore create mode 100644 packages/runtime/CHANGELOG.md create mode 100644 packages/runtime/README.md create mode 100644 packages/runtime/analysis_options.yaml create mode 100644 packages/runtime/lib/runtime.dart create mode 100644 packages/runtime/lib/slow_coerce.dart create mode 100644 packages/runtime/lib/src/analyzer.dart create mode 100644 packages/runtime/lib/src/build.dart create mode 100644 packages/runtime/lib/src/build_context.dart create mode 100644 packages/runtime/lib/src/build_manager.dart create mode 100644 packages/runtime/lib/src/compiler.dart create mode 100644 packages/runtime/lib/src/context.dart create mode 100644 packages/runtime/lib/src/exceptions.dart create mode 100644 packages/runtime/lib/src/generator.dart create mode 100644 packages/runtime/lib/src/mirror_coerce.dart create mode 100644 packages/runtime/lib/src/mirror_context.dart create mode 100644 packages/runtime/pubspec.yaml rename packages/{database/lib/src => runtime/test}/.gitkeep (100%) diff --git a/packages/runtime/.gitignore b/packages/runtime/.gitignore new file mode 100644 index 0000000..3cceda5 --- /dev/null +++ b/packages/runtime/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/runtime/CHANGELOG.md b/packages/runtime/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/packages/runtime/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/runtime/README.md b/packages/runtime/README.md new file mode 100644 index 0000000..8b55e73 --- /dev/null +++ b/packages/runtime/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/runtime/analysis_options.yaml b/packages/runtime/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/packages/runtime/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/packages/runtime/lib/runtime.dart b/packages/runtime/lib/runtime.dart new file mode 100644 index 0000000..a89b332 --- /dev/null +++ b/packages/runtime/lib/runtime.dart @@ -0,0 +1,64 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +library runtime; + +import 'dart:io'; + +export 'package:protevus_runtime/src/analyzer.dart'; +export 'package:protevus_runtime/src/build.dart'; +export 'package:protevus_runtime/src/build_context.dart'; +export 'package:protevus_runtime/src/build_manager.dart'; +export 'package:protevus_runtime/src/compiler.dart'; +export 'package:protevus_runtime/src/context.dart'; +export 'package:protevus_runtime/src/exceptions.dart'; +export 'package:protevus_runtime/src/generator.dart'; +export 'package:protevus_runtime/src/mirror_coerce.dart'; +export 'package:protevus_runtime/src/mirror_context.dart'; + +import 'package:protevus_runtime/runtime.dart'; + +/// Compiler for the runtime package itself. +/// +/// Removes dart:mirror from a replica of this package, and adds +/// a generated runtime to the replica's pubspec. +class RuntimePackageCompiler extends Compiler { + @override + Map compile(MirrorContext context) => {}; + + @override + void deflectPackage(Directory destinationDirectory) { + final libraryFile = File.fromUri( + destinationDirectory.uri.resolve("lib/").resolve("runtime.dart"), + ); + libraryFile.writeAsStringSync( + "library runtime;\nexport 'src/context.dart';\nexport 'src/exceptions.dart';", + ); + + final contextFile = File.fromUri( + destinationDirectory.uri + .resolve("lib/") + .resolve("src/") + .resolve("context.dart"), + ); + final contextFileContents = contextFile.readAsStringSync().replaceFirst( + "import 'package:protevus_runtime/src/mirror_context.dart';", + "import 'package:generated_runtime/generated_runtime.dart';", + ); + contextFile.writeAsStringSync(contextFileContents); + + final pubspecFile = + File.fromUri(destinationDirectory.uri.resolve("pubspec.yaml")); + final pubspecContents = pubspecFile.readAsStringSync().replaceFirst( + "\ndependencies:", + "\ndependencies:\n generated_runtime:\n path: ../../generated_runtime/", + ); + pubspecFile.writeAsStringSync(pubspecContents); + } +} diff --git a/packages/runtime/lib/slow_coerce.dart b/packages/runtime/lib/slow_coerce.dart new file mode 100644 index 0000000..b1f5093 --- /dev/null +++ b/packages/runtime/lib/slow_coerce.dart @@ -0,0 +1,86 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'package:protevus_runtime/runtime.dart'; + +const String _listPrefix = "List<"; +const String _mapPrefix = "Map(dynamic input) { + try { + var typeString = T.toString(); + if (typeString.endsWith('?')) { + if (input == null) { + return null as T; + } else { + typeString = typeString.substring(0, typeString.length - 1); + } + } + if (typeString.startsWith(_listPrefix)) { + if (input is! List) { + throw TypeError(); + } + + if (typeString.startsWith("List")) { + return List.from(input) as T; + } else if (typeString.startsWith("List")) { + return List.from(input) as T; + } else if (typeString.startsWith("List")) { + return List.from(input) as T; + } else if (typeString.startsWith("List")) { + return List.from(input) as T; + } else if (typeString.startsWith("List")) { + return List.from(input) as T; + } else if (typeString.startsWith("List")) { + return List.from(input) as T; + } else if (typeString.startsWith("List")) { + return List.from(input) as T; + } else if (typeString.startsWith("List")) { + return List.from(input) as T; + } else if (typeString.startsWith("List")) { + return List.from(input) as T; + } else if (typeString.startsWith("List")) { + return List.from(input) as T; + } else if (typeString.startsWith("List>")) { + return List>.from(input) as T; + } + } else if (typeString.startsWith(_mapPrefix)) { + if (input is! Map) { + throw TypeError(); + } + + final inputMap = input as Map; + if (typeString.startsWith("Map")) { + return Map.from(inputMap) as T; + } else if (typeString.startsWith("Map")) { + return Map.from(inputMap) as T; + } else if (typeString.startsWith("Map")) { + return Map.from(inputMap) as T; + } else if (typeString.startsWith("Map")) { + return Map.from(inputMap) as T; + } else if (typeString.startsWith("Map")) { + return Map.from(inputMap) as T; + } else if (typeString.startsWith("Map")) { + return Map.from(inputMap) as T; + } else if (typeString.startsWith("Map")) { + return Map.from(inputMap) as T; + } else if (typeString.startsWith("Map")) { + return Map.from(inputMap) as T; + } else if (typeString.startsWith("Map")) { + return Map.from(inputMap) as T; + } else if (typeString.startsWith("Map")) { + return Map.from(inputMap) as T; + } + } + + return input as T; + } on TypeError { + throw TypeCoercionException(T, input.runtimeType); + } +} diff --git a/packages/runtime/lib/src/analyzer.dart b/packages/runtime/lib/src/analyzer.dart new file mode 100644 index 0000000..d6b2881 --- /dev/null +++ b/packages/runtime/lib/src/analyzer.dart @@ -0,0 +1,143 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'dart:io'; +import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:path/path.dart'; + +class CodeAnalyzer { + CodeAnalyzer(this.uri) { + if (!uri.isAbsolute) { + throw ArgumentError("'uri' must be absolute for CodeAnalyzer"); + } + + contexts = AnalysisContextCollection(includedPaths: [path]); + + if (contexts.contexts.isEmpty) { + throw ArgumentError("no analysis context found for path '$path'"); + } + } + + String get path { + return getPath(uri); + } + + late final Uri uri; + + late AnalysisContextCollection contexts; + + final _resolvedAsts = {}; + + Future resolveUnitOrLibraryAt(Uri uri) async { + if (FileSystemEntity.isFileSync( + uri.toFilePath(windows: Platform.isWindows), + )) { + return resolveUnitAt(uri); + } else { + return resolveLibraryAt(uri); + } + } + + Future resolveLibraryAt(Uri uri) async { + assert( + FileSystemEntity.isDirectorySync( + uri.toFilePath(windows: Platform.isWindows), + ), + ); + for (final ctx in contexts.contexts) { + final path = getPath(uri); + if (_resolvedAsts.containsKey(path)) { + return _resolvedAsts[path]! as ResolvedLibraryResult; + } + + final output = await ctx.currentSession.getResolvedLibrary(path) + as ResolvedLibraryResult; + return _resolvedAsts[path] = output; + } + + throw ArgumentError("'uri' could not be resolved (contexts: " + "${contexts.contexts.map((c) => c.contextRoot.root.toUri()).join(", ")})"); + } + + Future resolveUnitAt(Uri uri) async { + assert( + FileSystemEntity.isFileSync( + uri.toFilePath(windows: Platform.isWindows), + ), + ); + for (final ctx in contexts.contexts) { + final path = getPath(uri); + if (_resolvedAsts.containsKey(path)) { + return _resolvedAsts[path]! as ResolvedUnitResult; + } + + final output = + await ctx.currentSession.getResolvedUnit(path) as ResolvedUnitResult; + return _resolvedAsts[path] = output; + } + + throw ArgumentError("'uri' could not be resolved (contexts: " + "${contexts.contexts.map((c) => c.contextRoot.root.toUri()).join(", ")})"); + } + + ClassDeclaration? getClassFromFile(String className, Uri fileUri) { + try { + return _getFileAstRoot(fileUri) + .declarations + .whereType() + .firstWhere((c) => c.name.value() == className); + } catch (e) { + if (e is StateError || e is TypeError || e is ArgumentError) { + return null; + } + rethrow; + } + } + + List getSubclassesFromFile( + String superclassName, + Uri fileUri, + ) { + return _getFileAstRoot(fileUri) + .declarations + .whereType() + .where((c) => + c.extendsClause?.superclass.name2.toString() == superclassName) + .toList(); + } + + CompilationUnit _getFileAstRoot(Uri fileUri) { + assert( + FileSystemEntity.isFileSync( + fileUri.toFilePath(windows: Platform.isWindows), + ), + ); + try { + final path = getPath(fileUri); + if (_resolvedAsts.containsKey(path)) { + return (_resolvedAsts[path]! as ResolvedUnitResult).unit; + } + } finally {} + final unit = contexts.contextFor(path).currentSession.getParsedUnit( + normalize( + absolute(fileUri.toFilePath(windows: Platform.isWindows)), + ), + ) as ParsedUnitResult; + return unit.unit; + } + + static String getPath(dynamic inputUri) { + return PhysicalResourceProvider.INSTANCE.pathContext.normalize( + PhysicalResourceProvider.INSTANCE.pathContext.fromUri(inputUri), + ); + } +} diff --git a/packages/runtime/lib/src/build.dart b/packages/runtime/lib/src/build.dart new file mode 100644 index 0000000..bc248ef --- /dev/null +++ b/packages/runtime/lib/src/build.dart @@ -0,0 +1,214 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +// ignore_for_file: avoid_print +import 'dart:convert'; +import 'dart:io'; +import 'dart:mirrors'; + +import 'package:protevus_runtime/runtime.dart'; +import 'package:io/io.dart'; +import 'package:package_config/package_config.dart'; + +class Build { + Build(this.context); + + final BuildContext context; + + Future execute() async { + final compilers = context.context.compilers; + + print("Resolving ASTs..."); + final astsToResolve = { + ...compilers.expand((c) => c.getUrisToResolve(context)) + }; + await Future.forEach( + astsToResolve, + (astUri) async { + final package = await context.getPackageFromUri(astUri); + final Uri packageUri = + package?.packageUriRoot.resolve(package.name) ?? astUri; + return context.analyzer.resolveUnitOrLibraryAt(packageUri); + }, + ); + + print("Generating runtime..."); + + final runtimeGenerator = RuntimeGenerator(); + for (final MapEntry entry + in context.context.runtimes.map.entries) { + if (entry.value is SourceCompiler) { + await (entry.value as SourceCompiler).compile(context).then( + (source) => + runtimeGenerator.addRuntime(name: entry.key, source: source), + ); + } + } + + await runtimeGenerator.writeTo(context.buildRuntimeDirectory.uri); + print("Generated runtime at '${context.buildRuntimeDirectory.uri}'."); + + final nameOfPackageBeingCompiled = context.sourceApplicationPubspec.name; + final pubspecMap = { + 'name': 'runtime_target', + 'version': '1.0.0', + 'environment': {'sdk': '>=3.4.0 <4.0.0'}, + 'dependency_overrides': {} + }; + final overrides = pubspecMap['dependency_overrides'] as Map; + var sourcePackageIsCompiled = false; + + for (final compiler in compilers) { + final packageInfo = await _getPackageInfoForCompiler(compiler); + final sourceDirUri = packageInfo.root; + final targetDirUri = + context.buildPackagesDirectory.uri.resolve("${packageInfo.name}/"); + print("Compiling package '${packageInfo.name}'..."); + await copyPackage(sourceDirUri, targetDirUri); + compiler.deflectPackage(Directory.fromUri(targetDirUri)); + + if (packageInfo.name != nameOfPackageBeingCompiled) { + overrides[packageInfo.name] = { + "path": targetDirUri.toFilePath(windows: Platform.isWindows) + }; + } else { + sourcePackageIsCompiled = true; + } + print("Package '${packageInfo.name}' compiled to '$targetDirUri'."); + } + + final appDst = context.buildApplicationDirectory.uri; + if (!sourcePackageIsCompiled) { + print( + "Copying application package (from '${context.sourceApplicationDirectory.uri}')...", + ); + await copyPackage(context.sourceApplicationDirectory.uri, appDst); + print("Application packaged copied to '$appDst'."); + } + pubspecMap['dependencies'] = { + nameOfPackageBeingCompiled: { + "path": appDst.toFilePath(windows: Platform.isWindows) + } + }; + + if (context.forTests) { + final devDeps = context.sourceApplicationPubspecMap['dev_dependencies']; + if (devDeps != null) { + pubspecMap['dev_dependencies'] = devDeps; + } + + overrides['conduit_core'] = { + 'path': appDst.toFilePath(windows: Platform.isWindows) + }; + } + + File.fromUri(context.buildDirectoryUri.resolve("pubspec.yaml")) + .writeAsStringSync(json.encode(pubspecMap)); + + context + .getFile(context.targetScriptFileUri) + .writeAsStringSync(context.source); + + for (final compiler in context.context.compilers) { + compiler.didFinishPackageGeneration(context); + } + + print("Fetching dependencies (--offline --no-precompile)..."); + await getDependencies(); + print("Finished fetching dependencies."); + if (!context.forTests) { + print("Compiling..."); + await compile(context.targetScriptFileUri, context.executableUri); + print("Success. Executable is located at '${context.executableUri}'."); + } + } + + Future getDependencies() async { + const String cmd = "dart"; + + final res = await Process.run( + cmd, + ["pub", "get", "--offline", "--no-precompile"], + workingDirectory: + context.buildDirectoryUri.toFilePath(windows: Platform.isWindows), + runInShell: true, + ); + if (res.exitCode != 0) { + print("${res.stdout}"); + throw StateError( + "'pub get' failed with the following message: ${res.stderr}", + ); + } + } + + Future compile(Uri srcUri, Uri dstUri) async { + final res = await Process.run( + "dart", + [ + "compile", + "exe", + ...(context.environment?.entries.map((e) => "-D${e.key}=${e.value}") ?? + []), + "-v", + srcUri.toFilePath(windows: Platform.isWindows), + "-o", + dstUri.toFilePath(windows: Platform.isWindows) + ], + workingDirectory: context.buildApplicationDirectory.uri + .toFilePath(windows: Platform.isWindows), + runInShell: true, + ); + + if (res.exitCode != 0) { + throw StateError( + "'dart2native' failed with the following message: ${res.stderr}", + ); + } + print("${res.stdout}"); + } + + Future copyPackage(Uri srcUri, Uri dstUri) async { + final dstDir = Directory.fromUri(dstUri); + if (!dstDir.existsSync()) { + dstDir.createSync(recursive: true); + } + try { + await copyPath( + srcUri.toFilePath(windows: Platform.isWindows), + dstUri.toFilePath(windows: Platform.isWindows), + ); + } on FileSystemException catch (e) { + if (Platform.isWindows) { + final File f = File(e.path!); + if (f.existsSync()) { + f.deleteSync(); + } + File(e.path!).writeAsStringSync('dummy'); + await copyPath( + srcUri.toFilePath(windows: Platform.isWindows), + dstUri.toFilePath(windows: Platform.isWindows), + ); + } else { + rethrow; + } + } + + return context.getFile(srcUri.resolve("pubspec.yaml")).copy( + dstUri + .resolve("pubspec.yaml") + .toFilePath(windows: Platform.isWindows), + ); + } + + Future _getPackageInfoForCompiler(Compiler compiler) async { + final compilerUri = reflect(compiler).type.location!.sourceUri; + + return (await context.packageConfig)[compilerUri.pathSegments.first]!; + } +} diff --git a/packages/runtime/lib/src/build_context.dart b/packages/runtime/lib/src/build_context.dart new file mode 100644 index 0000000..7032e5b --- /dev/null +++ b/packages/runtime/lib/src/build_context.dart @@ -0,0 +1,252 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'dart:io'; +import 'dart:mirrors'; + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:protevus_runtime/runtime.dart'; +import 'package:package_config/package_config.dart'; +import 'package:path/path.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:yaml/yaml.dart'; + +/// Configuration and context values used during [Build.execute]. +class BuildContext { + BuildContext( + this.rootLibraryFileUri, + this.buildDirectoryUri, + this.executableUri, + this.source, { + this.environment, + this.forTests = false, + }) { + analyzer = CodeAnalyzer(sourceApplicationDirectory.uri); + } + + factory BuildContext.fromMap(Map map) { + return BuildContext( + Uri.parse(map['rootLibraryFileUri']), + Uri.parse(map['buildDirectoryUri']), + Uri.parse(map['executableUri']), + map['source'], + environment: map['environment'], + forTests: map['forTests'] ?? false, + ); + } + + Map get safeMap => { + 'rootLibraryFileUri': sourceLibraryFile.uri.toString(), + 'buildDirectoryUri': buildDirectoryUri.toString(), + 'source': source, + 'executableUri': executableUri.toString(), + 'environment': environment, + 'forTests': forTests + }; + + late final CodeAnalyzer analyzer; + + /// A [Uri] to the library file of the application to be compiled. + final Uri rootLibraryFileUri; + + /// A [Uri] to the executable build product file. + final Uri executableUri; + + /// A [Uri] to directory where build artifacts are stored during the build process. + final Uri buildDirectoryUri; + + /// The source script for the executable. + final String source; + + /// Whether dev dependencies of the application package are included in the dependencies of the compiled executable. + final bool forTests; + + PackageConfig? _packageConfig; + + final Map? environment; + + /// The [RuntimeContext] available during the build process. + MirrorContext get context => RuntimeContext.current as MirrorContext; + + Uri get targetScriptFileUri => forTests + ? getDirectory(buildDirectoryUri.resolve("test/")) + .uri + .resolve("main_test.dart") + : buildDirectoryUri.resolve("main.dart"); + + Pubspec get sourceApplicationPubspec => Pubspec.parse( + File.fromUri(sourceApplicationDirectory.uri.resolve("pubspec.yaml")) + .readAsStringSync(), + ); + + Map get sourceApplicationPubspecMap => loadYaml( + File.fromUri( + sourceApplicationDirectory.uri.resolve("pubspec.yaml"), + ).readAsStringSync(), + ) as Map; + + /// The directory of the application being compiled. + Directory get sourceApplicationDirectory => + getDirectory(rootLibraryFileUri.resolve("../")); + + /// The library file of the application being compiled. + File get sourceLibraryFile => getFile(rootLibraryFileUri); + + /// The directory where build artifacts are stored. + Directory get buildDirectory => getDirectory(buildDirectoryUri); + + /// The generated runtime directory + Directory get buildRuntimeDirectory => + getDirectory(buildDirectoryUri.resolve("generated_runtime/")); + + /// Directory for compiled packages + Directory get buildPackagesDirectory => + getDirectory(buildDirectoryUri.resolve("packages/")); + + /// Directory for compiled application + Directory get buildApplicationDirectory => getDirectory( + buildPackagesDirectory.uri.resolve("${sourceApplicationPubspec.name}/"), + ); + + /// Gets dependency package location relative to [sourceApplicationDirectory]. + Future get packageConfig async { + return _packageConfig ??= + (await findPackageConfig(sourceApplicationDirectory))!; + } + + /// Returns a [Directory] at [uri], creates it recursively if it doesn't exist. + Directory getDirectory(Uri uri) { + final dir = Directory.fromUri(uri); + if (!dir.existsSync()) { + dir.createSync(recursive: true); + } + return dir; + } + + /// Returns a [File] at [uri], creates all parent directories recursively if necessary. + File getFile(Uri uri) { + final file = File.fromUri(uri); + if (!file.parent.existsSync()) { + file.parent.createSync(recursive: true); + } + + return file; + } + + Future getPackageFromUri(Uri? uri) async { + if (uri == null) { + return null; + } + if (uri.scheme == "package") { + final segments = uri.pathSegments; + return (await packageConfig)[segments.first]!; + } else if (!uri.isAbsolute) { + throw ArgumentError("'uri' must be absolute or a package URI"); + } + return null; + } + + Future> getImportDirectives({ + Uri? uri, + String? source, + bool alsoImportOriginalFile = false, + }) async { + if (uri != null && source != null) { + throw ArgumentError( + "either uri or source must be non-null, but not both", + ); + } + + if (uri == null && source == null) { + throw ArgumentError( + "either uri or source must be non-null, but not both", + ); + } + + if (alsoImportOriginalFile == true && uri == null) { + throw ArgumentError( + "flag 'alsoImportOriginalFile' may only be set if 'uri' is also set", + ); + } + final Package? package = await getPackageFromUri(uri); + final String? trailingSegments = uri?.pathSegments.sublist(1).join('/'); + final fileUri = + package?.packageUriRoot.resolve(trailingSegments ?? '') ?? uri; + final text = source ?? File.fromUri(fileUri!).readAsStringSync(); + final importRegex = RegExp("import [\\'\\\"]([^\\'\\\"]*)[\\'\\\"];"); + + final imports = importRegex.allMatches(text).map((m) { + final importedUri = Uri.parse(m.group(1)!); + + if (!importedUri.isAbsolute) { + final path = fileUri + ?.resolve(importedUri.path) + .toFilePath(windows: Platform.isWindows); + return "import 'file:${absolute(path!)}';"; + } + + return text.substring(m.start, m.end); + }).toList(); + + if (alsoImportOriginalFile) { + imports.add("import '$uri';"); + } + + return imports; + } + + Future getClassDeclarationFromType(Type type) async { + final classMirror = reflectType(type); + Uri uri = classMirror.location!.sourceUri; + if (!classMirror.location!.sourceUri.isAbsolute) { + final Package? package = await getPackageFromUri(uri); + uri = package!.packageUriRoot; + } + return analyzer.getClassFromFile( + MirrorSystem.getName(classMirror.simpleName), + uri, + ); + } + + Future _getField(ClassMirror type, String propertyName) { + return getClassDeclarationFromType(type.reflectedType).then((cd) { + try { + return cd!.members.firstWhere( + (m) => (m as FieldDeclaration) + .fields + .variables + .any((v) => v.name.value() == propertyName), + ) as FieldDeclaration; + } catch (e) { + return null; + } + }); + } + + Future> getAnnotationsFromField( + Type type1, + String propertyName, + ) async { + var type = reflectClass(type1); + FieldDeclaration? field = await _getField(type, propertyName); + while (field == null) { + type = type.superclass!; + if (type.reflectedType == Object) { + break; + } + field = await _getField(type, propertyName); + } + + if (field == null) { + return []; + } + + return field.metadata; + } +} diff --git a/packages/runtime/lib/src/build_manager.dart b/packages/runtime/lib/src/build_manager.dart new file mode 100644 index 0000000..45b2214 --- /dev/null +++ b/packages/runtime/lib/src/build_manager.dart @@ -0,0 +1,89 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'dart:io'; + +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:protevus_isolate/isolate.dart'; +import 'package:protevus_runtime/runtime.dart'; +import 'package:io/io.dart'; + +class BuildExecutable extends Executable { + BuildExecutable(Map message) : super(message) { + context = BuildContext.fromMap(message); + } + + late final BuildContext context; + + @override + Future execute() async { + final build = Build(context); + await build.execute(); + } +} + +class BuildManager { + /// Creates a new build manager to compile a non-mirrored build. + BuildManager(this.context); + + final BuildContext context; + + Uri get sourceDirectoryUri => context.sourceApplicationDirectory.uri; + + Future build() async { + if (!context.buildDirectory.existsSync()) { + context.buildDirectory.createSync(); + } + + // Here is where we need to provide a temporary copy of the script file with the main function stripped; + // this is because when the RuntimeGenerator loads, it needs Mirror access to any declarations in this file + var scriptSource = context.source; + final strippedScriptFile = File.fromUri(context.targetScriptFileUri) + ..writeAsStringSync(scriptSource); + final analyzer = CodeAnalyzer(strippedScriptFile.absolute.uri); + final analyzerContext = analyzer.contexts.contextFor(analyzer.path); + final parsedUnit = analyzerContext.currentSession + .getParsedUnit(analyzer.path) as ParsedUnitResult; + + final mainFunctions = parsedUnit.unit.declarations + .whereType() + .where((f) => f.name.value() == "main") + .toList(); + + for (final f in mainFunctions.reversed) { + scriptSource = scriptSource.replaceRange(f.offset, f.end, ""); + } + + strippedScriptFile.writeAsStringSync(scriptSource); + + try { + await copyPath( + context.sourceApplicationDirectory.uri.resolve('test/not_tests').path, + context.buildDirectoryUri.resolve('not_tests').path); + } catch (_) {} + + await IsolateExecutor.run( + BuildExecutable(context.safeMap), + packageConfigURI: + sourceDirectoryUri.resolve('.dart_tool/package_config.json'), + imports: [ + "package:conduit_runtime/runtime.dart", + context.targetScriptFileUri.toString() + ], + logHandler: (s) => print(s), //ignore: avoid_print + ); + } + + Future clean() async { + if (context.buildDirectory.existsSync()) { + context.buildDirectory.deleteSync(recursive: true); + } + } +} diff --git a/packages/runtime/lib/src/compiler.dart b/packages/runtime/lib/src/compiler.dart new file mode 100644 index 0000000..c463395 --- /dev/null +++ b/packages/runtime/lib/src/compiler.dart @@ -0,0 +1,40 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'dart:io'; + +import 'package:protevus_runtime/runtime.dart'; + +abstract class Compiler { + /// Modifies a package on the filesystem in order to remove dart:mirrors from the package. + /// + /// A copy of this compiler's package will be written to [destinationDirectory]. + /// This method is overridden to modify the contents of that directory + /// to remove all uses of dart:mirrors. + /// + /// Packages should export their [Compiler] in their main library file and only + /// import mirrors in files directly or transitively imported by the Compiler file. + /// This method should remove that export statement and therefore remove all transitive mirror imports. + void deflectPackage(Directory destinationDirectory); + + /// Returns a map of runtime objects that can be used at runtime while running in mirrored mode. + Map compile(MirrorContext context); + + void didFinishPackageGeneration(BuildContext context) {} + + List getUrisToResolve(BuildContext context) => []; +} + +/// Runtimes that generate source code implement this method. +abstract class SourceCompiler { + /// The source code, including directives, that declare a class that is equivalent in behavior to this runtime. + Future compile(BuildContext ctx) async { + throw UnimplementedError(); + } +} diff --git a/packages/runtime/lib/src/context.dart b/packages/runtime/lib/src/context.dart new file mode 100644 index 0000000..6d49164 --- /dev/null +++ b/packages/runtime/lib/src/context.dart @@ -0,0 +1,76 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'package:protevus_runtime/runtime.dart'; + +/// Contextual values used during runtime. +abstract class RuntimeContext { + /// The current [RuntimeContext] available to the executing application. + /// + /// Is either a `MirrorContext` or a `GeneratedContext`, + /// depending on the execution type. + static final RuntimeContext current = instance; + + /// The runtimes available to the executing application. + late RuntimeCollection runtimes; + + /// Gets a runtime object for [type]. + /// + /// Callers typically invoke this method, passing their [runtimeType] + /// in order to retrieve their runtime object. + /// + /// It is important to note that a runtime object must exist for every + /// class that extends a class that has a runtime. Use `MirrorContext.getSubclassesOf` when compiling. + /// + /// In other words, if the type `Base` has a runtime and the type `Subclass` extends `Base`, + /// `Subclass` must also have a runtime. The runtime objects for both `Subclass` and `Base` + /// must be the same type. + dynamic operator [](Type type) => runtimes[type]; + + T coerce(dynamic input); +} + +class RuntimeCollection { + RuntimeCollection(this.map); + + final Map map; + + Iterable get iterable => map.values; + + Object operator [](Type t) { + //todo: optimize by keeping a cache where keys are of type [Type] to avoid the + // expensive indexOf and substring calls in this method + final typeName = t.toString(); + final r = map[typeName]; + if (r != null) { + return r; + } + + final genericIndex = typeName.indexOf("<"); + if (genericIndex == -1) { + throw ArgumentError("Runtime not found for type '$t'."); + } + + final genericTypeName = typeName.substring(0, genericIndex); + final out = map[genericTypeName]; + if (out == null) { + throw ArgumentError("Runtime not found for type '$t'."); + } + + return out; + } +} + +/// Prevents a type from being compiled when it otherwise would be. +/// +/// Annotate a type with the const instance of this type to prevent its +/// compilation. +class PreventCompilation { + const PreventCompilation(); +} diff --git a/packages/runtime/lib/src/exceptions.dart b/packages/runtime/lib/src/exceptions.dart new file mode 100644 index 0000000..1ff625d --- /dev/null +++ b/packages/runtime/lib/src/exceptions.dart @@ -0,0 +1,20 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +class TypeCoercionException implements Exception { + TypeCoercionException(this.expectedType, this.actualType); + + final Type expectedType; + final Type actualType; + + @override + String toString() { + return "input is not expected type '$expectedType' (input is '$actualType')"; + } +} diff --git a/packages/runtime/lib/src/generator.dart b/packages/runtime/lib/src/generator.dart new file mode 100644 index 0000000..2545d04 --- /dev/null +++ b/packages/runtime/lib/src/generator.dart @@ -0,0 +1,123 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'dart:async'; +import 'dart:io'; + +const String _directiveToken = "___DIRECTIVES___"; +const String _assignmentToken = "___ASSIGNMENTS___"; + +class RuntimeGenerator { + final _elements = <_RuntimeElement>[]; + + void addRuntime({required String name, required String source}) { + _elements.add(_RuntimeElement(name, source)); + } + + Future writeTo(Uri directoryUri) async { + final dir = Directory.fromUri(directoryUri); + final libDir = Directory.fromUri(dir.uri.resolve("lib/")); + final srcDir = Directory.fromUri(libDir.uri.resolve("src/")); + if (!libDir.existsSync()) { + libDir.createSync(recursive: true); + } + if (!srcDir.existsSync()) { + srcDir.createSync(recursive: true); + } + + final libraryFile = + File.fromUri(libDir.uri.resolve("generated_runtime.dart")); + await libraryFile.writeAsString(loaderSource); + + final pubspecFile = File.fromUri(dir.uri.resolve("pubspec.yaml")); + await pubspecFile.writeAsString(pubspecSource); + + await Future.forEach(_elements, (_RuntimeElement e) async { + final file = File.fromUri(srcDir.uri.resolveUri(e.relativeUri)); + if (!file.parent.existsSync()) { + file.parent.createSync(recursive: true); + } + + await file.writeAsString(e.source); + }); + } + + String get pubspecSource => """ +name: generated_runtime +description: A runtime generated by package:conduit_runtime +version: 1.0.0 + +environment: + sdk: '>=3.4.0 <4.0.0' +"""; + + String get _loaderShell => """ +import 'package:conduit_runtime/runtime.dart'; +import 'package:conduit_runtime/slow_coerce.dart' as runtime_cast; +$_directiveToken + +RuntimeContext instance = GeneratedContext._(); + +class GeneratedContext extends RuntimeContext { + GeneratedContext._() { + final map = {}; + + $_assignmentToken + + runtimes = RuntimeCollection(map); + } + + @override + T coerce(dynamic input) { + return runtime_cast.cast(input); + } +} + """; + + String get loaderSource { + return _loaderShell + .replaceFirst(_directiveToken, _directives) + .replaceFirst(_assignmentToken, _assignments); + } + + String get _directives { + final buf = StringBuffer(); + + for (final e in _elements) { + buf.writeln( + "import 'src/${e.relativeUri.toFilePath(windows: Platform.isWindows)}' as ${e.importAlias};", + ); + } + + return buf.toString(); + } + + String get _assignments { + final buf = StringBuffer(); + + for (final e in _elements) { + buf.writeln("map['${e.typeName}'] = ${e.importAlias}.instance;"); + } + + return buf.toString(); + } +} + +class _RuntimeElement { + _RuntimeElement(this.typeName, this.source); + + final String typeName; + final String source; + + Uri get relativeUri => Uri.file("${typeName.toLowerCase()}.dart"); + + String get importAlias { + return "g_${typeName.toLowerCase()}"; + } +} diff --git a/packages/runtime/lib/src/mirror_coerce.dart b/packages/runtime/lib/src/mirror_coerce.dart new file mode 100644 index 0000000..557db7c --- /dev/null +++ b/packages/runtime/lib/src/mirror_coerce.dart @@ -0,0 +1,79 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'dart:mirrors'; + +import 'package:protevus_runtime/runtime.dart'; + +Object runtimeCast(Object object, TypeMirror intoType) { + final exceptionToThrow = + TypeCoercionException(intoType.reflectedType, object.runtimeType); + + try { + final objectType = reflect(object).type; + if (objectType.isAssignableTo(intoType)) { + return object; + } + + if (intoType.isSubtypeOf(reflectType(List))) { + if (object is! List) { + throw exceptionToThrow; + } + + final elementType = intoType.typeArguments.first; + final elements = object.map((e) => runtimeCast(e, elementType)); + return (intoType as ClassMirror).newInstance(#from, [elements]).reflectee; + } else if (intoType.isSubtypeOf(reflectType(Map, [String, dynamic]))) { + if (object is! Map) { + throw exceptionToThrow; + } + + final output = (intoType as ClassMirror) + .newInstance(Symbol.empty, []).reflectee as Map; + final valueType = intoType.typeArguments.last; + object.forEach((key, val) { + output[key] = runtimeCast(val, valueType); + }); + return output; + } + } on TypeError { + throw exceptionToThrow; + } on TypeCoercionException { + throw exceptionToThrow; + } + + throw exceptionToThrow; +} + +bool isTypeFullyPrimitive(TypeMirror type) { + if (type == reflectType(dynamic)) { + return true; + } + + if (type.isSubtypeOf(reflectType(List))) { + return isTypeFullyPrimitive(type.typeArguments.first); + } else if (type.isSubtypeOf(reflectType(Map))) { + return isTypeFullyPrimitive(type.typeArguments.first) && + isTypeFullyPrimitive(type.typeArguments.last); + } + + if (type.isSubtypeOf(reflectType(num))) { + return true; + } + + if (type.isSubtypeOf(reflectType(String))) { + return true; + } + + if (type.isSubtypeOf(reflectType(bool))) { + return true; + } + + return false; +} diff --git a/packages/runtime/lib/src/mirror_context.dart b/packages/runtime/lib/src/mirror_context.dart new file mode 100644 index 0000000..c4e94aa --- /dev/null +++ b/packages/runtime/lib/src/mirror_context.dart @@ -0,0 +1,90 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'dart:mirrors'; + +import 'package:protevus_runtime/runtime.dart'; + +RuntimeContext instance = MirrorContext._(); + +class MirrorContext extends RuntimeContext { + MirrorContext._() { + final m = {}; + + for (final c in compilers) { + final compiledRuntimes = c.compile(this); + if (m.keys.any((k) => compiledRuntimes.keys.contains(k))) { + final matching = m.keys.where((k) => compiledRuntimes.keys.contains(k)); + throw StateError( + 'Could not compile. Type conflict for the following types: ${matching.join(", ")}.', + ); + } + m.addAll(compiledRuntimes); + } + + runtimes = RuntimeCollection(m); + } + + final List types = currentMirrorSystem() + .libraries + .values + .where((lib) => lib.uri.scheme == "package" || lib.uri.scheme == "file") + .expand((lib) => lib.declarations.values) + .whereType() + .where((cm) => firstMetadataOfType(cm) == null) + .toList(); + + List get compilers { + return types + .where((b) => b.isSubclassOf(reflectClass(Compiler)) && !b.isAbstract) + .map((b) => b.newInstance(Symbol.empty, []).reflectee as Compiler) + .toList(); + } + + List getSubclassesOf(Type type) { + final mirror = reflectClass(type); + return types.where((decl) { + if (decl.isAbstract) { + return false; + } + + if (!decl.isSubclassOf(mirror)) { + return false; + } + + if (decl.hasReflectedType) { + if (decl.reflectedType == type) { + return false; + } + } + + return true; + }).toList(); + } + + @override + T coerce(dynamic input) { + try { + return input as T; + } catch (_) { + return runtimeCast(input, reflectType(T)) as T; + } + } +} + +T? firstMetadataOfType(DeclarationMirror dm, {TypeMirror? dynamicType}) { + final tMirror = dynamicType ?? reflectType(T); + try { + return dm.metadata + .firstWhere((im) => im.type.isSubtypeOf(tMirror)) + .reflectee as T; + } on StateError { + return null; + } +} diff --git a/packages/runtime/pubspec.yaml b/packages/runtime/pubspec.yaml new file mode 100644 index 0000000..bae9a92 --- /dev/null +++ b/packages/runtime/pubspec.yaml @@ -0,0 +1,24 @@ +name: protevus_runtime +description: Provides behaviors and base types for packages that can use mirrors and be AOT compiled. +version: 0.0.1 +homepage: https://protevus.com +documentation: https://docs.protevus.com +repository: https://git.protevus.com/protevus/platform + +environment: + sdk: ^3.4.3 + +# Add regular dependencies here. +dependencies: + analyzer: ^6.5.0 + args: ^2.0.0 + protevus_isolate: ^0.0.1 + io: ^1.0.4 + package_config: ^2.1.0 + path: ^1.9.0 + pubspec_parse: ^1.2.3 + yaml: ^3.1.2 + +dev_dependencies: + lints: ^3.0.0 + test: ^1.24.0 diff --git a/packages/database/lib/src/.gitkeep b/packages/runtime/test/.gitkeep similarity index 100% rename from packages/database/lib/src/.gitkeep rename to packages/runtime/test/.gitkeep