alef 0.34.9

Opinionated polyglot binding generator for Rust libraries
Documentation
// Generated by alef. Do not edit by hand.
// Shared native-library resolution for {{ crate_name }}: cache-path computation,
// release-asset download, SHA-256 verification, and tar.gz extraction.
//
// This helper is imported by both `bin/download_libs.dart` (the install-time
// fetcher) and the flutter_rust_bridge loader override in
// `frb_generated.dart` (the first-use resolver), so the cache-path and checksum
// logic lives in exactly one place.

import 'dart:ffi' show Abi;
import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' show sha256;
import 'package:http/http.dart' as http;

/// Package version baked at codegen time. The release tag and the cache
/// subdirectory both derive from this, so a downloaded native can never
/// mismatch the package version: an upgrade changes the cache path and forces
/// a fresh download.
const String nativeModuleVersion = '{{ version }}';

/// GitHub repository base URL (e.g. `https://github.com/owner/repo`).
const String nativeRepoUrl = '{{ repo_url }}';

/// Release-asset name prefix and cache namespace (the package name).
const String nativeAssetPrefix = '{{ crate_name }}';

/// Native library file stem (without the platform prefix/suffix).
const String nativeLibStem = '{{ lib_stem }}';

/// Environment variable that, when set to a directory, short-circuits all
/// resolution and native download (dev/test path, no network). The directory
/// is expected to contain the platform native library directly.
const String nativeLibDirEnv = 'FRB_DART_LOAD_EXTERNAL_LIBRARY_NATIVE_LIB_DIR';

/// Version with any leading `v` stripped (asset names embed the bare version).
String nativeCleanVersion() {
  return nativeModuleVersion.startsWith('v') ? nativeModuleVersion.substring(1) : nativeModuleVersion;
}

/// Runtime identifier used for the cache subdirectory and the loader search
/// path (`macos-arm64`, `linux-x64`, `windows-arm64`, …), or `null` on an
/// unsupported platform.
String? nativeComputeRid() {
  final abi = Abi.current();
  final os = Platform.operatingSystem;
  if (os == 'linux') {
    if (abi == Abi.linuxX64) return 'linux-x64';
    if (abi == Abi.linuxArm64) return 'linux-arm64';
  } else if (os == 'macos') {
    if (abi == Abi.macosX64) return 'macos-x64';
    if (abi == Abi.macosArm64) return 'macos-arm64';
  } else if (os == 'windows') {
    if (abi == Abi.windowsX64) return 'windows-x64';
    if (abi == Abi.windowsArm64) return 'windows-arm64';
  }
  return null;
}

/// Platform-specific native library filename (`lib<stem>_dart.dylib`, etc.).
String nativeLibFilename() {
  if (Platform.isMacOS) return 'lib${nativeLibStem}_dart.dylib';
  if (Platform.isWindows) return '${nativeLibStem}_dart.dll';
  return 'lib${nativeLibStem}_dart.so';
}

/// User-cache base directory, resolved from `XDG_CACHE_HOME`, then the
/// platform default (`~/Library/Caches` on macOS, `%LOCALAPPDATA%` on Windows,
/// `~/.cache` elsewhere). Returns `null` if no base can be determined.
String? nativeCacheBase() {
  final env = Platform.environment;
  final xdg = env['XDG_CACHE_HOME'];
  if (xdg != null && xdg.isNotEmpty) return xdg;
  if (Platform.isMacOS) {
    final home = env['HOME'];
    if (home != null && home.isNotEmpty) {
      return '$home${Platform.pathSeparator}Library${Platform.pathSeparator}Caches';
    }
  } else if (Platform.isWindows) {
    final localAppData = env['LOCALAPPDATA'];
    if (localAppData != null && localAppData.isNotEmpty) return localAppData;
  }
  final home = env['HOME'];
  if (home != null && home.isNotEmpty) {
    return '$home${Platform.pathSeparator}.cache';
  }
  return null;
}

/// Versioned per-RID cache directory `<cache>/<prefix>/<version>/<rid>/`, or
/// `null` when the cache base or RID cannot be resolved.
String? nativeCacheDir() {
  final base = nativeCacheBase();
  final rid = nativeComputeRid();
  if (base == null || rid == null) return null;
  final sep = Platform.pathSeparator;
  return '$base$sep$nativeAssetPrefix$sep${nativeCleanVersion()}$sep$rid';
}

/// Absolute path of the cached native library, or `null` if the cache
/// directory cannot be resolved.
String? nativeCachedLibPath() {
  final dir = nativeCacheDir();
  if (dir == null) return null;
  return '$dir${Platform.pathSeparator}${nativeLibFilename()}';
}

/// Release-asset blob name for the current platform (`macos-x86_64`,
/// `linux-aarch64`, …) — the asset filenames use `x86_64`/`aarch64` while the
/// cache RID uses `x64`/`arm64`.
String nativeAssetBlob() {
  final os = _osName();
  final abi = Abi.current();
  final arch = (abi == Abi.macosArm64 ||
          abi == Abi.linuxArm64 ||
          abi == Abi.windowsArm64)
      ? 'aarch64'
      : 'x86_64';
  return '$os-$arch';
}

String _osName() {
  if (Platform.isMacOS) return 'macos';
  if (Platform.isWindows) return 'windows';
  if (Platform.isLinux) return 'linux';
  throw UnsupportedError('Unsupported OS: ${Platform.operatingSystem}');
}

/// Base URL (without the trailing extension) for the current platform's
/// release asset: `<repo>/releases/download/v<version>/<prefix>-dart-v<version>-<blob>`.
String nativeAssetUrlBase() {
  final version = nativeCleanVersion();
  final assetStem = '$nativeAssetPrefix-dart-v$version-${nativeAssetBlob()}';
  return '$nativeRepoUrl/releases/download/v$version/$assetStem';
}

/// Download the platform native library into the versioned user cache,
/// verifying its SHA-256 against the published `.sha256` sidecar before
/// extraction. Returns the absolute path of the extracted library.
///
/// Throws a [StateError] on checksum mismatch and an [Exception] on any HTTP
/// or extraction failure.
Future<String> nativeDownloadAndCacheLibrary() async {
  final cacheDir = nativeCacheDir();
  if (cacheDir == null) {
    throw StateError(
      'Unable to determine a user-cache directory for the $nativeAssetPrefix native library. '
      'Set XDG_CACHE_HOME, or override with $nativeLibDirEnv.',
    );
  }
  final libPath = '$cacheDir${Platform.pathSeparator}${nativeLibFilename()}';
  if (File(libPath).existsSync()) {
    return libPath;
  }

  final urlBase = nativeAssetUrlBase();
  final tarballUrl = '$urlBase.tar.gz';
  final sha256Url = '$tarballUrl.sha256';

  // Download the tarball.
  final tarballResponse = await http.get(Uri.parse(tarballUrl));
  if (tarballResponse.statusCode != 200) {
    throw Exception(
      'HTTP ${tarballResponse.statusCode}: failed to download $tarballUrl',
    );
  }
  final Uint8List tarballBytes = tarballResponse.bodyBytes;

  // Download the `.sha256` sidecar and verify before extracting.
  final shaResponse = await http.get(Uri.parse(sha256Url));
  if (shaResponse.statusCode != 200) {
    throw Exception(
      'HTTP ${shaResponse.statusCode}: failed to download checksum $sha256Url',
    );
  }
  final expected = _parseSha256Sidecar(shaResponse.body);
  final actual = sha256.convert(tarballBytes).toString();
  if (expected != actual) {
    throw StateError(
      'SHA-256 mismatch for $tarballUrl: expected $expected, got $actual. '
      'Refusing to install a native library that does not match its published checksum.',
    );
  }

  await Directory(cacheDir).create(recursive: true);
  await _extractTarGz(tarballBytes, cacheDir);
  return libPath;
}

/// Parse the hex digest from a `sha256sum`-style sidecar body, which is either
/// a bare digest or `<digest>  <filename>`.
String _parseSha256Sidecar(String body) {
  final trimmed = body.trim();
  final firstToken = trimmed.split(RegExp(r'\s+')).first;
  return firstToken.toLowerCase();
}

Future<void> _extractTarGz(Uint8List bytes, String dstDir) async {
  // Extract via the OS `tar` (ubiquitous on Unix/macOS; bundled on Windows 10+),
  // avoiding a pure-Dart tar+gzip dependency.
  final tempFile = File('$dstDir${Platform.pathSeparator}.download.tar.gz');
  try {
    await Directory(dstDir).create(recursive: true);
    await tempFile.writeAsBytes(bytes);
    final result = await Process.run('tar', ['-xzf', tempFile.path, '-C', dstDir]);
    if (result.exitCode != 0) {
      throw Exception('tar extraction failed (exit ${result.exitCode}): ${result.stderr}');
    }
  } finally {
    if (tempFile.existsSync()) {
      await tempFile.delete();
    }
  }
}