Skip to content

Commit

Permalink
[native_assets_cli] Add example for downloading assets (#1860)
Browse files Browse the repository at this point in the history
Add an example for how to download assets in a hook which are built on GitHub CI.

Design approach:

* `tool/build.dart` uses `package:native_toolchain_c`.
* GitHub action that publishes artifacts on a tag (not the package release tag).
* `hook/build.dart` downloads the artifacts.
* `tool/generate_asset_hashes.dart` generates hashes that are used in `hook/build.dart` to check download integrity.

GitHub actions mostly cargo culted from:

* https://github.com/dart-lang/i18n/blob/83e8d08568e26888dc0252c89f5d1faf92052e44/.github/workflows/intl4x_artifacts.yml

Closes: #157
  • Loading branch information
dcharkes authored Jan 9, 2025
1 parent a646680 commit 43ad99c
Show file tree
Hide file tree
Showing 19 changed files with 615 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/native.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ jobs:
- run: dart pub get -C test_data/native_dynamic_linking/
if: ${{ matrix.package == 'native_assets_builder' }}

- run: dart pub get -C example/build/download_asset/
if: ${{ matrix.package == 'native_assets_cli' }}

- run: dart pub get -C example/build/native_dynamic_linking/
if: ${{ matrix.package == 'native_assets_cli' }}

Expand Down
119 changes: 119 additions & 0 deletions .github/workflows/package_download_asset.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# A workflow that goes together with the example package:download_asset inside
# package:native_assets_cli.
name: package_download_asset

permissions:
contents: write

on:
pull_request:
branches: [ main ]
paths:
- .github/workflows/package_download_asset.yaml
- pkgs/native_assets_cli/example/build/download_asset/
push:
tags:
- 'download_asset-prebuild-assets-*'
workflow_dispatch:

jobs:
build:
strategy:
fail-fast: false
matrix:
os: [ubuntu, macos, windows]

runs-on: ${{ matrix.os }}-latest

defaults:
run:
working-directory: pkgs/native_assets_cli/example/build/download_asset/

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

- uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
with:
sdk: stable

- uses: nttld/setup-ndk@afb4c9964b521afb97c864b7d40b11e6911bd410
with:
ndk-version: r27
if: ${{ matrix.os == 'ubuntu' }} # Only build on one host.

- name: Install native toolchains
run: sudo apt-get update && sudo apt-get install clang-15 gcc-i686-linux-gnu gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf gcc-riscv64-linux-gnu
if: ${{ matrix.os == 'ubuntu' }}

- run: dart pub get

# Keep this list consistent with pkgs/native_assets_cli/example/build/download_asset/lib/src/hook_helpers/target_versions.dart
- name: Build Linux host
if: matrix.os == 'ubuntu'
run: |
dart tool/build.dart -oandroid -aarm
dart tool/build.dart -oandroid -aarm64
dart tool/build.dart -oandroid -aia32
dart tool/build.dart -oandroid -ariscv64
dart tool/build.dart -oandroid -ax64
dart tool/build.dart -olinux -aarm
dart tool/build.dart -olinux -aarm64
dart tool/build.dart -olinux -aia32
dart tool/build.dart -olinux -ariscv64
dart tool/build.dart -olinux -ax64
- name: Build MacOS host
if: matrix.os == 'macos'
run: |
dart tool/build.dart -omacos -aarm64
dart tool/build.dart -omacos -ax64
dart tool/build.dart -oios -iiphoneos -aarm64
dart tool/build.dart -oios -iiphonesimulator -aarm64
dart tool/build.dart -oios -iiphonesimulator -ax64
- name: Build Windows host
if: matrix.os == 'windows'
run: |
dart tool/build.dart -owindows -aarm
dart tool/build.dart -owindows -aarm64
dart tool/build.dart -owindows -aia32
dart tool/build.dart -owindows -ax64
- name: Upload artifacts
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882
with:
name: ${{ matrix.os }}-host
path: |
pkgs/native_assets_cli/example/build/download_asset/.dart_tool/download_asset/**/*.dll
pkgs/native_assets_cli/example/build/download_asset/.dart_tool/download_asset/**/*.dylib
pkgs/native_assets_cli/example/build/download_asset/.dart_tool/download_asset/**/*.so
if-no-files-found: error

release:
needs: build
runs-on: ubuntu-latest

defaults:
run:
working-directory: pkgs/native_assets_cli/example/build/download_asset/

steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9
with:
submodules: true

- name: Download assets
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16
with:
merge-multiple: true
path: pkgs/native_assets_cli/example/build/download_asset/.dart_tool/download_asset/

- name: Display structure of downloaded assets
run: ls -R .dart_tool/download_asset/

- name: Release
uses: softprops/action-gh-release@e7a8f85e1c67a31e6ed99a94b41bd0b71bbee6b8
if: startsWith(github.ref, 'refs/tags/download_asset-prebuild-assets')
with:
files: 'pkgs/native_assets_cli/example/build/download_asset/.dart_tool/download_asset/**'
fail_on_unmatched_files: true
15 changes: 15 additions & 0 deletions pkgs/native_assets_cli/example/build/download_asset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
An example of a library depending on prebuilt assets which are downloaded in
the build hook.

## Usage

Run tests with `dart --enable-experiment=native-assets test`.

## Code organization

A typical layout of a package which downloads assets:

* `tool/build.dart` prebuilts assets and is exercised from a GitHub workflow.
* A [GitHub workflow](../../../../../.github/workflows/package_download_asset.yaml) that builds assets.
* `hook/build.dart` downloads the prebuilt assets.
* `lib/` contains Dart code which uses the assets.
20 changes: 20 additions & 0 deletions pkgs/native_assets_cli/example/build/download_asset/ffigen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Run with `flutter pub run ffigen --config ffigen.yaml`.
name: NativeAddBindings
description: |
Bindings for `src/native_add.h`.
Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`.
output: 'lib/native_add.dart'
headers:
entry-points:
- 'src/native_add.h'
include-directives:
- 'src/native_add.h'
preamble: |
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
comments:
style: any
length: full
ffi-native:
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:io';

import 'package:download_asset/src/hook_helpers/c_build.dart';
import 'package:download_asset/src/hook_helpers/download.dart';
import 'package:download_asset/src/hook_helpers/hashes.dart';
import 'package:native_assets_cli/code_assets_builder.dart';
import 'package:native_assets_cli/native_assets_cli.dart';

void main(List<String> args) async {
// TODO(https://github.com/dart-lang/native/issues/39): Use user-defines to
// control this instead.
const localBuild = false;

await build(args, (input, output) async {
// ignore: dead_code
if (localBuild) {
await runBuild(input, output);
} else {
final targetOS = input.config.code.targetOS;
final targetArchitecture = input.config.code.targetArchitecture;
final iOSSdk =
targetOS == OS.iOS ? input.config.code.iOS.targetSdk : null;
final outputDirectory = Directory.fromUri(input.outputDirectory);
final file = await downloadAsset(
targetOS,
targetArchitecture,
iOSSdk,
outputDirectory,
);
final fileHash = await hashAsset(file);
final expectedHash = assetHashes[createTargetName(
targetOS.name,
targetArchitecture.name,
iOSSdk?.type,
)];
if (fileHash != expectedHash) {
throw Exception('File $file was not downloaded correctly. '
'Found hash $fileHash, expected $expectedHash.');
}
output.assets.code.add(CodeAsset(
package: input.packageName,
name: 'native_add.dart',
linkMode: DynamicLoadingBundled(),
os: targetOS,
architecture: targetArchitecture,
file: file.uri,
));
}
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// AUTO GENERATED FILE, DO NOT EDIT.
//
// Generated by `package:ffigen`.
// ignore_for_file: type=lint
import 'dart:ffi' as ffi;

@ffi.Native<ffi.Int32 Function(ffi.Int32, ffi.Int32)>(symbol: 'add')
external int add(
int a,
int b,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:logging/logging.dart';
import 'package:native_assets_cli/code_assets_builder.dart';
import 'package:native_toolchain_c/native_toolchain_c.dart';

Future<void> runBuild(BuildInput input, BuildOutputBuilder output) async {
final name = createTargetName(
input.config.code.targetOS.name,
input.config.code.targetArchitecture.name,
input.config.code.targetOS == OS.iOS
? input.config.code.iOS.targetSdk.type
: null,
);
final cbuilder = CBuilder.library(
name: name,
assetName: 'native_add.dart',
sources: [
'src/native_add.c',
],
);
await cbuilder.run(
input: input,
output: output,
logger: Logger('')
..level = Level.ALL
..onRecord.listen((record) => print(record.message)),
);
}

String createTargetName(String osString, String architecture, String? iOSSdk) {
var targetName = 'native_add_${osString}_$architecture';
if (iOSSdk != null) {
targetName += '_$iOSSdk';
}
return targetName;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:io';

import 'package:crypto/crypto.dart';
import 'package:native_assets_cli/code_assets_builder.dart';

import 'c_build.dart';
import 'version.dart';

Uri downloadUri(String target) => Uri.parse(
'https://github.com/dart-lang/native/releases/download/$version/$target');

Future<File> downloadAsset(
OS targetOS,
Architecture targetArchitecture,
IOSSdk? iOSSdk,
Directory outputDirectory,
) async {
final targetName = targetOS.dylibFileName(createTargetName(
targetOS.name,
targetArchitecture.name,
iOSSdk?.type,
));
final uri = downloadUri(targetName);
final request = await HttpClient().getUrl(uri);
final response = await request.close();
if (response.statusCode != 200) {
throw ArgumentError('The request to $uri failed.');
}
final library = File.fromUri(outputDirectory.uri.resolve(targetName));
await library.create();
await response.pipe(library.openWrite());
return library;
}

Future<String> hashAsset(File assetFile) async {
// TODO(dcharkes): Should this be a strong hash to not only check for download
// integrity but also safeguard against tampering? This would protected
// against the case where the binary hoster is compromised but pub is not
// compromised.
final fileHash = md5.convert(await assetFile.readAsBytes()).toString();
return fileHash;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// THIS FILE IS AUTOGENERATED. TO UPDATE, RUN
//
// dart --enable-experiment=native-assets tool/generate_asset_hashes.dart
//

const assetHashes = <String, String>{
'libnative_add_android_arm.so': '2c38f3edc805a399dad866d619f9157d',
'libnative_add_android_arm64.so': 'c4f0d8c4c50d1e83592e499e7434b967',
'libnative_add_android_ia32.so': 'e3277144d97bd2c54beee581ed7e6665',
'libnative_add_android_riscv64.so': '8c2576cbe75c9a23f2532ff895f94f76',
'libnative_add_android_x64.so': '9a7bec53e1591091669ecd2bd20911d1',
'libnative_add_ios_arm64_iphoneos.dylib': '1bf1473cacb7fd2778fc5bb28f0b61a2',
'libnative_add_ios_arm64_iphonesimulator.dylib':
'cdddffc0787e6a3a846affcb05fac3a8',
'libnative_add_ios_x64_iphonesimulator.dylib':
'4aea6d631350540d50452ac2bdd1a422',
'libnative_add_linux_arm.so': '1a5b9e4b459e13ee85c148582b9b2252',
'libnative_add_linux_arm64.so': '2b3d736e0c0ac1e1537bc43bd9b82cad',
'libnative_add_linux_ia32.so': 'ed6e130e53fa18eab5572ed106cdaab1',
'libnative_add_linux_riscv64.so': '7fa82325ba7803a0443ca27e3300e7f9',
'libnative_add_linux_x64.so': 'f7af8d1547cdfb150a73d513a7957999',
'libnative_add_macos_arm64.dylib': 'f0804ff4b55126996c180114f25ca5bd',
'libnative_add_macos_x64.dylib': 'b62263803dceb3c23508cb909b5a5583',
'native_add_windows_arm64.dll': '7aa6f5e0275ba1b94cd5b7356f89ef04',
'native_add_windows_ia32.dll': 'ab57b5504d92b5b5dc09732d990fd5e7',
'native_add_windows_x64.dll': 'be9ba2125800aa2e3481a759b1845a50',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

const androidTargetNdkApi = 30;
const int macOSTargetVersion = 13;
const iOSTargetVersion = 16;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c, null) 2025, the Dart project authors. Please see the AUTHORS
// file for details. All rights reserved. Use of this source code is governed by
// a BSD-style license that can be found in the LICENSE file.

// THIS FILE IS AUTOGENERATED. TO UPDATE, RUN
//
// dart --enable-experiment=native-assets tool/generate_asset_hashes.dart
//

import 'package:native_assets_cli/code_assets_builder.dart';

const supportedTargets = [
(OS.android, Architecture.arm, null),
(OS.android, Architecture.arm64, null),
(OS.android, Architecture.ia32, null),
(OS.android, Architecture.riscv64, null),
(OS.android, Architecture.x64, null),
(OS.iOS, Architecture.arm64, IOSSdk.iPhoneOS),
(OS.iOS, Architecture.arm64, IOSSdk.iPhoneSimulator),
(OS.iOS, Architecture.x64, IOSSdk.iPhoneSimulator),
(OS.linux, Architecture.arm, null),
(OS.linux, Architecture.arm64, null),
(OS.linux, Architecture.ia32, null),
(OS.linux, Architecture.riscv64, null),
(OS.linux, Architecture.x64, null),
(OS.macOS, Architecture.arm64, null),
(OS.macOS, Architecture.x64, null),
(OS.windows, Architecture.arm64, null),
(OS.windows, Architecture.ia32, null),
(OS.windows, Architecture.x64, null),
];
Loading

0 comments on commit 43ad99c

Please sign in to comment.