blob: e20940797da4817a5d036040ca2d504715273828 [file]
// Copyright (c) 2024, 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:io';
import 'package:code_assets/code_assets.dart';
import 'package:crypto/crypto.dart' show sha256;
import 'package:hooks/hooks.dart';
import 'package:intl4x/src/hook_helpers/build_libs.g.dart' show buildLib;
import 'package:intl4x/src/hook_helpers/build_options.dart'
show BuildModeEnum, BuildOptions;
import 'package:intl4x/src/hook_helpers/hashes.dart' show fileHashes;
import 'package:intl4x/src/hook_helpers/shared.dart' show assetId, package;
import 'package:intl4x/src/hook_helpers/version.dart' show version;
void main(List<String> args) async {
await build(args, (input, output) async {
BuildOptions buildOptions;
try {
buildOptions = BuildOptions.fromDefines(input.userDefines);
print('Got build options: ${buildOptions.toJson()}');
} catch (e) {
throw ArgumentError('''
Error: $e
Set the build mode with either `fetch`, `local`, or `checkout` by writing into your pubspec:
* fetch: Fetch the precompiled binary from a CDN.
```
hooks:
user_defines:
intl4x:
buildMode: fetch
```
* local: Use a locally existing binary at the environment variable `LOCAL_ICU4X_BINARY`.
```
hooks:
user_defines:
intl4x:
buildMode: local
localDylibPath: path/to/dylib.so
```
* checkout: Build a fresh library from a local git checkout of the icu4x repository.
```
hooks:
user_defines:
intl4x:
buildMode: checkout
checkoutPath: path/to/checkout
```
''');
}
print('Read build options: ${buildOptions.toJson()}');
final treeshake = buildOptions.treeshake ?? false;
final buildMode = switch (buildOptions.buildMode) {
BuildModeEnum.local => LocalMode(
input,
buildOptions.localDylibPath,
treeshake,
),
BuildModeEnum.checkout => CheckoutMode(
input,
buildOptions.checkoutPath,
treeshake,
),
BuildModeEnum.fetch => FetchMode(input, treeshake),
};
final builtLibrary = await buildMode.build();
output.assets.code.add(
CodeAsset(
package: package,
name: assetId,
linkMode: DynamicLoadingBundled(),
file: builtLibrary,
),
routing:
input.config.linkingEnabled
? const ToLinkHook(package)
: const ToAppBundle(),
);
output.addDependencies(buildMode.dependencies);
output.addDependency(input.packageRoot.resolve('pubspec.yaml'));
});
}
sealed class BuildMode {
final BuildInput input;
final bool treeshake;
const BuildMode(this.input, this.treeshake);
List<Uri> get dependencies;
Future<Uri> build();
}
final class FetchMode extends BuildMode {
FetchMode(super.input, super.treeshake);
final httpClient = HttpClient();
@override
Future<Uri> build() async {
print('Running in `fetch` mode');
final targetOS = input.config.code.targetOS;
final targetArchitecture = input.config.code.targetArchitecture;
final libraryType =
input.config.buildStatic(treeshake) ? 'static' : 'dynamic';
final target = [
targetOS,
targetArchitecture,
libraryType,
].join('_'); //TODO: Add with-data if static
print('Fetching pre-built binary for $version and $target');
final dylibRemoteUri = Uri.parse(
'https://github.com/dart-lang/i18n/releases/download/$version/$target',
);
final library = await fetchToFile(
dylibRemoteUri,
input.outputDirectory.resolve(input.config.filename(treeshake)('icu4x')),
);
final bytes = await library.readAsBytes();
final fileHash = sha256.convert(bytes).toString();
final expectedFileHash =
fileHashes[(targetOS, targetArchitecture, libraryType)];
if (fileHash != expectedFileHash) {
throw Exception(
'The pre-built binary for the target $target at $dylibRemoteUri has a'
' hash of $fileHash, which does not match $expectedFileHash fixed in'
' the build hook of package:intl4x.',
);
}
return library.uri;
}
Future<File> fetchToFile(Uri uri, Uri fileUri) async {
final request = await httpClient.getUrl(uri);
final response = await request.close();
if (response.statusCode != 200) {
throw ArgumentError('The request to $uri failed');
}
final file = File.fromUri(fileUri);
await file.create();
await response.pipe(file.openWrite());
return file;
}
@override
List<Uri> get dependencies => [];
}
final class LocalMode extends BuildMode {
final Uri? localPath;
LocalMode(super.input, this.localPath, super.treeshake);
String get _localLibraryPath {
if (localPath != null) {
return localPath!.toFilePath(windows: Platform.isWindows);
}
throw ArgumentError(
'`LOCAL_ICU4X_BINARY` is empty. '
'If the `ICU4X_BUILD_MODE` is set to `local`, the '
'`LOCAL_ICU4X_BINARY` environment variable must contain the path to '
'the binary.',
);
}
@override
Future<Uri> build() async {
print('Running in `local` mode');
final targetOS = input.config.code.targetOS;
final dylibFileName = targetOS.dylibFileName('icu4x');
final dylibFileUri = input.outputDirectory.resolve(dylibFileName);
final file = File(_localLibraryPath);
if (!(await file.exists())) {
throw FileSystemException('Could not find binary.', _localLibraryPath);
}
await file.copy(dylibFileUri.toFilePath(windows: Platform.isWindows));
return dylibFileUri;
}
@override
List<Uri> get dependencies => [Uri.file(_localLibraryPath)];
}
final class CheckoutMode extends BuildMode {
final Uri? checkoutPath;
CheckoutMode(super.input, this.checkoutPath, super.treeshake);
@override
Future<Uri> build() async {
print('Running in `checkout` mode');
if (checkoutPath == null) {
throw ArgumentError(
'Specify the ICU4X checkout folder with the `checkoutPath` key in your '
'pubspec build options.',
);
}
final builtLib = await buildLib(
input.config.code.targetOS,
input.config.code.targetArchitecture,
input.config.buildStatic(treeshake),
input.config.code.targetOS == OS.iOS &&
input.config.code.iOS.targetSdk == IOSSdk.iPhoneSimulator,
Directory.fromUri(checkoutPath!),
[
'collator',
'datetime',
'list',
'decimal',
'plurals',
'buffer_provider',
'experimental',
'default_components',
'compiled_data',
],
);
return builtLib.uri;
}
@override
List<Uri> get dependencies => [checkoutPath!.resolve('Cargo.lock')];
}
extension on BuildConfig {
bool buildStatic(bool treeshake) =>
code.linkModePreference == LinkModePreference.static ||
(linkingEnabled && treeshake);
String Function(String) filename(bool treeshake) =>
buildStatic(treeshake)
? code.targetOS.staticlibFileName
: code.targetOS.dylibFileName;
}