diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a786d23..e582f5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,36 +1,36 @@ -name: Cargo Build & Test - -on: - pull_request: - -env: - CARGO_TERM_COLOR: always - -jobs: - build_and_test: - name: Rust project - latest - strategy: - max-parallel: 1 - fail-fast: true - matrix: - include: - - os: ubuntu-latest - - os: windows-latest - - os: macos-latest - runs-on: ${{ matrix.os }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Toolchain install & update - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} - - name: Build GUI - run: cargo build --bin infuse_decoder --verbose - - name: Build CLI - run: cargo build --bin infuse_decoder_cli --verbose - - name: Run CLI - run: cargo run --bin infuse_decoder_cli -- --path ./data/tdf_test_blocks.bin --output . --name ci_test --verbose - - name: Display and cleanup results - shell: bash - run: | - ls -la *.csv - rm ./*.csv +name: Cargo Build & Test + +on: + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + build_and_test: + name: Rust project - latest + strategy: + max-parallel: 1 + fail-fast: true + matrix: + include: + - os: ubuntu-latest + - os: windows-latest + - os: macos-latest + runs-on: ${{ matrix.os }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Toolchain install & update + run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} + - name: Build GUI + run: cargo build --bin infuse_decoder --verbose + - name: Build CLI + run: cargo build --bin infuse_decoder_cli --verbose + - name: Run CLI + run: cargo run --bin infuse_decoder_cli -- --path ./data/tdf_test_blocks.bin --output . --name ci_test --verbose + - name: Display and cleanup results + shell: bash + run: | + ls -la *.csv + rm ./*.csv diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 30c8e05..e9ba7cb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,75 +1,198 @@ -name: Release - -permissions: - contents: write - -on: - pull_request: - types: [ labeled ] - push: - tags: - - v[0-9]+.* - -jobs: - create-release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - if: ${{ github.event_name == 'push' }} - - uses: taiki-e/create-gh-release-action@v1 - if: ${{ github.event_name == 'push' }} - with: - # (optional) Path to changelog. - changelog: CHANGELOG.md - # (required) GitHub token for creating GitHub Releases. - token: ${{ secrets.GITHUB_TOKEN }} - upload-assets: - if: ${{ (github.event_name == 'push') || (github.event.label.name == 'release') }} - needs: create-release - strategy: - matrix: - include: - - target: aarch64-unknown-linux-gnu - os: ubuntu-latest - - target: aarch64-apple-darwin - os: macos-latest - - target: x86_64-unknown-linux-gnu - os: ubuntu-latest - - target: x86_64-pc-windows-msvc - os: windows-latest - # Universal macOS binary is supported as universal-apple-darwin. - - target: x86_64-apple-darwin - os: macos-latest - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - name: MacOS Signing setup - if: runner.os == 'macOS' - shell: bash - run: | - echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12 - security create-keychain -p "" build.keychain - security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign - security list-keychains -s build.keychain - security default-keychain -s build.keychain - security unlock-keychain -p "" build.keychain - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" build.keychain - env: - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - - uses: taiki-e/upload-rust-binary-action@v1 - with: - # (required) Comma-separated list of binary names (non-extension portion of filename) to build and upload. - # Note that glob pattern is not supported yet. - bin: infuse_decoder,infuse_decoder_cli - # (optional) Archive name (non-extension portion of filename) to be uploaded. - archive: infuse_decoder-$tag-$target - # (optional) Target triple, default is host triple. - target: ${{ matrix.target }} - # (required) GitHub token for uploading assets to GitHub Releases. - token: ${{ secrets.GITHUB_TOKEN }} - # Dry-run unless a release - dry-run: ${{ github.event_name != 'push' }} - # MacOS codesign parameters - codesign: ${{ secrets.MACOS_SIGNING_IDENTITY }} - codesign-options: runtime +name: Release + +permissions: + contents: write + +on: + pull_request: + types: [ labeled ] + push: + tags: + - v[0-9]+.* + +jobs: + create-release: + runs-on: ubuntu-latest + steps: + - name: Skip release creation + if: ${{ github.event_name != 'push' }} + run: echo "Release creation only runs for tagged pushes." + - uses: actions/checkout@v4 + if: ${{ github.event_name == 'push' }} + - uses: taiki-e/create-gh-release-action@v1 + if: ${{ github.event_name == 'push' }} + with: + # (optional) Path to changelog. + changelog: CHANGELOG.md + # (required) GitHub token for creating GitHub Releases. + token: ${{ secrets.GITHUB_TOKEN }} + upload-assets: + if: ${{ (github.event_name == 'push') || (github.event.label.name == 'release') }} + needs: create-release + strategy: + matrix: + include: + - target: aarch64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-pc-windows-msvc + os: windows-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: taiki-e/upload-rust-binary-action@v1 + with: + # (required) Comma-separated list of binary names (non-extension portion of filename) to build and upload. + # Note that glob pattern is not supported yet. + bin: infuse_decoder,infuse_decoder_cli + # (optional) Archive name (non-extension portion of filename) to be uploaded. + archive: infuse_decoder-$tag-$target + # (optional) Target triple, default is host triple. + target: ${{ matrix.target }} + # (required) GitHub token for uploading assets to GitHub Releases. + token: ${{ secrets.GITHUB_TOKEN }} + # Dry-run unless a release + dry-run: ${{ github.event_name != 'push' }} + + upload-macos-assets: + if: ${{ (github.event_name == 'push') || (github.event.label.name == 'release') }} + needs: create-release + strategy: + matrix: + target: + - aarch64-apple-darwin + - x86_64-apple-darwin + runs-on: macos-latest + env: + MACOS_APP_NAME: Infuse Decoder + MACOS_BUNDLE_ID: iot.infuse.decoder + MACOS_REQUIRE_SIGNING: ${{ github.event_name == 'push' }} + MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }} + steps: + - uses: actions/checkout@v4 + + - name: Install Rust target + run: rustup target add ${{ matrix.target }} + + - name: macOS signing setup + shell: bash + env: + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + run: | + set -euo pipefail + + if [[ -z "${MACOS_CERTIFICATE:-}" || -z "${MACOS_CERTIFICATE_PASSWORD:-}" ]]; then + if [[ "$MACOS_REQUIRE_SIGNING" == "true" ]]; then + echo "MACOS_CERTIFICATE and MACOS_CERTIFICATE_PASSWORD are required for release signing" >&2 + exit 1 + fi + + echo "Skipping Developer ID certificate import for unsigned dry-run build." + exit 0 + fi + + certificate_path="$RUNNER_TEMP/developer_id_application.p12" + keychain_path="$RUNNER_TEMP/app-signing.keychain-db" + keychain_password="$(openssl rand -hex 24)" + + if ! printf '%s' "$MACOS_CERTIFICATE" | base64 --decode > "$certificate_path" 2>/dev/null; then + printf '%s' "$MACOS_CERTIFICATE" | base64 -D > "$certificate_path" + fi + + security create-keychain -p "$keychain_password" "$keychain_path" + security set-keychain-settings -lut 21600 "$keychain_path" + security unlock-keychain -p "$keychain_password" "$keychain_path" + security import "$certificate_path" \ + -k "$keychain_path" \ + -P "$MACOS_CERTIFICATE_PASSWORD" \ + -T /usr/bin/codesign + security list-keychains -d user -s "$keychain_path" + security default-keychain -s "$keychain_path" + security set-key-partition-list \ + -S apple-tool:,apple:,codesign: \ + -s \ + -k "$keychain_password" \ + "$keychain_path" + + - name: Build and sign macOS CLI + shell: bash + run: | + set -euo pipefail + + cargo build --release --bin infuse_decoder_cli --target "${{ matrix.target }}" + + if [[ -n "${MACOS_SIGNING_IDENTITY:-}" ]]; then + codesign --force \ + --timestamp \ + --options runtime \ + --sign "$MACOS_SIGNING_IDENTITY" \ + "target/${{ matrix.target }}/release/infuse_decoder_cli" + codesign --verify --strict --verbose=2 "target/${{ matrix.target }}/release/infuse_decoder_cli" + elif [[ "$MACOS_REQUIRE_SIGNING" == "true" ]]; then + echo "MACOS_SIGNING_IDENTITY is required for release signing" >&2 + exit 1 + else + codesign --force --sign - "target/${{ matrix.target }}/release/infuse_decoder_cli" + fi + + - name: Package macOS DMG + id: package + shell: bash + run: | + set -euo pipefail + + dmg_path="$(bash scripts/package_macos_dmg.sh "${{ matrix.target }}" | tail -n 1)" + version="$(sed -nE 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' Cargo.toml | head -n 1)" + cli_archive="target/macos-dmg/${{ matrix.target }}/infuse_decoder_cli-$version-${{ matrix.target }}.tar.gz" + + tar -czf "$cli_archive" -C "target/${{ matrix.target }}/release" infuse_decoder_cli + + echo "dmg_path=$dmg_path" >> "$GITHUB_OUTPUT" + echo "cli_archive=$cli_archive" >> "$GITHUB_OUTPUT" + + - name: Notarize macOS DMG + if: github.event_name == 'push' + shell: bash + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + DMG_PATH: ${{ steps.package.outputs.dmg_path }} + run: | + set -euo pipefail + + if [[ -z "${APPLE_ID:-}" || -z "${APPLE_TEAM_ID:-}" || -z "${APPLE_APP_SPECIFIC_PASSWORD:-}" ]]; then + echo "Skipping notarization; APPLE_ID, APPLE_TEAM_ID, and APPLE_APP_SPECIFIC_PASSWORD were not provided." + exit 0 + fi + + xcrun notarytool submit "$DMG_PATH" \ + --apple-id "$APPLE_ID" \ + --team-id "$APPLE_TEAM_ID" \ + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ + --wait + xcrun stapler staple "$DMG_PATH" + xcrun stapler validate "$DMG_PATH" + spctl -a -vvv -t open --context context:primary-signature "$DMG_PATH" + + - name: Upload macOS release assets + if: github.event_name == 'push' + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release upload "$GITHUB_REF_NAME" \ + "${{ steps.package.outputs.dmg_path }}" \ + "${{ steps.package.outputs.cli_archive }}" \ + --clobber + + - name: Upload macOS dry-run artifacts + if: github.event_name != 'push' + uses: actions/upload-artifact@v4 + with: + name: macos-${{ matrix.target }}-artifacts + path: | + ${{ steps.package.outputs.dmg_path }} + ${{ steps.package.outputs.cli_archive }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ea9f98..e148b35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org). +## [1.8.1] - 2026-06-05 + + - MacOS GUI distributed as notorized `.dmg` + ## [1.8.0] - 2026-05-01 - Fix decoding of constant length strings (Introduced in `1.7.0`) diff --git a/Cargo.toml b/Cargo.toml index af0e63f..498cffc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "infuse_decoder" -version = "1.8.0" +version = "1.8.1" edition = "2024" [[bin]] diff --git a/packaging/macos/entitlements.plist b/packaging/macos/entitlements.plist new file mode 100644 index 0000000..d238725 --- /dev/null +++ b/packaging/macos/entitlements.plist @@ -0,0 +1,6 @@ + + + + + diff --git a/scripts/package_macos_dmg.sh b/scripts/package_macos_dmg.sh new file mode 100755 index 0000000..5627664 --- /dev/null +++ b/scripts/package_macos_dmg.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " >&2 + exit 64 +fi + +TARGET="$1" +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +APP_NAME="${MACOS_APP_NAME:-Infuse Decoder}" +BUNDLE_ID="${MACOS_BUNDLE_ID:-iot.infuse.decoder}" +BIN_NAME="${MACOS_BIN_NAME:-infuse_decoder}" +EXECUTABLE_NAME="${MACOS_EXECUTABLE_NAME:-infuse_decoder}" +SIGNING_IDENTITY="${MACOS_SIGNING_IDENTITY:-${CODESIGN_IDENTITY:-}}" +REQUIRE_SIGNING="${MACOS_REQUIRE_SIGNING:-false}" +ENTITLEMENTS="${MACOS_ENTITLEMENTS:-$ROOT_DIR/packaging/macos/entitlements.plist}" +ICON_PNG="${MACOS_ICON_PNG:-$ROOT_DIR/assets/icon.png}" + +cd "$ROOT_DIR" + +VERSION="$(sed -nE 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' Cargo.toml | head -n 1)" +if [[ -z "$VERSION" ]]; then + echo "Could not determine package version from Cargo.toml" >&2 + exit 1 +fi + +if [[ "$REQUIRE_SIGNING" == "true" || "$REQUIRE_SIGNING" == "1" ]]; then + if [[ -z "$SIGNING_IDENTITY" ]]; then + echo "MACOS_SIGNING_IDENTITY is required when MACOS_REQUIRE_SIGNING is true" >&2 + exit 1 + fi +fi + +if [[ -n "$SIGNING_IDENTITY" && ! -f "$ENTITLEMENTS" ]]; then + echo "Entitlements file not found: $ENTITLEMENTS" >&2 + exit 1 +fi + +for required_tool in cargo codesign hdiutil iconutil sips; do + if ! command -v "$required_tool" >/dev/null 2>&1; then + echo "Required macOS packaging tool not found: $required_tool" >&2 + exit 1 + fi +done + +APP_BASENAME="${APP_NAME// /-}" +BUILD_ROOT="$ROOT_DIR/target/macos-dmg/$TARGET" +APP_PATH="$BUILD_ROOT/$APP_NAME.app" +DMG_ROOT="$BUILD_ROOT/dmg-root" +DMG_PATH="$BUILD_ROOT/$APP_BASENAME-$VERSION-$TARGET.dmg" +ICONSET="$BUILD_ROOT/AppIcon.iconset" + +cargo build --release --bin "$BIN_NAME" --target "$TARGET" + +rm -rf "$APP_PATH" "$DMG_ROOT" "$DMG_PATH" "$ICONSET" +mkdir -p "$APP_PATH/Contents/MacOS" "$APP_PATH/Contents/Resources" "$DMG_ROOT" "$ICONSET" + +cp "$ROOT_DIR/target/$TARGET/release/$BIN_NAME" "$APP_PATH/Contents/MacOS/$EXECUTABLE_NAME" +chmod 755 "$APP_PATH/Contents/MacOS/$EXECUTABLE_NAME" + +make_icon() { + local size="$1" + local output="$2" + + sips -z "$size" "$size" "$ICON_PNG" --out "$ICONSET/$output" >/dev/null +} + +make_icon 16 "icon_16x16.png" +make_icon 32 "icon_16x16@2x.png" +make_icon 32 "icon_32x32.png" +make_icon 64 "icon_32x32@2x.png" +make_icon 128 "icon_128x128.png" +make_icon 256 "icon_128x128@2x.png" +make_icon 256 "icon_256x256.png" +make_icon 512 "icon_256x256@2x.png" +make_icon 512 "icon_512x512.png" +make_icon 1024 "icon_512x512@2x.png" +iconutil -c icns "$ICONSET" -o "$APP_PATH/Contents/Resources/AppIcon.icns" + +cat > "$APP_PATH/Contents/Info.plist" < + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + $APP_NAME + CFBundleExecutable + $EXECUTABLE_NAME + CFBundleIconFile + AppIcon + CFBundleIdentifier + $BUNDLE_ID + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $APP_NAME + CFBundlePackageType + APPL + CFBundleShortVersionString + $VERSION + CFBundleVersion + $VERSION + LSApplicationCategoryType + public.app-category.utilities + NSHighResolutionCapable + + NSPrincipalClass + NSApplication + NSSupportsAutomaticGraphicsSwitching + + + +EOF + +printf 'APPL????' > "$APP_PATH/Contents/PkgInfo" + +if [[ -n "$SIGNING_IDENTITY" ]]; then + codesign --force \ + --timestamp \ + --options runtime \ + --entitlements "$ENTITLEMENTS" \ + --sign "$SIGNING_IDENTITY" \ + "$APP_PATH/Contents/MacOS/$EXECUTABLE_NAME" + + codesign --force \ + --timestamp \ + --options runtime \ + --entitlements "$ENTITLEMENTS" \ + --sign "$SIGNING_IDENTITY" \ + "$APP_PATH" +else + echo "MACOS_SIGNING_IDENTITY is not set; creating an ad-hoc signed DMG for local validation only." >&2 + codesign --force --sign - "$APP_PATH/Contents/MacOS/$EXECUTABLE_NAME" + codesign --force --sign - "$APP_PATH" +fi + +codesign --verify --strict --deep --verbose=2 "$APP_PATH" + +ditto "$APP_PATH" "$DMG_ROOT/$APP_NAME.app" +ln -s /Applications "$DMG_ROOT/Applications" + +hdiutil create \ + -volname "$APP_NAME" \ + -srcfolder "$DMG_ROOT" \ + -ov \ + -format UDZO \ + "$DMG_PATH" +hdiutil verify "$DMG_PATH" + +if [[ -n "$SIGNING_IDENTITY" ]]; then + codesign --force --timestamp --sign "$SIGNING_IDENTITY" "$DMG_PATH" + codesign --verify --verbose=2 "$DMG_PATH" +fi + +echo "$DMG_PATH"