diff --git a/.azure-pipelines/azure-pipelines.yml b/.azure-pipelines/azure-pipelines.yml index 4219624758..3c91441066 100644 --- a/.azure-pipelines/azure-pipelines.yml +++ b/.azure-pipelines/azure-pipelines.yml @@ -103,7 +103,7 @@ stages: jobs: - job: Windows - timeoutInMinutes: 30 + timeoutInMinutes: 45 cancelTimeoutInMinutes: 1 displayName: Windows @@ -143,7 +143,7 @@ stages: - script: | cd /d "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/SerialPrograms" - cmake --build --preset=$(cmake_preset) + cmake --build --preset=$(cmake_preset) -j 16 displayName: 'Build' condition: succeeded() @@ -228,42 +228,34 @@ stages: $name = "SerialPrograms-Windows-$(compiler)-$(architecture)" $buildType = "${{ parameters.buildType }}" - $artifactSubdir = Join-Path $root "cache-artifacts/$name" - $zipPath = Join-Path $artifactSubdir "$name.zip" - New-Item -ItemType Directory -Force -Path $artifactSubdir | Out-Null - - $tempFolder = Join-Path $root $folderName - New-Item -ItemType Directory -Force -Path $tempFolder | Out-Null - Copy-Item "$root/cache-build/*" $tempFolder -Recurse -Force - - Push-Location $root - try { - if ($buildType -eq "PrivateBeta") { - Write-Host "=== Encrypting build artifact with password ===" - & "C:/Program Files/7-Zip/7z.exe" a -tzip -mx=9 "-p$env:ARTIFACT_PASSWORD" -mem=AES256 $zipPath $folderName - } else { - & "C:/Program Files/7-Zip/7z.exe" a -tzip -mx=9 $zipPath $folderName - } - if ($LASTEXITCODE -ne 0) { - throw "7-Zip failed with exit code: $LASTEXITCODE" - } - } finally { - Pop-Location + Write-Host "=== Creating enclosing folder: $folderName ===" + $finalFolder = Join-Path $root $folderName + Rename-Item "$root/cache-build" $finalFolder + + New-Item -ItemType Directory -Force -Path "$root/cache-build" | Out-Null + $zipPath = Join-Path $root "cache-build/$name.zip" + if ($buildType -eq "PrivateBeta") { + Write-Host "=== Creating password-protected archive ===" + & "C:/Program Files/7-Zip/7z.exe" a -tzip -mx=9 "-p$env:ARTIFACT_PASSWORD" -mem=AES256 $zipPath "$finalFolder" + } else { + Write-Host "=== Creating archive ===" + & "C:/Program Files/7-Zip/7z.exe" a -tzip -mx=9 $zipPath "$finalFolder" } - - Remove-Item $tempFolder -Recurse -Force - Write-Host "=== Created archive with enclosing folder: $folderName ===" + if ($LASTEXITCODE -ne 0) { + throw "7-Zip failed with exit code: $LASTEXITCODE" + } + Write-Host "=== Archive created ===" displayName: 'Archive Windows build artifact' - condition: succeeded() + condition: and(succeeded(), ne('${{ parameters.buildType }}', 'Commit')) env: ARTIFACT_PASSWORD: $(ARTIFACT_PASSWORD) - task: Cache@2 - displayName: 'Cache the build artifact' + displayName: 'Cache build artifact for GitHub release' inputs: key: 'windows-build | "$(Build.BuildId)"' - path: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-artifacts' - condition: succeeded() + path: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-build' + condition: and(succeeded(), eq('${{ parameters.targetOS }}', 'All'), or(eq('${{ parameters.buildType }}', 'Release'), eq('${{ parameters.buildType }}', 'Beta'))) - task: Cache@2 displayName: 'Cache debug symbols' @@ -272,12 +264,21 @@ stages: path: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-symbols' condition: succeeded() - - task: PublishBuildArtifacts@1 - displayName: 'Publish SerialPrograms' - condition: succeeded() + - task: PublishPipelineArtifact@1 + displayName: 'Publish Windows artifact (PrivateBeta)' + inputs: + targetPath: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/SerialPrograms-Windows-$(compiler)-$(architecture).zip' + artifactName: 'SerialPrograms-Windows-$(compiler)-$(architecture)' + publishLocation: 'pipeline' + condition: and(succeeded(), eq('${{ parameters.buildType }}', 'PrivateBeta')) + + - task: PublishPipelineArtifact@1 + displayName: 'Publish Windows artifact' inputs: - PathtoPublish: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-artifacts/SerialPrograms-Windows-$(compiler)-$(architecture)/SerialPrograms-Windows-$(compiler)-$(architecture).zip' - ArtifactName: 'SerialPrograms-Windows-$(compiler)-$(architecture)' + targetPath: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/SerialPrograms-${{ parameters.versionMajor }}.${{ parameters.versionMinor }}.${{ parameters.versionPatch }}-Windows-$(architecture)' + artifactName: 'SerialPrograms-Windows-$(compiler)-$(architecture)' + publishLocation: 'pipeline' + condition: and(succeeded(), ne('${{ parameters.buildType }}', 'PrivateBeta')) ########################################### # LINUX BUILD JOB # @@ -292,7 +293,7 @@ stages: jobs: - job: Ubuntu - timeoutInMinutes: 30 + timeoutInMinutes: 45 cancelTimeoutInMinutes: 1 displayName: Ubuntu @@ -327,7 +328,7 @@ stages: - script: | export PATH=$(linux_path) cd "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/SerialPrograms" - cmake --build --preset=$(cmake_preset) + cmake --build --preset=$(cmake_preset) -j 16 displayName: 'Build' condition: succeeded() @@ -353,30 +354,27 @@ stages: export PATH=$(linux_path) BUILD_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)" SRC_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public" - APPDIR="$BUILD_DIR/AppDir" + APP_DIR="$BUILD_DIR/AppDir" BUILD_CACHE_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-build" echo "=== Clean AppDir ===" - rm -rf "$APPDIR" - mkdir -p "$APPDIR" - mkdir -p "$APPDIR/usr/bin" - mkdir -p "$APPDIR/usr/lib" - mkdir -p "$APPDIR/usr/share/applications" - mkdir -p "$APPDIR/usr/share/icons/hicolor/256x256/apps" + rm -rf "$APP_DIR" + mkdir -p "$APP_DIR" + mkdir -p "$APP_DIR/usr/bin" + mkdir -p "$APP_DIR/usr/lib" + mkdir -p "$APP_DIR/usr/share/applications" + mkdir -p "$APP_DIR/usr/share/icons/hicolor/256x256/apps" + mkdir -p "$BUILD_CACHE_DIR" echo "=== Moving resources ===" - mkdir -p "$APPDIR/usr/plugins/iconengines" - cp -a "/opt/Qt/$(qt_version)/gcc_64/plugins/iconengines/" "$APPDIR/usr/plugins/iconengines/" - - mkdir -p "$APPDIR/usr/bin/Resources" - cp -a "$SRC_DIR/Packages/Resources/" "$APPDIR/usr/bin/" - - mkdir -p "$BUILD_CACHE_DIR" - cp -a "$SRC_DIR/Packages/Firmware/" "$BUILD_CACHE_DIR/" + mkdir -p "$APP_DIR/usr/plugins/iconengines" + cp -a "/opt/Qt/$(qt_version)/gcc_64/plugins/iconengines/" "$APP_DIR/usr/plugins/iconengines/" - cp "$SRC_DIR/IconResource/SerialPrograms.desktop" "$APPDIR/usr/share/applications/" - cp "$SRC_DIR/IconResource/SerialPrograms.png" "$APPDIR/usr/share/icons/hicolor/256x256/apps/" - cp "$BUILD_DIR/SerialPrograms" "$APPDIR/usr/bin" + mkdir -p "$APP_DIR/usr/share/metainfo" + cp "$SRC_DIR/IconResource/SerialPrograms.desktop" "$APP_DIR/usr/share/applications/" + cp "$SRC_DIR/IconResource/SerialPrograms.png" "$APP_DIR/usr/share/icons/hicolor/256x256/apps/" + cp "$SRC_DIR/IconResource/SerialPrograms.appdata.xml" "$APP_DIR/usr/share/metainfo/" + cp "$BUILD_DIR/SerialPrograms" "$APP_DIR/usr/bin" echo "=== Extracting Discord Social SDK ===" DISCORD_ZIP="$SRC_DIR/3rdPartyBinaries/discord_social_sdk_linux.zip" @@ -386,26 +384,35 @@ stages: unzip -q "$DISCORD_ZIP" -d "$SRC_DIR/3rdPartyBinaries" DISCORD_SO="$DISCORD_DIR/lib/release/libdiscord_partner_sdk.so" - echo "=== Downloading linuxdeploy & Qt plugin ===" + echo "=== Downloading linuxdeploy, Qt plugin, gstreamer plugin ===" cd "$BUILD_DIR" wget -nv https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage wget -nv https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage wget -nv https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage + wget -nv https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gstreamer/master/linuxdeploy-plugin-gstreamer.sh chmod +x linuxdeploy-*.AppImage chmod +x appimagetool-x86_64.AppImage + chmod +x linuxdeploy-plugin-gstreamer.sh echo "=== Running linuxdeploy to package AppImage ===" + export QMAKE="/opt/Qt/$(qt_version)/gcc_64/bin/qmake" + export EXTRA_QT_PLUGINS="multimedia" ./linuxdeploy-x86_64.AppImage \ - --appdir "AppDir" \ - -d "$APPDIR/usr/share/applications/SerialPrograms.desktop" \ - -i "$APPDIR/usr/share/icons/hicolor/256x256/apps/SerialPrograms.png" \ - -e "$APPDIR/usr/bin/SerialPrograms" \ + --appdir "$APP_DIR" \ + -d "$APP_DIR/usr/share/applications/SerialPrograms.desktop" \ + -i "$APP_DIR/usr/share/icons/hicolor/256x256/apps/SerialPrograms.png" \ + -e "$APP_DIR/usr/bin/SerialPrograms" \ -l "$DISCORD_SO" \ -p qt \ + -p gstreamer \ -v 3 echo "=== Removing unnecessary Qt files ===" - rm -rf "$APPDIR/usr/translations" + rm -rf "$APP_DIR/usr/translations" + + echo "=== Patching AppRun to force GStreamer backend ===" + sed -i '2i export PA_APPIMAGE_DIR=$(cd "$(dirname "$APPIMAGE")" && pwd)' "$APP_DIR/AppRun" + sed -i '2i export QT_MEDIA_BACKEND=gstreamer' "$APP_DIR/AppRun" echo "=== Packaging AppImage ===" ./appimagetool-x86_64.AppImage AppDir @@ -415,30 +422,39 @@ stages: echo "=== Archive Linux AppImage ===" VERSION="${{ parameters.versionMajor }}.${{ parameters.versionMinor }}.${{ parameters.versionPatch }}" FOLDER_NAME="SerialPrograms-$VERSION-Ubuntu-x64" - mkdir -p "$FOLDER_NAME" - chmod +x "SerialPrograms-x86_64.AppImage" - mv "SerialPrograms-x86_64.AppImage" "$FOLDER_NAME/" - BUILD_TYPE="${{ parameters.buildType }}" - if [ "$BUILD_TYPE" = "PrivateBeta" ]; then - echo "=== Encrypting build artifact with password ===" - 7z a -tzip -mx=9 "-p$ARTIFACT_PASSWORD" -mem=AES256 "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-build/SerialPrograms-Ubuntu-$(compiler)-$(architecture).zip" "$FOLDER_NAME" - else - 7z a -tzip -mx=9 "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-build/SerialPrograms-Ubuntu-$(compiler)-$(architecture).zip" "$FOLDER_NAME" - fi - - echo "=== AppImage build complete with enclosing folder: $FOLDER_NAME ===" + + mkdir -p "$FOLDER_NAME" + mv "SerialPrograms-x86_64.AppImage" "$FOLDER_NAME/SerialPrograms.AppImage" + chmod +x "$FOLDER_NAME/SerialPrograms.AppImage" + cp -a "$SRC_DIR/Packages/Firmware/" "$FOLDER_NAME/" + cp -a "$SRC_DIR/Packages/Resources/" "$FOLDER_NAME/" + + if [ "$BUILD_TYPE" != "Commit" ]; then + ZIP_NAME="SerialPrograms-Ubuntu-$(compiler)-$(architecture).zip" + rm -rf "$BUILD_CACHE_DIR"/* + if [ "$BUILD_TYPE" = "PrivateBeta" ]; then + echo "=== Creating password-protected archive ===" + 7z a -tzip -mx=9 "-p$ARTIFACT_PASSWORD" -mem=AES256 "$BUILD_CACHE_DIR/$ZIP_NAME" "$FOLDER_NAME" + else + echo "=== Creating archive ===" + 7z a -tzip -mx=9 "$BUILD_CACHE_DIR/$ZIP_NAME" "$FOLDER_NAME" + fi + echo "=== Archive created ===" + else + echo "=== Skipping archive creation for Commit build ===" + fi displayName: 'Deploy AppImage' condition: succeeded() env: ARTIFACT_PASSWORD: $(ARTIFACT_PASSWORD) - task: Cache@2 - displayName: 'Cache build' + displayName: 'Cache build artifact for GitHub release' inputs: key: 'ubuntu-build | "$(Build.BuildId)"' path: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-build' - condition: succeeded() + condition: and(succeeded(), eq('${{ parameters.targetOS }}', 'All'), or(eq('${{ parameters.buildType }}', 'Release'), eq('${{ parameters.buildType }}', 'Beta'))) - task: Cache@2 displayName: 'Cache debug symbols' @@ -447,12 +463,21 @@ stages: path: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-symbols' condition: succeeded() - - task: PublishBuildArtifacts@1 - displayName: 'Publish SerialPrograms' - condition: succeeded() + - task: PublishPipelineArtifact@1 + displayName: 'Publish Linux artifact (PrivateBeta)' inputs: - PathtoPublish: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-build/SerialPrograms-Ubuntu-$(compiler)-$(architecture).zip' - ArtifactName: 'SerialPrograms-Linux-$(compiler)-$(architecture)' + targetPath: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/SerialPrograms-Ubuntu-$(compiler)-$(architecture).zip' + artifactName: 'SerialPrograms-Linux-$(compiler)-$(architecture)' + publishLocation: 'pipeline' + condition: and(succeeded(), eq('${{ parameters.buildType }}', 'PrivateBeta')) + + - task: PublishPipelineArtifact@1 + displayName: 'Publish Linux artifact' + inputs: + targetPath: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/SerialPrograms-${{ parameters.versionMajor }}.${{ parameters.versionMinor }}.${{ parameters.versionPatch }}-Ubuntu-x64' + artifactName: 'SerialPrograms-Linux-$(compiler)-$(architecture)' + publishLocation: 'pipeline' + condition: and(succeeded(), ne('${{ parameters.buildType }}', 'PrivateBeta')) ########################################### # MACOS BUILD STAGES # @@ -463,6 +488,7 @@ stages: stageName: MacOS_Build_ARM64 displayName: 'MacOS Build ARM64' dependsOnStage: Set_Build_Number + buildType: ${{ parameters.buildType }} targetOS: ${{ parameters.targetOS }} poolName: ApplePool imageName: macos-26 @@ -473,6 +499,7 @@ stages: qt_modules: 'qtserialport qtmultimedia' cmake_preset: RelWithDebInfo macos_path: '/opt/Qt/6.10.2/macos/lib/cmake:/opt/Qt/6.10.2/macos/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/opt/homebrew:$PATH' + openssl_pkcs_args: '-legacy' cmake_version_params: '-DVERSION_MAJOR=${{ parameters.versionMajor }} -DVERSION_MINOR=${{ parameters.versionMinor }} -DVERSION_PATCH=${{ parameters.versionPatch }} -DIS_BETA=${{ lower(eq(parameters.buildType, ''PrivateBeta'')) }}' cmake_additional_param: '-DPACKAGE_BUILD=true -DUNIX_LINK_TESSERACT:BOOL=true -DIS_AZURE_BUILD=TRUE' versionMajor: ${{ parameters.versionMajor }} @@ -485,6 +512,7 @@ stages: stageName: MacOS_Build_x64 displayName: 'MacOS Build x64' dependsOnStage: Set_Build_Number + buildType: ${{ parameters.buildType }} targetOS: ${{ parameters.targetOS }} poolName: ApplePool imageName: macos-15 @@ -495,6 +523,7 @@ stages: qt_modules: 'qtserialport qtmultimedia' cmake_preset: RelWithDebInfo macos_path: '/usr/local/Qt/6.10.2/macos/lib/cmake:/usr/local/Qt/6.10.2/macos/bin:/usr/local/bin:/usr/local/sbin:/usr/local:$PATH' + openssl_pkcs_args: '-legacy' cmake_version_params: '-DVERSION_MAJOR=${{ parameters.versionMajor }} -DVERSION_MINOR=${{ parameters.versionMinor }} -DVERSION_PATCH=${{ parameters.versionPatch }} -DIS_BETA=${{ lower(eq(parameters.buildType, ''PrivateBeta'')) }}' cmake_additional_param: '-DPACKAGE_BUILD=true -DUNIX_LINK_TESSERACT:BOOL=true -DIS_AZURE_BUILD=TRUE' versionMajor: ${{ parameters.versionMajor }} @@ -502,46 +531,6 @@ stages: versionPatch: ${{ parameters.versionPatch }} condition: or(eq('${{ parameters.targetOS }}', 'MacOS'), eq('${{ parameters.targetOS }}', 'All')) -########################################### -# MACOS NOTARIZATION STAGES # -########################################### - -- template: templates/macos-notarize.yml - parameters: - stageName: MacOS_Notarize_ARM64 - displayName: 'MacOS Codesign ARM64' - dependsOnStage: MacOS_Build_ARM64 - buildType: ${{ parameters.buildType }} - poolName: ApplePool - imageName: macos-26 - architecture: arm64 - compiler: Clang - cmake_preset: RelWithDebInfo - macos_path: '/opt/Qt/6.10.2/macos/lib/cmake:/opt/Qt/6.10.2/macos/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/opt/homebrew:$PATH' - openssl_pkcs_args: '-legacy' - versionMajor: ${{ parameters.versionMajor }} - versionMinor: ${{ parameters.versionMinor }} - versionPatch: ${{ parameters.versionPatch }} - condition: and(succeeded('MacOS_Build_ARM64'), or(eq('${{ parameters.targetOS }}', 'MacOS'), eq('${{ parameters.targetOS }}', 'All'))) - -- template: templates/macos-notarize.yml - parameters: - stageName: MacOS_Notarize_x64 - displayName: 'MacOS Codesign x64' - dependsOnStage: MacOS_Build_x64 - buildType: ${{ parameters.buildType }} - poolName: ApplePool - imageName: macos-15 - architecture: x64 - compiler: Clang - cmake_preset: RelWithDebInfo - macos_path: '/usr/local/Qt/6.10.2/macos/lib/cmake:/usr/local/Qt/6.10.2/macos/bin:/usr/local/bin:/usr/local/sbin:/usr/local:$PATH' - openssl_pkcs_args: '-legacy' - versionMajor: ${{ parameters.versionMajor }} - versionMinor: ${{ parameters.versionMinor }} - versionPatch: ${{ parameters.versionPatch }} - condition: and(succeeded('MacOS_Build_x64'), or(eq('${{ parameters.targetOS }}', 'MacOS'), eq('${{ parameters.targetOS }}', 'All'))) - ########################################## # UPLOAD DEBUG SYMBOLS # ########################################## @@ -553,8 +542,8 @@ stages: dependsOn: - Windows_Build_x64 - Linux_Build_x64 - - MacOS_Notarize_ARM64 - - MacOS_Notarize_x64 + - MacOS_Build_ARM64 + - MacOS_Build_x64 platforms: - Windows - Linux @@ -638,7 +627,7 @@ stages: versionMajor: ${{ parameters.versionMajor }} versionMinor: ${{ parameters.versionMinor }} versionPatch: ${{ parameters.versionPatch }} - targetRepo: 'PokemonAutomation/ComputerControl' + targetRepo: PokemonAutomation/ComputerControl condition: and(succeeded(), eq('${{ parameters.targetOS }}', 'All'), or(eq('${{ parameters.buildType }}', 'Release'), eq('${{ parameters.buildType }}', 'Beta'))) ########################################## diff --git a/.azure-pipelines/templates/github-release.yml b/.azure-pipelines/templates/github-release.yml index a0c1dca3ca..83a8e4cf9c 100644 --- a/.azure-pipelines/templates/github-release.yml +++ b/.azure-pipelines/templates/github-release.yml @@ -205,7 +205,7 @@ stages: fi if [ "$(MACOS_ARM64_CACHE_RESTORED)" = "true" ]; then - MACOS_ARM64_ARTIFACT=$(find "$ARTIFACTS_DIR/artifacts-macos-arm64/" -name "*.zip" -type f | head -n 1) + MACOS_ARM64_ARTIFACT=$(find "$ARTIFACTS_DIR/artifacts-macos-arm64/" -name "*.dmg" -not -name "._*" -type f | head -n 1) if [ ! -f "$MACOS_ARM64_ARTIFACT" ]; then echo "✗ Error: MacOS ARM64 artifact not found" exit 1 @@ -217,7 +217,7 @@ stages: fi if [ "$(MACOS_X64_CACHE_RESTORED)" = "true" ]; then - MACOS_X64_ARTIFACT=$(find "$ARTIFACTS_DIR/artifacts-macos-x64/" -name "*.zip" -type f | head -n 1) + MACOS_X64_ARTIFACT=$(find "$ARTIFACTS_DIR/artifacts-macos-x64/" -name "*.dmg" -not -name "._*" -type f | head -n 1) if [ ! -f "$MACOS_X64_ARTIFACT" ]; then echo "✗ Error: MacOS X64 artifact not found" exit 1 @@ -260,7 +260,7 @@ stages: if [ "$(MACOS_ARM64_CACHE_RESTORED)" = "true" ] && [ -n "$(MACOS_ARM64_ARTIFACT)" ]; then OLD_PATH="$(MACOS_ARM64_ARTIFACT)" - NEW_NAME="PA-SerialPrograms-MacOS-ARM64-${VERSION}-${BUILD_DATE}.zip" + NEW_NAME="PA-SerialPrograms-MacOS-ARM64-${VERSION}-${BUILD_DATE}.dmg" NEW_PATH="$ARTIFACTS_DIR/$NEW_NAME" echo "Renaming: $(basename "$OLD_PATH") -> $NEW_NAME" @@ -270,7 +270,7 @@ stages: if [ "$(MACOS_X64_CACHE_RESTORED)" = "true" ] && [ -n "$(MACOS_X64_ARTIFACT)" ]; then OLD_PATH="$(MACOS_X64_ARTIFACT)" - NEW_NAME="PA-SerialPrograms-MacOS-x64-${VERSION}-${BUILD_DATE}.zip" + NEW_NAME="PA-SerialPrograms-MacOS-x64-${VERSION}-${BUILD_DATE}.dmg" NEW_PATH="$ARTIFACTS_DIR/$NEW_NAME" echo "Renaming: $(basename "$OLD_PATH") -> $NEW_NAME" diff --git a/.azure-pipelines/templates/macos-build.yml b/.azure-pipelines/templates/macos-build.yml index f9f835f240..39cca4c900 100644 --- a/.azure-pipelines/templates/macos-build.yml +++ b/.azure-pipelines/templates/macos-build.yml @@ -5,6 +5,7 @@ parameters: stageName: '' displayName: '' dependsOnStage: '' + buildType: '' targetOS: '' poolName: '' imageName: '' @@ -17,6 +18,7 @@ parameters: macos_path: '' cmake_version_params: '' cmake_additional_param: '' + openssl_pkcs_args: '' versionMajor: 0 versionMinor: 0 versionPatch: 0 @@ -30,7 +32,7 @@ stages: jobs: - job: MacOS - timeoutInMinutes: 60 + timeoutInMinutes: 180 cancelTimeoutInMinutes: 1 displayName: MacOS ${{ parameters.architecture }} @@ -72,14 +74,14 @@ stages: - script: | export PATH=$(macos_path) cd "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/SerialPrograms" - cmake $(cmake_additional_param) $(cmake_version_params) --preset=$(cmake_preset) --fresh + cmake $(cmake_additional_param) $(cmake_version_params) --preset=$(cmake_preset) displayName: 'Configure CMake' condition: succeeded() - script: | export PATH=$(macos_path) cd "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/SerialPrograms" - cmake --build --preset=$(cmake_preset) + cmake --build --preset=$(cmake_preset) -j $(sysctl -n hw.logicalcpu) displayName: 'Build' condition: succeeded() @@ -103,92 +105,480 @@ stages: APP_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/SerialPrograms.app" MACOS_DIR="$APP_DIR/Contents/MacOS" FW_DIR="$APP_DIR/Contents/Frameworks" + PLUGINS_DIR="$APP_DIR/Contents/PlugIns" BREW_PREFIX=$(brew --prefix) - mkdir -p "$FW_DIR" + mkdir -p "$FW_DIR" "$PLUGINS_DIR" + + echo "=== Setting up Qt paths ===" if [ "$(architecture)" = "x64" ]; then - QT_BIN="/usr/local/Qt/$(qt_version)/macos/bin" + QT_ROOT="/usr/local/Qt/$(qt_version)/macos" + LIPO_ARCH="x86_64" else - QT_BIN="/opt/Qt/$(qt_version)/macos/bin" + QT_ROOT="/opt/Qt/$(qt_version)/macos" + LIPO_ARCH="arm64" fi - echo "=== Copying additional deps ===" - cp $BREW_PREFIX/opt/gcc/lib/gcc/current/libgcc_s.1.1.dylib $APP_DIR/Contents/Frameworks - cp $BREW_PREFIX/opt/protobuf/lib/libutf8_validity.dylib $APP_DIR/Contents/Frameworks - cp $BREW_PREFIX/opt/little-cms2/lib/liblcms2.2.dylib $APP_DIR/Contents/Frameworks - cp $BREW_PREFIX/opt/webp/lib/libsharpyuv.0.dylib $APP_DIR/Contents/Frameworks - cp $BREW_PREFIX/opt/jpeg-xl/lib/libjxl_cms.0.11.dylib $APP_DIR/Contents/Frameworks - cp $BREW_PREFIX/opt/vtk/lib/libvtkCommonComputationalGeometry-*.dylib $APP_DIR/Contents/Frameworks - cp $BREW_PREFIX/opt/vtk/lib/libvtkFiltersVerdict-*.dylib $APP_DIR/Contents/Frameworks - cp $BREW_PREFIX/opt/vtk/lib/libvtkfmt-*.dylib $APP_DIR/Contents/Frameworks - cp $BREW_PREFIX/opt/vtk/lib/libvtkFiltersGeometry-*.dylib $APP_DIR/Contents/Frameworks - cp $BREW_PREFIX/opt/vtk/lib/libvtkFiltersCore-*.dylib $APP_DIR/Contents/Frameworks - cp $BREW_PREFIX/opt/vtk/lib/libvtkCommonCore-*.dylib $APP_DIR/Contents/Frameworks - cp $BREW_PREFIX/opt/vtk/lib/libvtkCommonSystem-*.dylib $APP_DIR/Contents/Frameworks - - echo "=== Running macdeployqt6 ===" - install_name_tool -add_rpath $BREW_PREFIX/lib $MACOS_DIR/SerialPrograms - otool -l $MACOS_DIR/SerialPrograms | grep -A2 LC_RPATH - "$QT_BIN/macdeployqt6" $APP_DIR -no-strip -verbose=3 - - echo "=== Fixing install IDs in copied libs ===" - for dylib in "$FW_DIR"/*.dylib; do - [ -f "$dylib" ] || continue - base=$(basename "$dylib") - install_name_tool -id "@executable_path/../Frameworks/$base" "$dylib" + QT_LIB="$QT_ROOT/lib" + QT_PLUGINS="$QT_ROOT/plugins" + QT_FRAMEWORKS="Core Gui Widgets Network SerialPort Multimedia MultimediaWidgets Concurrent DBus OpenGL OpenGLWidgets Svg PrintSupport" + + echo "=== Copying Qt frameworks $(architecture) ===" + for fw in $QT_FRAMEWORKS; do + [ -d "$QT_LIB/Qt${fw}.framework" ] || { echo "WARNING: Qt${fw}.framework not found, skipping"; continue; } + cp -R "$QT_LIB/Qt${fw}.framework" "$FW_DIR/" + FW_BINARY="$FW_DIR/Qt${fw}.framework/Versions/A/Qt${fw}" + if [ -f "$FW_BINARY" ]; then + NUM_ARCHS=$(lipo -archs "$FW_BINARY" 2>/dev/null | wc -w | tr -d ' ') + [ "$NUM_ARCHS" -gt 1 ] && lipo "$FW_BINARY" -thin "$LIPO_ARCH" -output "$FW_BINARY" || true + fi done - echo "=== Rewriting internal dylib references ===" - for dylib in "$FW_DIR"/*.dylib; do - [ -f "$dylib" ] || continue - for linked in $(otool -L "$dylib" | awk 'NR>1 {print $1}' | grep "$BREW_PREFIX" || true); do - base=$(basename "$linked") - if [ -f "$FW_DIR/$base" ]; then - install_name_tool -change "$linked" "@executable_path/../Frameworks/$base" "$dylib" - fi + echo "=== Copying Qt plugins $(architecture) ===" + for plugin_type in imageformats platforms styles iconengines platforminputcontexts multimedia tls; do + [ -d "$QT_PLUGINS/$plugin_type" ] || continue + mkdir -p "$PLUGINS_DIR/$plugin_type" + for plugin in "$QT_PLUGINS/$plugin_type/"*.dylib; do + [ -f "$plugin" ] || continue + dest="$PLUGINS_DIR/$plugin_type/$(basename "$plugin")" + cp "$plugin" "$dest" + NUM_ARCHS=$(lipo -archs "$dest" 2>/dev/null | wc -w | tr -d ' ') + [ "$NUM_ARCHS" -gt 1 ] && lipo "$dest" -thin "$LIPO_ARCH" -output "$dest" || true done done - echo "=== Rewriting main executable references ===" - for linked in $(otool -L "$MACOS_DIR/SerialPrograms" | awk 'NR>1 {print $1}' | grep "$BREW_PREFIX" || true); do - base=$(basename "$linked") - if [ -f "$FW_DIR/$base" ]; then - install_name_tool -change "$linked" "@executable_path/../Frameworks/$base" "$MACOS_DIR/SerialPrograms" + echo "=== Recursively resolving all native dependencies ===" + ALL_RPATHS=$(otool -l "$MACOS_DIR/SerialPrograms" 2>/dev/null \ + | awk '/cmd LC_RPATH/{f=1} f && /path /{print $2; f=0}' || true) + + ALL_RPATHS="$ALL_RPATHS + $BREW_PREFIX/lib + $BREW_PREFIX/opt/gcc/lib/gcc/current" + + add_rpaths_from() { + local binary="$1" + local new + new=$(otool -l "$binary" 2>/dev/null \ + | awk '/cmd LC_RPATH/{f=1} f && /path /{print $2; f=0}' || true) + if [ -n "$new" ]; then + ALL_RPATHS="$ALL_RPATHS $new" fi + } + + resolve_rpath() { + local libname="${1#@rpath/}" + while IFS= read -r rpath; do + [ -n "$rpath" ] || continue + [ -f "$rpath/$libname" ] && echo "$rpath/$libname" && return 0 + done <<< "$ALL_RPATHS" + return 0 + } + + copy_dep() { + local dep="$1" + + if echo "$dep" | grep -q '^@rpath/'; then + dep=$(resolve_rpath "$dep") + [ -z "$dep" ] && return 0 + fi + case "$dep" in @*|/usr/lib/*|/System/*|/Library/*) return 0 ;; esac + + if echo "$dep" | grep -q '\.framework/'; then + local fw_path fw_name dest_fw fw_bin num + fw_path=$(echo "$dep" | sed 's|\(.*\.framework\)/.*|\1|') + fw_name=$(basename "$fw_path" .framework) + dest_fw="$FW_DIR/${fw_name}.framework" + if [ ! -d "$dest_fw" ]; then + [ -d "$fw_path" ] || return 0 + echo " + framework: $fw_name" + cp -R "$fw_path" "$FW_DIR/" || return 0 + fw_bin="$dest_fw/Versions/A/$fw_name" + if [ -f "$fw_bin" ]; then + add_rpaths_from "$fw_path/Versions/A/$fw_name" + num=$(lipo -archs "$fw_bin" 2>/dev/null | wc -w | tr -d ' ') + [ "$num" -gt 1 ] && lipo "$fw_bin" -thin "$LIPO_ARCH" -output "$fw_bin" || true + resolve_deps "$fw_bin" || true + fi + fi + else + local base dest num + base=$(basename "$dep") + dest="$FW_DIR/$base" + if [ ! -f "$dest" ] && [ -f "$dep" ]; then + echo " + dylib: $base" + add_rpaths_from "$dep" + cp "$dep" "$dest" || return 0 + num=$(lipo -archs "$dest" 2>/dev/null | wc -w | tr -d ' ') + [ "$num" -gt 1 ] && lipo "$dest" -thin "$LIPO_ARCH" -output "$dest" || true + resolve_deps "$dest" || true + fi + fi + return 0 + } + + resolve_deps() { + local binary="$1" + local dep + while IFS= read -r dep; do + copy_dep "$dep" + done < <(otool -L "$binary" 2>/dev/null | awk 'NR>1 {print $1}' \ + | grep -Ev '^(/usr/lib/|/System/|/Library/)' || true) + } + + resolve_deps "$MACOS_DIR/SerialPrograms" + for fw in $QT_FRAMEWORKS; do + FW_BINARY="$FW_DIR/Qt${fw}.framework/Versions/A/Qt${fw}" + if [ -f "$FW_BINARY" ]; then + add_rpaths_from "$QT_LIB/Qt${fw}.framework/Versions/A/Qt${fw}" + resolve_deps "$FW_BINARY" + fi + done + + find "$PLUGINS_DIR" -name "*.dylib" -type f -print0 | while IFS= read -r -d '' plugin; do + add_rpaths_from "$plugin" + resolve_deps "$plugin" + done + + echo "=== Copying ONNX Runtime ===" + ONNX_VERSION="1.23.2" + REPO_ROOT="$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public" + ONNX_LIB_DIR="$REPO_ROOT/3rdPartyBinaries/onnxruntime-osx-$LIPO_ARCH-$ONNX_VERSION/lib" + if [ -d "$ONNX_LIB_DIR" ]; then + for lib in "$ONNX_LIB_DIR/"libonnxruntime*.dylib; do + real=$(readlink -f "$lib" 2>/dev/null || echo "$lib") + [ -f "$real" ] || continue + real_name=$(basename "$real") + cp "$real" "$FW_DIR/$real_name" + done + else + echo "WARNING: ONNX Runtime not found at $ONNX_LIB_DIR" + fi + + echo "=== Copying GCC runtime libraries ===" + GCC_LIB_PATH="$(brew --prefix)/opt/gcc/lib/gcc/current" + if [ -d "$GCC_LIB_PATH" ]; then + for lib in libgcc_s.1.1 libgcc_s.1 libgfortran.5 libquadmath.0 libgomp.1; do + src="$GCC_LIB_PATH/$lib.dylib" + [ -f "$src" ] || [ -L "$src" ] || continue + real=$(readlink -f "$src" 2>/dev/null || echo "$src") + [ -f "$real" ] || continue + real_name=$(basename "$real") + if [ ! -f "$FW_DIR/$real_name" ]; then + echo " + GCC runtime: $real_name" + cp "$real" "$FW_DIR/$real_name" + fi + done + else + echo "WARNING: GCC runtime not found at $GCC_LIB_PATH" + fi + + echo "=== Fixing all install IDs and references ===" + fix_refs() { + local binary="$1" + while IFS= read -r linked; do + if echo "$linked" | grep -q '\.framework/'; then + local fw_name fw_suffix + fw_name=$(echo "$linked" | sed 's|.*[/]\([A-Za-z0-9._-]*\.framework\)/.*|\1|' | sed 's|\.framework$||') + fw_suffix=$(echo "$linked" | sed 's|.*\.framework||') + if [ -d "$FW_DIR/${fw_name}.framework" ]; then + install_name_tool -change "$linked" \ + "@executable_path/../Frameworks/${fw_name}.framework${fw_suffix}" \ + "$binary" 2>/dev/null || true + fi + else + local base + base=$(basename "$linked") + if [ -f "$FW_DIR/$base" ]; then + install_name_tool -change "$linked" \ + "@executable_path/../Frameworks/$base" \ + "$binary" 2>/dev/null || true + fi + fi + done < <(otool -L "$binary" 2>/dev/null | awk 'NR>1 {print $1}' \ + | grep -Ev '^(@executable_path|@loader_path|/usr/lib/|/System/|/Library/)' || true) + } + + echo "=== Fix install IDs and refs for flat dylibs ===" + find "$FW_DIR" -maxdepth 1 -name "*.dylib" -type f -print0 | while IFS= read -r -d '' dylib; do + base=$(basename "$dylib") + install_name_tool -id "@executable_path/../Frameworks/$base" "$dylib" + fix_refs "$dylib" + done + + echo "=== Fix install IDs and refs for Qt framework binaries ===" + for fw in $QT_FRAMEWORKS; do + FW_BINARY="$FW_DIR/Qt${fw}.framework/Versions/A/Qt${fw}" + [ -f "$FW_BINARY" ] || continue + install_name_tool -id \ + "@executable_path/../Frameworks/Qt${fw}.framework/Versions/A/Qt${fw}" \ + "$FW_BINARY" + fix_refs "$FW_BINARY" done - echo "=== Creating tarballs for build and symbols ===" - mkdir -p "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-build" + echo "=== Fix plugin refs ===" + find "$PLUGINS_DIR" -name "*.dylib" -type f -print0 | while IFS= read -r -d '' plugin; do + fix_refs "$plugin" + done + + echo "=== Fix main executable refs ===" + install_name_tool -add_rpath "$BREW_PREFIX/lib" "$MACOS_DIR/SerialPrograms" 2>/dev/null || true + install_name_tool -add_rpath "@executable_path/../Frameworks" "$MACOS_DIR/SerialPrograms" 2>/dev/null || true + fix_refs "$MACOS_DIR/SerialPrograms" + + echo "=== Preparing build folder ===" cd "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)" VERSION="${{ parameters.versionMajor }}.${{ parameters.versionMinor }}.${{ parameters.versionPatch }}" FOLDER_NAME="SerialPrograms-$VERSION-MacOS-${{ parameters.architecture }}" mkdir -p "$FOLDER_NAME" - cp -R SerialPrograms.app "$FOLDER_NAME/" - echo "=== Moving Firmware to outside of the .app bundle ===" - mv "$FOLDER_NAME/SerialPrograms.app/Contents/Firmware" "$FOLDER_NAME/" + echo "=== Moving .app bundle and related resources to $FOLDER_NAME ===" + mv SerialPrograms.app "$FOLDER_NAME/" + mv "Firmware" "$FOLDER_NAME/" + mv "Resources" "$FOLDER_NAME/" + mkdir -p "$FOLDER_NAME/SerialPrograms.app/Contents/Resources" - echo "=== Removing Scripts folder from .app bundle ===" - rm -rf "$FOLDER_NAME/SerialPrograms.app/Contents/Scripts" - - tar -czf "cache-build/macos-build-${{ parameters.architecture }}.tar.gz" "$FOLDER_NAME" + echo "=== Archiving debug symbols ===" tar -czf "cache-symbols/macos-symbols-${{ parameters.architecture }}.tar.gz" cache-symbols/SerialPrograms.dSYM echo "Deployment complete for $(architecture) with enclosing folder: $FOLDER_NAME" displayName: 'Deploy app' condition: succeeded() - - task: Cache@2 - displayName: 'Cache the build artifact' + - task: InstallAppleCertificate@2 + displayName: 'Install Apple certificate' inputs: - key: 'macos-build-${{ parameters.architecture }} | "$(Build.BuildId)"' - path: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-build' + certSecureFile: 'CodesignCertMac.p12' + certPwd: $(CERT_PW) + keychain: custom + keychainPassword: $(KEYCHAIN_PW) + customKeychainPath: '$(HOME)/Library/Keychains/azure-signing.keychain-db' + deleteCert: true + deleteCustomKeychain: true + opensslPkcsArgs: ${{ parameters.openssl_pkcs_args }} + condition: succeeded() + + - script: | + set -e + KEYCHAIN="$HOME/Library/Keychains/azure-signing.keychain-db" + + echo "=== Activating keychain ===" + security list-keychains -d user -s "$KEYCHAIN" "$HOME/Library/Keychains/login.keychain-db" + security unlock-keychain -p "$(KEYCHAIN_PW)" "$KEYCHAIN" + + echo "=== Allowing codesign to access private key ===" + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$(KEYCHAIN_PW)" "$KEYCHAIN" + + echo "=== Verifying identities ===" + security find-identity -p codesigning -v + env: + KEYCHAIN_PW: $(KEYCHAIN_PW) + displayName: 'Authorize signing key' + condition: succeeded() + + - script: | + set -euo pipefail + export PATH=${{ parameters.macos_path }} + VERSION="${{ parameters.versionMajor }}.${{ parameters.versionMinor }}.${{ parameters.versionPatch }}" + FOLDER_NAME="SerialPrograms-$VERSION-MacOS-${{ parameters.architecture }}" + BUILD_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)" + APP_DIR="$BUILD_DIR/$FOLDER_NAME/SerialPrograms.app" + ENTITLEMENTS_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/SerialPrograms/cmake/MacOSXEntitlements.plist" + + echo "=== Signing app ===" + codesign --deep --force --options runtime --timestamp --entitlements "$ENTITLEMENTS_DIR" --sign "$SIGN_IDENTITY" "$APP_DIR" + + echo "=== Verifying code signature ===" + codesign --verify --deep --strict --verbose=2 "$APP_DIR" + env: + SIGN_IDENTITY: $(SIGNING_IDENTITY) + displayName: '(APP) Codesign, verify' + condition: succeeded() + + - script: | + set -euo pipefail + export PATH=${{ parameters.macos_path }} + VERSION="${{ parameters.versionMajor }}.${{ parameters.versionMinor }}.${{ parameters.versionPatch }}" + BUILD_TYPE="${{ parameters.buildType }}" + FOLDER_NAME="SerialPrograms-$VERSION-MacOS-${{ parameters.architecture }}" + BUILD_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)" + DMG_PATH="$(Pipeline.Workspace)/SerialPrograms-MacOS-$(compiler)-$(architecture).dmg" + STAGING_DIR="$(Pipeline.Workspace)/dmg-staging" + SETTINGS_FILE="$(Pipeline.Workspace)/dmg-settings.py" + + echo "=== Creating DMG staging directory ===" + rm -rf "$STAGING_DIR" + mkdir -p "$STAGING_DIR" + cp -R "$BUILD_DIR/$FOLDER_NAME" "$STAGING_DIR/SerialPrograms" + + echo "=== Setting folder icon ===" + ICNS_PATH="$STAGING_DIR/SerialPrograms/SerialPrograms.app/Contents/Resources/icon.icns" + fileicon set "$STAGING_DIR/SerialPrograms" "$ICNS_PATH" + echo "✓ Folder icon set from $ICNS_PATH" + + echo "=== Writing dmgbuild settings ===" + printf '%s\n' \ + "format = 'UDZO'" \ + "files = [defines['app']]" \ + "symlinks = {'Applications': '/Applications'}" \ + "icon_locations = {'SerialPrograms': (150, 100), 'Applications': (490, 100)}" \ + "background = 'builtin-arrow'" \ + "window_rect = ((200, 120), (520, 240))" \ + "icon_size = 100" \ + "text_size = 12" \ + "default_view = 'icon-view'" \ + "show_icon_preview = False" \ + > "$SETTINGS_FILE" + + echo "=== Creating DMG ===" + python3 -m dmgbuild \ + -s "$SETTINGS_FILE" \ + -D app="$STAGING_DIR/SerialPrograms" \ + "SerialPrograms $VERSION-$BUILD_TYPE" \ + "$DMG_PATH" + + rm -rf "$STAGING_DIR" "$SETTINGS_FILE" + echo "=== DMG created: $DMG_PATH ===" + displayName: 'Create DMG' + condition: succeeded() + + - script: | + set -euo pipefail + DMG_PATH="$(Pipeline.Workspace)/SerialPrograms-MacOS-$(compiler)-$(architecture).dmg" + + echo "=== Signing DMG ===" + codesign --sign "$SIGN_IDENTITY" --timestamp "$DMG_PATH" + + echo "=== Verifying DMG signature ===" + codesign --verify --verbose=2 "$DMG_PATH" + env: + SIGN_IDENTITY: $(SIGNING_IDENTITY) + displayName: '(DMG) Codesign, verify' + condition: succeeded() + + - script: | + set -euo pipefail + DMG_PATH="$(Pipeline.Workspace)/SerialPrograms-MacOS-$(compiler)-$(architecture).dmg" + + SUBMIT_JSON=$(xcrun notarytool submit "$DMG_PATH" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_PW" \ + --team-id "$APPLE_TEAM_ID" \ + --output-format json) + + REQUEST_ID=$(echo "$SUBMIT_JSON" | jq -r '.id') + [ -n "$REQUEST_ID" ] && [ "$REQUEST_ID" != "null" ] + + echo "##vso[task.setvariable variable=DMG_NOTARY_REQUEST_ID]$REQUEST_ID" + env: + APPLE_ID: $(APPLE_ID) + APPLE_APP_PW: $(APPLE_APP_PW) + APPLE_TEAM_ID: $(APPLE_TEAM_ID) + displayName: '(DMG) Submit for notarization' + condition: succeeded() + + - script: | + set -euo pipefail + MAX_WAIT=14400 + INTERVAL=30 + ELAPSED=0 + + while true; do + STATUS=$(xcrun notarytool info "$(DMG_NOTARY_REQUEST_ID)" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_PW" \ + --team-id "$APPLE_TEAM_ID" \ + --output-format json | jq -r '.status') + + case "$STATUS" in + Accepted) break ;; + Invalid) + xcrun notarytool log "$(DMG_NOTARY_REQUEST_ID)" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_PW" \ + --team-id "$APPLE_TEAM_ID" + exit 1 ;; + esac + + sleep "$INTERVAL" + ELAPSED=$((ELAPSED + INTERVAL)) + [ "$ELAPSED" -lt "$MAX_WAIT" ] + done + env: + APPLE_ID: $(APPLE_ID) + APPLE_APP_PW: $(APPLE_APP_PW) + APPLE_TEAM_ID: $(APPLE_TEAM_ID) + displayName: '(DMG) Wait for notarization' + condition: succeeded() + + - script: | + set -euo pipefail + DMG_PATH="$(Pipeline.Workspace)/SerialPrograms-MacOS-$(compiler)-$(architecture).dmg" + CACHE_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/cache-notarized" + + echo "=== Stapling notarization ticket to DMG ===" + xcrun stapler staple "$DMG_PATH" + + echo "=== Validating staple ===" + xcrun stapler validate "$DMG_PATH" + + echo "=== Gatekeeper assessment ===" + spctl --assess --type open --context context:primary-signature --verbose=4 "$DMG_PATH" + + echo "=== Moving DMG to cache ===" + rm -rf "$CACHE_DIR" + mkdir -p "$CACHE_DIR" + mv "$DMG_PATH" "$CACHE_DIR/" + displayName: '(DMG) Staple, verify' condition: succeeded() + - script: | + set -e + DMG_NAME="SerialPrograms-MacOS-$(compiler)-$(architecture).dmg" + ZIP_NAME="SerialPrograms-MacOS-$(compiler)-$(architecture).zip" + CACHE_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/cache-notarized" + BUILD_TYPE="${{ parameters.buildType }}" + + cd "$CACHE_DIR" + if [ "$BUILD_TYPE" = "PrivateBeta" ]; then + echo "=== Creating password-protected archive for PrivateBeta ===" + 7z a -tzip -mx=9 "-p$ARTIFACT_PASSWORD" -mem=AES256 "$ZIP_NAME" "$DMG_NAME" + rm "$DMG_NAME" + echo "=== Password-protected .zip created ===" + else + echo "=== DMG ready for publish ===" + fi + displayName: 'Prepare artifact for publish' + condition: succeeded() + env: + ARTIFACT_PASSWORD: $(ARTIFACT_PASSWORD) + + - task: Cache@2 + displayName: 'Cache notarized artifact for GitHub release' + inputs: + key: 'macos-notarized-${{ parameters.architecture }} | "$(Build.BuildId)"' + path: '$(Pipeline.Workspace)/Arduino-Source-Internal/cache-notarized' + condition: and(succeeded(), eq('${{ parameters.targetOS }}', 'All'), or(eq('${{ parameters.buildType }}', 'Release'), eq('${{ parameters.buildType }}', 'Beta'))) + - task: Cache@2 displayName: 'Cache debug symbols' inputs: key: 'macos-symbols-${{ parameters.architecture }} | "$(Build.BuildId)"' path: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-symbols' condition: succeeded() + + - task: PublishPipelineArtifact@1 + displayName: 'Publish MacOS artifact (PrivateBeta)' + inputs: + targetPath: '$(Pipeline.Workspace)/Arduino-Source-Internal/cache-notarized/SerialPrograms-MacOS-$(compiler)-$(architecture).zip' + artifactName: 'SerialPrograms-MacOS-$(compiler)-$(architecture)' + publishLocation: 'pipeline' + condition: and(succeeded(), eq('${{ parameters.buildType }}', 'PrivateBeta')) + + - task: PublishPipelineArtifact@1 + displayName: 'Publish MacOS artifact' + inputs: + targetPath: '$(Pipeline.Workspace)/Arduino-Source-Internal/cache-notarized/SerialPrograms-MacOS-$(compiler)-$(architecture).dmg' + artifactName: 'SerialPrograms-MacOS-$(compiler)-$(architecture)' + publishLocation: 'pipeline' + condition: and(succeeded(), ne('${{ parameters.buildType }}', 'PrivateBeta')) diff --git a/.azure-pipelines/templates/macos-notarize.yml b/.azure-pipelines/templates/macos-notarize.yml deleted file mode 100644 index 72e8ebc564..0000000000 --- a/.azure-pipelines/templates/macos-notarize.yml +++ /dev/null @@ -1,214 +0,0 @@ -# templates/macos-notarize.yml -# Template for codesigning and notarizing SerialPrograms on MacOS - -parameters: - stageName: '' - displayName: '' - dependsOnStage: '' - buildType: '' - poolName: '' - imageName: '' - architecture: '' - compiler: '' - cmake_preset: '' - macos_path: '' - openssl_pkcs_args: '' - versionMajor: 0 - versionMinor: 0 - versionPatch: 0 - condition: '' - -stages: -- stage: ${{ parameters.stageName }} - displayName: ${{ parameters.displayName }} - dependsOn: ${{ parameters.dependsOnStage }} - condition: ${{ parameters.condition }} - - jobs: - - job: Codesign - displayName: Codesign ${{ parameters.architecture }} - timeoutInMinutes: 120 - cancelTimeoutInMinutes: 1 - - pool: - name: ${{ parameters.poolName }} - demands: - - Agent.OSArchitecture -equals ${{ lower(parameters.architecture) }} - - variables: - architecture: ${{ parameters.architecture }} - compiler: ${{ parameters.compiler }} - cmake_preset: ${{ parameters.cmake_preset }} - - steps: - - template: checkout.yml - - - task: Cache@2 - displayName: 'Restore the cached build artifact' - inputs: - key: 'macos-build-${{ parameters.architecture }} | "$(Build.BuildId)"' - path: $(Pipeline.Workspace) - restoreKeys: | - macos-build-${{ parameters.architecture }} | "$(Build.BuildId)" - condition: succeeded() - - - task: InstallAppleCertificate@2 - displayName: 'Install Apple certificate' - inputs: - certSecureFile: 'CodesignCertMac.p12' - certPwd: $(CERT_PW) - keychain: custom - keychainPassword: $(KEYCHAIN_PW) - customKeychainPath: '$(HOME)/Library/Keychains/azure-signing.keychain-db' - deleteCert: true - deleteCustomKeychain: true - opensslPkcsArgs: ${{ parameters.openssl_pkcs_args }} - condition: succeeded() - - - script: | - set -e - KEYCHAIN="$HOME/Library/Keychains/azure-signing.keychain-db" - - echo "=== Activating keychain ===" - security list-keychains -d user -s "$KEYCHAIN" "$HOME/Library/Keychains/login.keychain-db" - security unlock-keychain -p "$(KEYCHAIN_PW)" "$KEYCHAIN" - - echo "=== Allowing codesign to access private key ===" - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$(KEYCHAIN_PW)" "$KEYCHAIN" - - echo "=== Verifying identities ===" - security find-identity -p codesigning -v - env: - KEYCHAIN_PW: $(KEYCHAIN_PW) - displayName: 'Authorize signing key' - condition: succeeded() - - - script: | - set -euo pipefail - export PATH=${{ parameters.macos_path }} - VERSION="${{ parameters.versionMajor }}.${{ parameters.versionMinor }}.${{ parameters.versionPatch }}" - FOLDER_NAME="SerialPrograms-$VERSION-MacOS-${{ parameters.architecture }}" - APP_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/$FOLDER_NAME/SerialPrograms.app" - ZIP_PATH="$(Pipeline.Workspace)/SerialPrograms-MacOS-$(compiler)-$(architecture).zip" - ENTITLEMENTS_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/SerialPrograms/cmake/MacOSXEntitlements.plist" - - echo "=== Extracting tarball ===" - tar -xzf "$(Pipeline.Workspace)/macos-build-${{ parameters.architecture }}.tar.gz" - - echo "=== Signing app ===" - codesign --deep --force --options runtime --timestamp --entitlements "$ENTITLEMENTS_DIR" --sign "$SIGN_IDENTITY" "$APP_DIR" - - echo "=== Verifying code signature ===" - codesign --verify --deep --strict --verbose=2 "$APP_DIR" - - echo "Creating ZIP for notarization..." - ditto -c -k --keepParent "$APP_DIR" "$ZIP_PATH" - env: - SIGN_IDENTITY: $(SIGNING_IDENTITY) - displayName: 'Codesign, verify, create ZIP' - condition: succeeded() - - - script: | - set -euo pipefail - ZIP_PATH="$(Pipeline.Workspace)/SerialPrograms-MacOS-$(compiler)-$(architecture).zip" - - SUBMIT_JSON=$(xcrun notarytool submit "$ZIP_PATH" \ - --apple-id "$APPLE_ID" \ - --password "$APPLE_APP_PW" \ - --team-id "$APPLE_TEAM_ID" \ - --output-format json) - - REQUEST_ID=$(echo "$SUBMIT_JSON" | jq -r '.id') - [ -n "$REQUEST_ID" ] && [ "$REQUEST_ID" != "null" ] - - echo "##vso[task.setvariable variable=NOTARY_REQUEST_ID]$REQUEST_ID" - env: - APPLE_ID: $(APPLE_ID) - APPLE_APP_PW: $(APPLE_APP_PW) - APPLE_TEAM_ID: $(APPLE_TEAM_ID) - displayName: 'Submit for notarization' - condition: succeeded() - - - script: | - set -euo pipefail - MAX_WAIT=14400 - INTERVAL=30 - ELAPSED=0 - - while true; do - STATUS=$(xcrun notarytool info "$(NOTARY_REQUEST_ID)" \ - --apple-id "$APPLE_ID" \ - --password "$APPLE_APP_PW" \ - --team-id "$APPLE_TEAM_ID" \ - --output-format json | jq -r '.status') - - case "$STATUS" in - Accepted) break ;; - Invalid) - xcrun notarytool log "$(NOTARY_REQUEST_ID)" \ - --apple-id "$APPLE_ID" \ - --password "$APPLE_APP_PW" \ - --team-id "$APPLE_TEAM_ID" - exit 1 ;; - esac - - sleep "$INTERVAL" - ELAPSED=$((ELAPSED + INTERVAL)) - [ "$ELAPSED" -lt "$MAX_WAIT" ] - done - env: - APPLE_ID: $(APPLE_ID) - APPLE_APP_PW: $(APPLE_APP_PW) - APPLE_TEAM_ID: $(APPLE_TEAM_ID) - displayName: 'Wait for notarization' - condition: succeeded() - - - script: | - set -euo pipefail - VERSION="${{ parameters.versionMajor }}.${{ parameters.versionMinor }}.${{ parameters.versionPatch }}" - FOLDER_NAME="SerialPrograms-$VERSION-MacOS-${{ parameters.architecture }}" - APP_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/$FOLDER_NAME/SerialPrograms.app" - xcrun stapler staple "$APP_DIR" - spctl --assess --type execute --verbose=4 "$APP_DIR" - displayName: 'Staple and verify' - condition: succeeded() - - - script: | - set -e - VERSION="${{ parameters.versionMajor }}.${{ parameters.versionMinor }}.${{ parameters.versionPatch }}" - FOLDER_NAME="SerialPrograms-$VERSION-MacOS-${{ parameters.architecture }}" - BUILD_TYPE="${{ parameters.buildType }}" - - cd "$(Pipeline.Workspace)/Arduino-Source-Internal" - if [ "$BUILD_TYPE" = "PrivateBeta" ]; then - echo "=== Creating password-protected archive for PrivateBeta ===" - 7z a -tzip -mx=9 "-p$ARTIFACT_PASSWORD" -mem=AES256 "SerialPrograms-MacOS-$(compiler)-$(architecture).zip" "$FOLDER_NAME" - else - echo "=== Creating .zip with enclosing folder ===" - 7z a -tzip -mx=9 "SerialPrograms-MacOS-$(compiler)-$(architecture).zip" "$FOLDER_NAME" - fi - - echo "=== Creating cache directory and moving zipped archive ===" - CACHE_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/cache-notarized" - mkdir -p "$CACHE_DIR" - mv "$(Pipeline.Workspace)/Arduino-Source-Internal/SerialPrograms-MacOS-$(compiler)-$(architecture).zip" "$CACHE_DIR/" - - echo "=== .zip created with enclosing folder: $FOLDER_NAME ===" - displayName: 'Create a zipped archive' - condition: succeeded() - env: - ARTIFACT_PASSWORD: $(ARTIFACT_PASSWORD) - - - task: Cache@2 - displayName: 'Cache the notarized artifact' - inputs: - key: 'macos-notarized-${{ parameters.architecture }} | "$(Build.BuildId)"' - path: '$(Pipeline.Workspace)/Arduino-Source-Internal/cache-notarized' - condition: succeeded() - - - task: PublishPipelineArtifact@1 - displayName: 'Publish notarized app' - inputs: - targetPath: '$(Pipeline.Workspace)/Arduino-Source-Internal/cache-notarized/SerialPrograms-MacOS-$(compiler)-$(architecture).zip' - artifact: 'SerialPrograms-MacOS-$(compiler)-$(architecture)' - condition: succeeded() diff --git a/.azure-pipelines/templates/update-github-telemetry.yml b/.azure-pipelines/templates/update-github-telemetry.yml index 6e5e67fa8e..5d1c864ef0 100644 --- a/.azure-pipelines/templates/update-github-telemetry.yml +++ b/.azure-pipelines/templates/update-github-telemetry.yml @@ -65,7 +65,7 @@ stages: return 0 } - $token = "$(GITHUB_PAT)" + $token = $env:GITHUB_TOKEN $repo = "${{ parameters.telemetryRepo }}" $filePath = "Developer/Telemetry.json" $branch = "main" @@ -247,7 +247,7 @@ stages: exit 1 } env: - GITHUB_PAT: $(GITHUB_PAT) + GITHUB_TOKEN: $(GITHUB_PAT) TELEMETRY_KEY_1: $(TELEMETRY_KEY_1) TELEMETRY_KEY_2: $(TELEMETRY_KEY_2) TELEMETRY_VERSION: "v${{ parameters.versionMajor }}.${{ parameters.versionMinor }}.${{ parameters.versionPatch }}" diff --git a/.azure-pipelines/templates/update-github-version.yml b/.azure-pipelines/templates/update-github-version.yml index b98085b53f..eb062669da 100644 --- a/.azure-pipelines/templates/update-github-version.yml +++ b/.azure-pipelines/templates/update-github-version.yml @@ -96,7 +96,7 @@ stages: return 0 } - $token = "$(GITHUB_PAT)" + $token = $env:GITHUB_TOKEN $repo = "${{ parameters.versionRepo }}" $filePath = "LatestVersion.json" $changeLogPath = "ChangeLog.md" diff --git a/IconResource/SerialPrograms.appdata.xml b/IconResource/SerialPrograms.appdata.xml new file mode 100644 index 0000000000..1dc6e842ec --- /dev/null +++ b/IconResource/SerialPrograms.appdata.xml @@ -0,0 +1,65 @@ + + + com.gameautomation.SerialPrograms + + + CC0-1.0 + MIT + + + SerialPrograms + Game Automation Software + + Automate various games using a compatible microcontroller from your computer. + + Tick-precise controller inputs + Remote control via Discord + Machine learning, visual/audio capabilities to accomplish tasks + + + + + + /usr/share/icons/hicolor/256x256/apps/SerialPrograms.png + + + + com.gameautomation.SerialPrograms.desktop + + + + Utility + HardwareSettings + + + + automation + serial + computer + control + + + + https://github.com/PokemonAutomation/Arduino-Source + https://github.com/PokemonAutomation/Arduino-Source/issues + https://pokemonautomation.github.io + + + + + + + + + Initial AppStream metadata + + + + + diff --git a/SerialPrograms/CMakeLists.txt b/SerialPrograms/CMakeLists.txt index 9ffc2413ac..7d4654d73d 100644 --- a/SerialPrograms/CMakeLists.txt +++ b/SerialPrograms/CMakeLists.txt @@ -47,7 +47,7 @@ else() endif() if(APPLE) - set(DEST_DIR "$/..") + set(DEST_DIR "$/../../..") elseif(PACKAGE_BUILD) set(DEST_DIR "$/Output") set(WIN_DEPLOY_DIR "${DEST_DIR}/Binaries64") diff --git a/SerialPrograms/Source/CommonFramework/Globals.cpp b/SerialPrograms/Source/CommonFramework/Globals.cpp index 16f173ecde..c03f9ff93a 100644 --- a/SerialPrograms/Source/CommonFramework/Globals.cpp +++ b/SerialPrograms/Source/CommonFramework/Globals.cpp @@ -1,4 +1,4 @@ -/* Globals +/* Globals * * From: https://github.com/PokemonAutomation/ * @@ -9,6 +9,9 @@ #include #include #include +#if defined(__APPLE__) +#include +#endif #include "Globals.h" namespace PokemonAutomation{ @@ -110,22 +113,92 @@ const std::string COMPILER_VERSION = "Unknown Compiler"; const size_t LOG_HISTORY_LINES = 10000; +#if defined(__APPLE__) +static std::string g_startup_profile; + +void set_startup_profile(int& argc, char* argv[]){ + for (int i = 1; i + 1 < argc; i++){ + if (strcmp(argv[i], "--profile") == 0){ + QString profile = QString::fromUtf8(argv[i + 1]); + for (QChar& c : profile){ + if (!c.isLetterOrNumber() && c != u'_' && c != u'-') c = u'_'; + } + g_startup_profile = profile.toStdString(); + // Shift everything after --profile down by 2. + for (int j = i; j + 2 < argc; j++){ + argv[j] = argv[j + 2]; + } + argc -= 2; + return; + } + } +} + +const std::string& STARTUP_PROFILE(){ + return g_startup_profile; +} +#endif namespace{ QString get_application_base_dir_path(){ QString application_dir_path = qApp->applicationDirPath(); - if (application_dir_path.endsWith(".app/Contents/MacOS")){ - // a macOS bundle. Change working directory to the folder that hosts the .app folder. - QString app_bundle_path = application_dir_path.chopped(15); - QString base_folder_path = QFileInfo(app_bundle_path).dir().absolutePath(); - return base_folder_path; +#if defined(__APPLE__) + // Use CFBundle to find the .app bundle path. Change working directory to the folder that hosts the .app bundle. + CFURLRef bundleURL = CFBundleCopyBundleURL(CFBundleGetMainBundle()); + if (bundleURL){ + CFStringRef cfPath = CFURLCopyFileSystemPath(bundleURL, kCFURLPOSIXPathStyle); + CFRelease(bundleURL); + if (cfPath){ + QString bundlePath = QDir::cleanPath(QString::fromCFString(cfPath)); + CFRelease(cfPath); + if (bundlePath.endsWith(".app")){ + return QFileInfo(bundlePath).dir().absolutePath(); + } + } } +#elif defined(__linux__) + // Check for AppImage environment variables to find the root directory, if running as an AppImage. + // PA_APPIMAGE_DIR is set by Azure via a patched AppRun script. + QByteArray dir = qgetenv("PA_APPIMAGE_DIR"); + if (!dir.isEmpty()){ + return QString::fromUtf8(dir); + } + QByteArray path = qgetenv("APPIMAGE"); + if (!path.isEmpty()){ + return QDir::cleanPath(QFileInfo(QString::fromUtf8(path)).dir().absolutePath()); + } + QByteArray appDirBytes = qgetenv("APPDIR"); + if (!appDirBytes.isEmpty()){ + QString appDir = QString::fromUtf8(appDirBytes); + QFile mountinfo(QStringLiteral("/proc/self/mountinfo")); + if (mountinfo.open(QIODevice::ReadOnly | QIODevice::Text)){ + while (!mountinfo.atEnd()){ + QString line = QString::fromUtf8(mountinfo.readLine()).trimmed(); + int dashSep = line.indexOf(QStringLiteral(" - ")); + if (dashSep < 0){ + continue; + } + QStringList pre = line.left(dashSep).split(u' ', Qt::SkipEmptyParts); + QStringList post = line.mid(dashSep + 3).split(u' ', Qt::SkipEmptyParts); + if (pre.size() < 5 || post.size() < 2){ + continue; + } + QString mountPoint = pre[4].replace(QStringLiteral("\\040"), QStringLiteral(" ")); + QString source = post[1].replace(QStringLiteral("\\040"), QStringLiteral(" ")); + if (mountPoint == appDir && source.endsWith(QStringLiteral(".AppImage"))){ + return QDir::cleanPath(QFileInfo(source).dir().absolutePath()); + } + } + } + } +#endif return application_dir_path; } std::string get_resource_path(){ // Find the resource directory. - QString path = get_application_base_dir_path(); + QString base = get_application_base_dir_path(); + QString path = base; for (size_t c = 0; c < 5; c++){ QString try_path = path + "/Resources/"; QFile file(try_path); @@ -134,11 +207,12 @@ std::string get_resource_path(){ } path += "/.."; } - return (QCoreApplication::applicationDirPath() + "/../Resources/").toStdString(); + return (base + "/Resources/").toStdString(); } std::string get_training_path(){ // Find the training data directory. - QString path = get_application_base_dir_path(); + QString base = get_application_base_dir_path(); + QString path = base; for (size_t c = 0; c < 5; c++){ QString try_path = path + "/TrainingData/"; QFile file(try_path); @@ -147,26 +221,22 @@ std::string get_training_path(){ } path += "/.."; } - return (QCoreApplication::applicationDirPath() + "/../TrainingData/").toStdString(); + return (base + "/TrainingData/").toStdString(); } std::string get_runtime_base_path(){ - // On MacOS, find the writable application support directory - if (QSysInfo::productType() == "macos" || QSysInfo::productType() == "osx"){ - // QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) returns - // "/Users/$USERNAME/Library/Application Support", the parent folder - // to hold application-specific persistent data. - // QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) returns - // "/Users/$USERNAME/Library/Application Support/SerialPrograms", - // the folder where we store persistent data. - QString appSupportPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); - QDir dir(appSupportPath); - if (!dir.exists()){ - dir.mkpath("."); - } - return appSupportPath.toStdString() + "/"; +#if defined(__APPLE__) + // QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) returns + // "/Users/$USERNAME/Library/Application Support/SerialPrograms" + QString appSupportPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + if (!g_startup_profile.empty()){ + appSupportPath += "/Profiles/" + QString::fromStdString(g_startup_profile); } + QDir().mkpath(appSupportPath); + return appSupportPath.toStdString() + "/"; +#else return "./"; +#endif } std::string get_setting_path(){ diff --git a/SerialPrograms/Source/CommonFramework/Globals.h b/SerialPrograms/Source/CommonFramework/Globals.h index 754b585e14..7b68be402d 100644 --- a/SerialPrograms/Source/CommonFramework/Globals.h +++ b/SerialPrograms/Source/CommonFramework/Globals.h @@ -43,6 +43,14 @@ extern const std::string COMPILER_VERSION; extern const size_t LOG_HISTORY_LINES; +// Set a profile for program settings (/UserSettings/PROFILE_NAME/) on MacOS. +// Have to run the program with command-line argument "open -n PATH_TO_APP --args --profile PROFILE_NAME" to set the profile and launch a new window. +// This allows multiple instances of the program to run since settings are no longer shared. +#if defined(__APPLE__) +void set_startup_profile(int& argc, char* argv[]); +const std::string& STARTUP_PROFILE(); +#endif + // Path to the parent folder that holds all other folders, e.g. settings folder, screenshot folder, etc. const std::string& RUNTIME_BASE_PATH(); diff --git a/SerialPrograms/Source/CommonFramework/Main.cpp b/SerialPrograms/Source/CommonFramework/Main.cpp index 2435ccea78..585c2a234d 100644 --- a/SerialPrograms/Source/CommonFramework/Main.cpp +++ b/SerialPrograms/Source/CommonFramework/Main.cpp @@ -77,7 +77,12 @@ class ScopeExit{ int run_program(int argc, char *argv[]){ +#if defined(__APPLE__) + PokemonAutomation::set_startup_profile(argc, argv); QApplication application(argc, argv); +#else + QApplication application(argc, argv); +#endif GlobalOutputRedirector redirect_stdout(std::cout, "stdout", Color()); GlobalOutputRedirector redirect_stderr(std::cerr, "stderr", COLOR_RED); diff --git a/SerialPrograms/Source/CommonFramework/Windows/MainWindow.cpp b/SerialPrograms/Source/CommonFramework/Windows/MainWindow.cpp index 54800a5aee..643725f09b 100644 --- a/SerialPrograms/Source/CommonFramework/Windows/MainWindow.cpp +++ b/SerialPrograms/Source/CommonFramework/Windows/MainWindow.cpp @@ -66,7 +66,16 @@ MainWindow::MainWindow(QWidget* parent) // statusbar = new QStatusBar(this); // statusbar->setObjectName(QString::fromUtf8("statusbar")); // setStatusBar(statusbar); - setWindowTitle(QString::fromStdString(PROGRAM_NAME + " Computer-Control Programs (" + PROGRAM_VERSION + ")")); + std::string title = PROGRAM_NAME + " Computer-Control Programs (" + PROGRAM_VERSION + ")"; +#if defined(__APPLE__) + if (!STARTUP_PROFILE().empty()){ + setWindowTitle(QString::fromStdString(title + " [Profile: " + STARTUP_PROFILE() + "]")); + }else{ + setWindowTitle(QString::fromStdString(title)); + } +#else + setWindowTitle(QString::fromStdString(title)); +#endif QHBoxLayout* hbox = new QHBoxLayout(centralwidget); QVBoxLayout* left_layout = new QVBoxLayout();
Automate various games using a compatible microcontroller from your computer.
Initial AppStream metadata