blob: 85c4407e470116fa83bef2465b1a81e3c4fa8d4b [file] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async' show TimeoutException;
import 'dart:io';
import 'dart:typed_data' show BytesBuilder;
import 'package:code_assets/code_assets.dart';
import 'package:crypto/crypto.dart' show sha256;
import 'package:hooks/hooks.dart';
import 'hashes.dart' show fileHashes, releaseRepository, releaseTag;
import 'targets.dart'
show libraryFileName, releaseAssetName, releaseAssetUrl, targetTripleFor;
/// How long to wait for the connection and for each response.
const _connectionTimeout = Duration(seconds: 30);
/// How long to wait for the whole library to download.
const _downloadTimeout = Duration(minutes: 5);
const _checkoutHint =
'To build boring locally from source instead, set '
'`buildMode: checkout` in your pubspec.yaml under '
'`hooks.user_defines.boring`.';
/// Downloads and verifies the pre-built library for the target of [input] from
/// the GitHub release [releaseTag] of [releaseRepository], caching it in
/// [HookInput.outputDirectoryShared].
///
/// Throws a [BuildError] if no hash is registered in [fileHashes] for the
/// target, if downloading fails, or if the downloaded file's SHA-256 checksum
/// does not match [fileHashes].
Future<Uri> fetchPrebuiltLibrary(
HookInput input, {
required bool static,
}) async {
final targetOS = input.config.code.targetOS;
final targetArch = input.config.code.targetArchitecture;
final iosSdk = targetOS == OS.iOS ? input.config.code.iOS.targetSdk : null;
final assetRemoteName = releaseAssetName(
targetOS,
targetArch,
iosSdk: iosSdk,
static: static,
);
final expectedHash = fileHashes[assetRemoteName];
if (expectedHash == null || expectedHash.isEmpty) {
final target = targetTripleFor(targetOS, targetArch, iosSdk: iosSdk);
throw BuildError(
message:
'No prebuilt boring binary is available for $target '
'($assetRemoteName) in release $releaseTag of $releaseRepository.\n'
'$_checkoutHint',
);
}
final fileName = libraryFileName(targetOS, static: static);
final cachedFile = File.fromUri(
input.outputDirectoryShared
.resolve('$releaseTag/$assetRemoteName/')
.resolve(fileName),
);
if (await cachedFile.exists()) {
final cachedHash = sha256
.convert(await cachedFile.readAsBytes())
.toString();
if (cachedHash == expectedHash) {
stdout.writeln(
'boring: using cached prebuilt binary ($assetRemoteName).',
);
return cachedFile.uri;
}
}
final binaryUrl = releaseAssetUrl(
repository: releaseRepository,
tag: releaseTag,
assetName: assetRemoteName,
);
stdout.writeln('boring: fetching prebuilt binary from $binaryUrl...');
final bytes = await _download(binaryUrl);
final actualHash = sha256.convert(bytes).toString();
if (actualHash != expectedHash) {
throw BuildError(
message:
'SHA256 hash mismatch for prebuilt binary $assetRemoteName.\n'
'Expected: $expectedHash\n'
'Actual: $actualHash\n'
'$_checkoutHint',
);
}
stdout.writeln('boring: verified SHA256 checksum ($actualHash).');
await cachedFile.parent.create(recursive: true);
await cachedFile.writeAsBytes(bytes);
return cachedFile.uri;
}
/// Downloads [url], or throws a [BuildError] if the download fails.
///
/// Honors the `HTTPS_PROXY` family of environment variables, which the hooks
/// runner passes through to hooks, and gives up on a connection that hangs.
Future<List<int>> _download(Uri url) async {
final client = HttpClient()
..connectionTimeout = _connectionTimeout
..findProxy = HttpClient.findProxyFromEnvironment;
try {
final request = await client.getUrl(url).timeout(_connectionTimeout);
final response = await request.close().timeout(_connectionTimeout);
if (response.statusCode != HttpStatus.ok) {
await response.drain<void>();
throw BuildError(
message:
'Failed to download prebuilt boring binary from $url '
'(HTTP ${response.statusCode}).\n'
'$_checkoutHint',
);
}
final builder = BytesBuilder(copy: false);
await response.forEach(builder.add).timeout(_downloadTimeout);
return builder.takeBytes();
} on IOException catch (e) {
throw BuildError(
message:
'Network error downloading prebuilt boring binary from $url: $e\n'
'$_checkoutHint',
);
} on TimeoutException catch (e) {
throw BuildError(
message:
'Timed out downloading prebuilt boring binary from $url: $e\n'
'$_checkoutHint',
);
} finally {
client.close(force: true);
}
}