From e6c8c955d323d0329acc28970e76e001a22f5946 Mon Sep 17 00:00:00 2001 From: slopbot9k Date: Wed, 21 Oct 2026 23:55:31 -0600 Subject: [PATCH 1/9] Build the Rust library through dnix: a verified prebuilt by default, Nix otherwise The build hook is dnix's. By default it downloads the library of the release dnix.lock names and verifies its hash; in source mode it builds the same library with Nix and verifies the same hash; in dev mode it builds with Nix for work on the Rust code. dnix.json names the library, the asset, the Rust workspace and crate, the Rust and Xcode versions, the deployment floors, and the ten targets the prebuilts covered; dnix.lock records what the source builds to and names release native-fa0d223adae4. The flake hands dnix the configuration. The build's extras are declared there too: protoc for prost-build in the libp2p crates, the lmdb mutex flags on macOS, and a static libc++ on Android. OpenSSL is vendored only where native-tls links it, Linux and Android; Apple and Windows use their own TLS. The prebuilt producer, manifest, smoke tests and their tests go; two-line tools update the lock and verify one platform against the release. --- .gitignore | 3 + dnix.json | 51 +++ dnix.lock | 16 + example/pubspec.lock | 41 +- flake.lock | 123 ++++++ flake.nix | 10 + hook/build.dart | 82 +--- hook/src/prebuilt.dart | 377 ------------------ hook/src/source_fingerprint.dart | 57 --- pubspec.yaml | 6 +- rust/Cargo.toml | 9 +- test/prebuilt_producer_test.dart | 315 --------------- test/prebuilt_test.dart | 631 ------------------------------- tool/build_prebuilt.dart | 369 ------------------ tool/prebuilt_manifest.dart | 191 ---------- tool/smoke_prebuilt.dart | 78 ---- tool/src/build_support.dart | 95 ----- tool/test_prebuilt.dart | 41 -- tool/update_lock.dart | 4 + tool/verify_release.dart | 4 + 20 files changed, 230 insertions(+), 2273 deletions(-) create mode 100644 dnix.json create mode 100644 dnix.lock create mode 100644 flake.lock create mode 100644 flake.nix delete mode 100644 hook/src/prebuilt.dart delete mode 100644 hook/src/source_fingerprint.dart delete mode 100644 test/prebuilt_producer_test.dart delete mode 100644 test/prebuilt_test.dart delete mode 100644 tool/build_prebuilt.dart delete mode 100644 tool/prebuilt_manifest.dart delete mode 100644 tool/smoke_prebuilt.dart delete mode 100644 tool/src/build_support.dart delete mode 100644 tool/test_prebuilt.dart create mode 100644 tool/update_lock.dart create mode 100644 tool/verify_release.dart diff --git a/.gitignore b/.gitignore index 96486fd9..087637a8 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ migrate_working_dir/ .dart_tool/ .packages build/ + +# Per-checkout dnix build mode override (see README). +.dnix-build diff --git a/dnix.json b/dnix.json new file mode 100644 index 00000000..66f3e526 --- /dev/null +++ b/dnix.json @@ -0,0 +1,51 @@ +{ + "name": "flutter_libmwc", + "repository": "https://github.com/cypherstack/flutter_libmwc", + "library": "mwc_wallet", + "asset": "mwc.dart", + "rust": { + "workspace": "rust", + "crate": "mwc-wallet", + "cflags": { + "aarch64-apple-darwin": [ + "-DMDB_USE_POSIX_MUTEX=1", + "-DMDB_USE_ROBUST=0" + ], + "x86_64-apple-darwin": [ + "-DMDB_USE_POSIX_MUTEX=1", + "-DMDB_USE_ROBUST=0" + ] + }, + "env": { + "CXXSTDLIB_aarch64_linux_android": "static:-bundle=c++", + "CXXSTDLIB_armv7_linux_androideabi": "static:-bundle=c++", + "CXXSTDLIB_x86_64_linux_android": "static:-bundle=c++", + "PROTOC": "protoc" + }, + "tools": [ + "protobuf" + ] + }, + "toolchains": { + "rust": "1.99.1", + "xcode": "27.0" + }, + "floors": { + "macos": 11, + "ios": 13, + "android": 21, + "glibc": "2.28" + }, + "targets": [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-apple-ios", + "aarch64-apple-ios-sim", + "x86_64-apple-ios", + "aarch64-linux-android", + "armv7-linux-androideabi", + "x86_64-linux-android", + "x86_64-unknown-linux-gnu", + "x86_64-pc-windows-msvc" + ] +} diff --git a/dnix.lock b/dnix.lock new file mode 100644 index 00000000..529868e3 --- /dev/null +++ b/dnix.lock @@ -0,0 +1,16 @@ +{ + "release": "fa0d223adae4", + "date": "2026-10-22", + "files": { + "aarch64-apple-darwin/libmwc_wallet.dylib": "3b77d160ea9a574c941b69745eee94a85dd97f7d6728c6b50c27d0a06130b27c", + "aarch64-apple-ios-sim/libmwc_wallet.dylib": "abaf2be17f304dcfff8009a3f6634c7465d9a270c0023773828105678c4cb421", + "aarch64-apple-ios/libmwc_wallet.dylib": "b8ac640a488ab1bce7d2eb1144c3957f46bf1c0d0206cbdfaec6aac3053c0fa8", + "aarch64-linux-android/libmwc_wallet.so": "8b945b20e0a0428e865a589caa5b78c39843ceb7e32da22e9375ff9a44720419", + "armv7-linux-androideabi/libmwc_wallet.so": "86c5e372856cfdbf21210405431635d36da9b6312b92fdf3c2c8b49e1fe553b3", + "x86_64-apple-darwin/libmwc_wallet.dylib": "9318bffc19ac4685db4d66edf713f794f1e6f55bb640ac7a556d4110143deae7", + "x86_64-apple-ios/libmwc_wallet.dylib": "77bf055d942b3444c6da8a4b11d5bf059882fb2b4be02f146a78a4d87a658412", + "x86_64-linux-android/libmwc_wallet.so": "16b547953fadcb97daddc27819d1579b5ac8d35527068888ca666b305e279eb6", + "x86_64-pc-windows-msvc/mwc_wallet.dll": "e4726f9588f3b08b4253ea639ed87ecc240a6dc0c8488cd9910e735582be4ad8", + "x86_64-unknown-linux-gnu/libmwc_wallet.so": "4bcd009278dd3bfdc7ec0d873dcb7c9a7f022d31365ced5064a54882cb1394a5" + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 6da43790..0ea9ebe3 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -65,6 +65,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.3" + dnix: + dependency: transitive + description: + path: "." + ref: "448c5da6e61b3f0e62b4eae6d08b49158a9e45b2" + resolved-ref: "448c5da6e61b3f0e62b4eae6d08b49158a9e45b2" + url: "https://github.com/cypherstack/dnix.git" + source: git + version: "0.1.0" fake_async: dependency: transitive description: @@ -170,14 +179,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" - native_toolchain_rust: - dependency: transitive - description: - name: native_toolchain_rust - sha256: "8469a0e32a47a5c84b7fd300de9fd0e8082aac76c37922986324f5082a58350d" - url: "https://pub.dev" - source: hosted - version: "1.0.6" numeric_keyboard: dependency: "direct main" description: @@ -242,14 +243,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" - url: "https://pub.dev" - source: hosted - version: "7.0.2" platform: dependency: transitive description: @@ -343,14 +336,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.12" - toml: - dependency: transitive - description: - name: toml - sha256: "35a35f782228656a2af31e8c73d1353cc4ef3d683fd68af1111b44631879c05e" - url: "https://pub.dev" - source: hosted - version: "0.18.0" typed_data: dependency: transitive description: @@ -375,14 +360,6 @@ packages: url: "https://pub.dev" source: hosted version: "14.3.1" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" xdg_directories: dependency: transitive description: diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..4e0af1be --- /dev/null +++ b/flake.lock @@ -0,0 +1,123 @@ +{ + "nodes": { + "dnix": { + "inputs": { + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay", + "zig-overlay": "zig-overlay" + }, + "locked": { + "lastModified": 1792648476, + "narHash": "sha256-sNBlb09ut3gmCv2vMg+jEiUdF+slLC+T/9S+WBE6p3A=", + "owner": "cypherstack", + "repo": "dnix", + "rev": "448c5da6e61b3f0e62b4eae6d08b49158a9e45b2", + "type": "github" + }, + "original": { + "owner": "cypherstack", + "repo": "dnix", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1792502884, + "narHash": "sha256-IoNisKcGbUjdd45qCqywGMCYmZ5EnkxezvLII0FWwNA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "02c087f3fe673c9e317f206a627cb6b910336dbe", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "dnix": "dnix" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "dnix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1792567368, + "narHash": "sha256-mrM+zFDRO1pI85t2SEq5R739BIq7Nv5XkFtVZK9nkZM=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "83dd732f2428827f35d997deae108a2ced5125f4", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "flake": false, + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "zig-overlay": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": [ + "dnix", + "nixpkgs" + ], + "systems": "systems" + }, + "locked": { + "lastModified": 1792585651, + "narHash": "sha256-Kg8Qdi6ur+K5hNJwav0pEqdylGd7vXxEPeBvaKW5nxA=", + "owner": "mitchellh", + "repo": "zig-overlay", + "rev": "98819174f1bb7c9961c3ebbb00d19fc4e1c1960d", + "type": "github" + }, + "original": { + "owner": "mitchellh", + "repo": "zig-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..4b75d152 --- /dev/null +++ b/flake.nix @@ -0,0 +1,10 @@ +{ + description = "Reproducible reference builds of flutter_libmwc's Rust library"; + + inputs.dnix.url = "github:cypherstack/dnix"; + + outputs = { self, dnix, ... }: dnix.lib.package { + src = self; + config = ./dnix.json; + }; +} diff --git a/hook/build.dart b/hook/build.dart index c1748df1..7997f795 100644 --- a/hook/build.dart +++ b/hook/build.dart @@ -1,81 +1,3 @@ -import 'dart:io'; +import 'package:dnix/dnix.dart'; -import 'package:code_assets/code_assets.dart'; -import 'package:hooks/hooks.dart'; -import 'package:native_toolchain_rust/native_toolchain_rust.dart'; - -import 'src/prebuilt.dart'; - -void main(List args) async { - await build(args, (input, output) async { - if (!input.config.buildCodeAssets) return; - if (await usePrebuilt(input, output)) return; - - final code = input.config.code; - - // OpenSSL refuses to build for a non-Windows target using Windows' perl. - if (Platform.isWindows && code.targetOS != OS.windows) { - throw UnsupportedError( - 'Cannot build ${code.targetOS} from Windows; use Linux, macOS, or WSL.', - ); - } - - final environment = { - if (code.targetOS == .iOS) - "IPHONEOS_DEPLOYMENT_TARGET": "${code.iOS.targetVersion}.0", - if (code.targetOS == .macOS) ...{ - "MACOSX_DEPLOYMENT_TARGET": "${code.macOS.targetVersion}.0", - // LMDB defaults to SysV semaphores on Apple targets, which the App - // Sandbox denies (EPERM on mdb_env_open). Keep the lock in lock.mdb via - // pthread mutexes instead; macOS has no robust mutexes, so disable those. - "CFLAGS_${code.targetArchitecture == .arm64 ? "aarch64" : "x86_64"}_apple_darwin": - "-DMDB_USE_POSIX_MUTEX=1 -DMDB_USE_ROBUST=0", - }, - }; - if (code.targetOS == OS.android) { - // native_toolchain_rust 1.0.6 defaults to API 35. Honor Flutter's minimum. - final (rustTarget, clangTarget) = switch (code.targetArchitecture) { - Architecture.arm => ( - 'armv7-linux-androideabi', - 'armv7a-linux-androideabi', - ), - Architecture.arm64 => ( - 'aarch64-linux-android', - 'aarch64-linux-android', - ), - Architecture.x64 => ('x86_64-linux-android', 'x86_64-linux-android'), - _ => throw UnsupportedError('Unsupported Android architecture'), - }; - final bin = code.cCompiler!.compiler.resolve('.'); - final suffix = Platform.isWindows ? '.cmd' : ''; - final ndkTarget = '$clangTarget${code.android.targetNdkApi}'; - final compiler = '$ndkTarget-clang'; - final cc = bin.resolve('$compiler$suffix').toFilePath(); - final target = rustTarget.replaceAll('-', '_'); - environment.addAll({ - 'CC_$target': cc, - 'CXX_$target': bin.resolve('$compiler++$suffix').toFilePath(), - // cc also supplies --target; keep the API level on the final argument. - 'CFLAGS_$target': '--target=$ndkTarget', - 'CXXFLAGS_$target': '--target=$ndkTarget', - // The NDK's libc++.a linker script includes libc++abi as well. - // Defer it to the final link instead of bundling the script in an rlib. - 'CXXSTDLIB_$target': 'static:-bundle=c++', - 'CARGO_TARGET_${target.toUpperCase()}_LINKER': cc, - }); - } - - await RustBuilder( - assetName: 'mwc.dart', - extraCargoBuildArgs: ['--lib', '--locked'], - extraCargoEnvironmentVariables: environment, - ).run(input: input, output: output); - output.dependencies.addAll( - [ - 'rust/Cargo.toml', - 'rust/Cargo.lock', - 'rust/rust-toolchain.toml', - ].map(input.packageRoot.resolve), - ); - }); -} +void main(List args) => buildNativeLibrary(args); diff --git a/hook/src/prebuilt.dart b/hook/src/prebuilt.dart deleted file mode 100644 index e0acd497..00000000 --- a/hook/src/prebuilt.dart +++ /dev/null @@ -1,377 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:code_assets/code_assets.dart'; -import 'package:crypto/crypto.dart' as crypto; -import 'package:hooks/hooks.dart'; - -import 'source_fingerprint.dart'; - -const _maximumArtifactBytes = 2 * 1024 * 1024 * 1024; -final _sha256Pattern = RegExp(r'^[0-9a-fA-F]{64}$'); - -String _digest(Object? value, String label) { - if (value is! String || !_sha256Pattern.hasMatch(value)) { - throw FormatException( - '$label must be a SHA-256 digest (64 hex characters).', - ); - } - return value.toLowerCase(); -} - -void _requireHttps(Uri uri) { - if (uri.scheme != 'https' || - uri.host.isEmpty || - uri.userInfo.isNotEmpty || - uri.hasFragment) { - throw FormatException( - 'Prebuilt URLs must use HTTPS without credentials or fragments.', - ); - } -} - -class PrebuiltOptions { - final String nativeBuild; - final Uri? manifestUrl; - final String? manifestSha256; - - PrebuiltOptions._(this.nativeBuild, this.manifestUrl, this.manifestSha256); - - factory PrebuiltOptions.fromInput(BuildInput input) { - final defines = input.userDefines; - final mode = defines['native_build'] ?? 'source'; - if (mode != 'source' && mode != 'prebuilt') { - throw FormatException( - 'flutter_libmwc.native_build must be source or prebuilt.', - ); - } - if (mode == 'source') { - return PrebuiltOptions._('source', null, null); - } - final rawUrl = defines['prebuilt_manifest_url']; - if (rawUrl is! String || rawUrl.isEmpty) { - throw FormatException('Prebuilt mode requires prebuilt_manifest_url.'); - } - final url = Uri.parse(rawUrl); - _requireHttps(url); - final digest = _digest( - defines['prebuilt_manifest_sha256'], - 'prebuilt_manifest_sha256', - ); - return PrebuiltOptions._('prebuilt', url, digest); - } -} - -class PrebuiltTarget { - final String triple; - final String linkMode; - final int? minimumVersion; - final String libraryFileName; - - PrebuiltTarget._( - this.triple, - this.linkMode, - this.minimumVersion, - this.libraryFileName, - ); - - factory PrebuiltTarget.fromConfig(CodeConfig code) { - if (code.sanitizer != null) { - throw UnsupportedError('Prebuilt binaries do not support sanitizers.'); - } - final triple = switch ((code.targetOS, code.targetArchitecture)) { - (OS.android, Architecture.arm) => 'armv7-linux-androideabi', - (OS.android, Architecture.arm64) => 'aarch64-linux-android', - (OS.android, Architecture.x64) => 'x86_64-linux-android', - (OS.iOS, Architecture.arm64) when code.iOS.targetSdk == IOSSdk.iPhoneOS => - 'aarch64-apple-ios', - (OS.iOS, Architecture.arm64) - when code.iOS.targetSdk == IOSSdk.iPhoneSimulator => - 'aarch64-apple-ios-sim', - (OS.iOS, Architecture.x64) - when code.iOS.targetSdk == IOSSdk.iPhoneSimulator => - 'x86_64-apple-ios', - (OS.macOS, Architecture.arm64) => 'aarch64-apple-darwin', - (OS.macOS, Architecture.x64) => 'x86_64-apple-darwin', - (OS.linux, Architecture.arm64) => 'aarch64-unknown-linux-gnu', - (OS.linux, Architecture.x64) => 'x86_64-unknown-linux-gnu', - (OS.windows, Architecture.x64) => 'x86_64-pc-windows-msvc', - _ => throw UnsupportedError( - 'No prebuilt target for ${code.targetOS}/${code.targetArchitecture}.', - ), - }; - final mode = switch (code.linkModePreference) { - LinkModePreference.dynamic || - LinkModePreference.preferDynamic => 'dynamic', - LinkModePreference.static || LinkModePreference.preferStatic => 'static', - _ => throw UnsupportedError('Unsupported native link mode.'), - }; - final minimum = switch (code.targetOS) { - OS.android => code.android.targetNdkApi, - OS.iOS => code.iOS.targetVersion, - OS.macOS => code.macOS.targetVersion, - _ => null, - }; - final linkMode = mode == 'static' - ? StaticLinking() - : DynamicLoadingBundled(); - return PrebuiltTarget._( - triple, - mode, - minimum, - code.targetOS.libraryFileName('mwc_wallet', linkMode), - ); - } -} - -class PrebuiltArtifact { - final String file; - final String sha256; - final int size; - - PrebuiltArtifact._(this.file, this.sha256, this.size); - - factory PrebuiltArtifact.select( - Map manifest, - PrebuiltTarget target, { - required String sourceSha256, - }) { - if (manifest['schema_version'] != 1 || - manifest['package'] != 'flutter_libmwc') { - throw FormatException('Unsupported flutter_libmwc prebuilt manifest.'); - } - if (_digest(manifest['source_sha256'], 'source_sha256') != sourceSha256) { - throw StateError( - 'Prebuilt source fingerprint does not match this package. ' - 'Select a release built from this source or set native_build: source.', - ); - } - final artifacts = manifest['artifacts']; - if (artifacts is! List || - artifacts.any((entry) => entry is! Map)) { - throw FormatException('Manifest artifacts must be a list of objects.'); - } - final matches = artifacts - .cast>() - .where( - (entry) => - entry['target'] == target.triple && - entry['link_mode'] == target.linkMode, - ) - .toList(); - if (matches.length != 1) { - throw StateError( - 'Expected one ${target.triple}/${target.linkMode} prebuilt; ' - 'found ${matches.length}. Select another release or use native_build: source.', - ); - } - final entry = matches.single; - final file = entry['file']; - if (file is! String || - !RegExp(r'^[a-zA-Z0-9_-][a-zA-Z0-9_.-]*\.(so|dylib|dll|a|lib)$') - .hasMatch(file)) { - throw FormatException( - 'Prebuilt artifact file must be a library basename.', - ); - } - final digest = _digest(entry['sha256'], 'artifact sha256'); - final size = entry['size']; - if (size is! int || size <= 0 || size > _maximumArtifactBytes) { - throw FormatException( - 'Prebuilt artifact size must be between 1 byte and 2 GiB.', - ); - } - if (target.minimumVersion != null) { - final minimum = entry['minimum_os_version']; - if (minimum is! int || minimum <= 0) { - throw FormatException('Artifact must declare minimum_os_version.'); - } - if (minimum > target.minimumVersion!) { - throw UnsupportedError( - 'Prebuilt requires OS/API $minimum, but the app targets ' - '${target.minimumVersion}. Use native_build: source or a compatible release.', - ); - } - } - if (target.triple.endsWith('linux-gnu')) { - final glibc = entry['minimum_glibc_version']; - if (glibc is! String || !RegExp(r'^\d+\.\d+$').hasMatch(glibc)) { - throw FormatException( - 'Linux prebuilt must declare minimum_glibc_version.', - ); - } - } - return PrebuiltArtifact._(file, digest, size); - } -} - -/// Downloads fresh bytes and verifies them before making the file available. -class VerifiedDownloader { - final Directory directory; - final HttpClient? client; - - VerifiedDownloader(this.directory, {this.client}); - - Future get( - Uri uri, - String digest, { - int? size, - int maxBytes = _maximumArtifactBytes, - }) async { - _requireHttps(uri); - digest = _digest(digest, 'download digest'); - await directory.create(recursive: true); - final file = File.fromUri(directory.uri.resolve(digest)); - final temporaryDirectory = await directory.createTemp('.download-'); - final temporary = File.fromUri(temporaryDirectory.uri.resolve('asset')); - final http = client ?? HttpClient(); - try { - http.autoUncompress = false; - http.connectionTimeout = const Duration(seconds: 30); - final download = _download(http, uri, temporary, size ?? maxBytes); - try { - await download.timeout(const Duration(minutes: 10)); - } on TimeoutException { - // Stop I/O before deleting the partial download, including on Windows. - http.close(force: true); - try { - await download; - } catch (_) { - // Preserve the timeout that triggered cancellation. - } - rethrow; - } - await _verify(temporary, digest, size, maxBytes); - return await temporary.rename(file.path); - } finally { - if (client == null) http.close(force: true); - await temporaryDirectory.delete(recursive: true); - } - } - - Future _verify(File file, String digest, int? size, int maximum) async { - final length = await file.length(); - if (length > maximum || - (size != null && length != size) || - (await crypto.sha256.bind(file.openRead()).first).toString() != - digest) { - throw StateError( - 'Prebuilt integrity check failed for ${file.path}. ' - 'Never change the pin to bypass verification.', - ); - } - } - - Future _download( - HttpClient http, - Uri uri, - File file, - int maximum, - ) async { - for (var redirects = 0; redirects <= 5; redirects++) { - _requireHttps(uri); - final request = await http - .getUrl(uri) - .timeout(const Duration(seconds: 30)); - request.followRedirects = false; - request.headers.set(HttpHeaders.acceptEncodingHeader, 'identity'); - final response = await request.close().timeout( - const Duration(seconds: 30), - ); - if ([301, 302, 303, 307, 308].contains(response.statusCode)) { - final location = response.headers.value(HttpHeaders.locationHeader); - await response.listen(null).cancel(); - if (location == null) - throw HttpException('Prebuilt redirect has no Location.'); - uri = uri.resolve(location); - continue; - } - if (response.statusCode != HttpStatus.ok) { - await response.listen(null).cancel(); - throw HttpException( - 'Prebuilt download returned HTTP ${response.statusCode}. ' - 'Check that the release is published and publicly accessible.', - ); - } - if (response.contentLength > maximum) { - await response.listen(null).cancel(); - throw StateError('Prebuilt download exceeds the expected size.'); - } - final sink = file.openWrite(); - var received = 0; - try { - await for (final bytes in response.timeout( - const Duration(seconds: 30), - )) { - received += bytes.length; - if (received > maximum) - throw StateError('Prebuilt download exceeds the expected size.'); - sink.add(bytes); - // Apply disk backpressure instead of buffering a large library in RAM. - await sink.flush(); - } - } finally { - await sink.close(); - } - return; - } - throw HttpException('Too many prebuilt download redirects.'); - } -} - -Future usePrebuilt( - BuildInput input, - BuildOutputBuilder output, { - HttpClient? client, -}) async { - final options = PrebuiltOptions.fromInput(input); - if (options.nativeBuild == 'source') return false; - final target = PrebuiltTarget.fromConfig(input.config.code); - final source = await sourceFingerprint(input.packageRoot); - output.dependencies.addAll(source.dependencies); - // Keep all downloads in Flutter's hook output so flutter clean removes them. - final outputDirectory = Directory.fromUri(input.outputDirectory); - await outputDirectory.create(recursive: true); - final downloads = await outputDirectory.createTemp('.prebuilt-'); - try { - final downloader = VerifiedDownloader(downloads, client: client); - final manifestFile = await downloader.get( - options.manifestUrl!, - options.manifestSha256!, - maxBytes: 1024 * 1024, - ); - final json = jsonDecode(await manifestFile.readAsString()); - if (json is! Map) - throw FormatException('Manifest must be an object.'); - final artifact = PrebuiltArtifact.select( - json, - target, - sourceSha256: source.sha256, - ); - final downloaded = await downloader.get( - options.manifestUrl!.resolve(artifact.file), - artifact.sha256, - size: artifact.size, - ); - final destination = input.outputDirectory.resolve(target.libraryFileName); - final bundled = await downloaded.rename(destination.toFilePath()); - // The hook runner tracks CodeAsset output bytes separately from inputs. - output.assets.code.add( - CodeAsset( - package: input.packageName, - name: 'mwc.dart', - file: bundled.uri, - linkMode: target.linkMode == 'static' - ? StaticLinking() - : DynamicLoadingBundled(), - ), - ); - } finally { - await downloads.delete(recursive: true); - } - print( - 'Using verified flutter_libmwc prebuilt for ${target.triple}/${target.linkMode}.', - ); - return true; -} diff --git a/hook/src/source_fingerprint.dart b/hook/src/source_fingerprint.dart deleted file mode 100644 index 6299869f..00000000 --- a/hook/src/source_fingerprint.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:crypto/crypto.dart' as crypto; - -/// Shared by the build hook and release tooling. Include paths as well as bytes -/// so moving, adding, or deleting native sources invalidates a release. -Future<({String sha256, List dependencies})> sourceFingerprint( - Uri packageRoot, -) async { - final files = {}; - final dependencies = []; - for (final path in [ - 'rust/Cargo.toml', - 'rust/Cargo.lock', - 'rust/rust-toolchain.toml', - 'lib/mwc.dart', - 'tool/build_prebuilt.dart', - 'tool/src/build_support.dart', - ]) { - files[path] = File.fromUri(packageRoot.resolve(path)); - } - for (final path in ['rust/src/', 'hook/']) { - final directory = Directory.fromUri(packageRoot.resolve(path)); - dependencies.add(directory.uri); - await for (final entity in directory.list( - recursive: true, - followLinks: false, - )) { - if (entity is Link) { - throw StateError( - 'Prebuilt source inputs cannot be symlinks: ${entity.path}', - ); - } - if (entity is Directory) dependencies.add(entity.uri); - if (entity is File) { - final relative = entity.uri.path.substring(packageRoot.path.length); - files[Uri.decodeComponent(relative)] = entity; - } - } - } - final paths = files.keys.toList()..sort(); - final framed = StringBuffer(); - for (final path in paths) { - final file = files[path]!; - if (await FileSystemEntity.isLink(file.path)) { - throw StateError('Prebuilt source inputs cannot be symlinks: $path'); - } - final digest = await crypto.sha256.bind(file.openRead()).first; - framed.write('$path\u0000$digest\n'); - dependencies.add(file.uri); - } - return ( - sha256: crypto.sha256.convert(utf8.encode(framed.toString())).toString(), - dependencies: dependencies, - ); -} diff --git a/pubspec.yaml b/pubspec.yaml index b49dcbd2..2a8f28e4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,8 +14,10 @@ dependencies: mutex: ^3.0.0 code_assets: ^1.2.1 hooks: ^2.2.0 - native_toolchain_rust: ^1.0.6 - crypto: ^3.0.7 + dnix: + git: + url: https://github.com/cypherstack/dnix.git + ref: 448c5da6e61b3f0e62b4eae6d08b49158a9e45b2 dev_dependencies: diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 63c065a7..49572b50 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -48,11 +48,10 @@ ffi_helpers = "0.3.0" anyhow = "1.0.69" -# We never call openssl ourselves. This makes native-tls build OpenSSL from -# source instead of looking for a system copy, which is what lets Android and -# iOS cross-compile. Windows is excluded because native-tls uses SChannel -# there, and building OpenSSL produces paths past Windows' 260-character limit. -[target.'cfg(not(target_os = "windows"))'.dependencies] +# We never call openssl ourselves. Built from source where native-tls uses +# OpenSSL (Linux, Android) instead of looking for a system copy; Apple and +# Windows use their own TLS and never link it. +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] openssl = { version = "0.10", features = ["vendored"] } [lib] diff --git a/test/prebuilt_producer_test.dart b/test/prebuilt_producer_test.dart deleted file mode 100644 index 236bb2f8..00000000 --- a/test/prebuilt_producer_test.dart +++ /dev/null @@ -1,315 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:test/test.dart'; - -import '../tool/build_prebuilt.dart'; -import '../tool/prebuilt_manifest.dart'; -import '../tool/src/build_support.dart'; - -void main() { - late Directory temporary; - late Directory source; - late Directory artifacts; - late Directory output; - late File library; - late File fragmentFile; - late Map fragment; - - Future saveFragment() => - fragmentFile.writeAsString(jsonEncode(fragment)); - - Future assemble({bool requireAll = false}) => - assemblePrebuilts( - artifacts, - output, - root: source.uri, - requireAll: requireAll, - ); - - Map firstEntry() => - (fragment['artifacts'] as List).first as Map; - - Matcher invalid(String message) => throwsA( - isA().having( - (error) => error.message, - 'message', - contains(message), - ), - ); - - setUp(() async { - temporary = await Directory.systemTemp.createTemp('mwc-producer-test-'); - source = Directory.fromUri(temporary.uri.resolve('source/')); - artifacts = await Directory.fromUri(temporary.uri.resolve('artifacts/')) - .create(); - output = Directory.fromUri(temporary.uri.resolve('release/')); - for (final relative in [ - 'rust/Cargo.toml', - 'rust/Cargo.lock', - 'rust/rust-toolchain.toml', - 'rust/src/lib.rs', - 'hook/build.dart', - 'lib/mwc.dart', - 'tool/build_prebuilt.dart', - 'tool/src/build_support.dart', - ]) { - final file = File.fromUri(source.uri.resolve(relative)); - await file.parent.create(recursive: true); - await file.writeAsString('$relative\n'); - } - library = File.fromUri(artifacts.uri.resolve('mwc_wallet-test.dll')); - await library.writeAsString('native-binary-fixture'); - fragment = { - 'schema_version': 1, - 'package': 'flutter_libmwc', - 'source_sha256': await sourceSha256(source.uri), - 'artifacts': [ - { - 'target': 'x86_64-pc-windows-msvc', - 'link_mode': 'dynamic', - 'file': library.uri.pathSegments.last, - 'size': await library.length(), - 'sha256': await sha256File(library), - }, - ], - }; - fragmentFile = File.fromUri(artifacts.uri.resolve('fragment.json')); - await saveFragment(); - }); - - tearDown(() => temporary.delete(recursive: true)); - - test('source identity includes newly added nested Rust files', () async { - final original = await sourceSha256(source.uri); - final file = File.fromUri(source.uri.resolve('rust/src/new/module.rs')); - await file.parent.create(); - await file.writeAsString('fn changed() {}'); - expect(await sourceSha256(source.uri), isNot(original)); - }); - - test( - 'source identity ignores docs and includes release build policy', - () async { - final original = await sourceSha256(source.uri); - await File.fromUri(source.uri.resolve('README.md')).writeAsString('docs'); - expect(await sourceSha256(source.uri), original); - await File.fromUri(source.uri.resolve('tool/src/build_support.dart')) - .writeAsString('new policy'); - expect(await sourceSha256(source.uri), isNot(original)); - }, - ); - - test('assembled pin matches exact manifest', () async { - final result = await assemble(); - expect( - result.sha256, - await sha256File(File.fromUri(output.uri.resolve('manifest.json'))), - ); - expect( - await File.fromUri(output.uri.resolve('mwc_wallet-test.dll')) - .readAsBytes(), - await library.readAsBytes(), - ); - final pinFile = File.fromUri(output.uri.resolve('manifest.sha256')); - expect(await pinFile.readAsString(), '${result.sha256} manifest.json\n'); - }); - - test( - 'same-length corrupted artifact fails before output publication', - () async { - final bytes = await library.readAsBytes(); - bytes[0] ^= 1; - await library.writeAsBytes(bytes); - await expectLater(assemble(), invalid('checksum mismatch')); - expect(await output.exists(), isFalse); - }, - ); - - test('changed native sources reject old fragments', () async { - await File.fromUri(source.uri.resolve('hook/build.dart')) - .writeAsString('new source'); - await expectLater(assemble(), invalid('fingerprint mismatch')); - }); - - test('artifact filename cannot traverse out of its directory', () async { - firstEntry()['file'] = '../outside.dll'; - await saveFragment(); - await expectLater(assemble(), invalid('Unsafe artifact filename')); - }); - - test('same target and mode cannot appear twice', () async { - (fragment['artifacts'] as List).add( - Map.from(firstEntry()), - ); - await saveFragment(); - await expectLater(assemble(), invalid('Duplicate artifact')); - }); - - test('full release requires every supported target and link mode', () async { - await expectLater( - assemble(requireAll: true), - invalid('Missing release artifacts'), - ); - final entries = >[]; - for (final target in releaseTargets.entries) { - for (final mode in ['static', 'dynamic']) { - final suffix = libraryNames(target.key)[mode]!.split('.').last; - final filename = 'mwc_wallet-${target.key}-$mode.$suffix'; - final file = File.fromUri(artifacts.uri.resolve(filename)); - await file.writeAsString('${target.key}/$mode'); - entries.add({ - 'target': target.key, - 'link_mode': mode, - 'file': filename, - 'size': await file.length(), - 'sha256': await sha256File(file), - ...target.value, - }); - } - } - fragment['artifacts'] = entries; - await saveFragment(); - final result = await assemble(requireAll: true); - expect(result.manifest['artifacts'], hasLength(releaseTargets.length * 2)); - }); - - test('existing release output is never mixed with a new release', () async { - await assemble(); - await expectLater( - assemble(), - throwsA( - isA().having( - (error) => error.message, - 'message', - contains('Release output must be empty'), - ), - ), - ); - }); - - test('incorrect advertised deployment minimum is rejected', () async { - firstEntry().addAll({ - 'target': 'aarch64-linux-android', - 'minimum_os_version': 16, - }); - await saveFragment(); - await expectLater(assemble(), invalid('Incorrect minimum_os_version')); - }); - - test('noninteger sizes and unsupported schemas are rejected', () async { - firstEntry()['size'] = true; - await saveFragment(); - await expectLater(assemble(), invalid('Artifact size mismatch')); - firstEntry()['size'] = await library.length(); - fragment['schema_version'] = true; - await saveFragment(); - await expectLater(assemble(), invalid('Unsupported build fragment')); - }); - - test( - 'artifact symlinks are rejected', - () async { - final original = File.fromUri(temporary.uri.resolve('original.dll')); - await library.rename(original.path); - await Link(library.path).create(original.path); - await expectLater(assemble(), invalid('Missing or nonregular artifact')); - }, - skip: Platform.isWindows - ? 'Creating links requires Windows privileges' - : false, - ); - - test( - 'build CLI dry-run preserves release policy without invoking Rust', - () async { - final packageConfig = packageRoot - .resolve('.dart_tool/package_config.json') - .toFilePath(); - final process = await Process.run(Platform.resolvedExecutable, [ - '--packages=$packageConfig', - packageRoot.resolve('tool/build_prebuilt.dart').toFilePath(), - '--target', - 'aarch64-linux-android', - '--dry-run', - ], workingDirectory: packageRoot.toFilePath()); - expect(process.exitCode, 0, reason: process.stderr.toString()); - final plan = jsonDecode(process.stdout as String) as Map; - expect(plan['source_sha256'], await sourceSha256()); - expect(plan['target'], 'aarch64-linux-android'); - expect( - plan['cargo_arguments'], - containsAll(['--release', '--locked', '--lib']), - ); - expect(plan['compatibility'], {'minimum_os_version': 21}); - expect(plan['android_ndk'], androidNdkVersion); - expect(plan['libraries'], { - 'dynamic': 'libmwc_wallet.so', - 'static': 'libmwc_wallet.a', - }); - }, - ); - - test('manifest CLI help works without mandatory build inputs', () async { - final process = await Process.run(Platform.resolvedExecutable, [ - '--packages=${packageRoot.resolve('.dart_tool/package_config.json').toFilePath()}', - packageRoot.resolve('tool/prebuilt_manifest.dart').toFilePath(), - '--help', - ], workingDirectory: packageRoot.toFilePath()); - expect(process.exitCode, 0, reason: process.stderr.toString()); - expect(process.stdout, contains('--artifacts')); - expect(process.stdout, contains('--require-all')); - }); - - test('Linux Rust flags locate the actual static C++ archive', () async { - final env = await buildEnvironment('x86_64-unknown-linux-gnu'); - final searchPath = env['CARGO_ENCODED_RUSTFLAGS']! - .split('\u001f') - .singleWhere((flag) => flag.startsWith('-Lnative=')) - .substring('-Lnative='.length); - final archive = File.fromUri( - Directory(searchPath).uri.resolve('libstdc++.a'), - ); - final header = await archive - .openRead(0, 8) - .expand((bytes) => bytes) - .toList(); - expect(ascii.decode(header), '!\n'); - expect(env['CXXSTDLIB_x86_64_unknown_linux_gnu'], 'static=stdc++'); - }, skip: !Platform.isLinux); - - test('Linux metadata audit compares glibc versions numerically', () { - auditLinuxMetadata( - 'Name: GLIBC_2.9 Name: GLIBC_2.35', - '(NEEDED) Shared library: [libc.so.6]', - '2.35', - ); - expect( - () => auditLinuxMetadata('Name: GLIBC_2.36', '', '2.35'), - throwsA( - isA().having( - (error) => error.message, - 'message', - contains('above 2.35'), - ), - ), - ); - }); - - test('Linux metadata audit rejects a dynamic C++ runtime dependency', () { - expect( - () => auditLinuxMetadata( - 'Name: GLIBC_2.35', - '(NEEDED) Shared library: [libstdc++.so.6]', - '2.35', - ), - throwsA( - isA().having( - (error) => error.message, - 'message', - contains('Unexpected shared dependencies'), - ), - ), - ); - }); -} diff --git a/test/prebuilt_test.dart b/test/prebuilt_test.dart deleted file mode 100644 index dbbdf1a0..00000000 --- a/test/prebuilt_test.dart +++ /dev/null @@ -1,631 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:code_assets/code_assets.dart'; -import 'package:crypto/crypto.dart'; -import 'package:hooks/hooks.dart'; -import 'package:test/test.dart'; - -import '../hook/src/prebuilt.dart'; -import '../hook/src/source_fingerprint.dart'; - -void main() { - late Directory temporary; - - setUp(() async { - temporary = await Directory.systemTemp.createTemp('libmwc-prebuilt-test-'); - }); - - tearDown(() async { - await temporary.delete(recursive: true); - }); - - Future<_LoopbackClient> serve( - Future Function(HttpRequest) respond, - ) async { - final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); - final client = _LoopbackClient(server.port); - addTearDown(() async { - client.close(force: true); - await server.close(force: true); - }); - server.listen(respond); - return client; - } - - BuildInput input({ - Map defines = const {}, - OS os = OS.macOS, - Architecture architecture = Architecture.arm64, - LinkModePreference linkMode = LinkModePreference.dynamic, - IOSSdk sdk = IOSSdk.iPhoneOS, - int minimumVersion = 13, - Uri? packageRoot, - Sanitizer? sanitizer, - }) { - final builder = BuildInputBuilder() - ..setupShared( - packageRoot: packageRoot ?? Directory.current.uri, - packageName: 'flutter_libmwc', - outputDirectoryShared: temporary.uri.resolve('build/native-assets/'), - outputFile: temporary.uri.resolve('build/output.json'), - userDefines: PackageUserDefines( - workspacePubspec: PackageUserDefinesSource( - defines: defines, - basePath: temporary.uri, - ), - ), - ) - ..setupBuildInput(); - builder.config.setupBuild(linkingEnabled: false); - builder.addExtension( - CodeAssetExtension( - targetArchitecture: architecture, - targetOS: os, - linkModePreference: linkMode, - sanitizer: sanitizer, - macOS: os == OS.macOS - ? MacOSCodeConfig(targetVersion: minimumVersion) - : null, - iOS: os == OS.iOS - ? IOSCodeConfig(targetSdk: sdk, targetVersion: minimumVersion) - : null, - android: os == OS.android - ? AndroidCodeConfig(targetNdkApi: minimumVersion) - : null, - ), - ); - return builder.build(); - } - - Map settings([Map changes = const {}]) => { - 'native_build': 'prebuilt', - 'prebuilt_manifest_url': 'https://example.invalid/release/manifest.json', - 'prebuilt_manifest_sha256': 'a' * 64, - ...changes, - }; - - group('consumer settings', () { - test('defaults to source without requiring a manifest', () async { - final buildInput = input(); - expect(PrebuiltOptions.fromInput(buildInput).nativeBuild, 'source'); - expect(await usePrebuilt(buildInput, BuildOutputBuilder()), isFalse); - }); - - test('rejects malformed or unsafe settings', () { - for (final change in >[ - {'native_build': 'prefer'}, - {'native_build': true}, - {'prebuilt_manifest_url': 'http://example.invalid/manifest.json'}, - {'prebuilt_manifest_url': 'https://user:secret@example.invalid/m.json'}, - {'prebuilt_manifest_sha256': 'a' * 63}, - {'prebuilt_manifest_sha256': 'g' * 64}, - ]) { - expect( - () => PrebuiltOptions.fromInput(input(defines: settings(change))), - throwsA(anything), - reason: 'Invalid setting must fail: $change', - ); - } - }); - - test('requires an explicit manifest digest in prebuilt mode', () { - final defines = settings()..remove('prebuilt_manifest_sha256'); - expect( - () => PrebuiltOptions.fromInput(input(defines: defines)), - throwsA(anything), - ); - }); - }); - - Map artifact({ - String target = 'aarch64-apple-darwin', - String linkMode = 'dynamic', - String file = 'mwc_wallet-aarch64-apple-darwin-dynamic.dylib', - int minimumVersion = 11, - }) => { - 'target': target, - 'link_mode': linkMode, - 'file': file, - 'sha256': 'b' * 64, - 'size': 10, - 'minimum_os_version': minimumVersion, - }; - - Map manifest(List> artifacts) => { - 'schema_version': 1, - 'package': 'flutter_libmwc', - 'source_sha256': 'c' * 64, - 'artifacts': artifacts, - }; - - group('artifact compatibility', () { - test('rejects sanitizers and unsupported target combinations', () { - expect( - () => PrebuiltTarget.fromConfig( - input(sanitizer: Sanitizer.asan).config.code, - ), - throwsUnsupportedError, - ); - expect( - () => PrebuiltTarget.fromConfig( - input(os: OS.iOS, architecture: Architecture.x64).config.code, - ), - throwsUnsupportedError, - ); - }); - - test('distinguishes iOS device and simulator on the same architecture', () { - final device = PrebuiltTarget.fromConfig(input(os: OS.iOS).config.code); - final simulator = PrebuiltTarget.fromConfig( - input(os: OS.iOS, sdk: IOSSdk.iPhoneSimulator).config.code, - ); - expect(device.triple, 'aarch64-apple-ios'); - expect(simulator.triple, 'aarch64-apple-ios-sim'); - expect( - () => PrebuiltArtifact.select( - manifest([artifact(target: device.triple)]), - simulator, - sourceSha256: 'c' * 64, - ), - throwsA(anything), - ); - }); - - test('keeps canonical library filenames across Apple targets', () { - final arm = PrebuiltTarget.fromConfig(input().config.code); - final x64 = PrebuiltTarget.fromConfig( - input(architecture: Architecture.x64).config.code, - ); - expect(arm.libraryFileName, 'libmwc_wallet.dylib'); - expect(x64.libraryFileName, arm.libraryFileName); - }); - - test('honors both static and preferred static linking', () { - for (final preference in [ - LinkModePreference.static, - LinkModePreference.preferStatic, - ]) { - final target = PrebuiltTarget.fromConfig( - input(linkMode: preference).config.code, - ); - expect(target.linkMode, 'static'); - expect(target.libraryFileName, 'libmwc_wallet.a'); - expect( - () => PrebuiltArtifact.select( - manifest([artifact()]), - target, - sourceSha256: 'c' * 64, - ), - throwsA(anything), - ); - } - }); - - test('requires matching source and compatible deployment target', () { - final target = PrebuiltTarget.fromConfig(input().config.code); - expect( - PrebuiltArtifact.select( - manifest([artifact()]), - target, - sourceSha256: 'c' * 64, - ).file, - 'mwc_wallet-aarch64-apple-darwin-dynamic.dylib', - ); - expect( - () => PrebuiltArtifact.select( - manifest([artifact()]), - target, - sourceSha256: 'd' * 64, - ), - throwsA(anything), - ); - expect( - () => PrebuiltArtifact.select( - manifest([artifact(minimumVersion: 14)]), - target, - sourceSha256: 'c' * 64, - ), - throwsA(anything), - ); - }); - - test('rejects Android API levels newer than the consumer minimum', () { - final target = PrebuiltTarget.fromConfig( - input(os: OS.android, minimumVersion: 24).config.code, - ); - expect( - () => PrebuiltArtifact.select( - manifest([artifact(target: target.triple, minimumVersion: 35)]), - target, - sourceSha256: 'c' * 64, - ), - throwsUnsupportedError, - ); - }); - - test('requires a declared glibc baseline for Linux artifacts', () { - final target = PrebuiltTarget.fromConfig(input(os: OS.linux).config.code); - final library = artifact(target: target.triple, file: 'mwc_wallet.so'); - expect( - () => PrebuiltArtifact.select( - manifest([library]), - target, - sourceSha256: 'c' * 64, - ), - throwsFormatException, - ); - library['minimum_glibc_version'] = '2.35'; - expect( - PrebuiltArtifact.select( - manifest([library]), - target, - sourceSha256: 'c' * 64, - ).file, - 'mwc_wallet.so', - ); - }); - - test('rejects traversal, missing deployment metadata, and ambiguity', () { - final target = PrebuiltTarget.fromConfig(input().config.code); - for (final artifacts in [ - [artifact(file: '../outside.dylib')], - [artifact(file: 'https://elsewhere.invalid/library.dylib')], - [artifact()..remove('minimum_os_version')], - [artifact(), artifact()], - ]) { - expect( - () => PrebuiltArtifact.select( - manifest(artifacts), - target, - sourceSha256: 'c' * 64, - ), - throwsA(anything), - ); - } - }); - }); - - test( - 'source fingerprints detect edits, additions, renames, and symlinks', - () async { - final root = Directory.fromUri(temporary.uri.resolve('source/')); - final paths = [ - 'rust/Cargo.toml', - 'rust/Cargo.lock', - 'rust/rust-toolchain.toml', - 'rust/src/lib.rs', - 'rust/src/space % λ.rs', - 'lib/mwc.dart', - 'hook/build.dart', - 'tool/build_prebuilt.dart', - 'tool/src/build_support.dart', - ]..sort(); - final expectedFraming = StringBuffer(); - for (final path in paths) { - final file = File.fromUri(root.uri.resolveUri(Uri(path: path))); - await file.parent.create(recursive: true); - final content = 'fixture: $path'; - await file.writeAsString(content); - expectedFraming.write( - '$path\u0000${sha256.convert(utf8.encode(content))}\n', - ); - } - final initial = await sourceFingerprint(root.uri); - expect( - initial.sha256, - sha256.convert(utf8.encode(expectedFraming.toString())).toString(), - reason: 'The manifest protocol hashes decoded relative filenames.', - ); - expect((await sourceFingerprint(root.uri)).sha256, initial.sha256); - expect(initial.dependencies, contains(root.uri.resolve('rust/src/'))); - expect(initial.dependencies, contains(root.uri.resolve('hook/'))); - - final binding = File.fromUri(root.uri.resolve('lib/mwc.dart')); - await binding.writeAsString('changed FFI signature'); - final edited = await sourceFingerprint(root.uri); - expect(edited.sha256, isNot(initial.sha256)); - - final added = File.fromUri(root.uri.resolve('rust/src/added.rs')); - await added.writeAsString('new native module'); - final addedFingerprint = await sourceFingerprint(root.uri); - expect(addedFingerprint.sha256, isNot(edited.sha256)); - await added.rename(root.uri.resolve('rust/src/renamed.rs').toFilePath()); - expect( - (await sourceFingerprint(root.uri)).sha256, - isNot(addedFingerprint.sha256), - ); - - if (!Platform.isWindows) { - await Link.fromUri(root.uri.resolve('rust/src/link.rs')) - .create(binding.path); - await expectLater(sourceFingerprint(root.uri), throwsStateError); - } - }, - ); - - group('verified downloads', () { - test( - 'downloads afresh even when valid or corrupt bytes already exist', - () async { - final bytes = utf8.encode('downloaded binary'); - final digest = sha256.convert(bytes).toString(); - var requests = 0; - String? acceptEncoding; - final client = await serve((request) async { - requests++; - acceptEncoding = request.headers.value( - HttpHeaders.acceptEncodingHeader, - ); - request.response.add(bytes); - await request.response.close(); - }); - final downloader = VerifiedDownloader(temporary, client: client); - final uri = Uri.parse('https://example.invalid/library'); - final destination = File.fromUri(temporary.uri.resolve(digest)); - for (final previous in [bytes, utf8.encode('corrupt'), bytes]) { - await destination.writeAsBytes(previous); - final result = await downloader.get(uri, digest, size: bytes.length); - expect(result.path, destination.path); - expect(await result.readAsBytes(), bytes); - } - expect(requests, 3); - expect(acceptEncoding, 'identity'); - expect(temporary.listSync().map((entry) => entry.path), [ - destination.path, - ]); - }, - ); - - test('rejects an HTTPS redirect that downgrades to HTTP', () async { - var requests = 0; - final client = await serve((request) async { - requests++; - request.response.statusCode = HttpStatus.found; - request.response.headers.set( - HttpHeaders.locationHeader, - 'http://example.invalid/insecure-library', - ); - await request.response.close(); - }); - await expectLater( - VerifiedDownloader( - temporary, - client: client, - ).get(Uri.parse('https://example.invalid/library'), 'a' * 64), - throwsFormatException, - ); - expect(requests, 1); - expect( - File.fromUri(temporary.uri.resolve('a' * 64)).existsSync(), - isFalse, - ); - }); - - test( - 'rejects HTTP failures without publishing their response body', - () async { - final bytes = utf8.encode('not found'); - final digest = sha256.convert(bytes).toString(); - final client = await serve((request) async { - request.response.statusCode = HttpStatus.notFound; - request.response.add(bytes); - await request.response.close(); - }); - await expectLater( - VerifiedDownloader( - temporary, - client: client, - ).get(Uri.parse('https://example.invalid/library'), digest), - throwsA(isA()), - ); - expect( - File.fromUri(temporary.uri.resolve(digest)).existsSync(), - isFalse, - ); - }, - ); - - test('never publishes an unverified network response', () async { - final expected = utf8.encode('expected binary'); - final digest = sha256.convert(expected).toString(); - final client = await serve((request) async { - request.response.write('replaced binary'); - await request.response.close(); - }); - await expectLater( - VerifiedDownloader( - temporary, - client: client, - ).get(Uri.parse('https://example.invalid/library'), digest), - throwsA(anything), - ); - expect(File.fromUri(temporary.uri.resolve(digest)).existsSync(), isFalse); - }); - - test('does not reuse prior bytes after a failed fresh download', () async { - final bytes = utf8.encode('previous verified binary'); - final digest = sha256.convert(bytes).toString(); - final previous = File.fromUri(temporary.uri.resolve(digest)); - await previous.writeAsBytes(bytes); - var requests = 0; - final client = await serve((request) async { - requests++; - request.response.write('replacement with a different digest'); - await request.response.close(); - }); - await expectLater( - VerifiedDownloader( - temporary, - client: client, - ).get(Uri.parse('https://example.invalid/library'), digest), - throwsStateError, - ); - expect(requests, 1); - expect(await previous.readAsBytes(), bytes); - expect(temporary.listSync().map((entry) => entry.path), [previous.path]); - }); - - test( - 'cancels a timed-out download before removing temporary files', - () async { - final digest = 'a' * 64; - var requested = false; - var totalTimeoutScheduled = false; - final client = await serve((request) async { - requested = true; - request.response.bufferOutput = false; - request.response.add([1, 2, 3]); - // Deliberately leave the stream open after sending its first bytes. - // The production timeout must abort this response and close the sink. - unawaited(request.response.done.catchError((_) {})); - }); - await runZoned( - () => expectLater( - VerifiedDownloader( - temporary, - client: client, - ).get(Uri.parse('https://example.invalid/library'), digest), - throwsA(isA()), - ), - zoneSpecification: ZoneSpecification( - createTimer: (self, parent, zone, duration, callback) { - if (duration == const Duration(minutes: 10)) { - totalTimeoutScheduled = true; - duration = const Duration(milliseconds: 100); - } - return parent.createTimer(zone, duration, callback); - }, - ), - ); - expect(requested, isTrue); - expect(totalTimeoutScheduled, isTrue); - expect(client.forceClosed, isTrue); - expect( - File.fromUri(temporary.uri.resolve(digest)).existsSync(), - isFalse, - ); - expect(temporary.listSync().whereType(), isEmpty); - }, - timeout: const Timeout(Duration(seconds: 5)), - ); - - test('rejects oversized responses before committing an artifact', () async { - final bytes = List.filled(128, 42); - final digest = sha256.convert(bytes).toString(); - final client = await serve((request) async { - request.response.add(bytes); - await request.response.close(); - }); - await expectLater( - VerifiedDownloader(temporary, client: client).get( - Uri.parse('https://example.invalid/library'), - digest, - maxBytes: 64, - ), - throwsA(anything), - ); - expect(File.fromUri(temporary.uri.resolve(digest)).existsSync(), isFalse); - }); - }); - - test('resolver downloads into build output and fetches again after build cleanup', () async { - final fingerprint = await sourceFingerprint(Directory.current.uri); - final bytes = utf8.encode( - 'test library bytes; no native loading in this test', - ); - final binaryDigest = sha256.convert(bytes).toString(); - final document = manifest([ - artifact() - ..['sha256'] = binaryDigest - ..['size'] = bytes.length, - ])..['source_sha256'] = fingerprint.sha256; - final manifestBytes = utf8.encode(jsonEncode(document)); - final manifestDigest = sha256.convert(manifestBytes).toString(); - final paths = []; - final client = await serve((request) async { - paths.add(request.uri.path); - switch (request.uri.path) { - case '/release/manifest.json': - request.response.add(manifestBytes); - case '/release/mwc_wallet-aarch64-apple-darwin-dynamic.dylib': - request.response.add(bytes); - default: - request.response.statusCode = HttpStatus.notFound; - } - await request.response.close(); - }); - final buildInput = input( - defines: settings({'prebuilt_manifest_sha256': manifestDigest}), - ); - final outputDirectory = buildInput.outputDirectory; - for (var build = 0; build < 3; build++) { - if (build == 2) { - await Directory.fromUri(temporary.uri.resolve('build/')) - .delete(recursive: true); - expect(temporary.listSync(), isEmpty); - } - final output = BuildOutputBuilder(); - expect(await usePrebuilt(buildInput, output, client: client), isTrue); - final asset = output.build().assets.code.single; - expect(asset.id, 'package:flutter_libmwc/mwc.dart'); - expect(asset.linkMode, isA()); - expect(asset.file, outputDirectory.resolve('libmwc_wallet.dylib')); - expect(await File.fromUri(asset.file!).readAsBytes(), bytes); - expect( - output.build().dependencies, - containsAll(fingerprint.dependencies), - ); - expect( - temporary - .listSync(recursive: true) - .whereType() - .map((file) => file.uri), - [asset.file], - reason: - 'Only the canonical binary should survive in the build directory.', - ); - expect(paths.length, (build + 1) * 2); - } - expect(paths, [ - for (var build = 0; build < 3; build++) ...[ - '/release/manifest.json', - '/release/mwc_wallet-aarch64-apple-darwin-dynamic.dylib', - ], - ]); - }); -} - -/// Keep production HTTPS validation in place while routing test requests to a -/// local HTTP server. No external hosts or certificate exceptions are used. -final class _LoopbackClient implements HttpClient { - final int port; - final HttpClient delegate = HttpClient(); - bool forceClosed = false; - - _LoopbackClient(this.port); - - @override - Future getUrl(Uri url) => delegate.getUrl( - url.replace(scheme: 'http', host: '127.0.0.1', port: port), - ); - - @override - set connectionTimeout(Duration? value) => delegate.connectionTimeout = value; - - @override - set autoUncompress(bool value) => delegate.autoUncompress = value; - - @override - set findProxy(String Function(Uri)? value) => delegate.findProxy = value; - - @override - void close({bool force = false}) { - forceClosed = forceClosed || force; - delegate.close(force: force); - } - - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); -} diff --git a/tool/build_prebuilt.dart b/tool/build_prebuilt.dart deleted file mode 100644 index fa754768..00000000 --- a/tool/build_prebuilt.dart +++ /dev/null @@ -1,369 +0,0 @@ -/// Build one native release target and write its libraries and manifest fragment. -/// -/// Run with `dart --packages=.dart_tool/package_config.json -/// tool/build_prebuilt.dart` after `flutter pub get` to bypass native build hooks. -library; - -import 'dart:io'; - -import 'package:args/args.dart'; - -import 'src/build_support.dart'; - -Future> buildEnvironment( - String target, { - String? ndk, -}) async { - if (!releaseTargets.containsKey(target)) { - throw ArgumentError.value(target, 'target', 'Unsupported release target'); - } - final env = Map.of(Platform.environment) - // Local shell optimization flags must not silently change release ABI. - ..remove('RUSTFLAGS') - ..remove('CARGO_ENCODED_RUSTFLAGS') - ..['CARGO_INCREMENTAL'] = '0' - ..['CARGO_PROFILE_RELEASE_DEBUG'] = 'false'; - final key = target.replaceAll('-', '_'); - final rustflags = []; - - if (target.contains('apple')) { - if (!Platform.isMacOS) { - throw StateError('Apple targets require macOS and Xcode'); - } - env['PATH'] = (env['PATH'] ?? '') - .split(':') - .where((part) => !part.contains('Contents/Developer/')) - .join(':'); - rustflags.addAll([ - '-C', - 'link-arg=-Wl,-install_name,@rpath/libmwc_wallet.dylib', - ]); - if (target.contains('darwin')) { - env['MACOSX_DEPLOYMENT_TARGET'] = '11.0'; - // Match the source hook's LMDB sandbox fix. - env['CFLAGS_$key'] = '-DMDB_USE_POSIX_MUTEX=1 -DMDB_USE_ROBUST=0'; - } else { - env['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'; - final sdk = target == 'aarch64-apple-ios' - ? 'iphoneos' - : 'iphonesimulator'; - env['SDKROOT'] = (await runCommand([ - 'xcrun', - '--sdk', - sdk, - '--show-sdk-path', - ], capture: true))!; - } - } else if (target.contains('android')) { - if (Platform.isWindows) { - throw StateError( - 'Android releases require Linux or macOS (OpenSSL needs Unix Perl)', - ); - } - ndk ??= env['ANDROID_NDK_HOME'] ?? env['ANDROID_NDK_ROOT']; - if (ndk == null || ndk.isEmpty) { - throw StateError('Pass --ndk or set ANDROID_NDK_HOME'); - } - final ndkDirectory = Directory(ndk).absolute; - final properties = await File.fromUri( - ndkDirectory.uri.resolve('source.properties'), - ).readAsString(); - if (!RegExp( - '^Pkg.Revision\\s*=\\s*${RegExp.escape(androidNdkVersion)}\\s*\$', - multiLine: true, - ).hasMatch(properties)) { - throw StateError('Release builds require Android NDK $androidNdkVersion'); - } - final host = Platform.isMacOS ? 'darwin-x86_64' : 'linux-x86_64'; - final toolchain = ndkDirectory.uri.resolve( - 'toolchains/llvm/prebuilt/$host/', - ); - final binary = toolchain.resolve('bin/'); - final clangTarget = target.startsWith('armv7-') - ? 'armv7a-linux-androideabi' - : target; - final ndkTarget = '${clangTarget}21'; - final cc = File.fromUri(binary.resolve('$ndkTarget-clang')); - if (!await cc.exists()) { - throw StateError('Missing NDK compiler: ${cc.path}'); - } - final sysrootTarget = target.startsWith('armv7-') - ? 'arm-linux-androideabi' - : target; - final sysroot = toolchain.resolve('sysroot').toFilePath(); - env.addAll({ - 'CC_$key': cc.path, - 'CXX_$key': binary.resolve('$ndkTarget-clang++').toFilePath(), - 'AR_$key': binary.resolve('llvm-ar').toFilePath(), - 'RANLIB_$key': binary.resolve('llvm-ranlib').toFilePath(), - 'CFLAGS_$key': '--target=$ndkTarget', - 'CXXFLAGS_$key': '--target=$ndkTarget', - 'CXXSTDLIB_$key': 'static:-bundle=c++', - 'CARGO_TARGET_${key.toUpperCase()}_LINKER': cc.path, - 'BINDGEN_EXTRA_CLANG_ARGS_$key': - '--sysroot=$sysroot -I$sysroot/usr/include/$sysrootTarget', - }); - // Required for Android devices using 16 KiB pages, even at API 21. - rustflags.addAll(['-C', 'link-arg=-Wl,-z,max-page-size=16384']); - } else if (target.contains('linux')) { - if (!Platform.isLinux) { - throw StateError('Linux releases require a Linux host'); - } - env['CXXSTDLIB_$key'] = 'static=stdc++'; - // Rust bundles this archive before linking, without GCC's default paths. - final archive = File( - (await runCommand( - ['c++', '-print-file-name=libstdc++.a'], - environment: env, - capture: true, - ))!, - ).absolute; - if (!await archive.exists()) { - throw StateError( - 'Missing libstdc++.a; install the C++ development tools', - ); - } - rustflags.add('-Lnative=${archive.parent.path}'); - } else if (target.contains('windows') && !Platform.isWindows) { - throw StateError('Windows MSVC releases require Windows and Visual Studio'); - } - - // Preserve library paths containing spaces as a single compiler argument. - env['CARGO_ENCODED_RUSTFLAGS'] = rustflags.join('\u001f'); - return env; -} - -/// Reject a host ABI or shared dependency newer than the advertised floor. -Future auditLinuxLibrary(File library, String minimum) async { - final symbols = (await runCommand([ - 'readelf', - '--version-info', - library.path, - ], capture: true))!; - final dependencies = (await runCommand([ - 'readelf', - '--dynamic', - library.path, - ], capture: true))!; - auditLinuxMetadata(symbols, dependencies, minimum, name: library.path); -} - -void auditLinuxMetadata( - String symbols, - String dependencies, - String minimum, { - String name = 'Native library', -}) { - final floor = minimum.split('.').map(int.parse).toList(); - for (final match in RegExp(r'GLIBC_(\d+)\.(\d+)').allMatches(symbols)) { - final major = int.parse(match[1]!); - final minor = int.parse(match[2]!); - if (major > floor[0] || (major == floor[0] && minor > floor[1])) { - throw StateError('$name requires glibc $major.$minor, above $minimum'); - } - } - const allowed = { - 'libc.so.6', - 'libm.so.6', - 'libdl.so.2', - 'libpthread.so.0', - 'librt.so.1', - 'libgcc_s.so.1', - 'ld-linux-x86-64.so.2', - }; - final unexpected = - RegExp(r'\(NEEDED\).*\[([^\]]+)\]') - .allMatches(dependencies) - .map((match) => match[1]!) - .toSet() - .difference(allowed) - .toList() - ..sort(); - if (unexpected.isNotEmpty) { - throw StateError('Unexpected shared dependencies: $unexpected'); - } -} - -Future buildTarget( - String target, - Directory output, - Directory targetDirectory, { - String? ndk, - Uri? root, -}) async { - root ??= packageRoot; - output = output.absolute; - targetDirectory = targetDirectory.absolute; - final names = libraryNames(target); - final fingerprint = await sourceSha256(root); - final channel = await toolchainChannel(root); - final env = await buildEnvironment(target, ndk: ndk); - await runCommand([ - 'rustup', - 'toolchain', - 'install', - channel, - '--profile', - 'minimal', - '--no-self-update', - ], workingDirectory: root); - await runCommand([ - 'rustup', - 'target', - 'add', - '--toolchain', - channel, - target, - ], workingDirectory: root); - await runCommand( - [ - 'rustup', - 'run', - channel, - 'cargo', - 'build', - '--release', - '--locked', - '--lib', - '--manifest-path', - root.resolve('rust/Cargo.toml').toFilePath(), - '--target', - target, - '--target-dir', - targetDirectory.path, - ], - workingDirectory: root, - environment: env, - ); - if (await sourceSha256(root) != fingerprint) { - throw StateError( - 'Native sources changed during the build; refusing to package', - ); - } - final release = targetDirectory.uri.resolve('$target/release/'); - final minimumGlibc = releaseTargets[target]!['minimum_glibc_version']; - if (minimumGlibc is String) { - await auditLinuxLibrary( - File.fromUri(release.resolve(names['dynamic']!)), - minimumGlibc, - ); - } - final entries = <({String mode, File source, File destination})>[]; - final fragmentFile = File.fromUri(output.uri.resolve('$target.json')); - for (final entry in names.entries) { - final source = File.fromUri(release.resolve(entry.value)); - if (await FileSystemEntity.type(source.path, followLinks: false) != - FileSystemEntityType.file) { - throw StateError('Missing or nonregular built library: ${source.path}'); - } - final extension = entry.value.substring(entry.value.lastIndexOf('.')); - final destination = File.fromUri( - output.uri.resolve('mwc_wallet-$target-${entry.key}$extension'), - ); - entries.add((mode: entry.key, source: source, destination: destination)); - } - for (final file in [ - fragmentFile, - ...entries.map((entry) => entry.destination), - ]) { - if (await FileSystemEntity.type(file.path, followLinks: false) != - FileSystemEntityType.notFound) { - throw StateError('Refusing to overwrite an artifact: ${file.path}'); - } - } - await output.create(recursive: true); - final artifacts = >[]; - for (final entry in entries) { - final destination = await entry.source.copy(entry.destination.path); - artifacts.add({ - 'target': target, - 'link_mode': entry.mode, - 'file': destination.uri.pathSegments.last, - 'sha256': await sha256File(destination), - 'size': await destination.length(), - ...releaseTargets[target]!, - }); - } - await fragmentFile.writeAsString( - '${indentedJson.convert({ - 'schema_version': 1, - 'package': 'flutter_libmwc', - 'source_sha256': fingerprint, - 'artifacts': artifacts, - 'build': {'rust_version': channel, 'target': target}, - })}\n', - ); - stdout.writeln('Packaged $target; source_sha256=$fingerprint'); -} - -Future main(List arguments) async { - final parser = ArgParser() - ..addOption('target', allowed: releaseTargets.keys.toList()) - ..addOption( - 'output', - defaultsTo: packageRoot.resolve('build/prebuilts').toFilePath(), - ) - ..addOption( - 'target-dir', - defaultsTo: packageRoot.resolve('build/prebuilt-target').toFilePath(), - ) - ..addOption('ndk') - ..addFlag( - 'source-hash', - negatable: false, - help: 'Print the native source fingerprint without building.', - ) - ..addFlag( - 'dry-run', - negatable: false, - help: 'Print the release plan without installing or compiling anything.', - ) - ..addFlag('help', abbr: 'h', negatable: false); - try { - final args = parser.parse(arguments); - if (args.flag('help')) { - stdout.writeln('Build one native release target.\n${parser.usage}'); - return; - } - if (args.rest.isNotEmpty) - throw FormatException('Unexpected arguments: ${args.rest.join(' ')}'); - final target = args.option('target'); - if (args.flag('source-hash')) { - stdout.writeln(await sourceSha256()); - } else if (args.flag('dry-run') && target != null) { - stdout.writeln( - indentedJson.convert({ - 'source_sha256': await sourceSha256(), - 'rust_version': await toolchainChannel(), - 'target': target, - 'cargo_arguments': [ - 'build', - '--release', - '--locked', - '--lib', - '--target', - target, - ], - 'libraries': libraryNames(target), - 'compatibility': releaseTargets[target], - 'android_ndk': target.contains('android') ? androidNdkVersion : null, - }), - ); - } else if (target != null) { - await buildTarget( - target, - Directory(args.option('output')!), - Directory(args.option('target-dir')!), - ndk: args.option('ndk'), - ); - } else { - throw const FormatException('--target or --source-hash is required'); - } - } on FormatException catch (error) { - stderr.writeln('${error.message}\n${parser.usage}'); - exitCode = 64; - } catch (error) { - stderr.writeln('Native release build failed: $error'); - exitCode = 1; - } -} diff --git a/tool/prebuilt_manifest.dart b/tool/prebuilt_manifest.dart deleted file mode 100644 index be7e6806..00000000 --- a/tool/prebuilt_manifest.dart +++ /dev/null @@ -1,191 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:args/args.dart'; - -import 'src/build_support.dart'; - -typedef ManifestResult = ({Map manifest, String sha256}); -Map _object(Object? value, String description) { - if (value is! Map) { - throw FormatException('Expected an object: $description'); - } - return Map.from(value); -} - -bool _matches(String pattern, String value) => - RegExp(pattern).firstMatch(value)?.group(0) == value; - -/// Verify all producer fragments before writing any release files. -Future assemblePrebuilts( - Directory artifactsDirectory, - Directory outputDirectory, { - Uri? root, - bool requireAll = false, -}) async { - if (await outputDirectory.exists() && !await outputDirectory.list().isEmpty) { - throw StateError('Release output must be empty; refusing to mix releases'); - } - final expectedSource = await sourceSha256(root); - final fragments = []; - await for (final entity in artifactsDirectory.list( - recursive: true, - followLinks: false, - )) { - if (entity is File && entity.path.endsWith('.json')) fragments.add(entity); - } - fragments.sort((first, second) => first.path.compareTo(second.path)); - if (fragments.isEmpty) { - throw const FormatException('No build fragments found'); - } - final artifacts = <({Map entry, File source})>[]; - final seenPairs = <(String, String)>{}; - final seenNames = {}; - for (final path in fragments) { - final fragment = _object(jsonDecode(await path.readAsString()), path.path); - if (fragment['schema_version'] != 1 || - fragment['package'] != 'flutter_libmwc') { - throw FormatException('Unsupported build fragment: ${path.path}'); - } - if (fragment['source_sha256'] != expectedSource) { - throw FormatException('Source fingerprint mismatch: ${path.path}'); - } - final entries = fragment['artifacts']; - if (entries is! List || entries.isEmpty) { - throw FormatException('Missing artifact entries: ${path.path}'); - } - for (final value in entries) { - final entry = _object(value, '${path.path} artifact'); - final target = entry['target']; - final mode = entry['link_mode']; - final name = entry['file']; - if (target is! String || - !releaseTargets.containsKey(target) || - mode is! String || - !['static', 'dynamic'].contains(mode)) { - throw FormatException('Unknown target or link mode in ${path.path}'); - } - if (name is! String || - !_matches( - r'[A-Za-z0-9_-][A-Za-z0-9_.-]*\.(so|dylib|dll|a|lib)', - name, - )) { - throw FormatException('Unsafe artifact filename in ${path.path}'); - } - if (seenPairs.contains((target, mode)) || seenNames.contains(name)) { - throw FormatException('Duplicate artifact: $target/$mode/$name'); - } - for (final compatibility in releaseTargets[target]!.entries) { - if (entry[compatibility.key] != compatibility.value) { - throw FormatException('Incorrect ${compatibility.key} for $target'); - } - } - final source = File.fromUri(path.parent.uri.resolve(name)); - if (await FileSystemEntity.type(source.path, followLinks: false) != - FileSystemEntityType.file) { - throw FormatException('Missing or nonregular artifact: ${source.path}'); - } - final size = entry['size']; - if (size is! int || - size <= 0 || - size > 2 * 1024 * 1024 * 1024 || - await source.length() != size) { - throw FormatException('Artifact size mismatch: ${source.path}'); - } - if (await sha256File(source) != entry['sha256']) { - throw FormatException('Artifact checksum mismatch: ${source.path}'); - } - seenPairs.add((target, mode)); - seenNames.add(name); - artifacts.add((entry: entry, source: source)); - } - } - final expectedPairs = { - for (final target in releaseTargets.keys) - for (final mode in ['static', 'dynamic']) (target, mode), - }; - if (requireAll && !seenPairs.containsAll(expectedPairs)) { - final missing = - expectedPairs.difference(seenPairs).map((p) => '$p').toList()..sort(); - throw FormatException('Missing release artifacts: $missing'); - } - artifacts.sort((first, second) { - final target = (first.entry['target'] as String).compareTo( - second.entry['target'] as String, - ); - return target != 0 - ? target - : (first.entry['link_mode'] as String).compareTo( - second.entry['link_mode'] as String, - ); - }); - final manifest = { - 'schema_version': 1, - 'package': 'flutter_libmwc', - 'source_sha256': expectedSource, - 'artifacts': artifacts.map((artifact) => artifact.entry).toList(), - }; - await outputDirectory.create(recursive: true); - for (final artifact in artifacts) { - final destination = File.fromUri( - outputDirectory.uri.resolve(artifact.entry['file'] as String), - ); - await artifact.source.copy(destination.path); - } - final manifestFile = File.fromUri( - outputDirectory.uri.resolve('manifest.json'), - ); - await manifestFile.writeAsString('${indentedJson.convert(manifest)}\n'); - final pin = await sha256File(manifestFile); - await File.fromUri(outputDirectory.uri.resolve('manifest.sha256')) - .writeAsString('$pin manifest.json\n'); - stdout.writeln('Manifest SHA-256: $pin'); - return (manifest: manifest, sha256: pin); -} - -Future main(List arguments) async { - final parser = ArgParser() - ..addOption('artifacts') - ..addOption( - 'output', - defaultsTo: packageRoot.resolve('build/native-release/').toFilePath(), - ) - ..addFlag('require-all', negatable: false) - ..addFlag('help', abbr: 'h', negatable: false); - try { - final args = parser.parse(arguments); - if (args.flag('help')) { - stdout.writeln( - 'Verify native artifacts and assemble a release.\n${parser.usage}', - ); - return; - } - if (args.rest.isNotEmpty) - throw const FormatException('Unexpected arguments'); - final artifactsPath = args.option('artifacts'); - if (artifactsPath == null) { - throw const FormatException('--artifacts is required'); - } - final output = Directory(args['output'] as String).absolute; - final result = await assemblePrebuilts( - Directory(artifactsPath).absolute, - output, - requireAll: args['require-all'] as bool, - ); - final summaryPath = Platform.environment['GITHUB_STEP_SUMMARY']; - if (summaryPath != null) { - await File(summaryPath).writeAsString( - 'Manifest SHA-256: `${result.sha256}`\n\n' - 'Native source SHA-256: `${result.manifest['source_sha256']}`\n', - mode: FileMode.append, - ); - } - } on FormatException catch (error) { - stderr.writeln(error.message); - stderr.writeln(parser.usage); - exitCode = 1; - } on Object catch (error) { - stderr.writeln(error); - exitCode = 1; - } -} diff --git a/tool/smoke_prebuilt.dart b/tool/smoke_prebuilt.dart deleted file mode 100644 index 7d932eee..00000000 --- a/tool/smoke_prebuilt.dart +++ /dev/null @@ -1,78 +0,0 @@ -/// Load a desktop release library and check its C ABI without creating a wallet. -library; - -import 'dart:ffi'; -import 'dart:io'; - -import 'package:args/args.dart'; -import 'package:ffi/ffi.dart'; - -void smoke(File file) { - final library = DynamicLibrary.open(file.absolute.path); - try { - final mnemonic = library - .lookupFunction Function(), Pointer Function()>( - 'mwc_get_mnemonic', - )(); - if (mnemonic == nullptr || - mnemonic.toDartString().trim().split(RegExp(r'\s+')).length != 24) { - throw StateError('Mnemonic ABI smoke test failed'); - } - final openWallet = library - .lookupFunction< - Pointer Function(Pointer, Pointer), - Pointer Function(Pointer, Pointer) - >('mwc_rust_open_wallet'); - final freeString = library - .lookupFunction< - Void Function(Pointer), - void Function(Pointer) - >('mwc_string_free'); - final config = '{}'.toNativeUtf8(); - final password = 'unused'.toNativeUtf8(); - try { - for (var index = 0; index < 100; index++) { - final pointer = openWallet(config, password); - if (pointer == nullptr) { - throw StateError('Open-wallet ABI returned null'); - } - try { - if (!pointer.toDartString().contains('Unable to get wallet config')) { - throw StateError('Open-wallet error ABI smoke test failed'); - } - } finally { - freeString(pointer); - } - } - } finally { - calloc.free(config); - calloc.free(password); - } - stdout.writeln( - 'Native library loaded; mnemonic and error/free ABI checks passed', - ); - } finally { - library.close(); - } -} - -void main(List arguments) { - final parser = ArgParser()..addFlag('help', abbr: 'h', negatable: false); - try { - final args = parser.parse(arguments); - if (args.flag('help')) { - stdout.writeln('Usage: smoke_prebuilt.dart \n${parser.usage}'); - return; - } - if (args.rest.length != 1) { - throw const FormatException('Supply exactly one native library path'); - } - smoke(File(args.rest.single)); - } on FormatException catch (error) { - stderr.writeln('${error.message}\nUsage: smoke_prebuilt.dart '); - exitCode = 64; - } catch (error) { - stderr.writeln('Native smoke test failed: $error'); - exitCode = 1; - } -} diff --git a/tool/src/build_support.dart b/tool/src/build_support.dart deleted file mode 100644 index 27184558..00000000 --- a/tool/src/build_support.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:crypto/crypto.dart' as crypto; - -import '../../hook/src/source_fingerprint.dart'; - -const releaseTargets = >{ - 'x86_64-unknown-linux-gnu': {'minimum_glibc_version': '2.35'}, - 'x86_64-pc-windows-msvc': {}, - 'x86_64-apple-darwin': {'minimum_os_version': 11}, - 'aarch64-apple-darwin': {'minimum_os_version': 11}, - 'armv7-linux-androideabi': {'minimum_os_version': 21}, - 'aarch64-linux-android': {'minimum_os_version': 21}, - 'x86_64-linux-android': {'minimum_os_version': 21}, - 'aarch64-apple-ios': {'minimum_os_version': 13}, - 'aarch64-apple-ios-sim': {'minimum_os_version': 13}, - 'x86_64-apple-ios': {'minimum_os_version': 13}, -}; - -const androidNdkVersion = '28.2.13676358'; -const indentedJson = JsonEncoder.withIndent(' '); - -/// Run maintainer tools from the repository root. -final Uri packageRoot = Directory.current.uri; - -Future sha256File(File file) async => - (await crypto.sha256.bind(file.openRead()).first).toString(); - -/// Producer and consumer use the same source fingerprint implementation. -Future sourceSha256([Uri? root]) async => - (await sourceFingerprint(root ?? packageRoot)).sha256; - -Future toolchainChannel([Uri? root]) async { - final source = await File.fromUri( - (root ?? packageRoot).resolve('rust/rust-toolchain.toml'), - ).readAsString(); - final match = RegExp( - r'^channel\s*=\s*"(\d+\.\d+\.\d+)"\s*$', - multiLine: true, - ).firstMatch(source); - if (match == null) { - throw StateError('rust/rust-toolchain.toml must pin an exact Rust version'); - } - return match[1]!; -} - -Map libraryNames(String target) { - if (!releaseTargets.containsKey(target)) { - throw ArgumentError.value(target, 'target', 'Unsupported release target'); - } - if (target.contains('windows')) { - return {'dynamic': 'mwc_wallet.dll', 'static': 'mwc_wallet.lib'}; - } - return { - 'dynamic': 'libmwc_wallet${target.contains('apple') ? '.dylib' : '.so'}', - 'static': 'libmwc_wallet.a', - }; -} - -/// Run argument vectors without involving a shell. When an explicit environment -/// is supplied, omitted variables stay removed from the child process. -Future runCommand( - List command, { - Uri? workingDirectory, - Map? environment, - bool capture = false, -}) async { - stdout.writeln('+ ${command.join(' ')}'); - final process = await Process.start( - command.first, - command.skip(1).toList(), - workingDirectory: (workingDirectory ?? packageRoot).toFilePath(), - environment: environment, - includeParentEnvironment: environment == null, - ); - final (status, output, _, _) = await ( - process.exitCode, - capture - ? process.stdout.transform(systemEncoding.decoder).join() - : stdout.addStream(process.stdout).then((_) => ''), - stderr.addStream(process.stderr), - process.stdin.close(), - ).wait; - if (status != 0) { - throw ProcessException( - command.first, - command.skip(1).toList(), - 'Command failed', - status, - ); - } - return capture ? output.trim() : null; -} diff --git a/tool/test_prebuilt.dart b/tool/test_prebuilt.dart deleted file mode 100644 index de046ba3..00000000 --- a/tool/test_prebuilt.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'dart:io'; -import 'dart:isolate'; - -/// Run with `dart --packages=.dart_tool/package_config.json -/// tool/test_prebuilt.dart` after `flutter pub get`. Invoking the VM directly -/// keeps resolver and release-tool tests independent of the native Cargo hook. -Future main(List args) async { - final root = Platform.script.resolve('../'); - final packageConfig = File.fromUri( - root.resolve('.dart_tool/package_config.json'), - ); - if (!await packageConfig.exists()) { - stderr.writeln('Run flutter pub get before testing the prebuilt resolver.'); - exitCode = 1; - return; - } - final testLibrary = await Isolate.resolvePackageUri( - Uri.parse('package:test/test.dart'), - ); - if (testLibrary == null) { - stderr.writeln('Cannot resolve package:test. Run flutter pub get first.'); - exitCode = 1; - return; - } - final runner = File.fromUri(testLibrary.resolve('../bin/test.dart')); - final process = await Process.start( - Platform.resolvedExecutable, - [ - '--packages=${packageConfig.path}', - runner.path, - if (args.isEmpty) ...[ - 'test/prebuilt_test.dart', - 'test/prebuilt_producer_test.dart', - ] else - ...args, - ], - workingDirectory: root.toFilePath(), - mode: ProcessStartMode.inheritStdio, - ); - exitCode = await process.exitCode; -} diff --git a/tool/update_lock.dart b/tool/update_lock.dart new file mode 100644 index 00000000..73089196 --- /dev/null +++ b/tool/update_lock.dart @@ -0,0 +1,4 @@ +import 'dart:io'; +import 'package:dnix/tools.dart'; + +void main(List args) async => exit(await updateLock(args)); diff --git a/tool/verify_release.dart b/tool/verify_release.dart new file mode 100644 index 00000000..0dd9d24c --- /dev/null +++ b/tool/verify_release.dart @@ -0,0 +1,4 @@ +import 'dart:io'; +import 'package:dnix/tools.dart'; + +void main(List args) async => exit(await verifyRelease(args)); From c246081bdd313895ada6cf5b6823a3d9ccc8729c Mon Sep 17 00:00:00 2001 From: slopbot9k Date: Wed, 21 Oct 2026 23:55:31 -0600 Subject: [PATCH 2/9] Verify every push on four hosts and publish native releases from the lock dnix's reusable workflow builds the library on Linux x86_64, Linux aarch64, macOS with Xcode 27.0, and Linux inside WSL2 on Windows, rebuilds each sandboxed target, compares every host with every other and with the committed lock, and on main publishes the release the lock names if it does not exist yet. Runs on main are never cancelled, since one may be publishing. The test workflow runs the Flutter tests on every OS, against the prebuilt library once its release exists and from source before that. The tag-driven prebuilt workflow and the prebuilt tooling checks went with the pipeline they served. --- .github/workflows/native-prebuilts.yml | 197 ------------------------- .github/workflows/native.yml | 25 ++++ .github/workflows/prebuilt-checks.yml | 31 ---- .github/workflows/tests.yml | 60 ++++++++ 4 files changed, 85 insertions(+), 228 deletions(-) delete mode 100644 .github/workflows/native-prebuilts.yml create mode 100644 .github/workflows/native.yml delete mode 100644 .github/workflows/prebuilt-checks.yml create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/native-prebuilts.yml b/.github/workflows/native-prebuilts.yml deleted file mode 100644 index a8897559..00000000 --- a/.github/workflows/native-prebuilts.yml +++ /dev/null @@ -1,197 +0,0 @@ -name: Build native prebuilts - -on: - push: - tags: ['native-*'] - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: native-prebuilts-${{ github.ref }} - cancel-in-progress: false - -jobs: - build: - if: startsWith(github.ref, 'refs/tags/native-') && github.event.deleted != true - name: ${{ matrix.target }} - runs-on: ${{ matrix.runner }} - timeout-minutes: 120 - strategy: - fail-fast: false - matrix: - include: - - target: x86_64-unknown-linux-gnu - runner: ubuntu-22.04 - smoke_library: libmwc_wallet.so - - target: x86_64-pc-windows-msvc - runner: windows-2022 - smoke_library: mwc_wallet.dll - - target: x86_64-apple-darwin - runner: macos-15-intel - smoke_library: libmwc_wallet.dylib - - target: aarch64-apple-darwin - runner: macos-15 - smoke_library: libmwc_wallet.dylib - - target: armv7-linux-androideabi - runner: ubuntu-22.04 - - target: aarch64-linux-android - runner: ubuntu-22.04 - - target: x86_64-linux-android - runner: ubuntu-22.04 - - target: aarch64-apple-ios - runner: macos-15 - - target: aarch64-apple-ios-sim - runner: macos-15 - - target: x86_64-apple-ios - runner: macos-15-intel - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - with: - persist-credentials: false - - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2 - with: - channel: stable - flutter-version: '3.47.2' - cache: true - - run: flutter pub get - - name: Use short native build and Cargo paths - shell: bash - run: | - echo "CARGO_HOME=$RUNNER_TEMP/mwc-cargo" >> "$GITHUB_ENV" - echo "NATIVE_TARGET_DIR=$RUNNER_TEMP/mwc-target" >> "$GITHUB_ENV" - - name: Cache Cargo downloads - uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 - with: - path: | - ${{ env.CARGO_HOME }}/registry - ${{ env.CARGO_HOME }}/git - key: cargo-downloads-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('rust/Cargo.lock', 'rust/rust-toolchain.toml') }} - - name: Install Linux build dependencies - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y build-essential clang libclang-dev cmake pkg-config nasm perl protobuf-compiler - - name: Install Apple build dependencies - if: runner.os == 'macOS' - shell: bash - run: | - brew install protobuf cmake pkgconf nasm - echo "LIBCLANG_PATH=$(dirname "$(xcrun --find clang)")/../lib" >> "$GITHUB_ENV" - - name: Install Windows build dependencies - if: runner.os == 'Windows' - shell: pwsh - run: | - git config --global core.longpaths true - choco install protoc nasm --yes --no-progress - "LIBCLANG_PATH=C:\Program Files\LLVM\bin" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - - name: Install pinned Android NDK - if: contains(matrix.target, 'android') - shell: bash - run: | - "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" 'ndk;28.2.13676358' - echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/28.2.13676358" >> "$GITHUB_ENV" - - name: Build and package both link modes - env: - NATIVE_TARGET: ${{ matrix.target }} - shell: bash - run: dart --packages=.dart_tool/package_config.json tool/build_prebuilt.dart --target "$NATIVE_TARGET" --target-dir "$NATIVE_TARGET_DIR" --output build/prebuilts - - name: Smoke test the desktop C ABI - if: matrix.smoke_library != '' - env: - NATIVE_TARGET: ${{ matrix.target }} - SMOKE_LIBRARY: ${{ matrix.smoke_library }} - shell: bash - run: dart --packages=.dart_tool/package_config.json tool/smoke_prebuilt.dart "$NATIVE_TARGET_DIR/$NATIVE_TARGET/release/$SMOKE_LIBRARY" - - name: Upload verified target artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: prebuilt-${{ matrix.target }} - path: build/prebuilts/* - if-no-files-found: error - retention-days: 14 - compression-level: 1 - - release: - name: Publish verified prebuilts - needs: build - runs-on: ubuntu-22.04 - timeout-minutes: 30 - permissions: - contents: write - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - with: - persist-credentials: false - - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2 - with: - channel: stable - flutter-version: '3.47.2' - cache: true - - run: flutter pub get - - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 - with: - pattern: prebuilt-* - path: build/downloaded - - name: Verify artifacts and assemble the manifest - run: dart --packages=.dart_tool/package_config.json tool/prebuilt_manifest.dart --artifacts build/downloaded --require-all - - name: Keep the complete release as a workflow artifact - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: native-release - path: build/native-release/* - if-no-files-found: error - retention-days: 14 - compression-level: 1 - - name: Publish release - env: - GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ github.ref_name }} - shell: bash - run: | - [[ "$RELEASE_TAG" =~ ^native-[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || { echo 'Use a new native-* tag'; exit 1; } - source_commit=$(git rev-parse HEAD) - git fetch --depth=1 --no-tags origin "refs/tags/$RELEASE_TAG" - [[ "$(git rev-parse 'FETCH_HEAD^{commit}')" == "$source_commit" ]] || { echo 'Tag moved since this build started'; exit 1; } - read -r manifest_pin _ < build/native-release/manifest.sha256 - cat > "$RUNNER_TEMP/release-notes.md" < [!CAUTION] - > These binaries are provided AS IS, without warranty. Automated checks do not guarantee safety or correctness. Bugs may cause loss of funds or data. - - Built from source commit $source_commit. Use the matching package revision. - - Add to your root pubspec: - - hooks: - user_defines: - flutter_libmwc: - native_build: prebuilt - prebuilt_manifest_url: "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/download/$RELEASE_TAG/manifest.json" - prebuilt_manifest_sha256: "$manifest_pin" - - Use the same root pubspec settings and normal Flutter commands in CI. - Each hook run downloads fresh, verified files. Flutter can reuse its - output for incremental builds; flutter clean removes it, so the next - build downloads again. In pub workspaces, clean from the workspace root. - Omit these settings or use native_build: source to build from source. - EOF - - if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then - is_draft="$(gh release view "$RELEASE_TAG" --json isDraft --jq '.isDraft')" - - if [[ "$is_draft" == "true" ]]; then - echo "Draft release exists for $RELEASE_TAG; deleting stale draft and recreating it." - gh release delete "$RELEASE_TAG" --yes - else - echo "Published release already exists for $RELEASE_TAG; refusing to overwrite." - exit 1 - fi - fi - - # only create if the release does not already exist - gh release create "$RELEASE_TAG" build/native-release/* \ - --verify-tag --latest=false --title "$RELEASE_TAG" \ - --notes-file "$RUNNER_TEMP/release-notes.md" diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml new file mode 100644 index 00000000..f6dd84fa --- /dev/null +++ b/.github/workflows/native.yml @@ -0,0 +1,25 @@ +# Builds the Rust library on every reference host, fails if any host or the +# committed dnix.lock disagrees, and publishes the release the lock names +# when it reaches the default branch. +name: Native library + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: write + +# A newer push supersedes runs on a branch, never on main: a run there may +# be publishing. +concurrency: + group: native-${{ github.ref }} + cancel-in-progress: ${{ github.ref != format('refs/heads/{0}', github.event.repository.default_branch) }} + +jobs: + native: + # The same dnix commit as pubspec.yaml and flake.lock. + uses: cypherstack/dnix/.github/workflows/native.yml@448c5da6e61b3f0e62b4eae6d08b49158a9e45b2 + with: + macos-runner: xcode-27 diff --git a/.github/workflows/prebuilt-checks.yml b/.github/workflows/prebuilt-checks.yml deleted file mode 100644 index 7a2370f4..00000000 --- a/.github/workflows/prebuilt-checks.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Prebuilt integrity checks - -on: - pull_request: - push: - branches: [main, master] - workflow_dispatch: - -permissions: - contents: read - -jobs: - checks: - runs-on: ubuntu-22.04 - timeout-minutes: 20 - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - with: - persist-credentials: false - - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2 - with: - channel: stable - flutter-version: '3.47.2' - cache: true - - run: flutter pub get - - name: Analyze the hook and release tooling - run: dart analyze hook tool test/prebuilt_test.dart test/prebuilt_producer_test.dart - - name: Test release tooling and the resolver without compiling Rust - run: dart --packages=.dart_tool/package_config.json tool/test_prebuilt.dart - - name: Check native build planning - run: dart --packages=.dart_tool/package_config.json tool/build_prebuilt.dart --target x86_64-unknown-linux-gnu --dry-run diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..13310e29 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,60 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + flutter: + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, ubuntu-24.04-arm, xcode-27, windows-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 90 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: subosito/flutter-action@v2 + with: + flutter-version: 3.47.2 + channel: stable + cache: true + # The hook builds through the Nix flake; before the first native + # release that is the only path, and it is unavailable on Windows. + - uses: DeterminateSystems/nix-installer-action@v23 + if: runner.os != 'Windows' + - run: flutter pub get + - run: dart analyze hook tool test + # Prebuilt libraries exist once the lock's release is published from the + # default branch of the repository dnix.json names; until then only + # hosts with Nix can build them. + - name: Check whether the lock's release is published + id: release + shell: bash + run: | + repository=$(jq -r .repository dnix.json | sed 's/\.git$//; s#/$##') + tag="native-$(jq -r .release dnix.lock)" + echo "published=$([ "$(curl -s -o /dev/null -w '%{http_code}' "$repository/releases/tag/$tag")" = 200 ] && echo true || echo false)" >> "$GITHUB_OUTPUT" + - name: Choose the build mode + shell: bash + run: echo '${{ steps.release.outputs.published == 'true' && 'prebuilt' || 'source' }}' > .dnix-build + - name: Run flutter test + if: runner.os != 'Windows' || steps.release.outputs.published == 'true' + shell: bash + run: | + if ! flutter test 2>&1 | tee test.log; then + echo "::error::$(tail -c 1500 test.log | tr '\n' ' ')" + exit 1 + fi + From 53364f433745a242f566837d1073f0f5692194f3 Mon Sep 17 00:00:00 2001 From: slopbot9k Date: Wed, 21 Oct 2026 23:55:31 -0600 Subject: [PATCH 3/9] Describe the native library flow How the hook obtains the library and the three modes, how a change to the Rust code becomes a lock entry and a release, and how anyone rebuilds a platform and compares. --- README.md | 103 ++++++++++++++++++++++++------------------------------ 1 file changed, 45 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index f694da27..852f3c5e 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,68 @@ # `flutter_libmwc` MWC wallet bindings for Dart 3.13+ and Flutter 3.47+. APIs are in `lib/lib.dart` -and `lib/mwc.dart`; Flutter bundles the native library automatically. +and `lib/mwc.dart`; Flutter bundles the native library automatically. Nothing +beyond Flutter is needed to use the package: the build hook downloads the +Rust library built for this version and checks its hash. -## Use prebuilts +## Native library -Copy the URL and SHA-256 from a published native release into your app's root -`pubspec.yaml` (workspace root for pub workspaces): +The hook obtains the Rust library in one of three modes, chosen with the +`build` user define in the consuming app's root `pubspec.yaml` (the workspace +root for pub workspaces), or for this checkout with a `.dnix-build` file +holding the mode: ```yaml hooks: user_defines: flutter_libmwc: - native_build: prebuilt - prebuilt_manifest_url: https://github.com/cypherstack/flutter_libmwc/releases/download/native-YOUR-RELEASE/manifest.json - prebuilt_manifest_sha256: "REPLACE_WITH_RELEASE_MANIFEST_SHA256" + build: prebuilt # the default; or source, or dev ``` -Use the package revision named in that release. The hook checks source identity, -platform compatibility, and checksums. Missing or invalid prebuilts stop the build. -Downloads require public HTTPS; Flutter's normal app build tools are still needed. +- `prebuilt` downloads the library of the release `dnix.lock` names and + verifies it against the hash recorded there. A mismatch is an error, never + a fallback. Downloads are cached by hash and need public HTTPS. +- `source` builds the library with Nix, the same build CI uses, and verifies + it against the same hash. Needs Nix on Linux or an Apple Silicon Mac; the + Apple targets need Xcode 27.0. OpenSSL is compiled from source inside that + build. +- `dev` builds with Nix but skips the Xcode pin and the hash check, for + working on the Rust code. The hook notices a `.dnix-build` file from its + next run on, so touch `dnix.json` after creating it. -Each hook run downloads and verifies fresh files into Flutter's `.dart_tool` -output. Flutter can reuse that output for incremental builds. `flutter clean` -removes it, so the next build downloads again. In pub workspaces, run clean -from the workspace root. - -Use the same root pubspec settings and normal Flutter commands in CI, such as -`flutter pub get` and `flutter test`. - -## Build from source - -Omit the prebuilt settings or set `native_build: source`. Install Rust 1.99.1 via -[rustup](https://rustup.rs/), a C/C++ compiler, CMake, libclang, pkg-config, Perl, -and `protoc`. Use Xcode for Apple, Android SDK/NDK r27+ for Android, and Visual -Studio's C++ workload for Windows. Run Flutter normally; the hook compiles Rust -with `--locked`. - -## Build releases - -Push a new `native-*` tag to build all targets and automatically publish a release -after the checks pass. Each release includes consumer YAML and a prominent -**USE AT YOUR OWN RISK** warning. Existing releases are never overwritten. - -```sh -git tag native-0.1.0-1 -git push origin native-0.1.0-1 -``` - -Enable [release immutability](https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/establish-provenance-and-integrity/prevent-release-changes) -to prevent published assets from being replaced. - -Both static and dynamic libraries are built: +Only dynamic libraries are provided, without sanitizers: | Platform | Targets | Minimum | | --- | --- | --- | -| Linux | x64 | glibc 2.35; no musl/Alpine | -| Windows | x64 | MSVC | +| Linux | x64 | glibc 2.28; no musl/Alpine | +| Windows | x64 | MSVC runtime | | macOS | arm64, x64 | macOS 11 | -| Android | armv7, arm64, x64 | API 21 | +| Android | armv7, arm64, x64 | API 21; C++ runtime linked statically | | iOS | arm64 device; arm64/x64 simulator | iOS 13 | -Linux deployments must meet the glibc minimum. Sanitized runtimes are unsupported. -For local builds, run from this repository's root with the native tools installed: +The floors, the Rust and Xcode versions, the location of the Rust workspace, +and the LMDB flags for Apple targets live in `dnix.json`. -```sh -flutter pub get -dart --packages=.dart_tool/package_config.json tool/build_prebuilt.dart --target aarch64-apple-darwin -dart --packages=.dart_tool/package_config.json tool/prebuilt_manifest.dart --artifacts build/prebuilts -``` +## Releases -Use fresh output directories; upload `build/native-release/*` together. -For Android, pass `--ndk` pointing to NDK 28.2.13676358. See `--help` for options. +`dnix.lock` records the hash of every library the Rust source builds to and +names the GitHub release, `native-`, that holds those bytes. After +changing the Rust code, run `dart tool/update_lock.dart` on a Mac with Nix +and Xcode 27.0 and commit the lock with the change; without a Mac, push and +copy the lock CI prints once every host is green. CI +(`.github/workflows/native.yml`) builds on four hosts, fails if any host or +the lock disagrees, and publishes the release when the lock reaches the +default branch. Nothing is committed by CI, and old releases stay. -To test the prebuilt tooling without compiling Rust: +Anyone can rebuild one platform with Nix and compare: +`dart tool/verify_release.dart linux` (or android, windows, macos, ios, or a +target triple), on Windows inside WSL2 for the non-Apple platforms. The build +comes from the shared [dnix](https://github.com/cypherstack/dnix) package, +whose documentation explains how the bytes stay identical across hosts. -```sh -flutter pub get -dart --packages=.dart_tool/package_config.json tool/test_prebuilt.dart -``` +## Tests + +`flutter test` runs the Dart tests against the bundled library. Before the +lock's release is published, or to test local Rust changes, they need the +library built from source: `echo dev > .dnix-build` with Nix installed +(`source` to also check the hash). From b80655cf767c164d0f00e0780bca5975f96397fc Mon Sep 17 00:00:00 2001 From: slopbot9k Date: Thu, 22 Oct 2026 17:44:04 -0600 Subject: [PATCH 4/9] Pin the dnix commit cypherstack merged dnix 7b9ef2d adds the Windows GNU and arm64 targets and stops zig adding its debug-mode sanitizer to C compiled without an optimisation level. Neither changes what this package builds to: every library hashes as before, so dnix.lock and the release it names stay as they are. --- .github/workflows/native.yml | 2 +- example/pubspec.lock | 4 ++-- flake.lock | 6 +++--- pubspec.yaml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml index f6dd84fa..3e92473c 100644 --- a/.github/workflows/native.yml +++ b/.github/workflows/native.yml @@ -20,6 +20,6 @@ concurrency: jobs: native: # The same dnix commit as pubspec.yaml and flake.lock. - uses: cypherstack/dnix/.github/workflows/native.yml@448c5da6e61b3f0e62b4eae6d08b49158a9e45b2 + uses: cypherstack/dnix/.github/workflows/native.yml@7b9ef2d34b6e1aeb05869da0843f85d28fd19bbd with: macos-runner: xcode-27 diff --git a/example/pubspec.lock b/example/pubspec.lock index 0ea9ebe3..f9613aa1 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -69,8 +69,8 @@ packages: dependency: transitive description: path: "." - ref: "448c5da6e61b3f0e62b4eae6d08b49158a9e45b2" - resolved-ref: "448c5da6e61b3f0e62b4eae6d08b49158a9e45b2" + ref: "7b9ef2d34b6e1aeb05869da0843f85d28fd19bbd" + resolved-ref: "7b9ef2d34b6e1aeb05869da0843f85d28fd19bbd" url: "https://github.com/cypherstack/dnix.git" source: git version: "0.1.0" diff --git a/flake.lock b/flake.lock index 4e0af1be..18686c96 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ "zig-overlay": "zig-overlay" }, "locked": { - "lastModified": 1792648476, - "narHash": "sha256-sNBlb09ut3gmCv2vMg+jEiUdF+slLC+T/9S+WBE6p3A=", + "lastModified": 1792694365, + "narHash": "sha256-0LJn3xO9HjoazjttvGentSEzt84bzqDuOGkYDuzcUrk=", "owner": "cypherstack", "repo": "dnix", - "rev": "448c5da6e61b3f0e62b4eae6d08b49158a9e45b2", + "rev": "7b9ef2d34b6e1aeb05869da0843f85d28fd19bbd", "type": "github" }, "original": { diff --git a/pubspec.yaml b/pubspec.yaml index 2a8f28e4..876238ca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: dnix: git: url: https://github.com/cypherstack/dnix.git - ref: 448c5da6e61b3f0e62b4eae6d08b49158a9e45b2 + ref: 7b9ef2d34b6e1aeb05869da0843f85d28fd19bbd dev_dependencies: From e1bd9ff712bca7fbe084d15711ad4d449bc5a824 Mon Sep 17 00:00:00 2001 From: slopbot9k Date: Thu, 22 Oct 2026 19:39:50 -0600 Subject: [PATCH 5/9] Fix the test workflow and tidy what the old flow left The arm64 Linux test job could never start: subosito/flutter-action finds no arm64 Flutter release for Linux and exits, and this package ships no Linux arm64 library in any case. Actions are pinned by commit, as they were on main. The test and args dev dependencies served the prebuilt tools that are gone, and .gitattributes now gives the reason its LF rules still matter. The README gives the arm64 simulator's minimum, iOS 14, and the CHANGELOG records the move to dnix. --- .gitattributes | 3 ++- .github/workflows/tests.yml | 8 ++++---- CHANGELOG.md | 13 +++++++++++++ README.md | 2 +- pubspec.yaml | 2 -- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/.gitattributes b/.gitattributes index a2e74c4c..2d9bc2f9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ -# Source fingerprints must be identical on Windows and Unix checkouts. +# Line endings stay LF on every checkout, Windows included, so every host +# builds from the same bytes. hook/** text eol=lf rust/src/** text eol=lf rust/Cargo.toml text eol=lf diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 13310e29..89da7446 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,21 +18,21 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-24.04, ubuntu-24.04-arm, xcode-27, windows-latest] + os: [ubuntu-24.04, xcode-27, windows-latest] runs-on: ${{ matrix.os }} timeout-minutes: 90 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: subosito/flutter-action@v2 + - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 with: flutter-version: 3.47.2 channel: stable cache: true # The hook builds through the Nix flake; before the first native # release that is the only path, and it is unavailable on Windows. - - uses: DeterminateSystems/nix-installer-action@v23 + - uses: DeterminateSystems/nix-installer-action@3138316df39ed29be04236d7ffc686fa525866aa # v23 if: runner.os != 'Windows' - run: flutter pub get - run: dart analyze hook tool test diff --git a/CHANGELOG.md b/CHANGELOG.md index 41cc7d81..3fc32ee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## Unreleased + +* **Breaking change:** the native library now comes from + [dnix](https://github.com/cypherstack/dnix). By default the build hook + downloads the library of the release `dnix.lock` names and verifies its + SHA-256; with the `build` user define set to `source` it builds the same + library with Nix, on Linux or an Apple Silicon Mac. The `native_build`, + `prebuilt_manifest_url` and `prebuilt_manifest_sha256` user defines are + no longer read: pinning this package's commit pins its libraries. There + is no Linux arm64 library, as there was no prebuilt one before. +* Every change is built on four hosts, which must produce the bytes + `dnix.lock` records; the default branch publishes the release it names. + ## 0.0.1 * TODO: Describe initial release. diff --git a/README.md b/README.md index 852f3c5e..93dbd8bd 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Only dynamic libraries are provided, without sanitizers: | Windows | x64 | MSVC runtime | | macOS | arm64, x64 | macOS 11 | | Android | armv7, arm64, x64 | API 21; C++ runtime linked statically | -| iOS | arm64 device; arm64/x64 simulator | iOS 13 | +| iOS | arm64 device; arm64/x64 simulator | iOS 13; arm64 simulator 14 | The floors, the Rust and Xcode versions, the location of the Rust workspace, and the LMDB flags for Apple targets live in `dnix.json`. diff --git a/pubspec.yaml b/pubspec.yaml index 876238ca..7b63f395 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - test: ^1.31.0 - args: ^2.7.0 # flutter_lints: ^2.0.0 From feae458fd07ff986fe9f8264b11179653a27ce45 Mon Sep 17 00:00:00 2001 From: slopbot9k Date: Fri, 23 Oct 2026 07:26:01 -0600 Subject: [PATCH 6/9] Say plainly that Linux arm64 is unsupported The CHANGELOG implied nothing was lost on Linux arm64 because no prebuilt library existed before. The old hook could build it from source; dnix's hook refuses any target dnix.json does not list, in every build mode. --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fc32ee7..c9e232c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,9 @@ SHA-256; with the `build` user define set to `source` it builds the same library with Nix, on Linux or an Apple Silicon Mac. The `native_build`, `prebuilt_manifest_url` and `prebuilt_manifest_sha256` user defines are - no longer read: pinning this package's commit pins its libraries. There - is no Linux arm64 library, as there was no prebuilt one before. + no longer read: pinning this package's commit pins its libraries. Linux + arm64 is not supported in any build mode; before, it could only be built + from source. * Every change is built on four hosts, which must produce the bytes `dnix.lock` records; the default branch publishes the release it names. From cf9f105436894e6951f0e0ffd6e02062a7e9e871 Mon Sep 17 00:00:00 2001 From: slopbot9k Date: Fri, 23 Oct 2026 17:17:26 -0600 Subject: [PATCH 7/9] Pin the dnix commit that builds the same bytes on macOS and Linux slopbot9k/dnix 57efe93, the stable-sort branch, not yet on cypherstack: build-time dependencies are resolved alike on every build host, rustc sorts the same everywhere, bindgen gets fixed headers, and a target keeps only its dynamic library. Every library changes with the first of these; the new lock matches what Linux x64, Linux arm64, and macOS each build. --- .github/workflows/native.yml | 2 +- dnix.lock | 20 ++++++++++---------- example/pubspec.lock | 6 +++--- flake.lock | 10 +++++----- flake.nix | 2 +- pubspec.yaml | 4 ++-- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml index 3e92473c..8b36ec20 100644 --- a/.github/workflows/native.yml +++ b/.github/workflows/native.yml @@ -20,6 +20,6 @@ concurrency: jobs: native: # The same dnix commit as pubspec.yaml and flake.lock. - uses: cypherstack/dnix/.github/workflows/native.yml@7b9ef2d34b6e1aeb05869da0843f85d28fd19bbd + uses: slopbot9k/dnix/.github/workflows/native.yml@57efe93b683e77d86a326a3a96ff217e6d6a4283 with: macos-runner: xcode-27 diff --git a/dnix.lock b/dnix.lock index 529868e3..a2bd34dc 100644 --- a/dnix.lock +++ b/dnix.lock @@ -1,16 +1,16 @@ { - "release": "fa0d223adae4", - "date": "2026-10-22", + "release": "c50d3097347e", + "date": "2026-10-23", "files": { - "aarch64-apple-darwin/libmwc_wallet.dylib": "3b77d160ea9a574c941b69745eee94a85dd97f7d6728c6b50c27d0a06130b27c", - "aarch64-apple-ios-sim/libmwc_wallet.dylib": "abaf2be17f304dcfff8009a3f6634c7465d9a270c0023773828105678c4cb421", - "aarch64-apple-ios/libmwc_wallet.dylib": "b8ac640a488ab1bce7d2eb1144c3957f46bf1c0d0206cbdfaec6aac3053c0fa8", - "aarch64-linux-android/libmwc_wallet.so": "8b945b20e0a0428e865a589caa5b78c39843ceb7e32da22e9375ff9a44720419", - "armv7-linux-androideabi/libmwc_wallet.so": "86c5e372856cfdbf21210405431635d36da9b6312b92fdf3c2c8b49e1fe553b3", + "aarch64-apple-darwin/libmwc_wallet.dylib": "499899e7208f813e5504e5f6b8c30bb8453083d58347819288f627aee6217828", + "aarch64-apple-ios-sim/libmwc_wallet.dylib": "1118f3b853eca86f29379462ced5112f11632503a3e4f50ebdeaa67c692e127b", + "aarch64-apple-ios/libmwc_wallet.dylib": "7e0fbbb0a5bc79456a716f043f29fedb0e6fa8c0a1690c9eda5239b74c7f12cf", + "aarch64-linux-android/libmwc_wallet.so": "cb361758267530c2b454f3aee1bdd0665b48d1da52beb956d9f76db6e57bd4f7", + "armv7-linux-androideabi/libmwc_wallet.so": "29cc0dd76d762ac72ac80cb368d0c322453301a8c98005e6315455a09173a6b4", "x86_64-apple-darwin/libmwc_wallet.dylib": "9318bffc19ac4685db4d66edf713f794f1e6f55bb640ac7a556d4110143deae7", "x86_64-apple-ios/libmwc_wallet.dylib": "77bf055d942b3444c6da8a4b11d5bf059882fb2b4be02f146a78a4d87a658412", - "x86_64-linux-android/libmwc_wallet.so": "16b547953fadcb97daddc27819d1579b5ac8d35527068888ca666b305e279eb6", - "x86_64-pc-windows-msvc/mwc_wallet.dll": "e4726f9588f3b08b4253ea639ed87ecc240a6dc0c8488cd9910e735582be4ad8", - "x86_64-unknown-linux-gnu/libmwc_wallet.so": "4bcd009278dd3bfdc7ec0d873dcb7c9a7f022d31365ced5064a54882cb1394a5" + "x86_64-linux-android/libmwc_wallet.so": "9009fa19513ec49c04e7f032d3b47f046122446d99facf97067ea1e3ae188b24", + "x86_64-pc-windows-msvc/mwc_wallet.dll": "1592d57fcfb478820110a2a04f06adae3229635b2495646de60e310fd726edb4", + "x86_64-unknown-linux-gnu/libmwc_wallet.so": "f366361b59307190de1e999f7fabdfeaa7b85ba72e869a384d0d4130515fe0a0" } } diff --git a/example/pubspec.lock b/example/pubspec.lock index f9613aa1..caf73835 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -69,9 +69,9 @@ packages: dependency: transitive description: path: "." - ref: "7b9ef2d34b6e1aeb05869da0843f85d28fd19bbd" - resolved-ref: "7b9ef2d34b6e1aeb05869da0843f85d28fd19bbd" - url: "https://github.com/cypherstack/dnix.git" + ref: "57efe93b683e77d86a326a3a96ff217e6d6a4283" + resolved-ref: "57efe93b683e77d86a326a3a96ff217e6d6a4283" + url: "https://github.com/slopbot9k/dnix.git" source: git version: "0.1.0" fake_async: diff --git a/flake.lock b/flake.lock index 18686c96..cd42b425 100644 --- a/flake.lock +++ b/flake.lock @@ -7,15 +7,15 @@ "zig-overlay": "zig-overlay" }, "locked": { - "lastModified": 1792694365, - "narHash": "sha256-0LJn3xO9HjoazjttvGentSEzt84bzqDuOGkYDuzcUrk=", - "owner": "cypherstack", + "lastModified": 1792794500, + "narHash": "sha256-Nf8+4VKQWYAE7telMnxGa2no18Yu4BemMZJ2PCeG+II=", + "owner": "slopbot9k", "repo": "dnix", - "rev": "7b9ef2d34b6e1aeb05869da0843f85d28fd19bbd", + "rev": "57efe93b683e77d86a326a3a96ff217e6d6a4283", "type": "github" }, "original": { - "owner": "cypherstack", + "owner": "slopbot9k", "repo": "dnix", "type": "github" } diff --git a/flake.nix b/flake.nix index 4b75d152..48379c9c 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,7 @@ { description = "Reproducible reference builds of flutter_libmwc's Rust library"; - inputs.dnix.url = "github:cypherstack/dnix"; + inputs.dnix.url = "github:slopbot9k/dnix"; outputs = { self, dnix, ... }: dnix.lib.package { src = self; diff --git a/pubspec.yaml b/pubspec.yaml index 7b63f395..6713fb0d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,8 +16,8 @@ dependencies: hooks: ^2.2.0 dnix: git: - url: https://github.com/cypherstack/dnix.git - ref: 7b9ef2d34b6e1aeb05869da0843f85d28fd19bbd + url: https://github.com/slopbot9k/dnix.git + ref: 57efe93b683e77d86a326a3a96ff217e6d6a4283 dev_dependencies: From 9ae2e8b972620a988859a31f8cc84fffe66567b9 Mon Sep 17 00:00:00 2001 From: slopbot9k Date: Tue, 27 Oct 2026 15:39:26 -0600 Subject: [PATCH 8/9] Pin the dnix commit cypherstack merged --- .github/workflows/native.yml | 2 +- example/pubspec.lock | 6 +++--- flake.lock | 8 ++++---- flake.nix | 2 +- pubspec.yaml | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml index 8b36ec20..6752bb64 100644 --- a/.github/workflows/native.yml +++ b/.github/workflows/native.yml @@ -20,6 +20,6 @@ concurrency: jobs: native: # The same dnix commit as pubspec.yaml and flake.lock. - uses: slopbot9k/dnix/.github/workflows/native.yml@57efe93b683e77d86a326a3a96ff217e6d6a4283 + uses: cypherstack/dnix/.github/workflows/native.yml@e91763ef25d084b3a5cf271a0cfee69159b1f779 with: macos-runner: xcode-27 diff --git a/example/pubspec.lock b/example/pubspec.lock index caf73835..884dfba6 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -69,9 +69,9 @@ packages: dependency: transitive description: path: "." - ref: "57efe93b683e77d86a326a3a96ff217e6d6a4283" - resolved-ref: "57efe93b683e77d86a326a3a96ff217e6d6a4283" - url: "https://github.com/slopbot9k/dnix.git" + ref: e91763ef25d084b3a5cf271a0cfee69159b1f779 + resolved-ref: e91763ef25d084b3a5cf271a0cfee69159b1f779 + url: "https://github.com/cypherstack/dnix.git" source: git version: "0.1.0" fake_async: diff --git a/flake.lock b/flake.lock index cd42b425..928b9315 100644 --- a/flake.lock +++ b/flake.lock @@ -7,15 +7,15 @@ "zig-overlay": "zig-overlay" }, "locked": { - "lastModified": 1792794500, + "lastModified": 1793124403, "narHash": "sha256-Nf8+4VKQWYAE7telMnxGa2no18Yu4BemMZJ2PCeG+II=", - "owner": "slopbot9k", + "owner": "cypherstack", "repo": "dnix", - "rev": "57efe93b683e77d86a326a3a96ff217e6d6a4283", + "rev": "e91763ef25d084b3a5cf271a0cfee69159b1f779", "type": "github" }, "original": { - "owner": "slopbot9k", + "owner": "cypherstack", "repo": "dnix", "type": "github" } diff --git a/flake.nix b/flake.nix index 48379c9c..4b75d152 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,7 @@ { description = "Reproducible reference builds of flutter_libmwc's Rust library"; - inputs.dnix.url = "github:slopbot9k/dnix"; + inputs.dnix.url = "github:cypherstack/dnix"; outputs = { self, dnix, ... }: dnix.lib.package { src = self; diff --git a/pubspec.yaml b/pubspec.yaml index 6713fb0d..1416dc76 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,8 +16,8 @@ dependencies: hooks: ^2.2.0 dnix: git: - url: https://github.com/slopbot9k/dnix.git - ref: 57efe93b683e77d86a326a3a96ff217e6d6a4283 + url: https://github.com/cypherstack/dnix.git + ref: e91763ef25d084b3a5cf271a0cfee69159b1f779 dev_dependencies: From 9cab2f98b264f80aaab9f65d1d424ad514d723e8 Mon Sep 17 00:00:00 2001 From: slopbot9k Date: Wed, 28 Oct 2026 14:05:50 -0600 Subject: [PATCH 9/9] Build the Linux arm64 library The target was left out because main shipped no prebuilt for it, not because anything stops the build: the library compiles like the others, and the lock now records it. The ten entries already in the lock are unchanged; only the release name moves with the lock. --- CHANGELOG.md | 5 ++--- README.md | 2 +- dnix.json | 1 + dnix.lock | 5 +++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9e232c7..1ed0d663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,10 @@ SHA-256; with the `build` user define set to `source` it builds the same library with Nix, on Linux or an Apple Silicon Mac. The `native_build`, `prebuilt_manifest_url` and `prebuilt_manifest_sha256` user defines are - no longer read: pinning this package's commit pins its libraries. Linux - arm64 is not supported in any build mode; before, it could only be built - from source. + no longer read: pinning this package's commit pins its libraries. * Every change is built on four hosts, which must produce the bytes `dnix.lock` records; the default branch publishes the release it names. +* A Linux arm64 library, which before could only be built from source. ## 0.0.1 diff --git a/README.md b/README.md index 93dbd8bd..b4c65543 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Only dynamic libraries are provided, without sanitizers: | Platform | Targets | Minimum | | --- | --- | --- | -| Linux | x64 | glibc 2.28; no musl/Alpine | +| Linux | arm64, x64 | glibc 2.28; no musl/Alpine | | Windows | x64 | MSVC runtime | | macOS | arm64, x64 | macOS 11 | | Android | armv7, arm64, x64 | API 21; C++ runtime linked statically | diff --git a/dnix.json b/dnix.json index 66f3e526..2a90f98e 100644 --- a/dnix.json +++ b/dnix.json @@ -45,6 +45,7 @@ "aarch64-linux-android", "armv7-linux-androideabi", "x86_64-linux-android", + "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc" ] diff --git a/dnix.lock b/dnix.lock index a2bd34dc..cb43bcae 100644 --- a/dnix.lock +++ b/dnix.lock @@ -1,11 +1,12 @@ { - "release": "c50d3097347e", - "date": "2026-10-23", + "release": "41ce961e0a4b", + "date": "2026-10-28", "files": { "aarch64-apple-darwin/libmwc_wallet.dylib": "499899e7208f813e5504e5f6b8c30bb8453083d58347819288f627aee6217828", "aarch64-apple-ios-sim/libmwc_wallet.dylib": "1118f3b853eca86f29379462ced5112f11632503a3e4f50ebdeaa67c692e127b", "aarch64-apple-ios/libmwc_wallet.dylib": "7e0fbbb0a5bc79456a716f043f29fedb0e6fa8c0a1690c9eda5239b74c7f12cf", "aarch64-linux-android/libmwc_wallet.so": "cb361758267530c2b454f3aee1bdd0665b48d1da52beb956d9f76db6e57bd4f7", + "aarch64-unknown-linux-gnu/libmwc_wallet.so": "62928987451dfa14184bb818664100a792606a507129fb2eeedd8b96a908420b", "armv7-linux-androideabi/libmwc_wallet.so": "29cc0dd76d762ac72ac80cb368d0c322453301a8c98005e6315455a09173a6b4", "x86_64-apple-darwin/libmwc_wallet.dylib": "9318bffc19ac4685db4d66edf713f794f1e6f55bb640ac7a556d4110143deae7", "x86_64-apple-ios/libmwc_wallet.dylib": "77bf055d942b3444c6da8a4b11d5bf059882fb2b4be02f146a78a4d87a658412",