diff --git a/.azure-pipelines/azure-pipelines.yml b/.azure-pipelines/azure-pipelines.yml new file mode 100644 index 000000000..8593d5e1e --- /dev/null +++ b/.azure-pipelines/azure-pipelines.yml @@ -0,0 +1,509 @@ +# azure-pipelines.yml +# CI pipeline for SerialPrograms + +trigger: none +pr: none + +resources: + repositories: + - repository: Arduino-Source-Internal + type: github + name: PokemonAutomation/Arduino-Source-Internal + endpoint: PokemonAutomation + ref: refs/heads/master + + - repository: Arduino-Source + type: github + name: Koi-3088/Arduino-Source + endpoint: Koi-3088 + ref: refs/heads/azure + +variables: + - group: apple-signing + - group: github + - group: discord-symbols + - group: telemetry + - group: alarm_clock + +parameters: + - name: buildType + displayName: Build Type + type: string + default: Commit + values: [Commit, Release, Beta, PrivateBeta] + + - name: targetOS + displayName: Target OS + type: string + default: All + values: [All, Windows, Linux, MacOS] + + - name: versionMajor + displayName: Version Major + type: number + default: 0 + + - name: versionMinor + displayName: Version Minor + type: number + default: 0 + + - name: versionPatch + displayName: Version Patch + type: number + default: 0 + +stages: + +########################################### +# WAKE-UP SIGNAL # +########################################### +#- template: templates/wake-signal.yml +# parameters: +# stageName: Wake_Up_Signal +# displayName: 'Send Wake-Up Signal' +# dependsOn: [] +# poolName: AlarmClock +# condition: always() + +########################################### +# WINDOWS BUILD JOB # +########################################### + +- stage: Windows_Build_x64 + displayName: Windows Build x64 + #dependsOn: Wake_Up_Signal + dependsOn: [] + condition: and(succeeded(), or(eq('${{ parameters.targetOS }}', 'Windows'), eq('${{ parameters.targetOS }}', 'All'))) + + jobs: + - job: Windows + timeoutInMinutes: 30 + cancelTimeoutInMinutes: 1 + displayName: Windows + + strategy: + matrix: + MSVC: + poolName: 'WindowsPool' + imageName: 'windows-2025' + architecture: 'x64' + compiler: 'MSVC' + qt_version: '6.8.3' + qt_version_major: '6' + qt_modules: 'qtserialport qtmultimedia' + cmake_preset: 'RelWithDebInfo' + 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: '-DCMAKE_PREFIX_PATH=C:/Qt/$(qt_version)/msvc2022_64/lib/cmake -DIS_AZURE_BUILD=TRUE' + + pool: + name: $(poolName) + + steps: + - template: templates/checkout.yml + + - task: BatchScript@1 + inputs: + filename: 'C:/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools/Common7/Tools/VsDevCmd.bat' + arguments: '-arch=x64' + modifyEnvironment: true + displayName: 'Initialize VS Environment' + condition: succeeded() + + - script: | + cd /d "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/SerialPrograms" + cmake --preset=$(cmake_preset) $(cmake_additional_param) $(cmake_version_params) + displayName: 'Configure CMake' + condition: succeeded() + + - script: | + cd /d "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/SerialPrograms" + cmake --build --preset=$(cmake_preset) + displayName: 'Build' + condition: succeeded() + + - powershell: | + Write-Host "=== Running windeployqt ===" + $BUILD_DIR = "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)" + $APP_EXE = "$BUILD_DIR/SerialPrograms.exe" + $BUILD_CACHE = "$BUILD_DIR/cache-build" + $SYMBOLS_CACHE = "$BUILD_DIR/cache-symbols" + New-Item -ItemType Directory -Force -Path $BUILD_CACHE | Out-Null + & "C:/Qt/$(qt_version)/msvc2022_64/bin/windeployqt.exe" --dir "$BUILD_CACHE" --release "$APP_EXE" + New-Item -ItemType Directory -Force -Path $SYMBOLS_CACHE | Out-Null + Write-Host "=== windeployqt complete ===" + displayName: 'Deploy app' + condition: succeeded() + + - powershell: | + echo "=== Copying resources===" + robocopy $(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/Packages/Resources $(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-build/Resources /s + robocopy $(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/Packages/Firmware $(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-build/Firmware /s + robocopy $(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset) $(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-build/ *.dll + robocopy $(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset) $(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-build/ SerialPrograms.exe + robocopy $(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset) $(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-symbols/ SerialPrograms.pdb + Write-Host "Robocopy exited with exit code:" $LASTEXITCODE + if ($LASTEXITCODE -eq 1) { exit 0 } else { exit 1 } + displayName: 'Copy resources' + condition: succeeded() + + - powershell: | + $root = "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)" + $name = "SerialPrograms-Windows-$(compiler)-$(architecture)" + + $cacheArtifactsDir = Join-Path $root "cache-artifacts" + $artifactSubdir = Join-Path $cacheArtifactsDir $name + New-Item -ItemType Directory -Force -Path $artifactSubdir | Out-Null + + $temp = Join-Path $root "temp-archive" + New-Item -ItemType Directory -Force -Path $temp | Out-Null + Copy-Item "$root/cache-build/*" $temp -Recurse -Force + + Compress-Archive -Path $temp/* -DestinationPath "$artifactSubdir/$name.zip" + Remove-Item $temp -Recurse -Force + displayName: 'Archive Windows build artifact' + condition: succeeded() + + - task: Cache@2 + displayName: 'Cache the build artifact' + inputs: + key: 'windows-build | "$(Build.BuildId)"' + path: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-artifacts' + condition: succeeded() + + - task: Cache@2 + displayName: 'Cache debug symbols' + inputs: + key: 'windows-symbols | "$(Build.BuildId)"' + path: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-symbols' + condition: succeeded() + + - task: PublishBuildArtifacts@1 + displayName: 'Publish SerialPrograms' + condition: succeeded() + 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)' + +########################################### +# LINUX BUILD JOB # +########################################### + +- stage: Linux_Build_x64 + displayName: Linux Build x64 + #dependsOn: Wake_Up_Signal + dependsOn: [] + condition: and(succeeded(), or(eq('${{ parameters.targetOS }}', 'Linux'), eq('${{ parameters.targetOS }}', 'All'))) + + jobs: + - job: Ubuntu + timeoutInMinutes: 30 + cancelTimeoutInMinutes: 1 + displayName: Ubuntu + + strategy: + matrix: + GCC: + poolName: 'LinuxPool' + imageName: 'ubuntu-24.04' + architecture: 'x64' + compiler: 'GCC' + qt_version: '6.10.0' + qt_version_major: '6' + qt_modules: 'qtserialport qtmultimedia' + cmake_preset: 'RelWithDebInfo' + linux_path: '/opt/Qt/$(qt_version)/gcc_64/lib/cmake:/opt/Qt/$(qt_version)/gcc_64/bin:/opt/Qt/$(qt_version)/gcc_64/plugins:$PATH' + 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: '-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DIS_AZURE_BUILD=TRUE' + + pool: + name: $(poolName) + + steps: + - template: templates/checkout.yml + + - script: | + export PATH=$(linux_path) + cd "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/SerialPrograms" + cmake --preset=$(cmake_preset) $(cmake_additional_param) $(cmake_version_params) + displayName: 'Configure CMake' + condition: succeeded() + + - script: | + export PATH=$(linux_path) + cd "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/SerialPrograms" + cmake --build --preset=$(cmake_preset) + displayName: 'Build' + condition: succeeded() + + - script: | + set -e + export PATH=$(linux_path) + BUILD_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)" + + echo "=== Extracting debug symbols ===" + cd "$BUILD_DIR" + objcopy --only-keep-debug SerialPrograms SerialPrograms.debug + objcopy --strip-debug SerialPrograms + objcopy --add-gnu-debuglink=SerialPrograms.debug SerialPrograms + mkdir -p "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-symbols" + mv SerialPrograms.debug "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-symbols/SerialPrograms.debug" + echo "=== Debug symbols extracted to SerialPrograms.debug ===" + displayName: 'Extract Debug Symbols' + condition: succeeded() + + - script: | + set -e + export LINUXDEPLOY_DISABLE_APPSTREAM=1 + 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" + 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" + + 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/" + + 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" + + echo "=== Extracting Discord Social SDK ===" + DISCORD_ZIP="$SRC_DIR/3rdPartyBinaries/discord_social_sdk_linux.zip" + DISCORD_DIR="$SRC_DIR/3rdPartyBinaries/discord_social_sdk_linux" + rm -rf "$DISCORD_DIR" + mkdir -p "$DISCORD_DIR" + unzip -q "$DISCORD_ZIP" -d "$SRC_DIR/3rdPartyBinaries" + DISCORD_SO="$DISCORD_DIR/lib/release/libdiscord_partner_sdk.so" + + echo "=== Downloading linuxdeploy & Qt 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 + chmod +x linuxdeploy-*.AppImage + chmod +x appimagetool-x86_64.AppImage + + echo "=== Running linuxdeploy to package AppImage ===" + ./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" \ + -l "$DISCORD_SO" \ + -p qt \ + -v 3 + + ./appimagetool-x86_64.AppImage AppDir + rm linuxdeploy-*.AppImage + rm appimagetool-x86_64.AppImage + + echo "=== Archive Linux AppImage ===" + chmod +x "SerialPrograms-x86_64.AppImage" + tar -zcvf $(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-build/SerialPrograms-Ubuntu-$(compiler)-$(architecture).tar.gz SerialPrograms-x86_64.AppImage + echo "=== AppImage build complete ===" + displayName: 'Deploy AppImage' + condition: succeeded() + + - task: Cache@2 + displayName: 'Cache build' + inputs: + key: 'ubuntu-build | "$(Build.BuildId)"' + path: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-build' + condition: succeeded() + + - task: Cache@2 + displayName: 'Cache debug symbols' + inputs: + key: 'ubuntu-symbols | "$(Build.BuildId)"' + path: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-symbols' + condition: succeeded() + + - task: PublishBuildArtifacts@1 + displayName: 'Publish SerialPrograms' + condition: succeeded() + inputs: + PathtoPublish: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-build/SerialPrograms-Ubuntu-$(compiler)-$(architecture).tar.gz' + ArtifactName: 'SerialPrograms-Linux-$(compiler)-$(architecture)' + +########################################### +# MACOS BUILD STAGES # +########################################### + +- template: templates/macos-build.yml + parameters: + stageName: MacOS_Build_ARM64 + displayName: 'MacOS Build ARM64' + targetOS: ${{ parameters.targetOS }} + poolName: ApplePool + imageName: macos-26 + architecture: arm64 + compiler: Clang + qt_version: '6.9.3' + qt_version_major: '6' + qt_modules: 'qtserialport qtmultimedia' + cmake_preset: RelWithDebInfo + macos_path: '/opt/Qt/6.9.3/macos/lib/cmake:/opt/Qt/6.9.3/macos/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/opt/homebrew:$PATH' + 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' + condition: or(eq('${{ parameters.targetOS }}', 'MacOS'), eq('${{ parameters.targetOS }}', 'All')) + +- template: templates/macos-build.yml + parameters: + stageName: MacOS_Build_x64 + displayName: 'MacOS Build x64' + targetOS: ${{ parameters.targetOS }} + poolName: ApplePool + imageName: macos-15 + architecture: x64 + compiler: Clang + qt_version: '6.9.3' + qt_version_major: '6' + qt_modules: 'qtserialport qtmultimedia' + cmake_preset: RelWithDebInfo + macos_path: '/usr/local/Qt/6.9.3/macos/lib/cmake:/usr/local/Qt/6.9.3/macos/bin:/usr/local/bin:/usr/local/sbin:/usr/local:$PATH' + 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' + 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.9.3/macos/lib/cmake:/opt/Qt/6.9.3/macos/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/opt/homebrew:$PATH' + openssl_pkcs_args: '-legacy' + 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.9.3/macos/lib/cmake:/usr/local/Qt/6.9.3/macos/bin:/usr/local/bin:/usr/local/sbin:/usr/local:$PATH' + openssl_pkcs_args: '-legacy' + condition: and(succeeded('MacOS_Build_x64'), or(eq('${{ parameters.targetOS }}', 'MacOS'), eq('${{ parameters.targetOS }}', 'All'))) + +########################################## +# UPLOAD DEBUG SYMBOLS # +########################################## + +- template: templates/upload-symbols.yml + parameters: + stageName: Upload_Debug_Symbols + displayName: 'Upload Debug Symbols' + dependsOn: + - Windows_Build_x64 + - Linux_Build_x64 + - MacOS_Notarize_ARM64 + - MacOS_Notarize_x64 + platforms: + - Windows + - Linux + - MacOS_ARM64 + - MacOS_x64 + poolName: WindowsPool + buildType: ${{ parameters.buildType }} + versionMajor: ${{ parameters.versionMajor }} + versionMinor: ${{ parameters.versionMinor }} + versionPatch: ${{ parameters.versionPatch }} + condition: succeeded() + +########################################### +# SLEEP # +########################################### +#- template: templates/sleep-signal.yml +# parameters: +# stageName: Sleep_Signal +# displayName: 'Send Sleep Signal' +# dependsOn: +# - Upload_Debug_Symbols +# poolName: AlarmClock +# condition: always() + +########################################## +# UPDATE TELEMETRY JSON # +########################################## + +- template: templates/update-github-telemetry.yml + parameters: + stageName: Update_GitHub_Telemetry_JSON + displayName: 'Update GitHub Telemetry JSON' + dependsOn: + - Upload_Debug_Symbols + buildType: ${{ parameters.buildType }} + versionMajor: ${{ parameters.versionMajor }} + versionMinor: ${{ parameters.versionMinor }} + versionPatch: ${{ parameters.versionPatch }} + telemetryRepo: Koi-3088/ServerConfigs-PA-SHA + condition: and(succeeded(), eq('${{ parameters.targetOS }}', 'All'), ne('${{ parameters.buildType }}', 'Commit')) + +########################################## +# PUBLISH GITHUB RELEASE # +########################################## + +- template: templates/github-release.yml + parameters: + stageName: Publish_GitHub_Release + displayName: 'Publish GitHub Release' + dependsOn: + - Update_GitHub_Telemetry_JSON + platforms: + - Windows + - Linux + - MacOS_ARM64 + - MacOS_x64 + buildType: ${{ parameters.buildType }} + versionMajor: ${{ parameters.versionMajor }} + versionMinor: ${{ parameters.versionMinor }} + versionPatch: ${{ parameters.versionPatch }} + targetRepo: 'Koi-3088/ComputerControl' + condition: and(succeeded(), eq('${{ parameters.targetOS }}', 'All'), or(eq('${{ parameters.buildType }}', 'Release'), eq('${{ parameters.buildType }}', 'Beta'))) + +########################################## +# UPDATE LATEST VERSION JSON # +########################################## + +- template: templates/update-github-version.yml + parameters: + stageName: Update_GitHub_Latest_Version_JSON + displayName: 'Update GitHub Latest Version JSON' + dependsOn: + - Publish_GitHub_Release + buildType: ${{ parameters.buildType }} + versionMajor: ${{ parameters.versionMajor }} + versionMinor: ${{ parameters.versionMinor }} + versionPatch: ${{ parameters.versionPatch }} + versionRepo: Koi-3088/ComputerControl + condition: and(succeeded(), eq('${{ parameters.targetOS }}', 'All'), ne('${{ parameters.buildType }}', 'Commit')) diff --git a/.azure-pipelines/templates/checkout.yml b/.azure-pipelines/templates/checkout.yml new file mode 100644 index 000000000..1fdbbbd9e --- /dev/null +++ b/.azure-pipelines/templates/checkout.yml @@ -0,0 +1,18 @@ +# templates/checkout.yml +# Template for checking out the Arduino-Source repositories + +steps: + - checkout: Arduino-Source-Internal + path: Arduino-Source-Internal + fetchDepth: 1 + persistCredentials: true + sparseCheckoutPatterns: | + /* + !Repository/Public/* + !Repository/Deployment-SerialPrograms-Qt6.10.0-MSVC2022/ + !Repository/Deployment-SerialPrograms-Qt6.8.3-MSVC2022/ + workspaceRepo: true + + - checkout: Arduino-Source + path: Arduino-Source-Internal/Repository/Public + persistCredentials: true diff --git a/.azure-pipelines/templates/github-release.yml b/.azure-pipelines/templates/github-release.yml new file mode 100644 index 000000000..f27cb6cee --- /dev/null +++ b/.azure-pipelines/templates/github-release.yml @@ -0,0 +1,387 @@ +# templates/github-release.yml +# Template for publishing releases to GitHub + +parameters: + stageName: '' + displayName: '' + dependsOn: [] + platforms: [] + buildType: '' + targetRepo: '' + versionMajor: 0 + versionMinor: 0 + versionPatch: 0 + condition: '' + +stages: + - stage: ${{ parameters.stageName }} + displayName: ${{ parameters.displayName }} + dependsOn: ${{ parameters.dependsOn }} + condition: ${{ parameters.condition }} + + jobs: + - job: CreateRelease + displayName: 'Create GitHub Release' + pool: + vmImage: ubuntu-latest + + variables: + - name: VERSION_TAG + value: 'v${{ parameters.versionMajor }}.${{ parameters.versionMinor }}.${{ parameters.versionPatch }}' + - name: VERSION_NUMBER + value: '${{ parameters.versionMajor }}.${{ parameters.versionMinor }}.${{ parameters.versionPatch }}' + - name: IS_PRERELEASE + value: ${{ eq(parameters.buildType, 'Beta') }} + - name: BUILD_DATE + value: $[format('{0:yyyy}{0:MM}{0:dd}', pipeline.startTime)] + + steps: + - checkout: none + + - task: Cache@2 + displayName: 'Restore Windows Artifacts' + inputs: + key: 'windows-build | "$(Build.BuildId)"' + path: '$(Pipeline.Workspace)/artifacts-windows' + cacheHitVar: WINDOWS_CACHE_RESTORED + condition: and(succeeded(), contains('${{ join(',', parameters.platforms) }}', 'Windows')) + + - task: Cache@2 + displayName: 'Restore Linux Artifacts' + inputs: + key: 'ubuntu-build | "$(Build.BuildId)"' + path: '$(Pipeline.Workspace)/artifacts-linux' + cacheHitVar: LINUX_CACHE_RESTORED + condition: and(succeeded(), contains('${{ join(',', parameters.platforms) }}', 'Linux')) + + - task: Cache@2 + displayName: 'Restore MacOS ARM64 Artifacts' + inputs: + key: 'macos-notarized-arm64 | "$(Build.BuildId)"' + path: '$(Pipeline.Workspace)/artifacts-macos-arm64' + cacheHitVar: MACOS_ARM64_CACHE_RESTORED + condition: and(succeeded(), contains('${{ join(',', parameters.platforms) }}', 'MacOS_ARM64')) + + - task: Cache@2 + displayName: 'Restore MacOS x64 Artifacts' + inputs: + key: 'macos-notarized-x64 | "$(Build.BuildId)"' + path: '$(Pipeline.Workspace)/artifacts-macos-x64' + cacheHitVar: MACOS_X64_CACHE_RESTORED + condition: and(succeeded(), contains('${{ join(',', parameters.platforms) }}', 'MacOS_x64')) + + - script: | + type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y) + curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg + sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null + sudo apt update + sudo apt install gh -y + gh --version + displayName: 'Install GitHub CLI' + condition: succeeded() + + - script: | + echo "$(GITHUB_TOKEN)" | gh auth login --with-token + gh auth status + displayName: 'Authenticate with GitHub' + condition: succeeded() + env: + GITHUB_TOKEN: $(GITHUB_PAT) + + - script: | + REPO="${{ parameters.targetRepo }}" + echo "Fetching ChangeLog.md from $REPO..." + + gh api repos/$REPO/contents/ChangeLog.md --jq '.content' | base64 -d > changelog-raw.md + if [ ! -s changelog-raw.md ]; then + echo "✗ Failed to fetch ChangeLog.md or file is empty" + exit 1 + fi + + echo "✓ ChangeLog.md fetched successfully" + cat changelog-raw.md + displayName: 'Fetch ChangeLog.md' + condition: succeeded() + env: + GITHUB_TOKEN: $(GITHUB_PAT) + + - script: | + REPO="${{ parameters.targetRepo }}" + echo "Fetching latest commit SHA from $REPO..." + + DEFAULT_BRANCH=$(gh api repos/$REPO --jq '.default_branch') + if [ -z "$DEFAULT_BRANCH" ]; then + echo "✗ Failed to fetch default branch" + exit 1 + fi + + echo "✓ Default branch: $DEFAULT_BRANCH" + COMMIT_SHA=$(gh api repos/$REPO/commits/$DEFAULT_BRANCH --jq '.sha // empty') + if [ -z "$COMMIT_SHA" ] || [ ${#COMMIT_SHA} -ne 40 ]; then + echo "✗ Failed to fetch commit SHA or invalid SHA received" + exit 1 + fi + + echo "✓ Latest commit SHA: $COMMIT_SHA" + echo "##vso[task.setvariable variable=COMMIT_SHA]$COMMIT_SHA" + displayName: 'Fetch Latest Commit SHA' + condition: succeeded() + env: + GITHUB_TOKEN: $(GITHUB_PAT) + + - script: | + VERSION_TAG="$(VERSION_TAG)" + BUILD_TYPE="${{ parameters.buildType }}" + + if [ "$BUILD_TYPE" = "Beta" ]; then + VERSION_TAG="${VERSION_TAG}-beta" + fi + + echo "Creating release notes from ChangeLog.md..." + cp changelog-raw.md release-notes.md + + echo "Release notes created:" + cat release-notes.md + + echo "##vso[task.setvariable variable=RELEASE_VERSION]$VERSION_TAG" + displayName: 'Prepare Release Notes' + condition: succeeded() + + - script: | + ARTIFACTS_DIR="$(Pipeline.Workspace)" + echo "=== Debugging Cache Contents ===" + + if [ "$(WINDOWS_CACHE_RESTORED)" = "true" ]; then + echo "Windows artifacts directory structure:" + ls -lR "$ARTIFACTS_DIR/artifacts-windows/" || echo "Directory not found" + fi + + if [ "$(LINUX_CACHE_RESTORED)" = "true" ]; then + echo "Linux artifacts directory structure:" + ls -lR "$ARTIFACTS_DIR/artifacts-linux/" || echo "Directory not found" + fi + + if [ "$(MACOS_ARM64_CACHE_RESTORED)" = "true" ]; then + echo "MacOS ARM64 artifacts directory structure:" + ls -lR "$ARTIFACTS_DIR/artifacts-macos-arm64/" || echo "Directory not found" + fi + + if [ "$(MACOS_X64_CACHE_RESTORED)" = "true" ]; then + echo "MacOS X64 artifacts directory structure:" + ls -lR "$ARTIFACTS_DIR/artifacts-macos-x64/" || echo "Directory not found" + fi + + echo "=== End Debug ===" + displayName: 'Debug: List Cache Contents' + condition: succeeded() + + - script: | + ARTIFACTS_DIR="$(Pipeline.Workspace)" + echo "Validating artifacts..." + + if [ "$(WINDOWS_CACHE_RESTORED)" = "true" ]; then + WIN_ARTIFACT=$(find "$ARTIFACTS_DIR/artifacts-windows/" -name "*.zip" -type f | head -n 1) + if [ ! -f "$WIN_ARTIFACT" ]; then + echo "✗ Error: Windows artifact not found" + exit 1 + fi + echo "✓ Found Windows artifact: $WIN_ARTIFACT" + echo "##vso[task.setvariable variable=WIN_ARTIFACT]$WIN_ARTIFACT" + else + echo "Skipping Windows artifact validation" + fi + + if [ "$(LINUX_CACHE_RESTORED)" = "true" ]; then + LINUX_ARTIFACT=$(find "$ARTIFACTS_DIR/artifacts-linux/" -name "*.tar.gz" -type f | head -n 1) + if [ ! -f "$LINUX_ARTIFACT" ]; then + echo "✗ Error: Linux artifact not found" + exit 1 + fi + echo "✓ Found Linux artifact: $LINUX_ARTIFACT" + echo "##vso[task.setvariable variable=LINUX_ARTIFACT]$LINUX_ARTIFACT" + else + echo "Skipping Linux artifact validation" + fi + + if [ "$(MACOS_ARM64_CACHE_RESTORED)" = "true" ]; then + MACOS_ARM64_ARTIFACT=$(find "$ARTIFACTS_DIR/artifacts-macos-arm64/" -name "*.tar.gz" -type f | head -n 1) + if [ ! -f "$MACOS_ARM64_ARTIFACT" ]; then + echo "✗ Error: MacOS ARM64 artifact not found" + exit 1 + fi + echo "✓ Found MacOS ARM64 artifact: $MACOS_ARM64_ARTIFACT" + echo "##vso[task.setvariable variable=MACOS_ARM64_ARTIFACT]$MACOS_ARM64_ARTIFACT" + else + echo "Skipping MacOS ARM64 artifact validation" + fi + + if [ "$(MACOS_X64_CACHE_RESTORED)" = "true" ]; then + MACOS_X64_ARTIFACT=$(find "$ARTIFACTS_DIR/artifacts-macos-x64/" -name "*.tar.gz" -type f | head -n 1) + if [ ! -f "$MACOS_X64_ARTIFACT" ]; then + echo "✗ Error: MacOS X64 artifact not found" + exit 1 + fi + echo "✓ Found MacOS X64 artifact: $MACOS_X64_ARTIFACT" + echo "##vso[task.setvariable variable=MACOS_X64_ARTIFACT]$MACOS_X64_ARTIFACT" + else + echo "Skipping MacOS X64 artifact validation" + fi + + echo "✓ All required artifacts validated successfully" + displayName: 'Validate Artifacts' + condition: succeeded() + + - script: | + ARTIFACTS_DIR="$(Pipeline.Workspace)" + VERSION="$(VERSION_NUMBER)" + BUILD_DATE="$(BUILD_DATE)" + + echo "Renaming artifacts to standardized format..." + if [ "$(WINDOWS_CACHE_RESTORED)" = "true" ] && [ -n "$(WIN_ARTIFACT)" ]; then + OLD_PATH="$(WIN_ARTIFACT)" + NEW_NAME="PA-SerialPrograms-Windows-x64-${VERSION}-${BUILD_DATE}.zip" + NEW_PATH="$ARTIFACTS_DIR/$NEW_NAME" + + echo "Renaming: $(basename "$OLD_PATH") -> $NEW_NAME" + mv "$OLD_PATH" "$NEW_PATH" + echo "##vso[task.setvariable variable=WIN_ARTIFACT]$NEW_PATH" + fi + + if [ "$(LINUX_CACHE_RESTORED)" = "true" ] && [ -n "$(LINUX_ARTIFACT)" ]; then + OLD_PATH="$(LINUX_ARTIFACT)" + NEW_NAME="PA-SerialPrograms-Ubuntu-x64-${VERSION}-${BUILD_DATE}.tar.gz" + NEW_PATH="$ARTIFACTS_DIR/$NEW_NAME" + + echo "Renaming: $(basename "$OLD_PATH") -> $NEW_NAME" + mv "$OLD_PATH" "$NEW_PATH" + echo "##vso[task.setvariable variable=LINUX_ARTIFACT]$NEW_PATH" + fi + + 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}.tar.gz" + NEW_PATH="$ARTIFACTS_DIR/$NEW_NAME" + + echo "Renaming: $(basename "$OLD_PATH") -> $NEW_NAME" + mv "$OLD_PATH" "$NEW_PATH" + echo "##vso[task.setvariable variable=MACOS_ARM64_ARTIFACT]$NEW_PATH" + fi + + 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}.tar.gz" + NEW_PATH="$ARTIFACTS_DIR/$NEW_NAME" + + echo "Renaming: $(basename "$OLD_PATH") -> $NEW_NAME" + mv "$OLD_PATH" "$NEW_PATH" + echo "##vso[task.setvariable variable=MACOS_X64_ARTIFACT]$NEW_PATH" + fi + + echo "✓ All artifacts renamed successfully" + displayName: 'Rename Artifacts' + condition: succeeded() + + - script: | + RELEASE_VERSION="$(RELEASE_VERSION)" + REPO="${{ parameters.targetRepo }}" + COMMIT_SHA="$(COMMIT_SHA)" + + echo "Creating git tag $RELEASE_VERSION at commit $COMMIT_SHA..." + if gh api repos/$REPO/git/refs/tags/$RELEASE_VERSION 2>/dev/null; then + echo "Tag $RELEASE_VERSION already exists. Deleting..." + gh api -X DELETE repos/$REPO/git/refs/tags/$RELEASE_VERSION + fi + + gh api repos/$REPO/git/refs \ + -f ref="refs/tags/$RELEASE_VERSION" \ + -f sha="$COMMIT_SHA" + + if [ $? -eq 0 ]; then + echo "✓ Tag created successfully" + else + echo "✗ Failed to create tag" + exit 1 + fi + displayName: 'Create Git Tag' + condition: succeeded() + env: + GITHUB_TOKEN: $(GITHUB_PAT) + + - script: | + RELEASE_VERSION="$(RELEASE_VERSION)" + REPO="${{ parameters.targetRepo }}" + IS_PRERELEASE="$(IS_PRERELEASE)" + COMMIT_SHA="$(COMMIT_SHA)" + + echo "Creating GitHub release $RELEASE_VERSION in $REPO..." + if gh release view "$RELEASE_VERSION" --repo "$REPO" >/dev/null 2>&1; then + echo "Release $RELEASE_VERSION already exists. Deleting..." + gh release delete "$RELEASE_VERSION" --repo "$REPO" --yes + fi + + if [ "$IS_PRERELEASE" = "True" ]; then + gh release create "$RELEASE_VERSION" \ + --repo "$REPO" \ + --title "Version $RELEASE_VERSION" \ + --notes-file release-notes.md \ + --prerelease \ + --target "$COMMIT_SHA" + else + gh release create "$RELEASE_VERSION" \ + --repo "$REPO" \ + --title "SerialPrograms $RELEASE_VERSION" \ + --notes-file release-notes.md \ + --target "$COMMIT_SHA" + fi + + if [ $? -eq 0 ]; then + echo "✓ Release created successfully" + else + echo "✗ Failed to create release" + exit 1 + fi + displayName: 'Create GitHub Release' + condition: succeeded() + env: + GITHUB_TOKEN: $(GITHUB_PAT) + + - script: | + RELEASE_VERSION="$(RELEASE_VERSION)" + REPO="${{ parameters.targetRepo }}" + echo "Uploading artifacts to release $RELEASE_VERSION..." + + if [ "$(WINDOWS_CACHE_RESTORED)" = "true" ] && [ -n "$(WIN_ARTIFACT)" ]; then + echo "Uploading Windows artifact: $(WIN_ARTIFACT)" + gh release upload "$RELEASE_VERSION" "$(WIN_ARTIFACT)" --repo "$REPO" --clobber + else + echo "Skipping Windows artifact upload" + fi + + if [ "$(LINUX_CACHE_RESTORED)" = "true" ] && [ -n "$(LINUX_ARTIFACT)" ]; then + echo "Uploading Linux artifact: $(LINUX_ARTIFACT)" + gh release upload "$RELEASE_VERSION" "$(LINUX_ARTIFACT)" --repo "$REPO" --clobber + else + echo "Skipping Linux artifact upload" + fi + + if [ "$(MACOS_ARM64_CACHE_RESTORED)" = "true" ] && [ -n "$(MACOS_ARM64_ARTIFACT)" ]; then + echo "Uploading MacOS ARM64 artifact: $(MACOS_ARM64_ARTIFACT)" + gh release upload "$RELEASE_VERSION" "$(MACOS_ARM64_ARTIFACT)" --repo "$REPO" --clobber + else + echo "Skipping MacOS ARM64 artifact upload" + fi + + if [ "$(MACOS_X64_CACHE_RESTORED)" = "true" ] && [ -n "$(MACOS_X64_ARTIFACT)" ]; then + echo "Uploading MacOS X64 artifact: $(MACOS_X64_ARTIFACT)" + gh release upload "$RELEASE_VERSION" "$(MACOS_X64_ARTIFACT)" --repo "$REPO" --clobber + else + echo "Skipping MacOS X64 artifact upload" + fi + + echo "✓ All required artifacts uploaded successfully" + echo "Release URL: https://github.com/$REPO/releases/tag/$RELEASE_VERSION" + displayName: 'Upload Artifacts to Release' + condition: succeeded() + env: + GITHUB_TOKEN: $(GITHUB_PAT) diff --git a/.azure-pipelines/templates/macos-build.yml b/.azure-pipelines/templates/macos-build.yml new file mode 100644 index 000000000..7317f22f6 --- /dev/null +++ b/.azure-pipelines/templates/macos-build.yml @@ -0,0 +1,178 @@ +# templates/macos-build.yml +# Template for building SerialPrograms on MacOS + +parameters: + stageName: '' + displayName: '' + targetOS: '' + poolName: '' + imageName: '' + architecture: '' + compiler: '' + qt_version: '' + qt_version_major: '' + qt_modules: '' + cmake_preset: '' + macos_path: '' + cmake_version_params: '' + cmake_additional_param: '' + condition: '' + +stages: +- stage: ${{ parameters.stageName }} + displayName: ${{ parameters.displayName }} + dependsOn: [] + condition: ${{ parameters.condition }} + + jobs: + - job: MacOS + timeoutInMinutes: 60 + cancelTimeoutInMinutes: 1 + displayName: MacOS ${{ parameters.architecture }} + + pool: + name: ${{ parameters.poolName }} + demands: + - Agent.OSArchitecture -equals ${{ lower(parameters.architecture) }} + + variables: + architecture: ${{ parameters.architecture }} + compiler: ${{ parameters.compiler }} + qt_version: ${{ parameters.qt_version }} + cmake_preset: ${{ parameters.cmake_preset }} + macos_path: ${{ parameters.macos_path }} + cmake_version_params: ${{ parameters.cmake_version_params }} + cmake_additional_param: ${{ parameters.cmake_additional_param }} + + steps: + - template: checkout.yml + + #- script: | + # set -e + # PERSISTENT_ONNX="/Users/Shared/onnxruntime" + # WORK_ONNX="$(Pipeline.Workspace)/onnxruntime" + # + # if [ -d "$PERSISTENT_ONNX" ]; then + # echo "=== Found ONNX Runtime at $PERSISTENT_ONNX ===" + # if [ ! -L "$WORK_ONNX" ]; then + # ln -sf "$PERSISTENT_ONNX" "$WORK_ONNX" + # echo "=== Created symlink to ONNX Runtime ===" + # fi + # else + # echo "=== ERROR: ONNX Runtime not found at $PERSISTENT_ONNX ===" + # exit 1 + # fi + # displayName: 'Link ONNX Runtime' + # condition: succeeded() + + - 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 + displayName: 'Configure CMake' + condition: succeeded() + + - script: | + export PATH=$(macos_path) + cd "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/SerialPrograms" + cmake --build --preset=$(cmake_preset) + displayName: 'Build' + condition: succeeded() + + - script: | + set -e + export PATH=$(macos_path) + BUILD_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)" + + echo "=== Generating dSYM bundle ===" + mkdir -p "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-symbols" + cd "$BUILD_DIR" + dsymutil SerialPrograms.app/Contents/MacOS/SerialPrograms -o cache-symbols/SerialPrograms.dSYM + + echo "=== dSYM bundle created ===" + displayName: 'Generate Debug Symbols' + condition: succeeded() + + - script: | + set -e + export PATH=$(macos_path) + 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" + BREW_PREFIX=$(brew --prefix) + + mkdir -p "$FW_DIR" + if [ "$(architecture)" = "x64" ]; then + QT_BIN="/usr/local/Qt/$(qt_version)/macos/bin" + else + QT_BIN="/opt/Qt/$(qt_version)/macos/bin" + 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" + 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 + 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" + fi + done + + echo "=== Creating tarballs for build and symbols ===" + mkdir -p "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-build" + cd "$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)" + tar -czf "cache-build/macos-build-${{ parameters.architecture }}.tar.gz" SerialPrograms.app + tar -czf "cache-symbols/macos-symbols-${{ parameters.architecture }}.tar.gz" cache-symbols/SerialPrograms.dSYM + + echo "Deployment complete for $(architecture)" + displayName: 'Deploy app' + condition: succeeded() + + - task: Cache@2 + displayName: 'Cache the build artifact' + inputs: + key: 'macos-build-${{ parameters.architecture }} | "$(Build.BuildId)"' + path: '$(Pipeline.Workspace)/Arduino-Source-Internal/Repository/Public/build/$(cmake_preset)/cache-build' + condition: succeeded() + + - 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() diff --git a/.azure-pipelines/templates/macos-notarize.yml b/.azure-pipelines/templates/macos-notarize.yml new file mode 100644 index 000000000..32df262fa --- /dev/null +++ b/.azure-pipelines/templates/macos-notarize.yml @@ -0,0 +1,191 @@ +# 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: '' + 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 }} + APP_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/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 + APP_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/SerialPrograms.app" + xcrun stapler staple "$APP_DIR" + spctl --assess --type execute --verbose=4 "$APP_DIR" + displayName: 'Staple and verify' + condition: succeeded() + + - script: | + echo "=== Creating a tarball ===" + tar -czf "SerialPrograms-MacOS-$(compiler)-$(architecture).tar.gz" -C "$(Pipeline.Workspace)/Arduino-Source-Internal" "SerialPrograms.app" + echo "=== Creating cache directory and moving tarball ===" + CACHE_DIR="$(Pipeline.Workspace)/Arduino-Source-Internal/cache-notarized" + mkdir -p "$CACHE_DIR" + mv "$(Pipeline.Workspace)/Arduino-Source-Internal/SerialPrograms-MacOS-$(compiler)-$(architecture).tar.gz" "$CACHE_DIR/" + displayName: 'Create a tarball' + condition: succeeded() + + - 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).tar.gz' + artifact: 'SerialPrograms-MacOS-$(compiler)-$(architecture)' + condition: succeeded() diff --git a/.azure-pipelines/templates/sleep-signal.yml b/.azure-pipelines/templates/sleep-signal.yml new file mode 100644 index 000000000..52950a5ac --- /dev/null +++ b/.azure-pipelines/templates/sleep-signal.yml @@ -0,0 +1,51 @@ +# templates/sleep-signal.yml +# Template for putting Windows/Linux host to sleep + +parameters: + stageName: '' + displayName: '' + dependsOn: [] + condition: '' + +stages: + - stage: ${{ parameters.stageName }} + displayName: ${{ parameters.displayName }} + dependsOn: ${{ parameters.dependsOn }} + condition: ${{ parameters.condition }} + + jobs: + - job: SendSleepSignal + displayName: Send Sleep Signal + pool: + name: ${{ parameters.poolName }} + timeoutInMinutes: 10 + steps: + - checkout: none + + - script: | + if [ -z "$HOST_IP" ] || [ -z "$HOST_PORT" ] || [ -z "$HOST_SECRET" ]; then + echo "❌ One or more required environment variables are missing." + exit 1 + fi + + URL="http://${HOST_IP}:${HOST_PORT}/sleep/" + echo "Sending POST to the host" + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -X POST \ + -H "X-Auth: ${HOST_SECRET}" \ + -H "Content-Length: 0" \ + "$URL") + + echo "HTTP response code: $HTTP_CODE" + if [[ "$HTTP_CODE" =~ ^2 ]]; then + echo "✅ Sleep signal sent successfully." + else + echo "❌ Failed to send sleep signal (HTTP $HTTP_CODE)" + exit 1 + fi + displayName: 'Send Sleep Signal' + condition: succeeded() + env: + HOST_IP: $(HOST_IP) + HOST_PORT: $(HOST_PORT) + HOST_SECRET: $(HOST_SECRET) diff --git a/.azure-pipelines/templates/update-github-telemetry.yml b/.azure-pipelines/templates/update-github-telemetry.yml new file mode 100644 index 000000000..667f4c7d9 --- /dev/null +++ b/.azure-pipelines/templates/update-github-telemetry.yml @@ -0,0 +1,222 @@ +# templates/update-github-telemetry.yml +# Template for updating GitHub Telemetry JSON file with new telemetry information + +parameters: + stageName: '' + displayName: '' + dependsOn: [] + buildType: '' + versionMajor: 0 + versionMinor: 0 + versionPatch: 0 + telemetryRepo: '' + condition: '' + +stages: +- stage: ${{ parameters.stageName }} + displayName: ${{ parameters.displayName }} + dependsOn: ${{ parameters.dependsOn }} + condition: ${{ parameters.condition }} + + jobs: + - job: UpdateTelemetryJSON + displayName: 'Update Telemetry JSON' + timeoutInMinutes: 10 + cancelTimeoutInMinutes: 1 + + pool: + vmImage: windows-latest + + steps: + - checkout: none + + - task: DownloadSecureFile@1 + name: ObfuscateScript + displayName: 'Download Obfuscation Script' + inputs: + secureFile: 'Obfuscate-Telemetry.ps1' + + - task: PowerShell@2 + displayName: 'Add v${{ parameters.versionMajor }}.${{ parameters.versionMinor }}.${{ parameters.versionPatch }} to Telemetry JSON' + condition: succeeded() + inputs: + targetType: 'inline' + script: | + $ErrorActionPreference = "Stop" + + function Compare-Versions { + param( + [string]$Version1, + [string]$Version2 + ) + + $v1Parts = $Version1.TrimStart('v').Split('.') + $v2Parts = $Version2.TrimStart('v').Split('.') + + if ([int]$v1Parts[0] -gt [int]$v2Parts[0]) { return 1 } + if ([int]$v1Parts[0] -lt [int]$v2Parts[0]) { return -1 } + + if ([int]$v1Parts[1] -gt [int]$v2Parts[1]) { return 1 } + if ([int]$v1Parts[1] -lt [int]$v2Parts[1]) { return -1 } + + if ([int]$v1Parts[2] -gt [int]$v2Parts[2]) { return 1 } + if ([int]$v1Parts[2] -lt [int]$v2Parts[2]) { return -1 } + + return 0 + } + + $token = "$(GITHUB_PAT)" + $repo = "${{ parameters.telemetryRepo }}" + $filePath = "Developer/Telemetry.json" + $branch = "main" + $getUrl = "https://api.github.com/repos/" + $repo + "/contents/" + $filePath + "?ref=" + $branch + $versionKey = $env:TELEMETRY_VERSION + Write-Host "Constructed URL: $getUrl" + + Write-Host "================================================" + Write-Host "Adding $versionKey to Telemetry JSON" + Write-Host "Repository: $repo" + Write-Host "File: $filePath" + Write-Host "================================================" + Write-Host "" + + Write-Host "Generating obfuscated metadata..." + $scriptPath = "$(obfuscateScript.secureFilePath)" + $obfuscatedMetadata = & $scriptPath + Write-Host "✓ Generated metadata (length: $($obfuscatedMetadata.Length) chars)" + Write-Host "" + + $headers = @{ + Authorization = "token $token" + Accept = "application/vnd.github.v3+json" + } + + try { + Write-Host "Fetching current Telemetry JSON..." + $fileInfo = Invoke-RestMethod -Uri $getUrl -Headers $headers -Method Get + + $currentContent = [System.Text.Encoding]::UTF8.GetString( + [System.Convert]::FromBase64String($fileInfo.content) + ) + $json = $currentContent | ConvertFrom-Json + + if ($json.PSObject.Properties.Name -contains $versionKey) { + Write-Host "" + Write-Host "================================================" + Write-Host "⚠️ Version $versionKey already exists in Telemetry JSON" + Write-Host "Skipping update to prevent duplicates." + Write-Host "================================================" + exit 0 + } + + $highestVersion = $null + foreach ($property in $json.PSObject.Properties) { + $key = $property.Name + if ($key -eq "Default") { continue } + if ($key -match '^v\d+\.\d+\.\d+$') { + if ($null -eq $highestVersion -or (Compare-Versions $key $highestVersion) -gt 0) { + $highestVersion = $key + } + } + } + + if ($null -ne $highestVersion) { + Write-Host "Current highest version: $highestVersion" + $comparison = Compare-Versions $versionKey $highestVersion + + if ($comparison -le 0) { + Write-Host "" + Write-Host "================================================" + Write-Host "⚠️ Version $versionKey is not higher than existing version $highestVersion" + Write-Host "Skipping update. Only higher versions are added." + Write-Host "================================================" + exit 0 + } + + Write-Host "✓ Version $versionKey is higher than $highestVersion - proceeding with update" + } else { + Write-Host "No existing versions found - proceeding with update" + } + Write-Host "" + + $lines = $currentContent -split "`r?`n" + $updatedLines = @() + $insertionDone = $false + $inDefaultSection = $false + $bracketDepth = 0 + + for ($i = 0; $i -lt $lines.Count; $i++) { + $line = $lines[$i] + $updatedLines += $line + + if ($line -match '^\s*"Default"\s*:\s*\{') { + $inDefaultSection = $true + $bracketDepth = 1 + } + elseif ($inDefaultSection) { + $openBrackets = ([regex]::Matches($line, '\{')).Count + $closeBrackets = ([regex]::Matches($line, '\}')).Count + $bracketDepth += $openBrackets - $closeBrackets + + if ($bracketDepth -eq 0 -and $line -match '^\s*\}') { + $inDefaultSection = $false + $hasComma = $line -match '\},' + + if (-not $hasComma) { + $updatedLines[$updatedLines.Count - 1] = $line -replace '\}', '},' + } + + $baseIndent = " " + $updatedLines += "$baseIndent`"$versionKey`": {" + $updatedLines += "$baseIndent `"Metadata`": `"$obfuscatedMetadata`"," + $updatedLines += "$baseIndent `"ReportRate`": 1.0" + $updatedLines += "$baseIndent}," + + $insertionDone = $true + Write-Host "✓ Inserted $versionKey with generated Metadata and ReportRate: 1.0" + } + } + } + + if (-not $insertionDone) { + Write-Host "❌ Failed to find Default section for insertion" + exit 1 + } + + $newContent = ($updatedLines -join "`n") + $contentBytes = [System.Text.Encoding]::UTF8.GetBytes($newContent) + $contentBase64 = [System.Convert]::ToBase64String($contentBytes) + $commitMessage = "Add $versionKey to Telemetry JSON via Azure Pipeline" + + $updateBody = @{ + message = $commitMessage + content = $contentBase64 + sha = $fileInfo.sha + branch = $branch + } | ConvertTo-Json -Depth 10 + + Write-Host "Pushing changes to GitHub..." + $putUrl = "https://api.github.com/repos/$repo/contents/$filePath" + $response = Invoke-RestMethod -Uri $putUrl -Headers $headers -Method Put -Body $updateBody -ContentType "application/json" + + Write-Host "" + Write-Host "================================================" + Write-Host "✅ Successfully added $versionKey to Telemetry JSON" + Write-Host "================================================" + + } catch { + Write-Host "" + Write-Host "================================================" + Write-Host "❌ Failed to update Telemetry JSON" + Write-Host "Error: $($_.Exception.Message)" + if ($_.ErrorDetails.Message) { + Write-Host "Details: $($_.ErrorDetails.Message)" + } + Write-Host "================================================" + exit 1 + } + env: + GITHUB_PAT: $(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 new file mode 100644 index 000000000..65afd19fa --- /dev/null +++ b/.azure-pipelines/templates/update-github-version.yml @@ -0,0 +1,224 @@ +# templates/update-github-version.yml +# Template for updating GitHub LatestVersion JSON file with new version information + +parameters: + stageName: '' + displayName: '' + dependsOn: [] + buildType: '' + versionMajor: 0 + versionMinor: 0 + versionPatch: 0 + versionRepo: '' + condition: '' + +stages: +- stage: ${{ parameters.stageName }} + displayName: ${{ parameters.displayName }} + dependsOn: ${{ parameters.dependsOn }} + condition: ${{ parameters.condition }} + + jobs: + - job: UpdateVersionJSON + displayName: 'Update LatestVersion JSON' + timeoutInMinutes: 10 + cancelTimeoutInMinutes: 1 + + pool: + vmImage: ubuntu-latest + + steps: + - checkout: none + + - script: | + echo "$(GITHUB_TOKEN)" | gh auth login --with-token + gh auth status + displayName: 'Authenticate with GitHub' + condition: succeeded() + env: + GITHUB_TOKEN: $(GITHUB_PAT) + + - script: | + REPO="${{ parameters.versionRepo }}" + echo "Fetching ChangeLog.md from $REPO..." + + gh api repos/$REPO/contents/ChangeLog.md --jq '.content' | base64 -d > changelog.md + if [ ! -s changelog.md ]; then + echo "✗ Failed to fetch ChangeLog.md or file is empty" + exit 1 + fi + + echo "✓ ChangeLog.md fetched successfully" + CHANGELOG_CONTENT=$(cat changelog.md | base64 -w 0) + echo "##vso[task.setvariable variable=CHANGELOG_B64]$CHANGELOG_CONTENT" + displayName: 'Fetch ChangeLog.md' + condition: succeeded() + env: + GITHUB_TOKEN: $(GITHUB_PAT) + + - task: PowerShell@2 + name: UpdateVersionJSON + displayName: 'Update ${{ parameters.buildType }} Version in GitHub' + inputs: + targetType: 'inline' + pwsh: true + script: | + $ErrorActionPreference = "Stop" + + $changeLogB64 = "$(CHANGELOG_B64)" + $changeLog = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($changeLogB64)) + Write-Host "✓ ChangeLog decoded successfully" + function Compare-Versions { + param( + [int]$Major1, [int]$Minor1, [int]$Patch1, + [int]$Major2, [int]$Minor2, [int]$Patch2 + ) + + if ($Major1 -gt $Major2) { return 1 } + if ($Major1 -lt $Major2) { return -1 } + + if ($Minor1 -gt $Minor2) { return 1 } + if ($Minor1 -lt $Minor2) { return -1 } + + if ($Patch1 -gt $Patch2) { return 1 } + if ($Patch1 -lt $Patch2) { return -1 } + + return 0 + } + + $token = "$(GITHUB_PAT)" + $repo = "${{ parameters.versionRepo }}" + $filePath = "LatestVersion.json" + $changeLogPath = "ChangeLog.md" + $branch = "master" + $buildType = "${{ parameters.buildType }}" + $getUrl = "https://api.github.com/repos/${repo}/contents/${filePath}?ref=${branch}" + $versionMajor = ${{ parameters.versionMajor }} + $versionMinor = ${{ parameters.versionMinor }} + $versionPatch = ${{ parameters.versionPatch }} + + $headers = @{ + Authorization = "token $token" + Accept = "application/vnd.github.v3+json" + } + + try { + Write-Host "Fetching current JSON file..." + $fileInfo = Invoke-RestMethod -Uri $getUrl -Headers $headers -Method Get -Verbose + $currentContent = [System.Text.Encoding]::UTF8.GetString( + [System.Convert]::FromBase64String($fileInfo.content) + ) + + $json = $currentContent | ConvertFrom-Json + if ($json.PSObject.Properties.Name -contains $buildType) { + $currentMajor = $json.$buildType.VersionMajor + $currentMinor = $json.$buildType.VersionMinor + $currentPatch = $json.$buildType.VersionPatch + Write-Host "Current $buildType version: v$currentMajor.$currentMinor.$currentPatch" + + $comparison = Compare-Versions ` + -Major1 $versionMajor -Minor1 $versionMinor -Patch1 $versionPatch ` + -Major2 $currentMajor -Minor2 $currentMinor -Patch2 $currentPatch + + if ($comparison -lt 0) { + Write-Host "" + Write-Host "================================================" + Write-Host "⚠️ New version v$versionMajor.$versionMinor.$versionPatch is LOWER than current v$currentMajor.$currentMinor.$currentPatch" + Write-Host "Aborting update. Only higher versions are pushed." + Write-Host "================================================" + exit 1 + } + elseif ($comparison -eq 0) { + Write-Host "" + Write-Host "================================================" + Write-Host "⚠️ Version v$versionMajor.$versionMinor.$versionPatch already exists" + Write-Host "Skipping update to prevent duplicate." + Write-Host "================================================" + Write-Host "" + exit 0 + } + else { + Write-Host "✓ New version v$versionMajor.$versionMinor.$versionPatch is HIGHER - proceeding with update" + } + } else { + Write-Host "No existing $buildType section found - proceeding with update" + } + Write-Host "" + + $lines = $currentContent -split "`r?`n" + $inTargetSection = $false + $updatedLines = @() + + foreach ($line in $lines) { + if ($line -match "`"$buildType`"\s*:\s*\{") { + $inTargetSection = $true + } + + if ($inTargetSection) { + if ($line -match '^\s*"VersionMajor"\s*:\s*\d+') { + $indent = $line -replace '"VersionMajor".*', '' + $line = $indent + '"VersionMajor": ' + $versionMajor + ',' + } + elseif ($line -match '^\s*"VersionMinor"\s*:\s*\d+') { + $indent = $line -replace '"VersionMinor".*', '' + $line = $indent + '"VersionMinor": ' + $versionMinor + ',' + } + elseif ($line -match '^\s*"VersionPatch"\s*:\s*\d+') { + $indent = $line -replace '"VersionPatch".*', '' + $line = $indent + '"VersionPatch": ' + $versionPatch + ',' + } + elseif (($changeLog.Trim() -ne "") -and ($line -match '^\s*"Changes"\s*:')) { + $indent = $line -replace '"Changes".*', '' + $escaped = $changeLog.Trim() ` + -replace '\\', '\\' ` + -replace '"', '\"' ` + -replace "`n", '\n' ` + -replace "`r", '\r' ` + -replace "`t", '\t' + $line = $indent + '"Changes": "' + $escaped + '"' + } + + if ($line -match '^\s*\}') { + $inTargetSection = $false + } + } + + $updatedLines += $line + } + + $newContent = ($updatedLines -join "`n") + $contentBytes = [System.Text.Encoding]::UTF8.GetBytes($newContent) + $contentBase64 = [System.Convert]::ToBase64String($contentBytes) + $commitMessage = "Update $buildType to v$versionMajor.$versionMinor.$versionPatch via Azure Pipeline" + + $updateBody = @{ + message = $commitMessage + content = $contentBase64 + sha = $fileInfo.sha + branch = $branch + } | ConvertTo-Json -Depth 10 + + Write-Host "Pushing changes to GitHub..." + $putUrl = "https://api.github.com/repos/$repo/contents/$filePath" + $response = Invoke-RestMethod -Uri $putUrl -Headers $headers -Method Put -Body $updateBody -ContentType "application/json" + + Write-Host "" + Write-Host "================================================" + Write-Host "✅ Successfully updated $buildType section" + Write-Host "================================================" + } catch { + Write-Host "" + Write-Host "================================================" + Write-Host "❌ Failed to update JSON file" + Write-Host "Error: $($_.Exception.Message)" + Write-Host "Status Code: $($_.Exception.Response.StatusCode.value__)" + Write-Host "Request URL: $getUrl" + if ($_.ErrorDetails.Message) { + Write-Host "Details: $($_.ErrorDetails.Message)" + } + Write-Host "================================================" + exit 1 + } + condition: succeeded() + env: + GITHUB_TOKEN: $(GITHUB_PAT) diff --git a/.azure-pipelines/templates/upload-symbols.yml b/.azure-pipelines/templates/upload-symbols.yml new file mode 100644 index 000000000..d9c26f85c --- /dev/null +++ b/.azure-pipelines/templates/upload-symbols.yml @@ -0,0 +1,359 @@ +# templates/upload-symbols.yml +# Template for uploading debug symbols to Discord + +parameters: + stageName: '' + displayName: '' + dependsOn: [] + platforms: [] + buildType: '' + versionMajor: 0 + versionMinor: 0 + versionPatch: 0 + condition: '' + +stages: + - stage: ${{ parameters.stageName }} + displayName: ${{ parameters.displayName }} + dependsOn: ${{ parameters.dependsOn }} + condition: ${{ parameters.condition }} + + jobs: + - job: UploadSymbols + displayName: 'Upload Debug Symbols' + pool: + name: ${{ parameters.poolName }} + + variables: + - name: VERSION + value: '${{ parameters.versionMajor }}.${{ parameters.versionMinor }}.${{ parameters.versionPatch }}' + - name: BUILD_DATE + value: $[format('{0:yyyy}{0:MM}{0:dd}', pipeline.startTime)] + - name: SHORT_SHA + value: $[substring(variables['Build.SourceVersion'], 0, 8)] + - name: BUILD_TYPE + value: '${{ parameters.buildType }}' + - name: WINDOWS_UPLOADED + value: 'false' + - name: LINUX_UPLOADED + value: 'false' + - name: MACOS_ARM64_UPLOADED + value: 'false' + - name: MACOS_X64_UPLOADED + value: 'false' + + steps: + - checkout: none + + - task: Cache@2 + displayName: 'Restore Windows symbols' + inputs: + key: 'windows-symbols | "$(Build.BuildId)"' + path: $(Pipeline.Workspace)/symbols-windows + cacheHitVar: WINDOWS_CACHE_RESTORED + condition: and(succeeded(), contains('${{ join(',', parameters.platforms) }}', 'Windows')) + + - task: Cache@2 + displayName: 'Restore Linux symbols' + inputs: + key: 'ubuntu-symbols | "$(Build.BuildId)"' + path: $(Pipeline.Workspace)/symbols-linux + cacheHitVar: LINUX_CACHE_RESTORED + condition: and(succeeded(), contains('${{ join(',', parameters.platforms) }}', 'Linux')) + + - task: Cache@2 + displayName: 'Restore MacOS ARM64 symbols' + inputs: + key: 'macos-symbols-arm64 | "$(Build.BuildId)"' + path: $(Pipeline.Workspace)/symbols-macos-arm64 + cacheHitVar: MACOS_ARM64_CACHE_RESTORED + condition: and(succeeded(), contains('${{ join(',', parameters.platforms) }}', 'MacOS_ARM64')) + + - task: Cache@2 + displayName: 'Restore MacOS x64 symbols' + inputs: + key: 'macos-symbols-x64 | "$(Build.BuildId)"' + path: $(Pipeline.Workspace)/symbols-macos-x64 + cacheHitVar: MACOS_X64_CACHE_RESTORED + condition: and(succeeded(), contains('${{ join(',', parameters.platforms) }}', 'MacOS_x64')) + + # Mount NAS share + - powershell: | + $ErrorActionPreference = 'Stop' + + $nasName = "$(NAS_NAME)" + $shareName = "SerialPrograms Symbols" + $nasPath = "\\$nasName\$shareName" + $username = "$(NAS_USERNAME)" + $password = "$(NAS_PASSWORD)" + + Write-Host "Mapping network drive to NAS..." + $netUseCmd = "net use Z: `"$nasPath`" /user:`"$username`" `"$password`" /persistent:no" + Invoke-Expression $netUseCmd + + if ($LASTEXITCODE -ne 0) { + throw "Failed to map network drive" + } + + Write-Host "✓ NAS mounted successfully at Z:" + displayName: 'Mount NAS Share' + condition: succeeded() + env: + NAS_USERNAME: $(NAS_USERNAME) + NAS_PASSWORD: $(NAS_PASSWORD) + NAS_NAME: $(NAS_NAME) + + # Upload Windows symbols + - powershell: | + $ErrorActionPreference = 'Stop' + + $version = "$(VERSION)" + $symbolsDir = "$(Pipeline.Workspace)\symbols-windows" + $buildDate = "$(BUILD_DATE)" + $shortSha = "$(SHORT_SHA)" + $buildType = "$(BUILD_TYPE)" + + if (-not (Test-Path $symbolsDir)) { + Write-Host "Windows symbols not found" + exit 1 + } + + Write-Host "=== Packaging Windows symbols ===" + $archiveName = "SerialPrograms-Windows-$version-$buildDate-$shortSha.zip" + $archivePath = Join-Path $symbolsDir $archiveName + + Push-Location $symbolsDir + $pdbFiles = Get-ChildItem -Filter "*.pdb" + + if ($pdbFiles.Count -eq 0) { + Write-Host "No Windows symbols found to upload" + Pop-Location + exit 1 + } + + Compress-Archive -Path *.pdb -DestinationPath $archiveName -CompressionLevel Optimal -Force + Pop-Location + + if (-not (Test-Path $archivePath)) { + Write-Host "Failed to create archive" + exit 1 + } + + Copy-Item $archivePath "Z:\Windows\$buildType" -Force + Write-Host "##vso[task.setvariable variable=WINDOWS_UPLOADED]true" + + Write-Host "✓ Windows symbols uploaded to NAS" + displayName: 'Upload Windows Symbols' + condition: and(succeeded(), eq(variables.WINDOWS_CACHE_RESTORED, 'true')) + + # Upload Linux symbols + - powershell: | + $ErrorActionPreference = 'Stop' + + $version = "$(VERSION)" + $symbolsDir = "$(Pipeline.Workspace)\symbols-linux" + $buildDate = "$(BUILD_DATE)" + $shortSha = "$(SHORT_SHA)" + $buildType = "$(BUILD_TYPE)" + + if (-not (Test-Path $symbolsDir)) { + Write-Host "Linux symbols not found" + exit 1 + } + + Write-Host "=== Packaging Linux symbols ===" + $archiveName = "SerialPrograms-Linux-$version-$buildDate-$shortSha.tar.gz" + $archivePath = Join-Path $symbolsDir $archiveName + + Push-Location $symbolsDir + $debugFiles = Get-ChildItem -Filter "*.debug" -ErrorAction SilentlyContinue + $execFile = Get-Item "SerialPrograms" -ErrorAction SilentlyContinue + + if ($debugFiles.Count -eq 0 -and -not $execFile) { + Write-Host "No Linux symbols found to upload" + Pop-Location + exit 1 + } + + Write-Host "Creating tar.gz archive with Windows tar..." + $filesToArchive = @() + if ($debugFiles) { $filesToArchive += "*.debug" } + if ($execFile) { $filesToArchive += "SerialPrograms" } + + tar -czf $archiveName $filesToArchive + Pop-Location + + if (-not (Test-Path $archivePath)) { + Write-Host "Failed to create archive" + exit 1 + } + + Copy-Item $archivePath "Z:\Linux\$buildType" -Force + Write-Host "##vso[task.setvariable variable=LINUX_UPLOADED]true" + Write-Host "✓ Linux symbols uploaded to NAS" + displayName: 'Upload Linux Symbols' + condition: and(succeeded(), eq(variables.LINUX_CACHE_RESTORED, 'true')) + + # Upload MacOS ARM64 symbols + - powershell: | + $ErrorActionPreference = 'Stop' + + $version = "$(VERSION)" + $symbolsDir = "$(Pipeline.Workspace)\symbols-macos-arm64" + $buildDate = "$(BUILD_DATE)" + $shortSha = "$(SHORT_SHA)" + $buildType = "$(BUILD_TYPE)" + + if (-not (Test-Path $symbolsDir)) { + Write-Host "MacOS ARM64 symbols not found" + exit 1 + } + + Write-Host "=== Packaging MacOS ARM64 symbols ===" + $archiveName = "SerialPrograms-MacOS-ARM64-Symbols-$version-$buildDate-$shortSha.tar.gz" + $archivePath = Join-Path $symbolsDir $archiveName + + Push-Location $symbolsDir + $dsymFiles = Get-ChildItem -Filter "*.dSYM" -ErrorAction SilentlyContinue + $execFile = Get-Item "SerialPrograms" -ErrorAction SilentlyContinue + + if ($dsymFiles.Count -eq 0 -and -not $execFile) { + Write-Host "No MacOS ARM64 symbols found to upload" + Pop-Location + exit 1 + } + + Write-Host "Creating tar.gz archive with Windows tar..." + $filesToArchive = @() + if ($dsymFiles) { $filesToArchive += "*.dSYM" } + if ($execFile) { $filesToArchive += "SerialPrograms" } + + tar -czf $archiveName $filesToArchive + Pop-Location + + if (-not (Test-Path $archivePath)) { + Write-Host "Failed to create archive" + exit 1 + } + + Copy-Item $archivePath "Z:\MacOS_ARM64\$buildType" -Force + Write-Host "##vso[task.setvariable variable=MACOS_ARM64_UPLOADED]true" + Write-Host "✓ MacOS ARM64 symbols uploaded to NAS" + displayName: 'Upload MacOS ARM64 Symbols' + condition: and(succeeded(), eq(variables.MACOS_ARM64_CACHE_RESTORED, 'true')) + + # Upload MacOS x64 symbols + - powershell: | + $ErrorActionPreference = 'Stop' + + $version = "$(VERSION)" + $symbolsDir = "$(Pipeline.Workspace)\symbols-macos-x64" + $buildDate = "$(BUILD_DATE)" + $shortSha = "$(SHORT_SHA)" + $buildType = "$(BUILD_TYPE)" + + if (-not (Test-Path $symbolsDir)) { + Write-Host "MacOS x64 symbols not found" + exit 1 + } + + Write-Host "=== Packaging MacOS x64 symbols ===" + $archiveName = "SerialPrograms-MacOS-x64-Symbols-$version-$buildDate-$shortSha.tar.gz" + $archivePath = Join-Path $symbolsDir $archiveName + + Push-Location $symbolsDir + $dsymFiles = Get-ChildItem -Filter "*.dSYM" -ErrorAction SilentlyContinue + $execFile = Get-Item "SerialPrograms" -ErrorAction SilentlyContinue + + if ($dsymFiles.Count -eq 0 -and -not $execFile) { + Write-Host "No MacOS x64 symbols found to upload" + Pop-Location + exit 1 + } + + Write-Host "Creating tar.gz archive with Windows tar..." + $filesToArchive = @() + if ($dsymFiles) { $filesToArchive += "*.dSYM" } + if ($execFile) { $filesToArchive += "SerialPrograms" } + + tar -czf $archiveName $filesToArchive + Pop-Location + + if (-not (Test-Path $archivePath)) { + Write-Host "Failed to create archive" + exit 1 + } + + Copy-Item $archivePath "Z:\MacOS_x64\$buildType" -Force + Write-Host "##vso[task.setvariable variable=MACOS_X64_UPLOADED]true" + Write-Host "✓ MacOS x64 symbols uploaded to NAS" + displayName: 'Upload MacOS x64 Symbols' + condition: and(succeeded(), eq(variables.MACOS_X64_CACHE_RESTORED, 'true')) + + # Send Discord notification + - powershell: | + $ErrorActionPreference = 'Stop' + [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + $PSDefaultParameterValues['*:Encoding'] = 'utf8' + + $version = "$(VERSION)" + $webhook = "$(DISCORD_SYMBOLS_WEBHOOK)" + $nasUrl = "$(NAS_DOMAIN_URL)" + $buildId = "$(Build.BuildId)" + $buildType = "$(BUILD_TYPE)" + $commit = "$(SHORT_SHA)" + $buildDate = "$(BUILD_DATE)" + $platforms = "" + + if ("$(WINDOWS_UPLOADED)" -eq "true") { $platforms += "✅ Windows (x64, MSVC)`n" } + if ("$(LINUX_UPLOADED)" -eq "true") { $platforms += "✅ Linux (x64, GCC)`n" } + if ("$(MACOS_ARM64_UPLOADED)" -eq "true") { $platforms += "✅ MacOS (ARM64, Clang)`n" } + if ("$(MACOS_X64_UPLOADED)" -eq "true") { $platforms += "✅ MacOS (x64, Clang)`n" } + + $timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") + if ([string]::IsNullOrEmpty($platforms)) { + Write-Host "No symbols were uploaded" + $errorPayload = @{ + embeds = @( + @{ + title = "No Debug Symbols Uploaded" + description = "No debug symbols were available to upload for this build." + color = 15158332 + footer = @{ text = "Azure DevOps Pipeline" } + timestamp = $timestamp + } + ) + } | ConvertTo-Json -Depth 10 + + Invoke-RestMethod -Uri $webhook -Method Post -Body $errorPayload -ContentType "application/json" + exit 1 + } + + $payload = [PSCustomObject]@{ + embeds = @( + [PSCustomObject]@{ + title = "Debug Symbols Uploaded Successfully" + url = $nasUrl + description = "**Version:** $version`n**Build ID:** $buildId`n**Build Type:** $buildType`n**Short commit SHA:** $commit`n**Date:** $buildDate`n`n**Platforms:**`n$platforms`n`n**[📦 Open NAS Symbols]($nasUrl)**" + color = 3066993 + footer = [PSCustomObject]@{ text = "Azure DevOps Pipeline" } + timestamp = $timestamp + } + ) + } + + $json = $payload | ConvertTo-Json -Depth 10 -Compress + $utf8Bytes = [System.Text.Encoding]::UTF8.GetBytes($json) + Invoke-RestMethod -Uri $webhook -Method Post -Body $utf8Bytes -ContentType "application/json; charset=utf-8" + Write-Host "✓ Discord notification sent" + displayName: 'Send Discord Notification' + condition: always() + env: + DISCORD_SYMBOLS_WEBHOOK: $(DISCORD_SYMBOLS_WEBHOOK) + NAS_DOMAIN_URL: $(NAS_DOMAIN_URL) + + - powershell: | + net use Z: /delete /yes 2>$null + Write-Host "NAS drive unmounted" + displayName: 'Unmount NAS Share' + condition: always() diff --git a/.azure-pipelines/templates/wake-signal.yml b/.azure-pipelines/templates/wake-signal.yml new file mode 100644 index 000000000..46291c203 --- /dev/null +++ b/.azure-pipelines/templates/wake-signal.yml @@ -0,0 +1,53 @@ +# templates/wake-signal.yml +# Template for waking up the Windows/Linux host + +parameters: + stageName: '' + displayName: '' + dependsOn: [] + condition: '' + +stages: + - stage: ${{ parameters.stageName }} + displayName: ${{ parameters.displayName }} + dependsOn: ${{ parameters.dependsOn }} + condition: ${{ parameters.condition }} + + jobs: + - job: SendWakeUpSignal + displayName: Send Wake-Up Signal + pool: + name: ${{ parameters.poolName }} + timeoutInMinutes: 10 + steps: + - checkout: none + + - script: | + macAddress="${HOST_MAC}" + macAddressCleaned=$(echo "$macAddress" | tr -d ':-') + if [ ${#macAddressCleaned} -ne 12 ]; then + echo "❌ Invalid MAC address format" + exit 1 + fi + + magicPacket="" + for i in {1..6}; do + magicPacket="${magicPacket}ff" + done + + for i in {1..16}; do + magicPacket="${magicPacket}${macAddressCleaned}" + done + + if [ -n "$HOST_IP" ]; then + echo "$magicPacket" | xxd -r -p | nc -w1 -u "$HOST_IP" 9 + echo "✅ Sent directly to $HOST_IP:9" + fi + + echo "$magicPacket" | xxd -r -p | nc -b -w1 -u 255.255.255.255 9 + echo "✅ Sent to global broadcast (255.255.255.255:9)" + displayName: 'Send Wake-Up Signal' + condition: succeeded() + env: + HOST_MAC: $(HOST_MAC) + HOST_IP: $(HOST_IP)