mirror of
https://mirror.ghproxy.com/https://github.com/StarCitizenToolBox/app.git
synced 2024-12-24 10:33:43 +08:00
310 lines
9.9 KiB
Dart
310 lines
9.9 KiB
Dart
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||
|
|
||
|
import 'dart:io';
|
||
|
|
||
|
import 'package:collection/collection.dart';
|
||
|
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||
|
import 'package:hex/hex.dart';
|
||
|
import 'package:logging/logging.dart';
|
||
|
import 'package:path/path.dart' as path;
|
||
|
import 'package:source_span/source_span.dart';
|
||
|
import 'package:yaml/yaml.dart';
|
||
|
|
||
|
import 'builder.dart';
|
||
|
import 'environment.dart';
|
||
|
import 'rustup.dart';
|
||
|
|
||
|
final _log = Logger('options');
|
||
|
|
||
|
/// A class for exceptions that have source span information attached.
|
||
|
class SourceSpanException implements Exception {
|
||
|
// This is a getter so that subclasses can override it.
|
||
|
/// A message describing the exception.
|
||
|
String get message => _message;
|
||
|
final String _message;
|
||
|
|
||
|
// This is a getter so that subclasses can override it.
|
||
|
/// The span associated with this exception.
|
||
|
///
|
||
|
/// This may be `null` if the source location can't be determined.
|
||
|
SourceSpan? get span => _span;
|
||
|
final SourceSpan? _span;
|
||
|
|
||
|
SourceSpanException(this._message, this._span);
|
||
|
|
||
|
/// Returns a string representation of `this`.
|
||
|
///
|
||
|
/// [color] may either be a [String], a [bool], or `null`. If it's a string,
|
||
|
/// it indicates an ANSI terminal color escape that should be used to
|
||
|
/// highlight the span's text. If it's `true`, it indicates that the text
|
||
|
/// should be highlighted using the default color. If it's `false` or `null`,
|
||
|
/// it indicates that the text shouldn't be highlighted.
|
||
|
@override
|
||
|
String toString({Object? color}) {
|
||
|
if (span == null) return message;
|
||
|
return 'Error on ${span!.message(message, color: color)}';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
enum Toolchain {
|
||
|
stable,
|
||
|
beta,
|
||
|
nightly,
|
||
|
}
|
||
|
|
||
|
class CargoBuildOptions {
|
||
|
final Toolchain toolchain;
|
||
|
final List<String> flags;
|
||
|
|
||
|
CargoBuildOptions({
|
||
|
required this.toolchain,
|
||
|
required this.flags,
|
||
|
});
|
||
|
|
||
|
static Toolchain _toolchainFromNode(YamlNode node) {
|
||
|
if (node case YamlScalar(value: String name)) {
|
||
|
final toolchain =
|
||
|
Toolchain.values.firstWhereOrNull((element) => element.name == name);
|
||
|
if (toolchain != null) {
|
||
|
return toolchain;
|
||
|
}
|
||
|
}
|
||
|
throw SourceSpanException(
|
||
|
'Unknown toolchain. Must be one of ${Toolchain.values.map((e) => e.name)}.',
|
||
|
node.span);
|
||
|
}
|
||
|
|
||
|
static CargoBuildOptions parse(YamlNode node) {
|
||
|
if (node is! YamlMap) {
|
||
|
throw SourceSpanException('Cargo options must be a map', node.span);
|
||
|
}
|
||
|
Toolchain toolchain = Toolchain.stable;
|
||
|
List<String> flags = [];
|
||
|
for (final MapEntry(:key, :value) in node.nodes.entries) {
|
||
|
if (key case YamlScalar(value: 'toolchain')) {
|
||
|
toolchain = _toolchainFromNode(value);
|
||
|
} else if (key case YamlScalar(value: 'extra_flags')) {
|
||
|
if (value case YamlList(nodes: List<YamlNode> list)) {
|
||
|
if (list.every((element) {
|
||
|
if (element case YamlScalar(value: String _)) {
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
})) {
|
||
|
flags = list.map((e) => e.value as String).toList();
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
throw SourceSpanException(
|
||
|
'Extra flags must be a list of strings', value.span);
|
||
|
} else {
|
||
|
throw SourceSpanException(
|
||
|
'Unknown cargo option type. Must be "toolchain" or "extra_flags".',
|
||
|
key.span);
|
||
|
}
|
||
|
}
|
||
|
return CargoBuildOptions(toolchain: toolchain, flags: flags);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
extension on YamlMap {
|
||
|
/// Map that extracts keys so that we can do map case check on them.
|
||
|
Map<dynamic, YamlNode> get valueMap =>
|
||
|
nodes.map((key, value) => MapEntry(key.value, value));
|
||
|
}
|
||
|
|
||
|
class PrecompiledBinaries {
|
||
|
final String uriPrefix;
|
||
|
final PublicKey publicKey;
|
||
|
|
||
|
PrecompiledBinaries({
|
||
|
required this.uriPrefix,
|
||
|
required this.publicKey,
|
||
|
});
|
||
|
|
||
|
static PublicKey _publicKeyFromHex(String key, SourceSpan? span) {
|
||
|
final bytes = HEX.decode(key);
|
||
|
if (bytes.length != 32) {
|
||
|
throw SourceSpanException(
|
||
|
'Invalid public key. Must be 32 bytes long.', span);
|
||
|
}
|
||
|
return PublicKey(bytes);
|
||
|
}
|
||
|
|
||
|
static PrecompiledBinaries parse(YamlNode node) {
|
||
|
if (node case YamlMap(valueMap: Map<dynamic, YamlNode> map)) {
|
||
|
if (map
|
||
|
case {
|
||
|
'url_prefix': YamlNode urlPrefixNode,
|
||
|
'public_key': YamlNode publicKeyNode,
|
||
|
}) {
|
||
|
final urlPrefix = switch (urlPrefixNode) {
|
||
|
YamlScalar(value: String urlPrefix) => urlPrefix,
|
||
|
_ => throw SourceSpanException(
|
||
|
'Invalid URL prefix value.', urlPrefixNode.span),
|
||
|
};
|
||
|
final publicKey = switch (publicKeyNode) {
|
||
|
YamlScalar(value: String publicKey) =>
|
||
|
_publicKeyFromHex(publicKey, publicKeyNode.span),
|
||
|
_ => throw SourceSpanException(
|
||
|
'Invalid public key value.', publicKeyNode.span),
|
||
|
};
|
||
|
return PrecompiledBinaries(
|
||
|
uriPrefix: urlPrefix,
|
||
|
publicKey: publicKey,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
throw SourceSpanException(
|
||
|
'Invalid precompiled binaries value. '
|
||
|
'Expected Map with "url_prefix" and "public_key".',
|
||
|
node.span);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Cargokit options specified for Rust crate.
|
||
|
class CargokitCrateOptions {
|
||
|
CargokitCrateOptions({
|
||
|
this.cargo = const {},
|
||
|
this.precompiledBinaries,
|
||
|
});
|
||
|
|
||
|
final Map<BuildConfiguration, CargoBuildOptions> cargo;
|
||
|
final PrecompiledBinaries? precompiledBinaries;
|
||
|
|
||
|
static CargokitCrateOptions parse(YamlNode node) {
|
||
|
if (node is! YamlMap) {
|
||
|
throw SourceSpanException('Cargokit options must be a map', node.span);
|
||
|
}
|
||
|
final options = <BuildConfiguration, CargoBuildOptions>{};
|
||
|
PrecompiledBinaries? precompiledBinaries;
|
||
|
|
||
|
for (final entry in node.nodes.entries) {
|
||
|
if (entry
|
||
|
case MapEntry(
|
||
|
key: YamlScalar(value: 'cargo'),
|
||
|
value: YamlNode node,
|
||
|
)) {
|
||
|
if (node is! YamlMap) {
|
||
|
throw SourceSpanException('Cargo options must be a map', node.span);
|
||
|
}
|
||
|
for (final MapEntry(:YamlNode key, :value) in node.nodes.entries) {
|
||
|
if (key case YamlScalar(value: String name)) {
|
||
|
final configuration = BuildConfiguration.values
|
||
|
.firstWhereOrNull((element) => element.name == name);
|
||
|
if (configuration != null) {
|
||
|
options[configuration] = CargoBuildOptions.parse(value);
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
throw SourceSpanException(
|
||
|
'Unknown build configuration. Must be one of ${BuildConfiguration.values.map((e) => e.name)}.',
|
||
|
key.span);
|
||
|
}
|
||
|
} else if (entry.key case YamlScalar(value: 'precompiled_binaries')) {
|
||
|
precompiledBinaries = PrecompiledBinaries.parse(entry.value);
|
||
|
} else {
|
||
|
throw SourceSpanException(
|
||
|
'Unknown cargokit option type. Must be "cargo" or "precompiled_binaries".',
|
||
|
entry.key.span);
|
||
|
}
|
||
|
}
|
||
|
return CargokitCrateOptions(
|
||
|
cargo: options,
|
||
|
precompiledBinaries: precompiledBinaries,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
static CargokitCrateOptions load({
|
||
|
required String manifestDir,
|
||
|
}) {
|
||
|
final uri = Uri.file(path.join(manifestDir, "cargokit.yaml"));
|
||
|
final file = File.fromUri(uri);
|
||
|
if (file.existsSync()) {
|
||
|
final contents = loadYamlNode(file.readAsStringSync(), sourceUrl: uri);
|
||
|
return parse(contents);
|
||
|
} else {
|
||
|
return CargokitCrateOptions();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class CargokitUserOptions {
|
||
|
// When Rustup is installed always build locally unless user opts into
|
||
|
// using precompiled binaries.
|
||
|
static bool defaultUsePrecompiledBinaries() {
|
||
|
return Rustup.executablePath() == null;
|
||
|
}
|
||
|
|
||
|
CargokitUserOptions({
|
||
|
required this.usePrecompiledBinaries,
|
||
|
required this.verboseLogging,
|
||
|
});
|
||
|
|
||
|
CargokitUserOptions._()
|
||
|
: usePrecompiledBinaries = defaultUsePrecompiledBinaries(),
|
||
|
verboseLogging = false;
|
||
|
|
||
|
static CargokitUserOptions parse(YamlNode node) {
|
||
|
if (node is! YamlMap) {
|
||
|
throw SourceSpanException('Cargokit options must be a map', node.span);
|
||
|
}
|
||
|
bool usePrecompiledBinaries = defaultUsePrecompiledBinaries();
|
||
|
bool verboseLogging = false;
|
||
|
|
||
|
for (final entry in node.nodes.entries) {
|
||
|
if (entry.key case YamlScalar(value: 'use_precompiled_binaries')) {
|
||
|
if (entry.value case YamlScalar(value: bool value)) {
|
||
|
usePrecompiledBinaries = value;
|
||
|
continue;
|
||
|
}
|
||
|
throw SourceSpanException(
|
||
|
'Invalid value for "use_precompiled_binaries". Must be a boolean.',
|
||
|
entry.value.span);
|
||
|
} else if (entry.key case YamlScalar(value: 'verbose_logging')) {
|
||
|
if (entry.value case YamlScalar(value: bool value)) {
|
||
|
verboseLogging = value;
|
||
|
continue;
|
||
|
}
|
||
|
throw SourceSpanException(
|
||
|
'Invalid value for "verbose_logging". Must be a boolean.',
|
||
|
entry.value.span);
|
||
|
} else {
|
||
|
throw SourceSpanException(
|
||
|
'Unknown cargokit option type. Must be "use_precompiled_binaries" or "verbose_logging".',
|
||
|
entry.key.span);
|
||
|
}
|
||
|
}
|
||
|
return CargokitUserOptions(
|
||
|
usePrecompiledBinaries: usePrecompiledBinaries,
|
||
|
verboseLogging: verboseLogging,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
static CargokitUserOptions load() {
|
||
|
String fileName = "cargokit_options.yaml";
|
||
|
var userProjectDir = Directory(Environment.rootProjectDir);
|
||
|
|
||
|
while (userProjectDir.parent.path != userProjectDir.path) {
|
||
|
final configFile = File(path.join(userProjectDir.path, fileName));
|
||
|
if (configFile.existsSync()) {
|
||
|
final contents = loadYamlNode(
|
||
|
configFile.readAsStringSync(),
|
||
|
sourceUrl: configFile.uri,
|
||
|
);
|
||
|
final res = parse(contents);
|
||
|
if (res.verboseLogging) {
|
||
|
_log.info('Found user options file at ${configFile.path}');
|
||
|
}
|
||
|
return res;
|
||
|
}
|
||
|
userProjectDir = userProjectDir.parent;
|
||
|
}
|
||
|
return CargokitUserOptions._();
|
||
|
}
|
||
|
|
||
|
final bool usePrecompiledBinaries;
|
||
|
final bool verboseLogging;
|
||
|
}
|