From 9dcc2f5282784ddc37aaaced73db204fb99ed3a3 Mon Sep 17 00:00:00 2001 From: Patrick Stewart Date: Sun, 4 Aug 2024 19:55:10 -0700 Subject: [PATCH] update(conduit): updating hashing, runtime and isolate packages --- packages/hashing/lib/hashing.dart | 29 ++ packages/hashing/lib/src/pbkdf2.dart | 140 ++++++++++ packages/hashing/lib/src/salt.dart | 34 +++ packages/hashing/pubspec.yaml | 2 +- packages/isolate/lib/isolate.dart | 35 ++- packages/isolate/lib/src/executable.dart | 63 +++++ packages/isolate/lib/src/executor.dart | 128 +++++++++ packages/isolate/lib/src/isolate_base.dart | 6 - .../isolate/lib/src/source_generator.dart | 104 ++++++++ packages/isolate/pubspec.yaml | 10 +- .../lib/src => isolate/test}/.gitkeep | 0 packages/isolate/test/isolate_test.dart | 16 -- packages/runtime/lib/runtime.dart | 67 ++++- 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/lib/src/runtime_base.dart | 6 - packages/runtime/pubspec.yaml | 15 +- packages/runtime/test/.gitkeep | 0 packages/runtime/test/runtime_test.dart | 16 -- 28 files changed, 1821 insertions(+), 62 deletions(-) create mode 100644 packages/hashing/lib/hashing.dart create mode 100644 packages/hashing/lib/src/pbkdf2.dart create mode 100644 packages/hashing/lib/src/salt.dart create mode 100644 packages/isolate/lib/src/executable.dart create mode 100644 packages/isolate/lib/src/executor.dart delete mode 100644 packages/isolate/lib/src/isolate_base.dart create mode 100644 packages/isolate/lib/src/source_generator.dart rename packages/{hashing/lib/src => isolate/test}/.gitkeep (100%) delete mode 100644 packages/isolate/test/isolate_test.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 delete mode 100644 packages/runtime/lib/src/runtime_base.dart create mode 100644 packages/runtime/test/.gitkeep delete mode 100644 packages/runtime/test/runtime_test.dart diff --git a/packages/hashing/lib/hashing.dart b/packages/hashing/lib/hashing.dart new file mode 100644 index 0000000..cede9bf --- /dev/null +++ b/packages/hashing/lib/hashing.dart @@ -0,0 +1,29 @@ +/* + * 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. + */ + +/* + * 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. + */ + +/// This library provides hashing functionality for the Protevus Platform. +/// +/// It exports two main components: +/// - PBKDF2 (Password-Based Key Derivation Function 2) implementation +/// - Salt generation utilities +/// +/// These components are essential for secure password hashing and storage. +library hashing; + +export 'package:protevus_hashing/src/pbkdf2.dart'; +export 'package:protevus_hashing/src/salt.dart'; diff --git a/packages/hashing/lib/src/pbkdf2.dart b/packages/hashing/lib/src/pbkdf2.dart new file mode 100644 index 0000000..0aa94f0 --- /dev/null +++ b/packages/hashing/lib/src/pbkdf2.dart @@ -0,0 +1,140 @@ +/* + * 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:convert'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; + +/// Instances of this type derive a key from a password, salt, and hash function. +/// +/// https://en.wikipedia.org/wiki/PBKDF2 +class PBKDF2 { + /// Creates instance capable of generating a key. + /// + /// [hashAlgorithm] defaults to [sha256]. + PBKDF2({Hash? hashAlgorithm}) { + this.hashAlgorithm = hashAlgorithm ?? sha256; + } + + Hash get hashAlgorithm => _hashAlgorithm; + set hashAlgorithm(Hash algorithm) { + _hashAlgorithm = algorithm; + _blockSize = _hashAlgorithm.convert([1, 2, 3]).bytes.length; + } + + late Hash _hashAlgorithm; + late int _blockSize; + + /// Hashes a [password] with a given [salt]. + /// + /// The length of this return value will be [keyLength]. + /// + /// See [generateAsBase64String] for generating a random salt. + /// + /// See also [generateBase64Key], which base64 encodes the key returned from this method for storage. + List generateKey( + String password, + String salt, + int rounds, + int keyLength, + ) { + if (keyLength > (pow(2, 32) - 1) * _blockSize) { + throw PBKDF2Exception("Derived key too long"); + } + + final numberOfBlocks = (keyLength / _blockSize).ceil(); + final hmac = Hmac(hashAlgorithm, utf8.encode(password)); + final key = ByteData(keyLength); + var offset = 0; + + final saltBytes = utf8.encode(salt); + final saltLength = saltBytes.length; + final inputBuffer = ByteData(saltBytes.length + 4) + ..buffer.asUint8List().setRange(0, saltBytes.length, saltBytes); + + for (var blockNumber = 1; blockNumber <= numberOfBlocks; blockNumber++) { + inputBuffer.setUint8(saltLength, blockNumber >> 24); + inputBuffer.setUint8(saltLength + 1, blockNumber >> 16); + inputBuffer.setUint8(saltLength + 2, blockNumber >> 8); + inputBuffer.setUint8(saltLength + 3, blockNumber); + + final block = _XORDigestSink.generate(inputBuffer, hmac, rounds); + var blockLength = _blockSize; + if (offset + blockLength > keyLength) { + blockLength = keyLength - offset; + } + key.buffer.asUint8List().setRange(offset, offset + blockLength, block); + + offset += blockLength; + } + + return key.buffer.asUint8List(); + } + + /// Hashed a [password] with a given [salt] and base64 encodes the result. + /// + /// This method invokes [generateKey] and base64 encodes the result. + String generateBase64Key( + String password, + String salt, + int rounds, + int keyLength, + ) { + const converter = Base64Encoder(); + + return converter.convert(generateKey(password, salt, rounds, keyLength)); + } +} + +/// Thrown when [PBKDF2] throws an exception. +class PBKDF2Exception implements Exception { + PBKDF2Exception(this.message); + String message; + + @override + String toString() => "PBKDF2Exception: $message"; +} + +class _XORDigestSink implements Sink { + _XORDigestSink(ByteData inputBuffer, Hmac hmac) { + lastDigest = hmac.convert(inputBuffer.buffer.asUint8List()).bytes; + bytes = ByteData(lastDigest.length) + ..buffer.asUint8List().setRange(0, lastDigest.length, lastDigest); + } + + static Uint8List generate(ByteData inputBuffer, Hmac hmac, int rounds) { + final hashSink = _XORDigestSink(inputBuffer, hmac); + + // If rounds == 1, we have already run the first hash in the constructor + // so this loop won't run. + for (var round = 1; round < rounds; round++) { + final hmacSink = hmac.startChunkedConversion(hashSink); + hmacSink.add(hashSink.lastDigest); + hmacSink.close(); + } + + return hashSink.bytes.buffer.asUint8List(); + } + + late ByteData bytes; + late List lastDigest; + + @override + void add(Digest digest) { + lastDigest = digest.bytes; + for (var i = 0; i < digest.bytes.length; i++) { + bytes.setUint8(i, bytes.getUint8(i) ^ lastDigest[i]); + } + } + + @override + void close() {} +} diff --git a/packages/hashing/lib/src/salt.dart b/packages/hashing/lib/src/salt.dart new file mode 100644 index 0000000..2e5ca8d --- /dev/null +++ b/packages/hashing/lib/src/salt.dart @@ -0,0 +1,34 @@ +/* + * 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:convert'; +import 'dart:math'; +import 'dart:typed_data'; + +/// Generates a random salt of [length] bytes from a cryptographically secure random number generator. +/// +/// Each element of this list is a byte. +List generate(int length) { + final buffer = Uint8List(length); + final rng = Random.secure(); + for (var i = 0; i < length; i++) { + buffer[i] = rng.nextInt(256); + } + + return buffer; +} + +/// Generates a random salt of [length] bytes from a cryptographically secure random number generator and encodes it to Base64. +/// +/// [length] is the number of bytes generated, not the [length] of the base64 encoded string returned. Decoding +/// the base64 encoded string will yield [length] number of bytes. +String generateAsBase64String(int length) { + const encoder = Base64Encoder(); + return encoder.convert(generate(length)); +} diff --git a/packages/hashing/pubspec.yaml b/packages/hashing/pubspec.yaml index af0c857..3c13239 100644 --- a/packages/hashing/pubspec.yaml +++ b/packages/hashing/pubspec.yaml @@ -10,7 +10,7 @@ environment: # Add regular dependencies here. dependencies: - # path: ^1.8.0 + crypto: ^3.0.3 dev_dependencies: lints: ^3.0.0 diff --git a/packages/isolate/lib/isolate.dart b/packages/isolate/lib/isolate.dart index 1367ae7..595750a 100644 --- a/packages/isolate/lib/isolate.dart +++ b/packages/isolate/lib/isolate.dart @@ -1,8 +1,31 @@ -/// Support for doing something awesome. +/* + * 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. + */ + +/* + * 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. + */ + +/// This library provides functionality for working with isolates in Dart. +/// It exports three main components: +/// 1. Executable: Defines the structure for tasks that can be executed in isolates. +/// 2. Executor: Provides mechanisms for running executables in isolates. +/// 3. SourceGenerator: Offers utilities for generating source code for isolates. /// -/// More dartdocs go here. -library; +/// These components work together to facilitate concurrent programming and +/// improve performance in Dart applications by leveraging isolates. +library isolate; -export 'src/isolate_base.dart'; - -// TODO: Export any libraries intended for clients of this package. +export 'package:protevus_isolate/src/executable.dart'; +export 'package:protevus_isolate/src/executor.dart'; +export 'package:protevus_isolate/src/source_generator.dart'; diff --git a/packages/isolate/lib/src/executable.dart b/packages/isolate/lib/src/executable.dart new file mode 100644 index 0000000..f24d344 --- /dev/null +++ b/packages/isolate/lib/src/executable.dart @@ -0,0 +1,63 @@ +/* + * 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:isolate'; +import 'dart:mirrors'; + +abstract class Executable { + Executable(this.message) : _sendPort = message["_sendPort"]; + + Future execute(); + + final Map message; + final SendPort? _sendPort; + + U instanceOf( + String typeName, { + List positionalArguments = const [], + Map namedArguments = const {}, + Symbol constructorName = Symbol.empty, + }) { + ClassMirror? typeMirror = currentMirrorSystem() + .isolate + .rootLibrary + .declarations[Symbol(typeName)] as ClassMirror?; + + typeMirror ??= currentMirrorSystem() + .libraries + .values + .where((lib) => lib.uri.scheme == "package" || lib.uri.scheme == "file") + .expand((lib) => lib.declarations.values) + .firstWhere( + (decl) => + decl is ClassMirror && + MirrorSystem.getName(decl.simpleName) == typeName, + orElse: () => throw ArgumentError( + "Unknown type '$typeName'. Did you forget to import it?", + ), + ) as ClassMirror?; + + return typeMirror! + .newInstance( + constructorName, + positionalArguments, + namedArguments, + ) + .reflectee as U; + } + + void send(dynamic message) { + _sendPort!.send(message); + } + + void log(String message) { + _sendPort!.send({"_line_": message}); + } +} diff --git a/packages/isolate/lib/src/executor.dart b/packages/isolate/lib/src/executor.dart new file mode 100644 index 0000000..fa8946d --- /dev/null +++ b/packages/isolate/lib/src/executor.dart @@ -0,0 +1,128 @@ +/* + * 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'; +import 'dart:isolate'; +import 'package:protevus_isolate/isolate.dart'; + +class IsolateExecutor { + IsolateExecutor( + this.generator, { + this.packageConfigURI, + this.message = const {}, + }); + + final SourceGenerator generator; + final Map message; + final Uri? packageConfigURI; + final Completer completer = Completer(); + + Stream get events => _eventListener.stream; + + Stream get console => _logListener.stream; + + final StreamController _logListener = StreamController(); + final StreamController _eventListener = StreamController(); + + Future execute() async { + if (packageConfigURI != null && + !File.fromUri(packageConfigURI!).existsSync()) { + throw StateError( + "Package file '$packageConfigURI' not found. Run 'pub get' and retry.", + ); + } + + final scriptSource = Uri.encodeComponent(await generator.scriptSource); + + final onErrorPort = ReceivePort() + ..listen((err) async { + if (err is List) { + final stack = + StackTrace.fromString(err.last.replaceAll(scriptSource, "")); + + completer.completeError(StateError(err.first), stack); + } else { + completer.completeError(err); + } + }); + + final controlPort = ReceivePort() + ..listen((results) { + if (results is Map && results.length == 1) { + if (results.containsKey("_result")) { + completer.complete(results['_result']); + return; + } else if (results.containsKey("_line_")) { + _logListener.add(results["_line_"]); + return; + } + } + _eventListener.add(results); + }); + try { + message["_sendPort"] = controlPort.sendPort; + + final dataUri = Uri.parse( + "data:application/dart;charset=utf-8,$scriptSource", + ); + + await Isolate.spawnUri( + dataUri, + [], + message, + onError: onErrorPort.sendPort, + packageConfig: packageConfigURI, + automaticPackageResolution: packageConfigURI == null, + ); + return await completer.future; + } catch (e) { + print(e); + rethrow; + } finally { + onErrorPort.close(); + controlPort.close(); + _eventListener.close(); + _logListener.close(); + } + } + + static Future run( + Executable executable, { + List imports = const [], + Uri? packageConfigURI, + String? additionalContents, + List additionalTypes = const [], + void Function(dynamic event)? eventHandler, + void Function(String line)? logHandler, + }) async { + final source = SourceGenerator( + executable.runtimeType, + imports: imports, + additionalContents: additionalContents, + additionalTypes: additionalTypes, + ); + + final executor = IsolateExecutor( + source, + packageConfigURI: packageConfigURI, + message: executable.message, + ); + + if (eventHandler != null) { + executor.events.listen(eventHandler); + } + + if (logHandler != null) { + executor.console.listen(logHandler); + } + + return executor.execute(); + } +} diff --git a/packages/isolate/lib/src/isolate_base.dart b/packages/isolate/lib/src/isolate_base.dart deleted file mode 100644 index e8a6f15..0000000 --- a/packages/isolate/lib/src/isolate_base.dart +++ /dev/null @@ -1,6 +0,0 @@ -// TODO: Put public facing types in this file. - -/// Checks if you are awesome. Spoiler: you are. -class Awesome { - bool get isAwesome => true; -} diff --git a/packages/isolate/lib/src/source_generator.dart b/packages/isolate/lib/src/source_generator.dart new file mode 100644 index 0000000..7c4ba86 --- /dev/null +++ b/packages/isolate/lib/src/source_generator.dart @@ -0,0 +1,104 @@ +/* + * 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:isolate'; +import 'dart:mirrors'; +import 'package:analyzer/dart/analysis/analysis_context.dart'; +import 'package:analyzer/dart/analysis/context_builder.dart'; +import 'package:analyzer/dart/analysis/context_locator.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/file_system/file_system.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:path/path.dart'; +import 'package:protevus_isolate/isolate.dart'; + +class SourceGenerator { + SourceGenerator( + this.executableType, { + this.imports = const [], + this.additionalTypes = const [], + this.additionalContents, + }); + + Type executableType; + + String get typeName => + MirrorSystem.getName(reflectType(executableType).simpleName); + final List imports; + final String? additionalContents; + final List additionalTypes; + + Future get scriptSource async { + final typeSource = (await _getClass(executableType)).toSource(); + final builder = StringBuffer(); + + builder.writeln("import 'dart:async';"); + builder.writeln("import 'dart:isolate';"); + builder.writeln("import 'dart:mirrors';"); + for (final anImport in imports) { + builder.writeln("import '$anImport';"); + } + builder.writeln( + """ +Future main (List args, Map message) async { + final sendPort = message['_sendPort']; + final executable = $typeName(message); + final result = await executable.execute(); + sendPort.send({"_result": result}); +} + """, + ); + builder.writeln(typeSource); + + builder.writeln((await _getClass(Executable)).toSource()); + for (final type in additionalTypes) { + final source = await _getClass(type); + builder.writeln(source.toSource()); + } + + if (additionalContents != null) { + builder.writeln(additionalContents); + } + + return builder.toString(); + } + + static Future _getClass(Type type) async { + final uri = + await Isolate.resolvePackageUri(reflectClass(type).location!.sourceUri); + final path = + absolute(normalize(uri!.toFilePath(windows: Platform.isWindows))); + + final context = _createContext(path); + final session = context.currentSession; + final unit = session.getParsedUnit(path) as ParsedUnitResult; + final typeName = MirrorSystem.getName(reflectClass(type).simpleName); + + return unit.unit.declarations + .whereType() + .firstWhere((classDecl) => classDecl.name.value() == typeName); + } +} + +AnalysisContext _createContext( + String path, { + ResourceProvider? resourceProvider, +}) { + resourceProvider ??= PhysicalResourceProvider.INSTANCE; + final builder = ContextBuilder(resourceProvider: resourceProvider); + final contextLocator = ContextLocator( + resourceProvider: resourceProvider, + ); + final root = contextLocator.locateRoots( + includedPaths: [path], + ); + return builder.createContext(contextRoot: root.first); +} diff --git a/packages/isolate/pubspec.yaml b/packages/isolate/pubspec.yaml index 52f85ab..649f416 100644 --- a/packages/isolate/pubspec.yaml +++ b/packages/isolate/pubspec.yaml @@ -1,14 +1,18 @@ name: protevus_isolate -description: A starting point for Dart libraries or applications. +description: This library contains types that allow for executing code in a spawned isolate, perhaps with additional imports. version: 0.0.1 -# repository: https://github.com/my_org/my_repo +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: - # path: ^1.8.0 + analyzer: ^6.5.0 + glob: ^2.1.2 + path: ^1.9.0 dev_dependencies: lints: ^3.0.0 diff --git a/packages/hashing/lib/src/.gitkeep b/packages/isolate/test/.gitkeep similarity index 100% rename from packages/hashing/lib/src/.gitkeep rename to packages/isolate/test/.gitkeep diff --git a/packages/isolate/test/isolate_test.dart b/packages/isolate/test/isolate_test.dart deleted file mode 100644 index aff4736..0000000 --- a/packages/isolate/test/isolate_test.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:protevus_isolate/isolate.dart'; -import 'package:test/test.dart'; - -void main() { - group('A group of tests', () { - final awesome = Awesome(); - - setUp(() { - // Additional setup goes here. - }); - - test('First Test', () { - expect(awesome.isAwesome, isTrue); - }); - }); -} diff --git a/packages/runtime/lib/runtime.dart b/packages/runtime/lib/runtime.dart index 72bccca..c97bd61 100644 --- a/packages/runtime/lib/runtime.dart +++ b/packages/runtime/lib/runtime.dart @@ -1,8 +1,65 @@ -/// Support for doing something awesome. +/* + * 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/src/compiler.dart'; +import 'package:protevus_runtime/src/mirror_context.dart'; + +/// Compiler for the runtime package itself. /// -/// More dartdocs go here. -library; +/// 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) => {}; -export 'src/runtime_base.dart'; + @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';", + ); -// TODO: Export any libraries intended for clients of this package. + 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/lib/src/runtime_base.dart b/packages/runtime/lib/src/runtime_base.dart deleted file mode 100644 index e8a6f15..0000000 --- a/packages/runtime/lib/src/runtime_base.dart +++ /dev/null @@ -1,6 +0,0 @@ -// TODO: Put public facing types in this file. - -/// Checks if you are awesome. Spoiler: you are. -class Awesome { - bool get isAwesome => true; -} diff --git a/packages/runtime/pubspec.yaml b/packages/runtime/pubspec.yaml index 5d384d7..bae9a92 100644 --- a/packages/runtime/pubspec.yaml +++ b/packages/runtime/pubspec.yaml @@ -1,14 +1,23 @@ name: protevus_runtime -description: A starting point for Dart libraries or applications. +description: Provides behaviors and base types for packages that can use mirrors and be AOT compiled. version: 0.0.1 -# repository: https://github.com/my_org/my_repo +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: - # path: ^1.8.0 + 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 diff --git a/packages/runtime/test/.gitkeep b/packages/runtime/test/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/runtime/test/runtime_test.dart b/packages/runtime/test/runtime_test.dart deleted file mode 100644 index e021d39..0000000 --- a/packages/runtime/test/runtime_test.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:protevus_runtime/runtime.dart'; -import 'package:test/test.dart'; - -void main() { - group('A group of tests', () { - final awesome = Awesome(); - - setUp(() { - // Additional setup goes here. - }); - - test('First Test', () { - expect(awesome.isAwesome, isTrue); - }); - }); -}