Import IKAR messenger client and server
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.dart_tool/
|
||||
build/
|
||||
rust/target/
|
||||
@@ -0,0 +1,18 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026-present dkaraush <dkaraush@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,29 @@
|
||||
# komet_tlottie
|
||||
|
||||
Local FFI plugin that ships [tlottie](https://github.com/dkaraush/tlottie) — a Lottie
|
||||
renderer written in Rust — to every native Komet build. It carries only the native
|
||||
library and its raw bindings (`TlottieBindings`); playback, the isolate pool and the
|
||||
frame caches live in the app under `lib/core/media/tlottie/`.
|
||||
|
||||
## How it is built
|
||||
|
||||
`rust/` is a wrapper crate that depends on tlottie by an exact git `rev` with the
|
||||
`c-api` feature and builds it as `cdylib` + `staticlib`, so the `tlottie_*` symbols of
|
||||
the C ABI end up in `libkomet_tlottie`. `cargokit/` is vendored from `fjs` 3.3.0 (the
|
||||
copy that also aligns Android arm64/x86_64 ELF segments to 16 KB pages) and compiles the
|
||||
crate during the Flutter build with the same toolchain as `kolibri` and `fjs`;
|
||||
`cargokit_options.yaml` at the app root keeps it building from source.
|
||||
|
||||
| Platform | Artifact | Loaded via |
|
||||
|----------|----------|------------|
|
||||
| Android | `libkomet_tlottie.so` per ABI, packaged into the APK | `DynamicLibrary.open('libkomet_tlottie.so')` |
|
||||
| Linux | `libkomet_tlottie.so` in the bundle's `lib/` | `DynamicLibrary.open('libkomet_tlottie.so')` |
|
||||
| Windows | `komet_tlottie.dll` next to the executable | `DynamicLibrary.open('komet_tlottie.dll')` |
|
||||
| iOS / macOS | `libkomet_tlottie.a`, `-force_load`ed into the pod | `DynamicLibrary.process()`, falling back to `komet_tlottie.framework` |
|
||||
|
||||
Web has no native path; the app plays animations with the pure-Dart `lottie` package there.
|
||||
|
||||
## Bumping tlottie
|
||||
|
||||
Change `rev` in `rust/Cargo.toml`, run `cargo update -p tlottie` in `rust/`, and check
|
||||
that `tlottie.h` upstream still matches the signatures in `lib/komet_tlottie.dart`.
|
||||
@@ -0,0 +1,43 @@
|
||||
group = "pw.komet.komet_tlottie"
|
||||
version = "0.1.0"
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.android.library"
|
||||
|
||||
android {
|
||||
namespace = "pw.komet.komet_tlottie"
|
||||
compileSdk = 35
|
||||
ndkVersion = android.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 23
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "../cargokit/gradle/plugin.gradle"
|
||||
cargokit {
|
||||
manifestDir = "../rust"
|
||||
libname = "komet_tlottie"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/// 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
|
||||
|
||||
Copyright 2022 Matej Knopp
|
||||
|
||||
================================================================================
|
||||
|
||||
MIT LICENSE
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
|
||||
APACHE LICENSE, VERSION 2.0
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/// 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
|
||||
|
||||
Experimental repository to provide glue for seamlessly integrating cargo build
|
||||
with flutter plugins and packages.
|
||||
|
||||
See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/
|
||||
for a tutorial on how to use Cargokit.
|
||||
|
||||
Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin.
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
BASEDIR=$(dirname "$0")
|
||||
|
||||
# Workaround for https://github.com/dart-lang/pub/issues/4010
|
||||
BASEDIR=$(cd "$BASEDIR" ; pwd -P)
|
||||
|
||||
# Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project
|
||||
NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"`
|
||||
|
||||
export PATH=${NEW_PATH%?} # remove trailing :
|
||||
|
||||
env
|
||||
|
||||
# Platform name (macosx, iphoneos, iphonesimulator)
|
||||
export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME
|
||||
|
||||
# Arctive architectures (arm64, armv7, x86_64), space separated.
|
||||
export CARGOKIT_DARWIN_ARCHS=$ARCHS
|
||||
|
||||
# Current build configuration (Debug, Release)
|
||||
export CARGOKIT_CONFIGURATION=$CONFIGURATION
|
||||
|
||||
# Path to directory containing Cargo.toml.
|
||||
export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1
|
||||
|
||||
# Temporary directory for build artifacts.
|
||||
export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR
|
||||
|
||||
# Output directory for final artifacts.
|
||||
export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME
|
||||
|
||||
# Directory to store built tool artifacts.
|
||||
export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool
|
||||
|
||||
# Directory inside root project. Not necessarily the top level directory of root project.
|
||||
export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT
|
||||
|
||||
FLUTTER_EXPORT_BUILD_ENVIRONMENT=(
|
||||
"$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS
|
||||
"$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS
|
||||
)
|
||||
|
||||
for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}"
|
||||
do
|
||||
if [[ -f "$path" ]]; then
|
||||
source "$path"
|
||||
fi
|
||||
done
|
||||
|
||||
sh "$BASEDIR/run_build_tool.sh" build-pod "$@"
|
||||
|
||||
# Make a symlink from built framework to phony file, which will be used as input to
|
||||
# build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate
|
||||
# attribute on custom build phase)
|
||||
ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony"
|
||||
ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out"
|
||||
@@ -0,0 +1,123 @@
|
||||
# fjs Cargokit Build Tool
|
||||
|
||||
This package is the local Cargokit build tool used by fjs. It is based on the
|
||||
Flutter Rust Bridge Cargokit integration and is vendored here so the Flutter
|
||||
plugin can build Rust artifacts without depending on a globally installed Dart
|
||||
package.
|
||||
|
||||
The entrypoint is normally invoked through:
|
||||
|
||||
```sh
|
||||
cargokit/run_build_tool.sh <command> ...
|
||||
```
|
||||
|
||||
`run_build_tool.sh` creates a temporary Dart runner, compiles it to a kernel
|
||||
snapshot, and invalidates that snapshot when this package changes.
|
||||
|
||||
## Commands
|
||||
|
||||
### build-pod
|
||||
|
||||
Used by the iOS, macOS, and shared Darwin podspecs:
|
||||
|
||||
```sh
|
||||
cargokit/run_build_tool.sh build-pod libfjs fjs
|
||||
```
|
||||
|
||||
Flutter and CocoaPods provide the required `CARGOKIT_*` and Xcode deployment
|
||||
environment variables. The command builds the Rust crate for the active Darwin
|
||||
platform and copies `libfjs.a` into the configured build output directory.
|
||||
|
||||
### build-gradle
|
||||
|
||||
Used by Android builds. Flutter/Gradle provide Android SDK, NDK, target platform,
|
||||
and output directories through `CARGOKIT_*` environment variables.
|
||||
|
||||
### build-cmake
|
||||
|
||||
Used by desktop CMake-based builds. The command builds the Rust crate for the
|
||||
requested desktop target and copies the produced dynamic library into the
|
||||
configured output directory.
|
||||
|
||||
### gen-key
|
||||
|
||||
Generates an Ed25519 key pair for signing precompiled Cargokit artifacts:
|
||||
|
||||
```sh
|
||||
cargokit/run_build_tool.sh gen-key
|
||||
```
|
||||
|
||||
Store the private key in CI secrets only. Put the public key in
|
||||
`libfjs/cargokit.yaml`.
|
||||
|
||||
### precompile-binaries
|
||||
|
||||
Builds release Rust artifacts, signs them, and uploads them to a GitHub Release
|
||||
tag named `precompiled_<crate-hash>`:
|
||||
|
||||
```sh
|
||||
PRIVATE_KEY=<hex-private-key> \
|
||||
GITHUB_TOKEN=<github-token> \
|
||||
cargokit/run_build_tool.sh precompile-binaries \
|
||||
--repository fluttercandies/fjs \
|
||||
--manifest-dir libfjs
|
||||
```
|
||||
|
||||
Pass `--target <rust-triple>` one or more times to publish a subset of targets.
|
||||
When omitted, the tool builds targets that are buildable on the current host and
|
||||
adds Android targets when Android SDK options are provided.
|
||||
|
||||
### verify-binaries
|
||||
|
||||
Checks that every configured precompiled artifact exists and that its signature
|
||||
matches `libfjs/cargokit.yaml`:
|
||||
|
||||
```sh
|
||||
cargokit/run_build_tool.sh verify-binaries --manifest-dir libfjs
|
||||
```
|
||||
|
||||
This command verifies per-target Cargokit artifacts. It does not validate the
|
||||
SwiftPM XCFramework zip; use `tool/prepare_darwin_release.sh` for that release
|
||||
path.
|
||||
|
||||
## Configuration
|
||||
|
||||
Crate-level settings live in `libfjs/cargokit.yaml`:
|
||||
|
||||
```yaml
|
||||
precompiled_binaries:
|
||||
url_prefix: https://github.com/fluttercandies/fjs/releases/download/precompiled_
|
||||
public_key: <32-byte-ed25519-public-key-hex>
|
||||
```
|
||||
|
||||
User-level settings are read from the nearest `cargokit_options.yaml` found by
|
||||
walking upward from `CARGOKIT_ROOT_PROJECT_DIR`:
|
||||
|
||||
```yaml
|
||||
use_precompiled_binaries: true
|
||||
verbose_logging: false
|
||||
```
|
||||
|
||||
Default behavior:
|
||||
|
||||
- If Rustup is available, builds use local Rust source.
|
||||
- If Rustup is missing, builds try signed precompiled binaries.
|
||||
- `use_precompiled_binaries: true` opts in to precompiled downloads even when
|
||||
Rustup is available.
|
||||
- `use_precompiled_binaries: false` forces local Rust builds.
|
||||
|
||||
## Darwin XCFramework Release Flow
|
||||
|
||||
CocoaPods and the Swift Package Manager artifact share the same Rust source and
|
||||
Cargokit build inputs, but the XCFramework is packaged by repository scripts:
|
||||
|
||||
```sh
|
||||
tool/build_fjs_xcframework.sh --configuration Release
|
||||
tool/prepare_darwin_release.sh --configuration Release
|
||||
```
|
||||
|
||||
`tool/build_fjs_xcframework.sh` writes a temporary options file that sets
|
||||
`use_precompiled_binaries: false`, so the release XCFramework is built from the
|
||||
current local Rust source. `tool/prepare_darwin_release.sh` then creates
|
||||
`build/darwin-release/fjs.xcframework.zip`, writes the SwiftPM checksum, checks
|
||||
Darwin package metadata, and runs `flutter pub publish --dry-run`.
|
||||
@@ -0,0 +1,34 @@
|
||||
# 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
|
||||
|
||||
# This file configures the static analysis results for your project (errors,
|
||||
# warnings, and lints).
|
||||
#
|
||||
# This enables the 'recommended' set of lints from `package:lints`.
|
||||
# This set helps identify many issues that may lead to problems when running
|
||||
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||
# style and format.
|
||||
#
|
||||
# If you want a smaller set of lints you can change this to specify
|
||||
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||
# (the recommended set includes the core lints).
|
||||
# The core lints are also what is used by pub.dev for scoring packages.
|
||||
|
||||
include: package:lints/recommended.yaml
|
||||
|
||||
# Uncomment the following section to specify additional rules.
|
||||
|
||||
linter:
|
||||
rules:
|
||||
- prefer_relative_imports
|
||||
- directives_ordering
|
||||
|
||||
# analyzer:
|
||||
# exclude:
|
||||
# - path/to/excluded/files/**
|
||||
|
||||
# For more information about the core and recommended set of lints, see
|
||||
# https://dart.dev/go/core-lints
|
||||
|
||||
# For additional information about configuring this file, see
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
@@ -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 'package:build_tool/build_tool.dart' as build_tool;
|
||||
|
||||
void main(List<String> arguments) {
|
||||
build_tool.runMain(arguments);
|
||||
}
|
||||
@@ -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,235 @@
|
||||
/// 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: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 cxxFlagsKey = '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;
|
||||
|
||||
final sysroot = path.join(
|
||||
ndkPath,
|
||||
'toolchains',
|
||||
'llvm',
|
||||
'prebuilt',
|
||||
hostArch,
|
||||
'sysroot',
|
||||
);
|
||||
|
||||
final bindgenKey = "BINDGEN_EXTRA_CLANG_ARGS_${target.rust}";
|
||||
final sysrootForClang = _clangPath(sysroot);
|
||||
final includeForClang =
|
||||
_clangPath(path.join(sysroot, 'usr', 'include', target.rust));
|
||||
final bindgenValue =
|
||||
"$targetArg --sysroot=$sysrootForClang -I$includeForClang";
|
||||
|
||||
final env = {
|
||||
arKey: arValue,
|
||||
ccKey: ccValue,
|
||||
cfFlagsKey: cFlagsValue,
|
||||
cxxKey: cxxValue,
|
||||
cxxFlagsKey: cxxFlagsValue,
|
||||
ranlibKey: ranlibValue,
|
||||
rustFlagsKey: rustFlagsValue,
|
||||
linkerKey: selfPath,
|
||||
bindgenKey: bindgenValue,
|
||||
// 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,
|
||||
};
|
||||
|
||||
log.info(JsonEncoder.withIndent(' ').convert(env));
|
||||
return env;
|
||||
}
|
||||
|
||||
String _clangPath(String value) {
|
||||
return value.replaceAll(r'\', '/');
|
||||
}
|
||||
|
||||
// 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';
|
||||
}
|
||||
|
||||
if (["arm64-v8a", "x86_64"].contains(target.android)) {
|
||||
rustFlags = '$rustFlags-L\x1f$workaroundDir\x1f';
|
||||
|
||||
const pageSizeArgs = [
|
||||
"-C",
|
||||
"link-arg=-Wl,--hash-style=both",
|
||||
"-C",
|
||||
"link-arg=-Wl,-z,max-page-size=16384"
|
||||
];
|
||||
final pageSizeArgsString = pageSizeArgs.join("\x1f");
|
||||
|
||||
rustFlags = '$rustFlags$pageSizeArgsString';
|
||||
} else {
|
||||
rustFlags = '$rustFlags-L\x1f$workaroundDir';
|
||||
}
|
||||
return rustFlags;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
/// 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: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 'precompiled_asset_store.dart';
|
||||
import 'precompiled_generation.dart';
|
||||
import 'rustup.dart';
|
||||
import 'target.dart';
|
||||
|
||||
class Artifact {
|
||||
Artifact({
|
||||
required this.path,
|
||||
required this.finalFileName,
|
||||
});
|
||||
|
||||
/// 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;
|
||||
}
|
||||
throw Exception('Unknown artifact type for $finalFileName');
|
||||
}
|
||||
}
|
||||
|
||||
typedef ArtifactHttpGet = Future<Response> Function(Uri url);
|
||||
typedef ArtifactGenerationHash = String Function(BuildEnvironment environment);
|
||||
typedef ArtifactLocalBuilder = Future<List<Artifact>> Function(
|
||||
Target target, BuildEnvironment environment);
|
||||
typedef ArtifactTransportFactory = PrecompiledAssetTransport Function();
|
||||
|
||||
final _log = Logger('artifacts_provider');
|
||||
|
||||
class ArtifactProvider {
|
||||
ArtifactProvider({
|
||||
required this.environment,
|
||||
required this.userOptions,
|
||||
ArtifactHttpGet? httpGet,
|
||||
PrecompiledHttpSend? httpSend,
|
||||
ArtifactGenerationHash? generationHash,
|
||||
ArtifactLocalBuilder? localBuilder,
|
||||
ArtifactTransportFactory? transportFactory,
|
||||
PrecompiledTransportPolicy transportPolicy =
|
||||
const PrecompiledTransportPolicy(),
|
||||
this.storePolicy = const PrecompiledAssetStorePolicy(),
|
||||
}) : generationHash = generationHash ??
|
||||
((environment) => CrateHash.compute(
|
||||
environment.manifestDir,
|
||||
tempStorage: environment.targetTempDir,
|
||||
)),
|
||||
localBuilder = localBuilder ?? _defaultLocalBuilder,
|
||||
transportFactory = transportFactory ??
|
||||
(() => PrecompiledAssetTransport(
|
||||
send: httpSend ??
|
||||
(httpGet == null ? null : _sendFromGet(httpGet)),
|
||||
policy: transportPolicy,
|
||||
));
|
||||
|
||||
final BuildEnvironment environment;
|
||||
final CargokitUserOptions userOptions;
|
||||
final ArtifactGenerationHash generationHash;
|
||||
final ArtifactLocalBuilder localBuilder;
|
||||
final ArtifactTransportFactory transportFactory;
|
||||
final PrecompiledAssetStorePolicy storePolicy;
|
||||
|
||||
Future<Map<Target, List<Artifact>>> getArtifacts(List<Target> targets) async {
|
||||
final result = await _getPrecompiledArtifacts(targets);
|
||||
final pendingTargets = List.of(targets)
|
||||
..removeWhere((target) => result.containsKey(target));
|
||||
|
||||
if (pendingTargets.isEmpty) return result;
|
||||
if (!userOptions.allowLocalBuild) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Required precompiled generation did not provide all requested targets.');
|
||||
}
|
||||
for (final target in pendingTargets) {
|
||||
_log.info('Building ${environment.crateInfo.packageName} for $target');
|
||||
result[target] = await localBuilder(target, environment);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Map<Target, List<Artifact>>> _getPrecompiledArtifacts(
|
||||
List<Target> targets) async {
|
||||
if (userOptions.precompiledBinariesMode ==
|
||||
PrecompiledBinariesMode.disabled) {
|
||||
_log.info('Precompiled binaries are disabled');
|
||||
return {};
|
||||
}
|
||||
final precompiled = environment.crateOptions.precompiledBinaries;
|
||||
if (precompiled == null) return {};
|
||||
final recipe = precompiled.buildRecipe;
|
||||
if (recipe == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Configured precompiled binaries need a complete build recipe.');
|
||||
}
|
||||
final pinnedTargets = recipe.rustTargets.toSet();
|
||||
if (targets.any((target) => !pinnedTargets.contains(target.rust))) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Requested Rust target is not included in the signed build recipe.');
|
||||
}
|
||||
|
||||
final requiredForTargets = <Target, List<String>>{
|
||||
for (final target in targets)
|
||||
target: getArtifactNames(
|
||||
target: target,
|
||||
libraryName: environment.crateInfo.packageName,
|
||||
remote: true,
|
||||
).map((name) => PrecompileBinaries.fileName(target, name)).toList(),
|
||||
};
|
||||
final generation = generationHash(environment);
|
||||
final uriPrefix = Uri.parse(precompiled.uriPrefix);
|
||||
final expectedAssetNames = _expectedAssetNames(recipe, precompiled);
|
||||
final expectedCompositeChecksums = _expectedCompositeChecksums(precompiled);
|
||||
final transport = transportFactory();
|
||||
try {
|
||||
final store = PrecompiledAssetStore(
|
||||
cacheRoot: path.join(environment.targetTempDir, 'precompiled'),
|
||||
uriPrefix: uriPrefix,
|
||||
publicKey: precompiled.publicKey,
|
||||
recipe: recipe,
|
||||
expectedAssetNames: expectedAssetNames,
|
||||
expectedCompositeChecksums: expectedCompositeChecksums,
|
||||
transport: transport,
|
||||
policy: storePolicy,
|
||||
logger: _log.info,
|
||||
);
|
||||
final snapshot = await store.snapshot(
|
||||
generationHash: generation,
|
||||
requestedAssetNames:
|
||||
requiredForTargets.values.expand((names) => names).toSet(),
|
||||
);
|
||||
if (snapshot == null) {
|
||||
if (userOptions.precompiledBinariesMode ==
|
||||
PrecompiledBinariesMode.auto) {
|
||||
if (!store.hasAnyState(generation)) return {};
|
||||
throw PrecompiledGenerationException(
|
||||
'Precompiled generation manifest is unavailable for existing cache state.');
|
||||
}
|
||||
throw PrecompiledGenerationException(
|
||||
'Required precompiled generation manifest is unavailable.');
|
||||
}
|
||||
|
||||
return {
|
||||
for (final entry in requiredForTargets.entries)
|
||||
entry.key: [
|
||||
for (final remoteName in entry.value)
|
||||
Artifact(
|
||||
path: snapshot.pathFor(remoteName),
|
||||
finalFileName:
|
||||
remoteName.substring('${entry.key.rust}_'.length),
|
||||
),
|
||||
],
|
||||
};
|
||||
} finally {
|
||||
transport.close();
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> _expectedAssetNames(
|
||||
PrecompiledBuildRecipe recipe, PrecompiledBinaries precompiled) {
|
||||
final names = <String>{};
|
||||
for (final rustTarget in recipe.rustTargets) {
|
||||
final target = Target.forRustTriple(rustTarget);
|
||||
if (target == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Manifest recipe has an unsupported Rust target.');
|
||||
}
|
||||
for (final type in AritifactType.values) {
|
||||
for (final name in getArtifactNames(
|
||||
target: target,
|
||||
libraryName: environment.crateInfo.packageName,
|
||||
remote: true,
|
||||
aritifactType: type,
|
||||
)) {
|
||||
names.add(PrecompileBinaries.fileName(target, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final group in precompiled.compositeGroups) {
|
||||
names.addAll(group.outputs);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
static Set<String> _expectedCompositeChecksums(
|
||||
PrecompiledBinaries precompiled) {
|
||||
final result = <String>{};
|
||||
for (final group in precompiled.compositeGroups) {
|
||||
for (final output in group.outputs) {
|
||||
if (!output.endsWith('.checksum')) continue;
|
||||
final archive = output.substring(0, output.length - '.checksum'.length);
|
||||
if (group.outputs.contains(archive)) {
|
||||
result.add('$archive\u0000$output');
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static PrecompiledHttpSend _sendFromGet(ArtifactHttpGet get) {
|
||||
return (request) async {
|
||||
final response = await get(request.url);
|
||||
return StreamedResponse(
|
||||
Stream.value(response.bodyBytes),
|
||||
response.statusCode,
|
||||
contentLength: response.bodyBytes.length,
|
||||
headers: response.headers,
|
||||
reasonPhrase: response.reasonPhrase,
|
||||
request: request,
|
||||
isRedirect: response.isRedirect,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Artifact>> _defaultLocalBuilder(
|
||||
Target target, BuildEnvironment environment) async {
|
||||
final rustup = Rustup();
|
||||
final builder = RustBuilder(target: target, environment: environment);
|
||||
builder.prepare(rustup);
|
||||
final targetDir = await builder.build();
|
||||
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,
|
||||
),
|
||||
};
|
||||
return artifactNames
|
||||
.map((artifactName) => Artifact(
|
||||
path: path.join(targetDir, artifactName),
|
||||
finalFileName: artifactName,
|
||||
))
|
||||
.where((artifact) => File(artifact.path).existsSync())
|
||||
.toList();
|
||||
}
|
||||
|
||||
enum AritifactType {
|
||||
staticlib,
|
||||
dylib,
|
||||
}
|
||||
|
||||
AritifactType artifactTypeForTarget(Target target) {
|
||||
if (target.darwinPlatform != null) {
|
||||
return AritifactType.staticlib;
|
||||
}
|
||||
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'];
|
||||
}
|
||||
return ['lib$libraryName.dylib'];
|
||||
} else if (target.rust.contains('-windows-')) {
|
||||
if (aritifactType == AritifactType.staticlib) {
|
||||
return ['$libraryName.lib'];
|
||||
}
|
||||
return [
|
||||
'$libraryName.dll',
|
||||
'$libraryName.dll.lib',
|
||||
if (!remote) '$libraryName.pdb',
|
||||
];
|
||||
} else if (target.rust.contains('-linux-')) {
|
||||
if (aritifactType == AritifactType.staticlib) {
|
||||
return ['lib$libraryName.a'];
|
||||
}
|
||||
return ['lib$libraryName.so'];
|
||||
}
|
||||
throw Exception('Unsupported target: ${target.rust}');
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
/// 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 BuildPrecompiledGenerationCommand extends Command {
|
||||
BuildPrecompiledGenerationCommand() {
|
||||
argParser
|
||||
..addOption(
|
||||
'manifest-dir',
|
||||
mandatory: true,
|
||||
help: 'Directory containing Cargo.toml and cargokit.yaml',
|
||||
)
|
||||
..addOption(
|
||||
'output-dir',
|
||||
mandatory: true,
|
||||
help: 'Directory to publish the unsigned local generation',
|
||||
)
|
||||
..addOption(
|
||||
'temp-dir',
|
||||
mandatory: true,
|
||||
help: 'Directory for Rust build intermediates',
|
||||
)
|
||||
..addMultiOption(
|
||||
'target',
|
||||
help: 'Pinned Rust target triple. Repeat for each target.',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final name = 'build-precompiled-generation';
|
||||
|
||||
@override
|
||||
final description =
|
||||
'Builds an unsigned local precompiled generation from the pinned recipe.';
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final generation = LocalPrecompiledGeneration(
|
||||
manifestDir: argResults!['manifest-dir'] as String,
|
||||
outputDir: argResults!['output-dir'] as String,
|
||||
tempDir: argResults!['temp-dir'] as String,
|
||||
targetTriples: argResults!['target'] as List<String>,
|
||||
);
|
||||
await generation.run();
|
||||
}
|
||||
}
|
||||
|
||||
class FinalizeLocalGenerationCommand extends Command {
|
||||
FinalizeLocalGenerationCommand() {
|
||||
argParser
|
||||
..addOption('manifest-dir', mandatory: true)
|
||||
..addOption('generation-dir', mandatory: true)
|
||||
..addOption(
|
||||
'scope',
|
||||
defaultsTo: 'full',
|
||||
allowed: ['full', 'darwin-acceptance'],
|
||||
)
|
||||
..addOption(
|
||||
'private-key',
|
||||
help: 'Ed25519 private key hex; defaults to PRIVATE_KEY.',
|
||||
)
|
||||
..addOption('source-commit');
|
||||
}
|
||||
|
||||
@override
|
||||
final name = 'finalize-local-generation';
|
||||
|
||||
@override
|
||||
final description =
|
||||
'Signs and validates an unsigned local precompiled generation.';
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final encodedKey = argResults!['private-key'] as String? ??
|
||||
Platform.environment['PRIVATE_KEY'];
|
||||
if (encodedKey == null) {
|
||||
throw ArgumentError('Missing --private-key or PRIVATE_KEY.');
|
||||
}
|
||||
final privateKey = HEX.decode(encodedKey);
|
||||
if (privateKey.length != 64) {
|
||||
throw ArgumentError('Private key must be 64 bytes long');
|
||||
}
|
||||
final scope = switch (argResults!['scope'] as String) {
|
||||
'full' => LocalGenerationFinalizationScope.full,
|
||||
'darwin-acceptance' => LocalGenerationFinalizationScope.darwinAcceptance,
|
||||
_ => throw ArgumentError('Unsupported generation scope.'),
|
||||
};
|
||||
final finalized = await LocalGenerationFinalizer(
|
||||
manifestDir: argResults!['manifest-dir'] as String,
|
||||
generationDir: argResults!['generation-dir'] as String,
|
||||
privateKey: PrivateKey(privateKey),
|
||||
scope: scope,
|
||||
sourceCommit: argResults!['source-commit'] as String?,
|
||||
).run();
|
||||
stdout.writeln(finalized.manifest.generationHash);
|
||||
}
|
||||
}
|
||||
|
||||
class PublishPrecompiledGenerationCommand extends Command {
|
||||
PublishPrecompiledGenerationCommand() {
|
||||
argParser
|
||||
..addOption('manifest-dir', mandatory: true)
|
||||
..addOption('generation-dir', mandatory: true)
|
||||
..addOption('repository', mandatory: true)
|
||||
..addOption('private-key', help: 'Ed25519 private key hex.')
|
||||
..addOption('github-token',
|
||||
help: 'GitHub token; defaults to GITHUB_TOKEN.')
|
||||
..addOption('source-commit');
|
||||
}
|
||||
|
||||
@override
|
||||
final name = 'publish-precompiled-generation';
|
||||
|
||||
@override
|
||||
final description = 'Finalizes and publishes one complete v2 generation.';
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final encodedKey = argResults!['private-key'] as String? ??
|
||||
Platform.environment['PRIVATE_KEY'];
|
||||
final token = argResults!['github-token'] as String? ??
|
||||
Platform.environment['GITHUB_TOKEN'];
|
||||
if (encodedKey == null || token == null) {
|
||||
throw ArgumentError('Missing private key or GitHub token.');
|
||||
}
|
||||
final privateKey = HEX.decode(encodedKey);
|
||||
if (privateKey.length != 64) {
|
||||
throw ArgumentError('Private key must be 64 bytes long');
|
||||
}
|
||||
final github = GitHub(auth: Authentication.withToken(token));
|
||||
final finalized = await LocalGenerationFinalizer(
|
||||
manifestDir: argResults!['manifest-dir'] as String,
|
||||
generationDir: argResults!['generation-dir'] as String,
|
||||
privateKey: PrivateKey(privateKey),
|
||||
scope: LocalGenerationFinalizationScope.full,
|
||||
sourceCommit: argResults!['source-commit'] as String?,
|
||||
).run();
|
||||
await PrecompiledGenerationPublisher(
|
||||
generation: finalized,
|
||||
gateway: GitHubPrecompiledReleaseGateway(
|
||||
repositories: github.repositories,
|
||||
repository: RepositorySlug.full(argResults!['repository'] as String),
|
||||
),
|
||||
).publish();
|
||||
}
|
||||
}
|
||||
|
||||
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 the complete v2 generation, assets, signatures, and checksums.';
|
||||
|
||||
@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(BuildPrecompiledGenerationCommand())
|
||||
..addCommand(FinalizeLocalGenerationCommand())
|
||||
..addCommand(PublishPrecompiledGenerationCommand())
|
||||
..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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/// 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:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'android_environment.dart';
|
||||
import 'cargo.dart';
|
||||
import 'darwin_environment.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;
|
||||
final String? iosDeploymentTarget;
|
||||
final String? macosDeploymentTarget;
|
||||
|
||||
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,
|
||||
this.iosDeploymentTarget,
|
||||
this.macosDeploymentTarget,
|
||||
});
|
||||
|
||||
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,
|
||||
iosDeploymentTarget: isAndroid ? null : Environment.iosDeploymentTarget,
|
||||
macosDeploymentTarget:
|
||||
isAndroid ? null : Platform.environment['MACOSX_DEPLOYMENT_TARGET'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RustBuilder {
|
||||
final Target target;
|
||||
final BuildEnvironment environment;
|
||||
|
||||
RustBuilder({
|
||||
required this.target,
|
||||
required this.environment,
|
||||
this.toolchain,
|
||||
});
|
||||
|
||||
final String? toolchain;
|
||||
|
||||
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 =>
|
||||
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');
|
||||
final buildEnvironment = await _buildEnvironment();
|
||||
buildEnvironment['RUSTC'] = _toolchainRustc();
|
||||
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: buildEnvironment,
|
||||
);
|
||||
return path.join(
|
||||
environment.targetTempDir,
|
||||
target.rust,
|
||||
environment.configuration.rustName,
|
||||
);
|
||||
}
|
||||
|
||||
String _toolchainRustc() {
|
||||
final result = runCommand('rustup', [
|
||||
'which',
|
||||
'--toolchain',
|
||||
_toolchain,
|
||||
'rustc',
|
||||
]);
|
||||
return (result.stdout as String).trim();
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _buildEnvironment() async {
|
||||
if (target.android == null) {
|
||||
if (target.darwinPlatform == 'macosx') {
|
||||
final deploymentTarget = environment.macosDeploymentTarget;
|
||||
return deploymentTarget == null
|
||||
? {}
|
||||
: {'MACOSX_DEPLOYMENT_TARGET': deploymentTarget};
|
||||
}
|
||||
if (target.darwinPlatform == 'iphonesimulator' ||
|
||||
target.darwinPlatform == 'iphoneos') {
|
||||
return DarwinEnvironment(
|
||||
target: target,
|
||||
deploymentTarget: environment.iosDeploymentTarget ??
|
||||
Environment.iosDeploymentTarget,
|
||||
).buildEnvironment();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'options.dart';
|
||||
|
||||
class CompositeArtifactException implements Exception {
|
||||
CompositeArtifactException(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => 'CompositeArtifactException: $message';
|
||||
}
|
||||
|
||||
class CompositeProcessInvocation {
|
||||
const CompositeProcessInvocation({
|
||||
required this.executable,
|
||||
required this.arguments,
|
||||
required this.workingDirectory,
|
||||
required this.environment,
|
||||
required this.timeout,
|
||||
});
|
||||
|
||||
final String executable;
|
||||
final List<String> arguments;
|
||||
final String workingDirectory;
|
||||
final Map<String, String> environment;
|
||||
final Duration timeout;
|
||||
}
|
||||
|
||||
class CompositeProcessResult {
|
||||
const CompositeProcessResult({
|
||||
required this.exitCode,
|
||||
this.stdout = '',
|
||||
this.stderr = '',
|
||||
this.timedOut = false,
|
||||
});
|
||||
|
||||
final int exitCode;
|
||||
final String stdout;
|
||||
final String stderr;
|
||||
final bool timedOut;
|
||||
}
|
||||
|
||||
typedef CompositeProcessRunner = Future<CompositeProcessResult> Function(
|
||||
CompositeProcessInvocation invocation,
|
||||
);
|
||||
|
||||
class CompositeArtifactResult {
|
||||
CompositeArtifactResult(this.outputs);
|
||||
|
||||
final Map<String, String> outputs;
|
||||
}
|
||||
|
||||
class CompositeArtifactBuilder {
|
||||
CompositeArtifactBuilder({
|
||||
required String workspaceRoot,
|
||||
required this.generationHash,
|
||||
required String targetOutputRoot,
|
||||
required String outputRoot,
|
||||
CompositeHost? currentHost,
|
||||
CompositeProcessRunner? processRunner,
|
||||
}) : workspaceRoot = path.normalize(path.absolute(workspaceRoot)),
|
||||
targetOutputRoot = path.normalize(path.absolute(targetOutputRoot)),
|
||||
outputRoot = path.normalize(path.absolute(outputRoot)),
|
||||
currentHost = currentHost ?? _platformHost(),
|
||||
processRunner = processRunner ?? _runProcess;
|
||||
|
||||
final String workspaceRoot;
|
||||
final String generationHash;
|
||||
final String targetOutputRoot;
|
||||
final String outputRoot;
|
||||
final CompositeHost currentHost;
|
||||
final CompositeProcessRunner processRunner;
|
||||
|
||||
bool supports(CompositeGroup group, Set<String> targets) =>
|
||||
group.host == currentHost &&
|
||||
group.requiredTargets.every(targets.contains);
|
||||
|
||||
Future<CompositeArtifactResult> build(CompositeGroup group) async {
|
||||
if (group.host != currentHost) {
|
||||
throw CompositeArtifactException(
|
||||
'Composite group ${group.name} requires host ${group.host.name}.',
|
||||
);
|
||||
}
|
||||
_requireDirectory(workspaceRoot, 'Workspace root');
|
||||
_requireDirectory(targetOutputRoot, 'Target output root');
|
||||
for (final target in group.requiredTargets) {
|
||||
final targetDirectory = path.join(targetOutputRoot, target);
|
||||
_requireContainedDirectory(
|
||||
targetOutputRoot,
|
||||
targetDirectory,
|
||||
'Required target output $target',
|
||||
);
|
||||
}
|
||||
|
||||
final executable = path.normalize(path.joinAll([
|
||||
workspaceRoot,
|
||||
...path.posix.split(group.argv.first),
|
||||
]));
|
||||
_requireContainedFile(workspaceRoot, executable, 'Composite executable');
|
||||
|
||||
_ensureNoSymlinkComponents(outputRoot, 'Composite output root');
|
||||
Directory(outputRoot).createSync(recursive: true);
|
||||
_ensureNoSymlinkComponents(outputRoot, 'Composite output root');
|
||||
final staging = Directory(path.join(
|
||||
outputRoot,
|
||||
'.${group.name}.$pid.${DateTime.now().microsecondsSinceEpoch}',
|
||||
))
|
||||
..createSync();
|
||||
final finalDirectory = Directory(
|
||||
path.join(outputRoot, 'composites', group.name),
|
||||
);
|
||||
|
||||
try {
|
||||
final environment = <String, String>{
|
||||
...group.environment,
|
||||
'CARGOKIT_GENERATION_HASH': generationHash,
|
||||
'CARGOKIT_WORKSPACE_ROOT': workspaceRoot,
|
||||
'CARGOKIT_TARGET_OUTPUT_ROOT': targetOutputRoot,
|
||||
'CARGOKIT_OUTPUT_ROOT': staging.path,
|
||||
'CARGOKIT_DART_EXECUTABLE': Platform.resolvedExecutable,
|
||||
};
|
||||
final result = await processRunner(CompositeProcessInvocation(
|
||||
executable: executable,
|
||||
arguments: List.unmodifiable(group.argv.skip(1)),
|
||||
workingDirectory: workspaceRoot,
|
||||
environment: Map.unmodifiable(environment),
|
||||
timeout: group.timeout,
|
||||
));
|
||||
if (result.timedOut) {
|
||||
throw CompositeArtifactException(
|
||||
'Composite group ${group.name} timed out.',
|
||||
);
|
||||
}
|
||||
if (result.exitCode != 0) {
|
||||
throw CompositeArtifactException(
|
||||
'Composite group ${group.name} failed with exit code '
|
||||
'${result.exitCode}: ${result.stderr.trim()}',
|
||||
);
|
||||
}
|
||||
|
||||
for (final output in group.outputs) {
|
||||
_requireContainedFile(
|
||||
staging.path,
|
||||
path.join(staging.path, output),
|
||||
'Composite output $output',
|
||||
);
|
||||
}
|
||||
if (FileSystemEntity.typeSync(finalDirectory.path, followLinks: false) !=
|
||||
FileSystemEntityType.notFound) {
|
||||
throw CompositeArtifactException(
|
||||
'Composite output already exists for ${group.name}.',
|
||||
);
|
||||
}
|
||||
finalDirectory.parent.createSync(recursive: true);
|
||||
staging.renameSync(finalDirectory.path);
|
||||
return CompositeArtifactResult(Map.unmodifiable({
|
||||
for (final output in group.outputs)
|
||||
output: path.join(finalDirectory.path, output),
|
||||
}));
|
||||
} on CompositeArtifactException {
|
||||
rethrow;
|
||||
} on Object catch (error) {
|
||||
throw CompositeArtifactException(
|
||||
'Composite group ${group.name} could not be built: $error',
|
||||
);
|
||||
} finally {
|
||||
if (staging.existsSync()) staging.deleteSync(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<CompositeProcessResult> _runProcess(
|
||||
CompositeProcessInvocation invocation,
|
||||
) async {
|
||||
final process = await Process.start(
|
||||
invocation.executable,
|
||||
invocation.arguments,
|
||||
workingDirectory: invocation.workingDirectory,
|
||||
environment: invocation.environment,
|
||||
includeParentEnvironment: false,
|
||||
runInShell: false,
|
||||
);
|
||||
final stdout = process.stdout.transform(utf8.decoder).join();
|
||||
final stderr = process.stderr.transform(utf8.decoder).join();
|
||||
var timedOut = false;
|
||||
int exitCode;
|
||||
try {
|
||||
exitCode = await process.exitCode.timeout(invocation.timeout);
|
||||
} on TimeoutException {
|
||||
timedOut = true;
|
||||
process.kill();
|
||||
exitCode = await process.exitCode;
|
||||
}
|
||||
return CompositeProcessResult(
|
||||
exitCode: exitCode,
|
||||
stdout: await stdout,
|
||||
stderr: await stderr,
|
||||
timedOut: timedOut,
|
||||
);
|
||||
}
|
||||
|
||||
static CompositeHost _platformHost() {
|
||||
if (Platform.isMacOS) return CompositeHost.macos;
|
||||
if (Platform.isWindows) return CompositeHost.windows;
|
||||
return CompositeHost.linux;
|
||||
}
|
||||
}
|
||||
|
||||
void _requireDirectory(String value, String description) {
|
||||
_ensureNoSymlinkComponents(value, description);
|
||||
if (FileSystemEntity.typeSync(value, followLinks: false) !=
|
||||
FileSystemEntityType.directory) {
|
||||
throw CompositeArtifactException('$description is not a directory: $value');
|
||||
}
|
||||
}
|
||||
|
||||
void _requireContainedDirectory(
|
||||
String root,
|
||||
String value,
|
||||
String description,
|
||||
) {
|
||||
_requireContained(root, value, description);
|
||||
_requireDirectory(value, description);
|
||||
}
|
||||
|
||||
void _requireContainedFile(String root, String value, String description) {
|
||||
_requireContained(root, value, description);
|
||||
_ensureNoSymlinkComponents(value, description);
|
||||
if (FileSystemEntity.typeSync(value, followLinks: false) !=
|
||||
FileSystemEntityType.file) {
|
||||
throw CompositeArtifactException('$description is not a file: $value');
|
||||
}
|
||||
}
|
||||
|
||||
void _requireContained(String root, String value, String description) {
|
||||
final normalizedRoot = path.normalize(path.absolute(root));
|
||||
final normalizedValue = path.normalize(path.absolute(value));
|
||||
if (!path.isWithin(normalizedRoot, normalizedValue)) {
|
||||
throw CompositeArtifactException('$description escapes $normalizedRoot.');
|
||||
}
|
||||
}
|
||||
|
||||
void _ensureNoSymlinkComponents(String value, String description) {
|
||||
final absolute = path.normalize(path.absolute(value));
|
||||
var current = path.rootPrefix(absolute);
|
||||
final relative = path.relative(absolute, from: current);
|
||||
if (relative == '.') return;
|
||||
for (final component in path.split(relative)) {
|
||||
current = path.join(current, component);
|
||||
if (FileSystemEntity.typeSync(current, followLinks: false) ==
|
||||
FileSystemEntityType.link) {
|
||||
throw CompositeArtifactException(
|
||||
'$description must not contain symbolic links: $current',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
/// 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:convert/convert.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'options.dart';
|
||||
|
||||
enum _HashEntryType {
|
||||
file,
|
||||
directory,
|
||||
}
|
||||
|
||||
class _HashEntry {
|
||||
const _HashEntry({
|
||||
required this.relativePath,
|
||||
required this.type,
|
||||
this.file,
|
||||
});
|
||||
|
||||
final String relativePath;
|
||||
final _HashEntryType type;
|
||||
final File? file;
|
||||
}
|
||||
|
||||
class CrateHash {
|
||||
static const _textExtensions = {
|
||||
'.rs',
|
||||
'.toml',
|
||||
'.yaml',
|
||||
'.yml',
|
||||
'.dart',
|
||||
'.sh',
|
||||
'.swift',
|
||||
'.json',
|
||||
'.md',
|
||||
'.lock',
|
||||
};
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// [tempStorage] is retained for call compatibility but is no longer used;
|
||||
/// file content remains authoritative on every call.
|
||||
static String compute(String manifestDir, {String? tempStorage}) {
|
||||
return CrateHash._(
|
||||
manifestDir: manifestDir,
|
||||
)._compute();
|
||||
}
|
||||
|
||||
CrateHash._({
|
||||
required this.manifestDir,
|
||||
});
|
||||
|
||||
String _compute() {
|
||||
return _computeHash(_getInputs());
|
||||
}
|
||||
|
||||
String _computeHash(List<_HashEntry> inputs) {
|
||||
final output = AccumulatorSink<Digest>();
|
||||
final input = sha256.startChunkedConversion(output);
|
||||
final data = ByteData(8);
|
||||
|
||||
for (final hashInput in inputs) {
|
||||
_addFramedBytes(input, [hashInput.type.index], data);
|
||||
_addFramedBytes(input, utf8.encode(hashInput.relativePath), data);
|
||||
_addFramedBytes(
|
||||
input,
|
||||
hashInput.file == null ? const [] : _canonicalContent(hashInput.file!),
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
input.close();
|
||||
final res = output.events.single;
|
||||
|
||||
return res.toString();
|
||||
}
|
||||
|
||||
static List<int> _canonicalContent(File file) {
|
||||
final bytes = file.readAsBytesSync();
|
||||
final extension = path.extension(file.path).toLowerCase();
|
||||
if (!_textExtensions.contains(extension)) {
|
||||
return bytes;
|
||||
}
|
||||
final text = utf8.decode(bytes);
|
||||
return utf8.encode(text.replaceAll('\r\n', '\n'));
|
||||
}
|
||||
|
||||
static void _addUint64(
|
||||
ByteConversionSink input,
|
||||
ByteData data,
|
||||
int value,
|
||||
) {
|
||||
data.setUint64(0, value);
|
||||
input.add(data.buffer.asUint8List());
|
||||
}
|
||||
|
||||
static void _addFramedBytes(
|
||||
ByteConversionSink input,
|
||||
List<int> bytes,
|
||||
ByteData data,
|
||||
) {
|
||||
_addUint64(input, data, bytes.length);
|
||||
input.add(bytes);
|
||||
}
|
||||
|
||||
List<_HashEntry> _getInputs() {
|
||||
final manifestPath = path.normalize(path.absolute(manifestDir));
|
||||
_rejectAbsoluteSymlinkComponents(manifestPath, 'Crate manifest path');
|
||||
final precompiled = CargokitCrateOptions.load(manifestDir: manifestPath)
|
||||
.precompiledBinaries;
|
||||
final configuredWorkspaceRoot = path.normalize(path.absolute(path.join(
|
||||
manifestPath,
|
||||
precompiled?.workspaceRoot ?? '.',
|
||||
)));
|
||||
_rejectAbsoluteSymlinkComponents(
|
||||
configuredWorkspaceRoot,
|
||||
'Workspace root',
|
||||
);
|
||||
|
||||
final workspaceRoot =
|
||||
Directory(configuredWorkspaceRoot).resolveSymbolicLinksSync();
|
||||
final resolvedManifest = Directory(manifestPath).resolveSymbolicLinksSync();
|
||||
if (workspaceRoot != resolvedManifest &&
|
||||
!path.isWithin(workspaceRoot, resolvedManifest)) {
|
||||
throw FileSystemException(
|
||||
'Workspace root must contain the crate manifest.',
|
||||
configuredWorkspaceRoot,
|
||||
);
|
||||
}
|
||||
|
||||
final inputs = <String, _HashEntry>{};
|
||||
|
||||
void addFile(File file) {
|
||||
final absolute = path.normalize(path.absolute(file.path));
|
||||
if (absolute != workspaceRoot &&
|
||||
!path.isWithin(workspaceRoot, absolute)) {
|
||||
throw FileSystemException(
|
||||
'Hash input escapes the workspace root.',
|
||||
absolute,
|
||||
);
|
||||
}
|
||||
final relative = _normalizedRelativePath(absolute, workspaceRoot);
|
||||
inputs[relative] = _HashEntry(
|
||||
relativePath: relative,
|
||||
type: _HashEntryType.file,
|
||||
file: file,
|
||||
);
|
||||
}
|
||||
|
||||
void addDirectory(Directory directory) {
|
||||
final absolute = path.normalize(path.absolute(directory.path));
|
||||
if (absolute != workspaceRoot &&
|
||||
!path.isWithin(workspaceRoot, absolute)) {
|
||||
throw FileSystemException(
|
||||
'Hash input escapes the workspace root.',
|
||||
absolute,
|
||||
);
|
||||
}
|
||||
final relative = _normalizedRelativePath(absolute, workspaceRoot);
|
||||
inputs[relative] = _HashEntry(
|
||||
relativePath: relative,
|
||||
type: _HashEntryType.directory,
|
||||
);
|
||||
}
|
||||
|
||||
void addEntity(FileSystemEntity entity) {
|
||||
final type = FileSystemEntity.typeSync(entity.path, followLinks: false);
|
||||
if (type == FileSystemEntityType.link) {
|
||||
throw FileSystemException(
|
||||
'Symbolic links are not allowed in generation hash inputs.',
|
||||
entity.path,
|
||||
);
|
||||
}
|
||||
if (type == FileSystemEntityType.file) {
|
||||
addFile(File(entity.path));
|
||||
return;
|
||||
}
|
||||
if (type == FileSystemEntityType.directory) {
|
||||
addDirectory(Directory(entity.path));
|
||||
final children = Directory(entity.path).listSync(followLinks: false)
|
||||
..sort((left, right) => left.path.compareTo(right.path));
|
||||
for (final child in children) {
|
||||
addEntity(child);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(
|
||||
'Generation hash input does not exist.', entity.path);
|
||||
}
|
||||
throw FileSystemException(
|
||||
'Unsupported generation hash input type.',
|
||||
entity.path,
|
||||
);
|
||||
}
|
||||
|
||||
final src = Directory(path.join(resolvedManifest, 'src'));
|
||||
addEntity(src);
|
||||
|
||||
void addOptionalManifestFile(String relative) {
|
||||
final filePath = path.join(resolvedManifest, relative);
|
||||
final type = FileSystemEntity.typeSync(filePath, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
return;
|
||||
}
|
||||
addEntity(File(filePath));
|
||||
}
|
||||
|
||||
addOptionalManifestFile('Cargo.toml');
|
||||
addOptionalManifestFile('Cargo.lock');
|
||||
addOptionalManifestFile('build.rs');
|
||||
addOptionalManifestFile('cargokit.yaml');
|
||||
|
||||
for (final declaredInput in precompiled?.hashInputs ?? const <String>[]) {
|
||||
final absolute = path.normalize(path.joinAll([
|
||||
workspaceRoot,
|
||||
...path.posix.split(declaredInput),
|
||||
]));
|
||||
if (absolute != workspaceRoot &&
|
||||
!path.isWithin(workspaceRoot, absolute)) {
|
||||
throw FileSystemException(
|
||||
'Declared hash input escapes the workspace root.',
|
||||
declaredInput,
|
||||
);
|
||||
}
|
||||
_rejectSymlinkComponents(workspaceRoot, absolute);
|
||||
addEntity(FileSystemEntity.isDirectorySync(absolute)
|
||||
? Directory(absolute)
|
||||
: File(absolute));
|
||||
}
|
||||
|
||||
final result = inputs.values.toList()
|
||||
..sort((left, right) => left.relativePath.compareTo(right.relativePath));
|
||||
return result;
|
||||
}
|
||||
|
||||
static String _normalizedRelativePath(String absolute, String root) {
|
||||
final relative = path.relative(absolute, from: root);
|
||||
return path.posix.joinAll(path.split(relative));
|
||||
}
|
||||
|
||||
static void _rejectSymlinkComponents(String root, String target) {
|
||||
var current = root;
|
||||
for (final component in path.split(path.relative(target, from: root))) {
|
||||
current = path.join(current, component);
|
||||
_rejectLink(current, 'Generation hash input');
|
||||
}
|
||||
}
|
||||
|
||||
static void _rejectAbsoluteSymlinkComponents(
|
||||
String target,
|
||||
String description,
|
||||
) {
|
||||
final root = path.rootPrefix(target);
|
||||
var current = root;
|
||||
final relative = path.relative(target, from: root);
|
||||
if (relative == '.') {
|
||||
_rejectLink(current, description);
|
||||
return;
|
||||
}
|
||||
for (final component in path.split(relative)) {
|
||||
current = path.join(current, component);
|
||||
_rejectLink(current, description);
|
||||
}
|
||||
}
|
||||
|
||||
static void _rejectLink(String entityPath, String description) {
|
||||
if (FileSystemEntity.typeSync(entityPath, followLinks: false) ==
|
||||
FileSystemEntityType.link) {
|
||||
throw FileSystemException(
|
||||
'$description must not contain symbolic links.',
|
||||
entityPath,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<File> getFiles() {
|
||||
return _getInputs()
|
||||
.map((input) => input.file)
|
||||
.whereType<File>()
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
final String manifestDir;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/// 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 'target.dart';
|
||||
import 'util.dart';
|
||||
|
||||
class DarwinEnvironment {
|
||||
DarwinEnvironment({
|
||||
required this.target,
|
||||
required this.deploymentTarget,
|
||||
});
|
||||
|
||||
final Target target;
|
||||
final String deploymentTarget;
|
||||
|
||||
Map<String, String> buildEnvironment() {
|
||||
final platform = target.darwinPlatform;
|
||||
if (platform == null || platform == 'macosx') {
|
||||
return {};
|
||||
}
|
||||
|
||||
final sdkPath = _resolveSdkPath(platform);
|
||||
final flags = _clangFlags(sdkPath);
|
||||
final rustTarget = target.rust;
|
||||
|
||||
return {
|
||||
'SDKROOT': sdkPath,
|
||||
'IPHONEOS_DEPLOYMENT_TARGET': deploymentTarget,
|
||||
'CC_$rustTarget': _findTool(platform, 'clang'),
|
||||
'CFLAGS_$rustTarget': flags,
|
||||
'CXX_$rustTarget': _findTool(platform, 'clang++'),
|
||||
'CXXFLAGS_$rustTarget': flags,
|
||||
'AR_$rustTarget': _findTool(platform, 'ar'),
|
||||
'RANLIB_$rustTarget': _findTool(platform, 'ranlib'),
|
||||
'BINDGEN_EXTRA_CLANG_ARGS_$rustTarget': flags,
|
||||
};
|
||||
}
|
||||
|
||||
String _resolveSdkPath(String platform) {
|
||||
final result = runCommand('xcrun', [
|
||||
'--sdk',
|
||||
platform,
|
||||
'--show-sdk-path',
|
||||
]);
|
||||
return (result.stdout as String).trim();
|
||||
}
|
||||
|
||||
String _findTool(String platform, String tool) {
|
||||
final result = runCommand('xcrun', [
|
||||
'--sdk',
|
||||
platform,
|
||||
'--find',
|
||||
tool,
|
||||
]);
|
||||
return (result.stdout as String).trim();
|
||||
}
|
||||
|
||||
String _clangFlags(String sdkPath) {
|
||||
final flags = <String>[];
|
||||
final clangTarget = _clangTarget();
|
||||
if (clangTarget != null) {
|
||||
flags.add('--target=$clangTarget');
|
||||
}
|
||||
flags.add('-isysroot $sdkPath');
|
||||
flags.add(_deploymentFlag());
|
||||
return flags.join(' ');
|
||||
}
|
||||
|
||||
String _deploymentFlag() {
|
||||
return switch (target.darwinPlatform) {
|
||||
'iphonesimulator' => '-mios-simulator-version-min=$deploymentTarget',
|
||||
'iphoneos' => '-miphoneos-version-min=$deploymentTarget',
|
||||
_ => throw ArgumentError(
|
||||
'Unsupported Darwin platform: ${target.darwinPlatform}'),
|
||||
};
|
||||
}
|
||||
|
||||
String? _clangTarget() {
|
||||
return switch (target.rust) {
|
||||
'aarch64-apple-ios-sim' => 'arm64-apple-ios-simulator',
|
||||
'x86_64-apple-ios' => 'x86_64-apple-ios-simulator',
|
||||
'aarch64-apple-ios' => 'arm64-apple-ios',
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/// 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(' ');
|
||||
|
||||
static String get iosDeploymentTarget =>
|
||||
Platform.environment['IPHONEOS_DEPLOYMENT_TARGET'] ?? '12.0';
|
||||
|
||||
// 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];
|
||||
print("Environment variable $key: $res");
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,760 @@
|
||||
/// 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';
|
||||
import 'target.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);
|
||||
}
|
||||
}
|
||||
|
||||
void _rejectUnknownFields(
|
||||
YamlMap map,
|
||||
Set<String> allowedFields,
|
||||
String context,
|
||||
) {
|
||||
for (final key in map.nodes.keys) {
|
||||
if (key is! YamlScalar ||
|
||||
key.value is! String ||
|
||||
!allowedFields.contains(key.value)) {
|
||||
throw SourceSpanException(
|
||||
'Unknown $context field "${key.value}".', key.span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
YamlMap _mapFromNode(YamlNode node, String context) {
|
||||
if (node is YamlMap) {
|
||||
return node;
|
||||
}
|
||||
throw SourceSpanException('$context must be a map.', node.span);
|
||||
}
|
||||
|
||||
YamlNode _requiredNode(YamlMap map, String key, String context) {
|
||||
final node = map.nodes[YamlScalar.wrap(key)];
|
||||
if (node != null) {
|
||||
return node;
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'$context must contain "$key".',
|
||||
map.span,
|
||||
);
|
||||
}
|
||||
|
||||
String _stringFromNode(YamlNode node, String description,
|
||||
{bool allowEmpty = false}) {
|
||||
if (node case YamlScalar(value: String value)) {
|
||||
if (allowEmpty || value.isNotEmpty) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'$description must be a non-empty string.', node.span);
|
||||
}
|
||||
|
||||
List<String> _stringListFromNode(
|
||||
YamlNode node,
|
||||
String description, {
|
||||
bool allowEmpty = false,
|
||||
}) {
|
||||
if (node is YamlList) {
|
||||
final result = <String>[];
|
||||
for (final item in node.nodes) {
|
||||
result.add(_stringFromNode(item, '$description entry'));
|
||||
}
|
||||
if (allowEmpty || result.isNotEmpty) {
|
||||
return List.unmodifiable(result);
|
||||
}
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'$description must be a non-empty list of strings.',
|
||||
node.span,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, String> _stringMapFromNode(
|
||||
YamlNode node,
|
||||
String description, {
|
||||
bool allowEmpty = false,
|
||||
bool allowEmptyValues = false,
|
||||
}) {
|
||||
if (node is YamlMap) {
|
||||
final result = <String, String>{};
|
||||
for (final MapEntry(:key, :value) in node.nodes.entries) {
|
||||
final name = _stringFromNode(key, '$description key');
|
||||
result[name] = _stringFromNode(
|
||||
value,
|
||||
'$description value',
|
||||
allowEmpty: allowEmptyValues,
|
||||
);
|
||||
}
|
||||
if (allowEmpty || result.isNotEmpty) {
|
||||
return Map.unmodifiable(result);
|
||||
}
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'$description must be a map of strings to strings.',
|
||||
node.span,
|
||||
);
|
||||
}
|
||||
|
||||
String _relativePathFromNode(
|
||||
YamlNode node,
|
||||
String description, {
|
||||
bool allowParent = false,
|
||||
}) {
|
||||
final value = _stringFromNode(node, description);
|
||||
if (value.contains('\\') ||
|
||||
path.posix.isAbsolute(value) ||
|
||||
path.windows.isAbsolute(value)) {
|
||||
throw SourceSpanException(
|
||||
'$description must be a relative path.', node.span);
|
||||
}
|
||||
final normalized = path.posix.normalize(value);
|
||||
if (!allowParent && (normalized == '..' || normalized.startsWith('../'))) {
|
||||
throw SourceSpanException(
|
||||
'$description must not escape the workspace root.',
|
||||
node.span,
|
||||
);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
List<String> _rustTargetsFromNode(YamlNode node, String description) {
|
||||
final targets = _stringListFromNode(node, description);
|
||||
final seen = <String>{};
|
||||
for (final target in targets) {
|
||||
if (Target.forRustTriple(target) == null) {
|
||||
throw SourceSpanException('Invalid Rust target "$target".', node.span);
|
||||
}
|
||||
if (!seen.add(target)) {
|
||||
throw SourceSpanException('Duplicate Rust target "$target".', node.span);
|
||||
}
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
enum CompositeHost {
|
||||
linux,
|
||||
macos,
|
||||
windows,
|
||||
}
|
||||
|
||||
class PrecompiledBuildRecipe {
|
||||
PrecompiledBuildRecipe({
|
||||
required this.rustToolchain,
|
||||
required this.flutterVersion,
|
||||
required this.xcodeVersion,
|
||||
required this.sdkVersions,
|
||||
required this.deploymentTargets,
|
||||
required this.rustTargets,
|
||||
});
|
||||
|
||||
static PrecompiledBuildRecipe parse(YamlNode node) {
|
||||
final map = _mapFromNode(node, 'Build recipe');
|
||||
_rejectUnknownFields(
|
||||
map,
|
||||
const {
|
||||
'rust_toolchain',
|
||||
'flutter_version',
|
||||
'xcode_version',
|
||||
'sdk_versions',
|
||||
'deployment_targets',
|
||||
'rust_targets',
|
||||
},
|
||||
'build recipe');
|
||||
return PrecompiledBuildRecipe(
|
||||
rustToolchain: _stringFromNode(
|
||||
_requiredNode(map, 'rust_toolchain', 'Build recipe'),
|
||||
'Rust toolchain',
|
||||
),
|
||||
flutterVersion: _stringFromNode(
|
||||
_requiredNode(map, 'flutter_version', 'Build recipe'),
|
||||
'Flutter version',
|
||||
),
|
||||
xcodeVersion: _stringFromNode(
|
||||
_requiredNode(map, 'xcode_version', 'Build recipe'),
|
||||
'Xcode version',
|
||||
),
|
||||
sdkVersions: _stringMapFromNode(
|
||||
_requiredNode(map, 'sdk_versions', 'Build recipe'),
|
||||
'SDK versions',
|
||||
),
|
||||
deploymentTargets: _stringMapFromNode(
|
||||
_requiredNode(map, 'deployment_targets', 'Build recipe'),
|
||||
'Deployment targets',
|
||||
),
|
||||
rustTargets: _rustTargetsFromNode(
|
||||
_requiredNode(map, 'rust_targets', 'Build recipe'),
|
||||
'Build recipe Rust targets',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final String rustToolchain;
|
||||
final String flutterVersion;
|
||||
final String xcodeVersion;
|
||||
final Map<String, String> sdkVersions;
|
||||
final Map<String, String> deploymentTargets;
|
||||
final List<String> rustTargets;
|
||||
}
|
||||
|
||||
class CompositeGroup {
|
||||
CompositeGroup({
|
||||
required this.name,
|
||||
required this.host,
|
||||
required this.requiredTargets,
|
||||
required this.argv,
|
||||
required this.environment,
|
||||
required this.timeout,
|
||||
required this.outputs,
|
||||
});
|
||||
|
||||
static CompositeGroup parse(YamlNode node) {
|
||||
final map = _mapFromNode(node, 'Composite group');
|
||||
_rejectUnknownFields(
|
||||
map,
|
||||
const {
|
||||
'name',
|
||||
'host',
|
||||
'required_targets',
|
||||
'argv',
|
||||
'environment',
|
||||
'timeout_seconds',
|
||||
'outputs',
|
||||
},
|
||||
'composite group');
|
||||
|
||||
final nameNode = _requiredNode(map, 'name', 'Composite group');
|
||||
final name = _safeNameFromNode(nameNode, 'Composite group name');
|
||||
final hostNode = _requiredNode(map, 'host', 'Composite group');
|
||||
final hostName = _stringFromNode(hostNode, 'Composite host');
|
||||
final host = CompositeHost.values
|
||||
.firstWhereOrNull((element) => element.name == hostName);
|
||||
if (host == null) {
|
||||
throw SourceSpanException(
|
||||
'Invalid composite host "$hostName". Must be one of '
|
||||
'${CompositeHost.values.map((host) => host.name)}.',
|
||||
hostNode.span,
|
||||
);
|
||||
}
|
||||
|
||||
final argvNode = _requiredNode(map, 'argv', 'Composite group');
|
||||
final argv = _stringListFromNode(argvNode, 'Composite argv');
|
||||
final argvList = argvNode as YamlList;
|
||||
final command = _relativePathFromNode(argvList.nodes.first, 'Command path');
|
||||
final normalizedArgv =
|
||||
List<String>.unmodifiable([command, ...argv.skip(1)]);
|
||||
|
||||
final timeoutNode =
|
||||
_requiredNode(map, 'timeout_seconds', 'Composite group');
|
||||
final timeoutSeconds = switch (timeoutNode) {
|
||||
YamlScalar(value: int value) when value > 0 => value,
|
||||
_ => throw SourceSpanException(
|
||||
'Composite timeout must be a positive integer.', timeoutNode.span),
|
||||
};
|
||||
|
||||
final outputsNode = _requiredNode(map, 'outputs', 'Composite group');
|
||||
final outputNodes = switch (outputsNode) {
|
||||
YamlList(nodes: final nodes) when nodes.isNotEmpty => nodes,
|
||||
_ => throw SourceSpanException(
|
||||
'Composite outputs must be a non-empty list.', outputsNode.span),
|
||||
};
|
||||
final outputs = <String>[];
|
||||
final seenOutputs = <String>{};
|
||||
for (final outputNode in outputNodes) {
|
||||
final output = _safeNameFromNode(outputNode, 'Composite output name');
|
||||
if (!seenOutputs.add(output)) {
|
||||
throw SourceSpanException(
|
||||
'Duplicate composite output "$output".',
|
||||
outputNode.span,
|
||||
);
|
||||
}
|
||||
outputs.add(output);
|
||||
}
|
||||
|
||||
final environmentNode = map.nodes[YamlScalar.wrap('environment')];
|
||||
final environment = environmentNode == null
|
||||
? const <String, String>{}
|
||||
: _stringMapFromNode(
|
||||
environmentNode,
|
||||
'Composite environment',
|
||||
allowEmpty: true,
|
||||
allowEmptyValues: true,
|
||||
);
|
||||
for (final key in environment.keys) {
|
||||
if (!RegExp(r'^[A-Za-z_][A-Za-z0-9_]*$').hasMatch(key)) {
|
||||
throw SourceSpanException(
|
||||
'Invalid composite environment variable "$key".',
|
||||
environmentNode?.span,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return CompositeGroup(
|
||||
name: name,
|
||||
host: host,
|
||||
requiredTargets: _rustTargetsFromNode(
|
||||
_requiredNode(map, 'required_targets', 'Composite group'),
|
||||
'Composite required targets',
|
||||
),
|
||||
argv: normalizedArgv,
|
||||
environment: environment,
|
||||
timeout: Duration(seconds: timeoutSeconds),
|
||||
outputs: List.unmodifiable(outputs),
|
||||
);
|
||||
}
|
||||
|
||||
static String _safeNameFromNode(YamlNode node, String description) {
|
||||
final value = _stringFromNode(node, description);
|
||||
if (!RegExp(r'^[A-Za-z0-9][A-Za-z0-9._-]*$').hasMatch(value) ||
|
||||
value == '.' ||
|
||||
value == '..') {
|
||||
throw SourceSpanException('$description is unsafe.', node.span);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
final String name;
|
||||
final CompositeHost host;
|
||||
final List<String> requiredTargets;
|
||||
final List<String> argv;
|
||||
final Map<String, String> environment;
|
||||
final Duration timeout;
|
||||
final List<String> outputs;
|
||||
}
|
||||
|
||||
class PrecompiledBinaries {
|
||||
final String uriPrefix;
|
||||
final PublicKey publicKey;
|
||||
final String workspaceRoot;
|
||||
final List<String> hashInputs;
|
||||
final PrecompiledBuildRecipe? buildRecipe;
|
||||
final List<CompositeGroup> compositeGroups;
|
||||
|
||||
PrecompiledBinaries({
|
||||
required this.uriPrefix,
|
||||
required this.publicKey,
|
||||
this.workspaceRoot = '.',
|
||||
this.hashInputs = const [],
|
||||
this.buildRecipe,
|
||||
this.compositeGroups = const [],
|
||||
});
|
||||
|
||||
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) {
|
||||
final map = _mapFromNode(node, 'Precompiled binaries');
|
||||
_rejectUnknownFields(
|
||||
map,
|
||||
const {
|
||||
'url_prefix',
|
||||
'public_key',
|
||||
'workspace_root',
|
||||
'hash_inputs',
|
||||
'build_recipe',
|
||||
'composite_groups',
|
||||
},
|
||||
'precompiled binaries');
|
||||
final urlPrefixNode =
|
||||
_requiredNode(map, 'url_prefix', 'Precompiled binaries');
|
||||
final publicKeyNode =
|
||||
_requiredNode(map, 'public_key', 'Precompiled binaries');
|
||||
final workspaceRootNode = map.nodes[YamlScalar.wrap('workspace_root')];
|
||||
final hashInputsNode = map.nodes[YamlScalar.wrap('hash_inputs')];
|
||||
final buildRecipeNode = map.nodes[YamlScalar.wrap('build_recipe')];
|
||||
final compositeGroupsNode = map.nodes[YamlScalar.wrap('composite_groups')];
|
||||
|
||||
final hashInputs = <String>[];
|
||||
final seenHashInputs = <String>{};
|
||||
if (hashInputsNode != null) {
|
||||
if (hashInputsNode is! YamlList) {
|
||||
throw SourceSpanException(
|
||||
'Hash inputs must be a list of relative paths.',
|
||||
hashInputsNode.span,
|
||||
);
|
||||
}
|
||||
for (final inputNode in hashInputsNode.nodes) {
|
||||
final input = _relativePathFromNode(inputNode, 'Hash input');
|
||||
if (!seenHashInputs.add(input)) {
|
||||
throw SourceSpanException(
|
||||
'Duplicate hash input "$input".', inputNode.span);
|
||||
}
|
||||
hashInputs.add(input);
|
||||
}
|
||||
}
|
||||
|
||||
final compositeGroups = <CompositeGroup>[];
|
||||
final groupNames = <String>{};
|
||||
final outputNames = <String>{};
|
||||
if (compositeGroupsNode != null) {
|
||||
if (compositeGroupsNode is! YamlList) {
|
||||
throw SourceSpanException(
|
||||
'Composite groups must be a list.',
|
||||
compositeGroupsNode.span,
|
||||
);
|
||||
}
|
||||
for (final groupNode in compositeGroupsNode.nodes) {
|
||||
final group = CompositeGroup.parse(groupNode);
|
||||
if (!groupNames.add(group.name)) {
|
||||
throw SourceSpanException(
|
||||
'Duplicate composite group "${group.name}".',
|
||||
groupNode.span,
|
||||
);
|
||||
}
|
||||
for (final output in group.outputs) {
|
||||
if (!outputNames.add(output)) {
|
||||
throw SourceSpanException(
|
||||
'Duplicate composite output "$output".',
|
||||
groupNode.span,
|
||||
);
|
||||
}
|
||||
}
|
||||
compositeGroups.add(group);
|
||||
}
|
||||
}
|
||||
|
||||
final publicKeyValue = _stringFromNode(publicKeyNode, 'Public key');
|
||||
return PrecompiledBinaries(
|
||||
uriPrefix: _stringFromNode(urlPrefixNode, 'URL prefix'),
|
||||
publicKey: _publicKeyFromHex(publicKeyValue, publicKeyNode.span),
|
||||
workspaceRoot: workspaceRootNode == null
|
||||
? '.'
|
||||
: _relativePathFromNode(
|
||||
workspaceRootNode,
|
||||
'Workspace root',
|
||||
allowParent: true,
|
||||
),
|
||||
hashInputs: List.unmodifiable(hashInputs),
|
||||
buildRecipe: buildRecipeNode == null
|
||||
? null
|
||||
: PrecompiledBuildRecipe.parse(buildRecipeNode),
|
||||
compositeGroups: List.unmodifiable(compositeGroups),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PrecompiledBinariesMode {
|
||||
auto,
|
||||
disabled,
|
||||
required,
|
||||
}
|
||||
|
||||
class CargokitUserOptions {
|
||||
// When Rustup is installed always build locally unless user opts into
|
||||
// using precompiled binaries.
|
||||
static bool defaultUsePrecompiledBinaries() {
|
||||
return Rustup.executablePath() == null;
|
||||
}
|
||||
|
||||
factory CargokitUserOptions({
|
||||
bool? usePrecompiledBinaries,
|
||||
PrecompiledBinariesMode? precompiledBinariesMode,
|
||||
required bool verboseLogging,
|
||||
}) {
|
||||
if (usePrecompiledBinaries != null && precompiledBinariesMode != null) {
|
||||
throw ArgumentError(
|
||||
'usePrecompiledBinaries and precompiledBinariesMode are mutually exclusive.',
|
||||
);
|
||||
}
|
||||
return CargokitUserOptions._values(
|
||||
precompiledBinariesMode: precompiledBinariesMode ??
|
||||
(usePrecompiledBinaries == null
|
||||
? _defaultMode()
|
||||
: _legacyMode(usePrecompiledBinaries)),
|
||||
verboseLogging: verboseLogging,
|
||||
);
|
||||
}
|
||||
|
||||
CargokitUserOptions._defaults()
|
||||
: precompiledBinariesMode = _defaultMode(),
|
||||
verboseLogging = false;
|
||||
|
||||
CargokitUserOptions._values({
|
||||
required this.precompiledBinariesMode,
|
||||
required this.verboseLogging,
|
||||
});
|
||||
|
||||
static PrecompiledBinariesMode _defaultMode() =>
|
||||
defaultUsePrecompiledBinaries()
|
||||
? PrecompiledBinariesMode.auto
|
||||
: PrecompiledBinariesMode.disabled;
|
||||
|
||||
static PrecompiledBinariesMode _legacyMode(bool value) =>
|
||||
value ? PrecompiledBinariesMode.auto : PrecompiledBinariesMode.disabled;
|
||||
|
||||
static CargokitUserOptions parse(YamlNode node) {
|
||||
if (node is! YamlMap) {
|
||||
throw SourceSpanException('Cargokit options must be a map', node.span);
|
||||
}
|
||||
var precompiledBinariesMode = _defaultMode();
|
||||
bool verboseLogging = false;
|
||||
SourceSpan? legacySettingSpan;
|
||||
SourceSpan? modeSettingSpan;
|
||||
|
||||
for (final entry in node.nodes.entries) {
|
||||
if (entry.key case YamlScalar(value: 'use_precompiled_binaries')) {
|
||||
if (entry.value case YamlScalar(value: bool value)) {
|
||||
precompiledBinariesMode = _legacyMode(value);
|
||||
legacySettingSpan = entry.key.span;
|
||||
continue;
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'Invalid value for "use_precompiled_binaries". Must be a boolean.',
|
||||
entry.value.span);
|
||||
} else if (entry.key
|
||||
case YamlScalar(value: 'precompiled_binaries_mode')) {
|
||||
final modeName = switch (entry.value) {
|
||||
YamlScalar(value: String value) => value,
|
||||
_ => throw SourceSpanException(
|
||||
'Invalid value for "precompiled_binaries_mode". Must be one of '
|
||||
'${PrecompiledBinariesMode.values.map((mode) => mode.name)}.',
|
||||
entry.value.span,
|
||||
),
|
||||
};
|
||||
final mode = PrecompiledBinariesMode.values
|
||||
.firstWhereOrNull((element) => element.name == modeName);
|
||||
if (mode == null) {
|
||||
throw SourceSpanException(
|
||||
'Invalid value for "precompiled_binaries_mode". Must be one of '
|
||||
'${PrecompiledBinariesMode.values.map((mode) => mode.name)}.',
|
||||
entry.value.span,
|
||||
);
|
||||
}
|
||||
precompiledBinariesMode = mode;
|
||||
modeSettingSpan = entry.key.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 "precompiled_binaries_mode", '
|
||||
'"use_precompiled_binaries", or "verbose_logging".',
|
||||
entry.key.span);
|
||||
}
|
||||
}
|
||||
if (legacySettingSpan != null && modeSettingSpan != null) {
|
||||
throw SourceSpanException(
|
||||
'"precompiled_binaries_mode" and legacy '
|
||||
'"use_precompiled_binaries" cannot both be set.',
|
||||
modeSettingSpan,
|
||||
);
|
||||
}
|
||||
return CargokitUserOptions(
|
||||
precompiledBinariesMode: precompiledBinariesMode,
|
||||
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._defaults();
|
||||
}
|
||||
|
||||
final PrecompiledBinariesMode precompiledBinariesMode;
|
||||
|
||||
bool get usePrecompiledBinaries =>
|
||||
precompiledBinariesMode != PrecompiledBinariesMode.disabled;
|
||||
|
||||
bool get allowLocalBuild =>
|
||||
precompiledBinariesMode != PrecompiledBinariesMode.required;
|
||||
|
||||
final bool verboseLogging;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,785 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'options.dart';
|
||||
|
||||
/// The manifest authenticates release bytes. Apple code signing remains a
|
||||
/// separate platform concern and is intentionally outside this boundary.
|
||||
const precompiledGenerationManifestFileName = 'completion.json';
|
||||
const precompiledGenerationManifestSignatureFileName = 'completion.json.sig';
|
||||
|
||||
const _manifestSchemaVersion = 2;
|
||||
const _manifestScope = 'cargokit-precompiled-generation';
|
||||
const precompiledAssetSignatureScheme = 'ed25519-cargokit-v2';
|
||||
const _assetSignatureDomain = 'cargokit-precompiled-asset-signature';
|
||||
final _sha256Pattern = RegExp(r'^[0-9a-f]{64}$');
|
||||
final _commitPattern = RegExp(r'^[0-9a-f]{40}(?:[0-9a-f]{24})?$');
|
||||
final _assetNamePattern = RegExp(r'^[A-Za-z0-9][A-Za-z0-9._-]*$');
|
||||
|
||||
class PrecompiledGenerationException implements Exception {
|
||||
PrecompiledGenerationException(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => 'PrecompiledGenerationException: $message';
|
||||
}
|
||||
|
||||
class PrecompiledAsset {
|
||||
const PrecompiledAsset({
|
||||
required this.name,
|
||||
required this.length,
|
||||
required this.sha256,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final int length;
|
||||
final String sha256;
|
||||
}
|
||||
|
||||
class PrecompiledCompositeChecksum {
|
||||
const PrecompiledCompositeChecksum({
|
||||
required this.archive,
|
||||
required this.checksum,
|
||||
this.algorithm = 'sha256',
|
||||
});
|
||||
|
||||
final String archive;
|
||||
final String checksum;
|
||||
final String algorithm;
|
||||
}
|
||||
|
||||
class PrecompiledBuildIdentity {
|
||||
PrecompiledBuildIdentity({
|
||||
required this.rustToolchain,
|
||||
required this.flutterVersion,
|
||||
required this.xcodeVersion,
|
||||
required Map<String, String> sdkVersions,
|
||||
required Map<String, String> deploymentTargets,
|
||||
required Iterable<String> rustTargets,
|
||||
}) : sdkVersions = Map.unmodifiable(Map.from(sdkVersions)),
|
||||
deploymentTargets = Map.unmodifiable(Map.from(deploymentTargets)),
|
||||
rustTargets = List.unmodifiable(rustTargets);
|
||||
|
||||
factory PrecompiledBuildIdentity.fromRecipe(PrecompiledBuildRecipe recipe) {
|
||||
return PrecompiledBuildIdentity(
|
||||
rustToolchain: recipe.rustToolchain,
|
||||
flutterVersion: recipe.flutterVersion,
|
||||
xcodeVersion: recipe.xcodeVersion,
|
||||
sdkVersions: recipe.sdkVersions,
|
||||
deploymentTargets: recipe.deploymentTargets,
|
||||
rustTargets: recipe.rustTargets,
|
||||
);
|
||||
}
|
||||
|
||||
final String rustToolchain;
|
||||
final String flutterVersion;
|
||||
final String xcodeVersion;
|
||||
final Map<String, String> sdkVersions;
|
||||
final Map<String, String> deploymentTargets;
|
||||
final List<String> rustTargets;
|
||||
|
||||
Map<String, dynamic> _json() => {
|
||||
'deployment_targets': _sortedStringMap(deploymentTargets),
|
||||
'flutter_version': flutterVersion,
|
||||
'rust_targets': [...rustTargets]..sort(),
|
||||
'rust_toolchain': rustToolchain,
|
||||
'sdk_versions': _sortedStringMap(sdkVersions),
|
||||
'xcode_version': xcodeVersion,
|
||||
};
|
||||
|
||||
bool matches(PrecompiledBuildIdentity other) =>
|
||||
_canonicalJson(_json()) == _canonicalJson(other._json());
|
||||
}
|
||||
|
||||
class PrecompiledGenerationProvenance {
|
||||
PrecompiledGenerationProvenance({
|
||||
required this.pinned,
|
||||
required this.actual,
|
||||
});
|
||||
|
||||
factory PrecompiledGenerationProvenance.fromRecipe(
|
||||
PrecompiledBuildRecipe recipe) {
|
||||
final identity = PrecompiledBuildIdentity.fromRecipe(recipe);
|
||||
return PrecompiledGenerationProvenance(pinned: identity, actual: identity);
|
||||
}
|
||||
|
||||
final PrecompiledBuildIdentity pinned;
|
||||
final PrecompiledBuildIdentity actual;
|
||||
|
||||
Map<String, dynamic> _json() => {
|
||||
'actual': actual._json(),
|
||||
'pinned': pinned._json(),
|
||||
};
|
||||
}
|
||||
|
||||
class PrecompiledGenerationManifest {
|
||||
PrecompiledGenerationManifest({
|
||||
required this.generationHash,
|
||||
required this.sourceCommit,
|
||||
required this.provenance,
|
||||
required Iterable<PrecompiledAsset> assets,
|
||||
required Iterable<PrecompiledCompositeChecksum> compositeChecksums,
|
||||
}) : assets = List.unmodifiable(assets),
|
||||
compositeChecksums = List.unmodifiable(compositeChecksums) {
|
||||
_validateCompositeMetadata(this.assets, this.compositeChecksums);
|
||||
}
|
||||
|
||||
final String generationHash;
|
||||
final String sourceCommit;
|
||||
final PrecompiledGenerationProvenance provenance;
|
||||
final List<PrecompiledAsset> assets;
|
||||
final List<PrecompiledCompositeChecksum> compositeChecksums;
|
||||
|
||||
PrecompiledAsset? asset(String name) {
|
||||
for (final item in assets) {
|
||||
if (item.name == name) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<int> canonicalBytes() => utf8.encode(_canonicalJson(_json()));
|
||||
|
||||
Map<String, dynamic> _json() => {
|
||||
'asset_signature_scheme': precompiledAssetSignatureScheme,
|
||||
'assets': [
|
||||
...assets.map((asset) => {
|
||||
'length': asset.length,
|
||||
'name': asset.name,
|
||||
'sha256': asset.sha256,
|
||||
})
|
||||
]..sort((a, b) => (a['name'] as String).compareTo(b['name'] as String)),
|
||||
'composite_checksums': [
|
||||
...compositeChecksums.map((binding) => {
|
||||
'algorithm': binding.algorithm,
|
||||
'archive': binding.archive,
|
||||
'checksum': binding.checksum,
|
||||
})
|
||||
]..sort((a, b) {
|
||||
final archive =
|
||||
(a['archive'] as String).compareTo(b['archive'] as String);
|
||||
return archive != 0
|
||||
? archive
|
||||
: (a['checksum'] as String).compareTo(b['checksum'] as String);
|
||||
}),
|
||||
'generation_hash': generationHash,
|
||||
'provenance': provenance._json(),
|
||||
'schema_version': _manifestSchemaVersion,
|
||||
'scope': _manifestScope,
|
||||
'source_commit': sourceCommit,
|
||||
};
|
||||
|
||||
Uint8List sign(PrivateKey privateKey) =>
|
||||
ed25519Sign(privateKey, Uint8List.fromList(canonicalBytes()));
|
||||
|
||||
void verifySignature(PublicKey publicKey, List<int> signature) {
|
||||
_verifySignature(publicKey, canonicalBytes(), signature, 'manifest');
|
||||
}
|
||||
|
||||
void validateFor({
|
||||
required String generationHash,
|
||||
required PrecompiledBuildRecipe recipe,
|
||||
required Set<String> expectedAssetNames,
|
||||
}) {
|
||||
if (this.generationHash != generationHash) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Manifest generation hash does not match the current generation.');
|
||||
}
|
||||
final expected = PrecompiledBuildIdentity.fromRecipe(recipe);
|
||||
if (!provenance.pinned.matches(expected) ||
|
||||
!provenance.actual.matches(provenance.pinned)) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Manifest provenance does not match the build recipe.');
|
||||
}
|
||||
final actualNames = assets.map((asset) => asset.name).toSet();
|
||||
if (actualNames.length != assets.length ||
|
||||
actualNames.length != expectedAssetNames.length ||
|
||||
!actualNames.containsAll(expectedAssetNames) ||
|
||||
!expectedAssetNames.containsAll(actualNames)) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Manifest asset inventory is incomplete or unexpected.');
|
||||
}
|
||||
}
|
||||
|
||||
void verifyAsset({
|
||||
required String name,
|
||||
required List<int> bytes,
|
||||
required List<int> signature,
|
||||
required PublicKey publicKey,
|
||||
}) {
|
||||
final expected = asset(name);
|
||||
if (expected == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Asset "$name" is not in the manifest inventory.');
|
||||
}
|
||||
if (bytes.length != expected.length ||
|
||||
sha256.convert(bytes).toString() != expected.sha256) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Asset "$name" does not match its manifest digest or length.');
|
||||
}
|
||||
verifyAssetMetadataSignature(
|
||||
name: name,
|
||||
signature: signature,
|
||||
publicKey: publicKey,
|
||||
);
|
||||
}
|
||||
|
||||
void verifyAssetMetadataSignature({
|
||||
required String name,
|
||||
required List<int> signature,
|
||||
required PublicKey publicKey,
|
||||
}) {
|
||||
final expected = asset(name);
|
||||
if (expected == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Asset "$name" is not in the manifest inventory.');
|
||||
}
|
||||
verifyPrecompiledAssetMetadataSignature(
|
||||
publicKey,
|
||||
generationHash: generationHash,
|
||||
name: expected.name,
|
||||
length: expected.length,
|
||||
sha256: expected.sha256,
|
||||
signature: signature,
|
||||
);
|
||||
}
|
||||
|
||||
void validateCompositeChecksums(Map<String, List<int>> bytesByName) {
|
||||
for (final binding in compositeChecksums) {
|
||||
if (binding.algorithm != 'sha256') {
|
||||
throw PrecompiledGenerationException('Unsupported checksum algorithm.');
|
||||
}
|
||||
final archive = bytesByName[binding.archive];
|
||||
final checksum = bytesByName[binding.checksum];
|
||||
if (archive == null || checksum == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Composite checksum assets are incomplete.');
|
||||
}
|
||||
final expected = ascii.encode('${sha256.convert(archive)}\n');
|
||||
if (checksum.length != expected.length ||
|
||||
!List<int>.generate(
|
||||
expected.length,
|
||||
(index) => checksum[index] ^ expected[index],
|
||||
).every((difference) => difference == 0)) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Composite checksum does not match its archive.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static PrecompiledGenerationManifest parse(List<int> bytes) {
|
||||
final text = _decodeUtf8(bytes);
|
||||
final value = _StrictJsonParser(text).parse();
|
||||
final map = _object(value, 'manifest');
|
||||
_requireFields(
|
||||
map,
|
||||
const {
|
||||
'schema_version',
|
||||
'scope',
|
||||
'asset_signature_scheme',
|
||||
'generation_hash',
|
||||
'source_commit',
|
||||
'provenance',
|
||||
'assets',
|
||||
'composite_checksums',
|
||||
},
|
||||
'manifest',
|
||||
);
|
||||
if (map['schema_version'] != _manifestSchemaVersion) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Unsupported manifest schema version.');
|
||||
}
|
||||
if (map['scope'] != _manifestScope) {
|
||||
throw PrecompiledGenerationException('Unsupported manifest scope.');
|
||||
}
|
||||
if (map['asset_signature_scheme'] != precompiledAssetSignatureScheme) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Unsupported asset signature scheme.');
|
||||
}
|
||||
final generationHash = _string(map['generation_hash'], 'generation hash');
|
||||
if (!_sha256Pattern.hasMatch(generationHash)) {
|
||||
throw PrecompiledGenerationException('Malformed generation hash.');
|
||||
}
|
||||
final sourceCommit = _string(map['source_commit'], 'source commit');
|
||||
if (!_commitPattern.hasMatch(sourceCommit)) {
|
||||
throw PrecompiledGenerationException('Malformed source commit.');
|
||||
}
|
||||
|
||||
final provenanceMap = _object(map['provenance'], 'provenance');
|
||||
_requireFields(provenanceMap, const {'pinned', 'actual'}, 'provenance');
|
||||
final provenance = PrecompiledGenerationProvenance(
|
||||
pinned: _identity(provenanceMap['pinned'], 'pinned provenance'),
|
||||
actual: _identity(provenanceMap['actual'], 'actual provenance'),
|
||||
);
|
||||
|
||||
final assetsValue = map['assets'];
|
||||
if (assetsValue is! List || assetsValue.isEmpty) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Manifest assets must be a non-empty list.');
|
||||
}
|
||||
final assets = <PrecompiledAsset>[];
|
||||
String? previousName;
|
||||
final seenAssets = <String>{};
|
||||
for (final value in assetsValue) {
|
||||
final assetMap = _object(value, 'asset');
|
||||
_requireFields(assetMap, const {'name', 'length', 'sha256'}, 'asset');
|
||||
final name =
|
||||
_safeName(_string(assetMap['name'], 'asset name'), 'asset name');
|
||||
if (!seenAssets.add(name)) {
|
||||
throw PrecompiledGenerationException('Duplicate asset "$name".');
|
||||
}
|
||||
if (previousName != null && name.compareTo(previousName) <= 0) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Manifest assets must be sorted by name.');
|
||||
}
|
||||
previousName = name;
|
||||
final length = _nonNegativeInt(assetMap['length'], 'asset length');
|
||||
final hash = _string(assetMap['sha256'], 'asset hash');
|
||||
if (!_sha256Pattern.hasMatch(hash)) {
|
||||
throw PrecompiledGenerationException('Malformed asset hash.');
|
||||
}
|
||||
assets.add(PrecompiledAsset(name: name, length: length, sha256: hash));
|
||||
}
|
||||
|
||||
final compositesValue = map['composite_checksums'];
|
||||
if (compositesValue is! List) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Composite checksum relationships must be a list.');
|
||||
}
|
||||
final composites = <PrecompiledCompositeChecksum>[];
|
||||
final seenComposites = <String>{};
|
||||
String? previousComposite;
|
||||
for (final value in compositesValue) {
|
||||
final bindingMap = _object(value, 'composite checksum');
|
||||
_requireFields(bindingMap, const {'algorithm', 'archive', 'checksum'},
|
||||
'composite checksum');
|
||||
final algorithm = _string(bindingMap['algorithm'], 'checksum algorithm');
|
||||
final archive = _safeName(
|
||||
_string(bindingMap['archive'], 'archive asset'), 'archive asset');
|
||||
final checksum = _safeName(
|
||||
_string(bindingMap['checksum'], 'checksum asset'), 'checksum asset');
|
||||
if (algorithm != 'sha256') {
|
||||
throw PrecompiledGenerationException('Unsupported checksum algorithm.');
|
||||
}
|
||||
final identity = '$archive\u0000$checksum';
|
||||
if (!seenComposites.add(identity)) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Duplicate composite checksum relationship.');
|
||||
}
|
||||
if (previousComposite != null &&
|
||||
identity.compareTo(previousComposite) <= 0) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Composite checksum relationships must be sorted.');
|
||||
}
|
||||
previousComposite = identity;
|
||||
if (!seenAssets.contains(archive) || !seenAssets.contains(checksum)) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Composite checksum references an unknown asset.');
|
||||
}
|
||||
composites.add(PrecompiledCompositeChecksum(
|
||||
archive: archive,
|
||||
checksum: checksum,
|
||||
algorithm: algorithm,
|
||||
));
|
||||
}
|
||||
return PrecompiledGenerationManifest(
|
||||
generationHash: generationHash,
|
||||
sourceCommit: sourceCommit,
|
||||
provenance: provenance,
|
||||
assets: assets,
|
||||
compositeChecksums: composites,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List signPrecompiledAsset(PrivateKey privateKey, List<int> bytes) =>
|
||||
ed25519Sign(privateKey, Uint8List.fromList(bytes));
|
||||
|
||||
Uint8List precompiledAssetSignaturePayload({
|
||||
required String generationHash,
|
||||
required String name,
|
||||
required Object length,
|
||||
required String sha256,
|
||||
}) {
|
||||
if (!_sha256Pattern.hasMatch(generationHash) ||
|
||||
!_sha256Pattern.hasMatch(sha256)) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Asset signature metadata contains a malformed digest.');
|
||||
}
|
||||
_safeName(name, 'asset name');
|
||||
final lengthValue = switch (length) {
|
||||
int value => BigInt.from(value),
|
||||
BigInt value => value,
|
||||
_ => throw PrecompiledGenerationException(
|
||||
'Asset signature metadata length is not an integer.'),
|
||||
};
|
||||
if (lengthValue < BigInt.zero ||
|
||||
lengthValue > (BigInt.one << 64) - BigInt.one) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Asset signature metadata length is outside u64.');
|
||||
}
|
||||
final nameBytes = utf8.encode(name);
|
||||
if (nameBytes.length > 0xffffffff) {
|
||||
throw PrecompiledGenerationException('Asset name is too long.');
|
||||
}
|
||||
final nameLength = ByteData(4)..setUint32(0, nameBytes.length, Endian.big);
|
||||
final wordMask = BigInt.from(0xffffffff);
|
||||
final assetLength = ByteData(8)
|
||||
..setUint32(0, (lengthValue >> 32).toInt(), Endian.big)
|
||||
..setUint32(4, (lengthValue & wordMask).toInt(), Endian.big);
|
||||
return Uint8List.fromList([
|
||||
...ascii.encode(_assetSignatureDomain),
|
||||
0,
|
||||
2,
|
||||
..._decodeSha256(generationHash),
|
||||
...nameLength.buffer.asUint8List(),
|
||||
...nameBytes,
|
||||
...assetLength.buffer.asUint8List(),
|
||||
..._decodeSha256(sha256),
|
||||
]);
|
||||
}
|
||||
|
||||
Uint8List signPrecompiledAssetMetadata(
|
||||
PrivateKey privateKey, {
|
||||
required String generationHash,
|
||||
required String name,
|
||||
required int length,
|
||||
required String sha256,
|
||||
}) =>
|
||||
ed25519Sign(
|
||||
privateKey,
|
||||
precompiledAssetSignaturePayload(
|
||||
generationHash: generationHash,
|
||||
name: name,
|
||||
length: length,
|
||||
sha256: sha256,
|
||||
),
|
||||
);
|
||||
|
||||
void verifyPrecompiledAssetMetadataSignature(
|
||||
PublicKey publicKey, {
|
||||
required String generationHash,
|
||||
required String name,
|
||||
required int length,
|
||||
required String sha256,
|
||||
required List<int> signature,
|
||||
}) {
|
||||
_verifySignature(
|
||||
publicKey,
|
||||
precompiledAssetSignaturePayload(
|
||||
generationHash: generationHash,
|
||||
name: name,
|
||||
length: length,
|
||||
sha256: sha256,
|
||||
),
|
||||
signature,
|
||||
'asset "$name" metadata',
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List ed25519Sign(PrivateKey privateKey, Uint8List bytes) =>
|
||||
sign(privateKey, bytes);
|
||||
|
||||
void _verifySignature(
|
||||
PublicKey publicKey, List<int> bytes, List<int> signature, String subject) {
|
||||
if (signature.length != 64 ||
|
||||
!verify(publicKey, Uint8List.fromList(bytes),
|
||||
Uint8List.fromList(signature))) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Invalid Ed25519 signature for $subject.');
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List _decodeSha256(String value) => Uint8List.fromList([
|
||||
for (var index = 0; index < value.length; index += 2)
|
||||
int.parse(value.substring(index, index + 2), radix: 16),
|
||||
]);
|
||||
|
||||
void _validateCompositeMetadata(
|
||||
List<PrecompiledAsset> assets,
|
||||
List<PrecompiledCompositeChecksum> composites,
|
||||
) {
|
||||
final byName = {for (final asset in assets) asset.name: asset};
|
||||
final boundAssets = <String>{};
|
||||
for (final binding in composites) {
|
||||
if (binding.algorithm != 'sha256') {
|
||||
throw PrecompiledGenerationException('Unsupported checksum algorithm.');
|
||||
}
|
||||
if (binding.archive == binding.checksum) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Composite archive and checksum must be distinct assets.');
|
||||
}
|
||||
if (!boundAssets.add(binding.archive) ||
|
||||
!boundAssets.add(binding.checksum)) {
|
||||
throw PrecompiledGenerationException(
|
||||
'An asset may appear in at most one composite binding.');
|
||||
}
|
||||
final archive = byName[binding.archive];
|
||||
final checksum = byName[binding.checksum];
|
||||
if (archive == null || checksum == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Composite checksum references an unknown asset.');
|
||||
}
|
||||
final expectedBytes = ascii.encode('${archive.sha256}\n');
|
||||
if (checksum.length != 65 ||
|
||||
checksum.sha256 != sha256.convert(expectedBytes).toString()) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Composite checksum metadata is inconsistent with its archive.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PrecompiledBuildIdentity _identity(Object? value, String context) {
|
||||
final map = _object(value, context);
|
||||
_requireFields(
|
||||
map,
|
||||
const {
|
||||
'rust_toolchain',
|
||||
'flutter_version',
|
||||
'xcode_version',
|
||||
'sdk_versions',
|
||||
'deployment_targets',
|
||||
'rust_targets',
|
||||
},
|
||||
context);
|
||||
return PrecompiledBuildIdentity(
|
||||
rustToolchain: _string(map['rust_toolchain'], '$context Rust toolchain'),
|
||||
flutterVersion: _string(map['flutter_version'], '$context Flutter version'),
|
||||
xcodeVersion: _string(map['xcode_version'], '$context Xcode version'),
|
||||
sdkVersions: _stringMap(map['sdk_versions'], '$context SDK versions'),
|
||||
deploymentTargets:
|
||||
_stringMap(map['deployment_targets'], '$context deployment targets'),
|
||||
rustTargets:
|
||||
_sortedUniqueStrings(map['rust_targets'], '$context Rust targets'),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, String> _stringMap(Object? value, String context) {
|
||||
final map = _object(value, context);
|
||||
final result = <String, String>{};
|
||||
for (final entry in map.entries) {
|
||||
if (entry.key.isEmpty) {
|
||||
throw PrecompiledGenerationException('$context contains an empty key.');
|
||||
}
|
||||
result[entry.key] = _string(entry.value, '$context value');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<String> _sortedUniqueStrings(Object? value, String context) {
|
||||
if (value is! List || value.isEmpty) {
|
||||
throw PrecompiledGenerationException('$context must be a non-empty list.');
|
||||
}
|
||||
final result = <String>[];
|
||||
final seen = <String>{};
|
||||
for (final item in value) {
|
||||
final string = _string(item, '$context entry');
|
||||
if (!seen.add(string)) {
|
||||
throw PrecompiledGenerationException('Duplicate $context entry.');
|
||||
}
|
||||
result.add(string);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _object(Object? value, String context) {
|
||||
if (value is Map<String, dynamic>) return value;
|
||||
throw PrecompiledGenerationException('$context must be an object.');
|
||||
}
|
||||
|
||||
void _requireFields(
|
||||
Map<String, dynamic> map, Set<String> required, String context) {
|
||||
if (map.keys.length != required.length ||
|
||||
!map.keys.toSet().containsAll(required)) {
|
||||
final unknown = map.keys.where((key) => !required.contains(key));
|
||||
final missing = required.where((key) => !map.containsKey(key));
|
||||
throw PrecompiledGenerationException(
|
||||
'$context fields are invalid (unknown: ${unknown.join(', ')}, missing: ${missing.join(', ')}).');
|
||||
}
|
||||
}
|
||||
|
||||
String _string(Object? value, String context) {
|
||||
if (value is String && value.isNotEmpty) return value;
|
||||
throw PrecompiledGenerationException('$context must be a non-empty string.');
|
||||
}
|
||||
|
||||
int _nonNegativeInt(Object? value, String context) {
|
||||
if (value is int && value >= 0) return value;
|
||||
throw PrecompiledGenerationException(
|
||||
'$context must be a non-negative integer.');
|
||||
}
|
||||
|
||||
String _safeName(String value, String context) {
|
||||
if (value.contains('\\') ||
|
||||
path.posix.isAbsolute(value) ||
|
||||
path.windows.isAbsolute(value) ||
|
||||
path.posix.normalize(value) != value ||
|
||||
value.split('/').any((part) =>
|
||||
part.isEmpty ||
|
||||
part == '.' ||
|
||||
part == '..' ||
|
||||
!_assetNamePattern.hasMatch(part))) {
|
||||
throw PrecompiledGenerationException('$context is unsafe.');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
Map<String, String> _sortedStringMap(Map<String, String> map) {
|
||||
final keys = map.keys.toList()..sort();
|
||||
return {for (final key in keys) key: map[key]!};
|
||||
}
|
||||
|
||||
String _canonicalJson(Object? value) {
|
||||
if (value is Map<String, dynamic>) {
|
||||
final keys = value.keys.toList()..sort();
|
||||
return '{${keys.map((key) => '${jsonEncode(key)}:${_canonicalJson(value[key])}').join(',')}}';
|
||||
}
|
||||
if (value is List) return '[${value.map(_canonicalJson).join(',')}]';
|
||||
return jsonEncode(value);
|
||||
}
|
||||
|
||||
String _decodeUtf8(List<int> bytes) {
|
||||
try {
|
||||
return utf8.decode(bytes);
|
||||
} on FormatException {
|
||||
throw PrecompiledGenerationException('Manifest is not valid UTF-8.');
|
||||
}
|
||||
}
|
||||
|
||||
class _StrictJsonParser {
|
||||
_StrictJsonParser(this.input);
|
||||
|
||||
final String input;
|
||||
int index = 0;
|
||||
|
||||
Object? parse() {
|
||||
_space();
|
||||
final value = _value();
|
||||
_space();
|
||||
if (index != input.length) _fail('Trailing data.');
|
||||
return value;
|
||||
}
|
||||
|
||||
Object? _value() {
|
||||
_space();
|
||||
if (index >= input.length) _fail('Unexpected end of JSON.');
|
||||
return switch (input.codeUnitAt(index)) {
|
||||
0x7b => _objectValue(),
|
||||
0x5b => _arrayValue(),
|
||||
0x22 => _stringValue(),
|
||||
0x74 => _literal('true', true),
|
||||
0x66 => _literal('false', false),
|
||||
0x6e => _literal('null', null),
|
||||
_ => _numberValue(),
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _objectValue() {
|
||||
index++;
|
||||
final result = <String, dynamic>{};
|
||||
_space();
|
||||
if (_take(0x7d)) return result;
|
||||
while (true) {
|
||||
_space();
|
||||
if (index >= input.length || input.codeUnitAt(index) != 0x22) {
|
||||
_fail('Object key must be a string.');
|
||||
}
|
||||
final key = _stringValue();
|
||||
if (result.containsKey(key)) _fail('Duplicate object key "$key".');
|
||||
_space();
|
||||
if (!_take(0x3a)) _fail('Expected colon after object key.');
|
||||
result[key] = _value();
|
||||
_space();
|
||||
if (_take(0x7d)) return result;
|
||||
if (!_take(0x2c)) _fail('Expected comma between object fields.');
|
||||
}
|
||||
}
|
||||
|
||||
List<dynamic> _arrayValue() {
|
||||
index++;
|
||||
final result = <dynamic>[];
|
||||
_space();
|
||||
if (_take(0x5d)) return result;
|
||||
while (true) {
|
||||
result.add(_value());
|
||||
_space();
|
||||
if (_take(0x5d)) return result;
|
||||
if (!_take(0x2c)) _fail('Expected comma between array values.');
|
||||
}
|
||||
}
|
||||
|
||||
String _stringValue() {
|
||||
final start = index;
|
||||
index++;
|
||||
while (index < input.length) {
|
||||
final code = input.codeUnitAt(index++);
|
||||
if (code == 0x22) {
|
||||
final raw = input.substring(start, index);
|
||||
try {
|
||||
final value = jsonDecode(raw);
|
||||
if (value is String) return value;
|
||||
} on FormatException {
|
||||
_fail('Malformed string.');
|
||||
}
|
||||
_fail('Malformed string.');
|
||||
}
|
||||
if (code == 0x5c) {
|
||||
if (index >= input.length) _fail('Malformed escape.');
|
||||
final escaped = input.codeUnitAt(index++);
|
||||
if (escaped == 0x75) {
|
||||
if (index + 4 > input.length ||
|
||||
!RegExp(r'^[0-9a-fA-F]{4}$')
|
||||
.hasMatch(input.substring(index, index + 4))) {
|
||||
_fail('Malformed unicode escape.');
|
||||
}
|
||||
index += 4;
|
||||
} else if (!const [0x22, 0x5c, 0x2f, 0x62, 0x66, 0x6e, 0x72, 0x74]
|
||||
.contains(escaped)) {
|
||||
_fail('Malformed escape.');
|
||||
}
|
||||
} else if (code < 0x20) {
|
||||
_fail('Unescaped control character.');
|
||||
}
|
||||
}
|
||||
_fail('Unterminated string.');
|
||||
}
|
||||
|
||||
Object? _literal(String literal, Object? value) {
|
||||
if (!input.startsWith(literal, index)) _fail('Malformed literal.');
|
||||
index += literal.length;
|
||||
return value;
|
||||
}
|
||||
|
||||
Object _numberValue() {
|
||||
final match =
|
||||
RegExp(r'-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?')
|
||||
.matchAsPrefix(input.substring(index));
|
||||
if (match == null) _fail('Malformed JSON value.');
|
||||
index += match.end;
|
||||
final text = match.group(0)!;
|
||||
return text.contains('.') || text.contains('e') || text.contains('E')
|
||||
? double.parse(text)
|
||||
: int.parse(text);
|
||||
}
|
||||
|
||||
bool _take(int code) {
|
||||
if (index < input.length && input.codeUnitAt(index) == code) {
|
||||
index++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _space() {
|
||||
while (index < input.length &&
|
||||
const [0x20, 0x09, 0x0a, 0x0d].contains(input.codeUnitAt(index))) {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
Never _fail(String message) => throw PrecompiledGenerationException(message);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/// 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';
|
||||
|
||||
String? Function()? testRustupExecutablePathOverride;
|
||||
|
||||
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() {
|
||||
if (testRustupExecutablePathOverride != null) {
|
||||
return testRustupExecutablePathOverride!();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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: 'aarch64-apple-ios-sim',
|
||||
darwinPlatform: 'iphonesimulator',
|
||||
darwinArch: 'arm64',
|
||||
),
|
||||
Target(
|
||||
rust: 'x86_64-apple-ios',
|
||||
darwinPlatform: 'iphonesimulator',
|
||||
darwinArch: 'x86_64',
|
||||
),
|
||||
Target(
|
||||
rust: 'aarch64-apple-ios',
|
||||
darwinPlatform: 'iphoneos',
|
||||
darwinArch: 'arm64',
|
||||
),
|
||||
Target(
|
||||
rust: 'x86_64-apple-darwin',
|
||||
darwinPlatform: 'macosx',
|
||||
darwinArch: 'x86_64',
|
||||
),
|
||||
Target(
|
||||
rust: 'aarch64-apple-darwin',
|
||||
darwinPlatform: 'macosx',
|
||||
darwinArch: 'arm64',
|
||||
),
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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,95 @@
|
||||
/// 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 'cargo.dart';
|
||||
import 'crate_hash.dart';
|
||||
import 'options.dart';
|
||||
import 'precompile_binaries.dart';
|
||||
import 'precompiled_asset_store.dart';
|
||||
import 'precompiled_generation.dart';
|
||||
import 'target.dart';
|
||||
|
||||
class VerifyBinaries {
|
||||
VerifyBinaries({
|
||||
required this.manifestDir,
|
||||
});
|
||||
|
||||
final String manifestDir;
|
||||
|
||||
Future<void> run() async {
|
||||
final config = CargokitCrateOptions.load(manifestDir: manifestDir);
|
||||
final precompiledBinaries = config.precompiledBinaries;
|
||||
if (precompiledBinaries == null) {
|
||||
stdout.writeln('Crate does not support precompiled binaries.');
|
||||
return;
|
||||
}
|
||||
final recipe = precompiledBinaries.buildRecipe;
|
||||
if (recipe == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Configured precompiled binaries need a complete build recipe.');
|
||||
}
|
||||
final crateInfo = CrateInfo.load(manifestDir);
|
||||
final expectedNames = <String>{};
|
||||
for (final triple in recipe.rustTargets) {
|
||||
final target = Target.forRustTriple(triple);
|
||||
if (target == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Build recipe contains an unsupported Rust target.');
|
||||
}
|
||||
for (final type in AritifactType.values) {
|
||||
for (final artifact in getArtifactNames(
|
||||
target: target,
|
||||
libraryName: crateInfo.packageName,
|
||||
remote: true,
|
||||
aritifactType: type,
|
||||
)) {
|
||||
expectedNames.add(PrecompileBinaries.fileName(target, artifact));
|
||||
}
|
||||
}
|
||||
}
|
||||
final compositeChecksums = <String>{};
|
||||
for (final group in precompiledBinaries.compositeGroups) {
|
||||
for (final checksum
|
||||
in group.outputs.where((output) => output.endsWith('.checksum'))) {
|
||||
final archive =
|
||||
checksum.substring(0, checksum.length - '.checksum'.length);
|
||||
if (group.outputs.contains(archive)) {
|
||||
expectedNames.add(archive);
|
||||
expectedNames.add(checksum);
|
||||
compositeChecksums.add('$archive\u0000$checksum');
|
||||
}
|
||||
}
|
||||
}
|
||||
final generationHash = CrateHash.compute(manifestDir);
|
||||
final cacheRoot = Directory.systemTemp.createTempSync('cargokit-verify-');
|
||||
final transport = PrecompiledAssetTransport();
|
||||
try {
|
||||
final store = PrecompiledAssetStore(
|
||||
cacheRoot: path.join(cacheRoot.path, 'cache'),
|
||||
uriPrefix: Uri.parse(precompiledBinaries.uriPrefix),
|
||||
publicKey: precompiledBinaries.publicKey,
|
||||
recipe: recipe,
|
||||
expectedAssetNames: expectedNames,
|
||||
expectedCompositeChecksums: compositeChecksums,
|
||||
transport: transport,
|
||||
);
|
||||
final snapshot = await store.snapshot(
|
||||
generationHash: generationHash,
|
||||
requestedAssetNames: expectedNames,
|
||||
);
|
||||
if (snapshot == null) {
|
||||
throw PrecompiledGenerationException(
|
||||
'Completed v2 precompiled generation is unavailable.');
|
||||
}
|
||||
stdout.writeln('Verified generation $generationHash');
|
||||
} finally {
|
||||
transport.close();
|
||||
if (cacheRoot.existsSync()) cacheRoot.deleteSync(recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "67.0.0"
|
||||
adaptive_number:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: adaptive_number
|
||||
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
args:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: args
|
||||
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
convert:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: convert
|
||||
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
ed25519_edwards:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: ed25519_edwards
|
||||
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
github:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: github
|
||||
sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.17.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
hex:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hex
|
||||
sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.1"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: lints
|
||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
logging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logging
|
||||
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_preamble
|
||||
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.0"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
shelf_packages_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_packages_handler
|
||||
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
shelf_static:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_static
|
||||
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_map_stack_trace
|
||||
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.13"
|
||||
source_span:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.25.15"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.8"
|
||||
toml:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: toml
|
||||
sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: version
|
||||
sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.6"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
yaml:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: yaml
|
||||
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.5.0 <4.0.0"
|
||||
@@ -0,0 +1,33 @@
|
||||
# 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
|
||||
|
||||
name: build_tool
|
||||
description: Cargokit build_tool. Facilitates the build of Rust crate during Flutter application build.
|
||||
publish_to: none
|
||||
version: 1.0.0
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
# Add regular dependencies here.
|
||||
dependencies:
|
||||
# these are pinned on purpose because the bundle_tool_runner doesn't have
|
||||
# pubspec.lock. See run_build_tool.sh
|
||||
logging: 1.2.0
|
||||
path: 1.8.0
|
||||
version: 3.0.0
|
||||
collection: 1.18.0
|
||||
ed25519_edwards: 0.3.1
|
||||
hex: 0.2.0
|
||||
yaml: 3.1.2
|
||||
source_span: 1.10.0
|
||||
github: 9.17.0
|
||||
args: 2.4.2
|
||||
crypto: 3.0.3
|
||||
convert: 3.1.1
|
||||
http: 1.1.0
|
||||
toml: 0.14.0
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^2.1.0
|
||||
test: ^1.24.0
|
||||
@@ -0,0 +1,123 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:build_tool/src/android_environment.dart';
|
||||
import 'package:build_tool/src/target.dart';
|
||||
import 'package:build_tool/src/util.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
tearDown(() {
|
||||
testRunCommandOverride = null;
|
||||
});
|
||||
|
||||
test('arm64 and x64 android targets use 16 KB page size linker flags',
|
||||
() async {
|
||||
final sdkPath = _createFakeAndroidSdk();
|
||||
final targets = [
|
||||
Target.forRustTriple('aarch64-linux-android')!,
|
||||
Target.forRustTriple('x86_64-linux-android')!,
|
||||
];
|
||||
|
||||
for (final target in targets) {
|
||||
final env = await AndroidEnvironment(
|
||||
sdkPath: sdkPath,
|
||||
ndkVersion: '26.3.11579264',
|
||||
minSdkVersion: 21,
|
||||
targetTempDir: '/tmp/fjs-cargokit-test',
|
||||
target: target,
|
||||
).buildEnvironment();
|
||||
|
||||
expect(
|
||||
env['CARGO_ENCODED_RUSTFLAGS'],
|
||||
contains('link-arg=-Wl,-z,max-page-size=16384'),
|
||||
);
|
||||
expect(
|
||||
env['CARGO_ENCODED_RUSTFLAGS'],
|
||||
contains('link-arg=-Wl,--hash-style=both'),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('32-bit android targets keep the libgcc workaround without 16 KB flags',
|
||||
() async {
|
||||
final sdkPath = _createFakeAndroidSdk();
|
||||
final target = Target.forRustTriple('armv7-linux-androideabi')!;
|
||||
final env = await AndroidEnvironment(
|
||||
sdkPath: sdkPath,
|
||||
ndkVersion: '26.3.11579264',
|
||||
minSdkVersion: 16,
|
||||
targetTempDir: '/tmp/fjs-cargokit-test',
|
||||
target: target,
|
||||
).buildEnvironment();
|
||||
|
||||
expect(env['CARGO_ENCODED_RUSTFLAGS'], contains('libgcc_workaround'));
|
||||
expect(
|
||||
env['CARGO_ENCODED_RUSTFLAGS'],
|
||||
isNot(contains('link-arg=-Wl,-z,max-page-size=16384')),
|
||||
);
|
||||
});
|
||||
|
||||
test('bindgen clang args use libclang-safe android paths', () async {
|
||||
final sdkPath = _createFakeAndroidSdk(
|
||||
sdkPathSegment: r'C:\Users\test\AppData\Local\Android\Sdk',
|
||||
);
|
||||
final target = Target.forRustTriple('aarch64-linux-android')!;
|
||||
final env = await AndroidEnvironment(
|
||||
sdkPath: sdkPath,
|
||||
ndkVersion: '26.3.11579264',
|
||||
minSdkVersion: 24,
|
||||
targetTempDir: '/tmp/fjs-cargokit-test',
|
||||
target: target,
|
||||
).buildEnvironment();
|
||||
|
||||
final bindgenArgs = env['BINDGEN_EXTRA_CLANG_ARGS_aarch64-linux-android'];
|
||||
expect(bindgenArgs, isNotNull);
|
||||
expect(bindgenArgs, startsWith('--target=aarch64-linux-android24 '));
|
||||
expect(bindgenArgs, isNot(contains(r'\')));
|
||||
expect(
|
||||
bindgenArgs,
|
||||
contains('/C:/Users/test/AppData/Local/Android/Sdk/ndk/26.3.11579264/'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
String _createFakeAndroidSdk({String? sdkPathSegment}) {
|
||||
final temp = Directory.systemTemp.createTempSync('fjs-cargokit-android-sdk-');
|
||||
final sdk = sdkPathSegment == null
|
||||
? temp
|
||||
: Directory(path.join(temp.path, sdkPathSegment))
|
||||
..createSync(recursive: true);
|
||||
final toolchainBin = Directory(path.join(
|
||||
sdk.path,
|
||||
'ndk',
|
||||
'26.3.11579264',
|
||||
'toolchains',
|
||||
'llvm',
|
||||
'prebuilt',
|
||||
Platform.isWindows
|
||||
? 'windows-x86_64'
|
||||
: Platform.isLinux
|
||||
? 'linux-x86_64'
|
||||
: 'darwin-x86_64',
|
||||
'bin',
|
||||
))
|
||||
..createSync(recursive: true);
|
||||
|
||||
for (final tool in [
|
||||
'llvm-ar',
|
||||
'llvm-ranlib',
|
||||
'clang',
|
||||
'clang++',
|
||||
if (Platform.isWindows) ...[
|
||||
'llvm-ar.exe',
|
||||
'llvm-ranlib.exe',
|
||||
'clang.exe',
|
||||
'clang++.exe',
|
||||
],
|
||||
]) {
|
||||
File(path.join(toolchainBin.path, tool)).writeAsStringSync('');
|
||||
}
|
||||
|
||||
return sdk.path;
|
||||
}
|
||||
@@ -0,0 +1,912 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:build_tool/src/artifacts_provider.dart';
|
||||
import 'package:build_tool/src/builder.dart';
|
||||
import 'package:build_tool/src/cargo.dart';
|
||||
import 'package:build_tool/src/options.dart';
|
||||
import 'package:build_tool/src/precompile_binaries.dart';
|
||||
import 'package:build_tool/src/precompiled_asset_store.dart';
|
||||
import 'package:build_tool/src/precompiled_generation.dart';
|
||||
import 'package:build_tool/src/target.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
const _hash =
|
||||
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
const _sourceCommit = '1111111111111111111111111111111111111111';
|
||||
const _privateSeed = <int>[
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
31,
|
||||
];
|
||||
|
||||
void main() {
|
||||
late Directory temp;
|
||||
late Target target;
|
||||
late KeyPair keyPair;
|
||||
late PrecompiledBuildRecipe recipe;
|
||||
late BuildEnvironment environment;
|
||||
|
||||
setUp(() {
|
||||
temp = Directory.systemTemp.createTempSync('artifacts-provider-test-');
|
||||
target = Target.forRustTriple('x86_64-unknown-linux-gnu')!;
|
||||
keyPair = newKeyPair();
|
||||
recipe = PrecompiledBuildRecipe(
|
||||
rustToolchain: '1.88.0',
|
||||
flutterVersion: '3.32.8',
|
||||
xcodeVersion: '16.4',
|
||||
sdkVersions: const {'linux': '6.8'},
|
||||
deploymentTargets: const {'linux': 'glibc-2.35'},
|
||||
rustTargets: [target.rust],
|
||||
);
|
||||
environment = BuildEnvironment(
|
||||
configuration: BuildConfiguration.release,
|
||||
crateOptions: CargokitCrateOptions(
|
||||
precompiledBinaries: PrecompiledBinaries(
|
||||
uriPrefix: 'https://assets.example/precompiled/',
|
||||
publicKey: keyPair.publicKey,
|
||||
buildRecipe: recipe,
|
||||
),
|
||||
),
|
||||
targetTempDir: temp.path,
|
||||
manifestDir: temp.path,
|
||||
crateInfo: CrateInfo(packageName: 'fjs'),
|
||||
isAndroid: false,
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() => temp.deleteSync(recursive: true));
|
||||
|
||||
test('disabled mode uses local Rust and never contacts remote', () async {
|
||||
var localCalls = 0;
|
||||
var remoteCalls = 0;
|
||||
final local = await _localArtifact(temp, target, 'local.a');
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.disabled,
|
||||
httpGet: (_) async {
|
||||
remoteCalls++;
|
||||
return Response('', 500);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return [local];
|
||||
},
|
||||
);
|
||||
|
||||
final result = await provider.getArtifacts([target]);
|
||||
|
||||
expect(result[target]!.single.path, local.path);
|
||||
expect(localCalls, 1);
|
||||
expect(remoteCalls, 0);
|
||||
});
|
||||
|
||||
test('auto mode falls back locally only when completion manifest is absent',
|
||||
() async {
|
||||
var localCalls = 0;
|
||||
var requested = <String>[];
|
||||
final local = await _localArtifact(temp, target, 'local.a');
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.auto,
|
||||
httpGet: (url) async {
|
||||
requested.add(url.path);
|
||||
return Response('', 404);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return [local];
|
||||
},
|
||||
);
|
||||
|
||||
final result = await provider.getArtifacts([target]);
|
||||
|
||||
expect(result[target]!.single.path, local.path);
|
||||
expect(localCalls, 1);
|
||||
expect(requested, hasLength(1));
|
||||
expect(requested.single, contains('completion.json'));
|
||||
});
|
||||
|
||||
test('auto mode fails closed when completion is missing for existing cache',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
final installProvider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async => fixture.responses[url.path] ?? Response('', 404),
|
||||
);
|
||||
await installProvider.getArtifacts([target]);
|
||||
final cacheFile = File(path.join(
|
||||
temp.path,
|
||||
'precompiled',
|
||||
'v2',
|
||||
_hash,
|
||||
'snapshots',
|
||||
PrecompiledAssetStore.requestKey({fixture.assetName}),
|
||||
fixture.assetName,
|
||||
));
|
||||
cacheFile.writeAsBytesSync([0, 0, 0]);
|
||||
|
||||
final requested = <String>[];
|
||||
var localCalls = 0;
|
||||
final orphanedProvider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.auto,
|
||||
httpGet: (url) async {
|
||||
requested.add(url.path);
|
||||
return Response('', 404);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
orphanedProvider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
|
||||
expect(cacheFile.readAsBytesSync(), [0, 0, 0]);
|
||||
expect(requested, [
|
||||
'/precompiled/$_hash/completion.json',
|
||||
]);
|
||||
expect(localCalls, 0);
|
||||
});
|
||||
|
||||
test('auto 404 fails closed for legacy staging previous and malformed v2',
|
||||
() async {
|
||||
final precompiledRoot = Directory(path.join(temp.path, 'precompiled'))
|
||||
..createSync();
|
||||
final states = <FileSystemEntity>[
|
||||
Directory(path.join(precompiledRoot.path, '$_hash.staging-foreign')),
|
||||
Directory(path.join(precompiledRoot.path, '$_hash.previous-foreign')),
|
||||
File(path.join(precompiledRoot.path, 'v2')),
|
||||
];
|
||||
for (final state in states) {
|
||||
if (state is Directory) {
|
||||
state.createSync();
|
||||
} else if (state is File) {
|
||||
state.writeAsStringSync('malformed');
|
||||
}
|
||||
var localCalls = 0;
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.auto,
|
||||
httpGet: (_) async => Response('', 404),
|
||||
localBuilder: (_, __) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
);
|
||||
await expectLater(
|
||||
provider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(localCalls, 0);
|
||||
if (state.existsSync()) state.deleteSync(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'auto mode distinguishes unsupported precompilation from incomplete config',
|
||||
() async {
|
||||
var localCalls = 0;
|
||||
var remoteCalls = 0;
|
||||
final local = await _localArtifact(temp, target, 'local.a');
|
||||
final noPrecompiled = _withPrecompiled(environment, null);
|
||||
final unsupportedProvider = _provider(
|
||||
noPrecompiled,
|
||||
PrecompiledBinariesMode.auto,
|
||||
httpGet: (_) async {
|
||||
remoteCalls++;
|
||||
return Response('', 500);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return [local];
|
||||
},
|
||||
);
|
||||
|
||||
final localResult = await unsupportedProvider.getArtifacts([target]);
|
||||
|
||||
expect(localResult[target]!.single.path, local.path);
|
||||
expect(localCalls, 1);
|
||||
expect(remoteCalls, 0);
|
||||
|
||||
final incompleteConfig = _withPrecompiled(
|
||||
environment,
|
||||
PrecompiledBinaries(
|
||||
uriPrefix: 'https://assets.example/precompiled/',
|
||||
publicKey: keyPair.publicKey,
|
||||
),
|
||||
);
|
||||
final incompleteProvider = _provider(
|
||||
incompleteConfig,
|
||||
PrecompiledBinariesMode.auto,
|
||||
httpGet: (_) async {
|
||||
remoteCalls++;
|
||||
return Response('', 500);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return [local];
|
||||
},
|
||||
);
|
||||
await expectLater(
|
||||
incompleteProvider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(localCalls, 1);
|
||||
expect(remoteCalls, 0);
|
||||
|
||||
final disabledProvider = _provider(
|
||||
incompleteConfig,
|
||||
PrecompiledBinariesMode.disabled,
|
||||
httpGet: (_) async {
|
||||
remoteCalls++;
|
||||
return Response('', 500);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return [local];
|
||||
},
|
||||
);
|
||||
final disabledResult = await disabledProvider.getArtifacts([target]);
|
||||
expect(disabledResult[target]!.single.path, local.path);
|
||||
expect(localCalls, 2);
|
||||
expect(remoteCalls, 0);
|
||||
|
||||
final requiredProvider = _provider(
|
||||
incompleteConfig,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (_) async {
|
||||
remoteCalls++;
|
||||
return Response('', 500);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return [local];
|
||||
},
|
||||
);
|
||||
await expectLater(
|
||||
requiredProvider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(localCalls, 2);
|
||||
expect(remoteCalls, 0);
|
||||
});
|
||||
|
||||
test('required mode fails on missing completion and never invokes local Rust',
|
||||
() async {
|
||||
var localCalls = 0;
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async => Response('', 404),
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
provider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(localCalls, 0);
|
||||
});
|
||||
|
||||
test('fetches signed completion first and only exact requested target assets',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
final calls = <String>[];
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async {
|
||||
calls.add(url.path);
|
||||
return fixture.responses[url.path] ?? Response('', 404);
|
||||
},
|
||||
);
|
||||
|
||||
final result = await provider.getArtifacts([target]);
|
||||
|
||||
expect(result[target], hasLength(1));
|
||||
expect(result[target]!.single.finalFileName, 'libfjs.so');
|
||||
expect(calls.first, contains('completion.json'));
|
||||
expect(calls[1], contains('completion.json.sig'));
|
||||
expect(calls.where((call) => call.endsWith('.sig')).length, 2);
|
||||
expect(calls, everyElement(isNot(contains('other-target'))));
|
||||
});
|
||||
|
||||
test(
|
||||
'rejects a requested target outside the signed recipe before asset requests',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
final unsupportedTarget =
|
||||
Target.forRustTriple('aarch64-unknown-linux-gnu')!;
|
||||
final calls = <String>[];
|
||||
var localCalls = 0;
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async {
|
||||
calls.add(url.path);
|
||||
return fixture.responses[url.path] ?? Response('', 404);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
provider.getArtifacts([unsupportedTarget]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
|
||||
expect(calls, isEmpty);
|
||||
expect(localCalls, 0);
|
||||
});
|
||||
|
||||
test('target-only request does not download unrelated composite assets',
|
||||
() async {
|
||||
final group = _compositeGroup(target);
|
||||
final compositeEnvironment = _withPrecompiled(
|
||||
environment,
|
||||
PrecompiledBinaries(
|
||||
uriPrefix: 'https://assets.example/precompiled/',
|
||||
publicKey: keyPair.publicKey,
|
||||
buildRecipe: recipe,
|
||||
compositeGroups: [group],
|
||||
),
|
||||
);
|
||||
final fixture = _fixture(
|
||||
recipe,
|
||||
target: target,
|
||||
keyPair: keyPair,
|
||||
includeComposite: true,
|
||||
);
|
||||
final calls = <String>[];
|
||||
var localCalls = 0;
|
||||
final provider = _provider(
|
||||
compositeEnvironment,
|
||||
PrecompiledBinariesMode.auto,
|
||||
httpGet: (url) async {
|
||||
calls.add(url.path);
|
||||
return fixture.responses[url.path] ?? Response('', 404);
|
||||
},
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
);
|
||||
|
||||
final result = await provider.getArtifacts([target]);
|
||||
|
||||
expect(result[target], hasLength(1));
|
||||
expect(calls, isNot(contains(endsWith('/fjs.xcframework.zip'))));
|
||||
expect(calls, isNot(contains(endsWith('/fjs.xcframework.zip.checksum'))));
|
||||
expect(localCalls, 0);
|
||||
});
|
||||
|
||||
test('revalidates immutable snapshot on every use and never repairs tamper',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
var assetRequests = 0;
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async {
|
||||
if (url.path.endsWith('.so') || url.path.endsWith('.so.sig')) {
|
||||
assetRequests++;
|
||||
}
|
||||
return fixture.responses[url.path] ?? Response('', 404);
|
||||
},
|
||||
);
|
||||
await provider.getArtifacts([target]);
|
||||
final cacheFile = File(path.join(
|
||||
temp.path,
|
||||
'precompiled',
|
||||
'v2',
|
||||
_hash,
|
||||
'snapshots',
|
||||
PrecompiledAssetStore.requestKey({fixture.assetName}),
|
||||
fixture.assetName,
|
||||
));
|
||||
cacheFile.writeAsBytesSync([0, 0, 0]);
|
||||
|
||||
await expectLater(
|
||||
provider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
|
||||
expect(cacheFile.readAsBytesSync(), [0, 0, 0]);
|
||||
expect(assetRequests, 2);
|
||||
});
|
||||
|
||||
test(
|
||||
'tampered cache plus invalid remote repair fails closed without local Rust',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
final initialProvider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async => fixture.responses[url.path] ?? Response('', 404),
|
||||
);
|
||||
await initialProvider.getArtifacts([target]);
|
||||
final cacheFile = File(path.join(
|
||||
temp.path,
|
||||
'precompiled',
|
||||
'v2',
|
||||
_hash,
|
||||
'snapshots',
|
||||
PrecompiledAssetStore.requestKey({fixture.assetName}),
|
||||
fixture.assetName,
|
||||
));
|
||||
cacheFile.writeAsBytesSync([0, 0, 0]);
|
||||
|
||||
final invalidRemote = Map<String, Response>.from(fixture.responses);
|
||||
final signaturePath = invalidRemote.keys
|
||||
.firstWhere((key) => key.endsWith('${fixture.assetName}.sig'));
|
||||
invalidRemote[signaturePath] = Response.bytes(Uint8List(64), 200);
|
||||
var localCalls = 0;
|
||||
final repairProvider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async => invalidRemote[url.path] ?? Response('', 404),
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
repairProvider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
|
||||
expect(cacheFile.readAsBytesSync(), [0, 0, 0]);
|
||||
expect(localCalls, 0);
|
||||
});
|
||||
|
||||
test('tampered manifest anchor remains unchanged and never builds locally',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
await _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async => fixture.responses[url.path] ?? Response('', 404),
|
||||
).getArtifacts([target]);
|
||||
final anchor = File(path.join(
|
||||
temp.path,
|
||||
'precompiled',
|
||||
'v2',
|
||||
_hash,
|
||||
'anchor',
|
||||
precompiledGenerationManifestFileName,
|
||||
))
|
||||
..writeAsBytesSync([0]);
|
||||
var localCalls = 0;
|
||||
|
||||
await expectLater(
|
||||
_provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.auto,
|
||||
httpGet: (url) async =>
|
||||
fixture.responses[url.path] ?? Response('', 404),
|
||||
localBuilder: (_, __) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
).getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
|
||||
expect(anchor.readAsBytesSync(), [0]);
|
||||
expect(localCalls, 0);
|
||||
});
|
||||
|
||||
test(
|
||||
'partial or signature-invalid remote generation fails closed without local fallback',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
var localCalls = 0;
|
||||
final missingAssetResponses = Map<String, Response>.from(fixture.responses)
|
||||
..removeWhere((key, _) => key.endsWith('.so'));
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.auto,
|
||||
httpGet: (url) async =>
|
||||
missingAssetResponses[url.path] ?? Response('', 404),
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
);
|
||||
await expectLater(
|
||||
provider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(localCalls, 0);
|
||||
|
||||
final badSignatureResponses = Map<String, Response>.from(fixture.responses);
|
||||
final signaturePath =
|
||||
badSignatureResponses.keys.firstWhere((key) => key.endsWith('.so.sig'));
|
||||
badSignatureResponses[signaturePath] = Response.bytes(Uint8List(64), 200);
|
||||
final badProvider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async =>
|
||||
badSignatureResponses[url.path] ?? Response('', 404),
|
||||
localBuilder: (target, _) async {
|
||||
localCalls++;
|
||||
return const [];
|
||||
},
|
||||
);
|
||||
await expectLater(
|
||||
badProvider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(localCalls, 0);
|
||||
});
|
||||
|
||||
test('download failure leaves anchor but no partial snapshot staging',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
final responses = Map<String, Response>.from(fixture.responses);
|
||||
final assetPath = responses.keys.firstWhere((key) => key.endsWith('.so'));
|
||||
responses[assetPath] = Response.bytes([1, 2, 3], 200);
|
||||
final provider = _provider(
|
||||
environment,
|
||||
PrecompiledBinariesMode.required,
|
||||
httpGet: (url) async => responses[url.path] ?? Response('', 404),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
provider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
final generation =
|
||||
Directory(path.join(temp.path, 'precompiled', 'v2', _hash));
|
||||
expect(
|
||||
Directory(path.join(generation.path, 'anchor')).existsSync(), isTrue);
|
||||
expect(
|
||||
Directory(path.join(generation.path, 'snapshots'))
|
||||
.listSync(followLinks: false)
|
||||
.where((entity) => path.basename(entity.path).contains('.staging-')),
|
||||
isEmpty,
|
||||
);
|
||||
});
|
||||
|
||||
test('creates and closes a fresh transport for every remote lookup',
|
||||
() async {
|
||||
final fixture = _fixture(recipe, target: target, keyPair: keyPair);
|
||||
final transports = <_TrackingTransport>[];
|
||||
final provider = ArtifactProvider(
|
||||
environment: environment,
|
||||
userOptions: CargokitUserOptions(
|
||||
precompiledBinariesMode: PrecompiledBinariesMode.required,
|
||||
verboseLogging: false,
|
||||
),
|
||||
generationHash: (_) => _hash,
|
||||
transportFactory: () {
|
||||
final transport = _TrackingTransport(
|
||||
(request) async => _streamedResponse(
|
||||
fixture.responses[request.url.path] ?? Response('', 404),
|
||||
request,
|
||||
),
|
||||
);
|
||||
transports.add(transport);
|
||||
return transport;
|
||||
},
|
||||
);
|
||||
|
||||
await provider.getArtifacts([target]);
|
||||
await provider.getArtifacts([target]);
|
||||
|
||||
expect(transports, hasLength(2));
|
||||
expect(transports.map((transport) => transport.closeCalls), [1, 1]);
|
||||
});
|
||||
|
||||
test('closes its transport when a remote lookup fails', () async {
|
||||
_TrackingTransport? transport;
|
||||
final provider = ArtifactProvider(
|
||||
environment: environment,
|
||||
userOptions: CargokitUserOptions(
|
||||
precompiledBinariesMode: PrecompiledBinariesMode.required,
|
||||
verboseLogging: false,
|
||||
),
|
||||
generationHash: (_) => _hash,
|
||||
transportFactory: () => transport = _TrackingTransport(
|
||||
(_) async => StreamedResponse(
|
||||
const Stream<List<int>>.empty(),
|
||||
500,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
provider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
|
||||
expect(transport!.closeCalls, 1);
|
||||
});
|
||||
|
||||
test('validates the complete recipe before creating a transport', () async {
|
||||
final invalidRecipe = PrecompiledBuildRecipe(
|
||||
rustToolchain: recipe.rustToolchain,
|
||||
flutterVersion: recipe.flutterVersion,
|
||||
xcodeVersion: recipe.xcodeVersion,
|
||||
sdkVersions: recipe.sdkVersions,
|
||||
deploymentTargets: recipe.deploymentTargets,
|
||||
rustTargets: [target.rust, 'unsupported-target'],
|
||||
);
|
||||
final invalidEnvironment = _withPrecompiled(
|
||||
environment,
|
||||
PrecompiledBinaries(
|
||||
uriPrefix: 'https://assets.example/precompiled/',
|
||||
publicKey: keyPair.publicKey,
|
||||
buildRecipe: invalidRecipe,
|
||||
),
|
||||
);
|
||||
var transports = 0;
|
||||
final provider = ArtifactProvider(
|
||||
environment: invalidEnvironment,
|
||||
userOptions: CargokitUserOptions(
|
||||
precompiledBinariesMode: PrecompiledBinariesMode.required,
|
||||
verboseLogging: false,
|
||||
),
|
||||
generationHash: (_) => _hash,
|
||||
transportFactory: () {
|
||||
transports++;
|
||||
return PrecompiledAssetTransport(
|
||||
send: (_) async =>
|
||||
StreamedResponse(const Stream<List<int>>.empty(), 500),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
provider.getArtifacts([target]),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
|
||||
expect(transports, 0);
|
||||
});
|
||||
}
|
||||
|
||||
class _TrackingTransport extends PrecompiledAssetTransport {
|
||||
_TrackingTransport(PrecompiledHttpSend send) : super(send: send);
|
||||
|
||||
var closeCalls = 0;
|
||||
|
||||
@override
|
||||
void close() {
|
||||
closeCalls++;
|
||||
super.close();
|
||||
}
|
||||
}
|
||||
|
||||
StreamedResponse _streamedResponse(Response response, BaseRequest request) {
|
||||
return StreamedResponse(
|
||||
Stream.value(response.bodyBytes),
|
||||
response.statusCode,
|
||||
contentLength: response.bodyBytes.length,
|
||||
headers: response.headers,
|
||||
reasonPhrase: response.reasonPhrase,
|
||||
request: request,
|
||||
isRedirect: response.isRedirect,
|
||||
);
|
||||
}
|
||||
|
||||
ArtifactProvider _provider(
|
||||
BuildEnvironment environment,
|
||||
PrecompiledBinariesMode mode, {
|
||||
required ArtifactHttpGet httpGet,
|
||||
ArtifactLocalBuilder? localBuilder,
|
||||
}) {
|
||||
return ArtifactProvider(
|
||||
environment: environment,
|
||||
userOptions: CargokitUserOptions(
|
||||
precompiledBinariesMode: mode,
|
||||
verboseLogging: false,
|
||||
),
|
||||
httpGet: httpGet,
|
||||
generationHash: (_) => _hash,
|
||||
localBuilder: localBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Artifact> _localArtifact(
|
||||
Directory temp, Target target, String name) async {
|
||||
final file = File(path.join(temp.path, name))..writeAsBytesSync([1]);
|
||||
return Artifact(path: file.path, finalFileName: name);
|
||||
}
|
||||
|
||||
class _Fixture {
|
||||
_Fixture(
|
||||
{required this.responses,
|
||||
required this.assetName,
|
||||
required this.assetBytes});
|
||||
final Map<String, Response> responses;
|
||||
final String assetName;
|
||||
final Uint8List assetBytes;
|
||||
}
|
||||
|
||||
_Fixture _fixture(
|
||||
PrecompiledBuildRecipe recipe, {
|
||||
required Target target,
|
||||
required KeyPair keyPair,
|
||||
bool includeComposite = false,
|
||||
}) {
|
||||
final assetName = PrecompileBinaries.fileName(target, 'libfjs.so');
|
||||
final assetBytes = Uint8List.fromList([5, 4, 3, 2]);
|
||||
final staticAssetName = PrecompileBinaries.fileName(target, 'libfjs.a');
|
||||
final staticAssetBytes = Uint8List.fromList([4, 3, 2, 1]);
|
||||
final archiveBytes = Uint8List.fromList([8, 7, 6, 5]);
|
||||
final checksumText = sha256Bytes(archiveBytes);
|
||||
final checksumBytes = Uint8List.fromList(utf8.encode('$checksumText\n'));
|
||||
final assets = <PrecompiledAsset>[
|
||||
PrecompiledAsset(
|
||||
name: assetName,
|
||||
length: assetBytes.length,
|
||||
sha256: sha256Bytes(assetBytes),
|
||||
),
|
||||
PrecompiledAsset(
|
||||
name: staticAssetName,
|
||||
length: staticAssetBytes.length,
|
||||
sha256: sha256Bytes(staticAssetBytes),
|
||||
),
|
||||
if (includeComposite) ...[
|
||||
PrecompiledAsset(
|
||||
name: 'fjs.xcframework.zip',
|
||||
length: archiveBytes.length,
|
||||
sha256: sha256Bytes(archiveBytes),
|
||||
),
|
||||
PrecompiledAsset(
|
||||
name: 'fjs.xcframework.zip.checksum',
|
||||
length: checksumBytes.length,
|
||||
sha256: sha256Bytes(checksumBytes),
|
||||
),
|
||||
],
|
||||
]..sort((a, b) => a.name.compareTo(b.name));
|
||||
final manifest = PrecompiledGenerationManifest(
|
||||
generationHash: _hash,
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: assets,
|
||||
compositeChecksums: includeComposite
|
||||
? const [
|
||||
PrecompiledCompositeChecksum(
|
||||
archive: 'fjs.xcframework.zip',
|
||||
checksum: 'fjs.xcframework.zip.checksum',
|
||||
),
|
||||
]
|
||||
: const [],
|
||||
);
|
||||
final prefix = 'https://assets.example/precompiled/$_hash/';
|
||||
return _Fixture(
|
||||
assetName: assetName,
|
||||
assetBytes: assetBytes,
|
||||
responses: {
|
||||
'/precompiled/$_hash/completion.json.sig':
|
||||
Response.bytes(manifest.sign(keyPair.privateKey), 200),
|
||||
'/precompiled/$_hash/completion.json':
|
||||
Response.bytes(manifest.canonicalBytes(), 200),
|
||||
'/precompiled/$_hash/$assetName.sig': Response.bytes(
|
||||
signPrecompiledAssetMetadata(
|
||||
keyPair.privateKey,
|
||||
generationHash: _hash,
|
||||
name: assetName,
|
||||
length: assetBytes.length,
|
||||
sha256: sha256Bytes(assetBytes),
|
||||
),
|
||||
200),
|
||||
'/precompiled/$_hash/$assetName': Response.bytes(assetBytes, 200),
|
||||
if (includeComposite) ...{
|
||||
'/precompiled/$_hash/fjs.xcframework.zip.sig': Response.bytes(
|
||||
signPrecompiledAssetMetadata(
|
||||
keyPair.privateKey,
|
||||
generationHash: _hash,
|
||||
name: 'fjs.xcframework.zip',
|
||||
length: archiveBytes.length,
|
||||
sha256: sha256Bytes(archiveBytes),
|
||||
),
|
||||
200,
|
||||
),
|
||||
'/precompiled/$_hash/fjs.xcframework.zip':
|
||||
Response.bytes(archiveBytes, 200),
|
||||
'/precompiled/$_hash/fjs.xcframework.zip.checksum.sig': Response.bytes(
|
||||
signPrecompiledAssetMetadata(
|
||||
keyPair.privateKey,
|
||||
generationHash: _hash,
|
||||
name: 'fjs.xcframework.zip.checksum',
|
||||
length: checksumBytes.length,
|
||||
sha256: sha256Bytes(checksumBytes),
|
||||
),
|
||||
200,
|
||||
),
|
||||
'/precompiled/$_hash/fjs.xcframework.zip.checksum':
|
||||
Response.bytes(checksumBytes, 200),
|
||||
},
|
||||
prefix: Response('', 404),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
BuildEnvironment _withPrecompiled(
|
||||
BuildEnvironment environment,
|
||||
PrecompiledBinaries? precompiled,
|
||||
) {
|
||||
return BuildEnvironment(
|
||||
configuration: environment.configuration,
|
||||
crateOptions: CargokitCrateOptions(precompiledBinaries: precompiled),
|
||||
targetTempDir: environment.targetTempDir,
|
||||
manifestDir: environment.manifestDir,
|
||||
crateInfo: environment.crateInfo,
|
||||
isAndroid: environment.isAndroid,
|
||||
androidSdkPath: environment.androidSdkPath,
|
||||
androidNdkVersion: environment.androidNdkVersion,
|
||||
androidMinSdkVersion: environment.androidMinSdkVersion,
|
||||
javaHome: environment.javaHome,
|
||||
);
|
||||
}
|
||||
|
||||
CompositeGroup _compositeGroup(Target target) {
|
||||
return CompositeGroup(
|
||||
name: 'swiftpm',
|
||||
host: CompositeHost.linux,
|
||||
requiredTargets: [target.rust],
|
||||
argv: const ['tool/build.sh'],
|
||||
environment: const {},
|
||||
timeout: const Duration(minutes: 1),
|
||||
outputs: const [
|
||||
'fjs.xcframework.zip',
|
||||
'fjs.xcframework.zip.checksum',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String sha256Bytes(List<int> bytes) => sha256.convert(bytes).toString();
|
||||
|
||||
KeyPair newKeyPair() {
|
||||
final private = newKeyFromSeed(Uint8List.fromList(_privateSeed));
|
||||
return KeyPair(private, public(private));
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:build_tool/src/builder.dart';
|
||||
import 'package:build_tool/src/cargo.dart';
|
||||
import 'package:build_tool/src/options.dart';
|
||||
import 'package:build_tool/src/target.dart';
|
||||
import 'package:build_tool/src/util.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
tearDown(() {
|
||||
testRunCommandOverride = null;
|
||||
});
|
||||
|
||||
test('cargo build uses rustc from the selected rustup toolchain', () async {
|
||||
Map<String, String>? cargoEnvironment;
|
||||
testRunCommandOverride = (args) {
|
||||
if (args.executable == 'rustup' &&
|
||||
args.arguments.length == 4 &&
|
||||
args.arguments[0] == 'which' &&
|
||||
args.arguments[1] == '--toolchain' &&
|
||||
args.arguments[2] == 'stable' &&
|
||||
args.arguments[3] == 'rustc') {
|
||||
return TestRunCommandResult(stdout: '/rustup/stable/bin/rustc\n');
|
||||
}
|
||||
|
||||
if (args.executable == 'rustup' &&
|
||||
args.arguments.length >= 4 &&
|
||||
args.arguments[0] == 'run' &&
|
||||
args.arguments[1] == 'stable' &&
|
||||
args.arguments[2] == 'cargo' &&
|
||||
args.arguments[3] == 'build') {
|
||||
cargoEnvironment = args.environment;
|
||||
return TestRunCommandResult();
|
||||
}
|
||||
|
||||
throw StateError(
|
||||
'Unexpected command: ${args.executable} ${args.arguments}');
|
||||
};
|
||||
|
||||
final builder = RustBuilder(
|
||||
target: Target.forRustTriple('x86_64-pc-windows-msvc')!,
|
||||
environment: BuildEnvironment(
|
||||
configuration: BuildConfiguration.release,
|
||||
crateOptions: CargokitCrateOptions(),
|
||||
targetTempDir: '/tmp/fjs-cargokit-test',
|
||||
manifestDir: '/tmp/fjs-cargokit-manifest',
|
||||
crateInfo: CrateInfo(packageName: 'fjs'),
|
||||
isAndroid: false,
|
||||
),
|
||||
);
|
||||
|
||||
await builder.build();
|
||||
|
||||
expect(cargoEnvironment?['RUSTC'], '/rustup/stable/bin/rustc');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:build_tool/src/build_tool.dart';
|
||||
import 'package:build_tool/src/builder.dart';
|
||||
import 'package:build_tool/src/composite_artifacts.dart';
|
||||
import 'package:build_tool/src/options.dart';
|
||||
import 'package:build_tool/src/precompile_binaries.dart';
|
||||
import 'package:build_tool/src/target.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
late Directory temp;
|
||||
late Directory workspace;
|
||||
late Directory targetRoot;
|
||||
late CompositeGroup group;
|
||||
|
||||
setUp(() {
|
||||
final systemTemp =
|
||||
Directory(Directory.systemTemp.resolveSymbolicLinksSync());
|
||||
temp = systemTemp.createTempSync('composite-artifacts-test.');
|
||||
workspace = Directory(path.join(temp.path, 'workspace'))..createSync();
|
||||
targetRoot = Directory(path.join(temp.path, 'targets'))..createSync();
|
||||
File(path.join(workspace.path, 'assemble.sh'))
|
||||
.writeAsStringSync('#!/bin/sh\n');
|
||||
Directory(path.join(targetRoot.path, 'aarch64-apple-ios')).createSync();
|
||||
group = CompositeGroup(
|
||||
name: 'swiftpm',
|
||||
host: CompositeHost.macos,
|
||||
requiredTargets: const ['aarch64-apple-ios'],
|
||||
argv: const ['assemble.sh', r'$literal'],
|
||||
environment: const {'LC_ALL': 'C', 'PATH': '/usr/bin:/bin'},
|
||||
timeout: const Duration(seconds: 3),
|
||||
outputs: const ['bundle.zip', 'bundle.zip.checksum'],
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() => temp.deleteSync(recursive: true));
|
||||
|
||||
test('local generation command requires explicit paths and repeats targets',
|
||||
() {
|
||||
final command = BuildPrecompiledGenerationCommand();
|
||||
final arguments = command.argParser.parse(const [
|
||||
'--manifest-dir',
|
||||
'/manifest',
|
||||
'--output-dir',
|
||||
'/output',
|
||||
'--temp-dir',
|
||||
'/temp',
|
||||
'--target',
|
||||
'aarch64-apple-ios',
|
||||
'--target',
|
||||
'aarch64-apple-darwin',
|
||||
]);
|
||||
|
||||
expect(command.name, 'build-precompiled-generation');
|
||||
expect(arguments['manifest-dir'], '/manifest');
|
||||
expect(arguments['output-dir'], '/output');
|
||||
expect(arguments['temp-dir'], '/temp');
|
||||
expect(arguments['target'], [
|
||||
'aarch64-apple-ios',
|
||||
'aarch64-apple-darwin',
|
||||
]);
|
||||
});
|
||||
|
||||
test('composite uses controlled argv/env and atomically retains outputs',
|
||||
() async {
|
||||
CompositeProcessInvocation? invocation;
|
||||
final outputRoot = path.join(temp.path, 'output');
|
||||
final builder = CompositeArtifactBuilder(
|
||||
workspaceRoot: workspace.path,
|
||||
generationHash: 'generation-hash',
|
||||
targetOutputRoot: targetRoot.path,
|
||||
outputRoot: outputRoot,
|
||||
currentHost: CompositeHost.macos,
|
||||
processRunner: (value) async {
|
||||
invocation = value;
|
||||
final output = Directory(value.environment['CARGOKIT_OUTPUT_ROOT']!);
|
||||
File(path.join(output.path, 'bundle.zip')).writeAsBytesSync([1, 2, 3]);
|
||||
File(path.join(output.path, 'bundle.zip.checksum'))
|
||||
.writeAsStringSync('checksum\n');
|
||||
return const CompositeProcessResult(exitCode: 0);
|
||||
},
|
||||
);
|
||||
|
||||
final result = await builder.build(group);
|
||||
|
||||
expect(invocation!.executable, path.join(workspace.path, 'assemble.sh'));
|
||||
expect(invocation!.arguments, [r'$literal']);
|
||||
expect(invocation!.workingDirectory, workspace.path);
|
||||
expect(invocation!.timeout, const Duration(seconds: 3));
|
||||
expect(invocation!.environment, {
|
||||
'LC_ALL': 'C',
|
||||
'PATH': '/usr/bin:/bin',
|
||||
'CARGOKIT_GENERATION_HASH': 'generation-hash',
|
||||
'CARGOKIT_WORKSPACE_ROOT': workspace.path,
|
||||
'CARGOKIT_TARGET_OUTPUT_ROOT': targetRoot.path,
|
||||
'CARGOKIT_OUTPUT_ROOT': isNotEmpty,
|
||||
'CARGOKIT_DART_EXECUTABLE': Platform.resolvedExecutable,
|
||||
});
|
||||
expect(File(result.outputs['bundle.zip']!).readAsBytesSync(), [1, 2, 3]);
|
||||
expect(
|
||||
Directory(outputRoot).listSync(recursive: true).where(
|
||||
(entity) => path.basename(entity.path).startsWith('.swiftpm.')),
|
||||
isEmpty,
|
||||
);
|
||||
});
|
||||
|
||||
test('composite rejects host/target, timeout, nonzero, and missing output',
|
||||
() async {
|
||||
Future<void> expectFailure(
|
||||
String name,
|
||||
CompositeProcessResult result,
|
||||
) async {
|
||||
final builder = CompositeArtifactBuilder(
|
||||
workspaceRoot: workspace.path,
|
||||
generationHash: 'generation-hash',
|
||||
targetOutputRoot: targetRoot.path,
|
||||
outputRoot: path.join(temp.path, name),
|
||||
currentHost: CompositeHost.macos,
|
||||
processRunner: (_) async => result,
|
||||
);
|
||||
await expectLater(
|
||||
builder.build(group),
|
||||
throwsA(isA<CompositeArtifactException>()),
|
||||
);
|
||||
}
|
||||
|
||||
final wrongHost = CompositeArtifactBuilder(
|
||||
workspaceRoot: workspace.path,
|
||||
generationHash: 'generation-hash',
|
||||
targetOutputRoot: targetRoot.path,
|
||||
outputRoot: path.join(temp.path, 'wrong-host'),
|
||||
currentHost: CompositeHost.linux,
|
||||
);
|
||||
await expectLater(
|
||||
wrongHost.build(group),
|
||||
throwsA(isA<CompositeArtifactException>()),
|
||||
);
|
||||
Directory(path.join(targetRoot.path, 'aarch64-apple-ios'))
|
||||
.deleteSync(recursive: true);
|
||||
final missingTarget = CompositeArtifactBuilder(
|
||||
workspaceRoot: workspace.path,
|
||||
generationHash: 'generation-hash',
|
||||
targetOutputRoot: targetRoot.path,
|
||||
outputRoot: path.join(temp.path, 'missing-target'),
|
||||
currentHost: CompositeHost.macos,
|
||||
);
|
||||
await expectLater(
|
||||
missingTarget.build(group),
|
||||
throwsA(isA<CompositeArtifactException>()),
|
||||
);
|
||||
Directory(path.join(targetRoot.path, 'aarch64-apple-ios')).createSync();
|
||||
|
||||
await expectFailure(
|
||||
'timeout',
|
||||
const CompositeProcessResult(exitCode: -1, timedOut: true),
|
||||
);
|
||||
await expectFailure(
|
||||
'nonzero',
|
||||
const CompositeProcessResult(exitCode: 9, stderr: 'failed'),
|
||||
);
|
||||
await expectFailure(
|
||||
'missing-output',
|
||||
const CompositeProcessResult(exitCode: 0),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'local generation builds targets once, retains both forms, and is deterministic',
|
||||
() async {
|
||||
_writeCrate(workspace);
|
||||
final buildCalls = <String, int>{};
|
||||
var compositeCalls = 0;
|
||||
|
||||
Future<String> buildTarget(
|
||||
Target target,
|
||||
BuildEnvironment environment,
|
||||
String toolchain,
|
||||
) async {
|
||||
expect(toolchain, '1.88.0');
|
||||
buildCalls.update(target.rust, (value) => value + 1, ifAbsent: () => 1);
|
||||
final output =
|
||||
Directory(path.join(environment.targetTempDir, target.rust))
|
||||
..createSync(recursive: true);
|
||||
File(path.join(output.path, 'libfjs.a'))
|
||||
.writeAsStringSync('static:${target.rust}');
|
||||
File(path.join(output.path, 'libfjs.dylib'))
|
||||
.writeAsStringSync('dynamic:${target.rust}');
|
||||
return output.path;
|
||||
}
|
||||
|
||||
Future<CompositeProcessResult> assemble(
|
||||
CompositeProcessInvocation invocation) async {
|
||||
compositeCalls++;
|
||||
final output = Directory(invocation.environment['CARGOKIT_OUTPUT_ROOT']!);
|
||||
File(path.join(output.path, 'bundle.zip')).writeAsStringSync('archive');
|
||||
File(path.join(output.path, 'bundle.zip.checksum'))
|
||||
.writeAsStringSync('checksum\n');
|
||||
return const CompositeProcessResult(exitCode: 0);
|
||||
}
|
||||
|
||||
final output = path.join(temp.path, 'generation');
|
||||
final generator = LocalPrecompiledGeneration(
|
||||
manifestDir: workspace.path,
|
||||
outputDir: output,
|
||||
tempDir: path.join(temp.path, 'build'),
|
||||
targetTriples: const [
|
||||
'aarch64-apple-ios',
|
||||
'aarch64-apple-darwin',
|
||||
],
|
||||
targetBuilder: buildTarget,
|
||||
currentHost: CompositeHost.macos,
|
||||
compositeProcessRunner: assemble,
|
||||
);
|
||||
|
||||
final firstFragment = await generator.run();
|
||||
final firstBytes = firstFragment.readAsBytesSync();
|
||||
expect(buildCalls, {
|
||||
'aarch64-apple-ios': 1,
|
||||
'aarch64-apple-darwin': 1,
|
||||
});
|
||||
expect(compositeCalls, 1);
|
||||
final secondFragment = await generator.run();
|
||||
|
||||
expect(buildCalls, {
|
||||
'aarch64-apple-ios': 2,
|
||||
'aarch64-apple-darwin': 2,
|
||||
});
|
||||
expect(compositeCalls, 2);
|
||||
expect(secondFragment.readAsBytesSync(), firstBytes);
|
||||
for (final target in buildCalls.keys) {
|
||||
expect(
|
||||
File(path.join(output, 'targets', target, '${target}_libfjs.a'))
|
||||
.existsSync(),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
File(path.join(output, 'targets', target, '${target}_libfjs.dylib'))
|
||||
.existsSync(),
|
||||
isTrue,
|
||||
);
|
||||
}
|
||||
final fragment =
|
||||
jsonDecode(utf8.decode(firstBytes)) as Map<String, dynamic>;
|
||||
expect(fragment['schema_version'], 1);
|
||||
expect(fragment['scope'], 'cargokit-local-precompiled-generation');
|
||||
expect(fragment['generation_hash'], matches(RegExp(r'^[0-9a-f]{64}$')));
|
||||
expect((fragment['recipe'] as Map)['rust_toolchain'], '1.88.0');
|
||||
expect(fragment['assets'], hasLength(6));
|
||||
expect(fragment.containsKey('signature'), isFalse);
|
||||
});
|
||||
|
||||
test('local generation rejects duplicate, unknown, and unpinned targets',
|
||||
() async {
|
||||
_writeCrate(workspace);
|
||||
|
||||
Future<void> rejects(List<String> targets, String suffix) async {
|
||||
final generator = LocalPrecompiledGeneration(
|
||||
manifestDir: workspace.path,
|
||||
outputDir: path.join(temp.path, 'invalid-$suffix'),
|
||||
tempDir: path.join(temp.path, 'invalid-build-$suffix'),
|
||||
targetTriples: targets,
|
||||
targetBuilder: (_, __, ___) => throw StateError('must not build'),
|
||||
currentHost: CompositeHost.macos,
|
||||
);
|
||||
await expectLater(
|
||||
generator.run(),
|
||||
throwsA(isA<ArgumentError>()),
|
||||
);
|
||||
}
|
||||
|
||||
await rejects(
|
||||
const ['aarch64-apple-ios', 'aarch64-apple-ios'],
|
||||
'duplicate',
|
||||
);
|
||||
await rejects(const ['invented-target'], 'unknown');
|
||||
await rejects(const ['x86_64-apple-ios'], 'unpinned');
|
||||
await rejects(const ['aarch64-apple-ios'], 'incomplete-composite');
|
||||
});
|
||||
}
|
||||
|
||||
void _writeCrate(Directory workspace) {
|
||||
File(path.join(workspace.path, 'Cargo.toml')).writeAsStringSync('''
|
||||
[package]
|
||||
name = "fjs"
|
||||
version = "1.0.0"
|
||||
''');
|
||||
Directory(path.join(workspace.path, 'src')).createSync();
|
||||
File(path.join(workspace.path, 'src/lib.rs'))
|
||||
.writeAsStringSync('pub fn fjs() {}\n');
|
||||
File(path.join(workspace.path, 'cargokit.yaml')).writeAsStringSync('''
|
||||
precompiled_binaries:
|
||||
url_prefix: https://example.com/precompiled_
|
||||
public_key: a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445
|
||||
workspace_root: .
|
||||
build_recipe:
|
||||
rust_toolchain: '1.88.0'
|
||||
flutter_version: '3.32.8'
|
||||
xcode_version: '16.4'
|
||||
sdk_versions:
|
||||
iphoneos: '18.5'
|
||||
macosx: '15.5'
|
||||
deployment_targets:
|
||||
ios: '12.0'
|
||||
macos: '10.14'
|
||||
rust_targets:
|
||||
- aarch64-apple-ios
|
||||
- aarch64-apple-darwin
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
- aarch64-apple-darwin
|
||||
argv:
|
||||
- assemble.sh
|
||||
- --retained
|
||||
environment:
|
||||
LC_ALL: C
|
||||
PATH: /usr/bin:/bin
|
||||
timeout_seconds: 3
|
||||
outputs:
|
||||
- bundle.zip
|
||||
- bundle.zip.checksum
|
||||
''');
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:build_tool/src/crate_hash.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
const _publicKey =
|
||||
'a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445';
|
||||
|
||||
void main() {
|
||||
late Directory temporaryDirectory;
|
||||
late Directory workspace;
|
||||
late Directory manifest;
|
||||
|
||||
setUp(() {
|
||||
final resolvedSystemTemp =
|
||||
Directory(Directory.systemTemp.resolveSymbolicLinksSync());
|
||||
temporaryDirectory = resolvedSystemTemp.createTempSync('crate_hash_test.');
|
||||
workspace = Directory(path.join(temporaryDirectory.path, 'workspace'))
|
||||
..createSync();
|
||||
manifest = Directory(path.join(workspace.path, 'crate'))..createSync();
|
||||
_write(manifest, 'Cargo.toml', '[package]\nname = "test_crate"\n');
|
||||
_write(manifest, 'src/lib.rs', 'pub fn answer() -> i32 { 42 }\n');
|
||||
_writeConfig(manifest);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
temporaryDirectory.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
test('existing Rust input names and contents affect the hash', () {
|
||||
final original = CrateHash.compute(manifest.path);
|
||||
|
||||
_write(manifest, 'src/lib.rs', 'pub fn answer() -> i32 { 43 }\n');
|
||||
final contentChanged = CrateHash.compute(manifest.path);
|
||||
expect(contentChanged, isNot(original));
|
||||
|
||||
final source = File(path.join(manifest.path, 'src/lib.rs'));
|
||||
source.renameSync(path.join(manifest.path, 'src/renamed.rs'));
|
||||
final pathChanged = CrateHash.compute(manifest.path);
|
||||
expect(pathChanged, isNot(contentChanged));
|
||||
});
|
||||
|
||||
const textProducerPaths = [
|
||||
'producer/source.rs',
|
||||
'producer/Cargo.toml',
|
||||
'producer/config.yaml',
|
||||
'producer/config.yml',
|
||||
'producer/source.dart',
|
||||
'producer/build.sh',
|
||||
'producer/Plugin.swift',
|
||||
'producer/data.json',
|
||||
'producer/README.md',
|
||||
'producer/Cargo.lock',
|
||||
];
|
||||
for (final producerPath in textProducerPaths) {
|
||||
test('normalizes Git newlines in $producerPath', () {
|
||||
_write(workspace, producerPath, 'first line\nsecond line\n');
|
||||
_writeConfig(manifest, hashInputs: [producerPath]);
|
||||
final lfHash = CrateHash.compute(manifest.path);
|
||||
|
||||
_write(workspace, producerPath, 'first line\r\nsecond line\r\n');
|
||||
|
||||
expect(CrateHash.compute(manifest.path), lfHash);
|
||||
});
|
||||
}
|
||||
|
||||
test('meaningful text changes affect the canonical hash', () {
|
||||
const producerPath = 'producer/source.dart';
|
||||
_write(workspace, producerPath, 'const answer = 42;\n');
|
||||
_writeConfig(manifest, hashInputs: const [producerPath]);
|
||||
final original = CrateHash.compute(manifest.path);
|
||||
|
||||
_write(workspace, producerPath, 'const answer = 43;\r\n');
|
||||
|
||||
expect(CrateHash.compute(manifest.path), isNot(original));
|
||||
});
|
||||
|
||||
test('binary inputs preserve CRLF and invalid UTF-8 bytes', () {
|
||||
const producerPath = 'producer/artifact.bin';
|
||||
final producer = File(path.join(workspace.path, producerPath));
|
||||
producer.parent.createSync(recursive: true);
|
||||
producer.writeAsBytesSync([0xff, 0x0d, 0x0a]);
|
||||
_writeConfig(manifest, hashInputs: const [producerPath]);
|
||||
final crlfHash = CrateHash.compute(manifest.path);
|
||||
|
||||
producer.writeAsBytesSync([0xff, 0x0a]);
|
||||
|
||||
expect(CrateHash.compute(manifest.path), isNot(crlfHash));
|
||||
});
|
||||
|
||||
test('declared files and directories are hashed recursively', () {
|
||||
_write(workspace, 'producer.txt', 'producer-v1');
|
||||
_write(workspace, 'tool/nested/build.sh', 'build-v1');
|
||||
_writeConfig(
|
||||
manifest,
|
||||
hashInputs: const ['producer.txt', 'tool'],
|
||||
);
|
||||
final original = CrateHash.compute(manifest.path);
|
||||
|
||||
_write(workspace, 'producer.txt', 'producer-v2');
|
||||
final fileChanged = CrateHash.compute(manifest.path);
|
||||
expect(fileChanged, isNot(original));
|
||||
|
||||
_write(workspace, 'tool/nested/build.sh', 'build-v2');
|
||||
final nestedFileChanged = CrateHash.compute(manifest.path);
|
||||
expect(nestedFileChanged, isNot(fileChanged));
|
||||
|
||||
File(path.join(workspace.path, 'tool/nested/build.sh')).renameSync(
|
||||
path.join(workspace.path, 'tool/nested/assemble.sh'),
|
||||
);
|
||||
final nestedPathChanged = CrateHash.compute(manifest.path);
|
||||
expect(nestedPathChanged, isNot(nestedFileChanged));
|
||||
});
|
||||
|
||||
test('adding empty nested declared directories affects the hash', () {
|
||||
Directory(path.join(workspace.path, 'tool')).createSync();
|
||||
_writeConfig(manifest, hashInputs: const ['tool']);
|
||||
final original = CrateHash.compute(manifest.path);
|
||||
|
||||
Directory(path.join(workspace.path, 'tool/nested/empty'))
|
||||
.createSync(recursive: true);
|
||||
|
||||
expect(CrateHash.compute(manifest.path), isNot(original));
|
||||
});
|
||||
|
||||
test('removing empty nested declared directories affects the hash', () {
|
||||
final nested = Directory(path.join(workspace.path, 'tool/nested/empty'))
|
||||
..createSync(recursive: true);
|
||||
_writeConfig(manifest, hashInputs: const ['tool']);
|
||||
final original = CrateHash.compute(manifest.path);
|
||||
|
||||
nested.parent.deleteSync(recursive: true);
|
||||
|
||||
expect(CrateHash.compute(manifest.path), isNot(original));
|
||||
});
|
||||
|
||||
test('renaming empty nested declared directories affects the hash', () {
|
||||
final empty = Directory(path.join(workspace.path, 'tool/nested/empty'))
|
||||
..createSync(recursive: true);
|
||||
_writeConfig(manifest, hashInputs: const ['tool']);
|
||||
final original = CrateHash.compute(manifest.path);
|
||||
|
||||
empty.renameSync(path.join(workspace.path, 'tool/nested/renamed'));
|
||||
|
||||
expect(CrateHash.compute(manifest.path), isNot(original));
|
||||
});
|
||||
|
||||
test('undeclared workspace files do not affect the hash', () {
|
||||
_write(workspace, 'producer.txt', 'producer');
|
||||
_writeConfig(manifest, hashInputs: const ['producer.txt']);
|
||||
final original = CrateHash.compute(manifest.path);
|
||||
|
||||
_write(workspace, 'unrelated.txt', 'first');
|
||||
expect(CrateHash.compute(manifest.path), original);
|
||||
|
||||
_write(workspace, 'unrelated.txt', 'second');
|
||||
expect(CrateHash.compute(manifest.path), original);
|
||||
});
|
||||
|
||||
test('pinned build recipe changes affect the hash', () {
|
||||
_writeConfig(manifest, rustToolchain: '1.88.0');
|
||||
final original = CrateHash.compute(manifest.path);
|
||||
|
||||
_writeConfig(manifest, rustToolchain: '1.89.0');
|
||||
expect(CrateHash.compute(manifest.path), isNot(original));
|
||||
});
|
||||
|
||||
test('temp storage compatibility does not create persistent markers', () {
|
||||
final cachePath = path.join(temporaryDirectory.path, 'cache');
|
||||
final source = File(path.join(manifest.path, 'src/lib.rs'));
|
||||
final original = CrateHash.compute(manifest.path, tempStorage: cachePath);
|
||||
expect(CrateHash.compute(manifest.path, tempStorage: cachePath), original);
|
||||
|
||||
source.writeAsStringSync('pub fn answer() -> i32 { 43 }\n');
|
||||
expect(CrateHash.compute(manifest.path, tempStorage: cachePath),
|
||||
isNot(original));
|
||||
expect(FileSystemEntity.typeSync(cachePath), FileSystemEntityType.notFound);
|
||||
});
|
||||
|
||||
test('missing declared inputs fail closed', () {
|
||||
_writeConfig(manifest, hashInputs: const ['missing.txt']);
|
||||
|
||||
expect(
|
||||
() => CrateHash.compute(manifest.path),
|
||||
throwsA(isA<FileSystemException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('declared symlinks fail closed', () {
|
||||
_write(workspace, 'producer.txt', 'producer');
|
||||
Link(path.join(workspace.path, 'producer-link'))
|
||||
.createSync(path.join(workspace.path, 'producer.txt'));
|
||||
_writeConfig(manifest, hashInputs: const ['producer-link']);
|
||||
|
||||
expect(
|
||||
() => CrateHash.compute(manifest.path),
|
||||
throwsA(isA<FileSystemException>()),
|
||||
);
|
||||
},
|
||||
skip: Platform.isWindows
|
||||
? 'Creating symlinks requires privileges.'
|
||||
: false);
|
||||
|
||||
test('workspace root must contain the crate manifest', () {
|
||||
_writeConfig(manifest, workspaceRoot: '../other');
|
||||
|
||||
expect(
|
||||
() => CrateHash.compute(manifest.path),
|
||||
throwsA(isA<FileSystemException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('intermediate workspace root symlinks fail closed', () {
|
||||
final realWorkspace =
|
||||
Directory(path.join(temporaryDirectory.path, 'real/workspace'))
|
||||
..createSync(recursive: true);
|
||||
final realManifest = Directory(path.join(realWorkspace.path, 'crate'))
|
||||
..createSync();
|
||||
_write(realManifest, 'Cargo.toml', '[package]\nname = "linked_crate"\n');
|
||||
_write(realManifest, 'src/lib.rs', 'pub fn linked() {}\n');
|
||||
Link(path.join(temporaryDirectory.path, 'workspace-link'))
|
||||
.createSync(path.join(temporaryDirectory.path, 'real'));
|
||||
_writeConfig(
|
||||
realManifest,
|
||||
workspaceRoot: '../../../workspace-link/workspace',
|
||||
);
|
||||
|
||||
expect(
|
||||
() => CrateHash.compute(realManifest.path),
|
||||
throwsA(isA<FileSystemException>()),
|
||||
);
|
||||
},
|
||||
skip: Platform.isWindows
|
||||
? 'Creating symlinks requires privileges.'
|
||||
: false);
|
||||
|
||||
test('intermediate manifest symlinks fail closed', () {
|
||||
final realWorkspace =
|
||||
Directory(path.join(temporaryDirectory.path, 'real/workspace'))
|
||||
..createSync(recursive: true);
|
||||
final realManifest = Directory(path.join(realWorkspace.path, 'crate'))
|
||||
..createSync();
|
||||
_write(realManifest, 'Cargo.toml', '[package]\nname = "linked_crate"\n');
|
||||
_write(realManifest, 'src/lib.rs', 'pub fn linked() {}\n');
|
||||
_writeConfig(realManifest, workspaceRoot: '../..');
|
||||
Link(path.join(temporaryDirectory.path, 'workspace-link'))
|
||||
.createSync(realWorkspace.path);
|
||||
final linkedManifest =
|
||||
Directory(path.join(temporaryDirectory.path, 'workspace-link/crate'));
|
||||
|
||||
expect(
|
||||
() => CrateHash.compute(linkedManifest.path),
|
||||
throwsA(isA<FileSystemException>()),
|
||||
);
|
||||
},
|
||||
skip: Platform.isWindows
|
||||
? 'Creating symlinks requires privileges.'
|
||||
: false);
|
||||
}
|
||||
|
||||
void _write(Directory root, String relativePath, String contents) {
|
||||
final file = File(path.join(root.path, relativePath));
|
||||
file.parent.createSync(recursive: true);
|
||||
file.writeAsStringSync(contents);
|
||||
}
|
||||
|
||||
void _writeConfig(
|
||||
Directory manifest, {
|
||||
String workspaceRoot = '..',
|
||||
List<String> hashInputs = const [],
|
||||
String? rustToolchain,
|
||||
}) {
|
||||
final recipe = rustToolchain == null
|
||||
? ''
|
||||
: '''
|
||||
build_recipe:
|
||||
rust_toolchain: '$rustToolchain'
|
||||
flutter_version: '3.32.8'
|
||||
xcode_version: '16.4'
|
||||
sdk_versions:
|
||||
iphoneos: '18.5'
|
||||
deployment_targets:
|
||||
ios: '12.0'
|
||||
rust_targets:
|
||||
- aarch64-apple-ios
|
||||
''';
|
||||
final inputs = hashInputs.isEmpty
|
||||
? ''
|
||||
: 'hash_inputs:\n${hashInputs.map((input) => ' - $input').join('\n')}\n';
|
||||
_write(manifest, 'cargokit.yaml', '''
|
||||
precompiled_binaries:
|
||||
url_prefix: https://example.com/precompiled_
|
||||
public_key: $_publicKey
|
||||
workspace_root: $workspaceRoot
|
||||
${_indent(inputs, 2)}${_indent(recipe, 2)}''');
|
||||
}
|
||||
|
||||
String _indent(String value, int spaces) {
|
||||
if (value.isEmpty) {
|
||||
return value;
|
||||
}
|
||||
final prefix = ' ' * spaces;
|
||||
return value
|
||||
.split('\n')
|
||||
.map((line) => line.isEmpty ? line : '$prefix$line')
|
||||
.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:build_tool/src/darwin_environment.dart';
|
||||
import 'package:build_tool/src/target.dart';
|
||||
import 'package:build_tool/src/util.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
testRunCommandOverride = (args) {
|
||||
if (args.executable != 'xcrun') {
|
||||
throw StateError('Unexpected executable: ${args.executable}');
|
||||
}
|
||||
|
||||
final sdk = args.arguments[1];
|
||||
if (args.arguments[2] == '--show-sdk-path') {
|
||||
return TestRunCommandResult(stdout: '/sdk/$sdk\n');
|
||||
}
|
||||
if (args.arguments[2] == '--find') {
|
||||
final tool = args.arguments[3];
|
||||
return TestRunCommandResult(stdout: '/toolchain/$sdk/$tool\n');
|
||||
}
|
||||
|
||||
throw StateError('Unexpected xcrun args: ${args.arguments}');
|
||||
};
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
testRunCommandOverride = null;
|
||||
});
|
||||
|
||||
test('builds simulator environment for arm64 ios sim target', () {
|
||||
final env = DarwinEnvironment(
|
||||
target: Target.forRustTriple('aarch64-apple-ios-sim')!,
|
||||
deploymentTarget: '12.0',
|
||||
).buildEnvironment();
|
||||
|
||||
expect(env['SDKROOT'], '/sdk/iphonesimulator');
|
||||
expect(env['CC_aarch64-apple-ios-sim'], '/toolchain/iphonesimulator/clang');
|
||||
expect(
|
||||
env['CFLAGS_aarch64-apple-ios-sim'],
|
||||
'--target=arm64-apple-ios-simulator -isysroot /sdk/iphonesimulator -mios-simulator-version-min=12.0',
|
||||
);
|
||||
expect(
|
||||
env['BINDGEN_EXTRA_CLANG_ARGS_aarch64-apple-ios-sim'],
|
||||
'--target=arm64-apple-ios-simulator -isysroot /sdk/iphonesimulator -mios-simulator-version-min=12.0',
|
||||
);
|
||||
});
|
||||
|
||||
test('builds simulator environment for x86_64 ios target', () {
|
||||
final env = DarwinEnvironment(
|
||||
target: Target.forRustTriple('x86_64-apple-ios')!,
|
||||
deploymentTarget: '12.0',
|
||||
).buildEnvironment();
|
||||
|
||||
expect(env['SDKROOT'], '/sdk/iphonesimulator');
|
||||
expect(
|
||||
env['CFLAGS_x86_64-apple-ios'],
|
||||
'--target=x86_64-apple-ios-simulator -isysroot /sdk/iphonesimulator -mios-simulator-version-min=12.0',
|
||||
);
|
||||
});
|
||||
|
||||
test('builds device environment for arm64 ios target', () {
|
||||
final env = DarwinEnvironment(
|
||||
target: Target.forRustTriple('aarch64-apple-ios')!,
|
||||
deploymentTarget: '12.0',
|
||||
).buildEnvironment();
|
||||
|
||||
expect(env['SDKROOT'], '/sdk/iphoneos');
|
||||
expect(
|
||||
env['CFLAGS_aarch64-apple-ios'],
|
||||
'--target=arm64-apple-ios -isysroot /sdk/iphoneos -miphoneos-version-min=12.0',
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
import 'package:build_tool/src/builder.dart';
|
||||
import 'package:build_tool/src/options.dart';
|
||||
import 'package:build_tool/src/rustup.dart';
|
||||
import 'package:build_tool/src/util.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
void main() {
|
||||
tearDown(() {
|
||||
testRunCommandOverride = null;
|
||||
testRustupExecutablePathOverride = null;
|
||||
});
|
||||
|
||||
test('parse cargo build options', () {
|
||||
final yaml = '''
|
||||
toolchain: nightly
|
||||
extra_flags:
|
||||
- -Z
|
||||
- build-std=panic_abort,std
|
||||
''';
|
||||
|
||||
final options = CargoBuildOptions.parse(loadYamlNode(yaml));
|
||||
|
||||
expect(options.toolchain, Toolchain.nightly);
|
||||
expect(options.flags, ['-Z', 'build-std=panic_abort,std']);
|
||||
});
|
||||
|
||||
test('parse precompiled binaries config', () {
|
||||
final yaml = '''
|
||||
url_prefix: https://example.com/precompiled_
|
||||
public_key: a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445
|
||||
''';
|
||||
|
||||
final config = PrecompiledBinaries.parse(loadYamlNode(yaml));
|
||||
|
||||
expect(config.uriPrefix, 'https://example.com/precompiled_');
|
||||
expect(
|
||||
config.publicKey.bytes,
|
||||
HEX.decode(
|
||||
'a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('parse complete precompiled generation config', () {
|
||||
final yaml = '''
|
||||
url_prefix: https://example.com/precompiled_
|
||||
public_key: a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445
|
||||
workspace_root: ..
|
||||
hash_inputs:
|
||||
- pubspec.yaml
|
||||
- tool
|
||||
build_recipe:
|
||||
rust_toolchain: '1.88.0'
|
||||
flutter_version: '3.32.8'
|
||||
xcode_version: '16.4'
|
||||
sdk_versions:
|
||||
iphoneos: '18.5'
|
||||
macosx: '15.5'
|
||||
deployment_targets:
|
||||
ios: '12.0'
|
||||
macos: '10.14'
|
||||
rust_targets:
|
||||
- aarch64-apple-ios
|
||||
- aarch64-apple-darwin
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
- aarch64-apple-darwin
|
||||
argv:
|
||||
- tool/assemble_fjs_xcframework.sh
|
||||
- --release
|
||||
environment:
|
||||
LC_ALL: C
|
||||
timeout_seconds: 900
|
||||
outputs:
|
||||
- fjs.xcframework.zip
|
||||
- fjs.xcframework.zip.checksum
|
||||
''';
|
||||
|
||||
final config = PrecompiledBinaries.parse(loadYamlNode(yaml));
|
||||
|
||||
expect(config.workspaceRoot, '..');
|
||||
expect(config.hashInputs, ['pubspec.yaml', 'tool']);
|
||||
expect(config.buildRecipe?.rustToolchain, '1.88.0');
|
||||
expect(config.buildRecipe?.flutterVersion, '3.32.8');
|
||||
expect(config.buildRecipe?.xcodeVersion, '16.4');
|
||||
expect(config.buildRecipe?.sdkVersions,
|
||||
{'iphoneos': '18.5', 'macosx': '15.5'});
|
||||
expect(config.buildRecipe?.deploymentTargets,
|
||||
{'ios': '12.0', 'macos': '10.14'});
|
||||
expect(config.buildRecipe?.rustTargets,
|
||||
['aarch64-apple-ios', 'aarch64-apple-darwin']);
|
||||
expect(config.compositeGroups, hasLength(1));
|
||||
final group = config.compositeGroups.single;
|
||||
expect(group.name, 'swiftpm');
|
||||
expect(group.host, CompositeHost.macos);
|
||||
expect(
|
||||
group.requiredTargets, ['aarch64-apple-ios', 'aarch64-apple-darwin']);
|
||||
expect(group.argv, ['tool/assemble_fjs_xcframework.sh', '--release']);
|
||||
expect(group.environment, {'LC_ALL': 'C'});
|
||||
expect(group.timeout, const Duration(seconds: 900));
|
||||
expect(
|
||||
group.outputs, ['fjs.xcframework.zip', 'fjs.xcframework.zip.checksum']);
|
||||
});
|
||||
|
||||
group('precompiled generation config rejects', () {
|
||||
const publicKey =
|
||||
'a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445';
|
||||
|
||||
String config(String body) => '''
|
||||
url_prefix: https://example.com/precompiled_
|
||||
public_key: $publicKey
|
||||
$body
|
||||
''';
|
||||
|
||||
void expectInvalid(String body) {
|
||||
expect(
|
||||
() => PrecompiledBinaries.parse(loadYamlNode(config(body))),
|
||||
throwsA(isA<SourceSpanException>()),
|
||||
);
|
||||
}
|
||||
|
||||
test('unknown fields', () {
|
||||
expectInvalid('unexpected: true');
|
||||
expectInvalid('''
|
||||
build_recipe:
|
||||
rust_toolchain: 1.88.0
|
||||
flutter_version: 3.32.8
|
||||
xcode_version: 16.4
|
||||
sdk_versions: {}
|
||||
deployment_targets: {}
|
||||
rust_targets:
|
||||
- aarch64-apple-ios
|
||||
unexpected: true
|
||||
''');
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
unexpected: true
|
||||
''');
|
||||
});
|
||||
|
||||
test('absolute and escaping paths', () {
|
||||
expectInvalid('workspace_root: /tmp/workspace');
|
||||
expectInvalid('''
|
||||
hash_inputs:
|
||||
- ../outside
|
||||
''');
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- /tmp/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
''');
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- ../build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
''');
|
||||
});
|
||||
|
||||
test('invalid hosts and targets', () {
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: darwin
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
''');
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- invented-rust-target
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
''');
|
||||
});
|
||||
|
||||
test('empty commands and outputs and invalid timeouts', () {
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv: []
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
''');
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs: []
|
||||
''');
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 0
|
||||
outputs:
|
||||
- output.zip
|
||||
''');
|
||||
});
|
||||
|
||||
test('unsafe and duplicate output names', () {
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- ../output.zip
|
||||
''');
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: first
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
- name: second
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-darwin
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
''');
|
||||
});
|
||||
|
||||
test('duplicate groups and targets', () {
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- first.zip
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-darwin
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- second.zip
|
||||
''');
|
||||
expectInvalid('''
|
||||
composite_groups:
|
||||
- name: swiftpm
|
||||
host: macos
|
||||
required_targets:
|
||||
- aarch64-apple-ios
|
||||
- aarch64-apple-ios
|
||||
argv:
|
||||
- tool/build.sh
|
||||
timeout_seconds: 60
|
||||
outputs:
|
||||
- output.zip
|
||||
''');
|
||||
});
|
||||
});
|
||||
|
||||
test('parse crate options with cargo and precompiled binaries', () {
|
||||
final yaml = '''
|
||||
cargo:
|
||||
debug:
|
||||
toolchain: nightly
|
||||
extra_flags:
|
||||
- -Z
|
||||
- build-std=panic_abort,std
|
||||
release:
|
||||
toolchain: beta
|
||||
|
||||
precompiled_binaries:
|
||||
url_prefix: https://example.com/precompiled_
|
||||
public_key: a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445
|
||||
''';
|
||||
|
||||
final options = CargokitCrateOptions.parse(loadYamlNode(yaml));
|
||||
|
||||
expect(
|
||||
options.cargo[BuildConfiguration.debug]!.toolchain, Toolchain.nightly);
|
||||
expect(
|
||||
options.cargo[BuildConfiguration.debug]!.flags,
|
||||
['-Z', 'build-std=panic_abort,std'],
|
||||
);
|
||||
expect(
|
||||
options.cargo[BuildConfiguration.release]!.toolchain, Toolchain.beta);
|
||||
expect(options.precompiledBinaries?.uriPrefix,
|
||||
'https://example.com/precompiled_');
|
||||
});
|
||||
|
||||
test('default user options build locally when rustup is available', () {
|
||||
testRustupExecutablePathOverride = () => '/usr/bin/rustup';
|
||||
|
||||
final options = CargokitUserOptions.parse(loadYamlNode('{}'));
|
||||
|
||||
expect(options.usePrecompiledBinaries, false);
|
||||
expect(options.verboseLogging, false);
|
||||
});
|
||||
|
||||
test('default user options use precompiled binaries when rustup is missing',
|
||||
() {
|
||||
testRustupExecutablePathOverride = () => null;
|
||||
|
||||
final options = CargokitUserOptions.parse(loadYamlNode('{}'));
|
||||
|
||||
expect(options.usePrecompiledBinaries, true);
|
||||
expect(options.verboseLogging, false);
|
||||
});
|
||||
|
||||
test('explicit user options override default precompiled binary behavior',
|
||||
() {
|
||||
final options = CargokitUserOptions.parse(loadYamlNode('''
|
||||
use_precompiled_binaries: true
|
||||
verbose_logging: true
|
||||
'''));
|
||||
|
||||
expect(options.usePrecompiledBinaries, true);
|
||||
expect(options.verboseLogging, true);
|
||||
});
|
||||
|
||||
test('parse precompiled binary modes', () {
|
||||
for (final mode in PrecompiledBinariesMode.values) {
|
||||
final options = CargokitUserOptions.parse(loadYamlNode('''
|
||||
precompiled_binaries_mode: ${mode.name}
|
||||
'''));
|
||||
|
||||
expect(options.precompiledBinariesMode, mode);
|
||||
expect(options.usePrecompiledBinaries,
|
||||
mode != PrecompiledBinariesMode.disabled);
|
||||
expect(options.allowLocalBuild, mode != PrecompiledBinariesMode.required);
|
||||
}
|
||||
});
|
||||
|
||||
test('legacy precompiled boolean maps to auto or disabled', () {
|
||||
final enabled = CargokitUserOptions.parse(
|
||||
loadYamlNode('use_precompiled_binaries: true'));
|
||||
final disabled = CargokitUserOptions.parse(
|
||||
loadYamlNode('use_precompiled_binaries: false'));
|
||||
|
||||
expect(enabled.precompiledBinariesMode, PrecompiledBinariesMode.auto);
|
||||
expect(disabled.precompiledBinariesMode, PrecompiledBinariesMode.disabled);
|
||||
});
|
||||
|
||||
test('precompiled mode rejects legacy setting conflict', () {
|
||||
expect(
|
||||
() => CargokitUserOptions.parse(loadYamlNode('''
|
||||
precompiled_binaries_mode: required
|
||||
use_precompiled_binaries: true
|
||||
''')),
|
||||
throwsA(isA<SourceSpanException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('precompiled mode rejects unknown values', () {
|
||||
expect(
|
||||
() => CargokitUserOptions.parse(
|
||||
loadYamlNode('precompiled_binaries_mode: sometimes')),
|
||||
throwsA(isA<SourceSpanException>()),
|
||||
);
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,550 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:build_tool/src/options.dart';
|
||||
import 'package:build_tool/src/precompiled_generation.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
const _generationHash =
|
||||
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
const _archiveHash =
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
|
||||
const _checksumHash =
|
||||
'5c6449da335badb355097ca2a7898509bce8feebfe31dedce47798825fdd9a11';
|
||||
const _sourceCommit = '1111111111111111111111111111111111111111';
|
||||
|
||||
void main() {
|
||||
late KeyPair keyPair;
|
||||
late PrecompiledBuildRecipe recipe;
|
||||
|
||||
setUp(() {
|
||||
keyPair = KeyPair(
|
||||
newKeyFromSeed(Uint8List.fromList(List<int>.generate(32, (i) => i))),
|
||||
public(
|
||||
newKeyFromSeed(Uint8List.fromList(List<int>.generate(32, (i) => i)))),
|
||||
);
|
||||
recipe = PrecompiledBuildRecipe(
|
||||
rustToolchain: '1.88.0',
|
||||
flutterVersion: '3.32.8',
|
||||
xcodeVersion: '16.4',
|
||||
sdkVersions: const {'macosx': '15.5', 'iphoneos': '18.5'},
|
||||
deploymentTargets: const {'macos': '10.14', 'ios': '12.0'},
|
||||
rustTargets: const [
|
||||
'x86_64-apple-darwin',
|
||||
'aarch64-apple-darwin',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('canonical encoding is deterministic and recursively sorted', () {
|
||||
final first = _manifest(recipe, reverseAssets: true);
|
||||
final second = _manifest(recipe);
|
||||
|
||||
expect(first.canonicalBytes(), second.canonicalBytes());
|
||||
expect(
|
||||
utf8.decode(first.canonicalBytes()),
|
||||
'{"asset_signature_scheme":"ed25519-cargokit-v2",'
|
||||
'"assets":[{"length":3,"name":"aarch64-apple-darwin_libfjs.a",'
|
||||
'"sha256":"$_checksumHash"},{"length":4,"name":"fjs.xcframework.zip",'
|
||||
'"sha256":"$_archiveHash"},{"length":65,'
|
||||
'"name":"fjs.xcframework.zip.checksum","sha256":"$_checksumHash"}],'
|
||||
'"composite_checksums":[{"algorithm":"sha256",'
|
||||
'"archive":"fjs.xcframework.zip",'
|
||||
'"checksum":"fjs.xcframework.zip.checksum"}],'
|
||||
'"generation_hash":"$_generationHash","provenance":{"actual":'
|
||||
'{"deployment_targets":{"ios":"12.0","macos":"10.14"},'
|
||||
'"flutter_version":"3.32.8","rust_targets":'
|
||||
'["aarch64-apple-darwin","x86_64-apple-darwin"],'
|
||||
'"rust_toolchain":"1.88.0","sdk_versions":'
|
||||
'{"iphoneos":"18.5","macosx":"15.5"},"xcode_version":"16.4"},'
|
||||
'"pinned":{"deployment_targets":{"ios":"12.0","macos":"10.14"},'
|
||||
'"flutter_version":"3.32.8","rust_targets":'
|
||||
'["aarch64-apple-darwin","x86_64-apple-darwin"],'
|
||||
'"rust_toolchain":"1.88.0","sdk_versions":'
|
||||
'{"iphoneos":"18.5","macosx":"15.5"},"xcode_version":"16.4"}},'
|
||||
'"schema_version":2,"scope":"cargokit-precompiled-generation",'
|
||||
'"source_commit":"$_sourceCommit"}',
|
||||
);
|
||||
});
|
||||
|
||||
test('canonical manifest round trips through strict parsing', () {
|
||||
final manifest = _manifest(recipe);
|
||||
|
||||
final parsed =
|
||||
PrecompiledGenerationManifest.parse(manifest.canonicalBytes());
|
||||
|
||||
expect(parsed.canonicalBytes(), manifest.canonicalBytes());
|
||||
expect(parsed.asset('fjs.xcframework.zip')?.length, 4);
|
||||
});
|
||||
|
||||
group('strict manifest parsing rejects', () {
|
||||
test('unknown, missing, and duplicate fields', () {
|
||||
final valid = jsonDecode(utf8.decode(_manifest(recipe).canonicalBytes()))
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(
|
||||
() => _parse({...valid, 'unexpected': true}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
final missing = {...valid}..remove('scope');
|
||||
expect(
|
||||
() => _parse(missing),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => PrecompiledGenerationManifest.parse(utf8.encode(
|
||||
'{"schema_version":2,"schema_version":2}',
|
||||
)),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('v1, unsupported schema, scheme, and scope', () {
|
||||
final valid = jsonDecode(utf8.decode(_manifest(recipe).canonicalBytes()))
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(
|
||||
() => _parse({...valid, 'schema_version': 1}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({...valid, 'schema_version': 3}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({...valid, 'asset_signature_scheme': 'ed25519'}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({...valid, 'scope': 'github-release'}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('unsafe paths, malformed hashes, duplicates, and unsorted inventory',
|
||||
() {
|
||||
final valid = jsonDecode(utf8.decode(_manifest(recipe).canonicalBytes()))
|
||||
as Map<String, dynamic>;
|
||||
final assets = (valid['assets'] as List).cast<Map<String, dynamic>>();
|
||||
|
||||
expect(
|
||||
() => _parse({...valid, 'generation_hash': 'not-a-sha256'}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({
|
||||
...valid,
|
||||
'assets': [
|
||||
{...assets.first, 'name': '../escape'},
|
||||
...assets.skip(1),
|
||||
],
|
||||
}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({
|
||||
...valid,
|
||||
'assets': [assets.first, assets.first]
|
||||
}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({...valid, 'assets': assets.reversed.toList()}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('validation rejects incomplete inventory and provenance mismatch', () {
|
||||
final manifest = _manifest(recipe);
|
||||
|
||||
expect(
|
||||
() => manifest.validateFor(
|
||||
generationHash: _generationHash,
|
||||
recipe: recipe,
|
||||
expectedAssetNames: const {
|
||||
'aarch64-apple-darwin_libfjs.a',
|
||||
'x86_64-apple-darwin_libfjs.a',
|
||||
'fjs.xcframework.zip',
|
||||
'fjs.xcframework.zip.checksum',
|
||||
},
|
||||
),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
|
||||
final differentRecipe = PrecompiledBuildRecipe(
|
||||
rustToolchain: '1.89.0',
|
||||
flutterVersion: recipe.flutterVersion,
|
||||
xcodeVersion: recipe.xcodeVersion,
|
||||
sdkVersions: recipe.sdkVersions,
|
||||
deploymentTargets: recipe.deploymentTargets,
|
||||
rustTargets: recipe.rustTargets,
|
||||
);
|
||||
expect(
|
||||
() => manifest.validateFor(
|
||||
generationHash: _generationHash,
|
||||
recipe: differentRecipe,
|
||||
expectedAssetNames: manifest.assets.map((asset) => asset.name).toSet(),
|
||||
),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('asset signature payload has the exact v2 metadata encoding', () {
|
||||
final payload = precompiledAssetSignaturePayload(
|
||||
generationHash: _generationHash,
|
||||
name: 'asset.bin',
|
||||
length: 3,
|
||||
sha256:
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
);
|
||||
|
||||
expect(
|
||||
_hex(payload),
|
||||
'636172676f6b69742d707265636f6d70696c65642d61737365742d7369676e6174757265'
|
||||
'0002'
|
||||
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
||||
'0000000961737365742e62696e'
|
||||
'0000000000000003'
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
);
|
||||
});
|
||||
|
||||
test('asset signature payload encodes the full unsigned u64 range', () {
|
||||
String encodedLength(BigInt length) {
|
||||
final payload = precompiledAssetSignaturePayload(
|
||||
generationHash: _generationHash,
|
||||
name: 'asset.bin',
|
||||
length: length,
|
||||
sha256:
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
);
|
||||
final offset = ascii
|
||||
.encode(
|
||||
'cargokit-precompiled-asset-signature',
|
||||
)
|
||||
.length +
|
||||
2 +
|
||||
32 +
|
||||
4 +
|
||||
ascii.encode('asset.bin').length;
|
||||
return _hex(payload.sublist(offset, offset + 8));
|
||||
}
|
||||
|
||||
expect(encodedLength(BigInt.one << 63), '8000000000000000');
|
||||
expect(encodedLength((BigInt.one << 64) - BigInt.one), 'ffffffffffffffff');
|
||||
});
|
||||
|
||||
test('asset signatures reject protocol substitution and u64 overflow', () {
|
||||
final payload = precompiledAssetSignaturePayload(
|
||||
generationHash: _generationHash,
|
||||
name: 'asset.bin',
|
||||
length: 3,
|
||||
sha256:
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
);
|
||||
final signature = sign(keyPair.privateKey, payload);
|
||||
final substituted = Uint8List.fromList(payload)..[payload.indexOf(2)] = 3;
|
||||
|
||||
expect(verify(keyPair.publicKey, payload, signature), isTrue);
|
||||
expect(verify(keyPair.publicKey, substituted, signature), isFalse);
|
||||
expect(
|
||||
() => precompiledAssetSignaturePayload(
|
||||
generationHash: _generationHash,
|
||||
name: 'asset.bin',
|
||||
length: BigInt.one << 64,
|
||||
sha256:
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('Ed25519 binds asset generation, name, length, and digest metadata', () {
|
||||
final assetBytes = Uint8List.fromList([9, 8, 7]);
|
||||
final assetHash = sha256.convert(assetBytes).toString();
|
||||
final manifest = PrecompiledGenerationManifest(
|
||||
generationHash: _generationHash,
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: [
|
||||
PrecompiledAsset(name: 'asset.bin', length: 3, sha256: assetHash),
|
||||
],
|
||||
compositeChecksums: const [],
|
||||
);
|
||||
|
||||
final manifestSignature = manifest.sign(keyPair.privateKey);
|
||||
final assetSignature = signPrecompiledAssetMetadata(
|
||||
keyPair.privateKey,
|
||||
generationHash: _generationHash,
|
||||
name: 'asset.bin',
|
||||
length: assetBytes.length,
|
||||
sha256: assetHash,
|
||||
);
|
||||
|
||||
manifest.verifySignature(keyPair.publicKey, manifestSignature);
|
||||
manifest.verifyAsset(
|
||||
name: 'asset.bin',
|
||||
bytes: assetBytes,
|
||||
signature: assetSignature,
|
||||
publicKey: keyPair.publicKey,
|
||||
);
|
||||
expect(
|
||||
() => manifest.verifyAsset(
|
||||
name: 'asset.bin',
|
||||
bytes: Uint8List.fromList([9, 8, 6]),
|
||||
signature: assetSignature,
|
||||
publicKey: keyPair.publicKey,
|
||||
),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
for (final substitution in <PrecompiledGenerationManifest>[
|
||||
PrecompiledGenerationManifest(
|
||||
generationHash:
|
||||
'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: [
|
||||
PrecompiledAsset(name: 'asset.bin', length: 3, sha256: assetHash),
|
||||
],
|
||||
compositeChecksums: const [],
|
||||
),
|
||||
PrecompiledGenerationManifest(
|
||||
generationHash: _generationHash,
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: [
|
||||
PrecompiledAsset(name: 'other.bin', length: 3, sha256: assetHash),
|
||||
],
|
||||
compositeChecksums: const [],
|
||||
),
|
||||
PrecompiledGenerationManifest(
|
||||
generationHash: _generationHash,
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: [
|
||||
PrecompiledAsset(name: 'asset.bin', length: 4, sha256: assetHash),
|
||||
],
|
||||
compositeChecksums: const [],
|
||||
),
|
||||
PrecompiledGenerationManifest(
|
||||
generationHash: _generationHash,
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: const [
|
||||
PrecompiledAsset(
|
||||
name: 'asset.bin',
|
||||
length: 3,
|
||||
sha256:
|
||||
'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
|
||||
),
|
||||
],
|
||||
compositeChecksums: const [],
|
||||
),
|
||||
]) {
|
||||
expect(
|
||||
() => substitution.verifyAssetMetadataSignature(
|
||||
name: substitution.assets.single.name,
|
||||
signature: assetSignature,
|
||||
publicKey: keyPair.publicKey,
|
||||
),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
}
|
||||
final badManifestSignature = Uint8List.fromList(manifestSignature)
|
||||
..[0] ^= 1;
|
||||
expect(
|
||||
() => manifest.verifySignature(keyPair.publicKey, badManifestSignature),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('zip/checksum relationship binds checksum contents to archive digest',
|
||||
() {
|
||||
final archive = Uint8List.fromList([1, 2, 3, 4]);
|
||||
final checksum =
|
||||
Uint8List.fromList(utf8.encode('${sha256.convert(archive)}\n'));
|
||||
final manifest = PrecompiledGenerationManifest(
|
||||
generationHash: _generationHash,
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: [
|
||||
PrecompiledAsset(
|
||||
name: 'fjs.xcframework.zip',
|
||||
length: archive.length,
|
||||
sha256: sha256.convert(archive).toString(),
|
||||
),
|
||||
PrecompiledAsset(
|
||||
name: 'fjs.xcframework.zip.checksum',
|
||||
length: checksum.length,
|
||||
sha256: sha256.convert(checksum).toString(),
|
||||
),
|
||||
],
|
||||
compositeChecksums: const [
|
||||
PrecompiledCompositeChecksum(
|
||||
archive: 'fjs.xcframework.zip',
|
||||
checksum: 'fjs.xcframework.zip.checksum',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
manifest.validateCompositeChecksums({
|
||||
'fjs.xcframework.zip': archive,
|
||||
'fjs.xcframework.zip.checksum': checksum,
|
||||
});
|
||||
expect(
|
||||
() => manifest.validateCompositeChecksums({
|
||||
'fjs.xcframework.zip': archive,
|
||||
'fjs.xcframework.zip.checksum': Uint8List.fromList(utf8.encode(
|
||||
'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\n',
|
||||
)),
|
||||
}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('composite checksum content is exact lowercase sha256 plus LF', () {
|
||||
final archive = Uint8List.fromList([1, 2, 3, 4]);
|
||||
final digest = sha256.convert(archive).toString();
|
||||
final manifest = PrecompiledGenerationManifest(
|
||||
generationHash: _generationHash,
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: [
|
||||
PrecompiledAsset(
|
||||
name: 'archive.zip',
|
||||
length: archive.length,
|
||||
sha256: digest,
|
||||
),
|
||||
PrecompiledAsset(
|
||||
name: 'archive.zip.checksum',
|
||||
length: 65,
|
||||
sha256: sha256.convert(ascii.encode('$digest\n')).toString(),
|
||||
),
|
||||
],
|
||||
compositeChecksums: const [
|
||||
PrecompiledCompositeChecksum(
|
||||
archive: 'archive.zip',
|
||||
checksum: 'archive.zip.checksum',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
manifest.validateCompositeChecksums({
|
||||
'archive.zip': archive,
|
||||
'archive.zip.checksum': Uint8List.fromList(ascii.encode('$digest\n')),
|
||||
});
|
||||
final invalid = <String, List<int>>{
|
||||
'no newline': ascii.encode(digest),
|
||||
'CRLF': ascii.encode('$digest\r\n'),
|
||||
'extra newline': ascii.encode('$digest\n\n'),
|
||||
'trailing space': ascii.encode('$digest \n'),
|
||||
'uppercase': ascii.encode('${digest.toUpperCase()}\n'),
|
||||
'invalid UTF-8': [...ascii.encode(digest), 0xff],
|
||||
};
|
||||
for (final entry in invalid.entries) {
|
||||
expect(
|
||||
() => manifest.validateCompositeChecksums({
|
||||
'archive.zip': archive,
|
||||
'archive.zip.checksum': entry.value,
|
||||
}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
reason: entry.key,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('composite metadata is normative without downloading assets', () {
|
||||
final valid = jsonDecode(utf8.decode(_manifest(recipe).canonicalBytes()))
|
||||
as Map<String, dynamic>;
|
||||
final assets = (valid['assets'] as List).cast<Map<String, dynamic>>();
|
||||
final bindings =
|
||||
(valid['composite_checksums'] as List).cast<Map<String, dynamic>>();
|
||||
|
||||
expect(
|
||||
() => _parse({
|
||||
...valid,
|
||||
'composite_checksums': [
|
||||
{...bindings.single, 'checksum': bindings.single['archive']},
|
||||
],
|
||||
}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({
|
||||
...valid,
|
||||
'composite_checksums': [
|
||||
bindings.single,
|
||||
{
|
||||
...bindings.single,
|
||||
'archive': 'aarch64-apple-darwin_libfjs.a',
|
||||
},
|
||||
],
|
||||
}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({
|
||||
...valid,
|
||||
'assets': [
|
||||
...assets.take(2),
|
||||
{...assets.last, 'length': 64},
|
||||
],
|
||||
}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
expect(
|
||||
() => _parse({
|
||||
...valid,
|
||||
'assets': [
|
||||
...assets.take(2),
|
||||
{...assets.last, 'sha256': _archiveHash},
|
||||
],
|
||||
}),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
PrecompiledGenerationManifest _manifest(
|
||||
PrecompiledBuildRecipe recipe, {
|
||||
bool reverseAssets = false,
|
||||
}) {
|
||||
final assets = [
|
||||
const PrecompiledAsset(
|
||||
name: 'aarch64-apple-darwin_libfjs.a',
|
||||
length: 3,
|
||||
sha256: _checksumHash,
|
||||
),
|
||||
const PrecompiledAsset(
|
||||
name: 'fjs.xcframework.zip',
|
||||
length: 4,
|
||||
sha256: _archiveHash,
|
||||
),
|
||||
const PrecompiledAsset(
|
||||
name: 'fjs.xcframework.zip.checksum',
|
||||
length: 65,
|
||||
sha256: _checksumHash,
|
||||
),
|
||||
];
|
||||
return PrecompiledGenerationManifest(
|
||||
generationHash: _generationHash,
|
||||
sourceCommit: _sourceCommit,
|
||||
provenance: PrecompiledGenerationProvenance.fromRecipe(recipe),
|
||||
assets: reverseAssets ? assets.reversed.toList() : assets,
|
||||
compositeChecksums: const [
|
||||
PrecompiledCompositeChecksum(
|
||||
archive: 'fjs.xcframework.zip',
|
||||
checksum: 'fjs.xcframework.zip.checksum',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
PrecompiledGenerationManifest _parse(Map<String, dynamic> value) =>
|
||||
PrecompiledGenerationManifest.parse(utf8.encode(jsonEncode(value)));
|
||||
|
||||
String _hex(List<int> bytes) =>
|
||||
bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
||||
@@ -0,0 +1,263 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:build_tool/src/builder.dart';
|
||||
import 'package:build_tool/src/options.dart';
|
||||
import 'package:build_tool/src/precompile_binaries.dart';
|
||||
import 'package:build_tool/src/precompiled_asset_store.dart';
|
||||
import 'package:build_tool/src/precompiled_generation.dart';
|
||||
import 'package:build_tool/src/target.dart';
|
||||
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:github/github.dart' as github;
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
late Directory temp;
|
||||
late Directory manifestDir;
|
||||
late KeyPair keyPair;
|
||||
|
||||
setUp(() {
|
||||
temp = Directory(Directory.systemTemp.resolveSymbolicLinksSync())
|
||||
.createTempSync('precompiled-publisher-test.');
|
||||
manifestDir = Directory(path.join(temp.path, 'crate'))..createSync();
|
||||
final privateKey = newKeyFromSeed(
|
||||
Uint8List.fromList(List<int>.generate(32, (index) => index)),
|
||||
);
|
||||
keyPair = KeyPair(privateKey, public(privateKey));
|
||||
_writeCrate(manifestDir, HEX.encode(keyPair.publicKey.bytes));
|
||||
});
|
||||
|
||||
tearDown(() => temp.deleteSync(recursive: true));
|
||||
|
||||
test('finalizer output is accepted by the v2 store without extra assets',
|
||||
() async {
|
||||
final finalized = await _finalize(temp, manifestDir, keyPair);
|
||||
|
||||
final remote = <String, List<int>>{
|
||||
for (final entry in finalized.publicationFiles.entries)
|
||||
entry.key: entry.value.readAsBytesSync(),
|
||||
};
|
||||
final requests = <String>[];
|
||||
final transport = PrecompiledAssetTransport(send: (request) async {
|
||||
final name = request.url.pathSegments.last;
|
||||
requests.add(name);
|
||||
final bytes = remote[name];
|
||||
return StreamedResponse(
|
||||
Stream.value(bytes ?? const <int>[]),
|
||||
bytes == null ? 404 : 200,
|
||||
contentLength: bytes?.length ?? 0,
|
||||
);
|
||||
});
|
||||
final options = CargokitCrateOptions.load(manifestDir: manifestDir.path);
|
||||
final recipe = options.precompiledBinaries!.buildRecipe!;
|
||||
final store = PrecompiledAssetStore(
|
||||
cacheRoot: path.join(temp.path, 'cache'),
|
||||
uriPrefix: Uri.parse('https://assets.example/precompiled_'),
|
||||
publicKey: keyPair.publicKey,
|
||||
recipe: recipe,
|
||||
expectedAssetNames:
|
||||
finalized.manifest.assets.map((asset) => asset.name).toSet(),
|
||||
expectedCompositeChecksums: const {},
|
||||
transport: transport,
|
||||
);
|
||||
|
||||
final requested =
|
||||
'${Target.forRustTriple('x86_64-unknown-linux-gnu')!.rust}_libfjs.a';
|
||||
final snapshot = await store.snapshot(
|
||||
generationHash: finalized.manifest.generationHash,
|
||||
requestedAssetNames: {requested},
|
||||
);
|
||||
|
||||
expect(File(snapshot!.pathFor(requested)).readAsStringSync(), 'static');
|
||||
expect(requests, isNot(contains(endsWith('_libfjs.so'))));
|
||||
transport.close();
|
||||
});
|
||||
|
||||
test('publisher uploads completion last, reuses valid, and rejects mismatch',
|
||||
() async {
|
||||
final finalized = await _finalize(temp, manifestDir, keyPair);
|
||||
final gateway = _FakeReleaseGateway();
|
||||
final publisher = PrecompiledGenerationPublisher(
|
||||
generation: finalized,
|
||||
gateway: gateway,
|
||||
);
|
||||
|
||||
final first = await publisher.publish();
|
||||
|
||||
expect(first.reused, isFalse);
|
||||
expect(gateway.events.sublist(gateway.events.length - 3), [
|
||||
'upload:completion.json',
|
||||
'upload:completion.json.sig',
|
||||
'publish',
|
||||
]);
|
||||
gateway.events.clear();
|
||||
|
||||
final reused = await publisher.publish();
|
||||
|
||||
expect(reused.reused, isTrue);
|
||||
expect(
|
||||
gateway.events.where((event) => event.startsWith('upload:')), isEmpty);
|
||||
gateway.corrupt('completion.json');
|
||||
await expectLater(
|
||||
publisher.publish(),
|
||||
throwsA(isA<PrecompiledGenerationException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('GitHub gateway downloads an authenticated release asset', () async {
|
||||
final client = MockClient((request) async {
|
||||
expect(request.method, 'GET');
|
||||
expect(
|
||||
request.url,
|
||||
Uri.parse(
|
||||
'https://api.github.com/repos/fluttercandies/fjs/releases/assets/42',
|
||||
),
|
||||
);
|
||||
expect(request.headers['Accept'], 'application/octet-stream');
|
||||
expect(request.headers['Authorization'], isNotEmpty);
|
||||
expect(request.headers['User-Agent'], isNotEmpty);
|
||||
return Response.bytes(const [1, 2, 3], 200);
|
||||
});
|
||||
final githubClient = github.GitHub(
|
||||
auth: const github.Authentication.withToken('token'),
|
||||
client: client,
|
||||
);
|
||||
addTearDown(githubClient.dispose);
|
||||
final gateway = GitHubPrecompiledReleaseGateway(
|
||||
repositories: githubClient.repositories,
|
||||
repository: github.RepositorySlug('fluttercandies', 'fjs'),
|
||||
);
|
||||
final release = PrecompiledRelease(
|
||||
id: github.Release(
|
||||
id: 7,
|
||||
assets: [github.ReleaseAsset(id: 42, name: 'completion.json')],
|
||||
),
|
||||
tagName: 'precompiled_hash',
|
||||
isDraft: false,
|
||||
assetNames: const {'completion.json'},
|
||||
);
|
||||
|
||||
expect(
|
||||
await gateway.readAsset(release, 'completion.json'),
|
||||
const [1, 2, 3],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<FinalizedPrecompiledGeneration> _finalize(
|
||||
Directory temp,
|
||||
Directory manifestDir,
|
||||
KeyPair keyPair,
|
||||
) async {
|
||||
final generationDir = path.join(temp.path, 'generation');
|
||||
await LocalPrecompiledGeneration(
|
||||
manifestDir: manifestDir.path,
|
||||
outputDir: generationDir,
|
||||
tempDir: path.join(temp.path, 'build'),
|
||||
targetTriples: const ['x86_64-unknown-linux-gnu'],
|
||||
targetBuilder: _buildTarget,
|
||||
).run();
|
||||
return LocalGenerationFinalizer(
|
||||
manifestDir: manifestDir.path,
|
||||
generationDir: generationDir,
|
||||
privateKey: keyPair.privateKey,
|
||||
scope: LocalGenerationFinalizationScope.full,
|
||||
sourceCommit: '1111111111111111111111111111111111111111',
|
||||
).run();
|
||||
}
|
||||
|
||||
class _FakeReleaseGateway implements PrecompiledReleaseGateway {
|
||||
PrecompiledRelease? release;
|
||||
final assets = <String, List<int>>{};
|
||||
final events = <String>[];
|
||||
|
||||
@override
|
||||
Future<PrecompiledRelease?> find(String tagName) async => release;
|
||||
|
||||
@override
|
||||
Future<PrecompiledRelease> createDraft(String tagName) async {
|
||||
events.add('create-draft');
|
||||
return release = PrecompiledRelease(
|
||||
id: 'release',
|
||||
tagName: tagName,
|
||||
isDraft: true,
|
||||
assetNames: assets.keys.toSet(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<int>> readAsset(PrecompiledRelease release, String name) async =>
|
||||
assets[name]!;
|
||||
|
||||
@override
|
||||
Future<void> upload(
|
||||
PrecompiledRelease release,
|
||||
String name,
|
||||
List<int> bytes,
|
||||
) async {
|
||||
events.add('upload:$name');
|
||||
assets[name] = List.of(bytes);
|
||||
this.release = PrecompiledRelease(
|
||||
id: release.id,
|
||||
tagName: release.tagName,
|
||||
isDraft: true,
|
||||
assetNames: assets.keys.toSet(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> publish(PrecompiledRelease release) async {
|
||||
events.add('publish');
|
||||
this.release = PrecompiledRelease(
|
||||
id: release.id,
|
||||
tagName: release.tagName,
|
||||
isDraft: false,
|
||||
assetNames: assets.keys.toSet(),
|
||||
);
|
||||
}
|
||||
|
||||
void corrupt(String name) => assets[name]![0] ^= 1;
|
||||
}
|
||||
|
||||
Future<String> _buildTarget(
|
||||
Target target,
|
||||
BuildEnvironment environment,
|
||||
String toolchain,
|
||||
) async {
|
||||
final output = Directory(path.join(environment.targetTempDir, target.rust))
|
||||
..createSync(recursive: true);
|
||||
File(path.join(output.path, 'libfjs.a')).writeAsStringSync('static');
|
||||
File(path.join(output.path, 'libfjs.so')).writeAsStringSync('dynamic');
|
||||
return output.path;
|
||||
}
|
||||
|
||||
void _writeCrate(Directory manifestDir, String publicKey) {
|
||||
File(path.join(manifestDir.path, 'Cargo.toml')).writeAsStringSync('''
|
||||
[package]
|
||||
name = "fjs"
|
||||
version = "1.0.0"
|
||||
''');
|
||||
Directory(path.join(manifestDir.path, 'src')).createSync();
|
||||
File(path.join(manifestDir.path, 'src/lib.rs'))
|
||||
.writeAsStringSync('pub fn fjs() {}\n');
|
||||
File(path.join(manifestDir.path, 'cargokit.yaml')).writeAsStringSync('''
|
||||
precompiled_binaries:
|
||||
url_prefix: https://assets.example/precompiled_
|
||||
public_key: $publicKey
|
||||
workspace_root: .
|
||||
build_recipe:
|
||||
rust_toolchain: '1.88.0'
|
||||
flutter_version: '3.32.8'
|
||||
xcode_version: '16.4'
|
||||
sdk_versions:
|
||||
linux: '6.8'
|
||||
deployment_targets:
|
||||
linux: 'glibc-2.35'
|
||||
rust_targets:
|
||||
- x86_64-unknown-linux-gnu
|
||||
''');
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:build_tool/src/options.dart';
|
||||
import 'package:build_tool/src/precompiled_asset_store.dart';
|
||||
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||
|
||||
Future<void> main(List<String> arguments) async {
|
||||
if (arguments.length != 1) {
|
||||
stderr.writeln('Expected one JSON configuration argument.');
|
||||
exitCode = 64;
|
||||
return;
|
||||
}
|
||||
final config = (jsonDecode(arguments.single) as Map).cast<String, dynamic>();
|
||||
final mode = config['mode'] as String;
|
||||
_emit({'event': 'ready', 'mode': mode});
|
||||
final commands = StreamIterator(
|
||||
stdin.transform(utf8.decoder).transform(const LineSplitter()),
|
||||
);
|
||||
if (!await commands.moveNext()) return;
|
||||
final first = (jsonDecode(commands.current) as Map).cast<String, dynamic>();
|
||||
if (first['command'] != 'proceed') {
|
||||
_emit({
|
||||
'event': 'result',
|
||||
'ok': false,
|
||||
'path': null,
|
||||
'error': 'expected proceed',
|
||||
});
|
||||
await commands.cancel();
|
||||
exitCode = 64;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (mode == 'install') {
|
||||
try {
|
||||
final snapshotPath = await _install(config);
|
||||
_emit({
|
||||
'event': 'result',
|
||||
'ok': true,
|
||||
'path': snapshotPath,
|
||||
'error': null,
|
||||
});
|
||||
} on Object catch (error) {
|
||||
_emit({
|
||||
'event': 'result',
|
||||
'ok': false,
|
||||
'path': null,
|
||||
'error': '$error',
|
||||
});
|
||||
exitCode = 1;
|
||||
}
|
||||
await commands.cancel();
|
||||
return;
|
||||
}
|
||||
if (!const {
|
||||
'hold-generation-lock',
|
||||
'hold-request-lock',
|
||||
'crash-with-lock',
|
||||
'acquire',
|
||||
}.contains(mode)) {
|
||||
throw ArgumentError.value(mode, 'mode', 'Unsupported worker mode.');
|
||||
}
|
||||
final lockPath = config['lock_path'] as String;
|
||||
final file = File(lockPath)..createSync(recursive: true);
|
||||
final lock = file.openSync(mode: FileMode.append);
|
||||
if (mode == 'acquire') {
|
||||
final acquired = await _acquire(
|
||||
lock,
|
||||
timeout: Duration(milliseconds: (config['timeout_ms'] as int?) ?? 1000),
|
||||
poll: Duration(milliseconds: (config['poll_ms'] as int?) ?? 10),
|
||||
);
|
||||
_emit({
|
||||
'event': 'result',
|
||||
'ok': acquired,
|
||||
'path': null,
|
||||
'error': acquired ? null : 'lock not acquired',
|
||||
'acquired': acquired,
|
||||
});
|
||||
if (acquired) lock.unlockSync();
|
||||
lock.closeSync();
|
||||
await commands.cancel();
|
||||
return;
|
||||
}
|
||||
lock.lockSync(FileLock.exclusive);
|
||||
_emit({
|
||||
'event': 'result',
|
||||
'ok': true,
|
||||
'path': null,
|
||||
'error': null,
|
||||
'acquired': true,
|
||||
});
|
||||
if (mode == 'crash-with-lock') {
|
||||
exit(23);
|
||||
}
|
||||
if (await commands.moveNext()) {
|
||||
final command =
|
||||
(jsonDecode(commands.current) as Map).cast<String, dynamic>();
|
||||
if (command['command'] == 'release') {
|
||||
lock.unlockSync();
|
||||
lock.closeSync();
|
||||
await commands.cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
lock.unlockSync();
|
||||
lock.closeSync();
|
||||
await commands.cancel();
|
||||
} on Object catch (error, stackTrace) {
|
||||
_emit({
|
||||
'event': 'result',
|
||||
'ok': false,
|
||||
'path': null,
|
||||
'error': '$error',
|
||||
'stack': '$stackTrace',
|
||||
});
|
||||
await commands.cancel();
|
||||
exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _acquire(
|
||||
RandomAccessFile lock, {
|
||||
required Duration timeout,
|
||||
required Duration poll,
|
||||
}) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
while (true) {
|
||||
try {
|
||||
lock.lockSync(FileLock.exclusive);
|
||||
return true;
|
||||
} on FileSystemException {
|
||||
if (stopwatch.elapsed >= timeout) return false;
|
||||
await Future<void>.delayed(poll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _install(Map<String, dynamic> config) async {
|
||||
final recipeJson = (config['recipe'] as Map).cast<String, dynamic>();
|
||||
final recipe = PrecompiledBuildRecipe(
|
||||
rustToolchain: recipeJson['rust_toolchain'] as String,
|
||||
flutterVersion: recipeJson['flutter_version'] as String,
|
||||
xcodeVersion: recipeJson['xcode_version'] as String,
|
||||
sdkVersions: (recipeJson['sdk_versions'] as Map).cast<String, String>(),
|
||||
deploymentTargets:
|
||||
(recipeJson['deployment_targets'] as Map).cast<String, String>(),
|
||||
rustTargets: (recipeJson['rust_targets'] as List).cast<String>(),
|
||||
);
|
||||
final transport = PrecompiledAssetTransport();
|
||||
try {
|
||||
final store = PrecompiledAssetStore(
|
||||
cacheRoot: config['cache_root'] as String,
|
||||
uriPrefix: Uri.parse(config['uri_prefix'] as String),
|
||||
publicKey: PublicKey((config['public_key'] as List).cast<int>()),
|
||||
recipe: recipe,
|
||||
expectedAssetNames:
|
||||
(config['expected_asset_names'] as List).cast<String>().toSet(),
|
||||
expectedCompositeChecksums:
|
||||
(config['expected_composites'] as List).cast<String>().toSet(),
|
||||
transport: transport,
|
||||
policy: PrecompiledAssetStorePolicy(
|
||||
lockTimeout: Duration(
|
||||
milliseconds: (config['lock_timeout_ms'] as int?) ?? 30000),
|
||||
),
|
||||
);
|
||||
final snapshot = await store.snapshot(
|
||||
generationHash: config['generation_hash'] as String,
|
||||
requestedAssetNames:
|
||||
(config['requested_assets'] as List).cast<String>().toSet(),
|
||||
);
|
||||
return snapshot?.directory;
|
||||
} finally {
|
||||
transport.close();
|
||||
}
|
||||
}
|
||||
|
||||
void _emit(Map<String, Object?> value) {
|
||||
stdout.writeln(jsonEncode(value));
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
SET(cargokit_cmake_root "${CMAKE_CURRENT_LIST_DIR}/..")
|
||||
|
||||
# Workaround for https://github.com/dart-lang/pub/issues/4010
|
||||
get_filename_component(cargokit_cmake_root "${cargokit_cmake_root}" REALPATH)
|
||||
|
||||
if(WIN32)
|
||||
# REALPATH does not properly resolve symlinks on windows :-/
|
||||
execute_process(COMMAND powershell -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_LIST_DIR}/resolve_symlinks.ps1" "${cargokit_cmake_root}" OUTPUT_VARIABLE cargokit_cmake_root OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
endif()
|
||||
|
||||
# Arguments
|
||||
# - target: CMAKE target to which rust library is linked
|
||||
# - manifest_dir: relative path from current folder to directory containing cargo manifest
|
||||
# - lib_name: cargo package name
|
||||
# - any_symbol_name: name of any exported symbol from the library.
|
||||
# used on windows to force linking with library.
|
||||
function(apply_cargokit target manifest_dir lib_name any_symbol_name)
|
||||
|
||||
set(CARGOKIT_LIB_NAME "${lib_name}")
|
||||
set(CARGOKIT_LIB_FULL_NAME "${CMAKE_SHARED_MODULE_PREFIX}${CARGOKIT_LIB_NAME}${CMAKE_SHARED_MODULE_SUFFIX}")
|
||||
if (CMAKE_CONFIGURATION_TYPES)
|
||||
set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>")
|
||||
set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/${CARGOKIT_LIB_FULL_NAME}")
|
||||
else()
|
||||
set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/${CARGOKIT_LIB_FULL_NAME}")
|
||||
endif()
|
||||
set(CARGOKIT_TEMP_DIR "${CMAKE_CURRENT_BINARY_DIR}/cargokit_build")
|
||||
|
||||
if (FLUTTER_TARGET_PLATFORM)
|
||||
set(CARGOKIT_TARGET_PLATFORM "${FLUTTER_TARGET_PLATFORM}")
|
||||
else()
|
||||
set(CARGOKIT_TARGET_PLATFORM "windows-x64")
|
||||
endif()
|
||||
|
||||
set(CARGOKIT_ENV
|
||||
"CARGOKIT_CMAKE=${CMAKE_COMMAND}"
|
||||
"CARGOKIT_CONFIGURATION=$<CONFIG>"
|
||||
"CARGOKIT_MANIFEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/${manifest_dir}"
|
||||
"CARGOKIT_TARGET_TEMP_DIR=${CARGOKIT_TEMP_DIR}"
|
||||
"CARGOKIT_OUTPUT_DIR=${CARGOKIT_OUTPUT_DIR}"
|
||||
"CARGOKIT_TARGET_PLATFORM=${CARGOKIT_TARGET_PLATFORM}"
|
||||
"CARGOKIT_TOOL_TEMP_DIR=${CARGOKIT_TEMP_DIR}/tool"
|
||||
"CARGOKIT_ROOT_PROJECT_DIR=${CMAKE_SOURCE_DIR}"
|
||||
)
|
||||
|
||||
if (WIN32)
|
||||
set(SCRIPT_EXTENSION ".cmd")
|
||||
set(IMPORT_LIB_EXTENSION ".lib")
|
||||
else()
|
||||
set(SCRIPT_EXTENSION ".sh")
|
||||
set(IMPORT_LIB_EXTENSION "")
|
||||
execute_process(COMMAND chmod +x "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}")
|
||||
endif()
|
||||
|
||||
# Using generators in custom command is only supported in CMake 3.20+
|
||||
if (CMAKE_CONFIGURATION_TYPES AND ${CMAKE_VERSION} VERSION_LESS "3.20.0")
|
||||
foreach(CONFIG IN LISTS CMAKE_CONFIGURATION_TYPES)
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/${CONFIG}/${CARGOKIT_LIB_FULL_NAME}"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/_phony_"
|
||||
COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV}
|
||||
"${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake
|
||||
VERBATIM
|
||||
)
|
||||
endforeach()
|
||||
else()
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
${OUTPUT_LIB}
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/_phony_"
|
||||
COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV}
|
||||
"${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/_phony_" PROPERTIES SYMBOLIC TRUE)
|
||||
|
||||
if (TARGET ${target})
|
||||
# If we have actual cmake target provided create target and make existing
|
||||
# target depend on it
|
||||
add_custom_target("${target}_cargokit" DEPENDS ${OUTPUT_LIB})
|
||||
add_dependencies("${target}" "${target}_cargokit")
|
||||
target_link_libraries("${target}" PRIVATE "${OUTPUT_LIB}${IMPORT_LIB_EXTENSION}")
|
||||
if(WIN32)
|
||||
target_link_options(${target} PRIVATE "/INCLUDE:${any_symbol_name}")
|
||||
endif()
|
||||
else()
|
||||
# Otherwise (FFI) just use ALL to force building always
|
||||
add_custom_target("${target}_cargokit" ALL DEPENDS ${OUTPUT_LIB})
|
||||
endif()
|
||||
|
||||
# Allow adding the output library to plugin bundled libraries
|
||||
set("${target}_cargokit_lib" ${OUTPUT_LIB} PARENT_SCOPE)
|
||||
|
||||
endfunction()
|
||||
@@ -0,0 +1,27 @@
|
||||
function Resolve-Symlinks {
|
||||
[CmdletBinding()]
|
||||
[OutputType([string])]
|
||||
param(
|
||||
[Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
|
||||
[string] $Path
|
||||
)
|
||||
|
||||
[string] $separator = '/'
|
||||
[string[]] $parts = $Path.Split($separator)
|
||||
|
||||
[string] $realPath = ''
|
||||
foreach ($part in $parts) {
|
||||
if ($realPath -and !$realPath.EndsWith($separator)) {
|
||||
$realPath += $separator
|
||||
}
|
||||
$realPath += $part
|
||||
$item = Get-Item $realPath
|
||||
if ($item.Target) {
|
||||
$realPath = $item.Target.Replace('\', '/')
|
||||
}
|
||||
}
|
||||
$realPath
|
||||
}
|
||||
|
||||
$path=Resolve-Symlinks -Path $args[0]
|
||||
Write-Host $path
|
||||
@@ -0,0 +1,196 @@
|
||||
/// 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 org.apache.tools.ant.taskdefs.condition.Os
|
||||
import org.gradle.process.ExecOperations
|
||||
|
||||
import javax.inject.Inject
|
||||
import java.nio.file.Paths
|
||||
|
||||
CargoKitPlugin.file = buildscript.sourceFile
|
||||
|
||||
apply plugin: CargoKitPlugin
|
||||
|
||||
class CargoKitExtension {
|
||||
String manifestDir; // Relative path to folder containing Cargo.toml
|
||||
String libname; // Library name within Cargo.toml. Must be a cdylib
|
||||
}
|
||||
|
||||
abstract class CargoKitBuildTask extends DefaultTask {
|
||||
|
||||
@Inject
|
||||
abstract ExecOperations getExecOperations()
|
||||
|
||||
@Input
|
||||
String buildMode
|
||||
|
||||
@Input
|
||||
String buildDir
|
||||
|
||||
@Input
|
||||
String outputDir
|
||||
|
||||
@Input
|
||||
String ndkVersion
|
||||
|
||||
@Input
|
||||
String sdkDirectory
|
||||
|
||||
@Input
|
||||
int compileSdkVersion;
|
||||
|
||||
@Input
|
||||
int minSdkVersion;
|
||||
|
||||
@Input
|
||||
String pluginFile
|
||||
|
||||
@Input
|
||||
List<String> targetPlatforms
|
||||
|
||||
@TaskAction
|
||||
def build() {
|
||||
if (project.cargokit.manifestDir == null) {
|
||||
throw new GradleException("Property 'manifestDir' must be set on cargokit extension");
|
||||
}
|
||||
|
||||
if (project.cargokit.libname == null) {
|
||||
throw new GradleException("Property 'libname' must be set on cargokit extension");
|
||||
}
|
||||
|
||||
def executableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "run_build_tool.cmd" : "run_build_tool.sh"
|
||||
def path = Paths.get(new File(pluginFile).parent, "..", executableName);
|
||||
|
||||
def manifestDir = Paths.get(project.buildscript.sourceFile.parent, project.cargokit.manifestDir)
|
||||
|
||||
def rootProjectDir = project.rootProject.projectDir
|
||||
|
||||
if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
execOperations.exec {
|
||||
commandLine 'chmod', '+x', path
|
||||
}
|
||||
}
|
||||
|
||||
execOperations.exec {
|
||||
executable path
|
||||
args "build-gradle"
|
||||
environment "CARGOKIT_ROOT_PROJECT_DIR", rootProjectDir
|
||||
environment "CARGOKIT_TOOL_TEMP_DIR", "${buildDir}/build_tool"
|
||||
environment "CARGOKIT_MANIFEST_DIR", manifestDir
|
||||
environment "CARGOKIT_CONFIGURATION", buildMode
|
||||
environment "CARGOKIT_TARGET_TEMP_DIR", buildDir
|
||||
environment "CARGOKIT_OUTPUT_DIR", outputDir
|
||||
environment "CARGOKIT_NDK_VERSION", ndkVersion
|
||||
environment "CARGOKIT_SDK_DIR", sdkDirectory
|
||||
environment "CARGOKIT_COMPILE_SDK_VERSION", compileSdkVersion
|
||||
environment "CARGOKIT_MIN_SDK_VERSION", minSdkVersion
|
||||
environment "CARGOKIT_TARGET_PLATFORMS", targetPlatforms.join(",")
|
||||
environment "CARGOKIT_JAVA_HOME", System.properties['java.home']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CargoKitPlugin implements Plugin<Project> {
|
||||
|
||||
static String file;
|
||||
|
||||
private Plugin findFlutterPlugin(Project rootProject) {
|
||||
_findFlutterPlugin(rootProject.childProjects)
|
||||
}
|
||||
|
||||
private Plugin _findFlutterPlugin(Map projects) {
|
||||
for (project in projects) {
|
||||
for (plugin in project.value.getPlugins()) {
|
||||
if (plugin.class.name == "FlutterPlugin" || plugin.class.name == "com.flutter.gradle.FlutterPlugin") {
|
||||
return plugin;
|
||||
}
|
||||
}
|
||||
def plugin = _findFlutterPlugin(project.value.childProjects);
|
||||
if (plugin != null) {
|
||||
return plugin;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
void apply(Project project) {
|
||||
def plugin = findFlutterPlugin(project.rootProject);
|
||||
|
||||
project.extensions.create("cargokit", CargoKitExtension)
|
||||
|
||||
if (plugin == null) {
|
||||
print("Flutter plugin not found, CargoKit plugin will not be applied.")
|
||||
return;
|
||||
}
|
||||
|
||||
def projectBuildDir = project.layout.buildDirectory.get().asFile
|
||||
def cargoBuildDir = new File(projectBuildDir, "build").path
|
||||
|
||||
// Determine if the project is an application or library
|
||||
def isApplication = plugin.project.plugins.hasPlugin('com.android.application')
|
||||
def variants = isApplication ? plugin.project.android.applicationVariants : plugin.project.android.libraryVariants
|
||||
|
||||
variants.all { variant ->
|
||||
|
||||
final buildType = variant.buildType.name
|
||||
|
||||
def cargoOutputDir = new File(projectBuildDir, "jniLibs/${buildType}").path;
|
||||
def jniLibs = project.android.sourceSets.maybeCreate(buildType).jniLibs;
|
||||
jniLibs.srcDir(new File(cargoOutputDir))
|
||||
|
||||
|
||||
def platforms;
|
||||
if (plugin.metaClass.respondsTo(plugin, 'getTargetPlatforms')) {
|
||||
platforms = plugin.getTargetPlatforms().collect();
|
||||
} else {
|
||||
platforms = com.flutter.gradle.FlutterPluginUtils.getTargetPlatforms(project).collect();
|
||||
}
|
||||
|
||||
// Same thing addFlutterDependencies does in flutter.gradle
|
||||
if (buildType == "debug") {
|
||||
platforms.add("android-x64")
|
||||
platforms.add("android-arm64")
|
||||
}
|
||||
|
||||
// platforms remove duplicates
|
||||
platforms = platforms.unique()
|
||||
|
||||
// The task name depends on plugin properties, which are not available
|
||||
// at this point
|
||||
project.getGradle().afterProject {
|
||||
def taskName = "cargokitCargoBuild${project.cargokit.libname.capitalize()}${buildType.capitalize()}";
|
||||
|
||||
if (project.tasks.findByName(taskName)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (plugin.project.android.ndkVersion == null) {
|
||||
throw new GradleException("Please set 'android.ndkVersion' in 'app/build.gradle'.")
|
||||
}
|
||||
|
||||
def task = project.tasks.create(taskName, CargoKitBuildTask.class) {
|
||||
buildMode = variant.buildType.name
|
||||
buildDir = cargoBuildDir
|
||||
outputDir = cargoOutputDir
|
||||
ndkVersion = plugin.project.android.ndkVersion
|
||||
sdkDirectory = plugin.project.android.sdkDirectory
|
||||
minSdkVersion = plugin.project.android.defaultConfig.minSdkVersion.apiLevel as int
|
||||
compileSdkVersion = plugin.project.android.compileSdkVersion.substring(8) as int
|
||||
targetPlatforms = platforms
|
||||
pluginFile = CargoKitPlugin.file
|
||||
}
|
||||
def onTask = { newTask ->
|
||||
if (newTask.name == "merge${buildType.capitalize()}NativeLibs") {
|
||||
newTask.dependsOn task
|
||||
// Fix gradle 7.4.2 not picking up JNI library changes
|
||||
newTask.outputs.upToDateWhen { false }
|
||||
}
|
||||
}
|
||||
project.tasks.each onTask
|
||||
project.tasks.whenTaskAdded onTask
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
setlocal ENABLEDELAYEDEXPANSION
|
||||
|
||||
SET BASEDIR=%~dp0
|
||||
|
||||
if not exist "%CARGOKIT_TOOL_TEMP_DIR%" (
|
||||
mkdir "%CARGOKIT_TOOL_TEMP_DIR%"
|
||||
)
|
||||
cd /D "%CARGOKIT_TOOL_TEMP_DIR%"
|
||||
|
||||
SET BUILD_TOOL_PKG_DIR=%BASEDIR%build_tool
|
||||
SET DART=%FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dart
|
||||
|
||||
set BUILD_TOOL_PKG_DIR_POSIX=%BUILD_TOOL_PKG_DIR:\=/%
|
||||
|
||||
(
|
||||
echo name: build_tool_runner
|
||||
echo version: 1.0.0
|
||||
echo publish_to: none
|
||||
echo.
|
||||
echo environment:
|
||||
echo sdk: '^>=3.0.0 ^<4.0.0'
|
||||
echo.
|
||||
echo dependencies:
|
||||
echo build_tool:
|
||||
echo path: %BUILD_TOOL_PKG_DIR_POSIX%
|
||||
) >pubspec.yaml
|
||||
|
||||
if not exist bin (
|
||||
mkdir bin
|
||||
)
|
||||
|
||||
(
|
||||
echo import 'package:build_tool/build_tool.dart' as build_tool;
|
||||
echo void main^(List^<String^> args^) ^{
|
||||
echo build_tool.runMain^(args^);
|
||||
echo ^}
|
||||
) >bin\build_tool_runner.dart
|
||||
|
||||
SET PRECOMPILED=bin\build_tool_runner.dill
|
||||
|
||||
REM To detect changes in package we compare output of DIR /s (recursive)
|
||||
set PREV_PACKAGE_INFO=.dart_tool\package_info.prev
|
||||
set CUR_PACKAGE_INFO=.dart_tool\package_info.cur
|
||||
|
||||
DIR "%BUILD_TOOL_PKG_DIR%" /s > "%CUR_PACKAGE_INFO%_orig"
|
||||
|
||||
REM Last line in dir output is free space on harddrive. That is bound to
|
||||
REM change between invocation so we need to remove it
|
||||
(
|
||||
Set "Line="
|
||||
For /F "UseBackQ Delims=" %%A In ("%CUR_PACKAGE_INFO%_orig") Do (
|
||||
SetLocal EnableDelayedExpansion
|
||||
If Defined Line Echo !Line!
|
||||
EndLocal
|
||||
Set "Line=%%A")
|
||||
) >"%CUR_PACKAGE_INFO%"
|
||||
DEL "%CUR_PACKAGE_INFO%_orig"
|
||||
|
||||
REM Compare current directory listing with previous
|
||||
FC /B "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" > nul 2>&1
|
||||
|
||||
If %ERRORLEVEL% neq 0 (
|
||||
REM Changed - copy current to previous and remove precompiled kernel
|
||||
if exist "%PREV_PACKAGE_INFO%" (
|
||||
DEL "%PREV_PACKAGE_INFO%"
|
||||
)
|
||||
MOVE /Y "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%"
|
||||
if exist "%PRECOMPILED%" (
|
||||
DEL "%PRECOMPILED%"
|
||||
)
|
||||
)
|
||||
|
||||
REM There is no CUR_PACKAGE_INFO it was renamed in previous step to %PREV_PACKAGE_INFO%
|
||||
REM which means we need to do pub get and precompile
|
||||
if not exist "%PRECOMPILED%" (
|
||||
echo Running pub get in "%cd%"
|
||||
"%DART%" pub get --no-precompile
|
||||
"%DART%" compile kernel bin/build_tool_runner.dart
|
||||
)
|
||||
|
||||
"%DART%" "%PRECOMPILED%" %*
|
||||
|
||||
REM 253 means invalid snapshot version.
|
||||
If %ERRORLEVEL% equ 253 (
|
||||
"%DART%" pub get --no-precompile
|
||||
"%DART%" compile kernel bin/build_tool_runner.dart
|
||||
"%DART%" "%PRECOMPILED%" %*
|
||||
)
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
BASEDIR=$(dirname "$0")
|
||||
|
||||
mkdir -p "$CARGOKIT_TOOL_TEMP_DIR"
|
||||
|
||||
cd "$CARGOKIT_TOOL_TEMP_DIR"
|
||||
|
||||
# Write a very simple bin package in temp folder that depends on build_tool package
|
||||
# from Cargokit. This is done to ensure that we don't pollute Cargokit folder
|
||||
# with .dart_tool contents.
|
||||
|
||||
BUILD_TOOL_PKG_DIR="$BASEDIR/build_tool"
|
||||
|
||||
if [[ -z $FLUTTER_ROOT ]]; then # not defined
|
||||
DART=dart
|
||||
else
|
||||
DART="$FLUTTER_ROOT/bin/cache/dart-sdk/bin/dart"
|
||||
fi
|
||||
|
||||
cat << EOF > "pubspec.yaml"
|
||||
name: build_tool_runner
|
||||
version: 1.0.0
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
build_tool:
|
||||
path: "$BUILD_TOOL_PKG_DIR"
|
||||
EOF
|
||||
|
||||
mkdir -p "bin"
|
||||
|
||||
cat << EOF > "bin/build_tool_runner.dart"
|
||||
import 'package:build_tool/build_tool.dart' as build_tool;
|
||||
void main(List<String> args) {
|
||||
build_tool.runMain(args);
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create alias for `shasum` if it does not exist and `sha1sum` exists
|
||||
if ! [ -x "$(command -v shasum)" ] && [ -x "$(command -v sha1sum)" ]; then
|
||||
shopt -s expand_aliases
|
||||
alias shasum="sha1sum"
|
||||
fi
|
||||
|
||||
# Dart run will not cache any package that has a path dependency, which
|
||||
# is the case for our build_tool_runner. So instead we precompile the package
|
||||
# ourselves.
|
||||
# To invalidate the cached kernel we use the hash of ls -LR of the build_tool
|
||||
# package directory. This should be good enough, as the build_tool package
|
||||
# itself is not meant to have any path dependencies.
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
PACKAGE_HASH=$(ls -lTR "$BUILD_TOOL_PKG_DIR" | shasum)
|
||||
else
|
||||
PACKAGE_HASH=$(ls -lR --full-time "$BUILD_TOOL_PKG_DIR" | shasum)
|
||||
fi
|
||||
|
||||
PACKAGE_HASH_FILE=".package_hash"
|
||||
|
||||
if [ -f "$PACKAGE_HASH_FILE" ]; then
|
||||
EXISTING_HASH=$(cat "$PACKAGE_HASH_FILE")
|
||||
if [ "$PACKAGE_HASH" != "$EXISTING_HASH" ]; then
|
||||
rm "$PACKAGE_HASH_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run pub get if needed.
|
||||
if [ ! -f "$PACKAGE_HASH_FILE" ]; then
|
||||
"$DART" pub get --no-precompile
|
||||
"$DART" compile kernel bin/build_tool_runner.dart
|
||||
echo "$PACKAGE_HASH" > "$PACKAGE_HASH_FILE"
|
||||
fi
|
||||
|
||||
set +e
|
||||
|
||||
"$DART" bin/build_tool_runner.dill "$@"
|
||||
|
||||
exit_code=$?
|
||||
|
||||
# 253 means invalid snapshot version.
|
||||
if [ $exit_code == 253 ]; then
|
||||
"$DART" pub get --no-precompile
|
||||
"$DART" compile kernel bin/build_tool_runner.dart
|
||||
"$DART" bin/build_tool_runner.dill "$@"
|
||||
exit_code=$?
|
||||
fi
|
||||
|
||||
exit $exit_code
|
||||
@@ -0,0 +1 @@
|
||||
// This is an empty file to force CocoaPods to create a framework.
|
||||
@@ -0,0 +1,29 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'komet_tlottie'
|
||||
s.version = '0.1.0'
|
||||
s.summary = 'tlottie Lottie renderer for Komet.'
|
||||
s.description = <<-DESC
|
||||
Builds the tlottie C ABI with cargokit and links it into the app, where dart:ffi reaches it.
|
||||
DESC
|
||||
s.homepage = 'https://github.com/dkaraush/tlottie'
|
||||
s.license = { :file => '../LICENSE' }
|
||||
s.author = { 'Komet' => 'dev@komet.pw' }
|
||||
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.dependency 'Flutter'
|
||||
s.platform = :ios, '13.0'
|
||||
|
||||
s.script_phase = {
|
||||
:name => 'Build Rust library',
|
||||
:script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../rust komet_tlottie',
|
||||
:execution_position => :before_compile,
|
||||
:input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'],
|
||||
:output_files => ["${BUILT_PRODUCTS_DIR}/libkomet_tlottie.a"],
|
||||
}
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
|
||||
'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/libkomet_tlottie.a',
|
||||
}
|
||||
end
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
final class TlottieAnimation extends Opaque {}
|
||||
|
||||
typedef _NewNative = Pointer<TlottieAnimation> Function(
|
||||
Pointer<Uint8>, Size, Uint32, Pointer<Void>, Size, Pointer<Void>, Size, Uint32);
|
||||
typedef _New = Pointer<TlottieAnimation> Function(
|
||||
Pointer<Uint8>, int, int, Pointer<Void>, int, Pointer<Void>, int, int);
|
||||
|
||||
typedef _DropNative = Void Function(Pointer<TlottieAnimation>);
|
||||
typedef _Drop = void Function(Pointer<TlottieAnimation>);
|
||||
|
||||
typedef _FrameCountNative = Uint32 Function(Pointer<TlottieAnimation>);
|
||||
typedef _FrameCount = int Function(Pointer<TlottieAnimation>);
|
||||
|
||||
typedef _FrameRateNative = Float Function(Pointer<TlottieAnimation>);
|
||||
typedef _FrameRate = double Function(Pointer<TlottieAnimation>);
|
||||
|
||||
typedef _RenderNative = Int32 Function(
|
||||
Pointer<TlottieAnimation>, Float, Uint32, Uint32, Pointer<Uint32>, Size, Uint32);
|
||||
typedef _Render = int Function(
|
||||
Pointer<TlottieAnimation>, double, int, int, Pointer<Uint32>, int, int);
|
||||
|
||||
class TlottieBindings {
|
||||
TlottieBindings._(DynamicLibrary lib)
|
||||
: _new = lib.lookupFunction<_NewNative, _New>('tlottie_new_with_options'),
|
||||
_drop = lib.lookupFunction<_DropNative, _Drop>('tlottie_drop'),
|
||||
_frameCount =
|
||||
lib.lookupFunction<_FrameCountNative, _FrameCount>('tlottie_frame_count'),
|
||||
_frameRate =
|
||||
lib.lookupFunction<_FrameRateNative, _FrameRate>('tlottie_frame_rate'),
|
||||
_render = lib.lookupFunction<_RenderNative, _Render>('tlottie_render');
|
||||
|
||||
static const int _fitzNone = 0;
|
||||
static const int _channelBgra = 1;
|
||||
static const int _statusOk = 0;
|
||||
|
||||
final _New _new;
|
||||
final _Drop _drop;
|
||||
final _FrameCount _frameCount;
|
||||
final _FrameRate _frameRate;
|
||||
final _Render _render;
|
||||
|
||||
static TlottieBindings? open({String? path}) {
|
||||
try {
|
||||
return TlottieBindings._(_openLibrary(path));
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static DynamicLibrary _openLibrary(String? path) {
|
||||
if (path != null) return DynamicLibrary.open(path);
|
||||
if (Platform.isIOS || Platform.isMacOS) {
|
||||
final process = DynamicLibrary.process();
|
||||
if (process.providesSymbol('tlottie_new_with_options')) return process;
|
||||
return DynamicLibrary.open('komet_tlottie.framework/komet_tlottie');
|
||||
}
|
||||
if (Platform.isWindows) return DynamicLibrary.open('komet_tlottie.dll');
|
||||
return DynamicLibrary.open('libkomet_tlottie.so');
|
||||
}
|
||||
|
||||
Pointer<TlottieAnimation>? parse(Uint8List json) {
|
||||
if (json.isEmpty) return null;
|
||||
final data = malloc<Uint8>(json.length);
|
||||
try {
|
||||
data.asTypedList(json.length).setAll(0, json);
|
||||
final anim = _new(data, json.length, _fitzNone, nullptr, 0, nullptr, 0,
|
||||
_channelBgra);
|
||||
return anim == nullptr ? null : anim;
|
||||
} finally {
|
||||
malloc.free(data);
|
||||
}
|
||||
}
|
||||
|
||||
int frameCount(Pointer<TlottieAnimation> anim) => _frameCount(anim);
|
||||
|
||||
double frameRate(Pointer<TlottieAnimation> anim) => _frameRate(anim);
|
||||
|
||||
bool render(Pointer<TlottieAnimation> anim, int frame, Pointer<Uint32> buffer,
|
||||
int px) {
|
||||
return _render(anim, frame.toDouble(), px, px, buffer, px * px, 1) ==
|
||||
_statusOk;
|
||||
}
|
||||
|
||||
void drop(Pointer<TlottieAnimation> anim) => _drop(anim);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
set(PROJECT_NAME "komet_tlottie")
|
||||
project(${PROJECT_NAME} LANGUAGES CXX)
|
||||
|
||||
include("../cargokit/cmake/cargokit.cmake")
|
||||
apply_cargokit(${PROJECT_NAME} ../rust komet_tlottie "")
|
||||
|
||||
set(komet_tlottie_bundled_libraries
|
||||
"${${PROJECT_NAME}_cargokit_lib}"
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
// This is an empty file to force CocoaPods to create a framework.
|
||||
@@ -0,0 +1,28 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'komet_tlottie'
|
||||
s.version = '0.1.0'
|
||||
s.summary = 'tlottie Lottie renderer for Komet.'
|
||||
s.description = <<-DESC
|
||||
Builds the tlottie C ABI with cargokit and links it into the app, where dart:ffi reaches it.
|
||||
DESC
|
||||
s.homepage = 'https://github.com/dkaraush/tlottie'
|
||||
s.license = { :file => '../LICENSE' }
|
||||
s.author = { 'Komet' => 'dev@komet.pw' }
|
||||
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.dependency 'FlutterMacOS'
|
||||
s.platform = :osx, '10.15'
|
||||
|
||||
s.script_phase = {
|
||||
:name => 'Build Rust library',
|
||||
:script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../rust komet_tlottie',
|
||||
:execution_position => :before_compile,
|
||||
:input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'],
|
||||
:output_files => ["${BUILT_PRODUCTS_DIR}/libkomet_tlottie.a"],
|
||||
}
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/libkomet_tlottie.a',
|
||||
}
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
name: komet_tlottie
|
||||
description: Native Lottie renderer (tlottie, Rust) for Komet's animated stickers, animoji and reactions.
|
||||
version: 0.1.0
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.3.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
ffi: ^2.1.0
|
||||
|
||||
flutter:
|
||||
plugin:
|
||||
platforms:
|
||||
android:
|
||||
ffiPlugin: true
|
||||
ios:
|
||||
ffiPlugin: true
|
||||
linux:
|
||||
ffiPlugin: true
|
||||
macos:
|
||||
ffiPlugin: true
|
||||
windows:
|
||||
ffiPlugin: true
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "komet_tlottie"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"tlottie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tlottie"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/dkaraush/tlottie?rev=31f1b542f88e7b4be9a01e749920d857535fc715#31f1b542f88e7b4be9a01e749920d857535fc715"
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "komet_tlottie"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Builds the tlottie C ABI into a library the Komet app loads through dart:ffi"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
tlottie = { git = "https://github.com/dkaraush/tlottie", rev = "31f1b542f88e7b4be9a01e749920d857535fc715", features = ["c-api"] }
|
||||
|
||||
[workspace]
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
|
||||
[profile.dev.package.tlottie]
|
||||
opt-level = 3
|
||||
@@ -0,0 +1 @@
|
||||
pub use tlottie;
|
||||
@@ -0,0 +1,12 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
set(PROJECT_NAME "komet_tlottie")
|
||||
project(${PROJECT_NAME} LANGUAGES CXX)
|
||||
|
||||
include("../cargokit/cmake/cargokit.cmake")
|
||||
apply_cargokit(${PROJECT_NAME} ../rust komet_tlottie "")
|
||||
|
||||
set(komet_tlottie_bundled_libraries
|
||||
"${${PROJECT_NAME}_cargokit_lib}"
|
||||
PARENT_SCOPE
|
||||
)
|
||||
Reference in New Issue
Block a user