diff --git a/.github/workflows/build-prebuilt-assets.yml b/.github/workflows/build-prebuilt-assets.yml new file mode 100644 index 000000000..79c925654 --- /dev/null +++ b/.github/workflows/build-prebuilt-assets.yml @@ -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 }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 74074b622..04d8cae92 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/hook/build.dart b/hook/build.dart index fcbf862bb..26ba2ce95 100644 --- a/hook/build.dart +++ b/hook/build.dart @@ -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 main(List args) async { await build(args, (input, output) async { @@ -31,12 +33,43 @@ Future main(List 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( @@ -69,6 +102,38 @@ Future main(List 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 _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', diff --git a/prebuilt/README.md b/prebuilt/README.md new file mode 100644 index 000000000..861b7b932 --- /dev/null +++ b/prebuilt/README.md @@ -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. + +Expected layout: + +- `prebuilt///` + +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