diff --git a/.github/workflows/README_linux_packages.md b/.github/workflows/README_linux_packages.md new file mode 100644 index 000000000..3573dd7fe --- /dev/null +++ b/.github/workflows/README_linux_packages.md @@ -0,0 +1,379 @@ +# Linux Package Workflows + +This document describes the automated Linux packaging workflows for Hyprnote. + +## Workflows Overview + +Hyprnote provides automated packaging for multiple Linux distributions: + +- **`linux_packages.yaml`**: .deb (Debian/Ubuntu) and .AppImage (universal) +- **`linux_packages_rpm.yaml`**: .rpm (Fedora/RHEL/openSUSE) +- **`linux_packages_arch.yaml`**: .pkg.tar.zst (Arch Linux) + +All workflows support both **x86_64** and **aarch64** (ARM64) architectures. + +## Workflow Details + +### `linux_packages.yaml` - Debian & AppImage + +Builds and publishes .deb packages and AppImage for Debian-based distributions. + +#### Trigger Conditions + +- **Manual dispatch**: Via GitHub Actions UI (workflow_dispatch) + - Choose channel: `stable` or `nightly` (default) +- **Automatic**: On release publish (when tag starts with `desktop_`) + +#### Jobs + +##### 1. `build-linux-packages` - Debian Package + +**Runner**: `ubuntu-24.04` + +**Builds**: `.deb` package for Debian/Ubuntu-based distributions + +**Architectures**: +- `x86_64-unknown-linux-gnu` (amd64) +- `aarch64-unknown-linux-gnu` (arm64) - with cross-compilation + +**Features**: +- ML Backend: OpenBLAS (STT) + Vulkan (LLM) +- Version validation against release tag +- Full dependency installation (WebKit, GTK, PulseAudio, ALSA, etc.) +- ARM64 cross-compilation using gcc-aarch64-linux-gnu +- **Installation testing** (x86_64 only): Installs the .deb package and verifies: + - Package installation succeeds + - Binary is present and executable + - Shared library dependencies are satisfied + - .desktop file exists and is valid +- **Package verification** (ARM64): Validates package structure and binary architecture +- Uploads artifact to GitHub Actions +- Publishes to GitHub Releases (on release events) + +**Package naming**: `hyprnote-{VERSION}-{amd64|arm64}.deb` + +##### 2. `build-appimage` - AppImage Package + +**Runner**: `ubuntu-20.04` (older for better compatibility) + +**Builds**: `.AppImage` portable package + +**Architectures**: +- `x86_64-unknown-linux-gnu` +- `aarch64-unknown-linux-gnu` (ARM64) - with cross-compilation + +**Features**: +- ML Backend: OpenBLAS (STT) + Vulkan (LLM) +- Self-contained portable format (no installation required) +- ARM64 cross-compilation support +- **AppImage testing** (x86_64 only): Validates the package by: + - Verifying file format + - Extracting and inspecting contents + - Checking for .desktop file and icons + - Validating binary and dependencies + - Testing execution (basic smoke test) +- **Package verification** (ARM64): Validates file format and binary architecture +- Uploads artifact to GitHub Actions +- Publishes to GitHub Releases (on release events) + +**Package naming**: `hyprnote-{VERSION}-{x86_64|aarch64}.AppImage` + +##### 3. `build-flatpak` - Flathub Publishing (DISABLED) + +**Status**: Commented out - awaiting Flathub permissions + +**When to enable**: +1. Create Flatpak manifest at `https://github.com/flathub/com.hyprnote.Hyprnote` +2. Get approval from Flathub reviewers +3. Set up `FLATHUB_TOKEN` secret in GitHub +4. Uncomment the job in the workflow + +**Required files** (to be created when enabling): +- `com.hyprnote.Hyprnote.yml` - Flatpak manifest +- `com.hyprnote.Hyprnote.metainfo.xml` - AppStream metadata + +--- + +### `linux_packages_rpm.yaml` - RPM Packages + +Builds and publishes .rpm packages for Fedora, RHEL, and openSUSE-based distributions. + +#### Trigger Conditions + +- **Manual dispatch**: Via GitHub Actions UI (workflow_dispatch) +- **Automatic**: On release publish (when tag starts with `desktop_`) + +#### Job: `build-rpm` + +**Container**: `fedora:40` + +**Builds**: `.rpm` package + +**Architectures**: +- `x86_64-unknown-linux-gnu` +- `aarch64-unknown-linux-gnu` (ARM64) - with cross-compilation + +**Features**: +- ML Backend: OpenBLAS (STT) + Vulkan (LLM) +- Native Fedora build environment +- ARM64 cross-compilation using gcc-aarch64-linux-gnu +- **Installation testing** (x86_64 only): Installs via dnf and verifies binary, dependencies, and .desktop file +- **Package verification** (ARM64): Uses rpm2cpio to extract and validate binary architecture +- Uploads artifact to GitHub Actions +- Publishes to GitHub Releases (on release events) + +**Package naming**: `hyprnote-{VERSION}-{x86_64|aarch64}.rpm` + +--- + +### `linux_packages_arch.yaml` - Arch Linux Packages + +Builds and publishes Arch Linux packages using PKGBUILD. + +#### Trigger Conditions + +- **Manual dispatch**: Via GitHub Actions UI (workflow_dispatch) +- **Automatic**: On release publish (when tag starts with `desktop_`) + +#### Job: `build-arch-package` + +**Container**: `archlinux:latest` + +**Builds**: `.pkg.tar.zst` package + +**Architectures**: +- `x86_64-unknown-linux-gnu` +- `aarch64-unknown-linux-gnu` (ARM64) - with cross-compilation + +**Features**: +- ML Backend: OpenBLAS (STT) + Vulkan (LLM) +- Native Arch Linux build environment +- PKGBUILD generated dynamically during build +- Built using makepkg (non-root builder user) +- ARM64 cross-compilation support +- **Installation testing** (x86_64 only): Installs via pacman and verifies binary +- **Package verification** (ARM64): Extracts tarball and validates binary architecture +- Uploads artifact to GitHub Actions +- Publishes to GitHub Releases (on release events) + +**Package naming**: `hyprnote-{VERSION}-{x86_64|aarch64}.pkg.tar.zst` + +--- + +## Configuration + +### Environment Variables + +- `RELEASE_CHANNEL`: `stable` or `nightly` +- `TAURI_CONF_PATH`: Points to appropriate Tauri config file + - Stable: `./src-tauri/tauri.conf.stable.json` + - Nightly: `./src-tauri/tauri.conf.nightly.json` + +### Required Secrets + +The workflow uses these GitHub secrets: + +- `GITHUB_TOKEN` (automatic) +- `POSTHOG_API_KEY` - Analytics +- `SENTRY_DSN` - Error tracking +- `KEYGEN_ACCOUNT_ID` - License management +- `KEYGEN_VERIFY_KEY` - License verification +- `TAURI_SIGNING_PRIVATE_KEY` - Code signing +- `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` - Key password +- `FLATHUB_TOKEN` (future) - Flathub publishing + +## Usage + +### Manual Release + +**For .deb and AppImage:** +1. Go to **Actions** → **Linux Packages** in GitHub +2. Click **Run workflow** +3. Select branch (e.g., `linux-development`) +4. Choose channel: `stable` or `nightly` +5. Click **Run workflow** + +**For RPM packages:** +1. Go to **Actions** → **Linux RPM Packages** in GitHub +2. Follow steps 2-5 above + +**For Arch packages:** +1. Go to **Actions** → **Arch Linux Packages** in GitHub +2. Follow steps 2-5 above + +### Automatic Release + +1. Create a git tag: `desktop_v{VERSION}` +2. Push the tag: `git push origin desktop_v{VERSION}` +3. Create a GitHub release from the tag +4. All workflows trigger automatically +5. Packages are uploaded to the release + +**Expected artifacts per release:** +- `hyprnote-{VERSION}-amd64.deb` +- `hyprnote-{VERSION}-arm64.deb` +- `hyprnote-{VERSION}-x86_64.AppImage` +- `hyprnote-{VERSION}-aarch64.AppImage` +- `hyprnote-{VERSION}-x86_64.rpm` +- `hyprnote-{VERSION}-aarch64.rpm` +- `hyprnote-{VERSION}-x86_64.pkg.tar.zst` +- `hyprnote-{VERSION}-aarch64.pkg.tar.zst` + +**Total: 8 packages per release** + +## Testing + +### Installation Testing + +Both workflows include comprehensive testing: + +#### .deb Package Testing +- Installs via `dpkg -i` +- Fixes dependencies with `apt-get install -f` +- Verifies package is listed +- Checks binary existence and executability +- Validates shared library dependencies (`ldd`) +- Verifies .desktop file +- Cleans up after testing + +#### AppImage Testing +- Validates file format +- Extracts contents (`--appimage-extract`) +- Inspects directory structure +- Verifies .desktop file and icons +- Checks binary and dependencies +- Attempts execution (smoke test) + +### Manual Testing + +After download, test the packages: + +```bash +# .deb package (Debian/Ubuntu) +sudo dpkg -i hyprnote-*-amd64.deb # or arm64.deb +sudo apt-get install -f # Fix dependencies if needed +hyprnote # Or launch from application menu + +# .rpm package (Fedora/RHEL) +sudo dnf install hyprnote-*-x86_64.rpm # or aarch64.rpm +# Or on RHEL/CentOS: +sudo yum install hyprnote-*-x86_64.rpm +hyprnote + +# Arch package +sudo pacman -U hyprnote-*-x86_64.pkg.tar.zst # or aarch64.pkg.tar.zst +hyprnote + +# AppImage (universal) +chmod +x hyprnote-*-x86_64.AppImage # or aarch64.AppImage +./hyprnote-*.AppImage +``` + +## Distribution Support + +### .deb Package +- ✅ Ubuntu 24.04+ (amd64, arm64) +- ✅ Debian 12+ Bookworm (amd64, arm64) +- ✅ Linux Mint 21+ (amd64, arm64) +- ✅ Pop!_OS 22.04+ (amd64, arm64) +- ✅ Elementary OS 7+ (amd64, arm64) +- ✅ Raspberry Pi OS 64-bit (arm64) + +### .rpm Package +- ✅ Fedora 40+ (x86_64, aarch64) +- ✅ RHEL 9+ (x86_64, aarch64) +- ✅ Rocky Linux 9+ (x86_64, aarch64) +- ✅ AlmaLinux 9+ (x86_64, aarch64) +- ✅ openSUSE Tumbleweed (x86_64, aarch64) + +### Arch Linux Package +- ✅ Arch Linux (x86_64, aarch64) +- ✅ Manjaro (x86_64, aarch64) +- ✅ EndeavourOS (x86_64, aarch64) +- ✅ Garuda Linux (x86_64, aarch64) + +### AppImage +- ✅ Universal (works on most distributions) +- ✅ No installation required +- ✅ Compatible with older systems (built on Ubuntu 20.04) +- ✅ Portable (can run from USB drive) +- ✅ x86_64 and aarch64 (ARM64) support + +## Architecture Support + +**Current**: ✅ `x86_64` (amd64) and `aarch64` (arm64) + +All workflows now support both architectures: +- Native builds for x86_64 +- Cross-compilation for ARM64 using gcc-aarch64-linux-gnu +- Installation testing on x86_64 runners +- Package verification for ARM64 (without requiring native execution) + +## Dependencies + +### Build Dependencies +- Rust toolchain +- Node.js + pnpm +- Python + Poetry +- protoc (Protocol Buffers) +- System libraries (WebKit, GTK, ALSA, PulseAudio, etc.) + +### Runtime Dependencies (included in packages) +- WebKit2GTK 4.1 +- GTK 3 +- AppIndicator +- ALSA + PulseAudio (audio) +- Vulkan (GPU acceleration) +- OpenBLAS (ML inference) + +## Troubleshooting + +### Build Failures + +**Issue**: Missing system dependencies +**Solution**: Check the "Install system dependencies" step in the workflow + +**Issue**: Rust compilation errors +**Solution**: Verify feature flags match your target platform + +**Issue**: Version mismatch +**Solution**: Ensure git tag matches version in `tauri.conf.json` + +**Issue**: ARM64 cross-compilation fails +**Solution**: +- Check that gcc-aarch64-linux-gnu is installed +- Verify PKG_CONFIG environment variables are set +- Ensure ARM64 libraries are available + +### Installation Issues + +**Issue**: .deb dependency errors +**Solution**: Run `sudo apt-get install -f` to resolve + +**Issue**: .rpm dependency errors +**Solution**: Run `sudo dnf install --best --allowerasing` or check for conflicting packages + +**Issue**: Arch package conflicts +**Solution**: Check for file conflicts with `pacman -Qo ` and resolve manually + +**Issue**: AppImage won't execute +**Solution**: +- Ensure it's marked executable: `chmod +x *.AppImage` +- Check if FUSE is installed (some systems need it) + +**Issue**: Missing graphics drivers +**Solution**: Install Vulkan drivers for your GPU + +**Issue**: ARM64 package won't run on x86_64 +**Solution**: ARM64 packages are architecture-specific and require ARM64 hardware (Raspberry Pi, ARM servers, etc.) + +## Future Work + +- [x] ~~Enable ARM64 (aarch64) builds~~ ✅ **COMPLETED** +- [x] ~~Add RPM packages for Fedora/RHEL~~ ✅ **COMPLETED** +- [x] ~~Add Arch Linux PKGBUILD~~ ✅ **COMPLETED** +- [ ] Enable Flathub publishing +- [ ] Automated testing in containers (different distros) +- [ ] Performance benchmarking in CI +- [ ] Add RISC-V support (when Tauri supports it) +- [ ] Publish to distribution repositories (PPAs, AUR, COPR) diff --git a/.github/workflows/linux_packages.yaml b/.github/workflows/linux_packages.yaml new file mode 100644 index 000000000..6a79b936c --- /dev/null +++ b/.github/workflows/linux_packages.yaml @@ -0,0 +1,620 @@ +name: Linux Packages (Debian/AppImage) + +on: + workflow_dispatch: + inputs: + channel: + description: "Release channel to use" + required: false + default: "nightly" + type: choice + options: + - "stable" + - "nightly" + release: + types: + - published + - prereleased + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + +env: + RELEASE_CHANNEL: ${{ github.event_name == 'workflow_dispatch' && inputs.channel || 'nightly' }} + TAURI_CONF_PATH: ${{ (github.event_name == 'workflow_dispatch' && inputs.channel == 'stable') && './apps/desktop/src-tauri/tauri.conf.stable.json' || './apps/desktop/src-tauri/tauri.conf.nightly.json' }} + +jobs: + build-linux-packages: + if: ${{ github.event_name == 'workflow_dispatch' || (github.event.release && startsWith(github.event.release.tag_name, 'desktop_')) }} + runs-on: ${{ matrix.runner }} + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + - target: "x86_64-unknown-linux-gnu" + arch: "amd64" + features: "stt-openblas,llm-vulkan" + runner: "ubuntu-24.04" + # ARM64 builds disabled temporarily due to cross-compilation dependency issues + # - target: "aarch64-unknown-linux-gnu" + # arch: "arm64" + # features: "stt-openblas,llm-vulkan" + # runner: "ubuntu-24.04" + + defaults: + run: + shell: bash + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install jq + run: sudo apt-get update && sudo apt-get install -y jq + + - name: Validate version + run: | + VERSION=$(jq -r '.version' "${{ env.TAURI_CONF_PATH }}") + + if [[ "${{ github.event_name }}" == "release" ]]; then + TAG_NAME="${{ github.event.release.tag_name }}" + echo "Version: $VERSION, Tag name: $TAG_NAME" + if [[ ! "$TAG_NAME" == *"$VERSION"* ]]; then + echo "Error: Tag version doesn't match package version" + exit 1 + fi + fi + + echo "VERSION=$VERSION" >> $GITHUB_ENV + + - name: Install system dependencies + run: | + sudo apt-get update + + # Install base build dependencies + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + patchelf \ + libssl-dev \ + libasound2-dev \ + libpulse-dev \ + libopenblas-dev \ + cmake \ + libvulkan-dev \ + libxdo-dev \ + libjavascriptcoregtk-4.1-dev \ + libsoup-3.0-dev \ + libclang-dev + + # Install ARM64 cross-compilation tools if needed + if [[ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]]; then + sudo apt-get install -y \ + gcc-aarch64-linux-gnu \ + g++-aarch64-linux-gnu \ + binutils-aarch64-linux-gnu + + # Add ARM64 architecture for dependencies + sudo dpkg --add-architecture arm64 + sudo apt-get update + + # Install ARM64 development libraries + # Note: Some ARM64 packages may not be available in all Ubuntu repos + # libopenblas-dev:arm64 may fail on some runners, but is required for stt-openblas feature + sudo apt-get install -y \ + libc6-dev:arm64 \ + libwebkit2gtk-4.1-dev:arm64 \ + libgtk-3-dev:arm64 \ + libssl-dev:arm64 \ + libasound2-dev:arm64 \ + libpulse-dev:arm64 + + # Try to install OpenBLAS for ARM64, continue if unavailable + sudo apt-get install -y libopenblas-dev:arm64 || { + echo "Warning: libopenblas-dev:arm64 not available, build may fail if stt-openblas feature is used" + echo "Attempting to continue without ARM64 OpenBLAS..." + } + + # Set cross-compilation environment + echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig" >> $GITHUB_ENV + echo "PKG_CONFIG_ALLOW_CROSS=1" >> $GITHUB_ENV + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV + echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> $GITHUB_ENV + echo "CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++" >> $GITHUB_ENV + fi + + - name: Setup Protoc + uses: ./.github/actions/setup_protoc + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Rust toolchain + uses: ./.github/actions/rust_install + with: + platform: "linux" + + - name: Add Rust target + run: rustup target add ${{ matrix.target }} + + - name: Install Node.js dependencies + uses: ./.github/actions/pnpm_install + + - name: Install Python dependencies + uses: ./.github/actions/poetry_install + + - name: Run pre-build script + run: poetry run python scripts/pre_build.py + + - name: Compile translations + run: pnpm -F desktop lingui:compile + + - name: Build UI package + run: pnpm -F ui build + + - name: Build Tauri app with .deb + run: | + # Note: When using pnpm -F desktop, the command runs in apps/desktop context + # So we need to extract just the filename from the full path + TAURI_CONF_FILE=$(basename ${{ env.TAURI_CONF_PATH }}) + pnpm -F desktop tauri build \ + --target ${{ matrix.target }} \ + --config "./src-tauri/${TAURI_CONF_FILE}" \ + --bundles deb \ + --verbose + env: + CI: false + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + KEYGEN_ACCOUNT_ID: ${{ secrets.KEYGEN_ACCOUNT_ID }} + KEYGEN_VERIFY_KEY: ${{ secrets.KEYGEN_VERIFY_KEY }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + + - name: Find .deb package + id: find-deb + run: | + DEB_PATH=$(find apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/deb -name "*.deb" | head -n 1) + if [ -z "$DEB_PATH" ]; then + echo "Error: No .deb package found" + exit 1 + fi + echo "deb_path=$DEB_PATH" >> $GITHUB_OUTPUT + echo "deb_name=$(basename $DEB_PATH)" >> $GITHUB_OUTPUT + echo "Found .deb package: $DEB_PATH" + + - name: Test .deb installation + if: matrix.target == 'x86_64-unknown-linux-gnu' + run: | + echo "Testing .deb package installation..." + + # Install the package + sudo dpkg -i "${{ steps.find-deb.outputs.deb_path }}" || true + + # Fix any dependency issues + sudo apt-get install -f -y + + # Verify installation + dpkg -l | grep hyprnote + + # Get the actual package name (handles both hyprnote and hyprnote-nightly) + PACKAGE_NAME=$(dpkg -l | grep hyprnote | awk '{print $2}') + echo "Package name: $PACKAGE_NAME" + + # Check if binary exists and is executable + BINARY_PATH=$(dpkg -L $PACKAGE_NAME | grep -E '/usr/bin/' | head -n 1) + if [ -z "$BINARY_PATH" ]; then + echo "Error: Binary not found in package" + echo "Package contents:" + dpkg -L $PACKAGE_NAME + exit 1 + fi + echo "Found binary: $BINARY_PATH" + + # Test binary execution (help/version flag) + if [ -f "$BINARY_PATH" ]; then + file "$BINARY_PATH" + ldd "$BINARY_PATH" || true + echo "Binary verification successful" + fi + + # Verify .desktop file exists + DESKTOP_FILE=$(find /usr/share/applications -name "*hyprnote*.desktop" 2>/dev/null | head -n 1) + if [ -n "$DESKTOP_FILE" ]; then + echo "Found .desktop file: $DESKTOP_FILE" + cat "$DESKTOP_FILE" + else + echo "Warning: .desktop file not found" + fi + + # Clean up + sudo apt-get remove -y $PACKAGE_NAME || true + + echo "✅ .deb package installation test passed" + + - name: Verify .deb package (cross-compiled ARM64) + if: matrix.target == 'aarch64-unknown-linux-gnu' + run: | + echo "Verifying ARM64 .deb package structure..." + + # Extract package contents without installing + dpkg-deb -c ${{ steps.find-deb.outputs.deb_path }} + + # Verify package metadata + dpkg-deb -I ${{ steps.find-deb.outputs.deb_path }} + + # Extract to temporary directory for inspection + mkdir -p /tmp/deb-test + dpkg-deb -x ${{ steps.find-deb.outputs.deb_path }} /tmp/deb-test + + # Find and verify binary architecture + BINARY=$(find /tmp/deb-test -type f -name "*hyprnote*" -executable | head -n 1) + if [ -n "$BINARY" ]; then + echo "Found binary: $BINARY" + file "$BINARY" | grep -i "aarch64\|ARM" + if [ $? -eq 0 ]; then + echo "✅ Binary is correctly compiled for ARM64" + else + echo "❌ Binary architecture mismatch" + exit 1 + fi + else + echo "❌ No binary found in package" + exit 1 + fi + + # Verify .desktop file + DESKTOP_FILE=$(find /tmp/deb-test -name "*.desktop" | head -n 1) + if [ -n "$DESKTOP_FILE" ]; then + echo "Found .desktop file: $DESKTOP_FILE" + cat "$DESKTOP_FILE" + fi + + # Clean up + rm -rf /tmp/deb-test + + echo "✅ ARM64 .deb package verification passed" + + - name: Upload .deb artifact + uses: actions/upload-artifact@v4 + with: + name: hyprnote-${{ env.VERSION }}-${{ matrix.arch }}.deb + path: ${{ steps.find-deb.outputs.deb_path }} + if-no-files-found: error + + - name: Upload .deb to release + if: github.event_name == 'release' + uses: softprops/action-gh-release@v1 + with: + files: ${{ steps.find-deb.outputs.deb_path }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build-appimage: + if: ${{ github.event_name == 'workflow_dispatch' || (github.event.release && startsWith(github.event.release.tag_name, 'desktop_')) }} + runs-on: ${{ matrix.runner }} + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + - target: "x86_64-unknown-linux-gnu" + arch: "x86_64" + features: "stt-openblas,llm-vulkan" + runner: "ubuntu-24.04" + # ARM64 builds disabled temporarily due to cross-compilation dependency issues + # - target: "aarch64-unknown-linux-gnu" + # arch: "aarch64" + # features: "stt-openblas,llm-vulkan" + # runner: "ubuntu-20.04" + + defaults: + run: + shell: bash + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install jq + run: sudo apt-get update && sudo apt-get install -y jq + + - name: Validate version + run: | + VERSION=$(jq -r '.version' "${{ env.TAURI_CONF_PATH }}") + + if [[ "${{ github.event_name }}" == "release" ]]; then + TAG_NAME="${{ github.event.release.tag_name }}" + echo "Version: $VERSION, Tag name: $TAG_NAME" + if [[ ! "$TAG_NAME" == *"$VERSION"* ]]; then + echo "Error: Tag version doesn't match package version" + exit 1 + fi + fi + + echo "VERSION=$VERSION" >> $GITHUB_ENV + + - name: Install system dependencies + run: | + sudo apt-get update + + # Install base build dependencies + # Note: Ubuntu 24.04 has webkit2gtk-4.1 which Tauri v2 requires + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + patchelf \ + libssl-dev \ + libasound2-dev \ + libpulse-dev \ + libopenblas-dev \ + cmake \ + libvulkan-dev \ + libxdo-dev \ + xdg-utils \ + file \ + wget \ + libclang-dev + + # Install ARM64 cross-compilation tools if needed + if [[ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]]; then + sudo apt-get install -y \ + gcc-aarch64-linux-gnu \ + g++-aarch64-linux-gnu \ + binutils-aarch64-linux-gnu + + # Add ARM64 architecture for dependencies + sudo dpkg --add-architecture arm64 + sudo apt-get update + + # Install ARM64 development libraries + sudo apt-get install -y \ + libc6-dev:arm64 \ + libwebkit2gtk-4.0-dev:arm64 \ + libgtk-3-dev:arm64 \ + libssl-dev:arm64 \ + libasound2-dev:arm64 \ + libpulse-dev:arm64 \ + libopenblas-dev:arm64 || true + + # Set cross-compilation environment + echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig" >> $GITHUB_ENV + echo "PKG_CONFIG_ALLOW_CROSS=1" >> $GITHUB_ENV + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV + echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> $GITHUB_ENV + echo "CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++" >> $GITHUB_ENV + fi + + - name: Setup Protoc + uses: ./.github/actions/setup_protoc + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Rust toolchain + uses: ./.github/actions/rust_install + with: + platform: "linux" + + - name: Add Rust target + run: rustup target add ${{ matrix.target }} + + - name: Install Node.js dependencies + uses: ./.github/actions/pnpm_install + + - name: Install Python dependencies + uses: ./.github/actions/poetry_install + + - name: Run pre-build script + run: poetry run python scripts/pre_build.py + + - name: Compile translations + run: pnpm -F desktop lingui:compile + + - name: Build UI package + run: pnpm -F ui build + + - name: Build Tauri app with AppImage + run: | + # Note: When using pnpm -F desktop, the command runs in apps/desktop context + # So we need to extract just the filename from the full path + TAURI_CONF_FILE=$(basename ${{ env.TAURI_CONF_PATH }}) + pnpm -F desktop tauri build \ + --target ${{ matrix.target }} \ + --config "./src-tauri/${TAURI_CONF_FILE}" \ + --bundles appimage \ + --verbose + env: + CI: false + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + KEYGEN_ACCOUNT_ID: ${{ secrets.KEYGEN_ACCOUNT_ID }} + KEYGEN_VERIFY_KEY: ${{ secrets.KEYGEN_VERIFY_KEY }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + + - name: Find AppImage package + id: find-appimage + run: | + APPIMAGE_PATH=$(find apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/appimage -name "*.AppImage" | head -n 1) + if [ -z "$APPIMAGE_PATH" ]; then + echo "Error: No AppImage package found" + exit 1 + fi + echo "appimage_path=$APPIMAGE_PATH" >> $GITHUB_OUTPUT + echo "appimage_name=$(basename $APPIMAGE_PATH)" >> $GITHUB_OUTPUT + echo "Found AppImage package: $APPIMAGE_PATH" + + - name: Test AppImage + if: matrix.target == 'x86_64-unknown-linux-gnu' + run: | + echo "Testing AppImage package..." + + APPIMAGE_PATH="${{ steps.find-appimage.outputs.appimage_path }}" + + # Make it executable + chmod +x "$APPIMAGE_PATH" + + # Verify it's a valid AppImage + file "$APPIMAGE_PATH" + + # Extract AppImage to inspect contents + "$APPIMAGE_PATH" --appimage-extract >/dev/null 2>&1 || true + + if [ -d "squashfs-root" ]; then + echo "AppImage extracted successfully" + + # Check for essential files + ls -la squashfs-root/ + + # Verify .desktop file + if [ -f squashfs-root/*.desktop ]; then + echo "Found .desktop file:" + cat squashfs-root/*.desktop + fi + + # Check binary + BINARY=$(find squashfs-root -type f -executable -name "*hyprnote*" | head -n 1) + if [ -n "$BINARY" ]; then + echo "Found binary: $BINARY" + file "$BINARY" + ldd "$BINARY" || true + fi + + # Verify icon files + if [ -d "squashfs-root/usr/share/icons" ] || [ -f "squashfs-root/*.png" ] || [ -f "squashfs-root/*.svg" ]; then + echo "Icon files found" + else + echo "Warning: No icon files found" + fi + + # Clean up + rm -rf squashfs-root + else + echo "Warning: Could not extract AppImage for inspection" + fi + + # Test execution with --help (in headless mode this will fail, but validates the binary works) + # Note: This might fail in CI without display, but at least validates the binary structure + timeout 5 "$APPIMAGE_PATH" --help 2>&1 || echo "AppImage execution attempted (expected to fail in headless CI)" + + echo "✅ AppImage package test completed" + + - name: Verify AppImage (cross-compiled ARM64) + if: matrix.target == 'aarch64-unknown-linux-gnu' + run: | + echo "Verifying ARM64 AppImage package..." + + APPIMAGE_PATH="${{ steps.find-appimage.outputs.appimage_path }}" + + # Make it executable + chmod +x "$APPIMAGE_PATH" + + # Verify file type + file "$APPIMAGE_PATH" + + # Extract AppImage to inspect contents (use --appimage-extract if possible) + "$APPIMAGE_PATH" --appimage-extract >/dev/null 2>&1 || { + echo "Could not extract with built-in tool, trying manual extraction..." + # Fallback: AppImage is an ELF with squashfs embedded + # Skip to squashfs offset and extract (advanced, may not work) + echo "Manual extraction not implemented for cross-platform AppImage" + } + + if [ -d "squashfs-root" ]; then + echo "AppImage extracted successfully" + + # Check for essential files + ls -la squashfs-root/ + + # Find and verify binary architecture + BINARY=$(find squashfs-root -type f -executable -name "*hyprnote*" | head -n 1) + if [ -n "$BINARY" ]; then + echo "Found binary: $BINARY" + file "$BINARY" | grep -i "aarch64\|ARM" + if [ $? -eq 0 ]; then + echo "✅ Binary is correctly compiled for ARM64" + else + echo "❌ Binary architecture mismatch" + exit 1 + fi + else + echo "❌ No binary found in AppImage" + exit 1 + fi + + # Verify .desktop file + if [ -f squashfs-root/*.desktop ]; then + echo "Found .desktop file:" + cat squashfs-root/*.desktop + fi + + # Clean up + rm -rf squashfs-root + else + echo "⚠️ Could not extract AppImage - skipping detailed verification" + echo "This is expected for cross-compiled ARM64 AppImages on x86_64 runners" + echo "Basic file verification completed" + fi + + echo "✅ ARM64 AppImage verification completed" + + - name: Upload AppImage artifact + uses: actions/upload-artifact@v4 + with: + name: hyprnote-${{ env.VERSION }}-${{ matrix.arch }}.AppImage + path: ${{ steps.find-appimage.outputs.appimage_path }} + if-no-files-found: error + + - name: Upload AppImage to release + if: github.event_name == 'release' + uses: softprops/action-gh-release@v1 + with: + files: ${{ steps.find-appimage.outputs.appimage_path }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Flathub publishing workflow (disabled for now - uncomment when ready) + # build-flatpak: + # if: false # Disabled - enable when Flathub permissions are ready + # runs-on: ubuntu-24.04 + # permissions: + # contents: write + # + # steps: + # - name: Checkout repository + # uses: actions/checkout@v4 + # + # - name: Build Flatpak + # uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + # with: + # bundle: hyprnote.flatpak + # manifest-path: com.hyprnote.Hyprnote.yml + # cache-key: flatpak-builder-${{ github.sha }} + # + # - name: Upload Flatpak artifact + # uses: actions/upload-artifact@v4 + # with: + # name: hyprnote-flatpak + # path: hyprnote.flatpak + # + # # Publishing to Flathub requires: + # # 1. Create manifest at: https://github.com/flathub/com.hyprnote.Hyprnote + # # 2. Get it approved by Flathub reviewers + # # 3. Set up FLATHUB_TOKEN secret + # # Then uncomment below: + # # - name: Publish to Flathub + # # if: github.event_name == 'release' + # # uses: flatpak/flatpak-github-actions/flat-manager@v6 + # # with: + # # repository: flathub + # # flat-manager-url: https://hub.flathub.org/ + # # token: ${{ secrets.FLATHUB_TOKEN }} diff --git a/.github/workflows/linux_packages_arch.yaml b/.github/workflows/linux_packages_arch.yaml new file mode 100644 index 000000000..01cd2760d --- /dev/null +++ b/.github/workflows/linux_packages_arch.yaml @@ -0,0 +1,422 @@ +name: Arch Linux Packages + +on: + workflow_dispatch: + inputs: + channel: + description: "Release channel to use" + required: false + default: "nightly" + type: choice + options: + - "stable" + - "nightly" + release: + types: + - published + - prereleased + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + +env: + RELEASE_CHANNEL: ${{ github.event_name == 'workflow_dispatch' && inputs.channel || 'nightly' }} + TAURI_CONF_PATH: ${{ github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && './apps/desktop/src-tauri/tauri.conf.stable.json' || './apps/desktop/src-tauri/tauri.conf.nightly.json' }} + +jobs: + build-arch-package: + if: ${{ github.event_name == 'workflow_dispatch' || (github.event.release && startsWith(github.event.release.tag_name, 'desktop_')) }} + runs-on: ubuntu-latest + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + - target: "x86_64-unknown-linux-gnu" + arch: "x86_64" + features: "stt-openblas,llm-vulkan" + platform: "linux/amd64" + # ARM64 builds temporarily disabled - archlinux:latest doesn't support ARM64 + # TODO: Implement ARM64 support using cross-compilation or alternative approach + # - target: "aarch64-unknown-linux-gnu" + # arch: "aarch64" + # features: "stt-openblas,llm-vulkan" + # platform: "linux/arm64" + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: all + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Install jq + run: sudo apt-get update && sudo apt-get install -y jq + + - name: Validate version + run: | + # Extract version using jq for robust JSON parsing + VERSION=$(jq -r '.version' "${{ env.TAURI_CONF_PATH }}") + + if [[ "${{ github.event_name }}" == "release" ]]; then + TAG_NAME="${{ github.event.release.tag_name }}" + echo "Version: $VERSION, Tag name: $TAG_NAME" + if [[ ! "$TAG_NAME" == *"$VERSION"* ]]; then + echo "Error: Tag version doesn't match package version" + exit 1 + fi + fi + + echo "VERSION=$VERSION" >> $GITHUB_ENV + + - name: Build in Arch Linux container + run: | + # Create build script that will run inside the container + cat > build.sh << 'BUILD_SCRIPT_EOF' + #!/bin/bash + set -e + + echo "=== Building Hyprnote $VERSION for $ARCH ===" + + # Update system and install dependencies + pacman -Syu --noconfirm + pacman -S --noconfirm \ + base-devel \ + git \ + wget \ + curl \ + file \ + cmake \ + pkgconf \ + openssl \ + gtk3 \ + webkit2gtk-4.1 \ + libappindicator-gtk3 \ + librsvg \ + alsa-lib \ + libpulse \ + openblas \ + cblas \ + vulkan-icd-loader \ + vulkan-headers \ + shaderc \ + xdotool \ + xdg-utils \ + unzip \ + clang \ + nodejs \ + npm \ + python \ + python-pip \ + python-poetry + + # Install pnpm + npm install -g pnpm + + # Install Rust + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + source "$HOME/.cargo/env" + rustup target add $RUST_TARGET + + # Install protoc + cd /tmp + PROTOC_VERSION=25.1 + PROTOC_ARCH=$PROTOC_ARCH_NAME + wget https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-${PROTOC_ARCH}.zip + unzip protoc-${PROTOC_VERSION}-linux-${PROTOC_ARCH}.zip -d /usr/local + chmod +x /usr/local/bin/protoc + + # Build the application + cd /workspace + + # Install Node dependencies + pnpm install + + # Install Python dependencies and run pre-build + poetry install + poetry run python scripts/pre_build.py + + # Compile translations + pnpm -F desktop lingui:compile + + # Build UI package + pnpm -F ui build + + # Find cblas.h location and set environment for build + CBLAS_LOCATION=$(find /usr/include -name "cblas.h" 2>/dev/null | head -n 1) + if [ -n "$CBLAS_LOCATION" ]; then + CBLAS_DIR=$(dirname "$CBLAS_LOCATION") + echo "Found cblas.h at: $CBLAS_LOCATION (directory: $CBLAS_DIR)" + export BLAS_INCLUDE_DIRS="$CBLAS_DIR" + else + echo "Warning: cblas.h not found, using default /usr/include" + export BLAS_INCLUDE_DIRS=/usr/include + fi + + export LIBCLANG_PATH=/usr/lib + export CARGO_LOG=cargo::core::compiler::fingerprint=info + export RUST_BACKTRACE=1 + + # Build with Cargo directly + # Note: We skip Tauri CLI bundling (which creates AppImage/deb/rpm) because + # AppImage bundling fails on modern Arch due to incompatible strip utility. + # We only need the binary for our Arch package, so we build with cargo directly. + echo "=== Starting cargo build ===" + cd /workspace/apps/desktop/src-tauri + + # Build with explicit output capture and error handling + echo "Running: cargo build --release --target $RUST_TARGET --features $FEATURES" + if cargo build --release --target $RUST_TARGET --features $FEATURES 2>&1 | tee /tmp/cargo-build.log; then + BUILD_EXIT_CODE=${PIPESTATUS[0]} + else + BUILD_EXIT_CODE=$? + fi + + echo "=== Cargo build exit code: $BUILD_EXIT_CODE ===" + + if [ $BUILD_EXIT_CODE -ne 0 ]; then + echo "❌ Cargo build failed with exit code $BUILD_EXIT_CODE" + echo "=== Last 100 lines of build log ===" + tail -n 100 /tmp/cargo-build.log || true + exit $BUILD_EXIT_CODE + fi + + echo "✅ Cargo build completed successfully" + + # Verify binary was built + # Note: The Cargo package name is "desktop", so the binary is named "desktop" + BINARY_PATH="/workspace/apps/desktop/src-tauri/target/${RUST_TARGET}/release/desktop" + echo "Checking for binary at: $BINARY_PATH" + if [ ! -f "$BINARY_PATH" ]; then + echo "❌ Error: Binary not found at $BINARY_PATH" + echo "=== Contents of target directory ===" + ls -la "/workspace/apps/desktop/src-tauri/target/${RUST_TARGET}/release/" || echo "Release directory does not exist" + echo "=== Last 50 lines of build log ===" + tail -n 50 /tmp/cargo-build.log || true + exit 1 + fi + echo "✅ Binary verified at: $BINARY_PATH" + ls -lh "$BINARY_PATH" + + # Create PKGBUILD directory + mkdir -p /pkgbuild + cd /pkgbuild + + # Create PKGBUILD + cat > PKGBUILD << PKGBUILD_EOF + # Maintainer: Hyprnote Team + pkgname=hyprnote + pkgver=${VERSION} + pkgrel=1 + pkgdesc="AI-powered note-taking and transcription application" + arch=("${ARCH}") + url="https://github.com/different-ai/hyprnote" + license=('custom') + depends=( + 'webkit2gtk-4.1' + 'gtk3' + 'libappindicator-gtk3' + 'librsvg' + 'openssl' + 'alsa-lib' + 'libpulse' + 'openblas' + 'vulkan-icd-loader' + 'xdotool' + ) + source=() + sha256sums=() + options=('!strip') + + package() { + # Find the binary (Cargo package name is "desktop", so binary is named "desktop") + BINARY=\$(find /workspace/apps/desktop/src-tauri/target/${RUST_TARGET}/release -maxdepth 1 -type f -executable -name "desktop" | head -n 1) + + if [ -z "\$BINARY" ]; then + echo "Error: Binary not found" + exit 1 + fi + + # Install binary as "hyprnote" (rename desktop -> hyprnote) + install -Dm755 "\$BINARY" "\${pkgdir}/usr/bin/hyprnote" + + # Install .desktop file if exists + DESKTOP_FILE=\$(find /workspace/apps/desktop/src-tauri -name "*.desktop" | head -n 1) + if [ -n "\$DESKTOP_FILE" ]; then + install -Dm644 "\$DESKTOP_FILE" "\${pkgdir}/usr/share/applications/hyprnote.desktop" + fi + + # Install icons if they exist + for size in 32 128 256; do + ICON=\$(find /workspace/apps/desktop/public -name "\${size}x\${size}.png" -o -name "icon_\${size}x\${size}.png" | head -n 1) + if [ -n "\$ICON" ]; then + install -Dm644 "\$ICON" "\${pkgdir}/usr/share/icons/hicolor/\${size}x\${size}/apps/hyprnote.png" + fi + done + + # Install license + if [ -f /workspace/LICENSE ]; then + install -Dm644 /workspace/LICENSE "\${pkgdir}/usr/share/licenses/\${pkgname}/LICENSE" + fi + } + PKGBUILD_EOF + + # Build package as non-root user + useradd -m builder + chown -R builder:builder /pkgbuild + chown -R builder:builder /workspace + echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + + su - builder -c "cd /pkgbuild && makepkg --noconfirm --skippgpcheck --nosign" + + # Copy package to output + cp /pkgbuild/*.pkg.tar.zst /output/ + + echo "=== Build completed successfully ===" + BUILD_SCRIPT_EOF + + chmod +x build.sh + + # Determine protoc architecture name + if [[ "${{ matrix.arch }}" == "x86_64" ]]; then + PROTOC_ARCH_NAME="x86_64" + else + PROTOC_ARCH_NAME="aarch64" + fi + + # Run build in Docker container with appropriate platform + mkdir -p "${GITHUB_WORKSPACE}/output" + docker run --rm \ + --platform ${{ matrix.platform }} \ + -v "${GITHUB_WORKSPACE}:/workspace" \ + -v "${GITHUB_WORKSPACE}/output:/output" \ + -v "${GITHUB_WORKSPACE}/build.sh:/build.sh" \ + -e VERSION="${{ env.VERSION }}" \ + -e ARCH="${{ matrix.arch }}" \ + -e RUST_TARGET="${{ matrix.target }}" \ + -e FEATURES="${{ matrix.features }}" \ + -e TAURI_CONF="${{ env.TAURI_CONF_PATH }}" \ + -e PROTOC_ARCH_NAME="$PROTOC_ARCH_NAME" \ + -e CI="false" \ + -e GITHUB_TOKEN="${{ secrets.GITHUB_TOKEN }}" \ + -e POSTHOG_API_KEY="${{ secrets.POSTHOG_API_KEY }}" \ + -e SENTRY_DSN="${{ secrets.SENTRY_DSN }}" \ + -e KEYGEN_ACCOUNT_ID="${{ secrets.KEYGEN_ACCOUNT_ID }}" \ + -e KEYGEN_VERIFY_KEY="${{ secrets.KEYGEN_VERIFY_KEY }}" \ + -e TAURI_SIGNING_PRIVATE_KEY="${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}" \ + -e TAURI_SIGNING_PRIVATE_KEY_PASSWORD="${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}" \ + archlinux:latest \ + /bin/bash /build.sh + + - name: Find package file + id: find-pkg + run: | + PKG_PATH=$(find "${GITHUB_WORKSPACE}/output" -name "*.pkg.tar.zst" | head -n 1) + if [ -z "$PKG_PATH" ]; then + echo "Error: No package file found" + exit 1 + fi + echo "pkg_path=$PKG_PATH" >> $GITHUB_OUTPUT + echo "pkg_name=$(basename $PKG_PATH)" >> $GITHUB_OUTPUT + echo "Found package: $PKG_PATH" + + - name: Test package installation + if: matrix.target == 'x86_64-unknown-linux-gnu' + run: | + echo "Testing Arch package installation..." + + # Run test in Docker container + docker run --rm \ + --platform ${{ matrix.platform }} \ + -v "${{ steps.find-pkg.outputs.pkg_path }}:/package.pkg.tar.zst" \ + archlinux:latest \ + /bin/bash -c " + # Install the package + pacman -Syu --noconfirm + pacman -U --noconfirm /package.pkg.tar.zst + + # Verify installation + pacman -Q hyprnote + + # Check if binary exists and is executable + if [ -f /usr/bin/hyprnote ]; then + echo 'Found binary: /usr/bin/hyprnote' + file /usr/bin/hyprnote + ldd /usr/bin/hyprnote || true + echo 'Binary verification successful' + else + echo 'Error: Binary not found' + exit 1 + fi + + # Verify .desktop file + DESKTOP_FILE=\$(find /usr/share/applications -name '*hyprnote*.desktop' 2>/dev/null | head -n 1) + if [ -n \"\$DESKTOP_FILE\" ]; then + echo \"Found .desktop file: \$DESKTOP_FILE\" + cat \"\$DESKTOP_FILE\" + else + echo 'Warning: .desktop file not found' + fi + + echo '✅ Arch package installation test passed' + " + + - name: Verify package (cross-compiled ARM64) + if: matrix.target == 'aarch64-unknown-linux-gnu' + run: | + echo "Verifying ARM64 Arch package structure..." + + # Extract and verify package + mkdir -p /tmp/pkg-test + cd /tmp/pkg-test + tar -xf "${{ steps.find-pkg.outputs.pkg_path }}" + + # List contents + ls -la + + # Find and verify binary architecture + if [ -f usr/bin/hyprnote ]; then + echo "Found binary: usr/bin/hyprnote" + file usr/bin/hyprnote | grep -i "aarch64\|ARM" + if [ $? -eq 0 ]; then + echo "✅ Binary is correctly compiled for ARM64" + else + echo "❌ Binary architecture mismatch" + file usr/bin/hyprnote + exit 1 + fi + else + echo "❌ No binary found in package" + exit 1 + fi + + # Verify .desktop file + DESKTOP_FILE=$(find usr/share/applications -name "*.desktop" 2>/dev/null | head -n 1) + if [ -n "$DESKTOP_FILE" ]; then + echo "Found .desktop file: $DESKTOP_FILE" + cat "$DESKTOP_FILE" + fi + + echo "✅ ARM64 Arch package verification passed" + + - name: Upload package artifact + uses: actions/upload-artifact@v4 + with: + name: hyprnote-${{ env.VERSION }}-${{ matrix.arch }}.pkg.tar.zst + path: ${{ steps.find-pkg.outputs.pkg_path }} + if-no-files-found: error + + - name: Upload package to release + if: github.event_name == 'release' + uses: softprops/action-gh-release@v1 + with: + files: ${{ steps.find-pkg.outputs.pkg_path }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/linux_packages_rpm.yaml b/.github/workflows/linux_packages_rpm.yaml new file mode 100644 index 000000000..546dcc0d7 --- /dev/null +++ b/.github/workflows/linux_packages_rpm.yaml @@ -0,0 +1,307 @@ +name: Linux RPM Packages + +on: + workflow_dispatch: + inputs: + channel: + description: "Release channel to use" + required: false + default: "nightly" + type: choice + options: + - "stable" + - "nightly" + release: + types: + - published + - prereleased + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + +env: + RELEASE_CHANNEL: ${{ github.event_name == 'workflow_dispatch' && inputs.channel || 'nightly' }} + TAURI_CONF_PATH: ${{ (github.event_name == 'workflow_dispatch' && inputs.channel == 'stable') && './apps/desktop/src-tauri/tauri.conf.stable.json' || './apps/desktop/src-tauri/tauri.conf.nightly.json' }} + +jobs: + build-rpm: + if: ${{ github.event_name == 'workflow_dispatch' || (github.event.release && startsWith(github.event.release.tag_name, 'desktop_')) }} + runs-on: ${{ matrix.runner }} + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + - target: "x86_64-unknown-linux-gnu" + arch: "x86_64" + features: "stt-openblas,llm-vulkan" + runner: "ubuntu-24.04" + fedora_version: "40" + # ARM64 builds disabled temporarily due to cross-compilation dependency issues + # - target: "aarch64-unknown-linux-gnu" + # arch: "aarch64" + # features: "stt-openblas,llm-vulkan" + # runner: "ubuntu-24.04" + # fedora_version: "40" + + container: + image: fedora:${{ matrix.fedora_version }} + + defaults: + run: + shell: bash + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install jq + run: dnf install -y jq + + - name: Validate version + run: | + # Extract version using jq for robust JSON parsing + VERSION=$(jq -r '.version' "${{ env.TAURI_CONF_PATH }}") + + if [[ "${{ github.event_name }}" == "release" ]]; then + TAG_NAME="${{ github.event.release.tag_name }}" + echo "Version: $VERSION, Tag name: $TAG_NAME" + if [[ ! "$TAG_NAME" == *"$VERSION"* ]]; then + echo "Error: Tag version doesn't match package version" + exit 1 + fi + fi + + echo "VERSION=$VERSION" >> $GITHUB_ENV + + - name: Install system dependencies + run: | + dnf install -y \ + gcc \ + gcc-c++ \ + make \ + cmake \ + pkgconfig \ + wget \ + curl \ + git \ + file \ + openssl-devel \ + gtk3-devel \ + webkit2gtk4.1-devel \ + libappindicator-gtk3-devel \ + librsvg2-devel \ + alsa-lib-devel \ + pulseaudio-libs-devel \ + openblas-devel \ + vulkan-devel \ + libxdo-devel \ + perl \ + rpm-build \ + rpmdevtools \ + clang \ + llvm-devel \ + unzip + + # Install cross-compilation tools for ARM64 if needed + if [[ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]]; then + dnf install -y \ + gcc-aarch64-linux-gnu \ + gcc-c++-aarch64-linux-gnu \ + binutils-aarch64-linux-gnu \ + glibc-devel.aarch64 + fi + + - name: Setup Protoc + uses: ./.github/actions/setup_protoc + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Rust toolchain + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + source "$HOME/.cargo/env" + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Add Rust target + run: | + source "$HOME/.cargo/env" + rustup target add ${{ matrix.target }} + + - name: Install Node.js + run: | + curl -fsSL https://rpm.nodesource.com/setup_20.x | bash - + dnf install -y nodejs + + - name: Install pnpm + run: npm install -g pnpm + + - name: Install Node.js dependencies + run: pnpm install + + - name: Install Python dependencies + run: | + dnf install -y python3 python3-pip + pip3 install poetry + poetry install + + - name: Run pre-build script + run: | + source "$HOME/.cargo/env" + poetry run python scripts/pre_build.py + + - name: Compile translations + run: pnpm -F desktop lingui:compile + + - name: Build UI package + run: pnpm -F ui build + + - name: Set cross-compilation environment for ARM64 + if: matrix.target == 'aarch64-unknown-linux-gnu' + run: | + echo "PKG_CONFIG_ALLOW_CROSS=1" >> $GITHUB_ENV + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV + echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> $GITHUB_ENV + echo "CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++" >> $GITHUB_ENV + + - name: Build Tauri app with RPM + run: | + source "$HOME/.cargo/env" + # Note: When using pnpm -F desktop, the command runs in apps/desktop context + # So we need to extract just the filename from the full path + TAURI_CONF_FILE=$(basename ${{ env.TAURI_CONF_PATH }}) + pnpm -F desktop tauri build \ + --target ${{ matrix.target }} \ + --config "./src-tauri/${TAURI_CONF_FILE}" \ + --bundles rpm \ + --verbose + env: + CI: false + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + KEYGEN_ACCOUNT_ID: ${{ secrets.KEYGEN_ACCOUNT_ID }} + KEYGEN_VERIFY_KEY: ${{ secrets.KEYGEN_VERIFY_KEY }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + + - name: Find RPM package + id: find-rpm + run: | + RPM_PATH=$(find apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/rpm -name "*.rpm" | head -n 1) + if [ -z "$RPM_PATH" ]; then + echo "Error: No RPM package found" + exit 1 + fi + echo "rpm_path=$RPM_PATH" >> $GITHUB_OUTPUT + echo "rpm_name=$(basename $RPM_PATH)" >> $GITHUB_OUTPUT + echo "Found RPM package: $RPM_PATH" + + - name: Test RPM installation + if: matrix.target == 'x86_64-unknown-linux-gnu' + run: | + echo "Testing RPM package installation..." + + # Install the package + dnf install -y "${{ steps.find-rpm.outputs.rpm_path }}" + + # Verify installation + rpm -qa | grep hyprnote + + # Get the actual package name (handles both hyprnote and hyprnote-nightly) + PACKAGE_NAME=$(rpm -qa | grep hyprnote) + echo "Package name: $PACKAGE_NAME" + + # Check if binary exists and is executable + BINARY_PATH=$(rpm -ql $PACKAGE_NAME | grep -E '/usr/bin/' | head -n 1) + if [ -z "$BINARY_PATH" ]; then + echo "Error: Binary not found in package" + echo "Package contents:" + rpm -ql $PACKAGE_NAME + exit 1 + fi + echo "Found binary: $BINARY_PATH" + + # Test binary + if [ -f "$BINARY_PATH" ]; then + file "$BINARY_PATH" + ldd "$BINARY_PATH" || true + echo "Binary verification successful" + fi + + # Verify .desktop file exists + DESKTOP_FILE=$(find /usr/share/applications -name "*hyprnote*.desktop" 2>/dev/null | head -n 1) + if [ -n "$DESKTOP_FILE" ]; then + echo "Found .desktop file: $DESKTOP_FILE" + cat "$DESKTOP_FILE" + else + echo "Warning: .desktop file not found" + fi + + # Clean up + dnf remove -y $PACKAGE_NAME || true + + echo "✅ RPM package installation test passed" + + - name: Verify RPM package (cross-compiled ARM64) + if: matrix.target == 'aarch64-unknown-linux-gnu' + run: | + echo "Verifying ARM64 RPM package structure..." + + # Display RPM info + rpm -qip "${{ steps.find-rpm.outputs.rpm_path }}" + + # List package contents + rpm -qlp "${{ steps.find-rpm.outputs.rpm_path }}" + + # Extract RPM to inspect + mkdir -p /tmp/rpm-test + cd /tmp/rpm-test + rpm2cpio "${{ steps.find-rpm.outputs.rpm_path }}" | cpio -idmv + + # Find and verify binary architecture + BINARY=$(find /tmp/rpm-test -type f -name "*hyprnote*" -executable 2>/dev/null | head -n 1) + if [ -n "$BINARY" ]; then + echo "Found binary: $BINARY" + file "$BINARY" | grep -i "aarch64\|ARM" + if [ $? -eq 0 ]; then + echo "✅ Binary is correctly compiled for ARM64" + else + echo "❌ Binary architecture mismatch" + file "$BINARY" + exit 1 + fi + else + echo "❌ No binary found in package" + exit 1 + fi + + # Verify .desktop file + DESKTOP_FILE=$(find /tmp/rpm-test -name "*.desktop" | head -n 1) + if [ -n "$DESKTOP_FILE" ]; then + echo "Found .desktop file: $DESKTOP_FILE" + cat "$DESKTOP_FILE" + fi + + # Clean up + cd / + rm -rf /tmp/rpm-test + + echo "✅ ARM64 RPM package verification passed" + + - name: Upload RPM artifact + uses: actions/upload-artifact@v4 + with: + name: hyprnote-${{ env.VERSION }}-${{ matrix.arch }}.rpm + path: ${{ steps.find-rpm.outputs.rpm_path }} + if-no-files-found: error + + - name: Upload RPM to release + if: github.event_name == 'release' + uses: softprops/action-gh-release@v1 + with: + files: ${{ steps.find-rpm.outputs.rpm_path }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 02f86fcbd..5f80d925a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,10 @@ internal .turbo .windsurfrules -CLAUDE.md \ No newline at end of file + CLAUDE.md + +# Local audio artifacts +crates/audio/normalized_output.wav + +# Local package builds +pkgbuild/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..6a6fa05c4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,70 @@ +# AGENTS.md + +This file gives automated agents concise guidance for working in this repository. + +## Overview +This is a polyglot monorepo: +- Rust crates under `crates/` and `plugins/` plus some standalone dirs. +- Tauri / desktop and web apps under `apps/`. +- Shared TypeScript packages under `packages/`. +- Scripts (Python, shell, Swift) under `scripts/`. + +## Build +- Rust: `cargo build` for whole workspace; `cargo build -p ` for a single crate. +- Use feature flags to avoid heavy ML deps: e.g. `cargo test -p tauri-plugin-listener --no-default-features --features export-types` for specta export without local LLM/STT. +- Prefer optional dependencies + a dedicated feature (e.g. `connector`) instead of unconditional linking. +- Node/TS: `pnpm install`, then `pnpm run build` (Turbo orchestrates). For a single package: `pnpm --filter run build`. + +## Testing +- Rust: `cargo test` (workspace). Single crate: `cargo test -p `. With feature gating: `cargo test -p --no-default-features --features `. +- Run a single Rust test: `cargo test -p `. +- TypeScript/Vitest: `pnpm --filter test`. Single test name: `pnpm --filter vitest run path/to/file.test.ts -t "test name"`. +- Run snapshot tests: `cargo install cargo-insta` then `cargo test`. + +## Formatting & Lint +- Rust formatting via `dprint fmt` (uses exec rustfmt). Run before committing. +- Keep Rust imports grouped: std, external crates, workspace crates, local modules. +- TypeScript formatting also via `dprint fmt`. Do not introduce other formatters unless necessary. +- Avoid trailing whitespace; keep line length reasonable (<120 typical). + +## i18n / Translations +- Uses Lingui for internationalization. After modifying UI text, run: `task i18n` (or `pnpm -F desktop lingui:extract`). +- **CRITICAL**: Always run `pnpm -F desktop lingui:compile` after extract and before building to sync translation keys. +- Build order for desktop: `poetry run python scripts/pre_build.py` → `pnpm -F desktop lingui:compile` → `pnpm -F ui build` → `pnpm -F desktop tauri build`. +- Translation catalogs: `apps/desktop/src/locales/{en,ko}/messages.{po,ts}`. The `.ts` files are compiled from `.po`. + +## Conventions (Rust) +- Modules & functions: `snake_case`; types & enums: `CamelCase`. +- Errors: use `thiserror`; prefer a central `Error` enum and `Result`. +- Instrument async/public functions with `#[tracing::instrument(skip(...))]` when adding tracing. +- Feature gating: wrap variants/APIs with `#[cfg(feature = "feat")]`; supply fallbacks when disabled. + +## Adding Features +- Introduce new optional deps with a matching feature name; add to `default` only if broadly needed. +- For specta/TS type export steps, minimize dependency surface (avoid heavy ML crates) by disabling default features. +- Extend builders (e.g. event/command registration) conditionally behind feature flags. + +## TypeScript/Apps +- Prefer explicit types for public APIs. Use consistent naming: `camelCase` for variables/functions, `PascalCase` for types/components. +- Centralized config and shared utilities live in `packages/utils` and `packages/ui`. +- Desktop app: `pnpm -F desktop tauri:dev` for development. Uses Vite + React + Tauri v2. + +## Scripts +- Python uses `poetry.lock` / `pyproject.toml`; prefer `poetry run python