Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build Qt Terminal - All Platforms | |
| on: | |
| workflow_dispatch: | |
| push: | |
| tags: | |
| - 'v*' | |
| env: | |
| BUILD_TYPE: Release | |
| QT_VERSION: "6.8.3" | |
| QT_MODULES: "qtcharts qtwebsockets qtmultimedia" | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| jobs: | |
| build: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # ---- Windows x64 ---- | |
| - os: windows-2022 | |
| arch: x64 | |
| platform: Windows | |
| artifact: FinceptTerminal-Windows-x64 | |
| exe: FinceptTerminal.exe | |
| qt_arch: win64_msvc2022_64 | |
| cmake_extra: "" | |
| # ---- Windows ARM64 ---- | |
| - os: windows-2022 | |
| arch: arm64 | |
| platform: Windows | |
| artifact: FinceptTerminal-Windows-arm64 | |
| exe: FinceptTerminal.exe | |
| qt_arch: win64_msvc2022_arm64_cross_compiled | |
| cmake_extra: "-A ARM64" | |
| # ---- Linux x64 ---- | |
| - os: ubuntu-22.04 | |
| arch: x64 | |
| platform: Linux | |
| artifact: FinceptTerminal-Linux-x64 | |
| exe: FinceptTerminal | |
| qt_arch: "" | |
| cmake_extra: "" | |
| # ---- macOS ARM64 (Apple Silicon) ---- | |
| - os: macos-15 | |
| arch: arm64 | |
| platform: macOS | |
| artifact: FinceptTerminal-macOS-arm64 | |
| exe: FinceptTerminal | |
| qt_arch: clang_64 | |
| cmake_extra: "-DCMAKE_OSX_ARCHITECTURES=arm64" | |
| # ---- macOS x64 (Intel) ---- | |
| # Cross-compiled on ARM64 runner. CMakeLists.txt auto-disables GGML_NATIVE | |
| # and GGML_METAL when CMAKE_OSX_ARCHITECTURES != host processor. | |
| - os: macos-15 | |
| arch: x64 | |
| platform: macOS | |
| artifact: FinceptTerminal-macOS-x64 | |
| exe: FinceptTerminal | |
| qt_arch: clang_64 | |
| cmake_extra: "-DCMAKE_OSX_ARCHITECTURES=x86_64" | |
| runs-on: ${{ matrix.os }} | |
| name: ${{ matrix.platform }} ${{ matrix.arch }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| # ---- ccache (macOS / Linux) ---- | |
| - name: Set up ccache | |
| if: runner.os != 'Windows' | |
| uses: hendrikmuhs/ccache-action@v1 | |
| with: | |
| key: ${{ matrix.platform }}-${{ matrix.arch }}-${{ env.QT_VERSION }} | |
| max-size: 500M | |
| update-package-index: true | |
| # ---- System dependencies (Linux) ---- | |
| # Ubuntu 22.04's default g++ is 11.4, but CMakeLists.txt requires GCC 12.3+. | |
| # Install gcc-12/g++-12 from the Ubuntu Toolchain PPA (pre-enabled on | |
| # ubuntu-22.04 runners) and pin it via CC/CXX for the rest of the job. | |
| - name: Install system dependencies (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo add-apt-repository universe -y | |
| sudo apt-get update -qq | |
| sudo apt-get install -y \ | |
| cmake ninja-build gcc-12 g++-12 \ | |
| libssl-dev \ | |
| qt6-base-dev qt6-base-private-dev \ | |
| qt6-tools-dev qt6-tools-dev-tools \ | |
| libqt6charts6-dev libqt6websockets6-dev \ | |
| qt6-multimedia-dev libqt6multimediawidgets6 \ | |
| libqt6svg6-dev libqt6concurrent6 \ | |
| libgl1-mesa-dev libglu1-mesa-dev \ | |
| libxkbcommon-dev libxkbcommon-x11-0 \ | |
| libxcb-cursor0 libxcb-icccm4 libxcb-image0 \ | |
| libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 \ | |
| libxcb-xinerama0 libxcb-xkb1 \ | |
| pkg-config zip | |
| echo "CC=gcc-12" >> "$GITHUB_ENV" | |
| echo "CXX=g++-12" >> "$GITHUB_ENV" | |
| # ---- OpenSSL dev libs (Windows) ---- | |
| # windows-2022 runners ship only the "Light" OpenSSL (no .lib files). | |
| # vcpkg is pre-installed on every windows-2022 runner at C:\vcpkg. | |
| - name: Install OpenSSL (Windows) | |
| if: runner.os == 'Windows' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| VCPKG_ROOT="${VCPKG_INSTALLATION_ROOT:-C:/vcpkg}" | |
| echo "Using vcpkg at: $VCPKG_ROOT" | |
| "$VCPKG_ROOT/vcpkg" install openssl:x64-windows --no-print-usage | |
| OPENSSL_DIR="$VCPKG_ROOT/installed/x64-windows" | |
| echo "OPENSSL_ROOT_DIR=$OPENSSL_DIR" >> "$GITHUB_ENV" | |
| echo "OpenSSL installed at: $OPENSSL_DIR" | |
| ls "$OPENSSL_DIR/lib/libcrypto.lib" && echo "libcrypto.lib confirmed" | |
| # ---- Qt6 target (Windows/macOS) ---- | |
| - name: Install Qt6 | |
| if: runner.os != 'Linux' | |
| uses: jurplel/install-qt-action@v4 | |
| with: | |
| version: ${{ env.QT_VERSION }} | |
| arch: ${{ matrix.qt_arch }} | |
| modules: ${{ env.QT_MODULES }} | |
| cache: true | |
| cache-key-prefix: qt-${{ matrix.platform }}-${{ matrix.arch }} | |
| # ---- Qt6 x64 host tools for Windows ARM64 cross-compilation ---- | |
| # moc/uic/rcc must run on the host (x64), not the target (arm64) | |
| - name: Install Qt6 host tools (Windows ARM64 only) | |
| if: runner.os == 'Windows' && matrix.arch == 'arm64' | |
| uses: jurplel/install-qt-action@v4 | |
| with: | |
| version: ${{ env.QT_VERSION }} | |
| arch: win64_msvc2022_64 | |
| modules: ${{ env.QT_MODULES }} | |
| cache: true | |
| set-env: false | |
| dir: "${{ runner.temp }}/qt-host" | |
| # ---- Configure CMake ---- | |
| - name: Configure CMake (Linux) | |
| if: runner.os == 'Linux' | |
| working-directory: fincept-qt | |
| run: | | |
| cmake -B build -G Ninja \ | |
| -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} \ | |
| -DCMAKE_C_COMPILER_LAUNCHER=ccache \ | |
| -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ | |
| ${{ matrix.cmake_extra }} | |
| - name: Configure CMake (Windows x64 / macOS x64 / macOS arm64) | |
| if: (runner.os == 'Windows' && matrix.arch == 'x64') || runner.os == 'macOS' | |
| shell: bash | |
| working-directory: fincept-qt | |
| run: | | |
| OPENSSL_ARG="" | |
| if [ -n "${OPENSSL_ROOT_DIR:-}" ]; then | |
| OPENSSL_ARG="-DOPENSSL_ROOT_DIR=${OPENSSL_ROOT_DIR}" | |
| fi | |
| cmake -B build \ | |
| -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} \ | |
| -DCMAKE_PREFIX_PATH="${QT_ROOT_DIR}" \ | |
| ${OPENSSL_ARG} \ | |
| ${{ runner.os == 'macOS' && '-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache' || '' }} \ | |
| ${{ matrix.cmake_extra }} | |
| - name: Configure CMake (Windows ARM64) | |
| if: runner.os == 'Windows' && matrix.arch == 'arm64' | |
| shell: bash | |
| working-directory: fincept-qt | |
| run: | | |
| HOST_QT="${{ runner.temp }}/qt-host/Qt/${{ env.QT_VERSION }}/msvc2022_64" | |
| OPENSSL_ARG="" | |
| if [ -n "${OPENSSL_ROOT_DIR:-}" ]; then | |
| OPENSSL_ARG="-DOPENSSL_ROOT_DIR=${OPENSSL_ROOT_DIR}" | |
| fi | |
| cmake -B build \ | |
| -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} \ | |
| -DCMAKE_PREFIX_PATH="${QT_ROOT_DIR}" \ | |
| -DQT_HOST_PATH="${HOST_QT}" \ | |
| -T ClangCL \ | |
| ${OPENSSL_ARG} \ | |
| ${{ matrix.cmake_extra }} | |
| # ---- Build ---- | |
| - name: Build | |
| shell: bash | |
| working-directory: fincept-qt | |
| run: cmake --build build --config ${{ env.BUILD_TYPE }} --parallel $(nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || echo 3) | |
| # ---- Verify binary ---- | |
| - name: Verify build output | |
| shell: bash | |
| working-directory: fincept-qt | |
| run: | | |
| if [ "${{ runner.os }}" == "Windows" ]; then | |
| BINARY="build/Release/${{ matrix.exe }}" | |
| elif [ "${{ runner.os }}" == "macOS" ]; then | |
| # Qt on macOS produces a .app bundle; the binary is inside it. | |
| # Try both plain binary and .app bundle locations. | |
| if [ -f "build/${{ matrix.exe }}" ]; then | |
| BINARY="build/${{ matrix.exe }}" | |
| elif [ -f "build/${{ matrix.exe }}.app/Contents/MacOS/${{ matrix.exe }}" ]; then | |
| BINARY="build/${{ matrix.exe }}.app/Contents/MacOS/${{ matrix.exe }}" | |
| else | |
| BINARY="build/${{ matrix.exe }}" # will fail the check below with a clear error | |
| fi | |
| else | |
| BINARY="build/${{ matrix.exe }}" | |
| fi | |
| if [ ! -f "$BINARY" ]; then | |
| echo "::error::Binary not found — searched: build/${{ matrix.exe }} and build/${{ matrix.exe }}.app/Contents/MacOS/${{ matrix.exe }}" | |
| echo "Build directory contents:" | |
| find build -maxdepth 3 -name "${{ matrix.exe }}" 2>/dev/null || true | |
| exit 1 | |
| fi | |
| echo "Binary verified: $BINARY ($(du -sh "$BINARY" | cut -f1))" | |
| # ---- Package: Windows (windeployqt + manual DLL sweep) ---- | |
| - name: Package (Windows) | |
| if: runner.os == 'Windows' | |
| shell: bash | |
| working-directory: fincept-qt | |
| run: | | |
| set -euo pipefail | |
| mkdir -p dist | |
| # Copy main executable | |
| cp build/Release/${{ matrix.exe }} dist/ | |
| # Copy qgeoview.dll (built by FetchContent, placed next to exe by post-build command) | |
| if [ -f "build/Release/qgeoview.dll" ]; then | |
| cp build/Release/qgeoview.dll dist/ | |
| echo "Copied qgeoview.dll" | |
| else | |
| echo "::warning::qgeoview.dll not found in build/Release — map widget may fail" | |
| fi | |
| # ---------------------------------------------------------------- | |
| # windeployqt | |
| # Always an x64 host tool. For ARM64 cross-compiled builds | |
| # QT_ROOT_DIR points to the ARM64 kit; windeployqt lives in x64. | |
| # ---------------------------------------------------------------- | |
| WDQT="${QT_ROOT_DIR}/bin/windeployqt.exe" | |
| if [ ! -f "$WDQT" ]; then | |
| WDQT="${QT_ROOT_DIR}/../msvc2022_64/bin/windeployqt.exe" | |
| fi | |
| if [ -f "$WDQT" ]; then | |
| echo "Running windeployqt: $WDQT" | |
| "$WDQT" \ | |
| --release \ | |
| --no-translations \ | |
| --compiler-runtime \ | |
| --include-soft-plugins \ | |
| --force \ | |
| dist/${{ matrix.exe }} | |
| else | |
| echo "::warning::windeployqt not found — Qt DLLs not bundled" | |
| fi | |
| QT_BIN="${QT_ROOT_DIR}/bin" | |
| QT_PLUGINS="${QT_ROOT_DIR}/plugins" | |
| # ---------------------------------------------------------------- | |
| # Qt Core DLLs (windeployqt should handle these, but belt+suspenders) | |
| # ---------------------------------------------------------------- | |
| for qt_dll in \ | |
| Qt6Core.dll Qt6Gui.dll Qt6Widgets.dll \ | |
| Qt6Charts.dll Qt6PrintSupport.dll \ | |
| Qt6Network.dll Qt6Sql.dll Qt6Concurrent.dll \ | |
| Qt6WebSockets.dll \ | |
| Qt6Multimedia.dll \ | |
| Qt6OpenGL.dll Qt6OpenGLWidgets.dll \ | |
| Qt6Svg.dll Qt6Xml.dll; do | |
| if [ -f "${QT_BIN}/${qt_dll}" ] && [ ! -f "dist/${qt_dll}" ]; then | |
| cp "${QT_BIN}/${qt_dll}" dist/ | |
| echo "Copied ${qt_dll}" | |
| fi | |
| done | |
| # ---------------------------------------------------------------- | |
| # OpenSSL DLLs — Qt6::Network requires these for HTTPS. | |
| # Qt 6.x ships them in its own bin/ directory. | |
| # ---------------------------------------------------------------- | |
| for ssl_dll in \ | |
| libssl-3-x64.dll libcrypto-3-x64.dll \ | |
| libssl-3.dll libcrypto-3.dll \ | |
| libssl-1_1-x64.dll libcrypto-1_1-x64.dll; do | |
| if [ -f "${QT_BIN}/${ssl_dll}" ] && [ ! -f "dist/${ssl_dll}" ]; then | |
| cp "${QT_BIN}/${ssl_dll}" dist/ | |
| echo "Copied ${ssl_dll}" | |
| fi | |
| done | |
| # ---------------------------------------------------------------- | |
| # MSVC Runtime DLLs — --compiler-runtime covers most, but copy | |
| # from System32 as a fallback for anything missed. | |
| # msvcp170 / vcruntime140_2 are the VS2022 v17.x toolset DLLs. | |
| # ---------------------------------------------------------------- | |
| for vcrt_dll in \ | |
| vcruntime140.dll vcruntime140_1.dll vcruntime140_2.dll \ | |
| msvcp140.dll msvcp140_1.dll msvcp140_2.dll \ | |
| msvcp170.dll \ | |
| concrt140.dll vccorlib140.dll; do | |
| SYS32="/c/Windows/System32/${vcrt_dll}" | |
| if [ -f "$SYS32" ] && [ ! -f "dist/${vcrt_dll}" ]; then | |
| cp "$SYS32" dist/ | |
| echo "Copied MSVC runtime ${vcrt_dll}" | |
| fi | |
| done | |
| # ---------------------------------------------------------------- | |
| # Qt Platform Plugin — mandatory, app won't start without it | |
| # ---------------------------------------------------------------- | |
| mkdir -p dist/platforms | |
| for plat_dll in qwindows.dll qwindowsd.dll; do | |
| if [ -f "${QT_PLUGINS}/platforms/${plat_dll}" ] && [ ! -f "dist/platforms/${plat_dll}" ]; then | |
| cp "${QT_PLUGINS}/platforms/${plat_dll}" dist/platforms/ | |
| echo "Copied platforms/${plat_dll}" | |
| fi | |
| done | |
| # ---------------------------------------------------------------- | |
| # Qt SQL Drivers — qsqlite required for SQLite database | |
| # ---------------------------------------------------------------- | |
| mkdir -p dist/sqldrivers | |
| for sql_dll in qsqlite.dll qsqlodbc.dll; do | |
| if [ -f "${QT_PLUGINS}/sqldrivers/${sql_dll}" ] && [ ! -f "dist/sqldrivers/${sql_dll}" ]; then | |
| cp "${QT_PLUGINS}/sqldrivers/${sql_dll}" dist/sqldrivers/ | |
| echo "Copied sqldrivers/${sql_dll}" | |
| fi | |
| done | |
| # ---------------------------------------------------------------- | |
| # Qt Image Format Plugins — needed to render PNG/JPG/SVG/GIF/ICO | |
| # in the UI (logos, icons, chart exports) | |
| # ---------------------------------------------------------------- | |
| mkdir -p dist/imageformats | |
| for img_dll in \ | |
| qbmp.dll qgif.dll qico.dll qjpeg.dll \ | |
| qpng.dll qsvg.dll qtga.dll qtiff.dll qwbmp.dll qwebp.dll; do | |
| if [ -f "${QT_PLUGINS}/imageformats/${img_dll}" ] && [ ! -f "dist/imageformats/${img_dll}" ]; then | |
| cp "${QT_PLUGINS}/imageformats/${img_dll}" dist/imageformats/ | |
| echo "Copied imageformats/${img_dll}" | |
| fi | |
| done | |
| # ---------------------------------------------------------------- | |
| # Qt Icon Engine Plugin — renders .svg icons in toolbars/menus | |
| # ---------------------------------------------------------------- | |
| mkdir -p dist/iconengines | |
| if [ -f "${QT_PLUGINS}/iconengines/qsvgicon.dll" ] && [ ! -f "dist/iconengines/qsvgicon.dll" ]; then | |
| cp "${QT_PLUGINS}/iconengines/qsvgicon.dll" dist/iconengines/ | |
| echo "Copied iconengines/qsvgicon.dll" | |
| fi | |
| # ---------------------------------------------------------------- | |
| # Qt Style Plugins — Fusion style (cross-platform look) | |
| # ---------------------------------------------------------------- | |
| mkdir -p dist/styles | |
| for style_dll in qwindowsvistastyle.dll; do | |
| if [ -f "${QT_PLUGINS}/styles/${style_dll}" ] && [ ! -f "dist/styles/${style_dll}" ]; then | |
| cp "${QT_PLUGINS}/styles/${style_dll}" dist/styles/ | |
| echo "Copied styles/${style_dll}" | |
| fi | |
| done | |
| # ---------------------------------------------------------------- | |
| # Qt Network TLS Backend Plugin — required for HTTPS on Qt 6.2+ | |
| # (replaces old OpenSSL linkage model) | |
| # ---------------------------------------------------------------- | |
| mkdir -p dist/tls | |
| for tls_dll in qschannelbackend.dll qopensslbackend.dll qcertonlybackend.dll; do | |
| if [ -f "${QT_PLUGINS}/tls/${tls_dll}" ] && [ ! -f "dist/tls/${tls_dll}" ]; then | |
| cp "${QT_PLUGINS}/tls/${tls_dll}" dist/tls/ | |
| echo "Copied tls/${tls_dll}" | |
| fi | |
| done | |
| # ---------------------------------------------------------------- | |
| # Qt Multimedia Plugins — audio/video backend (Windows MF + FFMPEG) | |
| # ---------------------------------------------------------------- | |
| if [ -d "${QT_PLUGINS}/multimedia" ]; then | |
| mkdir -p dist/multimedia | |
| for media_dll in windowsmediafoundation.dll ffmpegmediaplugin.dll; do | |
| if [ -f "${QT_PLUGINS}/multimedia/${media_dll}" ] && [ ! -f "dist/multimedia/${media_dll}" ]; then | |
| cp "${QT_PLUGINS}/multimedia/${media_dll}" dist/multimedia/ | |
| echo "Copied multimedia/${media_dll}" | |
| fi | |
| done | |
| fi | |
| # ---------------------------------------------------------------- | |
| # Qt MultimediaWidgets DLL — required when HAS_QT_MULTIMEDIA is set | |
| # (Qt6Multimedia.dll is in the core list above; this is the separate | |
| # widgets integration DLL that windeployqt may miss) | |
| # ---------------------------------------------------------------- | |
| if [ -f "${QT_BIN}/Qt6MultimediaWidgets.dll" ] && [ ! -f "dist/Qt6MultimediaWidgets.dll" ]; then | |
| cp "${QT_BIN}/Qt6MultimediaWidgets.dll" dist/ | |
| echo "Copied Qt6MultimediaWidgets.dll" | |
| fi | |
| # ---------------------------------------------------------------- | |
| # Qt TextToSpeech DLL + SAPI plugin (Windows TTS backend) | |
| # ---------------------------------------------------------------- | |
| if [ -f "${QT_BIN}/Qt6TextToSpeech.dll" ] && [ ! -f "dist/Qt6TextToSpeech.dll" ]; then | |
| cp "${QT_BIN}/Qt6TextToSpeech.dll" dist/ | |
| echo "Copied Qt6TextToSpeech.dll" | |
| fi | |
| if [ -d "${QT_PLUGINS}/texttospeech" ]; then | |
| mkdir -p dist/texttospeech | |
| cp -n "${QT_PLUGINS}/texttospeech/"*.dll dist/texttospeech/ 2>/dev/null || true | |
| echo "Copied texttospeech plugins" | |
| fi | |
| # ---------------------------------------------------------------- | |
| # Qt Network Information plugins (Qt 6.2+ — used by QNetworkInformation) | |
| # ---------------------------------------------------------------- | |
| if [ -d "${QT_PLUGINS}/networkinformation" ]; then | |
| mkdir -p dist/networkinformation | |
| cp -n "${QT_PLUGINS}/networkinformation/"*.dll dist/networkinformation/ 2>/dev/null || true | |
| echo "Copied networkinformation plugins" | |
| fi | |
| # ---------------------------------------------------------------- | |
| # Qt Platform Input Contexts — IME support (CJK, on-screen keyboard) | |
| # ---------------------------------------------------------------- | |
| if [ -d "${QT_PLUGINS}/platforminputcontexts" ]; then | |
| mkdir -p dist/platforminputcontexts | |
| cp -n "${QT_PLUGINS}/platforminputcontexts/"*.dll dist/platforminputcontexts/ 2>/dev/null || true | |
| echo "Copied platforminputcontexts plugins" | |
| fi | |
| # ---------------------------------------------------------------- | |
| # Qt Print Support Plugin — required by Qt6::PrintSupport | |
| # (the DLL is in the core list; this is the Windows printer backend plugin) | |
| # ---------------------------------------------------------------- | |
| if [ -d "${QT_PLUGINS}/printsupport" ]; then | |
| mkdir -p dist/printsupport | |
| cp -n "${QT_PLUGINS}/printsupport/"*.dll dist/printsupport/ 2>/dev/null || true | |
| echo "Copied printsupport plugins" | |
| fi | |
| # ---------------------------------------------------------------- | |
| # Qt Generic Plugins (touch/tablet input) | |
| # ---------------------------------------------------------------- | |
| if [ -d "${QT_PLUGINS}/generic" ]; then | |
| mkdir -p dist/generic | |
| cp -n "${QT_PLUGINS}/generic/"*.dll dist/generic/ 2>/dev/null || true | |
| fi | |
| # ---------------------------------------------------------------- | |
| # Direct3D compiler + OpenGL SW fallback | |
| # Needed on machines without a proper GPU driver (VMs, headless CI). | |
| # dxcompiler.dll + dxil.dll: Qt 6.6+ D3D12 backend (DXC shader compiler). | |
| # ---------------------------------------------------------------- | |
| for d3d_dll in \ | |
| d3dcompiler_47.dll opengl32sw.dll libEGL.dll libGLESv2.dll \ | |
| dxcompiler.dll dxil.dll; do | |
| if [ -f "${QT_BIN}/${d3d_dll}" ] && [ ! -f "dist/${d3d_dll}" ]; then | |
| cp "${QT_BIN}/${d3d_dll}" dist/ | |
| echo "Copied ${d3d_dll}" | |
| fi | |
| done | |
| # ---------------------------------------------------------------- | |
| # ICU DLLs — Qt 6 on Windows ships its own ICU for Unicode support | |
| # ---------------------------------------------------------------- | |
| for icu_dll in "${QT_BIN}"/icu*.dll; do | |
| base=$(basename "$icu_dll") | |
| if [ -f "$icu_dll" ] && [ ! -f "dist/${base}" ]; then | |
| cp "$icu_dll" dist/ | |
| echo "Copied ${base}" | |
| fi | |
| done | |
| # ---------------------------------------------------------------- | |
| # qt.conf — pins plugin and data paths relative to the executable. | |
| # Without this, Qt falls back to hardcoded build-time paths which | |
| # don't exist on a clean machine and can cause plugin load failures. | |
| # ---------------------------------------------------------------- | |
| printf '[Paths]\nPrefix = .\nPlugins = .\nLibraries = .\n' > dist/qt.conf | |
| # ---------------------------------------------------------------- | |
| # Copy app resources and Python analytics scripts | |
| # ---------------------------------------------------------------- | |
| cp -r resources dist/resources 2>/dev/null || true | |
| cp -r scripts dist/scripts 2>/dev/null || true | |
| # ---------------------------------------------------------------- | |
| # Final diagnostic — list everything that will be zipped | |
| # ---------------------------------------------------------------- | |
| echo "" | |
| echo "=== dist/ contents ($(find dist -type f | wc -l) files) ===" | |
| find dist -type f | sort | |
| # ---- Package: macOS ---- | |
| # Order is critical (see issue #139): Python.framework + resources must | |
| # be placed INSIDE the .app BEFORE macdeployqt / codesign run. If we | |
| # copy them as siblings of the .app (old behavior) the bundle can't find | |
| # them at runtime; if we embed after signing, dyld on macOS 14+ kills | |
| # the process with SIGKILL (Code Signature Invalid). | |
| # | |
| # On tag / workflow_dispatch: full stage -> macdeployqt -> deep ad-hoc | |
| # codesign -> verify. On push builds: stage + skip macdeployqt/codesign | |
| # (saves ~3-5 min; the resulting bundle won't run on stock macOS but is | |
| # fine as a CI artifact for inspection). | |
| # | |
| # upload-artifact strips execute bits (zip limitation) so we tar dist/. | |
| - name: Package (macOS) | |
| if: runner.os == 'macOS' | |
| working-directory: fincept-qt | |
| run: | | |
| set -euo pipefail | |
| mkdir -p dist | |
| MACDEPLOYQT="${QT_ROOT_DIR}/bin/macdeployqt" | |
| IS_TAG="${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}" | |
| # ── Locate or synthesize .app bundle ────────────────────────────── | |
| if [ -d "build/${{ matrix.exe }}.app" ]; then | |
| APP_BUNDLE="build/${{ matrix.exe }}.app" | |
| else | |
| APP_BUNDLE="build/${{ matrix.exe }}.app" | |
| mkdir -p "${APP_BUNDLE}/Contents/MacOS" | |
| cp "build/${{ matrix.exe }}" "${APP_BUNDLE}/Contents/MacOS/${{ matrix.exe }}" | |
| chmod +x "${APP_BUNDLE}/Contents/MacOS/${{ matrix.exe }}" | |
| cat > "${APP_BUNDLE}/Contents/Info.plist" <<'PLIST' | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"><dict> | |
| <key>CFBundleExecutable</key><string>FinceptTerminal</string> | |
| <key>CFBundleIdentifier</key><string>com.fincept.terminal</string> | |
| <key>CFBundleName</key><string>FinceptTerminal</string> | |
| <key>CFBundlePackageType</key><string>APPL</string> | |
| <key>CFBundleShortVersionString</key><string>4.0.2</string> | |
| <key>CFBundleVersion</key><string>4.0.2</string> | |
| <key>LSMinimumSystemVersion</key><string>13.0</string> | |
| <key>NSHighResolutionCapable</key><true/> | |
| </dict></plist> | |
| PLIST | |
| fi | |
| # ── Stage resources INSIDE the bundle (not as siblings) ─────────── | |
| mkdir -p "${APP_BUNDLE}/Contents/Resources" | |
| cp -r resources "${APP_BUNDLE}/Contents/Resources/" 2>/dev/null || true | |
| cp -r scripts "${APP_BUNDLE}/Contents/Resources/scripts" 2>/dev/null || true | |
| # ── Embed Python.framework (tag builds only — skip on push) ─────── | |
| if [ "$IS_TAG" = "true" ]; then | |
| PY_FRAMEWORK="" | |
| for candidate in \ | |
| "/Library/Frameworks/Python.framework" \ | |
| "$(brew --prefix python@3.12 2>/dev/null)/Frameworks/Python.framework" \ | |
| "$(brew --prefix python3 2>/dev/null)/Frameworks/Python.framework"; do | |
| [ -d "${candidate}/Versions" ] && { PY_FRAMEWORK="${candidate}"; break; } | |
| done | |
| BUNDLE_FRAMEWORKS="${APP_BUNDLE}/Contents/Frameworks" | |
| mkdir -p "${BUNDLE_FRAMEWORKS}" | |
| if [ -n "${PY_FRAMEWORK}" ]; then | |
| echo "Embedding Python.framework from: ${PY_FRAMEWORK}" | |
| rsync -a --copy-unsafe-links "${PY_FRAMEWORK}/" "${BUNDLE_FRAMEWORKS}/Python.framework/" 2>/dev/null || \ | |
| cp -R "${PY_FRAMEWORK}" "${BUNDLE_FRAMEWORKS}/" 2>/dev/null || true | |
| find "${BUNDLE_FRAMEWORKS}/Python.framework" -name "PrivateHeaders" -type l \ | |
| ! -exec test -e {} \; -delete 2>/dev/null || true | |
| PY_VER="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" | |
| ln -sf "../Frameworks/Python.framework/Versions/${PY_VER}/bin/python${PY_VER}" \ | |
| "${APP_BUNDLE}/Contents/MacOS/python3" 2>/dev/null || true | |
| else | |
| echo "::warning::Python.framework not found — bundle will rely on system python" | |
| fi | |
| fi | |
| # ── Run macdeployqt on the fully staged bundle (tag builds only) ── | |
| if [ "$IS_TAG" = "true" ] && [ -x "$MACDEPLOYQT" ]; then | |
| echo "Running macdeployqt on ${APP_BUNDLE}" | |
| "$MACDEPLOYQT" "${APP_BUNDLE}" -verbose=1 || \ | |
| echo "::warning::macdeployqt exited non-zero — continuing to codesign" | |
| fi | |
| # ── Deep ad-hoc codesign (tag builds only) — fix for issue #139 ─── | |
| # Strip every signature leaf-to-root, then re-sign whole bundle with | |
| # the ad-hoc identity ("-"). Do NOT pass --preserve-metadata: there | |
| # are no prior entitlements to preserve on a fresh bundle and the | |
| # flag errors out on resource-fork detritus left by macdeployqt. | |
| if [ "$IS_TAG" = "true" ]; then | |
| echo "Stripping existing signatures..." | |
| find "${APP_BUNDLE}" -type f \( -name "*.dylib" -o -name "*.so" -o -perm +111 \) 2>/dev/null | \ | |
| while read -r f; do | |
| if file "$f" 2>/dev/null | grep -q "Mach-O"; then | |
| codesign --remove-signature "$f" 2>/dev/null || true | |
| fi | |
| done | |
| codesign --remove-signature "${APP_BUNDLE}" 2>/dev/null || true | |
| echo "Re-signing bundle ad-hoc (deep)..." | |
| codesign --force --deep --sign - --timestamp=none "${APP_BUNDLE}" | |
| echo "Verifying signature..." | |
| codesign --verify --deep --strict --verbose=2 "${APP_BUNDLE}" || { | |
| echo "::error::Bundle verification failed" | |
| codesign --display --deep --verbose=4 "${APP_BUNDLE}" || true | |
| exit 1 | |
| } | |
| fi | |
| # ── Copy signed bundle to dist/ ────────────────────────────────── | |
| cp -R "${APP_BUNDLE}" dist/ | |
| # Tar to preserve execute permissions (upload-artifact uses zip which strips them) | |
| tar -czf dist.tar.gz -C dist . | |
| # ---- Package: Linux (AppImage) ---- | |
| - name: Package (Linux) | |
| if: runner.os == 'Linux' | |
| working-directory: fincept-qt | |
| run: | | |
| set -euo pipefail | |
| # ── Download linuxdeploy + Qt plugin ───────────────────────────── | |
| wget -q "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" | |
| wget -q "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" | |
| chmod +x linuxdeploy-x86_64.AppImage linuxdeploy-plugin-qt-x86_64.AppImage | |
| # ── Build AppDir structure ──────────────────────────────────────── | |
| mkdir -p AppDir/usr/bin | |
| mkdir -p AppDir/usr/share/applications | |
| mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps | |
| cp build/${{ matrix.exe }} AppDir/usr/bin/ | |
| chmod +x AppDir/usr/bin/${{ matrix.exe }} | |
| # Copy resources and scripts inside AppDir | |
| cp -r resources AppDir/usr/bin/resources 2>/dev/null || true | |
| cp -r scripts AppDir/usr/bin/scripts 2>/dev/null || true | |
| # Desktop entry (required by linuxdeploy) | |
| cat > AppDir/usr/share/applications/fincept-terminal.desktop <<'EOF' | |
| [Desktop Entry] | |
| Name=Fincept Terminal | |
| Exec=FinceptTerminal | |
| Icon=fincept-terminal | |
| Type=Application | |
| Categories=Finance; | |
| EOF | |
| # App icon (use existing .ico or create placeholder) | |
| if [ -f "resources/icons/app_icon_256.png" ]; then | |
| cp resources/icons/app_icon_256.png AppDir/usr/share/icons/hicolor/256x256/apps/fincept-terminal.png | |
| else | |
| # Generate a minimal placeholder icon with ImageMagick | |
| convert -size 256x256 xc:#1a1a2e -fill white \ | |
| -font DejaVu-Sans -pointsize 48 -gravity center \ | |
| -annotate 0 "FT" AppDir/usr/share/icons/hicolor/256x256/apps/fincept-terminal.png 2>/dev/null || \ | |
| cp /usr/share/icons/hicolor/256x256/apps/firefox.png \ | |
| AppDir/usr/share/icons/hicolor/256x256/apps/fincept-terminal.png 2>/dev/null || \ | |
| touch AppDir/usr/share/icons/hicolor/256x256/apps/fincept-terminal.png | |
| fi | |
| # ── Build AppImage ──────────────────────────────────────────────── | |
| # UPDATE_INFORMATION embeds zsync update URL so AppImageUpdate can | |
| # fetch deltas automatically from the GitHub release. | |
| export QMAKE=$(which qmake6 || which qmake) | |
| export OUTPUT="FinceptTerminal-Linux-x86_64.AppImage" | |
| export UPDATE_INFORMATION="gh-releases-zsync|Fincept-Corporation|FinceptTerminal|latest|FinceptTerminal-Linux-x86_64.AppImage.zsync" | |
| ./linuxdeploy-x86_64.AppImage \ | |
| --appdir AppDir \ | |
| --plugin qt \ | |
| --output appimage \ | |
| 2>&1 || true | |
| # Verify AppImage was created | |
| if [ ! -f "$OUTPUT" ]; then | |
| # Fallback: find any AppImage produced | |
| OUTPUT=$(find . -maxdepth 1 -name "*.AppImage" | head -1) | |
| fi | |
| if [ -z "$OUTPUT" ] || [ ! -f "$OUTPUT" ]; then | |
| echo "::error::AppImage build failed — no .AppImage produced" | |
| exit 1 | |
| fi | |
| echo "AppImage produced: $OUTPUT ($(du -sh "$OUTPUT" | cut -f1))" | |
| # Rename to canonical name and move to dist/ | |
| mkdir -p dist | |
| mv "$OUTPUT" dist/FinceptTerminal-Linux-x86_64.AppImage | |
| chmod +x dist/FinceptTerminal-Linux-x86_64.AppImage | |
| # Generate zsync file for delta updates | |
| if command -v zsyncmake &>/dev/null; then | |
| zsyncmake dist/FinceptTerminal-Linux-x86_64.AppImage -o dist/FinceptTerminal-Linux-x86_64.AppImage.zsync | |
| echo "zsync file generated" | |
| else | |
| sudo apt-get install -y zsync -qq 2>/dev/null || true | |
| if command -v zsyncmake &>/dev/null; then | |
| zsyncmake dist/FinceptTerminal-Linux-x86_64.AppImage -o dist/FinceptTerminal-Linux-x86_64.AppImage.zsync | |
| echo "zsync file generated" | |
| else | |
| echo "::warning::zsyncmake not available — delta updates disabled" | |
| fi | |
| fi | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.artifact }} | |
| # macOS: upload the tar to preserve execute permissions; others upload dist/ directly | |
| path: ${{ runner.os == 'macOS' && 'fincept-qt/dist.tar.gz' || 'fincept-qt/dist/' }} | |
| retention-days: 30 | |
| # ---- macOS Universal Binary ---- | |
| macos-universal: | |
| needs: build | |
| runs-on: macos-15 | |
| name: macOS Universal | |
| # Only run if ALL build matrix jobs succeeded (not just "didn't fail") | |
| if: "!cancelled() && needs.build.result == 'success'" | |
| steps: | |
| - name: Download macOS arm64 | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: FinceptTerminal-macOS-arm64 | |
| path: arm64-dl | |
| - name: Download macOS x64 | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: FinceptTerminal-macOS-x64 | |
| path: x64-dl | |
| - name: Extract macOS artifacts | |
| run: | | |
| # Artifacts were uploaded as dist.tar.gz to preserve execute permissions | |
| mkdir -p arm64 x64 | |
| tar -xzf arm64-dl/dist.tar.gz -C arm64 | |
| tar -xzf x64-dl/dist.tar.gz -C x64 | |
| - name: Create Universal Binary | |
| run: | | |
| # Locate the binary — may be inside a .app bundle or at the top level | |
| find_bin() { | |
| local base="$1" | |
| if [ -f "$base/FinceptTerminal" ]; then | |
| echo "$base/FinceptTerminal" | |
| elif [ -f "$base/FinceptTerminal.app/Contents/MacOS/FinceptTerminal" ]; then | |
| echo "$base/FinceptTerminal.app/Contents/MacOS/FinceptTerminal" | |
| else | |
| echo "::error::Cannot find FinceptTerminal binary under $base" >&2 | |
| find "$base" -type f | head -20 >&2 | |
| exit 1 | |
| fi | |
| } | |
| ARM64_BIN=$(find_bin arm64) | |
| X64_BIN=$(find_bin x64) | |
| echo "arm64 binary: $ARM64_BIN" | |
| echo "x64 binary: $X64_BIN" | |
| # Verify the two binaries are actually different architectures before lipo | |
| ARM64_ARCH=$(lipo -archs "$ARM64_BIN") | |
| X64_ARCH=$(lipo -archs "$X64_BIN") | |
| echo "arm64 artifact arch: $ARM64_ARCH" | |
| echo "x64 artifact arch: $X64_ARCH" | |
| if [ "$ARM64_ARCH" = "$X64_ARCH" ]; then | |
| echo "::error::Both binaries report the same architecture ($ARM64_ARCH) — cannot create universal binary" >&2 | |
| exit 1 | |
| fi | |
| mkdir -p dist | |
| lipo -create "$ARM64_BIN" "$X64_BIN" -output dist/FinceptTerminal | |
| chmod +x dist/FinceptTerminal | |
| cp -r arm64/resources dist/resources 2>/dev/null || true | |
| cp -r arm64/scripts dist/scripts 2>/dev/null || true | |
| file dist/FinceptTerminal | |
| lipo -info dist/FinceptTerminal | |
| ls -lh dist/FinceptTerminal | |
| # Tar to preserve execute permissions | |
| tar -czf dist.tar.gz -C dist . | |
| - name: Upload Universal artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: FinceptTerminal-macOS-universal | |
| path: dist.tar.gz | |
| retention-days: 30 | |
| # ---- Release ---- | |
| release: | |
| needs: [build, macos-universal] | |
| runs-on: ubuntu-latest | |
| name: Create Release | |
| if: "!cancelled() && needs.build.result == 'success' && needs.macos-universal.result == 'success' && startsWith(github.ref, 'refs/tags/')" | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Install zip | |
| run: sudo apt-get install -y zip | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - name: Package releases | |
| shell: bash | |
| run: | | |
| cd artifacts | |
| for dir in */; do | |
| name="${dir%/}" | |
| if [[ "$name" == *"Windows"* ]]; then | |
| # Windows artifacts are plain directories → zip | |
| cd "$name" | |
| zip -r "../../${name}.zip" . | |
| cd .. | |
| elif [[ "$name" == *"macOS"* ]]; then | |
| # macOS artifacts are already a dist.tar.gz (execute permissions preserved) | |
| mkdir -p "/tmp/${name}" | |
| tar -xzf "${name}/dist.tar.gz" -C "/tmp/${name}" | |
| tar -czf "../${name}.tar.gz" -C "/tmp/${name}" . | |
| elif [[ "$name" == *"Linux"* ]]; then | |
| # Linux: AppImage + optional zsync — copy directly, no re-archive | |
| cp "${name}/FinceptTerminal-Linux-x86_64.AppImage" "../FinceptTerminal-Linux-x86_64.AppImage" 2>/dev/null || true | |
| cp "${name}/FinceptTerminal-Linux-x86_64.AppImage.zsync" "../FinceptTerminal-Linux-x86_64.AppImage.zsync" 2>/dev/null || true | |
| fi | |
| done | |
| ls -lh ../ | |
| - name: Create Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ github.ref_name }} | |
| name: Fincept Terminal ${{ github.ref_name }} | |
| make_latest: true | |
| body: | | |
| ## Fincept Terminal ${{ github.ref_name }} | |
| ### Downloads | |
| **Windows** | |
| - `FinceptTerminal-Windows-x64.zip` — Windows x64 (MSVC) | |
| - `FinceptTerminal-Windows-arm64.zip` — Windows ARM64 | |
| **Linux** | |
| - `FinceptTerminal-Linux-x86_64.AppImage` — Linux x86_64 (portable, runs on any distro) | |
| - `FinceptTerminal-Linux-x86_64.AppImage.zsync` — Delta update file (used by auto-updater) | |
| **macOS** | |
| - `FinceptTerminal-macOS-universal.tar.gz` — macOS Universal (ARM64 + Intel) | |
| - `FinceptTerminal-macOS-arm64.tar.gz` — macOS Apple Silicon | |
| - `FinceptTerminal-macOS-x64.tar.gz` — macOS Intel | |
| files: | | |
| FinceptTerminal-Windows-x64.zip | |
| FinceptTerminal-Windows-arm64.zip | |
| FinceptTerminal-Linux-x86_64.AppImage | |
| FinceptTerminal-Linux-x86_64.AppImage.zsync | |
| FinceptTerminal-macOS-arm64.tar.gz | |
| FinceptTerminal-macOS-x64.tar.gz | |
| FinceptTerminal-macOS-universal.tar.gz | |
| fail_on_unmatched_files: false | |
| draft: false | |
| prerelease: false |