diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f6fd310 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,282 @@ +name: Build Packages + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + - master + tags: + - "[0-9]*.[0-9]*.[0-9]*" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + windows-installer: + name: Windows installer + runs-on: windows-latest + timeout-minutes: 45 + defaults: + run: + shell: pwsh + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Audit runner image + run: | + "ImageOS=$env:ImageOS" + "ImageVersion=$env:ImageVersion" + Get-ComputerInfo | Select-Object WindowsProductName, WindowsVersion, OsHardwareAbstractionLayer + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.26.2" + check-latest: true + cache: true + + - name: Set up MSYS2 + uses: msys2/setup-msys2@v2 + with: + update: true + path-type: inherit + install: >- + mingw-w64-x86_64-gcc + + - name: Set up Zig + uses: goto-bus-stop/setup-zig@v2 + with: + version: "0.17.0-dev.215+8c5542bd3" + + - name: Install Windows build tools + run: | + choco install llvm innosetup --yes --no-progress + $env:GITHUB_PATH | ForEach-Object { + Add-Content $_ "C:\Program Files\LLVM\bin" + Add-Content $_ "C:\msys64\mingw64\bin" + Add-Content $_ "C:\Program Files (x86)\Inno Setup 6" + } + + - name: Set up GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + install-only: true + distribution: goreleaser + version: latest + + - name: Audit toolchain + run: | + go version + goreleaser --version + clang --version + x86_64-w64-mingw32-gcc --version + zig version + iscc /? + + - name: Test + run: | + New-Item -ItemType Directory -Force build\.cache\gobuildtmp, build\.cache\gocache | Out-Null + $env:TEMP=(Resolve-Path build\.cache\gobuildtmp).Path + $env:TMP=$env:TEMP + $env:GOTMPDIR=$env:TEMP + $env:GOCACHE=(Resolve-Path build\.cache\gocache).Path + $env:CGO_ENABLED='1' + $env:CC='clang --target=x86_64-w64-windows-gnu' + $env:CXX='clang++ --target=x86_64-w64-windows-gnu' + $env:GOFLAGS='-ldflags=-extld=x86_64-w64-mingw32-gcc' + go test ./... + + - name: Check GoReleaser config + run: goreleaser check --config build\release\goreleaser-win-all.yaml + + - name: Build Windows binaries + run: | + New-Item -ItemType Directory -Force build\.cache\gobuildtmp, build\.cache\gocache | Out-Null + $env:TEMP=(Resolve-Path build\.cache\gobuildtmp).Path + $env:TMP=$env:TEMP + $env:GOTMPDIR=$env:TEMP + $env:GOCACHE=(Resolve-Path build\.cache\gocache).Path + goreleaser build --snapshot --clean --config build\release\goreleaser-win-all.yaml + + - name: Build Windows installer + run: .\build\scripts\build-installer.ps1 + + - name: Upload Windows artifacts + uses: actions/upload-artifact@v4 + with: + name: musicalc-windows + if-no-files-found: error + retention-days: 14 + path: | + build/dist/musicalc_x64.exe + build/dist/musicalc_arm64.exe + build/dist/installer/*.exe + build/dist/artifacts.json + build/dist/metadata.json + + linux-amd64-packages: + name: Linux AMD64 packages + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Audit runner image + run: | + echo "ImageOS=${ImageOS}" + echo "ImageVersion=${ImageVersion}" + lsb_release -a + uname -a + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.26.2" + check-latest: true + cache: true + + - name: Install Linux AMD64 dependencies + run: | + sudo apt-get update + sudo apt-get install --yes --no-install-recommends \ + gcc \ + g++ \ + libasound2-dev \ + libgl1-mesa-dev \ + pkg-config \ + rpm \ + xorg-dev + + - name: Set up GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + install-only: true + distribution: goreleaser + version: latest + + - name: Audit toolchain + run: | + go version + goreleaser --version + gcc --version + g++ --version + pkg-config --version + rpm --version + + - name: Test + run: go test ./... + + - name: Check GoReleaser config + run: goreleaser check --config build/release/goreleaser-linux-amd64.yaml + + - name: Build Linux AMD64 packages + run: goreleaser release --snapshot --clean --config build/release/goreleaser-linux-amd64.yaml --skip=publish + + - name: Upload Linux AMD64 artifacts + uses: actions/upload-artifact@v4 + with: + name: musicalc-linux-amd64 + if-no-files-found: error + retention-days: 14 + path: | + build/dist/*.tar.gz + build/dist/*.deb + build/dist/*.rpm + build/dist/artifacts.json + build/dist/metadata.json + + linux-arm64-packages: + name: Linux ARM64 packages + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Audit runner image + run: | + echo "ImageOS=${ImageOS}" + echo "ImageVersion=${ImageVersion}" + lsb_release -a + uname -a + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.26.2" + check-latest: true + cache: true + + - name: Install Linux ARM64 cross dependencies + run: | + sudo dpkg --add-architecture arm64 + sudo apt-get update + sudo apt-get install --yes --no-install-recommends \ + gcc-aarch64-linux-gnu \ + g++-aarch64-linux-gnu \ + libasound2-dev:arm64 \ + libgl1-mesa-dev:arm64 \ + libxcursor-dev:arm64 \ + libxi-dev:arm64 \ + libxinerama-dev:arm64 \ + libxrandr-dev:arm64 \ + libx11-dev:arm64 \ + libxxf86vm-dev:arm64 \ + pkg-config \ + rpm + + - name: Set up GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + install-only: true + distribution: goreleaser + version: latest + + - name: Audit toolchain + run: | + go version + goreleaser --version + aarch64-linux-gnu-gcc --version + aarch64-linux-gnu-g++ --version + pkg-config --version + rpm --version + + - name: Check GoReleaser config + run: goreleaser check --config build/release/goreleaser-linux-arm64.yaml + + - name: Build Linux ARM64 packages + run: goreleaser release --snapshot --clean --config build/release/goreleaser-linux-arm64.yaml --skip=publish + + - name: Upload Linux ARM64 artifacts + uses: actions/upload-artifact@v4 + with: + name: musicalc-linux-arm64 + if-no-files-found: error + retention-days: 14 + path: | + build/dist/*.tar.gz + build/dist/*.deb + build/dist/*.rpm + build/dist/artifacts.json + build/dist/metadata.json diff --git a/.gitignore b/.gitignore index 6a14833..3b45e2e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,10 +17,11 @@ musicalc coverage.* *.coverprofile profile.cov -installer -installer/* -dist -dist/* +/installer/ +/dist/ +build/dist/ +build/.cache/ +build/installer/out/ # Dependency directories (remove the comment below to include it) # vendor/ @@ -38,7 +39,9 @@ go.work.sum musicalc_linux_* musicalc_win_* -musicalc.apk - fyne-cross -fyne-cross/* \ No newline at end of file +fyne-cross/* + +# Legacy local Go build/cache directories +.gobuildtmp/ +.gocache/ diff --git a/README-INSTALLER.md b/README-INSTALLER.md deleted file mode 100644 index f9c3761..0000000 --- a/README-INSTALLER.md +++ /dev/null @@ -1,108 +0,0 @@ -# Creating Windows Installer - -## Prerequisites - -1. **Inno Setup**: Download and install from https://jrsoftware.org/isdl.php - -## Building the Installer - -1. **Build the application first:** - ```powershell - go build -ldflags="-s -w" -o musicalc.exe - ``` - -2. **Open Inno Setup Compiler** - -3. **Open the script:** - - File → Open → Select `musicalc.iss` - -4. **Compile:** - - Build → Compile (or press F9) - -5. **Output:** - - Installer will be created in `installer/MusiCalc-Setup-1.0.0.exe` - -## Customization - -Edit `musicalc.iss` to change: -- `MyAppVersion` - Application version number -- `MyAppPublisher` - Your name/company -- `MyAppURL` - Your website/repository URL -- `AppId` - Unique GUID (keep as-is or generate new one) - -## Testing - -1. Run the generated installer: `installer/MusiCalc-Setup-1.0.0.exe` -2. Install to default location or custom directory -3. Verify desktop shortcut (if selected) -4. Test the application runs correctly -5. Uninstall via Windows Settings → Apps - -## Distribution - -The installer file (`MusiCalc-Setup-1.0.0.exe`) is a single executable that can be distributed to users. It includes: -- Application executable -- App icon -- Start menu shortcuts -- Optional desktop shortcut -- Uninstaller - -# Creating Linux Packages and Tarballs - -GoReleaser automates the creation of `.deb`, `.rpm`, and `.tar.gz` packages for distribution. - -1. **Install GoReleaser** - ```bash - go install github.com/goreleaser/goreleaser/v2@latest - ``` - -2. **Install Package Dependencies* - See [Build Instructions](README-BUILD.md). - -3. **Building packages with goreleaser** - - For Linux AMD64: - - ```bash - goreleaser check --config .goreleaser-linux-amd64.yaml - # for snapshot - goreleaser release --snapshot --clean --config .goreleaser-linux-amd64.yaml - # for release - goreleaser release --clean --config .goreleaser-linux-amd64.yaml --skip=publish - ``` - - For Linux ARM64: - - ```bash - goreleaser check --config .goreleaser-linux-arm64.yaml - # for snapshot - goreleaser release --snapshot --clean --config .goreleaser-linux-arm64.yaml - # for release - goreleaser release --clean --config .goreleaser-linux-arm64.yaml --skip=publish - ``` - - For Windows AMD64/ARM64: - ```bash - goreleaser check --config .goreleaser-cross-win.yaml - # for snapshot - goreleaser release --snapshot --clean --config .goreleaser-cross-win.yaml - # for release - goreleaser release --clean --config .goreleaser-cross-win.yaml --skip=publish - ``` - - - This creates packages in the `dist/` directory without requiring a Git tag. - -4. **Create a release (requires Git tag)** - ```bash - # Create and push a version tag - git tag -a v0.1.0 -m "Release version 0.1.0" - git push origin v0.1.0 - - # Build and publish release using the gorelease release commands above - ``` - -**Generated artifacts**: -- `.tar.gz` archive with binary, install script, icon, and desktop entry -- `.deb` package for Debian/Ubuntu -- `.rpm` package for Fedora/RHEL diff --git a/README.md b/README.md index e22eb28..3f68f38 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A cross-platform music calculator application built with Go and Fyne. Provides real-time calculators for tempo, pitch, sampling, and time-stretching operations commonly used in music production and audio engineering. -This project is heavily inspired by [MusicMath](https://dev.laurentcolson.com/musicmath.html), an excellent tool currently exclusive to the Apple ecosystem. This version aims to bring that same utility to Windows, Linux and Android, with a simpler look and feel. For macOS users looking for a polished commercial solution, MusicMath is highly recommended. MusicMath has long been a reference tool for music-related calculations, and this project draws inspiration from that work. +This project is heavily inspired by [MusicMath](https://dev.laurentcolson.com/musicmath.html), an excellent tool currently exclusive to the Apple ecosystem. This version aims to bring that same utility to Windows and Linux, with a simpler look and feel. For macOS users looking for a polished commercial solution, MusicMath is highly recommended. MusicMath has long been a reference tool for music-related calculations, and this project draws inspiration from that work. @@ -96,16 +96,16 @@ This application is provided for informational purposes only and is not guarante ## Documentation - **[User Guide](README-USERGUIDE.md)** - How to use each calculator -- **[Build Instructions](README-BUILD.md)** - How to build from source on Windows, Linux, and macOS -- **[Creating Installers](README-INSTALLER.md)** - Creating Windows installers and Linux packages -- **[Icon Resources](README-ICONS.md)** - Icon specifications and resources -- **[Test Instructions](README-TESTS.md)** - How to run tests +- **[Build Instructions](build/docs/README-BUILD.md)** - How to build from source on Windows, Linux, and macOS +- **[Creating Installers](build/docs/README-INSTALLER.md)** - Creating Windows installers and Linux packages +- **[Icon Resources](build/docs/README-ICONS.md)** - Icon specifications and resources +- **[Test Instructions](build/docs/README-TESTS.md)** - How to run tests ## Requirements -- Go 1.24.5 or later +- Go 1.26.2 or later - GCC/MinGW (for CGO support on Windows) -- Fyne v2.7.1 +- Fyne v2.7.3 ## License diff --git a/VERSION b/VERSION deleted file mode 100644 index 7fc2521..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.8.6 diff --git a/build-android.sh b/build-android.sh deleted file mode 100755 index 0c55488..0000000 --- a/build-android.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/bin/bash - -# Exit immediately if a command exits with a non-zero status -set -e - -# --- 1, 2, 3) Check Environment Variables --- -check_env_var() { - if [ -z "${!1}" ]; then - echo "error: $1 is not set." - exit 1 - fi - if [ ! -d "${!1}" ]; then - echo "error: $1 path does not exist: ${!1}" - exit 1 - fi -} - -check_env_var "ANDROID_NDK_HOME" -check_env_var "ANDROID_HOME" -check_env_var "ANDROID_SDK_ROOT" - -# --- 4, 5) Locate zipalign --- -# Priority: 1. System Path, 2. build-tools (standard), 3. cmdline-tools (user request) -if command -v zipalign >/dev/null 2>&1; then - ZIPALIGN_PATH=$(command -v zipalign) -else - # Search for the highest version available in build-tools (Standard location) - # or the user-specified cmdline-tools path - SEARCH_PATHS=( - "${ANDROID_HOME}/build-tools" - "${ANDROID_HOME}/cmdline-tools/latest/bin" - ) - - ZIPALIGN_PATH=$(find "${SEARCH_PATHS[@]}" -name zipalign -type f 2>/dev/null | sort -V | tail -n 1) -fi - -if [ -z "$ZIPALIGN_PATH" ] || [ ! -x "$ZIPALIGN_PATH" ]; then - echo "error: zipalign not found in PATH or Android SDK structure." - exit 1 -fi - -echo "Using zipalign: $ZIPALIGN_PATH" - -# --- Build Configuration --- -export CGO_ENABLED=1 -export CGO_CFLAGS="-O3 -flto=auto -march=armv8-a+crc+crypto" -# Force 16KB Page Alignment for Android 15+ compatibility -export CGO_LDFLAGS="-O3 -flto=auto -Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384" -export GOFLAGS="-ldflags=-s -w" - -git pull && git checkout -- go.mod && git fetch --tags --force - -# --- Execution --- -echo "Building APK..." -fyne package --os android/arm64 --id com.github.bzeiss --release -icon icons/appicon.png - -# Ensure dist exists -mkdir -p dist - -# Aligning -echo "Aligning APK to 16KB boundary..." -mv musicalc.apk dist/musicalc-unaligned.apk -"$ZIPALIGN_PATH" -f -v -P 16 4 dist/musicalc-unaligned.apk dist/musicalc.apk - -# Verify alignment -"$ZIPALIGN_PATH" -c -v -P 16 4 dist/musicalc.apk - -# --- 6) Smarter Alignment Check --- -echo "Verifying 16KB ($2^{14}$) alignment of internal shared libraries..." -cd dist -unzip -p musicalc.apk lib/arm64-v8a/libmusicalc.so > tmp_lib.so - -# Extraction of alignment value using awk -# We check the 'align' value of the LOAD segments -ALIGNMENT=$(objdump -p tmp_lib.so | grep "LOAD" | awk '{print $NF}' | head -n 1) - -if [[ "$ALIGNMENT" == *"2**14"* ]] || [[ "$ALIGNMENT" == *"0x4000"* ]]; then - echo "SUCCESS: Library is correctly aligned to 16KB ($ALIGNMENT)." -else - echo "FAILURE: Library alignment is $ALIGNMENT, expected 2**14 (16384)." - rm tmp_lib.so - exit 1 -fi - -rm tmp_lib.so -echo "Build and Verification Complete: dist/musicalc.apk" \ No newline at end of file diff --git a/build-win.ps1 b/build-win.ps1 deleted file mode 100644 index 849b7be..0000000 --- a/build-win.ps1 +++ /dev/null @@ -1,4 +0,0 @@ -$env:CGO_FLAGS="-O3 -flto=auto -march=x86-64-v3" -$env:CGO_LDFLAGS="-O3 -flto=auto" -$env:CGO_ENABLED=1 -go build -ldflags="-s -w" -o musicalc.exe diff --git a/README-BUILD.md b/build/docs/README-BUILD.md similarity index 64% rename from README-BUILD.md rename to build/docs/README-BUILD.md index 9768ff3..587d3d2 100644 --- a/README-BUILD.md +++ b/build/docs/README-BUILD.md @@ -10,9 +10,10 @@ cd musicalc ``` -2. **Install MSYS2** (provides MinGW GCC compiler required for Fyne) +2. **Install LLVM and MSYS2** (provides clang plus the MinGW linker required for Fyne) ```powershell winget install -e --id MSYS2.MSYS2 + winget install -e --id LLVM.LLVM ``` 3. **Install GCC via MSYS2** @@ -32,11 +33,12 @@ - Add new entry: `C:\msys64\mingw64\bin` - Click OK and restart your terminal -5. **Verify GCC installation** +5. **Verify compiler installation** ```powershell - gcc --version + clang --version + x86_64-w64-mingw32-gcc --version ``` - Should output GCC version information + Both commands should output version information. 6. **Install Go dependencies** ```powershell @@ -45,19 +47,13 @@ 7. **Build the application** ```powershell - $env:CGO_FLAGS="-O3 -flto=auto -march=x86-64-v3" - $env:CGO_LDFLAGS="-O3 -flto=auto" - $env:CGO_ENABLED=1 - go build -ldflags="-s -w" -o musicalc.exe - ``` - or for a production build without the console: - ```powershell - go build -ldflags="-s -w -H=windowsgui" -o musicalc.exe + .\build\scripts\build-win.ps1 ``` + The script injects the application version from the current Git tag. If the current commit is not exactly tagged, it injects a local dev version derived from the latest tag and commit. 8. **Run the application** ```powershell - .\musicalc.exe + .\build\dist\musicalc.exe ``` ## Linux @@ -90,8 +86,9 @@ 4. **Windows ARM64 Cross-compilation support (requires a more recent Ubuntu Server version for building)** - Download Zig from: https://ziglang.org/download/ - - Put zig somewere into your environment PATH - - test by calling "zig" and "zig c++" + - Put `zig` somewhere into your environment `PATH` + - Test by calling `zig` and `zig c++` + - The Windows GoReleaser config stores Zig cache data under `build/.cache/zig-global` and `build/.cache/zig-local` so build-generated files stay inside the project tree. 5. **Install Go dependencies** ```bash @@ -100,6 +97,7 @@ 6. **Build the application for Linux AMD64** ```bash + mkdir -p build/dist export CGO_CFLAGS="-O3 -flto=auto -march=x86-64-v3" export CGO_LDFLAGS="-O3 -flto=auto" export CGO_ENABLED=1 @@ -107,10 +105,12 @@ export CXX=g++ export GOOS=linux export GOARCH=amd64 - go build -ldflags="-s -w" -o musicalc_linux_amd64 + VERSION="$(git describe --tags --exact-match 2>/dev/null || echo dev-$(git rev-parse --short HEAD))" + go build -ldflags="-s -w -X main.version=${VERSION}" -o build/dist/musicalc_linux_amd64 ``` 7. **Build the application for Linux ARM64** ```bash + mkdir -p build/dist export CGO_CFLAGS="-O3 -flto=auto -march=armv8.4-a+crc+crypto -fomit-frame-pointer" export CGO_LDFLAGS="-O3 -flto=auto -Wl,--gc-sections" export CGO_ENABLED=1 @@ -118,11 +118,13 @@ export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig export GOOS=linux export GOARCH=arm64 - go build -ldflags="-s -w" -o musicalc_linux_arm64 + VERSION="$(git describe --tags --exact-match 2>/dev/null || echo dev-$(git rev-parse --short HEAD))" + go build -ldflags="-s -w -X main.version=${VERSION}" -o build/dist/musicalc_linux_arm64 ``` 8. **Build the application for Windows AMD64** ```bash + mkdir -p build/dist export CGO_CFLAGS="-O3 -flto=auto -march=x86-64-v3 -m64" export CGO_LDFLAGS="-O3 -flto=auto" export CGO_ENABLED=1 @@ -130,46 +132,33 @@ export CXX=x86_64-w64-mingw32-g++ export GOOS=windows export GOARCH=amd64 - go build -ldflags="-s -w" -o musicalc_win_amd64 + VERSION="$(git describe --tags --exact-match 2>/dev/null || echo dev-$(git rev-parse --short HEAD))" + go build -ldflags="-s -w -X main.version=${VERSION}" -o build/dist/musicalc_win_amd64.exe ``` 9. **Build the application for Windows ARM64** ```bash + mkdir -p build/dist export CGO_CFLAGS="-O3 -mcpu=oryon_1 -fomit-frame-pointer" export CGO_LDFLAGS="-O3" export CGO_ENABLED=1 export CC="zig cc -target aarch64-windows-gnu" export CXX="zig c++ -target aarch64-windows-gnu" + export ZIG_GLOBAL_CACHE_DIR=build/.cache/zig-global + export ZIG_LOCAL_CACHE_DIR=build/.cache/zig-local export GOOS=windows export GOARCH=arm64 - go build -ldflags="-s -w" -o musicalc_win_arm64 - ``` - -10. **Build the application for Android ARM64** - ```bash - export ANDROID_NDK_HOME=/path/to/your/android-ndk - export ANDROID_HOME=/path/to/your/android-sdk - export ANDROID_SDK_ROOT=$ANDROID_HOME - export PATH=$PATH:${ANDROID_HOME}/cmdline-tools/latest/bin - ./build-android.sh + VERSION="$(git describe --tags --exact-match 2>/dev/null || echo dev-$(git rev-parse --short HEAD))" + go build -ldflags="-s -w -X main.version=${VERSION}" -o build/dist/musicalc_win_arm64.exe ``` - For the Android SDK and NDK, you can use the Android Studio SDK Manager to install them. +10. **Run the application** ```bash - sdkmanager --licenses # accept all licenses - sdkmanager "platform-tools" "build-tools;36.1.0" "platforms;android-36" # choose the most recent version - ``` - -11. **Run the application** - ```bash - ./musicalc_xxx + ./build/dist/musicalc_xxx ``` ## Requirements -- Go 1.24.5 or later -- GCC/MinGW (for CGO support on Windows) -- Fyne v2.7.1 - +- Go 1.26.2 or later - GCC/MinGW (for CGO support on Windows) -- Fyne v2.7.1 +- Fyne v2.7.3 diff --git a/README-ICONS.md b/build/docs/README-ICONS.md similarity index 100% rename from README-ICONS.md rename to build/docs/README-ICONS.md diff --git a/build/docs/README-INSTALLER.md b/build/docs/README-INSTALLER.md new file mode 100644 index 0000000..76fb8f5 --- /dev/null +++ b/build/docs/README-INSTALLER.md @@ -0,0 +1,116 @@ +# Creating Windows Installer + +## Prerequisites + +1. **Inno Setup**: Download and install from https://jrsoftware.org/isdl.php + +## Building the Installer + +1. **Build the application first:** + ```powershell + .\build\scripts\build-win.ps1 + ``` + +2. **Open Inno Setup Compiler** + +3. **Compile the installer:** + ```powershell + .\build\scripts\build-installer.ps1 + ``` + + For a release installer, first check out the exact version tag and then run: + ```powershell + .\build\scripts\build-installer.ps1 -Release + ``` + +4. **Output:** + - Installer will be created in `build/dist/installer/` + +## Customization + +Edit `build/installer/musicalc.iss` to change: +- `MyAppPublisher` - Your name/company +- `MyAppURL` - Your website/repository URL +- `AppId` - Unique GUID (keep as-is or generate new one) + +## Testing + +1. Run the generated installer from `build/dist/installer/` +2. Install to default location or custom directory +3. Verify desktop shortcut (if selected) +4. Test the application runs correctly +5. Uninstall via Windows Settings > Apps + +## Distribution + +The installer file is a single executable that can be distributed to users. It includes: +- Application executable +- App icon +- Start menu shortcuts +- Optional desktop shortcut +- Uninstaller + +# Creating Linux Packages and Tarballs + +GoReleaser automates the creation of `.deb`, `.rpm`, and `.tar.gz` packages for distribution. + +1. **Install GoReleaser** + ```bash + go install github.com/goreleaser/goreleaser/v2@latest + ``` + +2. **Install Package Dependencies** + See [Build Instructions](README-BUILD.md). + +3. **Building packages with GoReleaser** + + For Linux AMD64: + + ```bash + goreleaser check --config build/release/goreleaser-linux-amd64.yaml + # for snapshot + goreleaser release --snapshot --clean --config build/release/goreleaser-linux-amd64.yaml + # for release + goreleaser release --clean --config build/release/goreleaser-linux-amd64.yaml --skip=publish + ``` + + For Linux ARM64: + + ```bash + goreleaser check --config build/release/goreleaser-linux-arm64.yaml + # for snapshot + goreleaser release --snapshot --clean --config build/release/goreleaser-linux-arm64.yaml + # for release + goreleaser release --clean --config build/release/goreleaser-linux-arm64.yaml --skip=publish + ``` + + For Windows AMD64/ARM64: + ```bash + goreleaser check --config build/release/goreleaser-win-all.yaml + # for snapshot + goreleaser release --snapshot --clean --config build/release/goreleaser-win-all.yaml + # for release + goreleaser release --clean --config build/release/goreleaser-win-all.yaml --skip=publish + ``` + + The Windows ARM64 build uses Zig for cross-compilation. The GoReleaser config redirects Zig cache files to `build/.cache/zig-global` and `build/.cache/zig-local`. + + Snapshot builds create packages in the `build/dist/` directory without requiring an exact Git tag. Release builds must be run from an exact version tag. + +4. **Create a release (requires Git tag)** + ```bash + # Create and push tags manually; release tooling does not create or push tags. + git tag -a 0.1.0 -m "Release 0.1.0" + git push origin 0.1.0 + + # Validate the current exact tag and all GoReleaser configs. + python utils/release.py --check-only + + # Run one release config. Publishing is skipped unless --publish is supplied. + python utils/release.py --release --config build/release/goreleaser-linux-amd64.yaml + ``` + +**Generated artifacts**: +- `.tar.gz` archive with binary, install script, icon, and desktop entry +- `.deb` package for Debian/Ubuntu +- `.rpm` package for Fedora/RHEL diff --git a/README-TESTS.md b/build/docs/README-TESTS.md similarity index 100% rename from README-TESTS.md rename to build/docs/README-TESTS.md diff --git a/musicalc.iss b/build/installer/musicalc.iss similarity index 76% rename from musicalc.iss rename to build/installer/musicalc.iss index 6c22f5c..442e8e5 100644 --- a/musicalc.iss +++ b/build/installer/musicalc.iss @@ -2,7 +2,9 @@ ; Music Calculator Application - Universal Architecture Version #define MyAppName "MusiCalc" -#define MyAppVersion "0.8.6" +#ifndef MyAppVersion + #error MyAppVersion must be supplied by the build command, for example: ISCC.exe /DMyAppVersion=X.Y.Z build\installer\musicalc.iss +#endif #define MyAppPublisher "B. Zeiss" #define MyAppURL "https://github.com/bzeiss/musicalc" #define MyAppExeName "musicalc.exe" @@ -19,9 +21,9 @@ AppUpdatesURL={#MyAppURL} DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes ; PrivilegesRequired=lowest -OutputDir=installer +OutputDir=..\dist\installer OutputBaseFilename=musicalc-setup-{#MyAppVersion} -SetupIconFile=icons\appicon.ico +SetupIconFile=..\..\icons\appicon.ico Compression=lzma SolidCompression=yes WizardStyle=modern @@ -42,14 +44,14 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ [Files] ; 1. Install AMD64 version on x64 systems -Source: "dist\musicalc_x64.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}"; Check: IsX64; Flags: 64bit ignoreversion +Source: "..\dist\musicalc_x64.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}"; Check: IsX64; Flags: 64bit ignoreversion ; 2. Install ARM64 version on ARM64 systems -Source: "dist\musicalc_arm64.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}"; Check: IsArm64; Flags: 64bit ignoreversion +Source: "..\dist\musicalc_arm64.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}"; Check: IsArm64; Flags: 64bit ignoreversion ; Common files -Source: "icons\appicon.ico"; DestDir: "{app}"; Flags: ignoreversion -Source: "icons\appicon.png"; DestDir: "{app}\icons"; Flags: ignoreversion +Source: "..\..\icons\appicon.ico"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\..\icons\appicon.png"; DestDir: "{app}\icons"; Flags: ignoreversion [Icons] Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\appicon.ico" @@ -68,4 +70,4 @@ end; function IsX64: Boolean; begin Result := (ProcessorArchitecture = paX64); -end; \ No newline at end of file +end; diff --git a/musicalc-pkg.desktop b/build/packaging/musicalc-pkg.desktop similarity index 83% rename from musicalc-pkg.desktop rename to build/packaging/musicalc-pkg.desktop index f030e4c..7484dd6 100644 --- a/musicalc-pkg.desktop +++ b/build/packaging/musicalc-pkg.desktop @@ -6,4 +6,4 @@ Exec=/usr/bin/musicalc Icon=musicalc Terminal=false Categories=Utility;Audio; -StartupWMClass=MusiCalc v0.8.6 +StartupWMClass=com.musicalc diff --git a/musicalc.desktop b/build/packaging/musicalc.desktop similarity index 84% rename from musicalc.desktop rename to build/packaging/musicalc.desktop index b6ee087..d4d35d4 100644 --- a/musicalc.desktop +++ b/build/packaging/musicalc.desktop @@ -6,4 +6,4 @@ Exec=/usr/local/bin/musicalc Icon=musicalc Terminal=false Categories=Utility;Audio; -StartupWMClass=MusiCalc v0.8.6 +StartupWMClass=com.musicalc diff --git a/.goreleaser-linux-amd64.yaml b/build/release/goreleaser-linux-amd64.yaml similarity index 80% rename from .goreleaser-linux-amd64.yaml rename to build/release/goreleaser-linux-amd64.yaml index b12ff2f..d292511 100644 --- a/.goreleaser-linux-amd64.yaml +++ b/build/release/goreleaser-linux-amd64.yaml @@ -1,10 +1,6 @@ version: 2 project_name: musicalc - -before: - hooks: - - git pull && git checkout -- go.mod && git fetch --tags --force -# - go mod tidy +dist: build/dist builds: - id: linux-amd64 @@ -20,15 +16,17 @@ builds: - amd64 goamd64: [v3] ldflags: - - -s -w + - -s -w -X main.version={{ .Version }} # This creates the .tar.gz bundle archives: - files: - - src: "install.sh" + - src: "build/scripts/install.sh" + dst: "install.sh" - src: "icons/appicon.png" dst: "icon.png" - - src: "musicalc.desktop" + - src: "build/packaging/musicalc.desktop" + dst: "musicalc.desktop" # This creates the .deb and .rpm "Installers" nfpms: @@ -46,5 +44,5 @@ nfpms: dst: /usr/share/icons/hicolor/512x512/apps/musicalc.png # This creates the Start Menu entry automatically (uses /usr/bin path) - - src: musicalc-pkg.desktop + - src: build/packaging/musicalc-pkg.desktop dst: /usr/share/applications/musicalc.desktop diff --git a/.goreleaser-linux-arm64.yaml b/build/release/goreleaser-linux-arm64.yaml similarity index 81% rename from .goreleaser-linux-arm64.yaml rename to build/release/goreleaser-linux-arm64.yaml index ca10cd0..f8b69b0 100644 --- a/.goreleaser-linux-arm64.yaml +++ b/build/release/goreleaser-linux-arm64.yaml @@ -1,10 +1,6 @@ version: 2 project_name: musicalc - -before: - hooks: - - git pull && git checkout -- go.mod && git fetch --tags --force -# - go mod tidy +dist: build/dist builds: - id: linux-arm64 @@ -20,15 +16,17 @@ builds: goarch: - arm64 ldflags: - - -s -w + - -s -w -X main.version={{ .Version }} # This creates the .tar.gz bundle archives: - files: - - src: "install.sh" + - src: "build/scripts/install.sh" + dst: "install.sh" - src: "icons/appicon.png" dst: "icon.png" - - src: "musicalc.desktop" + - src: "build/packaging/musicalc.desktop" + dst: "musicalc.desktop" # This creates the .deb and .rpm "Installers" nfpms: @@ -46,5 +44,5 @@ nfpms: dst: /usr/share/icons/hicolor/512x512/apps/musicalc.png # This creates the Start Menu entry automatically (uses /usr/bin path) - - src: musicalc-pkg.desktop + - src: build/packaging/musicalc-pkg.desktop dst: /usr/share/applications/musicalc.desktop diff --git a/.goreleaser-win-all.yaml b/build/release/goreleaser-win-all.yaml similarity index 65% rename from .goreleaser-win-all.yaml rename to build/release/goreleaser-win-all.yaml index c39f416..1ae59d3 100644 --- a/.goreleaser-win-all.yaml +++ b/build/release/goreleaser-win-all.yaml @@ -1,24 +1,20 @@ version: 2 project_name: musicalc - -before: - hooks: - - git pull && git checkout -- go.mod && git fetch --tags --force -# - go mod tidy +dist: build/dist builds: - id: win-amd64 env: - CGO_ENABLED=1 - - CC=x86_64-w64-mingw32-gcc - - CXX=x86_64-w64-mingw32-g++ - - CGO_CFLAGS=-O3 -flto=auto -march=x86-64-v3 -m64 - - CGO_LDFLAGS=-O3 -flto=auto + - CC=clang --target=x86_64-w64-windows-gnu + - CXX=clang++ --target=x86_64-w64-windows-gnu + - CGO_CFLAGS=-O3 -march=x86-64-v3 + - CGO_LDFLAGS=-O3 goos: [windows] goarch: [amd64] goamd64: [v3] ldflags: - - -s -w -H=windowsgui + - -s -w -H=windowsgui -extld=x86_64-w64-mingw32-gcc -X main.version={{ .Version }} binary: musicalc_x64 # This keeps the binary at the top level of the dist folder no_unique_dist_dir: true @@ -28,12 +24,14 @@ builds: - CGO_ENABLED=1 - CC=zig cc -target aarch64-windows-gnu - CXX=zig c++ -target aarch64-windows-gnu + - ZIG_GLOBAL_CACHE_DIR=build/.cache/zig-global + - ZIG_LOCAL_CACHE_DIR=build/.cache/zig-local - CGO_CFLAGS=-O3 -mcpu=oryon_1 -fomit-frame-pointer - CGO_LDFLAGS=-O3 goos: [windows] goarch: [arm64] ldflags: - - -s -w -H=windowsgui + - -s -w -H=windowsgui -X main.version={{ .Version }} binary: musicalc_arm64 no_unique_dist_dir: true @@ -44,4 +42,4 @@ builds: # name_template: "{{ .Binary }}" # # This prevents GoReleaser from adding default README/LICENSE files # files: -# - none* \ No newline at end of file +# - none* diff --git a/build/scripts/build-installer.ps1 b/build/scripts/build-installer.ps1 new file mode 100644 index 0000000..6a76570 --- /dev/null +++ b/build/scripts/build-installer.ps1 @@ -0,0 +1,42 @@ +param( + [switch]$Release +) + +$ErrorActionPreference = "Stop" + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$projectRoot = Resolve-Path (Join-Path $scriptDir "..\..") +$installerScript = Join-Path $projectRoot "build\installer\musicalc.iss" + +Push-Location $projectRoot +try { + $version = git describe --tags --exact-match 2>$null + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { + if ($Release) { + throw "Release installer builds require HEAD to be exactly on a version tag." + } + + $latestTag = git describe --tags --abbrev=0 2>$null + $shortCommit = git rev-parse --short HEAD + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($shortCommit)) { + $version = "dev" + } elseif ([string]::IsNullOrWhiteSpace($latestTag)) { + $version = "dev-$shortCommit" + } else { + $version = "$latestTag-dev-$shortCommit" + } + } + + if ($Release -and ($version -notmatch '^\d+\.\d+\.\d+$')) { + throw "Release tag '$version' is invalid. Expected MAJOR.MINOR.PATCH, for example 0.8.7." + } + + iscc "/DMyAppVersion=$version" $installerScript + $exitCode = $LASTEXITCODE +} finally { + Pop-Location +} + +if ($exitCode -ne 0) { + exit $exitCode +} diff --git a/build/scripts/build-win.ps1 b/build/scripts/build-win.ps1 new file mode 100644 index 0000000..f121bb1 --- /dev/null +++ b/build/scripts/build-win.ps1 @@ -0,0 +1,38 @@ +$ErrorActionPreference = "Stop" + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$projectRoot = Resolve-Path (Join-Path $scriptDir "..\..") +$distDir = Join-Path $projectRoot "build\dist" + +New-Item -ItemType Directory -Force $distDir | Out-Null + +$env:CGO_ENABLED = "1" +$env:CC = "clang --target=x86_64-w64-windows-gnu" +$env:CXX = "clang++ --target=x86_64-w64-windows-gnu" +$env:CGO_CFLAGS = "-O3 -march=x86-64-v3" +$env:CGO_LDFLAGS = "-O3" + +Push-Location $projectRoot +try { + $version = git describe --tags --exact-match 2>$null + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { + $latestTag = git describe --tags --abbrev=0 2>$null + $shortCommit = git rev-parse --short HEAD + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($shortCommit)) { + $version = "dev" + } elseif ([string]::IsNullOrWhiteSpace($latestTag)) { + $version = "dev-$shortCommit" + } else { + $version = "$latestTag-dev-$shortCommit" + } + } + + go build -ldflags="-s -w -H=windowsgui -extld=x86_64-w64-mingw32-gcc -X main.version=$version" -o (Join-Path $distDir "musicalc.exe") + $exitCode = $LASTEXITCODE +} finally { + Pop-Location +} + +if ($exitCode -ne 0) { + exit $exitCode +} diff --git a/install.sh b/build/scripts/install.sh old mode 100755 new mode 100644 similarity index 100% rename from install.sh rename to build/scripts/install.sh diff --git a/go.mod b/go.mod index c5981fb..221a341 100644 --- a/go.mod +++ b/go.mod @@ -1,50 +1,49 @@ module musicalc -go 1.24.0 - -toolchain go1.24.5 +go 1.26.2 require ( - fyne.io/fyne/v2 v2.7.1 + fyne.io/fyne/v2 v2.7.3 github.com/chinenual/go-scala v1.2.0 github.com/faiface/beep v1.1.0 ) require ( - fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 // indirect - github.com/BurntSushi/toml v1.5.0 // indirect + fyne.io/systray v1.12.1 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fredbi/uri v1.1.1 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fsnotify/fsnotify v1.10.0 // indirect github.com/fyne-io/gl-js v0.2.0 // indirect github.com/fyne-io/glfw-js v0.3.0 // indirect github.com/fyne-io/image v0.1.1 // indirect github.com/fyne-io/oksvg v0.2.0 // indirect - github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect - github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect - github.com/go-text/render v0.2.0 // indirect - github.com/go-text/typesetting v0.2.1 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20260406072232-3ac4aa2bb164 // indirect + github.com/go-text/render v0.2.1 // indirect + github.com/go-text/typesetting v0.3.4 // indirect + github.com/godbus/dbus/v5 v5.2.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/hack-pad/go-indexeddb v0.3.2 // indirect - github.com/hack-pad/safejs v0.1.0 // indirect - github.com/hajimehoshi/oto v0.7.1 // indirect + github.com/hack-pad/safejs v0.1.1 // indirect + github.com/hajimehoshi/oto v1.0.2 // indirect github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect github.com/kr/text v0.2.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect - github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect + github.com/nicksnyder/go-i18n/v2 v2.6.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rymdport/portal v0.4.2 // indirect github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/stretchr/testify v1.11.1 // indirect - github.com/yuin/goldmark v1.7.8 // indirect - golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect - golang.org/x/image v0.24.0 // indirect - golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect + github.com/yuin/goldmark v1.8.2 // indirect + golang.org/x/exp/shiny v0.0.0-20260410095643-746e56fc9e2f // indirect + golang.org/x/image v0.39.0 // indirect + golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d482c0a..ddd91e7 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ -fyne.io/fyne/v2 v2.7.1 h1:ja7rNHWWEooha4XBIZNnPP8tVFwmTfwMJdpZmLxm2Zc= -fyne.io/fyne/v2 v2.7.1/go.mod h1:xClVlrhxl7D+LT+BWYmcrW4Nf+dJTvkhnPgji7spAwE= -fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 h1:eA5/u2XRd8OUkoMqEv3IBlFYSruNlXD8bRHDiqm0VNI= -fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +fyne.io/fyne/v2 v2.7.3 h1:xBT/iYbdnNHONWO38fZMBrVBiJG8rV/Jypmy4tVfRWE= +fyne.io/fyne/v2 v2.7.3/go.mod h1:gu+dlIcZWSzKZmnrY8Fbnj2Hirabv2ek+AKsfQ2bBlw= +fyne.io/systray v1.12.1 h1:ygBD6aZXwiOmZoY5N+ukbH9pih0Kq6fYgVeMYbr5skQ= +fyne.io/systray v1.12.1/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/chinenual/go-scala v1.2.0 h1:FA9qlw2eJQA+jyWgJH9oQm+mGwewN93387pm/kuy3rM= github.com/chinenual/go-scala v1.2.0/go.mod h1:Uc09g2xLaUHqx3sK+1h3E0R3hG+ZFMNkVVDtEJhi7F0= @@ -17,8 +17,8 @@ github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko= github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.10.0 h1:Xx/5Ydg9CeBDX/wi4VJqStNtohYjitZhhlHt4h3St1M= +github.com/fsnotify/fsnotify v1.10.0/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs= github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI= github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk= @@ -32,30 +32,32 @@ github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebK github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE= -github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA= -github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc= -github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU= -github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8= -github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M= -github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= -github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 h1:IO5P06Pcj9K04d+l4nrf3c2U56+dAotIFG6u4P1wAHI= +github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20260406072232-3ac4aa2bb164 h1:ddtotcEXIT7dFhSeXIWjK8YMFYl5bB2NdYQwgv6Uqjk= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20260406072232-3ac4aa2bb164/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc= +github.com/go-text/render v0.2.1 h1:qwHhxqGUjjg4L0XyJWj7M7bpY75NZM+kBpv2Yfw5mcg= +github.com/go-text/render v0.2.1/go.mod h1:HCCAq8MUlm/WRcXshBb4K/n+IkjeXQ1c2Ba+yICSm0A= +github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU= +github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY= +github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3 h1:drBZzMgdYPbmyXqOto4YhhJGrFIQCX94FpR4MzTCsos= +github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A= github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0= -github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8= -github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= +github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8= +github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= -github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk= github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos= +github.com/hajimehoshi/oto v1.0.2 h1:NxUZoQkV4UgbJobPIT/pVTcT/qpkd3YqEGRzPtk7Rc8= +github.com/hajimehoshi/oto v1.0.2/go.mod h1:AARGdOaQIhMJ1fhKu7nMzEesM2/mE4KZ8A1K1VZozuQ= github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A= github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA= github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE= @@ -72,8 +74,8 @@ github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= -github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= +github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= +github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -92,32 +94,35 @@ github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqd github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= -github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= +github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp/shiny v0.0.0-20260410095643-746e56fc9e2f h1:CMCUocbbREagqundn9s7nFTY3lrmw+Pmi90x2nrbw+g= +golang.org/x/exp/shiny v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:75UwHX2ZPO3acaGFaP5bhU5yJd6CzUV5v4rX5CiE9ag= golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= -golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= +golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww= +golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg= -golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc= +golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b h1:Qt2eaXcZ8x20iAcoZ6AceeMMtnjuPHvC51KRCH1DKSQ= +golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b/go.mod h1:5Fu78lew5ucMXt8w2KYcwvxu2rkC/liHzUvaoiI+H/M= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/ui/timecode.go b/internal/ui/timecode.go index 121b934..4da6858 100644 --- a/internal/ui/timecode.go +++ b/internal/ui/timecode.go @@ -108,6 +108,13 @@ func NewTimecodeTab() fyne.CanvasObject { resultTC, maxWidth, resultFrames, fpsLabel) } + pushHistory := func(entry string) { + historyList = append([]string{entry}, historyList...) + historyText.SetText(strings.Join(historyList, "\n\n")) + historyText.Refresh() + historyScroll.ScrollToTop() + } + // Track previous FPS for conversion history var previousFPS string previousFPS = "30 fps" @@ -156,10 +163,7 @@ func NewTimecodeTab() fyne.CanvasObject { conversionEntry := fmt.Sprintf(" %s (%*df) @%s\n= %s (%*df) @%s", oldTC, maxWidth, oldFrames, oldFpsLabel, newTC, maxWidth, newFrames, newFpsLabel) - historyList = append(historyList, conversionEntry) - historyText.SetText(strings.Join(historyList, "\n\n")) - historyText.Refresh() - historyScroll.ScrollToBottom() + pushHistory(conversionEntry) // Timecode H:M:S:F values don't change, only recalculate display with new FPS // No need to update entry fields since H:M:S:F stay the same @@ -189,10 +193,7 @@ func NewTimecodeTab() fyne.CanvasObject { fpsLabel := strings.Split(fpsSelect.Selected, " ")[0] historyEntry := formatHistoryEntry(tc1, frames1, tc2, frames2, result.Timecode, result.TotalFrames, fpsLabel, "+") - historyList = append(historyList, historyEntry) - historyText.SetText(strings.Join(historyList, "\n\n")) - historyText.Refresh() - historyScroll.ScrollToBottom() + pushHistory(historyEntry) // Update first timecode with result and reset second timecode updating = true @@ -223,10 +224,7 @@ func NewTimecodeTab() fyne.CanvasObject { fpsLabel := strings.Split(fpsSelect.Selected, " ")[0] historyEntry := formatHistoryEntry(tc1, frames1, tc2, frames2, result.Timecode, result.TotalFrames, fpsLabel, "-") - historyList = append(historyList, historyEntry) - historyText.SetText(strings.Join(historyList, "\n\n")) - historyText.Refresh() - historyScroll.ScrollToBottom() + pushHistory(historyEntry) // Update first timecode with result and reset second timecode updating = true diff --git a/internal/ui/widgets/timecode_entry.go b/internal/ui/widgets/timecode_entry.go index ac4fc09..25a3347 100644 --- a/internal/ui/widgets/timecode_entry.go +++ b/internal/ui/widgets/timecode_entry.go @@ -3,6 +3,7 @@ package widgets import ( "fmt" "strconv" + "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/driver/mobile" @@ -15,6 +16,8 @@ type TimecodeEntry struct { OnComplete func() // Called when all digits are entered OnOperationKey func(key fyne.KeyName) // Called when +/- keys are pressed fields []string // Field values [frames, seconds, minutes, hours] - right to left + lastOpKey fyne.KeyName + lastOpAt time.Time } func NewTimecodeEntry(threeDigits bool) *TimecodeEntry { @@ -41,15 +44,32 @@ func (e *TimecodeEntry) Keyboard() mobile.KeyboardType { return mobile.NumberKeyboard } +func (e *TimecodeEntry) shouldHandleOperationKey(key fyne.KeyName) bool { + // Some platforms deliver both TypedRune ('-' / '+') and TypedKey (KeyMinus/KeyEqual) + // for a single keypress. De-dupe within a short window. + if e.lastOpKey == key { + if !e.lastOpAt.IsZero() && time.Since(e.lastOpAt) < 50*time.Millisecond { + return false + } + } + e.lastOpKey = key + e.lastOpAt = time.Now() + return true +} + // TypedRune handles right-justified timecode entry with dot as field separator func (e *TimecodeEntry) TypedRune(r rune) { // Handle +/- as operation keys if r == '+' || r == '-' { if e.OnOperationKey != nil { if r == '+' { - e.OnOperationKey(fyne.KeyEqual) + if e.shouldHandleOperationKey(fyne.KeyEqual) { + e.OnOperationKey(fyne.KeyEqual) + } } else { - e.OnOperationKey(fyne.KeyMinus) + if e.shouldHandleOperationKey(fyne.KeyMinus) { + e.OnOperationKey(fyne.KeyMinus) + } } } return @@ -96,7 +116,9 @@ func (e *TimecodeEntry) TypedKey(k *fyne.KeyEvent) { case fyne.KeyEqual, fyne.KeyMinus: // Handle +/- operation keys if e.OnOperationKey != nil { - e.OnOperationKey(k.Name) + if e.shouldHandleOperationKey(k.Name) { + e.OnOperationKey(k.Name) + } } return case fyne.KeyPeriod, fyne.KeyComma: diff --git a/main.go b/main.go index 0f8393b..f41c607 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - _ "embed" "musicalc/internal/ui" "strings" @@ -12,8 +11,7 @@ import ( "fyne.io/fyne/v2/widget" ) -//go:embed VERSION -var version string +var version = "dev" // CategoryInfo represents a category with tab indices type CategoryInfo struct { @@ -54,7 +52,7 @@ func main() { "note2freq": "Note to Frequency", "freq2note": "Frequency to Note", "samplelength": "Sample Length", - "alignment": "Multi-Mic Alignment Delay", + "alignment": "Alignment Delay", } var switchCategory func(int) @@ -109,7 +107,7 @@ func main() { {Name: "Time & Tempo", TabIndices: []int{0, 1, 2}}, {Name: "Frequency & Pitch", TabIndices: []int{3, 4}}, {Name: "Analysis", TabIndices: []int{5}}, - {Name: "Multi Mic", TabIndices: []int{6}}, + {Name: "Multi-Mic", TabIndices: []int{6}}, } // Tab heading keys for each global tab index diff --git a/utils/release.py b/utils/release.py index 497c6e0..270539b 100644 --- a/utils/release.py +++ b/utils/release.py @@ -1,254 +1,141 @@ #!/usr/bin/env python3 """ -Release script for MusiCalc -Interactive release workflow with version management and git automation +Release helper for MusiCalc. + +Git tags are the only release version source. This script validates the +current tag and runs GoReleaser without creating commits, tags, or pushes. """ -import sys -import os +import argparse import re import subprocess +import sys from pathlib import Path -def find_project_root(): - """Find project root by looking for go.mod file""" +DEFAULT_CONFIGS = [ + Path("build/release/goreleaser-linux-amd64.yaml"), + Path("build/release/goreleaser-linux-arm64.yaml"), + Path("build/release/goreleaser-win-all.yaml"), +] + +VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$") + + +def find_project_root() -> Path: current = Path.cwd().resolve() - - # Check current directory and all parents for directory in [current] + list(current.parents): if (directory / "go.mod").exists(): return directory - - # If not found, check if we're in a subdirectory with ../go.mod + script_dir = Path(__file__).parent.resolve() for directory in [script_dir] + list(script_dir.parents): if (directory / "go.mod").exists(): return directory - - print("✗ Error: Could not find project root (go.mod not found)") - sys.exit(1) - - -def read_current_version(project_root): - """Read current version from VERSION file""" - version_file = project_root / "VERSION" - if version_file.exists(): - return version_file.read_text().strip() - return None - - -def update_version_file(project_root, new_version): - """Update VERSION file""" - version_file = project_root / "VERSION" - version_file.write_text(new_version + "\n") - print(f"✓ Updated VERSION: {new_version}") - - -def update_inno_setup(project_root, new_version): - """Update version in musicalc.iss Inno Setup script""" - iss_file = project_root / "musicalc.iss" - - if not iss_file.exists(): - print(f"✗ Error: {iss_file} not found") - return False - - content = iss_file.read_text(encoding='utf-8') - - # Update #define MyAppVersion - pattern = r'(#define MyAppVersion\s+")[^"]+(")' - - # Check if pattern exists in file - if not re.search(pattern, content): - print(f"✗ Error: Could not find version pattern in {iss_file}") - return False - - replacement = r'\g<1>' + new_version + r'\g<2>' - new_content = re.sub(pattern, replacement, content) - - # Only write if content changed - if new_content != content: - iss_file.write_text(new_content, encoding='utf-8') - print(f"✓ Updated musicalc.iss: {new_version}") - else: - print(f"✓ musicalc.iss already at version: {new_version}") - - return True - - -def update_desktop_files(project_root, new_version): - """Update version in .desktop files (StartupWMClass field)""" - desktop_files = [ - project_root / "musicalc.desktop", - project_root / "musicalc-pkg.desktop" - ] - - success = True - for desktop_file in desktop_files: - if not desktop_file.exists(): - print(f"⚠ Warning: {desktop_file.name} not found, skipping") - continue - - content = desktop_file.read_text(encoding='utf-8') - - # Update StartupWMClass=MusiCalc v0.8.4 - pattern = r'(StartupWMClass=MusiCalc v)[0-9.]+' - - # Check if pattern exists in file - if not re.search(pattern, content): - print(f"✗ Error: Could not find StartupWMClass pattern in {desktop_file.name}") - success = False - continue - - replacement = r'\g<1>' + new_version - new_content = re.sub(pattern, replacement, content) - - # Only write if content changed - if new_content != content: - desktop_file.write_text(new_content, encoding='utf-8') - print(f"✓ Updated {desktop_file.name}: {new_version}") - else: - print(f"✓ {desktop_file.name} already at version: {new_version}") - - return success - - -def validate_version(version_str): - """Validate version string format (e.g., 0.8.3)""" - pattern = r'^\d+\.\d+\.\d+$' - return re.match(pattern, version_str) is not None - - -def run_command(cmd, cwd, description): - """Run a shell command and return success status""" - print(f"\n→ {description}...") - try: - result = subprocess.run( - cmd, - cwd=cwd, - shell=True, - check=True, - capture_output=True, - text=True + + raise SystemExit("Error: could not find project root (go.mod not found)") + + +def run(args: list[str], cwd: Path, description: str, capture: bool = False) -> str: + print(f"\n{description}...") + result = subprocess.run( + args, + cwd=cwd, + check=False, + capture_output=capture, + text=True, + ) + if result.returncode != 0: + if capture and result.stderr: + print(result.stderr.strip()) + raise SystemExit(f"Error: {description} failed") + if capture: + return result.stdout.strip() + return "" + + +def exact_version_tag(project_root: Path) -> str: + tag = run( + ["git", "describe", "--tags", "--exact-match"], + project_root, + "Reading exact Git tag", + capture=True, + ) + if not VERSION_RE.match(tag): + raise SystemExit( + f"Error: release tag '{tag}' is invalid; expected MAJOR.MINOR.PATCH, for example 0.8.7" + ) + return tag + + +def check_configs(project_root: Path, configs: list[Path]) -> None: + for config in configs: + run( + ["goreleaser", "check", "--config", str(config)], + project_root, + f"Checking {config}", ) - if result.stdout: - print(result.stdout.strip()) - print(f"✓ {description} completed") - return True - except subprocess.CalledProcessError as e: - print(f"✗ Error: {description} failed") - if e.stderr: - print(e.stderr.strip()) - return False - - -def main(): - # Find project root + + +def release_configs(project_root: Path, configs: list[Path], skip_publish: bool) -> None: + for config in configs: + command = ["goreleaser", "release", "--clean", "--config", str(config)] + if skip_publish: + command.append("--skip=publish") + run(command, project_root, f"Running GoReleaser for {config}") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Validate and run a tag-based MusiCalc release." + ) + parser.add_argument( + "--config", + action="append", + type=Path, + dest="configs", + help="GoReleaser config to use. May be specified multiple times for --check-only; release mode requires exactly one.", + ) + parser.add_argument( + "--check-only", + action="store_true", + help="Only validate the current tag and GoReleaser configs. This is the default unless --release is set.", + ) + parser.add_argument( + "--release", + action="store_true", + help="Run GoReleaser after validation. Requires exactly one --config.", + ) + parser.add_argument( + "--publish", + action="store_true", + help="Allow GoReleaser to publish. By default, publishing is skipped.", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() project_root = find_project_root() - print(f"Project root: {project_root}") - print() - - # Read current version - current_version = read_current_version(project_root) - if not current_version: - print("✗ Error: VERSION file not found or empty") - sys.exit(1) - - print(f"Current version: {current_version}") - print() - - # Ask if user wants to keep or change version - response = input(f"Keep version {current_version}? [Y/n]: ").strip().lower() - - new_version = current_version - if response == 'n': - while True: - new_version = input("Enter new version (format: xx.yy.zz): ").strip() - if validate_version(new_version): - break - print("✗ Invalid version format. Please use format like 0.8.4") - - # Update VERSION file - update_version_file(project_root, new_version) - else: - print(f"Using version: {new_version}") - - print() - - # Run go mod tidy - if not run_command("go mod tidy", project_root, "Running go mod tidy"): - sys.exit(1) - - # Update musicalc.iss - print() - if not update_inno_setup(project_root, new_version): - sys.exit(1) - - # Update .desktop files - print() - if not update_desktop_files(project_root, new_version): - sys.exit(1) - - # Ask for commit message - print() - commit_message = input("Enter release commit message: ").strip() - if not commit_message: - commit_message = f"Release v{new_version}" - - # Git workflow - print() - print("═" * 50) - print("Git Workflow") - print("═" * 50) - - # Stage files - if not run_command("git add VERSION musicalc.iss musicalc.desktop musicalc-pkg.desktop go.mod go.sum", project_root, "Staging files"): - sys.exit(1) - - # Check if there are changes to commit - try: - result = subprocess.run( - "git diff --cached --quiet", - cwd=project_root, - shell=True + configs = args.configs or DEFAULT_CONFIGS + + tag = exact_version_tag(project_root) + print(f"Release version: {tag}") + print("Git tags are user-managed; this script will not create or push tags.") + + check_configs(project_root, configs) + + if args.check_only or not args.release: + print("\nValidation complete.") + return + + if len(configs) != 1: + raise SystemExit( + "Error: release mode requires exactly one --config. Use --check-only to validate all configs." ) - has_changes = result.returncode != 0 - except subprocess.CalledProcessError: - has_changes = True - - # Commit and push if there are changes - if has_changes: - if not run_command(f'git commit -m "{commit_message}"', project_root, "Committing changes"): - sys.exit(1) - - # Push commits - if not run_command("git push", project_root, "Pushing commits to origin"): - sys.exit(1) - else: - print("\n→ No changes to commit (version unchanged)") - print("✓ Skipping commit step") - - # Create and push tag - tag_name = f"{new_version}" - if not run_command(f'git tag -a {tag_name} -m "Release {tag_name}"', project_root, f"Creating tag {tag_name}"): - sys.exit(1) - - if not run_command(f"git push origin {tag_name}", project_root, f"Pushing tag {tag_name}"): - sys.exit(1) - - # Success summary - print() - print("═" * 50) - print("✓ Release Complete!") - print("═" * 50) - print(f"Version: {new_version}") - print(f"Tag: {tag_name}") - print(f"Commit: {commit_message}") - print() - print("Next steps:") - print(" 1. Build the application: go build -ldflags=\"-s -w -H=windowsgui\" -o musicalc.exe") - print(" 2. Test the application") - print(" 3. Create installer (if using Inno Setup)") + + release_configs(project_root, configs, skip_publish=not args.publish) + print("\nRelease commands completed.") if __name__ == "__main__":