runRegenerateHashesCli function

Future<void> runRegenerateHashesCli(
  1. List<String> args, {
  2. required String defaultVersion,
  3. required PrebuiltReleaseConfig releaseConfigForVersion(
    1. String version
    ),
  4. String hashesFilePath = 'lib/src/hook_helpers/hashes.dart',
  5. String? versionFilePath = 'lib/src/hook_helpers/version.dart',
  6. String versionConstName = 'releaseVersion',
  7. String licenseHeader = '// Copyright 2026 Moritz Sümmermann. Licensed under the Apache License,\n' '// Version 2.0. See the LICENSE file for details.\n',
  8. List<TargetSpec> targets = supportedTargets,
  9. List<bool> staticModes = const [false, true],
})

CLI runner for tool/regenerate_hashes.dart.

Computes SHA-256 digests for all targets and staticModes either from a local directory (args[1], e.g. in CI before/during a release) or by downloading from the release URLs returned by releaseConfigForVersion. Writes the resulting hashes to hashesFilePath (and optionally updates versionFilePath).

Implementation

Future<void> runRegenerateHashesCli(
  List<String> args, {
  required String defaultVersion,
  required PrebuiltReleaseConfig Function(String version)
  releaseConfigForVersion,
  String hashesFilePath = 'lib/src/hook_helpers/hashes.dart',
  String? versionFilePath = 'lib/src/hook_helpers/version.dart',
  String versionConstName = 'releaseVersion',
  String licenseHeader =
      '// Copyright 2026 Moritz Sümmermann. Licensed under the Apache License,\n'
      '// Version 2.0. See the LICENSE file for details.\n',
  List<TargetSpec> targets = supportedTargets,
  List<bool> staticModes = const [false, true],
}) async {
  final version = args.isNotEmpty ? args[0] : defaultVersion;
  final localDir = args.length > 1 ? Directory(args[1]) : null;
  final releaseConfig = releaseConfigForVersion(version);
  final httpClient = localDir == null
      ? (HttpClient()..findProxy = HttpClient.findProxyFromEnvironment)
      : null;

  stdout.writeln('Checking hashes for version $version...');
  final fileHashes = <String, String>{};

  try {
    for (final (os, arch, iosSdk) in targets) {
      for (final static in staticModes) {
        final assetName = releaseConfig.resolveAssetName(
          os,
          arch,
          iosSdk: iosSdk,
          static: static,
        );
        if (localDir != null) {
          final file = File('${localDir.path}/$assetName');
          if (!await file.exists()) {
            stdout.writeln('  Skipping missing local file: ${file.path}');
            continue;
          }
          final fileHash = sha256.convert(await file.readAsBytes()).toString();
          fileHashes[assetName] = fileHash;
          stdout.writeln('  $assetName: $fileHash');
          continue;
        }

        final uri = releaseConfig.resolveDownloadUri(version, assetName);
        stdout.writeln('Fetching from $uri...');
        try {
          final request = await httpClient!.getUrl(uri);
          final response = await request.close();
          if (response.statusCode != 200) {
            stdout.writeln('  Skipping: status ${response.statusCode}');
            await response.drain<void>();
            continue;
          }
          final builder = BytesBuilder(copy: false);
          await response.forEach(builder.add);
          final fileHash = sha256.convert(builder.takeBytes()).toString();
          fileHashes[assetName] = fileHash;
          stdout.writeln('  $assetName: $fileHash');
        } catch (e) {
          stdout.writeln('  Error fetching $uri: $e');
        }
      }
    }
  } finally {
    httpClient?.close(force: true);
  }

  final filesToFormat = <String>[hashesFilePath];

  if (versionFilePath != null) {
    await File(versionFilePath).writeAsString(
      '$licenseHeader\n'
      "const $versionConstName = '$version';\n",
    );
    filesToFormat.add(versionFilePath);
  }

  final versionFileBasename = versionFilePath?.split('/').last;
  final buffer = StringBuffer();
  if (licenseHeader.isNotEmpty) {
    buffer.write(licenseHeader);
    if (!licenseHeader.endsWith('\n')) buffer.writeln();
    buffer.writeln();
  }
  buffer
    ..writeln('// coverage:ignore-file')
    ..writeln('// THIS FILE IS GENERATED BY `tool/regenerate_hashes.dart`.')
    ..writeln();
  if (versionFileBasename != null) {
    buffer
      ..writeln("import '$versionFileBasename';")
      ..writeln()
      ..writeln('const version = $versionConstName;')
      ..writeln();
  } else {
    buffer
      ..writeln("const version = '$version';")
      ..writeln();
  }
  buffer
    ..writeln('/// Mapping from release asset name to SHA-256 hash.')
    ..writeln('const fileHashes = <String, String>{');

  for (final entry in fileHashes.entries) {
    buffer.writeln("  '${entry.key}':");
    buffer.writeln("      '${entry.value}',");
  }
  buffer.writeln('};');

  await File(hashesFilePath).writeAsString(buffer.toString());
  await Process.run(Platform.resolvedExecutable, ['format', ...filesToFormat]);
  stdout.writeln(
    'Updated $hashesFilePath with ${fileHashes.length} hashes.',
  );
}