Skip to content
Draft
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
110 changes: 110 additions & 0 deletions .github/workflows/release-artifacts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: Build & Upload Release Artifacts

on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*'
workflow_dispatch:

jobs:
build-artifacts:
name: Build Native Artifact (${{ matrix.target.os }}-${{ matrix.target.arch }})
runs-on: ${{ matrix.target.runner }}
strategy:
fail-fast: false
matrix:
target:
- os: linux
arch: x64
runner: ubuntu-latest
artifact: libwebcrypto.so
asset_name: webcrypto-linux-x64-libwebcrypto.so
- os: macos
arch: x64
runner: macos-13
artifact: libwebcrypto.dylib
asset_name: webcrypto-macOS-x64-libwebcrypto.dylib
- os: macos
arch: arm64
runner: macos-14
artifact: libwebcrypto.dylib
asset_name: webcrypto-macOS-arm64-libwebcrypto.dylib
- os: windows
arch: x64
runner: windows-latest
artifact: webcrypto.dll
asset_name: webcrypto-windows-x64-webcrypto.dll
- os: android
arch: arm64
abi: arm64-v8a
runner: ubuntu-latest
artifact: libwebcrypto.so
asset_name: webcrypto-android-arm64-libwebcrypto.so
- os: android
arch: arm
abi: armeabi-v7a
runner: ubuntu-latest
artifact: libwebcrypto.so
asset_name: webcrypto-android-arm-libwebcrypto.so
- os: android
arch: x64
abi: x86_64
runner: ubuntu-latest
artifact: libwebcrypto.so
asset_name: webcrypto-android-x64-libwebcrypto.so
- os: iOS
arch: arm64
runner: macos-14
artifact: libwebcrypto.dylib
asset_name: webcrypto-iOS-arm64-libwebcrypto.dylib

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: subosito/flutter-action@f37a2a73da312e3e63f5c0b9634526d2b0e7fe88
with:
channel: 'stable'
cache: true

- name: Configure Linux Dependencies
if: matrix.target.os == 'linux'
run: |
sudo apt-get update -y
sudo apt-get install -y cmake ninja-build libgtk-3-dev

- name: Build Native Library (native_toolchain_cmake)
run: |
dart pub get
dart run tool/build_native.dart --target-os ${{ matrix.target.os }} --target-architecture ${{ matrix.target.arch }} --output-dir ./build_native

- name: Locate, Rename, and Hash Artifact
shell: bash
run: |
LIBRARY_PATH=$(find build_native -name "${{ matrix.target.artifact }}" | head -n 1)
if [ -z "$LIBRARY_PATH" ]; then
echo "Error: Could not find ${{ matrix.target.artifact }}"
exit 1
fi
cp "$LIBRARY_PATH" "./${{ matrix.target.asset_name }}"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "./${{ matrix.target.asset_name }}" > "./${{ matrix.target.asset_name }}.sha256"
else
shasum -a 256 "./${{ matrix.target.asset_name }}" > "./${{ matrix.target.asset_name }}.sha256"
fi

- name: Upload Build Artifacts (Workflow Debugging)
uses: actions/upload-artifact@b4b15b348d6a79ff96702e61a5847e68810fe949
with:
name: ${{ matrix.target.asset_name }}
path: |
./${{ matrix.target.asset_name }}
./${{ matrix.target.asset_name }}.sha256

- name: Upload Release Asset (Release Tags Only)
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@c95fe0c20b7ea1f2c1494446929e7f3e254c693a
with:
files: |
./${{ matrix.target.asset_name }}
./${{ matrix.target.asset_name }}.sha256
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# 0.6.2-wip
* Fixed JS interop to enable WebAssembly.
* Upgrade `ffigen` and `hooks` versions.

# 0.6.1
* Added Dart native build hooks and native asset lookup for the bundled
Expand Down
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,29 @@ see `doc/design-rationale-md`.

## System dependencies

When you have a dependency on `package:webcrypto`, it will use
[hooks](https://dart.dev/tools/hooks) to build BoringSSL. Thus, your system
must have:
By default, `package:webcrypto` uses [hooks](https://dart.dev/tools/hooks) to fetch prebuilt native binaries, so no additional system dependencies are required for supported target platforms.

### Build Configuration

You can configure the build mode in `pubspec.yaml`:

```yaml
hooks:
user_defines:
webcrypto:
buildMode: fetch # Options: 'fetch' (default), 'build', 'local'
```

* `fetch` (default): Downloads prebuilt binaries from release assets (falls back to building from source if prebuilt binaries are unavailable).
* `build`: Always builds BoringSSL locally from source using CMake.
* `local`: Uses a prebuilt binary from a specified local path (`localPath`).

If you configure `package:webcrypto` to build from source, your system must have:

* `cmake`, and,
* a C compiler (like `gcc` or `clang`)


## Limitations
This package has a few limitations compared to the
[Web Cryptography API][webcrypto-spec]. For a discussion of parity with
Expand Down
200 changes: 174 additions & 26 deletions hook/build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,37 @@
import 'dart:io';

import 'package:code_assets/code_assets.dart';
import 'package:crypto/crypto.dart' show sha256;
import 'package:hooks/hooks.dart';
import 'package:native_toolchain_cmake/native_toolchain_cmake.dart';
import 'package:webcrypto/src/hook_helpers/hashes.dart'
show fileHashes, releaseVersion;

const _assetName = 'webcrypto.dart';

enum BuildModeEnum { fetch, build, local }

class BuildOptions {
final BuildModeEnum buildMode;
final Uri? localPath;

BuildOptions({required this.buildMode, this.localPath});

factory BuildOptions.fromDefines(HookInputUserDefines defines) {
return BuildOptions(
buildMode: BuildModeEnum.values.firstWhere(
(element) => element.name == defines['buildMode'],
orElse: () => BuildModeEnum.fetch,
),
localPath: defines.path('localPath'),
);
}

@override
String toString() =>
'BuildOptions(buildMode: $buildMode, localPath: $localPath)';
}

Future<void> main(List<String> args) async {
await build(args, (input, output) async {
// Skip build for non-code targets (e.g. web builds via flutter drive).
Expand All @@ -30,43 +56,165 @@ Future<void> main(List<String> args) async {
return;
}

final packageRoot = input.packageRoot;
final installDir = input.outputDirectory.resolve('install/');
final sourceDir = packageRoot.resolve('src/');
final buildOptions = BuildOptions.fromDefines(input.userDefines);
stdout.writeln('webcrypto: build options: $buildOptions');

switch (buildOptions.buildMode) {
case BuildModeEnum.fetch:
await _fetchPrebuiltBinary(input, output);
case BuildModeEnum.build:
await _buildLocalCMake(input, output);
case BuildModeEnum.local:
await _useLocalBinary(input, output, buildOptions.localPath);
}
output.dependencies.add(input.packageRoot.resolve('pubspec.yaml'));
});
}

Future<void> _fetchPrebuiltBinary(
BuildInput input,
BuildOutputBuilder output,
) async {
final targetOS = input.config.code.targetOS;
final targetArch = input.config.code.targetArchitecture;
final dylibFileName = targetOS.dylibFileName('webcrypto');

final targetTriple = '${targetOS.name}-${targetArch.name}';
final expectedHash = fileHashes[targetTriple];

if (expectedHash == null || expectedHash.isEmpty) {
stdout.writeln(
'webcrypto: building native asset for '
'${input.config.code.targetOS}-${input.config.code.targetArchitecture}.',
'webcrypto: no prebuilt binary hash registered for $targetTriple, falling back to building from source.',
);
await _buildLocalCMake(input, output);
return;
}

final builder = CMakeBuilder.create(
name: 'webcrypto',
sourceDir: sourceDir,
defines: {
'CMAKE_BUILD_TYPE': 'Release',
'CMAKE_INSTALL_PREFIX': installDir.toFilePath(),
},
targets: ['install'],
);
final assetName = 'webcrypto-$targetTriple-$dylibFileName';
final binaryUrl = Uri.parse(
'https://github.com/google/webcrypto.dart/releases/download/v$releaseVersion/$assetName',
);

await builder.run(input: input, output: output);
stdout.writeln('webcrypto: fetching prebuilt binary from $binaryUrl...');

final assets = await output.findAndAddCodeAssets(
input,
outDir: installDir,
names: {r'(lib)?webcrypto\.(dll|dylib|so)': _assetName},
regExp: true,
);
if (assets.isEmpty) {
final client = HttpClient();
try {
final request = await client.getUrl(binaryUrl);
final response = await request.close();
if (response.statusCode != 200) {
throw BuildError(
message:
'Failed to locate built webcrypto dynamic library in '
'${installDir.toFilePath()}',
'Failed to fetch prebuilt webcrypto binary from $binaryUrl (HTTP ${response.statusCode}).\n'
'To build webcrypto locally from source instead, set `buildMode: build` in your pubspec.yaml under `hooks.user_defines.webcrypto`.',
);
}

output.dependencies.addAll(_buildDependencies(packageRoot));
});
final bytes = await response.fold<List<int>>([], (a, b) => a..addAll(b));
final actualHash = sha256.convert(bytes).toString();

if (actualHash != expectedHash) {
throw BuildError(
message:
'SHA256 hash mismatch for prebuilt binary $assetName.\n'
'Expected: $expectedHash\n'
'Actual: $actualHash\n'
'To build webcrypto locally from source instead, set `buildMode: build` in your pubspec.yaml under `hooks.user_defines.webcrypto`.',
);
}

stdout.writeln('webcrypto: verified SHA256 checksum ($actualHash).');

final libraryFile = File.fromUri(
input.outputDirectory.resolve(dylibFileName),
);
await libraryFile.writeAsBytes(bytes);

output.assets.code.add(
CodeAsset(
package: input.packageName,
name: _assetName,
linkMode: DynamicLoadingBundled(),
file: libraryFile.uri,
),
);
} finally {
client.close();
}
}

Future<void> _useLocalBinary(
BuildInput input,
BuildOutputBuilder output,
Uri? localPath,
) async {
if (localPath == null) {
throw BuildError(
message:
'buildMode is set to `local`, but `localPath` was not specified under `hooks.user_defines.webcrypto`.',
);
}
final file = File.fromUri(localPath);
if (!file.existsSync()) {
throw BuildError(
message:
'Specified local binary does not exist at ${localPath.toFilePath()}',
);
}
final dylibFileName = input.config.code.targetOS.dylibFileName('webcrypto');
final destFile = File.fromUri(input.outputDirectory.resolve(dylibFileName));
await file.copy(destFile.path);

output.assets.code.add(
CodeAsset(
package: input.packageName,
name: _assetName,
linkMode: DynamicLoadingBundled(),
file: destFile.uri,
),
);
output.dependencies.add(localPath);
}

Future<void> _buildLocalCMake(
BuildInput input,
BuildOutputBuilder output,
) async {
final packageRoot = input.packageRoot;
final installDir = input.outputDirectory.resolve('install/');
final sourceDir = packageRoot.resolve('src/');

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

final builder = CMakeBuilder.create(
name: 'webcrypto',
sourceDir: sourceDir,
defines: {
'CMAKE_BUILD_TYPE': 'Release',
'CMAKE_INSTALL_PREFIX': installDir.toFilePath(),
},
targets: ['install'],
);

await builder.run(input: input, output: output);

final assets = await output.findAndAddCodeAssets(
input,
outDir: installDir,
names: {r'(lib)?webcrypto\.(dll|dylib|so)': _assetName},
regExp: true,
);
if (assets.isEmpty) {
throw BuildError(
message:
'Failed to locate built webcrypto dynamic library in '
'${installDir.toFilePath()}',
);
}

output.dependencies.addAll(_buildDependencies(packageRoot));
}

final _buildDependencyExtensions = {
Expand Down
Loading
Loading