Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .github/workflows/build-prebuilt-assets.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: build-prebuilt-assets

on:
workflow_dispatch:

# This workflow only produces artifacts. Maintainers can review and commit the
# resulting prebuilts in a follow-up PR or release-prep change.

permissions:
contents: read

jobs:
build:
name: Build prebuilt asset (${{ matrix.asset_os }} / ${{ matrix.asset_arch }})
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
asset_os: linux
asset_arch: x64
library_name: libwebcrypto.so
- runner: windows-latest
asset_os: windows
asset_arch: x64
library_name: webcrypto.dll
- runner: macos-13
asset_os: macos
asset_arch: x64
library_name: libwebcrypto.dylib
- runner: macos-15
asset_os: macos
asset_arch: arm64
library_name: libwebcrypto.dylib
steps:
- uses: actions/checkout@v4

- name: Install NASM
if: runner.os == 'Windows'
uses: ilammy/setup-nasm@v1

- name: Configure Linux dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build

- name: Build native library
shell: bash
run: |
set -euo pipefail

build_dir="$PWD/build/prebuilt"
install_dir="$PWD/build/prebuilt-install"
rm -rf "$build_dir" "$install_dir"

cmake -S src -B "$build_dir" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$install_dir"

cmake --build "$build_dir" --config Release --target install

staged_dir="prebuilt/${{ matrix.asset_os }}/${{ matrix.asset_arch }}"
mkdir -p "$staged_dir"
cp "$install_dir/${{ matrix.library_name }}" "$staged_dir/${{ matrix.library_name }}"

- name: Upload prebuilt artifact
uses: actions/upload-artifact@v4
with:
name: webcrypto-prebuilt-${{ matrix.asset_os }}-${{ matrix.asset_arch }}
path: prebuilt/${{ matrix.asset_os }}/${{ matrix.asset_arch }}/${{ matrix.library_name }}
13 changes: 12 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,18 @@ jobs:
- uses: actions/checkout@v4
- uses: dart-lang/setup-dart@v1
- run: dart pub get --no-example
- run: xvfb-run dart test --exclude-tags browser-interop -p vm,chrome,firefox -c dart2js,dart2wasm
- run: xvfb-run dart test -p vm,chrome,firefox -c dart2js,dart2wasm
linux-source-build:
name: Linux source build / Chrome / Firefox
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: dart-lang/setup-dart@v1
- name: Force source builds for hook validation
run: touch .ci_force_source_build
- run: dart pub get --no-example
- run: xvfb-run dart test -p vm,chrome,firefox -c dart2js,dart2wasm
macos:
name: MacOS desktop / Chrome
runs-on: macos-15 # Test with xcode 16
Expand Down
67 changes: 66 additions & 1 deletion hook/build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import 'package:hooks/hooks.dart';
import 'package:native_toolchain_cmake/native_toolchain_cmake.dart';

const _assetName = 'webcrypto.dart';
const _prebuiltRoot = 'prebuilt';
const _forceSourceBuildMarker = '.ci_force_source_build';

Future<void> main(List<String> args) async {
await build(args, (input, output) async {
Expand All @@ -31,12 +33,43 @@ Future<void> main(List<String> args) async {
}

final packageRoot = input.packageRoot;
final config = input.config.code;
final installDir = input.outputDirectory.resolve('install/');
final sourceDir = packageRoot.resolve('src/');
final forceSourceBuild = _shouldForceSourceBuild(packageRoot);
final prebuilt = forceSourceBuild
? null
: _findPrebuiltAsset(packageRoot, config);

if (forceSourceBuild) {
stdout.writeln('webcrypto: forcing source build for CI validation.');
}

if (prebuilt != null) {
stdout.writeln(
'webcrypto: using prebuilt native asset for '
'${config.targetOS}-${config.targetArchitecture}.',
);

final stagedPrebuilt = await _stagePrebuiltAsset(
input: input,
prebuilt: prebuilt,
);
output.assets.code.add(
CodeAsset(
package: input.packageName,
name: _assetName,
linkMode: DynamicLoadingBundled(),
file: stagedPrebuilt,
),
);
output.dependencies.add(prebuilt);
return;
}

stdout.writeln(
'webcrypto: building native asset for '
'${input.config.code.targetOS}-${input.config.code.targetArchitecture}.',
'${config.targetOS}-${config.targetArchitecture}.',
);

final builder = CMakeBuilder.create(
Expand Down Expand Up @@ -69,6 +102,38 @@ Future<void> main(List<String> args) async {
});
}

Uri? _findPrebuiltAsset(Uri packageRoot, CodeConfig config) {
final fileName = config.targetOS.dylibFileName('webcrypto');
final prebuilt = packageRoot.resolve(
'$_prebuiltRoot/'
'${config.targetOS.name}/'
'${config.targetArchitecture.name}/'
'$fileName',
);
return File.fromUri(prebuilt).existsSync() ? prebuilt : null;
}

bool _shouldForceSourceBuild(Uri packageRoot) =>
File.fromUri(packageRoot.resolve(_forceSourceBuildMarker)).existsSync();

Future<Uri> _stagePrebuiltAsset({
required BuildInput input,
required Uri prebuilt,
}) async {
final targetDir = input.outputDirectoryShared.resolve(
'$_prebuiltRoot/'
'${input.config.code.targetOS.name}/'
'${input.config.code.targetArchitecture.name}/',
);
final targetDirectory = Directory.fromUri(targetDir);
await targetDirectory.create(recursive: true);

final fileName = prebuilt.pathSegments.last;
final staged = targetDir.resolve(fileName);
await File.fromUri(prebuilt).copy(staged.toFilePath());
return staged;
}

final _buildDependencyExtensions = {
'.S',
'.asm',
Expand Down
28 changes: 28 additions & 0 deletions prebuilt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Prebuilt native assets
======================

This directory is reserved for prebuilt `webcrypto` native libraries that can
be bundled by `hook/build.dart` without requiring a local C/C++ toolchain.

These binaries should be generated by maintainer-triggered CI from merged
source, then committed by a maintainer. They should not be accepted from
user-authored PRs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just want to highlight alternative options, in no particular order.

  • (A) We build prebuilt/ as part of the automated publishing job, they are always build github actions and published inside the package to pub.dev
  • (B) We build prebuilt/ and upload as artifacts on the github release we create when we create a new version, the hook will then have to download these from github releases.
  • (C) We build prebuilt/ and then commit them to git.

(C) has the downside that repository size will increase significantly over time, especially if we add more architectures/platforms.

(A) has the downside that download size of users will increase, but not that much. pub.dev will refuse packages larger than 100mb or 200mb (I don't recall the exact limits), but we'll probably be okay unless we add many architectures.


If we can build the artifacts in github CI, then perhaps (A) is attractive? We also avoid maintainer churn and avoid having to store binary artifacts in github.


Expected layout:

- `prebuilt/<os>/<architecture>/<library-file>`

Examples:

- `prebuilt/linux/x64/libwebcrypto.so`
- `prebuilt/macos/arm64/libwebcrypto.dylib`
- `prebuilt/windows/x64/webcrypto.dll`

If a matching prebuilt is present for the current target, the build hook will
stage and bundle it. If not, the hook will fall back to building from source.

Expected refresh policy:

- refresh on release preparation
- refresh after native-impacting changes
- do not rebuild on a time-based schedule
Loading