Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verify binaries using hashes #813

Merged
merged 26 commits into from
Jul 30, 2024
Merged
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
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1 @@
pkgs/intl4x/lib/src/bindings linguist-generated=true
pkgs/intl4x/lib/src/bindings/* linguist-generated=true
116 changes: 114 additions & 2 deletions .github/workflows/intl4x.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ on:
- cron: '0 0 * * 0' # weekly

jobs:
build:
runs-on: ubuntu-latest
build_checkout:
runs-on: ${{ matrix.os }}

env:
ICU4X_BUILD_MODE: checkout
Expand All @@ -29,6 +29,7 @@ jobs:
strategy:
matrix:
sdk: [stable, dev] # {pkgs.versions}
os: [ubuntu-latest, windows-latest, macos-latest]
include:
- sdk: dev
run-tests: true
Expand All @@ -53,3 +54,114 @@ jobs:

- run: dart --enable-experiment=native-assets test -p chrome
if: ${{matrix.run-tests}}

build_fetch:
runs-on: ${{ matrix.os }}

env:
ICU4X_BUILD_MODE: fetch

strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]

defaults:
run:
working-directory: pkgs/intl4x

steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9
with:
submodules: true

- uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672
with:
sdk: dev

- run: dart --enable-experiment=native-assets pub get

- run: dart --enable-experiment=native-assets test

build_local:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
runs-on: ${{ matrix.os }}

env:
ICU4X_BUILD_MODE: local

steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9
with:
submodules: true

- name: Install Rust toolchains
run: |
rustup toolchain install stable

- name: Show the selected Rust toolchain
run: rustup show

- uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672
with:
sdk: dev

- name: Build Linux
if: matrix.os == 'ubuntu-latest'
run: |
cd submodules/icu4x

mkdir bin

cd ffi/dart
dart pub get
cd ../..
dart run ffi/dart/tool/build_libs.dart bin/linux_x64 linux_x64 dynamic default_components,experimental_components

- name: Build Mac
if: matrix.os == 'macos-latest'
run: |
cd submodules/icu4x

mkdir bin

cd ffi/dart
dart pub get
cd ../..
dart run ffi/dart/tool/build_libs.dart bin/macos_arm64 macos_arm64 dynamic default_components,experimental_components

- name: Build Windows
if: matrix.os == 'windows-latest'
run: |
cd submodules/icu4x

mkdir bin

cd ffi/dart
dart pub get
cd ../..
dart run ffi/dart/tool/build_libs.dart bin/windows_x64 windows_x64 dynamic default_components,experimental_components

- run: echo "LOCAL_ICU4X_BINARY=$(realpath submodules/icu4x/bin/linux_x64)" >> $GITHUB_ENV
if: matrix.os == 'ubuntu-latest'

- run: echo "LOCAL_ICU4X_BINARY=$(realpath submodules/icu4x/bin/macos_arm64)" >> $GITHUB_ENV
if: matrix.os == 'macos-latest'

- run: echo ("LOCAL_ICU4X_BINARY=" + (Get-Item submodules\icu4x\bin\windows_x64).FullName -replace '/', '\') >> $env:GITHUB_ENV
if: matrix.os == 'windows-latest'

- run: echo $LOCAL_ICU4X_BINARY

- name: Display structure of downloaded files
run: ls -R

- run: |
cd pkgs/intl4x
dart pub get

- run: |
cd pkgs/intl4x
dart --enable-experiment=native-assets test
37 changes: 36 additions & 1 deletion .github/workflows/intl4x_artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ permissions:
contents: write

on:
pull_request:
branches: [ main ]
paths:
- pkgs/intl4x/hook/hashes.dart
push:
tags:
- 'intl4x-icu*'
Expand Down Expand Up @@ -140,7 +144,38 @@ jobs:
with:
name: dart-${{matrix.os}}-libs
path: submodules/icu4x/bin


check_hashes:
needs: dart-libs
runs-on: ubuntu-latest

env:
ICU4X_BUILD_MODE: local

steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9
with:
submodules: true

- name: Download binaries
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e
with:
merge-multiple: true

- name: Display structure of downloaded files
run: ls -R

- uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672
with:
sdk: dev

- name: Check hashes of released artifacts
run: |
cd pkgs/intl4x
dart pub get
dart --enable-experiment=native-assets tool/generate_hashes.dart
git diff --exit-code

release:
needs: dart-libs
runs-on: ubuntu-latest
Expand Down
3 changes: 2 additions & 1 deletion pkgs/intl4x/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
## 0.9.2-wip

- Copy files instead of symlinking, for easier upgrading.
- Get binaries from Github and check their hashes.

## 0.9.1

- Small fixes in imports
- Small fixes in imports.

## 0.9.0

Expand Down
14 changes: 14 additions & 0 deletions pkgs/intl4x/hook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
### How to update the ICU4X version used in package:intl4x.

#### First PR
1. Create PR.
2. Update `submodules/icu4x` to whatever branch/hash you want.
3. Land PR.
4. Tag with `intl4x-icu*`, push. This creates new release with the new binaries.

#### Second PR
1. Create PR.
2. Run `bash tools/regenerate_bindings.sh`, and fix resulting errors.
3. Update `const version` in `hook/version.dart` to tag.
4. Regenerate hashes using `dart --enable-experiment=native-assets run tool/generate_hashes.dart`.
5. Land PR.
107 changes: 60 additions & 47 deletions pkgs/intl4x/hook/build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@

import 'dart:io';

import 'package:archive/archive_io.dart';
import 'package:crypto/crypto.dart' show sha256;
import 'package:native_assets_cli/native_assets_cli.dart';
import 'package:path/path.dart' as path;

import 'hashes.dart';
import 'version.dart';

const crateName = 'icu_capi';
const package = 'intl4x';
const assetId = 'src/bindings/lib.g.dart';

final env = 'ICU4X_BUILD_MODE';

void main(List<String> args) async {
await build(args, (config, output) async {
final buildMode = switch (Platform.environment['ICU4X_BUILD_MODE']) {
'local' => LocalMode(),
final environmentBuildMode = Platform.environment[env];
final buildMode = switch (environmentBuildMode) {
'local' => LocalMode(config),
'checkout' => CheckoutMode(config),
'fetch' || null => FetchMode(config),
String() => throw ArgumentError('''
Expand All @@ -30,6 +36,8 @@ Unknown build mode for icu4x. Set the `ICU4X_BUILD_MODE` environment variable wi
};

final builtLibrary = await buildMode.build();
// For debugging purposes
output.addMetadatum(env, environmentBuildMode ?? 'fetch');

output.addAsset(NativeCodeAsset(
package: package,
Expand All @@ -44,86 +52,91 @@ Unknown build mode for icu4x. Set the `ICU4X_BUILD_MODE` environment variable wi
[
...buildMode.dependencies,
config.packageRoot.resolve('hook/build.dart'),
//TODO: Fix this, currently causes a rebuild for checkout mode
//builtLibrary,
],
);
});
}

void unzipFirstFile({required File input, required File output}) {
final inputStream = InputFileStream(input.path);
final archive = ZipDecoder().decodeBuffer(inputStream);
final file = archive.files.firstOrNull;
// If it's a file and not a directory
if (file?.isFile ?? false) {
final outputStream = OutputFileStream(output.path);
file!.writeContent(outputStream);
outputStream.close();
}
}

sealed class BuildMode {
final BuildConfig config;

const BuildMode(this.config);

List<Uri> get dependencies;

Future<Uri> build();
}

final class FetchMode implements BuildMode {
final BuildConfig config;

FetchMode(this.config);
final class FetchMode extends BuildMode {
FetchMode(super.config);

@override
Future<Uri> build() async {
// TODO: Get a nicer CDN than a generated link to a privately owned repo.
final target = '${config.targetOS}_${config.targetArchitecture}';
final uri = Uri.parse(
'https://nightly.link/mosuem/i18n/workflows/intl4x_artifacts/main/lib-$platformName-latest.zip');
'https://github.com/dart-lang/i18n/releases/download/$version/$target');
final request = await HttpClient().getUrl(uri);
final response = await request.close();
if (response.statusCode != 200) {
throw ArgumentError('The request to $uri failed');
}
final zippedDynamicLibrary =
File(path.join(Directory.systemTemp.path, 'tmp.zip'));
zippedDynamicLibrary.createSync();
await response.pipe(zippedDynamicLibrary.openWrite());

final dynamicLibrary =
File.fromUri(config.outputDirectory.resolve('icu4xlib'));
final dynamicLibrary = File.fromUri(
config.outputDirectory.resolve(config.targetOS.dylibFileName('icu4x')));
await dynamicLibrary.create();
unzipFirstFile(input: zippedDynamicLibrary, output: dynamicLibrary);
return dynamicLibrary.uri;
}
await response.pipe(dynamicLibrary.openWrite());

String get platformName {
if (Platform.isMacOS) {
return 'macos';
} else if (Platform.isWindows) {
return 'windows';
final bytes = await dynamicLibrary.readAsBytes();
final fileHash = sha256.convert(bytes).toString();
final expectedFileHash = fileHashes[(
config.targetOS,
config.targetArchitecture,
)];
if (fileHash == expectedFileHash) {
return dynamicLibrary.uri;
} else {
return 'ubuntu';
throw Exception(
'The pre-built binary for the target $target at $uri has a hash of '
'$fileHash, which does not match $expectedFileHash fixed in the '
'build hook of package:intl4x.');
}
}

@override
List<Uri> get dependencies => [];
}

final class LocalMode implements BuildMode {
String get _localBinaryPath => Platform.environment['LOCAL_ICU4X_BINARY']!;
final class LocalMode extends BuildMode {
LocalMode(super.config);

String get _localBinaryPath {
final localPath = Platform.environment['LOCAL_ICU4X_BINARY'];
if (localPath != null) {
return localPath;
}
throw ArgumentError('`LOCAL_ICU4X_BINARY` is empty. '
'If the `ICU4X_BUILD_MODE` is set to `local`, the '
'`LOCAL_ICU4X_BINARY` environment variable must contain the path to '
'the binary.');
}

@override
Future<Uri> build() async => Uri.file(_localBinaryPath);
Future<Uri> build() async {
final dylibFileName = config.targetOS.dylibFileName('icu4x');
final dylibFileUri = config.outputDirectory.resolve(dylibFileName);
final file = File(_localBinaryPath);
if (!(await file.exists())) {
throw FileSystemException('Could not find binary.', _localBinaryPath);
}
await file.copy(dylibFileUri.toFilePath(windows: Platform.isWindows));
return dylibFileUri;
}

@override
List<Uri> get dependencies => [Uri.file(_localBinaryPath)];
}

final class CheckoutMode implements BuildMode {
final BuildConfig config;

CheckoutMode(this.config);
final class CheckoutMode extends BuildMode {
CheckoutMode(super.config);

String? get workingDirectory => Platform.environment['LOCAL_ICU4X_CHECKOUT'];

Expand Down Expand Up @@ -235,7 +248,7 @@ Future<Uri> buildLib(BuildConfig config, String workingDirectory) async {
if (!(await file.exists())) {
throw FileSystemException('Building the dylib failed', builtPath);
}
await file.copy(dylibFileUri.path);
await file.copy(dylibFileUri.toFilePath(windows: Platform.isWindows));
}
return dylibFileUri;
}
Expand Down
Loading
Loading