mirror of
https://ghfast.top/https://github.com/StarCitizenToolBox/app.git
synced 2025-07-07 09:57:45 +08:00
update rust_builder
This commit is contained in:
8
rust_builder/cargokit/build_tool/lib/build_tool.dart
Normal file
8
rust_builder/cargokit/build_tool/lib/build_tool.dart
Normal file
@ -0,0 +1,8 @@
|
||||
/// 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 'src/build_tool.dart' as build_tool;
|
||||
|
||||
Future<void> runMain(List<String> args) async {
|
||||
return build_tool.runMain(args);
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
/// 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 'dart:isolate';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:version/version.dart';
|
||||
|
||||
import 'target.dart';
|
||||
import 'util.dart';
|
||||
|
||||
class AndroidEnvironment {
|
||||
AndroidEnvironment({
|
||||
required this.sdkPath,
|
||||
required this.ndkVersion,
|
||||
required this.minSdkVersion,
|
||||
required this.targetTempDir,
|
||||
required this.target,
|
||||
});
|
||||
|
||||
static void clangLinkerWrapper(List<String> args) {
|
||||
final clang = Platform.environment['_CARGOKIT_NDK_LINK_CLANG'];
|
||||
if (clang == null) {
|
||||
throw Exception(
|
||||
"cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_CLANG env var");
|
||||
}
|
||||
final target = Platform.environment['_CARGOKIT_NDK_LINK_TARGET'];
|
||||
if (target == null) {
|
||||
throw Exception(
|
||||
"cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_TARGET env var");
|
||||
}
|
||||
|
||||
runCommand(clang, [
|
||||
target,
|
||||
...args,
|
||||
]);
|
||||
}
|
||||
|
||||
/// Full path to Android SDK.
|
||||
final String sdkPath;
|
||||
|
||||
/// Full version of Android NDK.
|
||||
final String ndkVersion;
|
||||
|
||||
/// Minimum supported SDK version.
|
||||
final int minSdkVersion;
|
||||
|
||||
/// Target directory for build artifacts.
|
||||
final String targetTempDir;
|
||||
|
||||
/// Target being built.
|
||||
final Target target;
|
||||
|
||||
bool ndkIsInstalled() {
|
||||
final ndkPath = path.join(sdkPath, 'ndk', ndkVersion);
|
||||
final ndkPackageXml = File(path.join(ndkPath, 'package.xml'));
|
||||
return ndkPackageXml.existsSync();
|
||||
}
|
||||
|
||||
void installNdk({
|
||||
required String javaHome,
|
||||
}) {
|
||||
final sdkManagerExtension = Platform.isWindows ? '.bat' : '';
|
||||
final sdkManager = path.join(
|
||||
sdkPath,
|
||||
'cmdline-tools',
|
||||
'latest',
|
||||
'bin',
|
||||
'sdkmanager$sdkManagerExtension',
|
||||
);
|
||||
|
||||
log.info('Installing NDK $ndkVersion');
|
||||
runCommand(sdkManager, [
|
||||
'--install',
|
||||
'ndk;$ndkVersion',
|
||||
], environment: {
|
||||
'JAVA_HOME': javaHome,
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, String>> buildEnvironment() async {
|
||||
final hostArch = Platform.isMacOS
|
||||
? "darwin-x86_64"
|
||||
: (Platform.isLinux ? "linux-x86_64" : "windows-x86_64");
|
||||
|
||||
final ndkPath = path.join(sdkPath, 'ndk', ndkVersion);
|
||||
final toolchainPath = path.join(
|
||||
ndkPath,
|
||||
'toolchains',
|
||||
'llvm',
|
||||
'prebuilt',
|
||||
hostArch,
|
||||
'bin',
|
||||
);
|
||||
|
||||
final minSdkVersion =
|
||||
math.max(target.androidMinSdkVersion!, this.minSdkVersion);
|
||||
|
||||
final exe = Platform.isWindows ? '.exe' : '';
|
||||
|
||||
final arKey = 'AR_${target.rust}';
|
||||
final arValue = ['${target.rust}-ar', 'llvm-ar', 'llvm-ar.exe']
|
||||
.map((e) => path.join(toolchainPath, e))
|
||||
.firstWhereOrNull((element) => File(element).existsSync());
|
||||
if (arValue == null) {
|
||||
throw Exception('Failed to find ar for $target in $toolchainPath');
|
||||
}
|
||||
|
||||
final targetArg = '--target=${target.rust}$minSdkVersion';
|
||||
|
||||
final ccKey = 'CC_${target.rust}';
|
||||
final ccValue = path.join(toolchainPath, 'clang$exe');
|
||||
final cfFlagsKey = 'CFLAGS_${target.rust}';
|
||||
final cFlagsValue = targetArg;
|
||||
|
||||
final cxxKey = 'CXX_${target.rust}';
|
||||
final cxxValue = path.join(toolchainPath, 'clang++$exe');
|
||||
final cxxfFlagsKey = 'CXXFLAGS_${target.rust}';
|
||||
final cxxFlagsValue = targetArg;
|
||||
|
||||
final linkerKey =
|
||||
'cargo_target_${target.rust.replaceAll('-', '_')}_linker'.toUpperCase();
|
||||
|
||||
final ranlibKey = 'RANLIB_${target.rust}';
|
||||
final ranlibValue = path.join(toolchainPath, 'llvm-ranlib$exe');
|
||||
|
||||
final ndkVersionParsed = Version.parse(ndkVersion);
|
||||
final rustFlagsKey = 'CARGO_ENCODED_RUSTFLAGS';
|
||||
final rustFlagsValue = _libGccWorkaround(targetTempDir, ndkVersionParsed);
|
||||
|
||||
final runRustTool =
|
||||
Platform.isWindows ? 'run_build_tool.cmd' : 'run_build_tool.sh';
|
||||
|
||||
final packagePath = (await Isolate.resolvePackageUri(
|
||||
Uri.parse('package:build_tool/buildtool.dart')))!
|
||||
.toFilePath();
|
||||
final selfPath = path.canonicalize(path.join(
|
||||
packagePath,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
runRustTool,
|
||||
));
|
||||
|
||||
// Make sure that run_build_tool is working properly even initially launched directly
|
||||
// through dart run.
|
||||
final toolTempDir =
|
||||
Platform.environment['CARGOKIT_TOOL_TEMP_DIR'] ?? targetTempDir;
|
||||
|
||||
return {
|
||||
arKey: arValue,
|
||||
ccKey: ccValue,
|
||||
cfFlagsKey: cFlagsValue,
|
||||
cxxKey: cxxValue,
|
||||
cxxfFlagsKey: cxxFlagsValue,
|
||||
ranlibKey: ranlibValue,
|
||||
rustFlagsKey: rustFlagsValue,
|
||||
linkerKey: selfPath,
|
||||
// Recognized by main() so we know when we're acting as a wrapper
|
||||
'_CARGOKIT_NDK_LINK_TARGET': targetArg,
|
||||
'_CARGOKIT_NDK_LINK_CLANG': ccValue,
|
||||
'CARGOKIT_TOOL_TEMP_DIR': toolTempDir,
|
||||
};
|
||||
}
|
||||
|
||||
// Workaround for libgcc missing in NDK23, inspired by cargo-ndk
|
||||
String _libGccWorkaround(String buildDir, Version ndkVersion) {
|
||||
final workaroundDir = path.join(
|
||||
buildDir,
|
||||
'cargokit',
|
||||
'libgcc_workaround',
|
||||
'${ndkVersion.major}',
|
||||
);
|
||||
Directory(workaroundDir).createSync(recursive: true);
|
||||
if (ndkVersion.major >= 23) {
|
||||
File(path.join(workaroundDir, 'libgcc.a'))
|
||||
.writeAsStringSync('INPUT(-lunwind)');
|
||||
} else {
|
||||
// Other way around, untested, forward libgcc.a from libunwind once Rust
|
||||
// gets updated for NDK23+.
|
||||
File(path.join(workaroundDir, 'libunwind.a'))
|
||||
.writeAsStringSync('INPUT(-lgcc)');
|
||||
}
|
||||
|
||||
var rustFlags = Platform.environment['CARGO_ENCODED_RUSTFLAGS'] ?? '';
|
||||
if (rustFlags.isNotEmpty) {
|
||||
rustFlags = '$rustFlags\x1f';
|
||||
}
|
||||
rustFlags = '$rustFlags-L\x1f$workaroundDir';
|
||||
return rustFlags;
|
||||
}
|
||||
}
|
266
rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart
Normal file
266
rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart
Normal file
@ -0,0 +1,266 @@
|
||||
/// 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:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'builder.dart';
|
||||
import 'crate_hash.dart';
|
||||
import 'options.dart';
|
||||
import 'precompile_binaries.dart';
|
||||
import 'rustup.dart';
|
||||
import 'target.dart';
|
||||
|
||||
class Artifact {
|
||||
/// File system location of the artifact.
|
||||
final String path;
|
||||
|
||||
/// Actual file name that the artifact should have in destination folder.
|
||||
final String finalFileName;
|
||||
|
||||
AritifactType get type {
|
||||
if (finalFileName.endsWith('.dll') ||
|
||||
finalFileName.endsWith('.dll.lib') ||
|
||||
finalFileName.endsWith('.pdb') ||
|
||||
finalFileName.endsWith('.so') ||
|
||||
finalFileName.endsWith('.dylib')) {
|
||||
return AritifactType.dylib;
|
||||
} else if (finalFileName.endsWith('.lib') || finalFileName.endsWith('.a')) {
|
||||
return AritifactType.staticlib;
|
||||
} else {
|
||||
throw Exception('Unknown artifact type for $finalFileName');
|
||||
}
|
||||
}
|
||||
|
||||
Artifact({
|
||||
required this.path,
|
||||
required this.finalFileName,
|
||||
});
|
||||
}
|
||||
|
||||
final _log = Logger('artifacts_provider');
|
||||
|
||||
class ArtifactProvider {
|
||||
ArtifactProvider({
|
||||
required this.environment,
|
||||
required this.userOptions,
|
||||
});
|
||||
|
||||
final BuildEnvironment environment;
|
||||
final CargokitUserOptions userOptions;
|
||||
|
||||
Future<Map<Target, List<Artifact>>> getArtifacts(List<Target> targets) async {
|
||||
final result = await _getPrecompiledArtifacts(targets);
|
||||
|
||||
final pendingTargets = List.of(targets);
|
||||
pendingTargets.removeWhere((element) => result.containsKey(element));
|
||||
|
||||
if (pendingTargets.isEmpty) {
|
||||
return result;
|
||||
}
|
||||
|
||||
final rustup = Rustup();
|
||||
for (final target in targets) {
|
||||
final builder = RustBuilder(target: target, environment: environment);
|
||||
builder.prepare(rustup);
|
||||
_log.info('Building ${environment.crateInfo.packageName} for $target');
|
||||
final targetDir = await builder.build();
|
||||
// For local build accept both static and dynamic libraries.
|
||||
final artifactNames = <String>{
|
||||
...getArtifactNames(
|
||||
target: target,
|
||||
libraryName: environment.crateInfo.packageName,
|
||||
aritifactType: AritifactType.dylib,
|
||||
remote: false,
|
||||
),
|
||||
...getArtifactNames(
|
||||
target: target,
|
||||
libraryName: environment.crateInfo.packageName,
|
||||
aritifactType: AritifactType.staticlib,
|
||||
remote: false,
|
||||
)
|
||||
};
|
||||
final artifacts = artifactNames
|
||||
.map((artifactName) => Artifact(
|
||||
path: path.join(targetDir, artifactName),
|
||||
finalFileName: artifactName,
|
||||
))
|
||||
.where((element) => File(element.path).existsSync())
|
||||
.toList();
|
||||
result[target] = artifacts;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Map<Target, List<Artifact>>> _getPrecompiledArtifacts(
|
||||
List<Target> targets) async {
|
||||
if (userOptions.usePrecompiledBinaries == false) {
|
||||
_log.info('Precompiled binaries are disabled');
|
||||
return {};
|
||||
}
|
||||
if (environment.crateOptions.precompiledBinaries == null) {
|
||||
_log.fine('Precompiled binaries not enabled for this crate');
|
||||
return {};
|
||||
}
|
||||
|
||||
final start = Stopwatch()..start();
|
||||
final crateHash = CrateHash.compute(environment.manifestDir,
|
||||
tempStorage: environment.targetTempDir);
|
||||
_log.fine(
|
||||
'Computed crate hash $crateHash in ${start.elapsedMilliseconds}ms');
|
||||
|
||||
final downloadedArtifactsDir =
|
||||
path.join(environment.targetTempDir, 'precompiled', crateHash);
|
||||
Directory(downloadedArtifactsDir).createSync(recursive: true);
|
||||
|
||||
final res = <Target, List<Artifact>>{};
|
||||
|
||||
for (final target in targets) {
|
||||
final requiredArtifacts = getArtifactNames(
|
||||
target: target,
|
||||
libraryName: environment.crateInfo.packageName,
|
||||
remote: true,
|
||||
);
|
||||
final artifactsForTarget = <Artifact>[];
|
||||
|
||||
for (final artifact in requiredArtifacts) {
|
||||
final fileName = PrecompileBinaries.fileName(target, artifact);
|
||||
final downloadedPath = path.join(downloadedArtifactsDir, fileName);
|
||||
if (!File(downloadedPath).existsSync()) {
|
||||
final signatureFileName =
|
||||
PrecompileBinaries.signatureFileName(target, artifact);
|
||||
await _tryDownloadArtifacts(
|
||||
crateHash: crateHash,
|
||||
fileName: fileName,
|
||||
signatureFileName: signatureFileName,
|
||||
finalPath: downloadedPath,
|
||||
);
|
||||
}
|
||||
if (File(downloadedPath).existsSync()) {
|
||||
artifactsForTarget.add(Artifact(
|
||||
path: downloadedPath,
|
||||
finalFileName: artifact,
|
||||
));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Only provide complete set of artifacts.
|
||||
if (artifactsForTarget.length == requiredArtifacts.length) {
|
||||
_log.fine('Found precompiled artifacts for $target');
|
||||
res[target] = artifactsForTarget;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static Future<Response> _get(Uri url, {Map<String, String>? headers}) async {
|
||||
int attempt = 0;
|
||||
const maxAttempts = 10;
|
||||
while (true) {
|
||||
try {
|
||||
return await get(url, headers: headers);
|
||||
} on SocketException catch (e) {
|
||||
// Try to detect reset by peer error and retry.
|
||||
if (attempt++ < maxAttempts &&
|
||||
(e.osError?.errorCode == 54 || e.osError?.errorCode == 10054)) {
|
||||
_log.severe(
|
||||
'Failed to download $url: $e, attempt $attempt of $maxAttempts, will retry...');
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
continue;
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _tryDownloadArtifacts({
|
||||
required String crateHash,
|
||||
required String fileName,
|
||||
required String signatureFileName,
|
||||
required String finalPath,
|
||||
}) async {
|
||||
final precompiledBinaries = environment.crateOptions.precompiledBinaries!;
|
||||
final prefix = precompiledBinaries.uriPrefix;
|
||||
final url = Uri.parse('$prefix$crateHash/$fileName');
|
||||
final signatureUrl = Uri.parse('$prefix$crateHash/$signatureFileName');
|
||||
_log.fine('Downloading signature from $signatureUrl');
|
||||
final signature = await _get(signatureUrl);
|
||||
if (signature.statusCode == 404) {
|
||||
_log.warning(
|
||||
'Precompiled binaries not available for crate hash $crateHash ($fileName)');
|
||||
return;
|
||||
}
|
||||
if (signature.statusCode != 200) {
|
||||
_log.severe(
|
||||
'Failed to download signature $signatureUrl: status ${signature.statusCode}');
|
||||
return;
|
||||
}
|
||||
_log.fine('Downloading binary from $url');
|
||||
final res = await _get(url);
|
||||
if (res.statusCode != 200) {
|
||||
_log.severe('Failed to download binary $url: status ${res.statusCode}');
|
||||
return;
|
||||
}
|
||||
if (verify(
|
||||
precompiledBinaries.publicKey, res.bodyBytes, signature.bodyBytes)) {
|
||||
File(finalPath).writeAsBytesSync(res.bodyBytes);
|
||||
} else {
|
||||
_log.shout('Signature verification failed! Ignoring binary.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AritifactType {
|
||||
staticlib,
|
||||
dylib,
|
||||
}
|
||||
|
||||
AritifactType artifactTypeForTarget(Target target) {
|
||||
if (target.darwinPlatform != null) {
|
||||
return AritifactType.staticlib;
|
||||
} else {
|
||||
return AritifactType.dylib;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> getArtifactNames({
|
||||
required Target target,
|
||||
required String libraryName,
|
||||
required bool remote,
|
||||
AritifactType? aritifactType,
|
||||
}) {
|
||||
aritifactType ??= artifactTypeForTarget(target);
|
||||
if (target.darwinArch != null) {
|
||||
if (aritifactType == AritifactType.staticlib) {
|
||||
return ['lib$libraryName.a'];
|
||||
} else {
|
||||
return ['lib$libraryName.dylib'];
|
||||
}
|
||||
} else if (target.rust.contains('-windows-')) {
|
||||
if (aritifactType == AritifactType.staticlib) {
|
||||
return ['$libraryName.lib'];
|
||||
} else {
|
||||
return [
|
||||
'$libraryName.dll',
|
||||
'$libraryName.dll.lib',
|
||||
if (!remote) '$libraryName.pdb'
|
||||
];
|
||||
}
|
||||
} else if (target.rust.contains('-linux-')) {
|
||||
if (aritifactType == AritifactType.staticlib) {
|
||||
return ['lib$libraryName.a'];
|
||||
} else {
|
||||
return ['lib$libraryName.so'];
|
||||
}
|
||||
} else {
|
||||
throw Exception("Unsupported target: ${target.rust}");
|
||||
}
|
||||
}
|
40
rust_builder/cargokit/build_tool/lib/src/build_cmake.dart
Normal file
40
rust_builder/cargokit/build_tool/lib/src/build_cmake.dart
Normal file
@ -0,0 +1,40 @@
|
||||
/// 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:path/path.dart' as path;
|
||||
|
||||
import 'artifacts_provider.dart';
|
||||
import 'builder.dart';
|
||||
import 'environment.dart';
|
||||
import 'options.dart';
|
||||
import 'target.dart';
|
||||
|
||||
class BuildCMake {
|
||||
final CargokitUserOptions userOptions;
|
||||
|
||||
BuildCMake({required this.userOptions});
|
||||
|
||||
Future<void> build() async {
|
||||
final targetPlatform = Environment.targetPlatform;
|
||||
final target = Target.forFlutterName(Environment.targetPlatform);
|
||||
if (target == null) {
|
||||
throw Exception("Unknown target platform: $targetPlatform");
|
||||
}
|
||||
|
||||
final environment = BuildEnvironment.fromEnvironment(isAndroid: false);
|
||||
final provider =
|
||||
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||
final artifacts = await provider.getArtifacts([target]);
|
||||
|
||||
final libs = artifacts[target]!;
|
||||
|
||||
for (final lib in libs) {
|
||||
if (lib.type == AritifactType.dylib) {
|
||||
File(lib.path)
|
||||
.copySync(path.join(Environment.outputDir, lib.finalFileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
49
rust_builder/cargokit/build_tool/lib/src/build_gradle.dart
Normal file
49
rust_builder/cargokit/build_tool/lib/src/build_gradle.dart
Normal file
@ -0,0 +1,49 @@
|
||||
/// 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:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'artifacts_provider.dart';
|
||||
import 'builder.dart';
|
||||
import 'environment.dart';
|
||||
import 'options.dart';
|
||||
import 'target.dart';
|
||||
|
||||
final log = Logger('build_gradle');
|
||||
|
||||
class BuildGradle {
|
||||
BuildGradle({required this.userOptions});
|
||||
|
||||
final CargokitUserOptions userOptions;
|
||||
|
||||
Future<void> build() async {
|
||||
final targets = Environment.targetPlatforms.map((arch) {
|
||||
final target = Target.forFlutterName(arch);
|
||||
if (target == null) {
|
||||
throw Exception(
|
||||
"Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}");
|
||||
}
|
||||
return target;
|
||||
}).toList();
|
||||
|
||||
final environment = BuildEnvironment.fromEnvironment(isAndroid: true);
|
||||
final provider =
|
||||
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||
final artifacts = await provider.getArtifacts(targets);
|
||||
|
||||
for (final target in targets) {
|
||||
final libs = artifacts[target]!;
|
||||
final outputDir = path.join(Environment.outputDir, target.android!);
|
||||
Directory(outputDir).createSync(recursive: true);
|
||||
|
||||
for (final lib in libs) {
|
||||
if (lib.type == AritifactType.dylib) {
|
||||
File(lib.path).copySync(path.join(outputDir, lib.finalFileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
89
rust_builder/cargokit/build_tool/lib/src/build_pod.dart
Normal file
89
rust_builder/cargokit/build_tool/lib/src/build_pod.dart
Normal file
@ -0,0 +1,89 @@
|
||||
/// 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:path/path.dart' as path;
|
||||
|
||||
import 'artifacts_provider.dart';
|
||||
import 'builder.dart';
|
||||
import 'environment.dart';
|
||||
import 'options.dart';
|
||||
import 'target.dart';
|
||||
import 'util.dart';
|
||||
|
||||
class BuildPod {
|
||||
BuildPod({required this.userOptions});
|
||||
|
||||
final CargokitUserOptions userOptions;
|
||||
|
||||
Future<void> build() async {
|
||||
final targets = Environment.darwinArchs.map((arch) {
|
||||
final target = Target.forDarwin(
|
||||
platformName: Environment.darwinPlatformName, darwinAarch: arch);
|
||||
if (target == null) {
|
||||
throw Exception(
|
||||
"Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}");
|
||||
}
|
||||
return target;
|
||||
}).toList();
|
||||
|
||||
final environment = BuildEnvironment.fromEnvironment(isAndroid: false);
|
||||
final provider =
|
||||
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||
final artifacts = await provider.getArtifacts(targets);
|
||||
|
||||
void performLipo(String targetFile, Iterable<String> sourceFiles) {
|
||||
runCommand("lipo", [
|
||||
'-create',
|
||||
...sourceFiles,
|
||||
'-output',
|
||||
targetFile,
|
||||
]);
|
||||
}
|
||||
|
||||
final outputDir = Environment.outputDir;
|
||||
|
||||
Directory(outputDir).createSync(recursive: true);
|
||||
|
||||
final staticLibs = artifacts.values
|
||||
.expand((element) => element)
|
||||
.where((element) => element.type == AritifactType.staticlib)
|
||||
.toList();
|
||||
final dynamicLibs = artifacts.values
|
||||
.expand((element) => element)
|
||||
.where((element) => element.type == AritifactType.dylib)
|
||||
.toList();
|
||||
|
||||
final libName = environment.crateInfo.packageName;
|
||||
|
||||
// If there is static lib, use it and link it with pod
|
||||
if (staticLibs.isNotEmpty) {
|
||||
final finalTargetFile = path.join(outputDir, "lib$libName.a");
|
||||
performLipo(finalTargetFile, staticLibs.map((e) => e.path));
|
||||
} else {
|
||||
// Otherwise try to replace bundle dylib with our dylib
|
||||
final bundlePaths = [
|
||||
'$libName.framework/Versions/A/$libName',
|
||||
'$libName.framework/$libName',
|
||||
];
|
||||
|
||||
for (final bundlePath in bundlePaths) {
|
||||
final targetFile = path.join(outputDir, bundlePath);
|
||||
if (File(targetFile).existsSync()) {
|
||||
performLipo(targetFile, dynamicLibs.map((e) => e.path));
|
||||
|
||||
// Replace absolute id with @rpath one so that it works properly
|
||||
// when moved to Frameworks.
|
||||
runCommand("install_name_tool", [
|
||||
'-id',
|
||||
'@rpath/$bundlePath',
|
||||
targetFile,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw Exception('Unable to find bundle for dynamic library');
|
||||
}
|
||||
}
|
||||
}
|
271
rust_builder/cargokit/build_tool/lib/src/build_tool.dart
Normal file
271
rust_builder/cargokit/build_tool/lib/src/build_tool.dart
Normal file
@ -0,0 +1,271 @@
|
||||
/// 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:args/command_runner.dart';
|
||||
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:github/github.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'android_environment.dart';
|
||||
import 'build_cmake.dart';
|
||||
import 'build_gradle.dart';
|
||||
import 'build_pod.dart';
|
||||
import 'logging.dart';
|
||||
import 'options.dart';
|
||||
import 'precompile_binaries.dart';
|
||||
import 'target.dart';
|
||||
import 'util.dart';
|
||||
import 'verify_binaries.dart';
|
||||
|
||||
final log = Logger('build_tool');
|
||||
|
||||
abstract class BuildCommand extends Command {
|
||||
Future<void> runBuildCommand(CargokitUserOptions options);
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final options = CargokitUserOptions.load();
|
||||
|
||||
if (options.verboseLogging ||
|
||||
Platform.environment['CARGOKIT_VERBOSE'] == '1') {
|
||||
enableVerboseLogging();
|
||||
}
|
||||
|
||||
await runBuildCommand(options);
|
||||
}
|
||||
}
|
||||
|
||||
class BuildPodCommand extends BuildCommand {
|
||||
@override
|
||||
final name = 'build-pod';
|
||||
|
||||
@override
|
||||
final description = 'Build cocoa pod library';
|
||||
|
||||
@override
|
||||
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||
final build = BuildPod(userOptions: options);
|
||||
await build.build();
|
||||
}
|
||||
}
|
||||
|
||||
class BuildGradleCommand extends BuildCommand {
|
||||
@override
|
||||
final name = 'build-gradle';
|
||||
|
||||
@override
|
||||
final description = 'Build android library';
|
||||
|
||||
@override
|
||||
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||
final build = BuildGradle(userOptions: options);
|
||||
await build.build();
|
||||
}
|
||||
}
|
||||
|
||||
class BuildCMakeCommand extends BuildCommand {
|
||||
@override
|
||||
final name = 'build-cmake';
|
||||
|
||||
@override
|
||||
final description = 'Build CMake library';
|
||||
|
||||
@override
|
||||
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||
final build = BuildCMake(userOptions: options);
|
||||
await build.build();
|
||||
}
|
||||
}
|
||||
|
||||
class GenKeyCommand extends Command {
|
||||
@override
|
||||
final name = 'gen-key';
|
||||
|
||||
@override
|
||||
final description = 'Generate key pair for signing precompiled binaries';
|
||||
|
||||
@override
|
||||
void run() {
|
||||
final kp = generateKey();
|
||||
final private = HEX.encode(kp.privateKey.bytes);
|
||||
final public = HEX.encode(kp.publicKey.bytes);
|
||||
print("Private Key: $private");
|
||||
print("Public Key: $public");
|
||||
}
|
||||
}
|
||||
|
||||
class PrecompileBinariesCommand extends Command {
|
||||
PrecompileBinariesCommand() {
|
||||
argParser
|
||||
..addOption(
|
||||
'repository',
|
||||
mandatory: true,
|
||||
help: 'Github repository slug in format owner/name',
|
||||
)
|
||||
..addOption(
|
||||
'manifest-dir',
|
||||
mandatory: true,
|
||||
help: 'Directory containing Cargo.toml',
|
||||
)
|
||||
..addMultiOption('target',
|
||||
help: 'Rust target triple of artifact to build.\n'
|
||||
'Can be specified multiple times or omitted in which case\n'
|
||||
'all targets for current platform will be built.')
|
||||
..addOption(
|
||||
'android-sdk-location',
|
||||
help: 'Location of Android SDK (if available)',
|
||||
)
|
||||
..addOption(
|
||||
'android-ndk-version',
|
||||
help: 'Android NDK version (if available)',
|
||||
)
|
||||
..addOption(
|
||||
'android-min-sdk-version',
|
||||
help: 'Android minimum rquired version (if available)',
|
||||
)
|
||||
..addOption(
|
||||
'temp-dir',
|
||||
help: 'Directory to store temporary build artifacts',
|
||||
)
|
||||
..addFlag(
|
||||
"verbose",
|
||||
abbr: "v",
|
||||
defaultsTo: false,
|
||||
help: "Enable verbose logging",
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final name = 'precompile-binaries';
|
||||
|
||||
@override
|
||||
final description = 'Prebuild and upload binaries\n'
|
||||
'Private key must be passed through PRIVATE_KEY environment variable. '
|
||||
'Use gen_key through generate priave key.\n'
|
||||
'Github token must be passed as GITHUB_TOKEN environment variable.\n';
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final verbose = argResults!['verbose'] as bool;
|
||||
if (verbose) {
|
||||
enableVerboseLogging();
|
||||
}
|
||||
|
||||
final privateKeyString = Platform.environment['PRIVATE_KEY'];
|
||||
if (privateKeyString == null) {
|
||||
throw ArgumentError('Missing PRIVATE_KEY environment variable');
|
||||
}
|
||||
final githubToken = Platform.environment['GITHUB_TOKEN'];
|
||||
if (githubToken == null) {
|
||||
throw ArgumentError('Missing GITHUB_TOKEN environment variable');
|
||||
}
|
||||
final privateKey = HEX.decode(privateKeyString);
|
||||
if (privateKey.length != 64) {
|
||||
throw ArgumentError('Private key must be 64 bytes long');
|
||||
}
|
||||
final manifestDir = argResults!['manifest-dir'] as String;
|
||||
if (!Directory(manifestDir).existsSync()) {
|
||||
throw ArgumentError('Manifest directory does not exist: $manifestDir');
|
||||
}
|
||||
String? androidMinSdkVersionString =
|
||||
argResults!['android-min-sdk-version'] as String?;
|
||||
int? androidMinSdkVersion;
|
||||
if (androidMinSdkVersionString != null) {
|
||||
androidMinSdkVersion = int.tryParse(androidMinSdkVersionString);
|
||||
if (androidMinSdkVersion == null) {
|
||||
throw ArgumentError(
|
||||
'Invalid android-min-sdk-version: $androidMinSdkVersionString');
|
||||
}
|
||||
}
|
||||
final targetStrigns = argResults!['target'] as List<String>;
|
||||
final targets = targetStrigns.map((target) {
|
||||
final res = Target.forRustTriple(target);
|
||||
if (res == null) {
|
||||
throw ArgumentError('Invalid target: $target');
|
||||
}
|
||||
return res;
|
||||
}).toList(growable: false);
|
||||
final precompileBinaries = PrecompileBinaries(
|
||||
privateKey: PrivateKey(privateKey),
|
||||
githubToken: githubToken,
|
||||
manifestDir: manifestDir,
|
||||
repositorySlug: RepositorySlug.full(argResults!['repository'] as String),
|
||||
targets: targets,
|
||||
androidSdkLocation: argResults!['android-sdk-location'] as String?,
|
||||
androidNdkVersion: argResults!['android-ndk-version'] as String?,
|
||||
androidMinSdkVersion: androidMinSdkVersion,
|
||||
tempDir: argResults!['temp-dir'] as String?,
|
||||
);
|
||||
|
||||
await precompileBinaries.run();
|
||||
}
|
||||
}
|
||||
|
||||
class VerifyBinariesCommand extends Command {
|
||||
VerifyBinariesCommand() {
|
||||
argParser.addOption(
|
||||
'manifest-dir',
|
||||
mandatory: true,
|
||||
help: 'Directory containing Cargo.toml',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final name = "verify-binaries";
|
||||
|
||||
@override
|
||||
final description = 'Verifies published binaries\n'
|
||||
'Checks whether there is a binary published for each targets\n'
|
||||
'and checks the signature.';
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final manifestDir = argResults!['manifest-dir'] as String;
|
||||
final verifyBinaries = VerifyBinaries(
|
||||
manifestDir: manifestDir,
|
||||
);
|
||||
await verifyBinaries.run();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> runMain(List<String> args) async {
|
||||
try {
|
||||
// Init logging before options are loaded
|
||||
initLogging();
|
||||
|
||||
if (Platform.environment['_CARGOKIT_NDK_LINK_TARGET'] != null) {
|
||||
return AndroidEnvironment.clangLinkerWrapper(args);
|
||||
}
|
||||
|
||||
final runner = CommandRunner('build_tool', 'Cargokit built_tool')
|
||||
..addCommand(BuildPodCommand())
|
||||
..addCommand(BuildGradleCommand())
|
||||
..addCommand(BuildCMakeCommand())
|
||||
..addCommand(GenKeyCommand())
|
||||
..addCommand(PrecompileBinariesCommand())
|
||||
..addCommand(VerifyBinariesCommand());
|
||||
|
||||
await runner.run(args);
|
||||
} on ArgumentError catch (e) {
|
||||
stderr.writeln(e.toString());
|
||||
exit(1);
|
||||
} catch (e, s) {
|
||||
log.severe(kDoubleSeparator);
|
||||
log.severe('Cargokit BuildTool failed with error:');
|
||||
log.severe(kSeparator);
|
||||
log.severe(e);
|
||||
// This tells user to install Rust, there's no need to pollute the log with
|
||||
// stack trace.
|
||||
if (e is! RustupNotFoundException) {
|
||||
log.severe(kSeparator);
|
||||
log.severe(s);
|
||||
log.severe(kSeparator);
|
||||
log.severe('BuildTool arguments: $args');
|
||||
}
|
||||
log.severe(kDoubleSeparator);
|
||||
exit(1);
|
||||
}
|
||||
}
|
198
rust_builder/cargokit/build_tool/lib/src/builder.dart
Normal file
198
rust_builder/cargokit/build_tool/lib/src/builder.dart
Normal file
@ -0,0 +1,198 @@
|
||||
/// 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 'package:collection/collection.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'android_environment.dart';
|
||||
import 'cargo.dart';
|
||||
import 'environment.dart';
|
||||
import 'options.dart';
|
||||
import 'rustup.dart';
|
||||
import 'target.dart';
|
||||
import 'util.dart';
|
||||
|
||||
final _log = Logger('builder');
|
||||
|
||||
enum BuildConfiguration {
|
||||
debug,
|
||||
release,
|
||||
profile,
|
||||
}
|
||||
|
||||
extension on BuildConfiguration {
|
||||
bool get isDebug => this == BuildConfiguration.debug;
|
||||
String get rustName => switch (this) {
|
||||
BuildConfiguration.debug => 'debug',
|
||||
BuildConfiguration.release => 'release',
|
||||
BuildConfiguration.profile => 'release',
|
||||
};
|
||||
}
|
||||
|
||||
class BuildException implements Exception {
|
||||
final String message;
|
||||
|
||||
BuildException(this.message);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BuildException: $message';
|
||||
}
|
||||
}
|
||||
|
||||
class BuildEnvironment {
|
||||
final BuildConfiguration configuration;
|
||||
final CargokitCrateOptions crateOptions;
|
||||
final String targetTempDir;
|
||||
final String manifestDir;
|
||||
final CrateInfo crateInfo;
|
||||
|
||||
final bool isAndroid;
|
||||
final String? androidSdkPath;
|
||||
final String? androidNdkVersion;
|
||||
final int? androidMinSdkVersion;
|
||||
final String? javaHome;
|
||||
|
||||
BuildEnvironment({
|
||||
required this.configuration,
|
||||
required this.crateOptions,
|
||||
required this.targetTempDir,
|
||||
required this.manifestDir,
|
||||
required this.crateInfo,
|
||||
required this.isAndroid,
|
||||
this.androidSdkPath,
|
||||
this.androidNdkVersion,
|
||||
this.androidMinSdkVersion,
|
||||
this.javaHome,
|
||||
});
|
||||
|
||||
static BuildConfiguration parseBuildConfiguration(String value) {
|
||||
// XCode configuration adds the flavor to configuration name.
|
||||
final firstSegment = value.split('-').first;
|
||||
final buildConfiguration = BuildConfiguration.values.firstWhereOrNull(
|
||||
(e) => e.name == firstSegment,
|
||||
);
|
||||
if (buildConfiguration == null) {
|
||||
_log.warning('Unknown build configuraiton $value, will assume release');
|
||||
return BuildConfiguration.release;
|
||||
}
|
||||
return buildConfiguration;
|
||||
}
|
||||
|
||||
static BuildEnvironment fromEnvironment({
|
||||
required bool isAndroid,
|
||||
}) {
|
||||
final buildConfiguration =
|
||||
parseBuildConfiguration(Environment.configuration);
|
||||
final manifestDir = Environment.manifestDir;
|
||||
final crateOptions = CargokitCrateOptions.load(
|
||||
manifestDir: manifestDir,
|
||||
);
|
||||
final crateInfo = CrateInfo.load(manifestDir);
|
||||
return BuildEnvironment(
|
||||
configuration: buildConfiguration,
|
||||
crateOptions: crateOptions,
|
||||
targetTempDir: Environment.targetTempDir,
|
||||
manifestDir: manifestDir,
|
||||
crateInfo: crateInfo,
|
||||
isAndroid: isAndroid,
|
||||
androidSdkPath: isAndroid ? Environment.sdkPath : null,
|
||||
androidNdkVersion: isAndroid ? Environment.ndkVersion : null,
|
||||
androidMinSdkVersion:
|
||||
isAndroid ? int.parse(Environment.minSdkVersion) : null,
|
||||
javaHome: isAndroid ? Environment.javaHome : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RustBuilder {
|
||||
final Target target;
|
||||
final BuildEnvironment environment;
|
||||
|
||||
RustBuilder({
|
||||
required this.target,
|
||||
required this.environment,
|
||||
});
|
||||
|
||||
void prepare(
|
||||
Rustup rustup,
|
||||
) {
|
||||
final toolchain = _toolchain;
|
||||
if (rustup.installedTargets(toolchain) == null) {
|
||||
rustup.installToolchain(toolchain);
|
||||
}
|
||||
if (toolchain == 'nightly') {
|
||||
rustup.installRustSrcForNightly();
|
||||
}
|
||||
if (!rustup.installedTargets(toolchain)!.contains(target.rust)) {
|
||||
rustup.installTarget(target.rust, toolchain: toolchain);
|
||||
}
|
||||
}
|
||||
|
||||
CargoBuildOptions? get _buildOptions =>
|
||||
environment.crateOptions.cargo[environment.configuration];
|
||||
|
||||
String get _toolchain => _buildOptions?.toolchain.name ?? 'stable';
|
||||
|
||||
/// Returns the path of directory containing build artifacts.
|
||||
Future<String> build() async {
|
||||
final extraArgs = _buildOptions?.flags ?? [];
|
||||
final manifestPath = path.join(environment.manifestDir, 'Cargo.toml');
|
||||
runCommand(
|
||||
'rustup',
|
||||
[
|
||||
'run',
|
||||
_toolchain,
|
||||
'cargo',
|
||||
'build',
|
||||
...extraArgs,
|
||||
'--manifest-path',
|
||||
manifestPath,
|
||||
'-p',
|
||||
environment.crateInfo.packageName,
|
||||
if (!environment.configuration.isDebug) '--release',
|
||||
'--target',
|
||||
target.rust,
|
||||
'--target-dir',
|
||||
environment.targetTempDir,
|
||||
],
|
||||
environment: await _buildEnvironment(),
|
||||
);
|
||||
return path.join(
|
||||
environment.targetTempDir,
|
||||
target.rust,
|
||||
environment.configuration.rustName,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _buildEnvironment() async {
|
||||
if (target.android == null) {
|
||||
return {};
|
||||
} else {
|
||||
final sdkPath = environment.androidSdkPath;
|
||||
final ndkVersion = environment.androidNdkVersion;
|
||||
final minSdkVersion = environment.androidMinSdkVersion;
|
||||
if (sdkPath == null) {
|
||||
throw BuildException('androidSdkPath is not set');
|
||||
}
|
||||
if (ndkVersion == null) {
|
||||
throw BuildException('androidNdkVersion is not set');
|
||||
}
|
||||
if (minSdkVersion == null) {
|
||||
throw BuildException('androidMinSdkVersion is not set');
|
||||
}
|
||||
final env = AndroidEnvironment(
|
||||
sdkPath: sdkPath,
|
||||
ndkVersion: ndkVersion,
|
||||
minSdkVersion: minSdkVersion,
|
||||
targetTempDir: environment.targetTempDir,
|
||||
target: target,
|
||||
);
|
||||
if (!env.ndkIsInstalled() && environment.javaHome != null) {
|
||||
env.installNdk(javaHome: environment.javaHome!);
|
||||
}
|
||||
return env.buildEnvironment();
|
||||
}
|
||||
}
|
||||
}
|
48
rust_builder/cargokit/build_tool/lib/src/cargo.dart
Normal file
48
rust_builder/cargokit/build_tool/lib/src/cargo.dart
Normal file
@ -0,0 +1,48 @@
|
||||
/// 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:path/path.dart' as path;
|
||||
import 'package:toml/toml.dart';
|
||||
|
||||
class ManifestException {
|
||||
ManifestException(this.message, {required this.fileName});
|
||||
|
||||
final String? fileName;
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (fileName != null) {
|
||||
return 'Failed to parse package manifest at $fileName: $message';
|
||||
} else {
|
||||
return 'Failed to parse package manifest: $message';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CrateInfo {
|
||||
CrateInfo({required this.packageName});
|
||||
|
||||
final String packageName;
|
||||
|
||||
static CrateInfo parseManifest(String manifest, {final String? fileName}) {
|
||||
final toml = TomlDocument.parse(manifest);
|
||||
final package = toml.toMap()['package'];
|
||||
if (package == null) {
|
||||
throw ManifestException('Missing package section', fileName: fileName);
|
||||
}
|
||||
final name = package['name'];
|
||||
if (name == null) {
|
||||
throw ManifestException('Missing package name', fileName: fileName);
|
||||
}
|
||||
return CrateInfo(packageName: name);
|
||||
}
|
||||
|
||||
static CrateInfo load(String manifestDir) {
|
||||
final manifestFile = File(path.join(manifestDir, 'Cargo.toml'));
|
||||
final manifest = manifestFile.readAsStringSync();
|
||||
return parseManifest(manifest, fileName: manifestFile.path);
|
||||
}
|
||||
}
|
124
rust_builder/cargokit/build_tool/lib/src/crate_hash.dart
Normal file
124
rust_builder/cargokit/build_tool/lib/src/crate_hash.dart
Normal file
@ -0,0 +1,124 @@
|
||||
/// 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:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
class CrateHash {
|
||||
/// Computes a hash uniquely identifying crate content. This takes into account
|
||||
/// content all all .rs files inside the src directory, as well as Cargo.toml,
|
||||
/// Cargo.lock, build.rs and cargokit.yaml.
|
||||
///
|
||||
/// If [tempStorage] is provided, computed hash is stored in a file in that directory
|
||||
/// and reused on subsequent calls if the crate content hasn't changed.
|
||||
static String compute(String manifestDir, {String? tempStorage}) {
|
||||
return CrateHash._(
|
||||
manifestDir: manifestDir,
|
||||
tempStorage: tempStorage,
|
||||
)._compute();
|
||||
}
|
||||
|
||||
CrateHash._({
|
||||
required this.manifestDir,
|
||||
required this.tempStorage,
|
||||
});
|
||||
|
||||
String _compute() {
|
||||
final files = getFiles();
|
||||
final tempStorage = this.tempStorage;
|
||||
if (tempStorage != null) {
|
||||
final quickHash = _computeQuickHash(files);
|
||||
final quickHashFolder = Directory(path.join(tempStorage, 'crate_hash'));
|
||||
quickHashFolder.createSync(recursive: true);
|
||||
final quickHashFile = File(path.join(quickHashFolder.path, quickHash));
|
||||
if (quickHashFile.existsSync()) {
|
||||
return quickHashFile.readAsStringSync();
|
||||
}
|
||||
final hash = _computeHash(files);
|
||||
quickHashFile.writeAsStringSync(hash);
|
||||
return hash;
|
||||
} else {
|
||||
return _computeHash(files);
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes a quick hash based on files stat (without reading contents). This
|
||||
/// is used to cache the real hash, which is slower to compute since it involves
|
||||
/// reading every single file.
|
||||
String _computeQuickHash(List<File> files) {
|
||||
final output = AccumulatorSink<Digest>();
|
||||
final input = sha256.startChunkedConversion(output);
|
||||
|
||||
final data = ByteData(8);
|
||||
for (final file in files) {
|
||||
input.add(utf8.encode(file.path));
|
||||
final stat = file.statSync();
|
||||
data.setUint64(0, stat.size);
|
||||
input.add(data.buffer.asUint8List());
|
||||
data.setUint64(0, stat.modified.millisecondsSinceEpoch);
|
||||
input.add(data.buffer.asUint8List());
|
||||
}
|
||||
|
||||
input.close();
|
||||
return base64Url.encode(output.events.single.bytes);
|
||||
}
|
||||
|
||||
String _computeHash(List<File> files) {
|
||||
final output = AccumulatorSink<Digest>();
|
||||
final input = sha256.startChunkedConversion(output);
|
||||
|
||||
void addTextFile(File file) {
|
||||
// text Files are hashed by lines in case we're dealing with github checkout
|
||||
// that auto-converts line endings.
|
||||
final splitter = LineSplitter();
|
||||
if (file.existsSync()) {
|
||||
final data = file.readAsStringSync();
|
||||
final lines = splitter.convert(data);
|
||||
for (final line in lines) {
|
||||
input.add(utf8.encode(line));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final file in files) {
|
||||
addTextFile(file);
|
||||
}
|
||||
|
||||
input.close();
|
||||
final res = output.events.single;
|
||||
|
||||
// Truncate to 128bits.
|
||||
final hash = res.bytes.sublist(0, 16);
|
||||
return hex.encode(hash);
|
||||
}
|
||||
|
||||
List<File> getFiles() {
|
||||
final src = Directory(path.join(manifestDir, 'src'));
|
||||
final files = src
|
||||
.listSync(recursive: true, followLinks: false)
|
||||
.whereType<File>()
|
||||
.toList();
|
||||
files.sortBy((element) => element.path);
|
||||
void addFile(String relative) {
|
||||
final file = File(path.join(manifestDir, relative));
|
||||
if (file.existsSync()) {
|
||||
files.add(file);
|
||||
}
|
||||
}
|
||||
|
||||
addFile('Cargo.toml');
|
||||
addFile('Cargo.lock');
|
||||
addFile('build.rs');
|
||||
addFile('cargokit.yaml');
|
||||
return files;
|
||||
}
|
||||
|
||||
final String manifestDir;
|
||||
final String? tempStorage;
|
||||
}
|
68
rust_builder/cargokit/build_tool/lib/src/environment.dart
Normal file
68
rust_builder/cargokit/build_tool/lib/src/environment.dart
Normal file
@ -0,0 +1,68 @@
|
||||
/// 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';
|
||||
|
||||
extension on String {
|
||||
String resolveSymlink() => File(this).resolveSymbolicLinksSync();
|
||||
}
|
||||
|
||||
class Environment {
|
||||
/// Current build configuration (debug or release).
|
||||
static String get configuration =>
|
||||
_getEnv("CARGOKIT_CONFIGURATION").toLowerCase();
|
||||
|
||||
static bool get isDebug => configuration == 'debug';
|
||||
static bool get isRelease => configuration == 'release';
|
||||
|
||||
/// Temporary directory where Rust build artifacts are placed.
|
||||
static String get targetTempDir => _getEnv("CARGOKIT_TARGET_TEMP_DIR");
|
||||
|
||||
/// Final output directory where the build artifacts are placed.
|
||||
static String get outputDir => _getEnvPath('CARGOKIT_OUTPUT_DIR');
|
||||
|
||||
/// Path to the crate manifest (containing Cargo.toml).
|
||||
static String get manifestDir => _getEnvPath('CARGOKIT_MANIFEST_DIR');
|
||||
|
||||
/// Directory inside root project. Not necessarily root folder. Symlinks are
|
||||
/// not resolved on purpose.
|
||||
static String get rootProjectDir => _getEnv('CARGOKIT_ROOT_PROJECT_DIR');
|
||||
|
||||
// Pod
|
||||
|
||||
/// Platform name (macosx, iphoneos, iphonesimulator).
|
||||
static String get darwinPlatformName =>
|
||||
_getEnv("CARGOKIT_DARWIN_PLATFORM_NAME");
|
||||
|
||||
/// List of architectures to build for (arm64, armv7, x86_64).
|
||||
static List<String> get darwinArchs =>
|
||||
_getEnv("CARGOKIT_DARWIN_ARCHS").split(' ');
|
||||
|
||||
// Gradle
|
||||
static String get minSdkVersion => _getEnv("CARGOKIT_MIN_SDK_VERSION");
|
||||
static String get ndkVersion => _getEnv("CARGOKIT_NDK_VERSION");
|
||||
static String get sdkPath => _getEnvPath("CARGOKIT_SDK_DIR");
|
||||
static String get javaHome => _getEnvPath("CARGOKIT_JAVA_HOME");
|
||||
static List<String> get targetPlatforms =>
|
||||
_getEnv("CARGOKIT_TARGET_PLATFORMS").split(',');
|
||||
|
||||
// CMAKE
|
||||
static String get targetPlatform => _getEnv("CARGOKIT_TARGET_PLATFORM");
|
||||
|
||||
static String _getEnv(String key) {
|
||||
final res = Platform.environment[key];
|
||||
if (res == null) {
|
||||
throw Exception("Missing environment variable $key");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static String _getEnvPath(String key) {
|
||||
final res = _getEnv(key);
|
||||
if (Directory(res).existsSync()) {
|
||||
return res.resolveSymlink();
|
||||
} else {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
52
rust_builder/cargokit/build_tool/lib/src/logging.dart
Normal file
52
rust_builder/cargokit/build_tool/lib/src/logging.dart
Normal file
@ -0,0 +1,52 @@
|
||||
/// 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:logging/logging.dart';
|
||||
|
||||
const String kSeparator = "--";
|
||||
const String kDoubleSeparator = "==";
|
||||
|
||||
bool _lastMessageWasSeparator = false;
|
||||
|
||||
void _log(LogRecord rec) {
|
||||
final prefix = '${rec.level.name}: ';
|
||||
final out = rec.level == Level.SEVERE ? stderr : stdout;
|
||||
if (rec.message == kSeparator) {
|
||||
if (!_lastMessageWasSeparator) {
|
||||
out.write(prefix);
|
||||
out.writeln('-' * 80);
|
||||
_lastMessageWasSeparator = true;
|
||||
}
|
||||
return;
|
||||
} else if (rec.message == kDoubleSeparator) {
|
||||
out.write(prefix);
|
||||
out.writeln('=' * 80);
|
||||
_lastMessageWasSeparator = true;
|
||||
return;
|
||||
}
|
||||
out.write(prefix);
|
||||
out.writeln(rec.message);
|
||||
_lastMessageWasSeparator = false;
|
||||
}
|
||||
|
||||
void initLogging() {
|
||||
Logger.root.level = Level.INFO;
|
||||
Logger.root.onRecord.listen((LogRecord rec) {
|
||||
final lines = rec.message.split('\n');
|
||||
for (final line in lines) {
|
||||
if (line.isNotEmpty || lines.length == 1 || line != lines.last) {
|
||||
_log(LogRecord(
|
||||
rec.level,
|
||||
line,
|
||||
rec.loggerName,
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void enableVerboseLogging() {
|
||||
Logger.root.level = Level.ALL;
|
||||
}
|
309
rust_builder/cargokit/build_tool/lib/src/options.dart
Normal file
309
rust_builder/cargokit/build_tool/lib/src/options.dart
Normal file
@ -0,0 +1,309 @@
|
||||
/// 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;
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
/// 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:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:github/github.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'artifacts_provider.dart';
|
||||
import 'builder.dart';
|
||||
import 'cargo.dart';
|
||||
import 'crate_hash.dart';
|
||||
import 'options.dart';
|
||||
import 'rustup.dart';
|
||||
import 'target.dart';
|
||||
|
||||
final _log = Logger('precompile_binaries');
|
||||
|
||||
class PrecompileBinaries {
|
||||
PrecompileBinaries({
|
||||
required this.privateKey,
|
||||
required this.githubToken,
|
||||
required this.repositorySlug,
|
||||
required this.manifestDir,
|
||||
required this.targets,
|
||||
this.androidSdkLocation,
|
||||
this.androidNdkVersion,
|
||||
this.androidMinSdkVersion,
|
||||
this.tempDir,
|
||||
});
|
||||
|
||||
final PrivateKey privateKey;
|
||||
final String githubToken;
|
||||
final RepositorySlug repositorySlug;
|
||||
final String manifestDir;
|
||||
final List<Target> targets;
|
||||
final String? androidSdkLocation;
|
||||
final String? androidNdkVersion;
|
||||
final int? androidMinSdkVersion;
|
||||
final String? tempDir;
|
||||
|
||||
static String fileName(Target target, String name) {
|
||||
return '${target.rust}_$name';
|
||||
}
|
||||
|
||||
static String signatureFileName(Target target, String name) {
|
||||
return '${target.rust}_$name.sig';
|
||||
}
|
||||
|
||||
Future<void> run() async {
|
||||
final crateInfo = CrateInfo.load(manifestDir);
|
||||
|
||||
final targets = List.of(this.targets);
|
||||
if (targets.isEmpty) {
|
||||
targets.addAll([
|
||||
...Target.buildableTargets(),
|
||||
if (androidSdkLocation != null) ...Target.androidTargets(),
|
||||
]);
|
||||
}
|
||||
|
||||
_log.info('Precompiling binaries for $targets');
|
||||
|
||||
final hash = CrateHash.compute(manifestDir);
|
||||
_log.info('Computed crate hash: $hash');
|
||||
|
||||
final String tagName = 'precompiled_$hash';
|
||||
|
||||
final github = GitHub(auth: Authentication.withToken(githubToken));
|
||||
final repo = github.repositories;
|
||||
final release = await _getOrCreateRelease(
|
||||
repo: repo,
|
||||
tagName: tagName,
|
||||
packageName: crateInfo.packageName,
|
||||
hash: hash,
|
||||
);
|
||||
|
||||
final tempDir = this.tempDir != null
|
||||
? Directory(this.tempDir!)
|
||||
: Directory.systemTemp.createTempSync('precompiled_');
|
||||
|
||||
tempDir.createSync(recursive: true);
|
||||
|
||||
final crateOptions = CargokitCrateOptions.load(
|
||||
manifestDir: manifestDir,
|
||||
);
|
||||
|
||||
final buildEnvironment = BuildEnvironment(
|
||||
configuration: BuildConfiguration.release,
|
||||
crateOptions: crateOptions,
|
||||
targetTempDir: tempDir.path,
|
||||
manifestDir: manifestDir,
|
||||
crateInfo: crateInfo,
|
||||
isAndroid: androidSdkLocation != null,
|
||||
androidSdkPath: androidSdkLocation,
|
||||
androidNdkVersion: androidNdkVersion,
|
||||
androidMinSdkVersion: androidMinSdkVersion,
|
||||
);
|
||||
|
||||
final rustup = Rustup();
|
||||
|
||||
for (final target in targets) {
|
||||
final artifactNames = getArtifactNames(
|
||||
target: target,
|
||||
libraryName: crateInfo.packageName,
|
||||
remote: true,
|
||||
);
|
||||
|
||||
if (artifactNames.every((name) {
|
||||
final fileName = PrecompileBinaries.fileName(target, name);
|
||||
return (release.assets ?? []).any((e) => e.name == fileName);
|
||||
})) {
|
||||
_log.info("All artifacts for $target already exist - skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
_log.info('Building for $target');
|
||||
|
||||
final builder =
|
||||
RustBuilder(target: target, environment: buildEnvironment);
|
||||
builder.prepare(rustup);
|
||||
final res = await builder.build();
|
||||
|
||||
final assets = <CreateReleaseAsset>[];
|
||||
for (final name in artifactNames) {
|
||||
final file = File(path.join(res, name));
|
||||
if (!file.existsSync()) {
|
||||
throw Exception('Missing artifact: ${file.path}');
|
||||
}
|
||||
|
||||
final data = file.readAsBytesSync();
|
||||
final create = CreateReleaseAsset(
|
||||
name: PrecompileBinaries.fileName(target, name),
|
||||
contentType: "application/octet-stream",
|
||||
assetData: data,
|
||||
);
|
||||
final signature = sign(privateKey, data);
|
||||
final signatureCreate = CreateReleaseAsset(
|
||||
name: signatureFileName(target, name),
|
||||
contentType: "application/octet-stream",
|
||||
assetData: signature,
|
||||
);
|
||||
bool verified = verify(public(privateKey), data, signature);
|
||||
if (!verified) {
|
||||
throw Exception('Signature verification failed');
|
||||
}
|
||||
assets.add(create);
|
||||
assets.add(signatureCreate);
|
||||
}
|
||||
_log.info('Uploading assets: ${assets.map((e) => e.name)}');
|
||||
for (final asset in assets) {
|
||||
// This seems to be failing on CI so do it one by one
|
||||
int retryCount = 0;
|
||||
while (true) {
|
||||
try {
|
||||
await repo.uploadReleaseAssets(release, [asset]);
|
||||
break;
|
||||
} on Exception catch (e) {
|
||||
if (retryCount == 10) {
|
||||
rethrow;
|
||||
}
|
||||
++retryCount;
|
||||
_log.shout(
|
||||
'Upload failed (attempt $retryCount, will retry): ${e.toString()}');
|
||||
await Future.delayed(Duration(seconds: 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_log.info('Cleaning up');
|
||||
tempDir.deleteSync(recursive: true);
|
||||
}
|
||||
|
||||
Future<Release> _getOrCreateRelease({
|
||||
required RepositoriesService repo,
|
||||
required String tagName,
|
||||
required String packageName,
|
||||
required String hash,
|
||||
}) async {
|
||||
Release release;
|
||||
try {
|
||||
_log.info('Fetching release $tagName');
|
||||
release = await repo.getReleaseByTagName(repositorySlug, tagName);
|
||||
} on ReleaseNotFound {
|
||||
_log.info('Release not found - creating release $tagName');
|
||||
release = await repo.createRelease(
|
||||
repositorySlug,
|
||||
CreateRelease.from(
|
||||
tagName: tagName,
|
||||
name: 'Precompiled binaries ${hash.substring(0, 8)}',
|
||||
targetCommitish: null,
|
||||
isDraft: false,
|
||||
isPrerelease: false,
|
||||
body: 'Precompiled binaries for crate $packageName, '
|
||||
'crate hash $hash.',
|
||||
));
|
||||
}
|
||||
return release;
|
||||
}
|
||||
}
|
136
rust_builder/cargokit/build_tool/lib/src/rustup.dart
Normal file
136
rust_builder/cargokit/build_tool/lib/src/rustup.dart
Normal file
@ -0,0 +1,136 @@
|
||||
/// 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:path/path.dart' as path;
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
class _Toolchain {
|
||||
_Toolchain(
|
||||
this.name,
|
||||
this.targets,
|
||||
);
|
||||
|
||||
final String name;
|
||||
final List<String> targets;
|
||||
}
|
||||
|
||||
class Rustup {
|
||||
List<String>? installedTargets(String toolchain) {
|
||||
final targets = _installedTargets(toolchain);
|
||||
return targets != null ? List.unmodifiable(targets) : null;
|
||||
}
|
||||
|
||||
void installToolchain(String toolchain) {
|
||||
log.info("Installing Rust toolchain: $toolchain");
|
||||
runCommand("rustup", ['toolchain', 'install', toolchain]);
|
||||
_installedToolchains
|
||||
.add(_Toolchain(toolchain, _getInstalledTargets(toolchain)));
|
||||
}
|
||||
|
||||
void installTarget(
|
||||
String target, {
|
||||
required String toolchain,
|
||||
}) {
|
||||
log.info("Installing Rust target: $target");
|
||||
runCommand("rustup", [
|
||||
'target',
|
||||
'add',
|
||||
'--toolchain',
|
||||
toolchain,
|
||||
target,
|
||||
]);
|
||||
_installedTargets(toolchain)?.add(target);
|
||||
}
|
||||
|
||||
final List<_Toolchain> _installedToolchains;
|
||||
|
||||
Rustup() : _installedToolchains = _getInstalledToolchains();
|
||||
|
||||
List<String>? _installedTargets(String toolchain) => _installedToolchains
|
||||
.firstWhereOrNull(
|
||||
(e) => e.name == toolchain || e.name.startsWith('$toolchain-'))
|
||||
?.targets;
|
||||
|
||||
static List<_Toolchain> _getInstalledToolchains() {
|
||||
String extractToolchainName(String line) {
|
||||
// ignore (default) after toolchain name
|
||||
final parts = line.split(' ');
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
final res = runCommand("rustup", ['toolchain', 'list']);
|
||||
|
||||
// To list all non-custom toolchains, we need to filter out lines that
|
||||
// don't start with "stable", "beta", or "nightly".
|
||||
Pattern nonCustom = RegExp(r"^(stable|beta|nightly)");
|
||||
final lines = res.stdout
|
||||
.toString()
|
||||
.split('\n')
|
||||
.where((e) => e.isNotEmpty && e.startsWith(nonCustom))
|
||||
.map(extractToolchainName)
|
||||
.toList(growable: true);
|
||||
|
||||
return lines
|
||||
.map(
|
||||
(name) => _Toolchain(
|
||||
name,
|
||||
_getInstalledTargets(name),
|
||||
),
|
||||
)
|
||||
.toList(growable: true);
|
||||
}
|
||||
|
||||
static List<String> _getInstalledTargets(String toolchain) {
|
||||
final res = runCommand("rustup", [
|
||||
'target',
|
||||
'list',
|
||||
'--toolchain',
|
||||
toolchain,
|
||||
'--installed',
|
||||
]);
|
||||
final lines = res.stdout
|
||||
.toString()
|
||||
.split('\n')
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toList(growable: true);
|
||||
return lines;
|
||||
}
|
||||
|
||||
bool _didInstallRustSrcForNightly = false;
|
||||
|
||||
void installRustSrcForNightly() {
|
||||
if (_didInstallRustSrcForNightly) {
|
||||
return;
|
||||
}
|
||||
// Useful for -Z build-std
|
||||
runCommand(
|
||||
"rustup",
|
||||
['component', 'add', 'rust-src', '--toolchain', 'nightly'],
|
||||
);
|
||||
_didInstallRustSrcForNightly = true;
|
||||
}
|
||||
|
||||
static String? executablePath() {
|
||||
final envPath = Platform.environment['PATH'];
|
||||
final envPathSeparator = Platform.isWindows ? ';' : ':';
|
||||
final home = Platform.isWindows
|
||||
? Platform.environment['USERPROFILE']
|
||||
: Platform.environment['HOME'];
|
||||
final paths = [
|
||||
if (home != null) path.join(home, '.cargo', 'bin'),
|
||||
if (envPath != null) ...envPath.split(envPathSeparator),
|
||||
];
|
||||
for (final p in paths) {
|
||||
final rustup = Platform.isWindows ? 'rustup.exe' : 'rustup';
|
||||
final rustupPath = path.join(p, rustup);
|
||||
if (File(rustupPath).existsSync()) {
|
||||
return rustupPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
140
rust_builder/cargokit/build_tool/lib/src/target.dart
Normal file
140
rust_builder/cargokit/build_tool/lib/src/target.dart
Normal file
@ -0,0 +1,140 @@
|
||||
/// 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 'util.dart';
|
||||
|
||||
class Target {
|
||||
Target({
|
||||
required this.rust,
|
||||
this.flutter,
|
||||
this.android,
|
||||
this.androidMinSdkVersion,
|
||||
this.darwinPlatform,
|
||||
this.darwinArch,
|
||||
});
|
||||
|
||||
static final all = [
|
||||
Target(
|
||||
rust: 'armv7-linux-androideabi',
|
||||
flutter: 'android-arm',
|
||||
android: 'armeabi-v7a',
|
||||
androidMinSdkVersion: 16,
|
||||
),
|
||||
Target(
|
||||
rust: 'aarch64-linux-android',
|
||||
flutter: 'android-arm64',
|
||||
android: 'arm64-v8a',
|
||||
androidMinSdkVersion: 21,
|
||||
),
|
||||
Target(
|
||||
rust: 'i686-linux-android',
|
||||
flutter: 'android-x86',
|
||||
android: 'x86',
|
||||
androidMinSdkVersion: 16,
|
||||
),
|
||||
Target(
|
||||
rust: 'x86_64-linux-android',
|
||||
flutter: 'android-x64',
|
||||
android: 'x86_64',
|
||||
androidMinSdkVersion: 21,
|
||||
),
|
||||
Target(
|
||||
rust: 'x86_64-pc-windows-msvc',
|
||||
flutter: 'windows-x64',
|
||||
),
|
||||
Target(
|
||||
rust: 'x86_64-unknown-linux-gnu',
|
||||
flutter: 'linux-x64',
|
||||
),
|
||||
Target(
|
||||
rust: 'aarch64-unknown-linux-gnu',
|
||||
flutter: 'linux-arm64',
|
||||
),
|
||||
Target(
|
||||
rust: 'x86_64-apple-darwin',
|
||||
darwinPlatform: 'macosx',
|
||||
darwinArch: 'x86_64',
|
||||
),
|
||||
Target(
|
||||
rust: 'aarch64-apple-darwin',
|
||||
darwinPlatform: 'macosx',
|
||||
darwinArch: 'arm64',
|
||||
),
|
||||
Target(
|
||||
rust: 'aarch64-apple-ios',
|
||||
darwinPlatform: 'iphoneos',
|
||||
darwinArch: 'arm64',
|
||||
),
|
||||
Target(
|
||||
rust: 'aarch64-apple-ios-sim',
|
||||
darwinPlatform: 'iphonesimulator',
|
||||
darwinArch: 'arm64',
|
||||
),
|
||||
Target(
|
||||
rust: 'x86_64-apple-ios',
|
||||
darwinPlatform: 'iphonesimulator',
|
||||
darwinArch: 'x86_64',
|
||||
),
|
||||
];
|
||||
|
||||
static Target? forFlutterName(String flutterName) {
|
||||
return all.firstWhereOrNull((element) => element.flutter == flutterName);
|
||||
}
|
||||
|
||||
static Target? forDarwin({
|
||||
required String platformName,
|
||||
required String darwinAarch,
|
||||
}) {
|
||||
return all.firstWhereOrNull((element) => //
|
||||
element.darwinPlatform == platformName &&
|
||||
element.darwinArch == darwinAarch);
|
||||
}
|
||||
|
||||
static Target? forRustTriple(String triple) {
|
||||
return all.firstWhereOrNull((element) => element.rust == triple);
|
||||
}
|
||||
|
||||
static List<Target> androidTargets() {
|
||||
return all
|
||||
.where((element) => element.android != null)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
/// Returns buildable targets on current host platform ignoring Android targets.
|
||||
static List<Target> buildableTargets() {
|
||||
if (Platform.isLinux) {
|
||||
// Right now we don't support cross-compiling on Linux. So we just return
|
||||
// the host target.
|
||||
final arch = runCommand('arch', []).stdout as String;
|
||||
if (arch.trim() == 'aarch64') {
|
||||
return [Target.forRustTriple('aarch64-unknown-linux-gnu')!];
|
||||
} else {
|
||||
return [Target.forRustTriple('x86_64-unknown-linux-gnu')!];
|
||||
}
|
||||
}
|
||||
return all.where((target) {
|
||||
if (Platform.isWindows) {
|
||||
return target.rust.contains('-windows-');
|
||||
} else if (Platform.isMacOS) {
|
||||
return target.darwinPlatform != null;
|
||||
}
|
||||
return false;
|
||||
}).toList(growable: false);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return rust;
|
||||
}
|
||||
|
||||
final String? flutter;
|
||||
final String rust;
|
||||
final String? android;
|
||||
final int? androidMinSdkVersion;
|
||||
final String? darwinPlatform;
|
||||
final String? darwinArch;
|
||||
}
|
172
rust_builder/cargokit/build_tool/lib/src/util.dart
Normal file
172
rust_builder/cargokit/build_tool/lib/src/util.dart
Normal file
@ -0,0 +1,172 @@
|
||||
/// 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:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'logging.dart';
|
||||
import 'rustup.dart';
|
||||
|
||||
final log = Logger("process");
|
||||
|
||||
class CommandFailedException implements Exception {
|
||||
final String executable;
|
||||
final List<String> arguments;
|
||||
final ProcessResult result;
|
||||
|
||||
CommandFailedException({
|
||||
required this.executable,
|
||||
required this.arguments,
|
||||
required this.result,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final stdout = result.stdout.toString().trim();
|
||||
final stderr = result.stderr.toString().trim();
|
||||
return [
|
||||
"External Command: $executable ${arguments.map((e) => '"$e"').join(' ')}",
|
||||
"Returned Exit Code: ${result.exitCode}",
|
||||
kSeparator,
|
||||
"STDOUT:",
|
||||
if (stdout.isNotEmpty) stdout,
|
||||
kSeparator,
|
||||
"STDERR:",
|
||||
if (stderr.isNotEmpty) stderr,
|
||||
].join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
class TestRunCommandArgs {
|
||||
final String executable;
|
||||
final List<String> arguments;
|
||||
final String? workingDirectory;
|
||||
final Map<String, String>? environment;
|
||||
final bool includeParentEnvironment;
|
||||
final bool runInShell;
|
||||
final Encoding? stdoutEncoding;
|
||||
final Encoding? stderrEncoding;
|
||||
|
||||
TestRunCommandArgs({
|
||||
required this.executable,
|
||||
required this.arguments,
|
||||
this.workingDirectory,
|
||||
this.environment,
|
||||
this.includeParentEnvironment = true,
|
||||
this.runInShell = false,
|
||||
this.stdoutEncoding,
|
||||
this.stderrEncoding,
|
||||
});
|
||||
}
|
||||
|
||||
class TestRunCommandResult {
|
||||
TestRunCommandResult({
|
||||
this.pid = 1,
|
||||
this.exitCode = 0,
|
||||
this.stdout = '',
|
||||
this.stderr = '',
|
||||
});
|
||||
|
||||
final int pid;
|
||||
final int exitCode;
|
||||
final String stdout;
|
||||
final String stderr;
|
||||
}
|
||||
|
||||
TestRunCommandResult Function(TestRunCommandArgs args)? testRunCommandOverride;
|
||||
|
||||
ProcessResult runCommand(
|
||||
String executable,
|
||||
List<String> arguments, {
|
||||
String? workingDirectory,
|
||||
Map<String, String>? environment,
|
||||
bool includeParentEnvironment = true,
|
||||
bool runInShell = false,
|
||||
Encoding? stdoutEncoding = systemEncoding,
|
||||
Encoding? stderrEncoding = systemEncoding,
|
||||
}) {
|
||||
if (testRunCommandOverride != null) {
|
||||
final result = testRunCommandOverride!(TestRunCommandArgs(
|
||||
executable: executable,
|
||||
arguments: arguments,
|
||||
workingDirectory: workingDirectory,
|
||||
environment: environment,
|
||||
includeParentEnvironment: includeParentEnvironment,
|
||||
runInShell: runInShell,
|
||||
stdoutEncoding: stdoutEncoding,
|
||||
stderrEncoding: stderrEncoding,
|
||||
));
|
||||
return ProcessResult(
|
||||
result.pid,
|
||||
result.exitCode,
|
||||
result.stdout,
|
||||
result.stderr,
|
||||
);
|
||||
}
|
||||
log.finer('Running command $executable ${arguments.join(' ')}');
|
||||
final res = Process.runSync(
|
||||
_resolveExecutable(executable),
|
||||
arguments,
|
||||
workingDirectory: workingDirectory,
|
||||
environment: environment,
|
||||
includeParentEnvironment: includeParentEnvironment,
|
||||
runInShell: runInShell,
|
||||
stderrEncoding: stderrEncoding,
|
||||
stdoutEncoding: stdoutEncoding,
|
||||
);
|
||||
if (res.exitCode != 0) {
|
||||
throw CommandFailedException(
|
||||
executable: executable,
|
||||
arguments: arguments,
|
||||
result: res,
|
||||
);
|
||||
} else {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
class RustupNotFoundException implements Exception {
|
||||
@override
|
||||
String toString() {
|
||||
return [
|
||||
' ',
|
||||
'rustup not found in PATH.',
|
||||
' ',
|
||||
'Maybe you need to install Rust? It only takes a minute:',
|
||||
' ',
|
||||
if (Platform.isWindows) 'https://www.rust-lang.org/tools/install',
|
||||
if (hasHomebrewRustInPath()) ...[
|
||||
'\$ brew unlink rust # Unlink homebrew Rust from PATH',
|
||||
],
|
||||
if (!Platform.isWindows)
|
||||
"\$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh",
|
||||
' ',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
static bool hasHomebrewRustInPath() {
|
||||
if (!Platform.isMacOS) {
|
||||
return false;
|
||||
}
|
||||
final envPath = Platform.environment['PATH'] ?? '';
|
||||
final paths = envPath.split(':');
|
||||
return paths.any((p) {
|
||||
return p.contains('homebrew') && File(path.join(p, 'rustc')).existsSync();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String _resolveExecutable(String executable) {
|
||||
if (executable == 'rustup') {
|
||||
final resolved = Rustup.executablePath();
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
throw RustupNotFoundException();
|
||||
} else {
|
||||
return executable;
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/// 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:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import 'artifacts_provider.dart';
|
||||
import 'cargo.dart';
|
||||
import 'crate_hash.dart';
|
||||
import 'options.dart';
|
||||
import 'precompile_binaries.dart';
|
||||
import 'target.dart';
|
||||
|
||||
class VerifyBinaries {
|
||||
VerifyBinaries({
|
||||
required this.manifestDir,
|
||||
});
|
||||
|
||||
final String manifestDir;
|
||||
|
||||
Future<void> run() async {
|
||||
final crateInfo = CrateInfo.load(manifestDir);
|
||||
|
||||
final config = CargokitCrateOptions.load(manifestDir: manifestDir);
|
||||
final precompiledBinaries = config.precompiledBinaries;
|
||||
if (precompiledBinaries == null) {
|
||||
stdout.writeln('Crate does not support precompiled binaries.');
|
||||
} else {
|
||||
final crateHash = CrateHash.compute(manifestDir);
|
||||
stdout.writeln('Crate hash: $crateHash');
|
||||
|
||||
for (final target in Target.all) {
|
||||
final message = 'Checking ${target.rust}...';
|
||||
stdout.write(message.padRight(40));
|
||||
stdout.flush();
|
||||
|
||||
final artifacts = getArtifactNames(
|
||||
target: target,
|
||||
libraryName: crateInfo.packageName,
|
||||
remote: true,
|
||||
);
|
||||
|
||||
final prefix = precompiledBinaries.uriPrefix;
|
||||
|
||||
bool ok = true;
|
||||
|
||||
for (final artifact in artifacts) {
|
||||
final fileName = PrecompileBinaries.fileName(target, artifact);
|
||||
final signatureFileName =
|
||||
PrecompileBinaries.signatureFileName(target, artifact);
|
||||
|
||||
final url = Uri.parse('$prefix$crateHash/$fileName');
|
||||
final signatureUrl =
|
||||
Uri.parse('$prefix$crateHash/$signatureFileName');
|
||||
|
||||
final signature = await get(signatureUrl);
|
||||
if (signature.statusCode != 200) {
|
||||
stdout.writeln('MISSING');
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
final asset = await get(url);
|
||||
if (asset.statusCode != 200) {
|
||||
stdout.writeln('MISSING');
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!verify(precompiledBinaries.publicKey, asset.bodyBytes,
|
||||
signature.bodyBytes)) {
|
||||
stdout.writeln('INVALID SIGNATURE');
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
stdout.writeln('OK');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user