diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..aab61867 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + release: + types: [ published ] + workflow_dispatch: + +defaults: + run: + shell: bash + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, macos-15 ] + steps: + - uses: actions/checkout@v4 + - if: github.event_name != 'release' && github.event_name != 'workflow_dispatch' + uses: Swatinem/rust-cache@v2 + - uses: dtolnay/rust-toolchain@stable + - uses: taiki-e/install-action@v2 + with: { tool: just } + - run: just ci-test + + test-msrv: + name: Test MSRV + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, macos-15 ] + steps: + - uses: actions/checkout@v4 + - if: github.event_name != 'release' && github.event_name != 'workflow_dispatch' + uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@v2 + with: { tool: just } + - name: Read MSRV + id: msrv + run: echo "value=$(just get-msrv)" >> $GITHUB_OUTPUT + - name: Install MSRV Rust ${{ steps.msrv.outputs.value }} + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ steps.msrv.outputs.value }} + - run: just ci_mode=0 ci-test-msrv # Ignore warnings in MSRV + + # This job checks if any of the previous jobs failed or were canceled. + # This approach also allows some jobs to be skipped if they are not needed. + ci-passed: + needs: [ test, test-msrv ] + if: always() + runs-on: ubuntu-latest + steps: + - if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} + run: exit 1 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 7a3c5ff1..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Rust - -on: - push: - branches: [main] - tags: ["*.*.*"] - pull_request: - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-15] - rust: [stable] - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Build without std - run: cargo build --no-default-features - - - name: Build with std - run: cargo build - - - name: MSRV Build (Rust 1.80) - run: cargo +1.80 build - - - name: Run tests - run: cargo test --verbose - - - name: Rustfmt - run: cargo fmt --all -- --check - - - name: Clippy - run: cargo clippy --all-features --all-targets -- -D warnings diff --git a/README.md b/README.md index 643bde55..f9f0dca6 100644 --- a/README.md +++ b/README.md @@ -59,16 +59,17 @@ The library is completely safe. There are no `unsafe` in this library and in most of its dependencies (excluding `bytemuck`). -## Developer documents - -For notes on the backporting process of HarfBuzz code, see [docs/backporting.md](docs/backporting.md). - -For notes on generating state machine using `ragel`, see [docs/ragel.md](docs/ragel.md). - -The following HarfBuzz _studies_ are relevant to HarfRust development: - -- 2025 - [Introducing HarfRust][2] -- 2025 – [Caching][1] +## Development + +* This project is easier to develop with [just](https://github.com/casey/just#readme), a modern alternative to `make`. + Install it with `cargo install just`. + * To get a list of available commands, run `just`. + * To run tests, use `just test`. +* For notes on the backporting process of HarfBuzz code, see [docs/backporting.md](docs/backporting.md). +* For notes on generating state machine using `ragel`, see [docs/ragel.md](docs/ragel.md). +* The following HarfBuzz _studies_ are relevant to HarfRust development: + - 2025 - [Introducing HarfRust][2] + - 2025 – [Caching][1] ## License diff --git a/justfile b/justfile new file mode 100755 index 00000000..09dd7d0b --- /dev/null +++ b/justfile @@ -0,0 +1,150 @@ +#!/usr/bin/env just --justfile + +main_crate := 'harfrust' +features_flag := '--all-features' + +# if running in CI, treat warnings as errors by setting RUSTFLAGS and RUSTDOCFLAGS to '-D warnings' unless they are already set +# Use `CI=true just ci-test` to run the same tests as in GitHub CI. +# Use `just env-info` to see the current values of RUSTFLAGS and RUSTDOCFLAGS +ci_mode := if env('CI', '') != '' {'1'} else {''} +export RUSTFLAGS := env('RUSTFLAGS', if ci_mode == '1' {'-D warnings'} else {''}) +export RUSTDOCFLAGS := env('RUSTDOCFLAGS', if ci_mode == '1' {'-D warnings'} else {''}) +export RUST_BACKTRACE := env('RUST_BACKTRACE', if ci_mode == '1' {'1'} else {''}) + +@_default: + {{just_executable()}} --list + +# Build the project +build: build-lib build-lib-no-std + cargo build --workspace --all-targets {{features_flag}} + +# Build just the core lib +build-lib: + cargo build --lib --package {{main_crate}} {{features_flag}} + +# Build just the core lib with no_std +build-lib-no-std: + cargo build --lib --package {{main_crate}} --no-default-features + +# Quick compile without building a binary +check: + cargo check --workspace --all-targets {{features_flag}} + cargo check --lib --package {{main_crate}} {{features_flag}} + cargo check --lib --package {{main_crate}} --no-default-features + +# Generate code coverage report to upload to codecov.io +ci-coverage: env-info && \ + (coverage '--codecov --output-path target/llvm-cov/codecov.info') + # ATTENTION: the full file path above is used in the CI workflow + mkdir -p target/llvm-cov + +# Run all tests as expected by CI +ci-test: env-info test-fmt build test test-doc clippy && assert-git-is-clean + +# Run minimal subset of tests to ensure compatibility with MSRV +ci-test-msrv: env-info build-lib build-lib-no-std + +# Clean all build artifacts +clean: + cargo clean + rm -f Cargo.lock + +# Run cargo clippy to lint the code +clippy *args: + cargo clippy --workspace --all-targets {{features_flag}} {{args}} + +# Generate code coverage report. Will install `cargo llvm-cov` if missing. +coverage *args='--no-clean --open': (cargo-install 'cargo-llvm-cov') + cargo llvm-cov --workspace --all-targets {{features_flag}} --include-build-script {{args}} + +# Build and open code documentation +docs *args='--open': + DOCS_RS=1 cargo doc --no-deps {{args}} --workspace {{features_flag}} + +# Print environment info +env-info: + @echo "Running {{if ci_mode == '1' {'in CI mode'} else {'in dev mode'} }} on {{os()}} / {{arch()}}" + {{just_executable()}} --version + rustc --version + cargo --version + rustup --version + @echo "RUSTFLAGS='$RUSTFLAGS'" + @echo "RUSTDOCFLAGS='$RUSTDOCFLAGS'" + @echo "RUST_BACKTRACE='$RUST_BACKTRACE'" + +# Reformat all code `cargo fmt`. +fmt: + cargo fmt --all + +# Get any package's field from the metadata +get-crate-field field package=main_crate: (assert-cmd 'jq') + cargo metadata --format-version 1 | jq -e -r '.packages | map(select(.name == "{{package}}")) | first | .{{field}} | select(. != null)' + +# Get the minimum supported Rust version (MSRV) for the crate +get-msrv package=main_crate: (get-crate-field 'rust_version' package) + +# Find the minimum supported Rust version (MSRV) using cargo-msrv extension, and update Cargo.toml +msrv: (cargo-install 'cargo-msrv') + cargo msrv find --write-msrv --ignore-lockfile {{features_flag}} + +release *args='': (cargo-install 'release-plz') + release-plz {{args}} + +# Check semver compatibility with prior published version. Install it with `cargo install cargo-semver-checks` +semver *args: (cargo-install 'cargo-semver-checks') + cargo semver-checks {{features_flag}} {{args}} + +# Run all unit and integration tests +test: + cargo test --workspace --all-targets {{features_flag}} + cargo test --workspace --doc {{features_flag}} + +# Test documentation generation +test-doc: (docs '') + +# Test code formatting +test-fmt: + cargo fmt --all -- --check + +# Find unused dependencies. Install it with `cargo install cargo-udeps` +udeps: (cargo-install 'cargo-udeps') + cargo +nightly udeps --workspace --all-targets {{features_flag}} + +# Update all dependencies, including breaking changes. Requires nightly toolchain (install with `rustup install nightly`) +update: + cargo +nightly -Z unstable-options update --breaking + cargo update + +# Ensure that a certain command is available +[private] +assert-cmd command: + @if ! type {{command}} > /dev/null; then \ + echo "Command '{{command}}' could not be found. Please make sure it has been installed on your computer." ;\ + exit 1 ;\ + fi + +# Make sure the git repo has no uncommitted changes +[private] +assert-git-is-clean: + @if [ -n "$(git status --untracked-files --porcelain)" ]; then \ + >&2 echo "ERROR: git repo is no longer clean. Make sure compilation and tests artifacts are in the .gitignore, and no repo files are modified." ;\ + >&2 echo "######### git status ##########" ;\ + git status ;\ + git --no-pager diff ;\ + exit 1 ;\ + fi + +# Check if a certain Cargo command is installed, and install it if needed +[private] +cargo-install $COMMAND $INSTALL_CMD='' *args='': + #!/usr/bin/env bash + set -euo pipefail + if ! command -v $COMMAND > /dev/null; then + if ! command -v cargo-binstall > /dev/null; then + echo "$COMMAND could not be found. Installing it with cargo install ${INSTALL_CMD:-$COMMAND} --locked {{args}}" + cargo install ${INSTALL_CMD:-$COMMAND} --locked {{args}} + else + echo "$COMMAND could not be found. Installing it with cargo binstall ${INSTALL_CMD:-$COMMAND} --locked {{args}}" + cargo binstall ${INSTALL_CMD:-$COMMAND} --locked {{args}} + fi + fi