diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9bd6af7ba..69146e83b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,21 +1,281 @@ name: Publish to pub.dev on: + pull_request: + paths: + - '.github/workflows/publish.yml' + - 'doc/prebuilt-native-assets.md' + - 'hook/build.dart' 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=11.0 -DCMAKE_OSX_ARCHITECTURES=arm64' + - target: macos-x64 + os: macos-15-intel + library: libwebcrypto.dylib + configure_args: '-DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DCMAKE_OSX_ARCHITECTURES=x86_64' + - target: windows-x64 + os: windows-2022 + library: webcrypto.dll + configure_args: '-A x64' + + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - uses: ilammy/setup-nasm@72793074d3c8cdda771dba85f6deafe00623038b # v1.5.2 + if: runner.os == 'Windows' + + - name: Build Linux prebuilt asset + if: runner.os == 'Linux' + shell: bash + run: | + docker run --rm \ + --env HOST_GID="$(id -g)" \ + --env HOST_UID="$(id -u)" \ + --volume "$GITHUB_WORKSPACE:/workspace" \ + --workdir /workspace \ + ubuntu@sha256:8feb4d8ca5354def3d8fce243717141ce31e2c428701f6682bd2fafe15388214 \ + bash -euxo pipefail -c ' + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get install --no-install-recommends --yes \ + build-essential ca-certificates cmake ninja-build perl + cmake \ + -S src \ + -B build/prebuilt \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/workspace/build/prebuilt-install + cmake --build build/prebuilt --target install + mkdir -p artifact/prebuilt/linux-x64 + cp build/prebuilt-install/libwebcrypto.so \ + artifact/prebuilt/linux-x64/libwebcrypto.so + chown -R "$HOST_UID:$HOST_GID" build artifact + ' + + - name: Build prebuilt asset + if: runner.os != 'Linux' + 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.' + } + $versionInfo = (& readelf --version-info $library) -join "`n" + $glibcVersions = [regex]::Matches( + $versionInfo, + 'GLIBC_([0-9]+\.[0-9]+)' + ) | ForEach-Object { [version]$_.Groups[1].Value } + $maxGlibc = $glibcVersions | Sort-Object -Descending | Select-Object -First 1 + if ($null -eq $maxGlibc) { + throw 'Expected at least one GLIBC version requirement.' + } + if ($maxGlibc -gt [version]'2.31') { + throw "Expected GLIBC 2.31 or older, got $maxGlibc." + } + "Maximum required GLIBC version: $maxGlibc" + + - 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' } + $expectedMinimumVersion = if ('${{ matrix.target }}' -eq 'macos-arm64') { + [version]'11.0' + } else { + [version]'10.15' + } + if ($architecture -ne $expected) { + throw "Expected $expected, got $architecture." + } + $buildInfo = (& vtool -show-build $library) -join "`n" + $minimumVersion = [regex]::Match( + $buildInfo, + '(?m)^\s*minos\s+([0-9.]+)\s*$' + ) + if (-not $minimumVersion.Success) { + throw 'Failed to read the macOS deployment target.' + } + $minimumVersion = [version]$minimumVersion.Groups[1].Value + if ($minimumVersion -gt $expectedMinimumVersion) { + throw "Expected macOS $expectedMinimumVersion or older, got $minimumVersion." + } + "Minimum macOS version: $minimumVersion" + + - 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@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: webcrypto-* + path: . + merge-multiple: true + - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 # v1 + - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # 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: Test source-build override + shell: pwsh + run: | + $library = 'prebuilt/linux-x64/libwebcrypto.so' + $libraryBackup = Join-Path $env:RUNNER_TEMP 'libwebcrypto.so' + $pubspecBackup = Join-Path $env:RUNNER_TEMP 'pubspec.yaml' + Copy-Item -LiteralPath $library -Destination $libraryBackup + Copy-Item -LiteralPath 'pubspec.yaml' -Destination $pubspecBackup + try { + Set-Content -LiteralPath $library -Value 'invalid prebuilt' + Add-Content -LiteralPath 'pubspec.yaml' -Value @( + '', + 'hooks:', + ' user_defines:', + ' webcrypto:', + ' build_from_source: true' + ) + Remove-Item -Recurse -Force '.dart_tool/hooks_runner' -ErrorAction SilentlyContinue + flutter pub get + dart test -p vm test/aes_ctr_counter_wrap_test.dart + } finally { + Copy-Item -LiteralPath $libraryBackup -Destination $library -Force + Copy-Item -LiteralPath $pubspecBackup -Destination 'pubspec.yaml' -Force + } + - name: Validate and measure package contents + shell: pwsh + run: | + $archive = Join-Path $env:RUNNER_TEMP 'webcrypto.tar.gz' + flutter pub publish --to-archive $archive + $archiveSize = (Get-Item -LiteralPath $archive).Length + $prebuiltSize = ( + Get-ChildItem -Path prebuilt -Recurse -File | + Measure-Object -Property Length -Sum + ).Sum + $archiveMiB = [math]::Round($archiveSize / 1MB, 2) + $prebuiltMiB = [math]::Round($prebuiltSize / 1MB, 2) + "Compressed package archive: $archiveMiB MiB ($archiveSize bytes)" + "Uncompressed prebuilt matrix: $prebuiltMiB MiB ($prebuiltSize bytes)" + @" + ### Package size + + - Compressed package archive: $archiveMiB MiB ($archiveSize bytes) + - Uncompressed prebuilt matrix: $prebuiltMiB MiB ($prebuiltSize bytes) + "@ | Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY + 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: dart-lang/setup-dart@v1 - - uses: subosito/flutter-action@v2 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: webcrypto-* + path: . + merge-multiple: true + - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 # v1 + - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2 with: channel: 'stable' - run: flutter pub get diff --git a/doc/prebuilt-native-assets.md b/doc/prebuilt-native-assets.md new file mode 100644 index 000000000..976a84b07 --- /dev/null +++ b/doc/prebuilt-native-assets.md @@ -0,0 +1,47 @@ +# 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. + +The Linux artifact is built on Ubuntu 20.04 and must not require a glibc +version newer than 2.31. The macOS x64 artifact uses a 10.15 deployment target, +while the arm64 artifact uses 11.0, the first macOS release supporting Apple +Silicon. The publishing workflow verifies these compatibility baselines before +it assembles the package. + +## 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 fcbf862bb..10c8d28db 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 _buildFromSourceDefine = 'build_from_source'; 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 = input.prebuiltAsset; + + if (!input.userDefines.buildFromSource && prebuiltAsset.existsSync()) { + stdout.writeln( + 'webcrypto: using prebuilt native asset for ' + '${input.targetName}.', + ); + 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}.', + '${input.targetName}.', ); final builder = CMakeBuilder.create( - name: 'webcrypto', + name: _libraryName, sourceDir: sourceDir, defines: { 'CMAKE_BUILD_TYPE': 'Release', @@ -69,6 +89,29 @@ Future main(List args) async { }); } +extension on BuildInput { + File get prebuiltAsset { + final libraryFileName = config.code.targetOS.dylibFileName(_libraryName); + return File.fromUri( + packageRoot.resolve('prebuilt/$targetName/$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}'; + } +} + +extension on HookInputUserDefines { + bool get buildFromSource => this[_buildFromSourceDefine] == true; +} + final _buildDependencyExtensions = { '.S', '.asm', diff --git a/third_party/boringssl/sources.cmake b/third_party/boringssl/sources.cmake index 90437ae92..751cfa3da 100644 --- a/third_party/boringssl/sources.cmake +++ b/third_party/boringssl/sources.cmake @@ -321,6 +321,10 @@ set(crypto_sources_apple_x86_64 ${BORINGSSL_ROOT}gen/crypto/md5-586-apple.S ${BORINGSSL_ROOT}gen/crypto/md5-x86_64-apple.S ${BORINGSSL_ROOT}gen/test_support/trampoline-x86_64-apple.S + ${BORINGSSL_ROOT}third_party/fiat/asm/fiat_curve25519_adx_mul.S + ${BORINGSSL_ROOT}third_party/fiat/asm/fiat_curve25519_adx_square.S + ${BORINGSSL_ROOT}third_party/fiat/asm/fiat_p256_adx_mul.S + ${BORINGSSL_ROOT}third_party/fiat/asm/fiat_p256_adx_sqr.S ) set(crypto_sources_linux_aarch64 diff --git a/tool/bump-boringssl-revision.sh b/tool/bump-boringssl-revision.sh index 32de27be2..67ca5fc28 100755 --- a/tool/bump-boringssl-revision.sh +++ b/tool/bump-boringssl-revision.sh @@ -208,7 +208,7 @@ def classify_asm(path): if normalized == "crypto/hrss/asm/poly_rq_mul.S": return "linux_x86_64" if normalized.startswith("third_party/fiat/asm/"): - return "linux_x86_64" + return ("apple_x86_64", "linux_x86_64") return None @@ -238,7 +238,9 @@ for file in ( ): key = classify_asm(file) if key is not None: - asm_outputs[key].append(file) + keys = key if isinstance(key, tuple) else (key,) + for output_key in keys: + asm_outputs[output_key].append(file) payload = { "crypto_sources": sorted(bcm.get("srcs", []) + crypto.get("srcs", [])),