From acb20d2b14398f3e33b40a2866c03ebc940131c1 Mon Sep 17 00:00:00 2001 From: HamdaanAliQuatil Date: Sun, 12 Jul 2026 15:00:53 +0530 Subject: [PATCH 1/2] feat: add prebuilt native asset infrastructure --- .github/workflows/build-prebuilt-assets.yml | 58 ++++++++++++++++++++ hook/build.dart | 49 ++++++++++++++++- prebuilt/README.md | 59 +++++++++++++++++++++ 3 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/build-prebuilt-assets.yml create mode 100644 prebuilt/README.md diff --git a/.github/workflows/build-prebuilt-assets.yml b/.github/workflows/build-prebuilt-assets.yml new file mode 100644 index 000000000..b5130562f --- /dev/null +++ b/.github/workflows/build-prebuilt-assets.yml @@ -0,0 +1,58 @@ +name: build-prebuilt-assets + +on: + workflow_dispatch: + +env: + PUB_ENVIRONMENT: bot.github + +jobs: + build: + name: ${{ matrix.target }} + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - target: linux-x64 + os: ubuntu-latest + configure_args: '' + - target: macos-arm64 + os: macos-14 + configure_args: '' + - target: macos-x64 + os: macos-13 + configure_args: '' + - target: windows-x64 + os: windows-latest + configure_args: '-A x64' + + steps: + - uses: actions/checkout@v4 + + - uses: ilammy/setup-nasm@v1 + if: runner.os == 'Windows' + + - name: Build prebuilt asset + shell: pwsh + run: | + $install = Join-Path $env:GITHUB_WORKSPACE "prebuilt/${{ matrix.target }}" + $configureArgs = @( + '-S', 'src', + '-B', 'build/prebuilt', + '-DCMAKE_BUILD_TYPE=Release', + "-DCMAKE_INSTALL_PREFIX=$install" + ) + if ('${{ matrix.configure_args }}' -ne '') { + $configureArgs += '${{ matrix.configure_args }}'.Split(' ') + } + cmake @configureArgs + cmake --build build/prebuilt --config Release --target install + + - name: Upload prebuilt asset + uses: actions/upload-artifact@v4 + with: + name: webcrypto-${{ matrix.target }} + path: prebuilt/${{ matrix.target }}/* + if-no-files-found: error diff --git a/hook/build.dart b/hook/build.dart index fcbf862bb..f9df7fea6 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 _libraryName = 'webcrypto'; +const _forceSourceBuildDefine = 'force_source_build'; Future main(List args) async { await build(args, (input, output) async { @@ -33,14 +35,32 @@ Future main(List args) async { final packageRoot = input.packageRoot; final installDir = input.outputDirectory.resolve('install/'); final sourceDir = packageRoot.resolve('src/'); + final prebuiltAsset = _prebuiltAsset(input); + + if (!_forceSourceBuild(input) && prebuiltAsset.existsSync()) { + stdout.writeln( + 'webcrypto: using prebuilt native asset for ' + '${_targetName(input)}.', + ); + output.assets.code.add( + CodeAsset( + package: input.packageName, + name: _assetName, + linkMode: DynamicLoadingBundled(), + file: prebuiltAsset.uri, + ), + ); + output.dependencies.add(prebuiltAsset.uri); + return; + } stdout.writeln( 'webcrypto: building native asset for ' - '${input.config.code.targetOS}-${input.config.code.targetArchitecture}.', + '${_targetName(input)}.', ); final builder = CMakeBuilder.create( - name: 'webcrypto', + name: _libraryName, sourceDir: sourceDir, defines: { 'CMAKE_BUILD_TYPE': 'Release', @@ -69,6 +89,31 @@ Future main(List args) async { }); } +bool _forceSourceBuild(BuildInput input) { + final value = input.userDefines[_forceSourceBuildDefine]; + return value == true || value == 'true'; +} + +File _prebuiltAsset(BuildInput input) { + final targetOS = input.config.code.targetOS; + final libraryFileName = targetOS.dylibFileName(_libraryName); + return File.fromUri( + input.packageRoot.resolve( + 'prebuilt/${_targetName(input)}/$libraryFileName', + ), + ); +} + +String _targetName(BuildInput input) { + final code = input.config.code; + final os = code.targetOS; + final arch = code.targetArchitecture; + if (os == OS.iOS) { + return '${os.name}-${code.iOS.targetSdk.type}-${arch.name}'; + } + return '${os.name}-${arch.name}'; +} + final _buildDependencyExtensions = { '.S', '.asm', diff --git a/prebuilt/README.md b/prebuilt/README.md new file mode 100644 index 000000000..677242d81 --- /dev/null +++ b/prebuilt/README.md @@ -0,0 +1,59 @@ +# Prebuilt Native Assets + +This directory is reserved for trusted prebuilt `webcrypto` native libraries. + +The build hook looks for a target-specific dynamic library before falling back +to a local CMake source build. + +## Layout + +Each target gets one directory: + +```text +prebuilt/ + linux-x64/ + libwebcrypto.so + macos-arm64/ + libwebcrypto.dylib + macos-x64/ + libwebcrypto.dylib + windows-x64/ + webcrypto.dll +``` + +iOS uses the SDK name to avoid mixing device and simulator artifacts: + +```text +prebuilt/ + ios-iphoneos-arm64/ + libwebcrypto.dylib + ios-iphonesimulator-arm64/ + libwebcrypto.dylib +``` + +Android uses the target architecture name: + +```text +prebuilt/ + android-arm64/ + libwebcrypto.so + android-x64/ + libwebcrypto.so +``` + +## Source-Build Override + +Consumers and CI can force a source build even when a matching prebuilt exists: + +```yaml +hooks: + user_defines: + webcrypto: + force_source_build: true +``` + +## Policy + +Do not accept arbitrary binary blobs from contributor PRs. Prebuilt libraries +should be generated by trusted project automation or another reviewable release +process. From bf79e79a49e88e8d80c481313b7111d8ff31bbfc Mon Sep 17 00:00:00 2001 From: HamdaanAliQuatil Date: Tue, 14 Jul 2026 20:35:04 +0530 Subject: [PATCH 2/2] refactor: integrate prebuilt asset publishing --- .github/workflows/build-prebuilt-assets.yml | 58 -------- .github/workflows/publish.yml | 155 +++++++++++++++++++- doc/prebuilt-native-assets.md | 41 ++++++ hook/build.dart | 48 +++--- prebuilt/README.md | 59 -------- 5 files changed, 218 insertions(+), 143 deletions(-) delete mode 100644 .github/workflows/build-prebuilt-assets.yml create mode 100644 doc/prebuilt-native-assets.md delete mode 100644 prebuilt/README.md diff --git a/.github/workflows/build-prebuilt-assets.yml b/.github/workflows/build-prebuilt-assets.yml deleted file mode 100644 index b5130562f..000000000 --- a/.github/workflows/build-prebuilt-assets.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: build-prebuilt-assets - -on: - workflow_dispatch: - -env: - PUB_ENVIRONMENT: bot.github - -jobs: - build: - name: ${{ matrix.target }} - runs-on: ${{ matrix.os }} - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - include: - - target: linux-x64 - os: ubuntu-latest - configure_args: '' - - target: macos-arm64 - os: macos-14 - configure_args: '' - - target: macos-x64 - os: macos-13 - configure_args: '' - - target: windows-x64 - os: windows-latest - configure_args: '-A x64' - - steps: - - uses: actions/checkout@v4 - - - uses: ilammy/setup-nasm@v1 - if: runner.os == 'Windows' - - - name: Build prebuilt asset - shell: pwsh - run: | - $install = Join-Path $env:GITHUB_WORKSPACE "prebuilt/${{ matrix.target }}" - $configureArgs = @( - '-S', 'src', - '-B', 'build/prebuilt', - '-DCMAKE_BUILD_TYPE=Release', - "-DCMAKE_INSTALL_PREFIX=$install" - ) - if ('${{ matrix.configure_args }}' -ne '') { - $configureArgs += '${{ matrix.configure_args }}'.Split(' ') - } - cmake @configureArgs - cmake --build build/prebuilt --config Release --target install - - - name: Upload prebuilt asset - uses: actions/upload-artifact@v4 - with: - name: webcrypto-${{ matrix.target }} - path: prebuilt/${{ matrix.target }}/* - if-no-files-found: error diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9bd6af7ba..e65a0a3bc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,16 +4,169 @@ on: push: tags: - '[0-9]+.[0-9]+.[0-9]+*' + workflow_dispatch: + +permissions: + contents: read jobs: + build-prebuilt: + name: Build ${{ matrix.target }} + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - target: linux-x64 + os: ubuntu-22.04 + library: libwebcrypto.so + configure_args: '' + - target: macos-arm64 + os: macos-15 + library: libwebcrypto.dylib + configure_args: '-DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 -DCMAKE_OSX_ARCHITECTURES=arm64' + - target: macos-x64 + os: macos-15-intel + library: libwebcrypto.dylib + configure_args: '-DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 -DCMAKE_OSX_ARCHITECTURES=x86_64' + - target: windows-x64 + os: windows-2022 + library: webcrypto.dll + configure_args: '-A x64' + + steps: + - uses: actions/checkout@v4 + + - uses: ilammy/setup-nasm@72793074d3c8cdda771dba85f6deafe00623038b # v1.5.2 + if: runner.os == 'Windows' + + - name: Build prebuilt asset + shell: pwsh + run: | + $install = Join-Path $env:GITHUB_WORKSPACE 'build/prebuilt-install' + $artifact = Join-Path $env:GITHUB_WORKSPACE 'artifact/prebuilt/${{ matrix.target }}' + $configureArgs = @( + '-S', 'src', + '-B', 'build/prebuilt', + '-DCMAKE_BUILD_TYPE=Release', + "-DCMAKE_INSTALL_PREFIX=$install" + ) + if ('${{ matrix.configure_args }}' -ne '') { + $configureArgs += '${{ matrix.configure_args }}'.Split(' ') + } + + cmake @configureArgs + cmake --build build/prebuilt --config Release --target install + + New-Item -ItemType Directory -Force -Path $artifact | Out-Null + Copy-Item -LiteralPath (Join-Path $install '${{ matrix.library }}') -Destination $artifact + + - name: Verify Linux architecture + if: runner.os == 'Linux' + shell: pwsh + run: | + $library = 'artifact/prebuilt/${{ matrix.target }}/${{ matrix.library }}' + $description = & file $library + $description + if ($description -notmatch 'x86-64') { + throw 'Expected an x86-64 ELF library.' + } + + - name: Verify macOS architecture + if: runner.os == 'macOS' + shell: pwsh + run: | + $library = 'artifact/prebuilt/${{ matrix.target }}/${{ matrix.library }}' + $architecture = (& lipo -archs $library).Trim() + $expected = if ('${{ matrix.target }}' -eq 'macos-arm64') { 'arm64' } else { 'x86_64' } + if ($architecture -ne $expected) { + throw "Expected $expected, got $architecture." + } + + - name: Verify Windows architecture + if: runner.os == 'Windows' + shell: pwsh + run: | + $library = 'artifact/prebuilt/${{ matrix.target }}/${{ matrix.library }}' + $stream = [System.IO.File]::OpenRead($library) + $reader = [System.IO.BinaryReader]::new($stream) + try { + $stream.Position = 0x3c + $peHeaderOffset = $reader.ReadInt32() + $stream.Position = $peHeaderOffset + if ($reader.ReadUInt32() -ne 0x00004550) { + throw 'Expected a PE library.' + } + $machine = $reader.ReadUInt16() + if ($machine -ne 0x8664) { + throw ('Expected an x64 PE library, got machine type 0x{0:x4}.' -f $machine) + } + } finally { + $reader.Dispose() + $stream.Dispose() + } + + - name: Upload prebuilt asset + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: webcrypto-${{ matrix.target }} + path: artifact/ + if-no-files-found: error + + validate-prebuilt: + name: Validate prebuilt package + needs: build-prebuilt + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: webcrypto-* + path: . + merge-multiple: true + - uses: dart-lang/setup-dart@v1 + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + - name: Verify prebuilt layout + shell: pwsh + run: | + @( + 'prebuilt/linux-x64/libwebcrypto.so', + 'prebuilt/macos-arm64/libwebcrypto.dylib', + 'prebuilt/macos-x64/libwebcrypto.dylib', + 'prebuilt/windows-x64/webcrypto.dll' + ) | ForEach-Object { + if (-not (Test-Path -LiteralPath $_ -PathType Leaf)) { + throw "Missing prebuilt asset: $_" + } + } + - run: flutter pub get + - name: Test prebuilt asset + run: dart test -p vm test/aes_ctr_counter_wrap_test.dart + - name: Validate package contents + run: flutter pub publish --dry-run + publish: name: Publish to pub.dev - runs-on: ubuntu-latest + needs: + - build-prebuilt + - validate-prebuilt + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-24.04 environment: pub.dev permissions: + contents: read id-token: write steps: - uses: actions/checkout@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: webcrypto-* + path: . + merge-multiple: true - uses: dart-lang/setup-dart@v1 - uses: subosito/flutter-action@v2 with: diff --git a/doc/prebuilt-native-assets.md b/doc/prebuilt-native-assets.md new file mode 100644 index 000000000..e78167b4d --- /dev/null +++ b/doc/prebuilt-native-assets.md @@ -0,0 +1,41 @@ +# Prebuilt Native Assets + +Published versions of `package:webcrypto` include trusted prebuilt native +libraries for the initial desktop target matrix: + +```text +prebuilt/ + linux-x64/ + libwebcrypto.so + macos-arm64/ + libwebcrypto.dylib + macos-x64/ + libwebcrypto.dylib + windows-x64/ + webcrypto.dll +``` + +The build hook uses a matching prebuilt library when one is available. Targets +without a packaged prebuilt continue to build the native library from source +with CMake. + +## Building From Source + +Consumers and CI can bypass a packaged prebuilt explicitly: + +```yaml +hooks: + user_defines: + webcrypto: + build_from_source: true +``` + +## Publishing + +The prebuilt libraries are not committed to the repository. The +`.github/workflows/publish.yml` workflow builds them from the tagged source, +validates the assembled package, and includes them in the package published to +pub.dev. + +Manually dispatching the workflow builds and validates the same artifacts but +does not publish a package. diff --git a/hook/build.dart b/hook/build.dart index f9df7fea6..10c8d28db 100644 --- a/hook/build.dart +++ b/hook/build.dart @@ -20,7 +20,7 @@ import 'package:native_toolchain_cmake/native_toolchain_cmake.dart'; const _assetName = 'webcrypto.dart'; const _libraryName = 'webcrypto'; -const _forceSourceBuildDefine = 'force_source_build'; +const _buildFromSourceDefine = 'build_from_source'; Future main(List args) async { await build(args, (input, output) async { @@ -35,12 +35,12 @@ Future main(List args) async { final packageRoot = input.packageRoot; final installDir = input.outputDirectory.resolve('install/'); final sourceDir = packageRoot.resolve('src/'); - final prebuiltAsset = _prebuiltAsset(input); + final prebuiltAsset = input.prebuiltAsset; - if (!_forceSourceBuild(input) && prebuiltAsset.existsSync()) { + if (!input.userDefines.buildFromSource && prebuiltAsset.existsSync()) { stdout.writeln( 'webcrypto: using prebuilt native asset for ' - '${_targetName(input)}.', + '${input.targetName}.', ); output.assets.code.add( CodeAsset( @@ -56,7 +56,7 @@ Future main(List args) async { stdout.writeln( 'webcrypto: building native asset for ' - '${_targetName(input)}.', + '${input.targetName}.', ); final builder = CMakeBuilder.create( @@ -89,29 +89,27 @@ Future main(List args) async { }); } -bool _forceSourceBuild(BuildInput input) { - final value = input.userDefines[_forceSourceBuildDefine]; - return value == true || value == 'true'; -} +extension on BuildInput { + File get prebuiltAsset { + final libraryFileName = config.code.targetOS.dylibFileName(_libraryName); + return File.fromUri( + packageRoot.resolve('prebuilt/$targetName/$libraryFileName'), + ); + } -File _prebuiltAsset(BuildInput input) { - final targetOS = input.config.code.targetOS; - final libraryFileName = targetOS.dylibFileName(_libraryName); - return File.fromUri( - input.packageRoot.resolve( - 'prebuilt/${_targetName(input)}/$libraryFileName', - ), - ); + String get targetName { + final code = config.code; + final os = code.targetOS; + final arch = code.targetArchitecture; + if (os == OS.iOS) { + return '${os.name}-${code.iOS.targetSdk.type}-${arch.name}'; + } + return '${os.name}-${arch.name}'; + } } -String _targetName(BuildInput input) { - final code = input.config.code; - final os = code.targetOS; - final arch = code.targetArchitecture; - if (os == OS.iOS) { - return '${os.name}-${code.iOS.targetSdk.type}-${arch.name}'; - } - return '${os.name}-${arch.name}'; +extension on HookInputUserDefines { + bool get buildFromSource => this[_buildFromSourceDefine] == true; } final _buildDependencyExtensions = { diff --git a/prebuilt/README.md b/prebuilt/README.md deleted file mode 100644 index 677242d81..000000000 --- a/prebuilt/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# Prebuilt Native Assets - -This directory is reserved for trusted prebuilt `webcrypto` native libraries. - -The build hook looks for a target-specific dynamic library before falling back -to a local CMake source build. - -## Layout - -Each target gets one directory: - -```text -prebuilt/ - linux-x64/ - libwebcrypto.so - macos-arm64/ - libwebcrypto.dylib - macos-x64/ - libwebcrypto.dylib - windows-x64/ - webcrypto.dll -``` - -iOS uses the SDK name to avoid mixing device and simulator artifacts: - -```text -prebuilt/ - ios-iphoneos-arm64/ - libwebcrypto.dylib - ios-iphonesimulator-arm64/ - libwebcrypto.dylib -``` - -Android uses the target architecture name: - -```text -prebuilt/ - android-arm64/ - libwebcrypto.so - android-x64/ - libwebcrypto.so -``` - -## Source-Build Override - -Consumers and CI can force a source build even when a matching prebuilt exists: - -```yaml -hooks: - user_defines: - webcrypto: - force_source_build: true -``` - -## Policy - -Do not accept arbitrary binary blobs from contributor PRs. Prebuilt libraries -should be generated by trusted project automation or another reviewable release -process.