From 2b476a74853d4479c964d06aac86b02f99cc831c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 30 May 2024 11:41:45 +0200 Subject: [PATCH] RDART-999: Fix flutter test dlopen (#1623) * Use lib in package directly during flutter test * Fix paths * Update CHANGELOG --- CHANGELOG.md | 1 + .../lib/src/cli/common/target_os_type.dart | 15 +-- .../lib/src/cli/install/install_command.dart | 72 +++++------ .../lib/src/cli/install/options.dart | 10 +- .../lib/src/cli/install/options.g.dart | 57 +++++--- packages/realm_dart/lib/src/init.dart | 122 ++++++++++++------ packages/realm_dart/lib/src/realm_dart.dart | 3 - .../realm_dart/lib/src/realm_flutter.dart | 3 - 8 files changed, 166 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 233abb1a2..8550b9c42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed * Fixed an issue that would cause macOS apps to be rejected with `Invalid Code Signing Entitlements` error. (Issue [#1679](https://github.com/realm/realm-dart/issues/1679)) +* Fixed a regression that makes it inconvenient to run unit tests using realm. (Issue [#1619](https://github.com/realm/realm-dart/issues/1619)) ### Compatibility * Realm Studio: 15.0.0 or later. diff --git a/packages/realm_dart/lib/src/cli/common/target_os_type.dart b/packages/realm_dart/lib/src/cli/common/target_os_type.dart index f87d958b6..6868ebfce 100644 --- a/packages/realm_dart/lib/src/cli/common/target_os_type.dart +++ b/packages/realm_dart/lib/src/cli/common/target_os_type.dart @@ -8,24 +8,19 @@ enum TargetOsType { ios, linux, macos, - windows, + windows; + + bool get isDesktop => [TargetOsType.linux, TargetOsType.macos, TargetOsType.windows].contains(this); } // Cannot use Dart 2.17 enhanced enums, due to an issue with build_cli :-/ enum Flavor { flutter, - dart, + dart; } extension FlavorEx on Flavor { - String get packageName { - switch (this) { - case Flavor.dart: - return 'realm_dart'; - case Flavor.flutter: - return 'realm'; - } - } + String get packageName => switch (this) { Flavor.dart => 'realm_dart', Flavor.flutter => 'realm' }; } extension StringEx on String { diff --git a/packages/realm_dart/lib/src/cli/install/install_command.dart b/packages/realm_dart/lib/src/cli/install/install_command.dart index 5d44ba5db..5f783f128 100644 --- a/packages/realm_dart/lib/src/cli/install/install_command.dart +++ b/packages/realm_dart/lib/src/cli/install/install_command.dart @@ -25,26 +25,34 @@ class InstallCommand extends Command { late Options options; - bool get debug => options.debug; + late Pubspec pubspec; + late final debug = options.debug; + late final flavor = options.flavor!; + late final force = options.force; // not used!?! + late final targetOsType = options.targetOsType!; InstallCommand() { - populateOptionsParser(argParser); + pubspec = parsePubspec(File('pubspec.yaml')); + final defaultFlavor = pubspec.dependencies['flutter'] == null ? Flavor.dart : Flavor.flutter; // default depends on project type + populateOptionsParser( + argParser, + targetOsTypeDefaultOverride: Platform.operatingSystem.asTargetOsType, + flavorDefaultOverride: defaultFlavor, + ); } - Directory getBinaryPath(Directory realmPackagePath, {required bool isFlutter}) { - if (isFlutter) { - final root = realmPackagePath.path; - return Directory(switch (options.targetOsType) { - TargetOsType.android => path.join(root, 'android', 'src', 'main', 'cpp', 'lib'), - TargetOsType.ios => path.join(root, 'ios'), - TargetOsType.macos => path.join(root, 'macos'), - TargetOsType.linux => path.join(root, 'linux', 'binary', 'linux'), - TargetOsType.windows => path.join(root, 'windows', 'binary', 'windows'), - _ => throw Exception('Unsupported target OS type for Flutter: ${options.targetOsType}') - }); - } - // TODO: Should binaries not go into package also for Dart? - return Directory(path.join(Directory.current.absolute.path, 'binary', options.targetOsType!.name)); + Directory getBinaryPath(Directory realmPackagePath, Flavor flavor) { + final root = realmPackagePath.path; + return switch (flavor) { + Flavor.dart => Directory(path.join(root, 'binary', targetOsType.name)), + Flavor.flutter => Directory(switch (targetOsType) { + TargetOsType.android => path.join(root, 'android', 'src', 'main', 'cpp', 'lib'), + TargetOsType.ios => path.join(root, 'ios'), + TargetOsType.macos => path.join(root, 'macos'), + TargetOsType.linux => path.join(root, 'linux', 'binary', 'linux'), + TargetOsType.windows => path.join(root, 'windows', 'binary', 'windows'), + }), + }; } Future shouldSkipDownload(String binariesPath, String expectedVersion) async { @@ -105,31 +113,29 @@ class InstallCommand extends Command { if (packageConfig == null) { abort('Run `dart pub get`'); } - final package = packageConfig.packages.where((p) => p.name == name).singleOrNull; + final package = packageConfig[name]; if (package == null) { abort('$name package not found in dependencies. Add $name package to your pubspec.yaml'); } return Directory.fromUri(package.root); } - Future parsePubspec(File file) async { + Pubspec parsePubspec(File file) { try { - return Pubspec.parse(await file.readAsString(), sourceUrl: file.uri); - } on Exception catch (e) { - throw Exception('Error parsing package pubspec at ${file.parent}. Error $e'); + return Pubspec.parse(file.readAsStringSync(), sourceUrl: file.uri); + } catch (e) { + abort('Error parsing package pubspec at ${file.parent}. Error $e'); } } @override FutureOr run() async { - final pubspec = await parsePubspec(File('pubspec.yaml')); - final flavor = pubspec.dependencies['flutter'] == null ? Flavor.dart : Flavor.flutter; - options = parseOptionsResult(argResults!); - validateOptions(flavor); final flavorName = flavor.packageName; final realmDependency = pubspec.dependencyOverrides[flavorName] ?? pubspec.dependencies[flavorName]; + print(pubspec.dependencyOverrides.values.join('\n')); + print(realmDependency); if (realmDependency is PathDependency) { print('Path dependency for $flavorName found. Skipping install of native lib (assuming local development)'); return; @@ -140,26 +146,16 @@ class InstallCommand extends Command { } final realmPackagePath = await getPackagePath(flavorName); - final realmPubspec = await parsePubspec(File(path.join(realmPackagePath.path, "pubspec.yaml"))); + final realmPubspec = parsePubspec(File(path.join(realmPackagePath.path, "pubspec.yaml"))); - final binaryPath = getBinaryPath(realmPackagePath, isFlutter: flavor == Flavor.flutter); + final binaryPath = getBinaryPath(realmPackagePath, flavor); print(binaryPath); - final archiveName = '${options.targetOsType!.name}.tar.gz'; + final archiveName = '${targetOsType.name}.tar.gz'; await downloadAndExtractBinaries(binaryPath, realmPubspec.version!, archiveName); print('Realm install command finished.'); } - void validateOptions(Flavor flavor) { - final targetOs = flavor == Flavor.dart ? getTargetOS() : options.targetOsType; - if (targetOs == null) { - abort('Target OS not specified'); - } - options.targetOsType = targetOs; - } - - TargetOsType getTargetOS() => Platform.operatingSystem.asTargetOsType ?? (throw UnsupportedError('Unsupported platform ${Platform.operatingSystem}')); - Never abort(String error) { print(error); print(usage); diff --git a/packages/realm_dart/lib/src/cli/install/options.dart b/packages/realm_dart/lib/src/cli/install/options.dart index f9c1440b6..59b35e96f 100644 --- a/packages/realm_dart/lib/src/cli/install/options.dart +++ b/packages/realm_dart/lib/src/cli/install/options.dart @@ -8,7 +8,10 @@ part 'options.g.dart'; @CliOptions() class Options { - @CliOption(help: 'The target OS to install binaries for.', abbr: 't') + @CliOption(help: 'The flavor to install binaries for.', abbr: 'f', provideDefaultToOverride: true) + Flavor? flavor; + + @CliOption(help: 'The target OS to install binaries for.', abbr: 't', provideDefaultToOverride: true) TargetOsType? targetOsType; // use to debug install command @@ -23,6 +26,9 @@ class Options { String get usage => _$parserForOptions.usage; -ArgParser populateOptionsParser(ArgParser p) => _$populateOptionsParser(p); +ArgParser populateOptionsParser(ArgParser parser, { + TargetOsType? targetOsTypeDefaultOverride, + Flavor? flavorDefaultOverride, +}) => _$populateOptionsParser(parser, targetOsTypeDefaultOverride: targetOsTypeDefaultOverride, flavorDefaultOverride: flavorDefaultOverride); Options parseOptionsResult(ArgResults results) => _$parseOptionsResult(results); diff --git a/packages/realm_dart/lib/src/cli/install/options.g.dart b/packages/realm_dart/lib/src/cli/install/options.g.dart index f0a4e6e05..d6a8b6f7f 100644 --- a/packages/realm_dart/lib/src/cli/install/options.g.dart +++ b/packages/realm_dart/lib/src/cli/install/options.g.dart @@ -30,7 +30,15 @@ Options _$parseOptionsResult(ArgResults result) => Options( ), force: result['force'] as bool, debug: result['debug'] as bool, - ); + )..flavor = _$nullableEnumValueHelperNullable( + _$FlavorEnumMapBuildCli, + result['flavor'] as String?, + ); + +const _$FlavorEnumMapBuildCli = { + Flavor.flutter: 'flutter', + Flavor.dart: 'dart' +}; const _$TargetOsTypeEnumMapBuildCli = { TargetOsType.android: 'android', @@ -40,23 +48,36 @@ const _$TargetOsTypeEnumMapBuildCli = { TargetOsType.windows: 'windows' }; -ArgParser _$populateOptionsParser(ArgParser parser) => parser - ..addOption( - 'target-os-type', - abbr: 't', - help: 'The target OS to install binaries for.', - allowed: ['android', 'ios', 'linux', 'macos', 'windows'], - ) - ..addFlag( - 'debug', - help: 'Download binary from http://localhost:8000/.', - hide: true, - ) - ..addFlag( - 'force', - help: 'Force install, even if we would normally skip it.', - hide: true, - ); +ArgParser _$populateOptionsParser( + ArgParser parser, { + Flavor? flavorDefaultOverride, + TargetOsType? targetOsTypeDefaultOverride, +}) => + parser + ..addOption( + 'flavor', + abbr: 'f', + help: 'The flavor to install binaries for.', + defaultsTo: _$FlavorEnumMapBuildCli[flavorDefaultOverride], + allowed: ['flutter', 'dart'], + ) + ..addOption( + 'target-os-type', + abbr: 't', + help: 'The target OS to install binaries for.', + defaultsTo: _$TargetOsTypeEnumMapBuildCli[targetOsTypeDefaultOverride], + allowed: ['android', 'ios', 'linux', 'macos', 'windows'], + ) + ..addFlag( + 'debug', + help: 'Download binary from http://localhost:8000/.', + hide: true, + ) + ..addFlag( + 'force', + help: 'Force install, even if we would normally skip it.', + hide: true, + ); final _$parserForOptions = _$populateOptionsParser(ArgParser()); diff --git a/packages/realm_dart/lib/src/init.dart b/packages/realm_dart/lib/src/init.dart index 21de3e93a..075ca4764 100644 --- a/packages/realm_dart/lib/src/init.dart +++ b/packages/realm_dart/lib/src/init.dart @@ -4,45 +4,61 @@ import 'dart:ffi'; import 'dart:io'; +import 'package:package_config/package_config.dart'; import 'package:path/path.dart' as p; import '../realm.dart' as realm show isFlutterPlatform; -import '../realm.dart' show realmBinaryName; import 'cli/common/target_os_type.dart'; import 'cli/metrics/metrics_command.dart'; import 'cli/metrics/options.dart'; import 'realm_class.dart'; +const realmBinaryName = 'realm_dart'; +final targetOsType = Platform.operatingSystem.asTargetOsType ?? _platformNotSupported(); +final nativeLibraryName = _getLibName(realmBinaryName); + DynamicLibrary? _library; -String _getPluginPath(String libName) { - if (Platform.isAndroid) { - return libName; - } - if (Platform.isLinux) { - return '$_exeDirName/lib/$libName'; - } - if (Platform.isMacOS) { - return '$_exeDirName/../Frameworks/$libName'; - } - if (Platform.isIOS) { - return '$_exeDirName/Frameworks/realm_dart.framework/$libName'; - } - if (Platform.isWindows) { - return libName; +String _getLibPathFlutter() { + final root = _exeDirName; + return switch (targetOsType) { + TargetOsType.android => nativeLibraryName, + TargetOsType.ios => p.join(root, 'Frameworks', 'realm_dart.framework', nativeLibraryName), + TargetOsType.linux => p.join(root, 'lib', nativeLibraryName), + TargetOsType.macos => p.join(p.dirname(root), 'Frameworks', nativeLibraryName), + TargetOsType.windows => nativeLibraryName, + }; +} + +String _getLibPathFlutterTest(Package realmPackage) { + assert(realmPackage.name == 'realm'); + final root = p.join(realmPackage.root.toFilePath(), targetOsType.name); + return switch (targetOsType) { + TargetOsType.linux => p.join(root, 'binary', 'linux', nativeLibraryName), + TargetOsType.macos => p.join(root, nativeLibraryName), + TargetOsType.windows => p.join(root, 'binary', 'windows', nativeLibraryName), + _ => _platformNotSupported(), + }; +} + +String _getLibPathDart(Package realmDartPackage) { + assert(realmDartPackage.name == 'realm_dart'); + final root = p.join(realmDartPackage.root.toFilePath(), 'binary', targetOsType.name); + if (targetOsType.isDesktop) { + return p.join(root, nativeLibraryName); } _platformNotSupported(); } bool get isFlutterPlatform => realm.isFlutterPlatform; -String _getLibName(String stem) { - if (Platform.isMacOS) return 'lib$stem.dylib'; - if (Platform.isIOS) return stem; - if (Platform.isWindows) return '$stem.dll'; - if (Platform.isAndroid || Platform.isLinux) return 'lib$stem.so'; - _platformNotSupported(); // we don't support Fuchsia yet -} +String _getLibName(String stem) => switch (targetOsType) { + TargetOsType.android => 'lib$stem.so', + TargetOsType.ios => stem, // xcframeworks are a directory + TargetOsType.linux => 'lib$stem.so', + TargetOsType.macos => 'lib$stem.dylib', + TargetOsType.windows => '$stem.dll', + }; String? _getNearestProjectRoot(String dir) { while (dir != p.dirname(dir)) { @@ -52,17 +68,24 @@ String? _getNearestProjectRoot(String dir) { return null; } +File _getPackageConfigJson(Directory d) { + final root = _getNearestProjectRoot(d.path); + if (root != null) { + final file = File(p.join(root, '.dart_tool', 'package_config.json')); + if (file.existsSync()) return file; + } + throw StateError('Could not find package_config.json'); +} + Never _platformNotSupported() => throw UnsupportedError('Platform ${Platform.operatingSystem} is not supported'); String get _exeDirName => p.dirname(Platform.resolvedExecutable); DynamicLibrary _openRealmLib() { - final libName = _getLibName(realmBinaryName); - DynamicLibrary? tryOpen(String path) { try { return DynamicLibrary.open(path); - } on Error catch (_) { + } catch (_) { return null; } } @@ -70,7 +93,7 @@ DynamicLibrary _openRealmLib() { Never throwError(Iterable candidatePaths) { throw RealmError( [ - 'Could not open $libName. Tried:', + 'Could not open $nativeLibraryName. Tried:', candidatePaths.map((p) => '- "$p"').join('\n'), isFlutterPlatform // ? 'Hint: Did you forget to add a dependency on the realm package?' @@ -79,23 +102,36 @@ DynamicLibrary _openRealmLib() { ); } - if (isFlutterPlatform) { - final path = _getPluginPath(libName); - return tryOpen(path) ?? throwError([path]); - } else { - final root = _getNearestProjectRoot(Platform.script.path) ?? _getNearestProjectRoot(p.current); - final candidatePaths = [ - libName, // just ask OS.. - p.join(_exeDirName, libName), // try finding it next to the executable - if (root != null) p.join(root, 'binary', Platform.operatingSystem, libName), // try finding it relative to project - ]; - DynamicLibrary? lib; - for (final path in candidatePaths) { - lib = tryOpen(path); - if (lib != null) return lib; - } - throwError(candidatePaths); + DynamicLibrary open(String path) => tryOpen(path) ?? throwError([path]); + + final isFlutterTest = Platform.environment.containsKey('FLUTTER_TEST'); + if (isFlutterPlatform && !isFlutterTest) { + return open(_getLibPathFlutter()); + } + + // NOTE: This needs to be sync, so we cannot use findPackageConfig + final packageConfigFile = _getPackageConfigJson(Directory.current); + final packageConfig = PackageConfig.parseBytes(packageConfigFile.readAsBytesSync(), packageConfigFile.uri); + + if (isFlutterTest) { + final realmPackage = packageConfig['realm']!; + return open(_getLibPathFlutterTest(realmPackage)); + } + + final realmDartPackage = packageConfig['realm_dart']!; + + // else plain dart + final candidatePaths = [ + nativeLibraryName, // just ask OS.. + p.join(_exeDirName, nativeLibraryName), // try finding it next to the executable + _getLibPathDart(realmDartPackage), // try finding it in the package + ]; + DynamicLibrary? lib; + for (final path in candidatePaths) { + lib = tryOpen(path); + if (lib != null) return lib; } + throwError(candidatePaths); } /// @nodoc diff --git a/packages/realm_dart/lib/src/realm_dart.dart b/packages/realm_dart/lib/src/realm_dart.dart index 9de3c2278..5c3c9c7c4 100644 --- a/packages/realm_dart/lib/src/realm_dart.dart +++ b/packages/realm_dart/lib/src/realm_dart.dart @@ -6,6 +6,3 @@ export 'realm_class.dart' hide RealmInternal; /// @nodoc // is Realm loaded in Flutter or Dart const bool isFlutterPlatform = false; - -/// @nodoc -const String realmBinaryName = 'realm_dart'; diff --git a/packages/realm_dart/lib/src/realm_flutter.dart b/packages/realm_dart/lib/src/realm_flutter.dart index cb7b7e9d6..63202c442 100644 --- a/packages/realm_dart/lib/src/realm_flutter.dart +++ b/packages/realm_dart/lib/src/realm_flutter.dart @@ -6,6 +6,3 @@ export 'realm_class.dart' hide RealmInternal; /// @nodoc // is Realm loaded in Flutter or Dart const bool isFlutterPlatform = true; - -/// @nodoc -const String realmBinaryName = 'realm_dart';