diff --git a/packages/komodo_defi_framework/README.md b/packages/komodo_defi_framework/README.md index a3277d3af..6143adfbd 100644 --- a/packages/komodo_defi_framework/README.md +++ b/packages/komodo_defi_framework/README.md @@ -103,6 +103,11 @@ flutter: You can customize sources and checksums via `app_build/build_config.json` in this package. See `packages/komodo_wallet_build_transformer/README.md` for CLI flags, environment variables, and troubleshooting. +On macOS, the build pipeline accepts both release-style static archives +(`libkdf-macos-universal2-.zip`) and CI-style executable archives +(`kdf-macos-universal2-.zip`). The transformer normalizes them into the +package layout consumed by the plugin podspec before Xcode builds. + ## Web (WASM) On Web, the plugin registers a WASM implementation automatically (see `lib/web/kdf_plugin_web.dart`). The WASM bundle and bootstrap scripts are provided via the build transformer. diff --git a/packages/komodo_defi_framework/app_build/BUILD_CONFIG_README.md b/packages/komodo_defi_framework/app_build/BUILD_CONFIG_README.md index e685354f7..dd4d930cd 100644 --- a/packages/komodo_defi_framework/app_build/BUILD_CONFIG_README.md +++ b/packages/komodo_defi_framework/app_build/BUILD_CONFIG_README.md @@ -12,6 +12,7 @@ This directory contains the artifact configuration used by `komodo_wallet_build_ - `api.api_commit_hash` – commit hash of the KDF artifacts to fetch - `api.source_urls` – list of base URLs to download from (GitHub API, CDN) - `api.platforms.*.matching_pattern` – regex to match artifact names per platform +- `api.platforms.*.matching_preference` – preferred filename substrings when multiple artifacts match - `api.platforms.*.valid_zip_sha256_checksums` – allow-list of artifact checksums - `api.platforms.*.path` – destination relative to artifact output package - `coins.bundled_coins_repo_commit` – commit of Komodo coins registry @@ -28,6 +29,18 @@ Artifacts are downloaded into the package specified by the transformer flag: Paths in the config are relative to that package directory. +### macOS canonical layout + +The transformer normalizes any compatible macOS archive into a single package +layout before marking it current: + +- executable builds: `macos/bin/kdf` +- dynamic library builds: `macos/lib/libkdflib.dylib` +- static library builds: `macos/Frameworks/libkdflib.a` + +GitHub release artifacts are tried first. Mirror artifacts are only accepted +after checksum validation and canonical-layout normalization succeed. + ## Updating artifacts 1. Update `api_commit_hash` and (optionally) checksums @@ -53,6 +66,9 @@ Paths in the config are relative to that package directory. ``` - The downloader expects branch-scoped directory listings (e.g., `.../dev/`) on both devbuilds and Nebula mirrors and will fallback to the base listing when available. It searches for artifacts that match the platform patterns and contain either the full commit hash or a 7-char short hash. +- For macOS, keep all accepted archive checksums for a commit in the + checksum allow-list when both release (`libkdf-*`) and mirror/CI + (`kdf-*`) archives should be usable. - To pin a specific commit (e.g., `4025b8c`) without changing branches, update `api.api_commit_hash` or use the CLI with `--commit`: ```bash diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index 3612b9282..05804972b 100644 --- a/packages/komodo_defi_framework/app_build/build_config.json +++ b/packages/komodo_defi_framework/app_build/build_config.json @@ -26,9 +26,16 @@ }, "macos": { "matching_pattern": "^(?:kdf-macos-universal2-[a-f0-9]{7,40}|kdf_[a-f0-9]{7,40}-mac-universal|libkdf-macos-universal2-[a-f0-9]{7,40})\\.zip$", - "matching_preference": ["universal2", "mac-arm64"], + "matching_preference": [ + "libkdf-macos-universal2", + "kdf-macos-universal2", + "kdf_", + "universal2", + "mac-arm64" + ], "valid_zip_sha256_checksums": [ - "14a1473d46706fdfbd04d18939994b686016a126e4dc2cb8937d00f5645b8773" + "14a1473d46706fdfbd04d18939994b686016a126e4dc2cb8937d00f5645b8773", + "61ed5295dfc7acfb962a976e915c0909a300f15552a3d1ae64b737f3add8a39d" ], "path": "macos/bin" }, diff --git a/packages/komodo_defi_framework/lib/src/native/kdf_executable_finder.dart b/packages/komodo_defi_framework/lib/src/native/kdf_executable_finder.dart index 9029babf3..636b31d12 100644 --- a/packages/komodo_defi_framework/lib/src/native/kdf_executable_finder.dart +++ b/packages/komodo_defi_framework/lib/src/native/kdf_executable_finder.dart @@ -34,6 +34,9 @@ class KdfExecutableFinder { /// Attempts to find the KDF executable in standard and platform-specific /// locations Future findExecutable({String executableName = 'kdf'}) async { + // Executable-based macOS builds copy kdf into the framework Helpers + // directory. Static-library builds do not populate this path and instead + // rely on DynamicLibrary.process()/executable() resolution. final macosHelpersInFrameworkPath = p.joinAll([ p.dirname(p.dirname(Platform.resolvedExecutable)), 'Frameworks', @@ -73,8 +76,9 @@ class KdfExecutableFinder { } } + final searchedPaths = files.map((e) => e.absolute.path).join('\n'); logCallback( - 'Executable not found in paths: ${files.map((e) => e.absolute.path).join('\n')}. ' + 'Executable not found in paths: $searchedPaths. ' 'If you are using the KDF Flutter SDK, open an issue on GitHub.', ); diff --git a/packages/komodo_defi_framework/lib/src/operations/kdf_operations_native.dart b/packages/komodo_defi_framework/lib/src/operations/kdf_operations_native.dart index ee9a8a327..3c22c35ee 100644 --- a/packages/komodo_defi_framework/lib/src/operations/kdf_operations_native.dart +++ b/packages/komodo_defi_framework/lib/src/operations/kdf_operations_native.dart @@ -365,6 +365,10 @@ ffi.DynamicLibrary _loadLibrary() { List _getLibraryPaths() { if (Platform.isMacOS) { + // macOS supports three packaging modes: + // - a bundled kdf executable copied into the framework Helpers directory + // - a bundled libkdflib.dylib + // - a force-loaded libkdflib.a, resolved via PROCESS/EXECUTABLE symbols return ['kdf', 'mm2', 'libkdflib.dylib', 'PROCESS', 'EXECUTABLE']; } else if (Platform.isIOS) { return ['libkdflib.dylib', 'PROCESS', 'EXECUTABLE']; diff --git a/packages/komodo_defi_framework/macos/komodo_defi_framework.podspec b/packages/komodo_defi_framework/macos/komodo_defi_framework.podspec index 25fdac500..907a71c45 100644 --- a/packages/komodo_defi_framework/macos/komodo_defi_framework.podspec +++ b/packages/komodo_defi_framework/macos/komodo_defi_framework.podspec @@ -1,3 +1,6 @@ +has_static_library = File.exist?('Frameworks/libkdflib.a') +has_dynamic_library = File.exist?('lib/libkdflib.dylib') + Pod::Spec.new do |s| s.name = 'komodo_defi_framework' s.version = '0.0.1' @@ -15,11 +18,17 @@ A new Flutter FFI plugin project. s.source_files = 'Classes/**/*' s.dependency 'FlutterMacOS' - s.resource_bundles = { - 'kdf_resources' => ['lib/*.dylib'].select { |f| Dir.exist?(File.dirname(f)) } - } + if has_dynamic_library + s.resource_bundles = { + 'kdf_resources' => ['lib/*.dylib'] + } + end - # s.preserve_paths = ['bin/kdf'] + if has_static_library + s.vendored_libraries = ['Frameworks/libkdflib.a'] + end + + s.preserve_paths = ['bin/kdf', 'lib/libkdflib.dylib', 'Frameworks/libkdflib.a'] s.script_phase = { :name => 'Install kdf executable and/or dylib', @@ -55,6 +64,13 @@ A new Flutter FFI plugin project. else echo "Warning: libkdflib.dylib not found in lib/libkdflib.dylib" fi + + if [ -f "${PODS_TARGET_SRCROOT}/Frameworks/libkdflib.a" ]; then + echo "Static libkdflib.a found, linking through CocoaPods vendored_libraries" + FOUND_REQUIRED_FILE=1 + else + echo "Warning: libkdflib.a not found in Frameworks/libkdflib.a" + fi # Prune binary slices to match $ARCHS (preserve universals) in Release builds only case "$CONFIGURATION" in @@ -125,20 +141,31 @@ A new Flutter FFI plugin project. if [ -f "$APP_SUPPORT_DIR/kdf" ]; then cp "$APP_SUPPORT_DIR/kdf" "$HELPERS_DIR/kdf"; fi code_sign_if_enabled "$FRAMEWORKS_DIR/libkdflib.dylib" || true - # Fail if neither file was found + # Static-library builds rely on vendored_libraries/force_load and + # DynamicLibrary.process() at runtime, so there is nothing to copy/sign. if [ $FOUND_REQUIRED_FILE -eq 0 ]; then - echo "Error: Neither kdf executable nor libkdflib.dylib was found. At least one is required." + echo "Error: No compatible macOS KDF artefact was found." + echo "Expected one of:" + echo " - bin/kdf" + echo " - lib/libkdflib.dylib" + echo " - Frameworks/libkdflib.a" exit 1 fi SCRIPT } # Configuration for macOS build + other_ldflags = if has_static_library + '-force_load $(PODS_TARGET_SRCROOT)/Frameworks/libkdflib.a -lstdc++ -framework SystemConfiguration' + else + '-framework SystemConfiguration' + end + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', # Allow building universal macOS apps (arm64 + x86_64). i386 remains excluded by default Xcode settings. - 'OTHER_LDFLAGS' => '-framework SystemConfiguration', - # Add rpath to ensure dylib can be found at runtime + 'OTHER_LDFLAGS' => other_ldflags, + # Add rpath to ensure dylib can be found at runtime when the dynamic artefact is used. 'LD_RUNPATH_SEARCH_PATHS' => [ '$(inherited)', '@executable_path/../Frameworks', diff --git a/packages/komodo_defi_framework/macos/komodo_defi_framework.podspec.staticlib b/packages/komodo_defi_framework/macos/komodo_defi_framework.podspec.staticlib deleted file mode 100644 index 24fe081bc..000000000 --- a/packages/komodo_defi_framework/macos/komodo_defi_framework.podspec.staticlib +++ /dev/null @@ -1,42 +0,0 @@ -#! Template podspec for static library KDF binary - - -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint komodo_defi_framework.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'komodo_defi_framework' - s.version = '0.0.1' - s.summary = 'A new Flutter FFI plugin project.' - s.description = <<-DESC -A new Flutter FFI plugin project. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - s.public_header_files = 'Classes/**/*.h' - - # This will ensure the source files in Classes/ are included in the native - # builds of apps using this FFI plugin. Podspec does not support relative - # paths, so Classes contains a forwarder C file that relatively imports - # `../src/*` so that the C sources can be shared among all target platforms. - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'FlutterMacOS' - # s.vendored_libraries = 'Frameworks/libkdf.a' - # s.vendored_libraries = ['Frameworks/libkdflib.a'] - s.vendored_libraries = ['Frameworks/*.a'] - # Exclude i386 and arm64 from iOS Simulator build - - # s.pod_target_xcconfig = { "OTHER_LDFLAGS" => "$(inherited) -force_load $(PODS_TARGET_SRCROOT)/Frameworks/libkdflib.a -lstdc++" } - s.pod_target_xcconfig = { - 'DEFINES_MODULE' => 'YES', - # Allow building universal macOS apps (arm64 + x86_64). i386 remains excluded by default Xcode settings. - 'OTHER_LDFLAGS' => '-force_load $(PODS_TARGET_SRCROOT)/Frameworks/libkdflib.a -lstdc++ -framework SystemConfiguration' - } - - - s.platform = :osx, '14.0' - s.swift_version = '5.0' -end diff --git a/packages/komodo_wallet_build_transformer/README.md b/packages/komodo_wallet_build_transformer/README.md index bc2553aff..eafd64a69 100644 --- a/packages/komodo_wallet_build_transformer/README.md +++ b/packages/komodo_wallet_build_transformer/README.md @@ -37,6 +37,17 @@ flutter: Artifacts and checksums are configured in `packages/komodo_defi_framework/app_build/build_config.json`. +For macOS, the transformer normalizes compatible archives into a canonical +layout before they are marked current: + +- `bin/kdf` for executable builds +- `lib/libkdflib.dylib` for dynamic-library builds +- `Frameworks/libkdflib.a` for static-library builds + +GitHub release assets remain the primary source. Mirror/CI assets are accepted +only when they match the requested commit, pass checksum validation, and +normalize into one of the supported macOS layouts. + ## CLI You can run the transformer directly for local testing: diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/defi_api_build_step/macos_artefact_layout_normalizer.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/defi_api_build_step/macos_artefact_layout_normalizer.dart new file mode 100644 index 000000000..55eadd891 --- /dev/null +++ b/packages/komodo_wallet_build_transformer/lib/src/steps/defi_api_build_step/macos_artefact_layout_normalizer.dart @@ -0,0 +1,246 @@ +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +class MacosArtefactInstallResult { + const MacosArtefactInstallResult({ + required this.hasExecutable, + required this.hasDynamicLibrary, + required this.hasStaticLibrary, + }); + + final bool hasExecutable; + final bool hasDynamicLibrary; + final bool hasStaticLibrary; + + bool get hasAnyArtefact => + hasExecutable || hasDynamicLibrary || hasStaticLibrary; +} + +class MacosArtefactLayoutNormalizer { + MacosArtefactLayoutNormalizer({Logger? log}) + : _log = log ?? Logger('MacosArtefactLayoutNormalizer'); + + static const executableRelativePath = 'bin/kdf'; + static const dynamicLibraryRelativePath = 'lib/libkdflib.dylib'; + static const staticLibraryRelativePath = 'Frameworks/libkdflib.a'; + + static const _managedDirectoryNames = ['bin', 'lib', 'Frameworks']; + + final Logger _log; + + Future installFromExtractedDirectory({ + required Directory extractedDirectory, + required Directory destinationRoot, + }) async { + final stagingDirectory = await Directory.systemTemp.createTemp( + 'kdf_macos_stage_', + ); + + try { + final result = await _normalizeIntoStagingDirectory( + extractedDirectory: extractedDirectory, + stagingDirectory: stagingDirectory, + ); + + if (!result.hasAnyArtefact) { + throw StateError( + 'No compatible macOS KDF artefacts were found in ' + '${extractedDirectory.path}', + ); + } + + await _replaceManagedDirectories( + sourceRoot: stagingDirectory, + destinationRoot: destinationRoot, + ); + + return result; + } finally { + if (stagingDirectory.existsSync()) { + await stagingDirectory.delete(recursive: true); + } + } + } + + Future _normalizeIntoStagingDirectory({ + required Directory extractedDirectory, + required Directory stagingDirectory, + }) async { + final files = await extractedDirectory + .list(recursive: true, followLinks: false) + .where((entity) => entity is File) + .cast() + .toList(); + + final executable = _findPreferredFile(files, const ['kdf', 'mm2']); + final dynamicLibrary = _findPreferredFile(files, const [ + 'libkdflib.dylib', + 'libkdf.dylib', + 'libmm2.dylib', + ]); + final staticLibrary = _findPreferredFile(files, const [ + 'libkdflib.a', + 'libkdf.a', + 'libmm2.a', + ]); + + if (executable != null) { + await _copyCanonicalFile( + source: executable, + destinationRoot: stagingDirectory, + relativeDestinationPath: executableRelativePath, + makeExecutable: true, + ); + } + + if (dynamicLibrary != null) { + await _copyCanonicalFile( + source: dynamicLibrary, + destinationRoot: stagingDirectory, + relativeDestinationPath: dynamicLibraryRelativePath, + ); + } + + if (staticLibrary != null) { + await _copyCanonicalFile( + source: staticLibrary, + destinationRoot: stagingDirectory, + relativeDestinationPath: staticLibraryRelativePath, + ); + } + + return MacosArtefactInstallResult( + hasExecutable: executable != null, + hasDynamicLibrary: dynamicLibrary != null, + hasStaticLibrary: staticLibrary != null, + ); + } + + File? _findPreferredFile(List files, List preferredBasenames) { + for (final basename in preferredBasenames) { + final matches = + files + .where( + (file) => path.basename(file.path).toLowerCase() == basename, + ) + .toList() + ..sort(_compareFilesBySpecificity); + + if (matches.isNotEmpty) { + return matches.first; + } + } + + return null; + } + + int _compareFilesBySpecificity(File left, File right) { + final leftDepth = path.split(path.normalize(left.path)).length; + final rightDepth = path.split(path.normalize(right.path)).length; + final depthComparison = leftDepth.compareTo(rightDepth); + if (depthComparison != 0) { + return depthComparison; + } + + return left.path.compareTo(right.path); + } + + Future _copyCanonicalFile({ + required File source, + required Directory destinationRoot, + required String relativeDestinationPath, + bool makeExecutable = false, + }) async { + final destination = File( + path.join(destinationRoot.path, relativeDestinationPath), + ); + await destination.parent.create(recursive: true); + await source.copy(destination.path); + + if (makeExecutable) { + await _setExecutablePermissions(destination); + } + + _log.info( + 'Normalized macOS artefact ${source.path} -> ${destination.path}', + ); + } + + Future _replaceManagedDirectories({ + required Directory sourceRoot, + required Directory destinationRoot, + }) async { + for (final directoryName in _managedDirectoryNames) { + final destinationDirectory = Directory( + path.join(destinationRoot.path, directoryName), + ); + if (destinationDirectory.existsSync()) { + await destinationDirectory.delete(recursive: true); + } + + final sourceDirectory = Directory( + path.join(sourceRoot.path, directoryName), + ); + if (sourceDirectory.existsSync()) { + await _copyDirectory( + sourceDirectory: sourceDirectory, + destinationDirectory: destinationDirectory, + ); + } + } + } + + Future _copyDirectory({ + required Directory sourceDirectory, + required Directory destinationDirectory, + }) async { + await destinationDirectory.create(recursive: true); + + await for (final entity in sourceDirectory.list( + recursive: true, + followLinks: false, + )) { + final relativePath = path.relative( + entity.path, + from: sourceDirectory.path, + ); + final destinationPath = path.join( + destinationDirectory.path, + relativePath, + ); + + if (entity is Directory) { + await Directory(destinationPath).create(recursive: true); + continue; + } + + if (entity is File) { + final destinationFile = File(destinationPath); + await destinationFile.parent.create(recursive: true); + await entity.copy(destinationFile.path); + + if (path.basename(destinationFile.path) == 'kdf') { + await _setExecutablePermissions(destinationFile); + } + } + } + } + + Future _setExecutablePermissions(File file) async { + if (Platform.isWindows) { + return; + } + + final chmodResult = await Process.run('chmod', ['+x', file.path]); + if (chmodResult.exitCode != 0) { + throw ProcessException( + 'chmod', + ['+x', file.path], + chmodResult.stderr.toString(), + chmodResult.exitCode, + ); + } + } +} diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/fetch_defi_api_build_step.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/fetch_defi_api_build_step.dart index 37ee24c62..0fad0e757 100644 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/fetch_defi_api_build_step.dart +++ b/packages/komodo_wallet_build_transformer/lib/src/steps/fetch_defi_api_build_step.dart @@ -5,6 +5,7 @@ import 'package:crypto/crypto.dart'; import 'package:komodo_wallet_build_transformer/src/build_step.dart'; import 'package:komodo_wallet_build_transformer/src/steps/defi_api_build_step/artefact_downloader.dart'; import 'package:komodo_wallet_build_transformer/src/steps/defi_api_build_step/artefact_downloader_factory.dart'; +import 'package:komodo_wallet_build_transformer/src/steps/defi_api_build_step/macos_artefact_layout_normalizer.dart'; import 'package:komodo_wallet_build_transformer/src/steps/defi_api_build_step/node_path.dart'; import 'package:komodo_wallet_build_transformer/src/steps/models/api/api_build_platform_config.dart'; import 'package:komodo_wallet_build_transformer/src/steps/models/build_config.dart'; @@ -209,9 +210,11 @@ class FetchDefiApiStep extends BuildStep { ); if (await _verifyChecksum(zipFilePath, platform)) { - await downloader.extractArtefact( + await _extractAndInstallArtefact( + downloader: downloader, filePath: zipFilePath, destinationFolder: destinationFolder, + platform: platform, ); _updateLastUpdatedFile(platform, destinationFolder, zipFilePath); _log.info('$platform platform update completed.'); @@ -278,6 +281,7 @@ class FetchDefiApiStep extends BuildStep { final lastUpdatedFile = File( path.join(destinationFolder, '.api_last_updated_$platform'), ); + lastUpdatedFile.parent.createSync(recursive: true); final currentTimestamp = DateTime.now().toIso8601String(); final targetChecksums = List.from( platformsConfig[platform]!.validZipSha256Checksums, @@ -320,7 +324,8 @@ class FetchDefiApiStep extends BuildStep { config.validZipSha256Checksums, ); - // Consider up-to-date only if the stored set exactly matches the target set + // Consider up-to-date only if the stored set exactly matches the + // target set. final storedSet = storedChecksums.toSet(); final targetSet = targetChecksums.toSet(); if (storedSet.length == targetSet.length && @@ -415,6 +420,10 @@ class FetchDefiApiStep extends BuildStep { // TODO: Consider adding npm if it makes a significant difference to // file build size or if it is required for cache-busting. } + if (platform == 'macos') { + // macOS artefacts are normalized into their final layout during install. + return Future.value(); + } if (_isBinaryExecutable(platform)) { _tryRenameExecutable(platform, destinationFolder); _setExecutablePermissions(destinationFolder); @@ -425,6 +434,49 @@ class FetchDefiApiStep extends BuildStep { return Future.value(); } + Future _extractAndInstallArtefact({ + required ArtefactDownloader downloader, + required String filePath, + required String destinationFolder, + required String platform, + }) async { + if (platform != 'macos') { + await downloader.extractArtefact( + filePath: filePath, + destinationFolder: destinationFolder, + ); + return; + } + + final extractedDirectory = await Directory.systemTemp.createTemp( + 'kdf_macos_extract_', + ); + try { + await downloader.extractArtefact( + filePath: filePath, + destinationFolder: extractedDirectory.path, + ); + + final destinationRoot = Directory(path.dirname(destinationFolder)); + final normalizer = MacosArtefactLayoutNormalizer(log: _log); + final result = await normalizer.installFromExtractedDirectory( + extractedDirectory: extractedDirectory, + destinationRoot: destinationRoot, + ); + + _log.info( + 'Installed macOS artefacts: ' + 'executable=${result.hasExecutable}, ' + 'dynamicLibrary=${result.hasDynamicLibrary}, ' + 'staticLibrary=${result.hasStaticLibrary}', + ); + } finally { + if (extractedDirectory.existsSync()) { + await extractedDirectory.delete(recursive: true); + } + } + } + /// if executable is named "mm2" or "mm2.exe", then rename to "kdf" void _tryRenameExecutable(String platform, String destinationFolder) { final executableName = platform == 'windows' ? 'mm2.exe' : 'mm2'; diff --git a/packages/komodo_wallet_build_transformer/test/steps/defi_api_build_step/macos_artefact_layout_normalizer_test.dart b/packages/komodo_wallet_build_transformer/test/steps/defi_api_build_step/macos_artefact_layout_normalizer_test.dart new file mode 100644 index 000000000..aceaf4c8a --- /dev/null +++ b/packages/komodo_wallet_build_transformer/test/steps/defi_api_build_step/macos_artefact_layout_normalizer_test.dart @@ -0,0 +1,152 @@ +import 'dart:io'; + +import 'package:komodo_wallet_build_transformer/src/steps/defi_api_build_step/macos_artefact_layout_normalizer.dart'; +import 'package:test/test.dart'; + +void main() { + group('MacosArtefactLayoutNormalizer', () { + late Directory tempDir; + late Directory extractedDir; + late Directory destinationRoot; + late MacosArtefactLayoutNormalizer normalizer; + + setUp(() { + tempDir = Directory.systemTemp.createTempSync('macos_artefact_test_'); + extractedDir = Directory('${tempDir.path}/extracted'); + destinationRoot = Directory('${tempDir.path}/macos'); + extractedDir.createSync(recursive: true); + destinationRoot.createSync(recursive: true); + normalizer = MacosArtefactLayoutNormalizer(); + }); + + tearDown(() { + if (tempDir.existsSync()) { + tempDir.deleteSync(recursive: true); + } + }); + + test('installs executable archives into macos/bin/kdf', () async { + final executable = File('${extractedDir.path}/nested/kdf'); + await executable.parent.create(recursive: true); + await executable.writeAsString('executable'); + + final result = await normalizer.installFromExtractedDirectory( + extractedDirectory: extractedDir, + destinationRoot: destinationRoot, + ); + + expect(result.hasExecutable, isTrue); + expect(result.hasDynamicLibrary, isFalse); + expect(result.hasStaticLibrary, isFalse); + expect( + File('${destinationRoot.path}/bin/kdf').readAsStringSync(), + equals('executable'), + ); + }); + + test( + 'installs static archives into macos/Frameworks/libkdflib.a', + () async { + final staticLibrary = File('${extractedDir.path}/libkdflib.a'); + await staticLibrary.writeAsString('static-library'); + + final result = await normalizer.installFromExtractedDirectory( + extractedDirectory: extractedDir, + destinationRoot: destinationRoot, + ); + + expect(result.hasExecutable, isFalse); + expect(result.hasDynamicLibrary, isFalse); + expect(result.hasStaticLibrary, isTrue); + expect( + File( + '${destinationRoot.path}/Frameworks/libkdflib.a', + ).readAsStringSync(), + equals('static-library'), + ); + }, + ); + + test('installs dynamic libraries into macos/lib/libkdflib.dylib', () async { + final dynamicLibrary = File('${extractedDir.path}/lib/libkdflib.dylib'); + await dynamicLibrary.parent.create(recursive: true); + await dynamicLibrary.writeAsString('dynamic-library'); + + final result = await normalizer.installFromExtractedDirectory( + extractedDirectory: extractedDir, + destinationRoot: destinationRoot, + ); + + expect(result.hasExecutable, isFalse); + expect(result.hasDynamicLibrary, isTrue); + expect(result.hasStaticLibrary, isFalse); + expect( + File('${destinationRoot.path}/lib/libkdflib.dylib').readAsStringSync(), + equals('dynamic-library'), + ); + }); + + test( + 'renames legacy mm2 artefacts into the canonical macOS layout', + () async { + final executable = File('${extractedDir.path}/legacy/mm2'); + final dynamicLibrary = File('${extractedDir.path}/legacy/libmm2.dylib'); + final staticLibrary = File('${extractedDir.path}/legacy/libmm2.a'); + + await executable.parent.create(recursive: true); + await executable.writeAsString('mm2-executable'); + await dynamicLibrary.writeAsString('mm2-dylib'); + await staticLibrary.writeAsString('mm2-static'); + + final result = await normalizer.installFromExtractedDirectory( + extractedDirectory: extractedDir, + destinationRoot: destinationRoot, + ); + + expect(result.hasExecutable, isTrue); + expect(result.hasDynamicLibrary, isTrue); + expect(result.hasStaticLibrary, isTrue); + expect( + File('${destinationRoot.path}/bin/kdf').readAsStringSync(), + equals('mm2-executable'), + ); + expect( + File( + '${destinationRoot.path}/lib/libkdflib.dylib', + ).readAsStringSync(), + equals('mm2-dylib'), + ); + expect( + File( + '${destinationRoot.path}/Frameworks/libkdflib.a', + ).readAsStringSync(), + equals('mm2-static'), + ); + }, + ); + + test( + 'invalid extracted layouts do not overwrite existing artefacts', + () async { + final existingExecutable = File('${destinationRoot.path}/bin/kdf'); + await existingExecutable.parent.create(recursive: true); + await existingExecutable.writeAsString('existing-executable'); + + await File('${extractedDir.path}/README.txt').writeAsString('invalid'); + + await expectLater( + () => normalizer.installFromExtractedDirectory( + extractedDirectory: extractedDir, + destinationRoot: destinationRoot, + ), + throwsStateError, + ); + + expect( + existingExecutable.readAsStringSync(), + equals('existing-executable'), + ); + }, + ); + }); +} diff --git a/packages/komodo_wallet_build_transformer/test/steps/models/api/api_file_matching_config_test.dart b/packages/komodo_wallet_build_transformer/test/steps/models/api/api_file_matching_config_test.dart new file mode 100644 index 000000000..4ed48c415 --- /dev/null +++ b/packages/komodo_wallet_build_transformer/test/steps/models/api/api_file_matching_config_test.dart @@ -0,0 +1,73 @@ +import 'package:komodo_wallet_build_transformer/src/steps/models/api/api_file_matching_config.dart'; +import 'package:test/test.dart'; + +void main() { + group('ApiFileMatchingConfig.choosePreferred', () { + const macosArchivePattern = + '^(?:kdf-macos-universal2-[a-f0-9]{7,40}|' + 'kdf_[a-f0-9]{7,40}-mac-universal|' + r'libkdf-macos-universal2-[a-f0-9]{7,40})\.zip$'; + + test('prefers GitHub macOS libkdf archives over executable archives', () { + final config = ApiFileMatchingConfig( + matchingPattern: macosArchivePattern, + matchingPreference: const [ + 'libkdf-macos-universal2', + 'kdf-macos-universal2', + 'kdf_', + 'universal2', + 'mac-arm64', + ], + ); + + final preferred = config.choosePreferred(const [ + 'kdf-macos-universal2-d56a7bc.zip', + 'libkdf-macos-universal2-d56a7bc.zip', + ]); + + expect(preferred, equals('libkdf-macos-universal2-d56a7bc.zip')); + }); + + test( + 'falls back to the executable macOS archive when libkdf is absent', + () { + final config = ApiFileMatchingConfig( + matchingPattern: macosArchivePattern, + matchingPreference: const [ + 'libkdf-macos-universal2', + 'kdf-macos-universal2', + 'kdf_', + 'universal2', + 'mac-arm64', + ], + ); + + final preferred = config.choosePreferred(const [ + 'kdf-macos-universal2-d56a7bc.zip', + ]); + + expect(preferred, equals('kdf-macos-universal2-d56a7bc.zip')); + }, + ); + + test('keeps CI/mac-universal fallback deterministic', () { + final config = ApiFileMatchingConfig( + matchingPattern: macosArchivePattern, + matchingPreference: const [ + 'libkdf-macos-universal2', + 'kdf-macos-universal2', + 'kdf_', + 'universal2', + 'mac-arm64', + ], + ); + + final preferred = config.choosePreferred(const [ + 'kdf_d56a7bc-mac-universal.zip', + 'kdf_d56a7bc-mac-universal.zip.backup', + ]); + + expect(preferred, equals('kdf_d56a7bc-mac-universal.zip')); + }); + }); +}