From 19ae04a9f0e7f225a047a8096c0e914af4d03cac Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Wed, 3 Jul 2024 23:25:53 +0200 Subject: [PATCH 01/46] Add: Fetching the Notary Log macOS build. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The notary log is a key tool for debugging notarisation and trusted execution issues. Review the log: - When notarisation fails, for information as to what’s wrong - When notarisation succeeds, to check for warnings - If you encounter a trusted execution problem, to confirm that all your code was included in the notarised ticket --- .github/workflows/cicd.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 4a3be84..b23c655 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -175,6 +175,9 @@ jobs: PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} MACOS_APP: enduser/trackereditor.app + NOTARIZE_RESULT: notarize_result.txt + NOTARIZE_LOG: notarize_log.json + SUBMISSION_ID: "" run: | # Store the notarization credentials so that we can prevent a UI password dialog # from blocking the CI @@ -195,7 +198,17 @@ jobs: # you're curious echo "Notarize app" - xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait + xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait > "$NOTARIZE_RESULT" 2>&1 + + echo "Notarize log" + SUBMISSION_ID=`awk '/id: / { print $2;exit; }' $NOTARIZE_RESULT` + echo "id: ${SUBMISSION_ID}" + xcrun notarytool log "$SUBMISSION_ID" --keychain-profile "notarytool-profile" "$NOTARIZE_LOG" + cat "$NOTARIZE_LOG" + + # These files are no longer needed, so we can remove them. + rm -f "$NOTARIZE_LOG" + rm -f "$NOTARIZE_RESULT" # Finally, we need to "attach the staple" to our executable, which will allow our app to be # validated by macOS even when an internet connection is not available. From 88fc022ed27487ba98b2170be85a2f9ae2fdd604 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Wed, 3 Jul 2024 23:39:41 +0200 Subject: [PATCH 02/46] macOS build target 10.14 SDK Notarization build gives error: "The binary uses an SDK older than the 10.9 SDK." Build target macOS 10.9 works, but without dark theme support. Must use 10.14 SDK Because dark theme is supported since macOS 10.14. --- source/project/tracker_editor/trackereditor.lpi | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/project/tracker_editor/trackereditor.lpi b/source/project/tracker_editor/trackereditor.lpi index bc0e2ec..011ff9f 100644 --- a/source/project/tracker_editor/trackereditor.lpi +++ b/source/project/tracker_editor/trackereditor.lpi @@ -72,6 +72,10 @@ + From bf52026d4845bd7846304660500eb6de814c4e0c Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Sat, 5 Oct 2024 20:54:49 +0200 Subject: [PATCH 03/46] add: running inside appimage detection. When running inside appimage, you need to know where to save the tracker list --- source/code/main.pas | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/source/code/main.pas b/source/code/main.pas index 5869dcf..32e6096 100644 --- a/source/code/main.pas +++ b/source/code/main.pas @@ -243,6 +243,11 @@ procedure TFormTrackerModify.FormCreate(Sender: TObject); begin FFolderForTrackerListLoadAndSave := GetEnvironmentVariable('XDG_DATA_HOME'); end; + // If it is a appimage program, save it in a present folder. + if GetEnvironmentVariable('APPIMAGE') <> '' then + begin // OWD = Path to working directory at the time the AppImage is called + FFolderForTrackerListLoadAndSave := GetEnvironmentVariable('OWD'); + end; if FFolderForTrackerListLoadAndSave = '' then begin // No container detected. Save in the same place as the application file From 09b2e368d3c4a13a1a5012ec8c558fc6a9ad7657 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Sat, 5 Oct 2024 21:02:20 +0200 Subject: [PATCH 04/46] delete: cicd.yml file This will be replace by 3 smaller yaml files --- .github/workflows/cicd.yml | 239 ------------------------------------- 1 file changed, 239 deletions(-) delete mode 100644 .github/workflows/cicd.yml diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml deleted file mode 100644 index b23c655..0000000 --- a/.github/workflows/cicd.yml +++ /dev/null @@ -1,239 +0,0 @@ -name: CI/CD with Lazarus IDE on multiple operating systems. - -permissions: - contents: write - -on: - push: - pull_request: - workflow_dispatch: - # Automatic cron build every 6 months to check if everything still works. - schedule: - - cron: "0 0 1 1/6 *" - -jobs: - build: - runs-on: ${{ matrix.os }} - - strategy: - # Set fail-fast to false to ensure that feedback is delivered for all matrix combinations. Consider changing this to true when your workflow is stable. - fail-fast: false - - # Set up an array to perform the following three build configurations. - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - include: - - os: windows-latest - LAZBUILD_WITH_PATH: c:/lazarus/lazbuild - RELEASE_ZIP_FILE: trackereditor_windows_amd64.zip - LAZ_OPT: - - os: ubuntu-latest - LAZBUILD_WITH_PATH: lazbuild - RELEASE_ZIP_FILE: trackereditor_linux_amd64_gtk2.zip - LAZ_OPT: --widgetset=gtk2 - - os: macos-latest - LAZBUILD_WITH_PATH: /Applications/Lazarus/lazbuild - RELEASE_ZIP_FILE: trackereditor_macOS_amd64.zip - LAZ_OPT: --widgetset=cocoa - - steps: - - uses: actions/checkout@v4 - - - name: Install Lazarus IDE - run: | - if [ "$RUNNER_OS" == "Linux" ]; then - sudo apt install -y lazarus zip xvfb - elif [ "$RUNNER_OS" == "Windows" ]; then - choco install lazarus zip - # https://wiki.overbyte.eu/wiki/index.php/ICS_Download#Download_OpenSSL_Binaries - curl -L -O --output-dir enduser https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases/download/V1.32.0/libssl-3-x64.dll - curl -L -O --output-dir enduser https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases/download/V1.32.0/libcrypto-3-x64.dll - elif [ "$RUNNER_OS" == "macOS" ]; then - brew install --cask lazarus - else - echo "$RUNNER_OS not supported" - exit 1 - fi - shell: bash - - - name: Build Release version - # Build trackereditor project (Release mode) - run: ${{ matrix.LAZBUILD_WITH_PATH }} --build-all --build-mode=Release ${{ matrix.LAZ_OPT }} source/project/tracker_editor/trackereditor.lpi - shell: bash - - - name: Build Unit Test on Windows - if: matrix.os == 'windows-latest' - # Build unit test project (Debug mode) - run: ${{ matrix.LAZBUILD_WITH_PATH }} --build-all --build-mode=Debug ${{ matrix.LAZ_OPT }} source/project/unit_test/tracker_editor_test.lpi - shell: bash - - - name: Run Unit Test on Windows - if: matrix.os == 'windows-latest' - # Also remove all the extra file created by test. - # We do not what it in the ZIP release files. - # Undo all changes made by testing. - run: | - set -e - enduser/test_trackereditor -a --format=plain - set +e - - # remove file created by unit test - rm -f enduser/console_log.txt - rm -f enduser/export_trackers.txt - git reset --hard - shell: bash - - - name: Test OpenSSL works on Linux CI - if: matrix.os == 'ubuntu-latest' - run: xvfb-run --auto-servernum enduser/trackereditor -TEST_SSL - - - name: Create a zip file for Linux release. - if: matrix.os == 'ubuntu-latest' - run: zip -j ${{ matrix.RELEASE_ZIP_FILE }} enduser/*.txt enduser/trackereditor - shell: bash - - - name: Create a zip file for Windows release. - if: matrix.os == 'windows-latest' - run: | - zip -j ${{ matrix.RELEASE_ZIP_FILE }} enduser/*.txt enduser/trackereditor.exe enduser/*.dll - shell: bash - - - name: Move program and icon into macOS .app - if: matrix.os == 'macos-latest' - env: - ICON_FILE: 'metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.png' - PROGRAM_NAME_WITH_PATH: 'enduser/trackereditor' - run: | - # remove the path - PROGRAM_NAME_ONLY=$(basename -- "$PROGRAM_NAME_WITH_PATH") - - # ------ Move program to app - # remove symbolic link in app. Need real program here. - rm -f "${PROGRAM_NAME_WITH_PATH}.app/Contents/MacOS/${PROGRAM_NAME_ONLY}" - # copy the program to the app version. - mv -f "${PROGRAM_NAME_WITH_PATH}" "${PROGRAM_NAME_WITH_PATH}.app/Contents/MacOS" - - # ------ Create icon set and move it into the app - iconset_folder="temp_folder.iconset" - rm -rf "${iconset_folder}" - mkdir -p "${iconset_folder}" - - for s in 16 32 128 256 512; do - d=$(($s*2)) - sips -Z $s $ICON_FILE --out "${iconset_folder}/icon_${s}x$s.png" - sips -Z $d $ICON_FILE --out "${iconset_folder}/icon_${s}x$s@2x.png" - done - - # create .icns icon file - iconutil -c icns "${iconset_folder}" -o "iconfile.icns" - rm -r "${iconset_folder}" - - # move icon file to the app - mv -f "iconfile.icns" "${PROGRAM_NAME_WITH_PATH}.app/Contents/Resources" - - # add icon to plist xml file CFBundleIconFile = "iconfile" - plutil -insert CFBundleIconFile -string "iconfile" "${PROGRAM_NAME_WITH_PATH}.app/Contents/Info.plist" - shell: bash - - - name: Codesign macOS app bundle - # This macOS Codesign step is copied from: - # https://federicoterzi.com/blog/automatic-code-signing-and-notarization-for-macos-apps-using-github-actions/ - # This is a bit different from the previous version for Travis-CI build system to build bittorrent tracker editor - if: matrix.os == 'macos-latest' - env: - MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} - MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} - MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} - MACOS_APP: enduser/trackereditor.app - run: | - # Turn our base64-encoded certificate back to a regular .p12 file - echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12 - - # We need to create a new keychain, otherwise using the certificate will prompt - # with a UI dialog asking for the certificate password, which we can't - # use in a headless CI environment - - security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain - security default-keychain -s build.keychain - security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain - security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain - - # We finally codesign our app bundle, specifying the Hardened runtime option. - #/usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime "$MACOS_APP" -v - - # sign the app. -sign is the developer cetificate ID - # Must use --deep to sign all internal content - /usr/bin/codesign --timestamp --force --options runtime --deep --sign "$MACOS_CERTIFICATE_NAME" "$MACOS_APP" - shell: bash - - - name: Notarize macOS app bundle - if: matrix.os == 'macos-latest' - env: - PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} - PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} - PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} - MACOS_APP: enduser/trackereditor.app - NOTARIZE_RESULT: notarize_result.txt - NOTARIZE_LOG: notarize_log.json - SUBMISSION_ID: "" - run: | - # Store the notarization credentials so that we can prevent a UI password dialog - # from blocking the CI - - echo "Create keychain profile" - xcrun notarytool store-credentials "notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD" - - # We can't notarize an app bundle directly, but we need to compress it as an archive. - # Therefore, we create a zip file containing our app bundle, so that we can send it to the - # notarization service - - echo "Creating temp notarization archive" - ditto -c -k --keepParent "$MACOS_APP" "notarization.zip" - - # Here we send the notarization request to the Apple's Notarization service, waiting for the result. - # This typically takes a few seconds inside a CI environment, but it might take more depending on the App - # characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if - # you're curious - - echo "Notarize app" - xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait > "$NOTARIZE_RESULT" 2>&1 - - echo "Notarize log" - SUBMISSION_ID=`awk '/id: / { print $2;exit; }' $NOTARIZE_RESULT` - echo "id: ${SUBMISSION_ID}" - xcrun notarytool log "$SUBMISSION_ID" --keychain-profile "notarytool-profile" "$NOTARIZE_LOG" - cat "$NOTARIZE_LOG" - - # These files are no longer needed, so we can remove them. - rm -f "$NOTARIZE_LOG" - rm -f "$NOTARIZE_RESULT" - - # Finally, we need to "attach the staple" to our executable, which will allow our app to be - # validated by macOS even when an internet connection is not available. - echo "Attach staple" - xcrun stapler staple "$MACOS_APP" - - # Remove notarization.zip, otherwise it will also be 'released' to the end user - rm -f "notarization.zip" - - # zip only the app folder. - echo "Zip macOS app file" - /usr/bin/ditto -c -k --keepParent "$MACOS_APP" "${{ matrix.RELEASE_ZIP_FILE }}" - shell: bash - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: artifact-${{ matrix.os }} - path: ${{ matrix.RELEASE_ZIP_FILE }} - compression-level: 0 # no compression. Content is already a zip file - if-no-files-found: error - - - name: Zip file release to end user - uses: softprops/action-gh-release@v1 - if: startsWith(github.ref, 'refs/tags/') - with: - files: | - *.zip From c540bc8389cce2bb9712dcc09f4ac800fdb9e84a Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Sat, 5 Oct 2024 21:06:03 +0200 Subject: [PATCH 05/46] add: 3 new GitHub action build files Each operating system has its own build file. This is a replacement for the previous cicd.yml file. --- .github/workflows/cicd_macos.yaml | 154 ++++++++++++++++++++++++++++ .github/workflows/cicd_ubuntu.yaml | 76 ++++++++++++++ .github/workflows/cicd_windows.yaml | 84 +++++++++++++++ 3 files changed, 314 insertions(+) create mode 100644 .github/workflows/cicd_macos.yaml create mode 100644 .github/workflows/cicd_ubuntu.yaml create mode 100644 .github/workflows/cicd_windows.yaml diff --git a/.github/workflows/cicd_macos.yaml b/.github/workflows/cicd_macos.yaml new file mode 100644 index 0000000..97a9a70 --- /dev/null +++ b/.github/workflows/cicd_macos.yaml @@ -0,0 +1,154 @@ +name: CI/CD on macOS systems. + +permissions: + contents: write + +on: + push: + pull_request: + workflow_dispatch: + # Automatic cron build every 6 months to check if everything still works. + schedule: + - cron: "0 0 1 1/6 *" + +jobs: + build: + runs-on: macos-latest + env: + LAZBUILD_WITH_PATH: /Applications/Lazarus/lazbuild + RELEASE_ZIP_FILE: trackereditor_macOS_amd64.zip + LAZ_OPT: --widgetset=cocoa + + steps: + - uses: actions/checkout@v4 + + - name: Install Lazarus IDE + run: brew install --cask lazarus + + - name: Build Release version + # Build trackereditor project (Release mode) + run: ${{ env.LAZBUILD_WITH_PATH }} --build-all --build-mode=Release ${{ env.LAZ_OPT }} source/project/tracker_editor/trackereditor.lpi + shell: bash + + - name: Move program and icon into macOS .app + env: + ICON_FILE: 'metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.png' + PROGRAM_NAME_WITH_PATH: 'enduser/trackereditor' + run: | + # remove the path + PROGRAM_NAME_ONLY=$(basename -- "$PROGRAM_NAME_WITH_PATH") + + # ------ Move program to app + # remove symbolic link in app. Need real program here. + rm -f "${PROGRAM_NAME_WITH_PATH}.app/Contents/MacOS/${PROGRAM_NAME_ONLY}" + # copy the program to the app version. + mv -f "${PROGRAM_NAME_WITH_PATH}" "${PROGRAM_NAME_WITH_PATH}.app/Contents/MacOS" + + # ------ Create icon set and move it into the app + iconset_folder="temp_folder.iconset" + rm -rf "${iconset_folder}" + mkdir -p "${iconset_folder}" + + for s in 16 32 128 256 512; do + d=$(($s*2)) + sips -Z $s $ICON_FILE --out "${iconset_folder}/icon_${s}x$s.png" + sips -Z $d $ICON_FILE --out "${iconset_folder}/icon_${s}x$s@2x.png" + done + + # create .icns icon file + iconutil -c icns "${iconset_folder}" -o "iconfile.icns" + rm -r "${iconset_folder}" + + # move icon file to the app + mv -f "iconfile.icns" "${PROGRAM_NAME_WITH_PATH}.app/Contents/Resources" + + # add icon to plist xml file CFBundleIconFile = "iconfile" + plutil -insert CFBundleIconFile -string "iconfile" "${PROGRAM_NAME_WITH_PATH}.app/Contents/Info.plist" + shell: bash + + - name: Codesign macOS app bundle + # This macOS Codesign step is copied from: + # https://federicoterzi.com/blog/automatic-code-signing-and-notarization-for-macos-apps-using-github-actions/ + # This is a bit different from the previous version for Travis-CI build system to build bittorrent tracker editor + env: + MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} + MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} + MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} + MACOS_APP: enduser/trackereditor.app + run: | + # Turn our base64-encoded certificate back to a regular .p12 file + echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12 + + # We need to create a new keychain, otherwise using the certificate will prompt + # with a UI dialog asking for the certificate password, which we can't + # use in a headless CI environment + + security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain + security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain + + # We finally codesign our app bundle, specifying the Hardened runtime option. + #/usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime "$MACOS_APP" -v + + # sign the app. -sign is the developer cetificate ID + # Must use --deep to sign all internal content + /usr/bin/codesign --timestamp --force --options runtime --deep --sign "$MACOS_CERTIFICATE_NAME" "$MACOS_APP" + shell: bash + + - name: Notarize macOS app bundle + env: + PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} + PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} + PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} + MACOS_APP: enduser/trackereditor.app + run: | + # Store the notarization credentials so that we can prevent a UI password dialog + # from blocking the CI + + echo "Create keychain profile" + xcrun notarytool store-credentials "notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD" + + # We can't notarize an app bundle directly, but we need to compress it as an archive. + # Therefore, we create a zip file containing our app bundle, so that we can send it to the + # notarization service + + echo "Creating temp notarization archive" + ditto -c -k --keepParent "$MACOS_APP" "notarization.zip" + + # Here we send the notarization request to the Apple's Notarization service, waiting for the result. + # This typically takes a few seconds inside a CI environment, but it might take more depending on the App + # characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if + # you're curious + + echo "Notarize app" + xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait + + # Finally, we need to "attach the staple" to our executable, which will allow our app to be + # validated by macOS even when an internet connection is not available. + echo "Attach staple" + xcrun stapler staple "$MACOS_APP" + + # Remove notarization.zip, otherwise it will also be 'released' to the end user + rm -f "notarization.zip" + + # zip only the app folder. + echo "Zip macOS app file" + /usr/bin/ditto -c -k --keepParent "$MACOS_APP" "${{ env.RELEASE_ZIP_FILE }}" + shell: bash + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: artifact-${{ runner.os }} + path: ${{ env.RELEASE_ZIP_FILE }} + compression-level: 0 # no compression. Content is already a zip file + if-no-files-found: error + + - name: File release to end user + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: ${{ env.RELEASE_ZIP_FILE }} diff --git a/.github/workflows/cicd_ubuntu.yaml b/.github/workflows/cicd_ubuntu.yaml new file mode 100644 index 0000000..2616563 --- /dev/null +++ b/.github/workflows/cicd_ubuntu.yaml @@ -0,0 +1,76 @@ +name: CI/CD on Linux systems. + +permissions: + contents: write + +on: + push: + pull_request: + workflow_dispatch: + # Automatic cron build every 6 months to check if everything still works. + schedule: + - cron: "0 0 1 1/6 *" + +jobs: + build: + runs-on: ubuntu-latest + env: + LAZBUILD_WITH_PATH: lazbuild + RELEASE_FILE_NAME: trackereditor_linux_amd64.AppImage + LAZ_OPT: --widgetset=qt5 + + steps: + - uses: actions/checkout@v4 + + - name: Install dependency + run: sudo apt-get install -y lazarus fpc fuse xvfb libqt5pas-dev qt5-qmake qtwayland5 qt5-gtk-platformtheme libqt5svg5 + shell: bash + + - name: Build Release version + # Build trackereditor project (Release mode) + run: ${{ env.LAZBUILD_WITH_PATH }} --build-all --build-mode=Release ${{ env.LAZ_OPT }} source/project/tracker_editor/trackereditor.lpi + shell: bash + + - name: Test OpenSSL works on Linux CI + run: xvfb-run --auto-servernum enduser/trackereditor -TEST_SSL + shell: bash + + - name: Download linuxdeploy + # Use static versions of AppImage builder. So it won't depend on some specific OS image or library version. + run: | + curl -L -O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-static-x86_64.AppImage + curl -L -O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-static-x86_64.AppImage + curl -L -O https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage + chmod +x linuxdeploy-*.AppImage + shell: bash + + - name: Create AppImage + # NO_STRIP: true => or else warning: qt.qpa.plugin: Could not find the Qt platform plugin "wayland" in "" + # LDAI_NO_APPSTREAM=1: skip checking AppStream metadata for issues + env: + NO_STRIP: true + LDAI_NO_APPSTREAM: 1 + LDAI_OUTPUT: ${{ env.RELEASE_FILE_NAME }} + run: | + ./linuxdeploy-static-x86_64.AppImage \ + --output appimage \ + --appdir temp_appdir \ + --plugin qt \ + --executable enduser/trackereditor \ + --desktop-file metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.desktop \ + --icon-file metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.png + shell: bash + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: artifact-${{ runner.os }} + path: ${{ env.RELEASE_FILE_NAME }} + compression-level: 0 # no compression. Content is already a compress file + if-no-files-found: error + + - name: File release to end user + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: ${{ env.RELEASE_FILE_NAME }} diff --git a/.github/workflows/cicd_windows.yaml b/.github/workflows/cicd_windows.yaml new file mode 100644 index 0000000..7465dd4 --- /dev/null +++ b/.github/workflows/cicd_windows.yaml @@ -0,0 +1,84 @@ +name: CI/CD on Windows systems. + +permissions: + contents: write + +on: + push: + pull_request: + workflow_dispatch: + # Automatic cron build every 6 months to check if everything still works. + schedule: + - cron: "0 0 1 1/6 *" + +jobs: + build: + runs-on: windows-2022 + env: + LAZBUILD_WITH_PATH: c:/lazarus/lazbuild + RELEASE_ZIP_FILE: trackereditor_windows_amd64.zip + LAZ_OPT: + + steps: + - uses: actions/checkout@v4 + + - name: Install winget + # winget will be included in windows server 2025 + uses: Cyberboss/install-winget@v1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Lazarus IDE + run: winget install lazarus --disable-interactivity --accept-source-agreements --silent + + - name: Download OpenSSL *.dll + run: | + # Need OpenSSL *.dll to download updated trackers from the internet. + # https://wiki.overbyte.eu/wiki/index.php/ICS_Download#Download_OpenSSL_Binaries + curl -L -O --output-dir enduser https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases/download/V1.32.0/libssl-3-x64.dll + curl -L -O --output-dir enduser https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases/download/V1.32.0/libcrypto-3-x64.dll + shell: bash + + - name: Build Release version + # Build trackereditor project (Release mode) + run: ${{ env.LAZBUILD_WITH_PATH }} --build-all --build-mode=Release ${{ env.LAZ_OPT }} source/project/tracker_editor/trackereditor.lpi + shell: bash + + - name: Build Unit Test on Windows + # Build unit test project (Debug mode) + run: ${{ env.LAZBUILD_WITH_PATH }} --build-all --build-mode=Debug ${{ env.LAZ_OPT }} source/project/unit_test/tracker_editor_test.lpi + shell: bash + + - name: Run Unit Test on Windows + # Also remove all the extra file created by test. + # We do not what it in the ZIP release files. + # Undo all changes made by testing. + run: | + set -e + enduser/test_trackereditor -a --format=plain + set +e + + # remove file created by unit test + rm -f enduser/console_log.txt + rm -f enduser/export_trackers.txt + git reset --hard + shell: bash + + - name: Create a zip file for Windows release. + run: | + Compress-Archive -Path enduser\*.txt, enduser\trackereditor.exe, enduser\*.dll -DestinationPath $Env:RELEASE_ZIP_FILE + shell: pwsh + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: artifact-${{ runner.os }} + path: ${{ env.RELEASE_ZIP_FILE }} + compression-level: 0 # no compression. Content is already a zip file + if-no-files-found: error + + - name: File release to end user + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: ${{ env.RELEASE_ZIP_FILE }} From 202616813dd8183b9e278f43502f6b33c5f947be Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Sat, 5 Oct 2024 21:48:39 +0200 Subject: [PATCH 06/46] Add new workflow status badge The three new GitHub action builds each require a separate build status. [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 713853c..4b619d9 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This software works on Windows 7+, macOS and Linux. ## Build Status: ## Continuous integration|Status| Generate an executable file for the operating system| Download link ------------|---------|---------|---------- -GitHub Actions |[![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/gerryferdinandus/bittorrent-tracker-editor/cicd.yml)](https://github.com/GerryFerdinandus/bittorrent-tracker-editor/actions/workflows/cicd.yml)|Linux(amd64), macOS(Intel processors) and Windows|[![GitHub Latest release](https://img.shields.io/github/release/GerryFerdinandus/bittorrent-tracker-editor/all.svg)](https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases) +GitHub Actions |[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/gerryferdinandus/bittorrent-tracker-editor/cicd_ubuntu.yaml?label=Ubuntu)](https://github.com/GerryFerdinandus/bittorrent-tracker-editor/actions/workflows/cicd_ubuntu.yaml)[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/gerryferdinandus/bittorrent-tracker-editor/cicd_windows.yaml?label=Windows)](https://github.com/GerryFerdinandus/bittorrent-tracker-editor/actions/workflows/cicd_windows.yaml)[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/gerryferdinandus/bittorrent-tracker-editor/cicd_macos.yaml?label=macOS)](https://github.com/GerryFerdinandus/bittorrent-tracker-editor/actions/workflows/cicd_macos.yaml)|Linux(amd64), macOS(Intel processors) and Windows|[![GitHub Latest release](https://img.shields.io/github/release/GerryFerdinandus/bittorrent-tracker-editor/all.svg)](https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases) GitHub Actions (Ubuntu snap) |[![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/gerryferdinandus/bittorrent-tracker-editor/snap.yml)](https://github.com/GerryFerdinandus/bittorrent-tracker-editor/actions/workflows/snap.yml)|Linux (amd64 and arm64)|[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-white.svg)](https://snapcraft.io/bittorrent-tracker-editor) Flathub build server||Linux (amd64 and arm64)|Download on Flathub --- From ab977d53512dbbcc0b8567d630d83ccb3c193798 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Tue, 29 Oct 2024 03:03:07 +0100 Subject: [PATCH 07/46] snap: build lazarus sdk from source Reason for change: - Ubuntu default lazarus via apt get is not the latest version. - Need to build from source to get the latest Arm64 version. --- snap/snapcraft.yaml | 84 +++++++++++++++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 3aabc9a..49154c3 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -12,10 +12,9 @@ architectures: apps: bittorrent-tracker-editor: - desktop: mainprogram/io.github.gerryferdinandus.bittorrent-tracker-editor.desktop extensions: - kde-neon - command: mainprogram/trackereditor + command: trackereditor environment: # Fallback to XWayland if running in a Wayland session. DISABLE_WAYLAND: 1 @@ -23,33 +22,68 @@ apps: - home - network - removable-media - - pulseaudio parts: - mainprogram: # Build and add to snap the main program: mainprogram/trackereditor - source: https://github.com/GerryFerdinandus/bittorrent-tracker-editor.git - parse-info: [metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.metainfo.xml] + build_lazarus: + source: . plugin: nil - override-build: | - craftctl default - lazbuild --build-mode=Release --widgetset=qt5 source/project/tracker_editor/trackereditor.lpi - mkdir $CRAFT_PART_INSTALL/mainprogram - cp enduser/trackereditor $CRAFT_PART_INSTALL/mainprogram - cp metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.desktop $CRAFT_PART_INSTALL/mainprogram build-packages: - fpc - - lazarus - - libqt5pas-dev + - libqt5x11extras5-dev + build-environment: + - LAZARUS_URL_TAR_GZ: "https://sourceforge.net/projects/lazarus/files/Lazarus%20Zip%20_%20GZip/Lazarus%203.4/lazarus-3.4-0.tar.gz" + - LAZARUS_QT_VERSION: "5" + - LIB_DIR: "/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR" + - LAZARUS_DIR: "$PWD/lazarus" + override-build: | + # Remove the older libQTpas.so + rm -f $LIB_DIR/libQt${LAZARUS_QT_VERSION}Pas.* - libQt5Pas: # Add to snap the libQt5Pas support library : usr/lib/*/libQt5Pas.* - plugin: nil - stage-packages: - - libqt5pas1 - prime: - - usr/lib/*/libQt5Pas.* - -# Only 3 files are explicitly added in this snap: -# trackereditor + .desktop file via copy command -# libQt5Pas.so via prime + #Download lazarus source code. Directory 'lazarus' will be created in the root. + curl -L -O $LAZARUS_URL_TAR_GZ + tar -xzf *.tar.gz + + # Create libQTpas.so and put it in snap $CRAFT_PART_INSTALL + cd "$LAZARUS_DIR/lcl/interfaces/qt${LAZARUS_QT_VERSION}/cbindings/" + qmake + make -j$(nproc) + make install + cp -av --parents $LIB_DIR/libQt${LAZARUS_QT_VERSION}Pas.* $CRAFT_PART_INSTALL + + # make lazbuild and put the link with extra parameter in /usr/bin/ + cd "$LAZARUS_DIR" + make lazbuild + echo "$LAZARUS_DIR/lazbuild --primary-config-path=$LAZARUS_DIR --lazarusdir=$LAZARUS_DIR \$*" > /usr/bin/lazbuild + chmod 777 /usr/bin/lazbuild + + mainprogram: # Build and add to snap the main program: trackereditor + after: [build_lazarus] + source: . + plugin: nil + parse-info: [metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.metainfo.xml] + override-build: | + lazbuild --build-mode=Release --widgetset=qt5 source/project/tracker_editor/trackereditor.lpi + install enduser/trackereditor $CRAFT_PART_INSTALL/ + +# -------------------------------------------------------------- +# Only 2 files are explicitly added in this snap +# - main program: enduser/trackereditor +# - Lazarus QT suport library: libQt5Pas.so +# +# Create snap. Run from the project root folder: +# snapcraft --verbosity verbose +# +# he snapTo look what is inside t file. Directory 'squashfs-root' will be created in the root folder: +# unsquashfs *.snap +# +# Install the snap: +# sudo snap install --devmode ./*.snap # -# Use: 'unsquashfs *.snap' to look what is inside the snap file. +# Run the snap +# snap run bittorrent-tracker-editor +# -------------------------------------------------------------- +# Todo: building for QT6 is still not working with snap +# https://askubuntu.com/questions/1460242/ubuntu-22-04-with-qt6-qmake-could-not-find-a-qt-installation-of +# qtchooser -install qt{LAZARUS_QT_VERSION} $(which qmake6) +# export QT_SELECT=qt{LAZARUS_QT_VERSION} +# -------------------------------------------------------------- From 34500f07e46ceaef03acdee9e0f8ee80d55a552e Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Tue, 29 Oct 2024 03:31:44 +0100 Subject: [PATCH 08/46] snap: add curl local + github acton ubuntu snap build pass the test. But failed on remote Ubuntu site build. :: /bin/bash: line 56: curl: command not found --- snap/snapcraft.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 49154c3..ba9f66d 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -28,6 +28,8 @@ parts: source: . plugin: nil build-packages: + - curl + - build-essential - fpc - libqt5x11extras5-dev build-environment: From c44d2f28af2e3a0d65a82d008a820592305ee8d0 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Tue, 29 Oct 2024 04:32:58 +0100 Subject: [PATCH 09/46] snap: add missing desktop file Strange that the snap linter does not give a warning about missing desktop files. --- snap/snapcraft.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index ba9f66d..fc9cb15 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -12,6 +12,7 @@ architectures: apps: bittorrent-tracker-editor: + desktop: io.github.gerryferdinandus.bittorrent-tracker-editor.desktop extensions: - kde-neon command: trackereditor @@ -66,6 +67,7 @@ parts: override-build: | lazbuild --build-mode=Release --widgetset=qt5 source/project/tracker_editor/trackereditor.lpi install enduser/trackereditor $CRAFT_PART_INSTALL/ + install metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.desktop $CRAFT_PART_INSTALL/ # -------------------------------------------------------------- # Only 2 files are explicitly added in this snap From e8d0394177fabc137e26c75c082a8547c2a67c27 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Sun, 17 Nov 2024 15:33:24 +0100 Subject: [PATCH 10/46] Add: build releases for qt5 and qt6 zip files There is only zip file for gtk2 Now add Qt5 and Qt6 zip file See issue #50 --- .github/workflows/cicd_ubuntu.yaml | 135 ++++++++++++++++++++++++----- 1 file changed, 115 insertions(+), 20 deletions(-) diff --git a/.github/workflows/cicd_ubuntu.yaml b/.github/workflows/cicd_ubuntu.yaml index 2616563..a6673df 100644 --- a/.github/workflows/cicd_ubuntu.yaml +++ b/.github/workflows/cicd_ubuntu.yaml @@ -13,44 +13,139 @@ on: jobs: build: - runs-on: ubuntu-latest - env: - LAZBUILD_WITH_PATH: lazbuild - RELEASE_FILE_NAME: trackereditor_linux_amd64.AppImage - LAZ_OPT: --widgetset=qt5 + runs-on: ubuntu-24.04 #ubuntu-latest + + env: # Use the latest Lazarus source code. + LAZARUS_URL_TAR_GZ: "https://sourceforge.net/projects/lazarus/files/Lazarus%20Zip%20_%20GZip/Lazarus%203.6/lazarus-3.6-0.tar.gz" + + strategy: + # Set fail-fast to false to ensure that feedback is delivered for all matrix combinations. + fail-fast: false + + # Set up an include to perform the following build configurations. + matrix: + include: + - BUILD_TARGET: gtk2_amd64 + RELEASE_FILE_NAME: trackereditor_linux_amd64_gtk2.zip + LAZ_OPT: --widgetset=gtk2 + + - BUILD_TARGET: qt5_amd64 + RELEASE_FILE_NAME: trackereditor_linux_amd64_qt5.zip + LAZ_OPT: --widgetset=qt5 + QT_VERSION_CI: '5' + + - BUILD_TARGET: qt6_amd64 + RELEASE_FILE_NAME: trackereditor_linux_amd64_qt6.zip + LAZ_OPT: --widgetset=qt6 + QT_VERSION_CI: '6' + + - BUILD_TARGET: AppImage_amd64 + RELEASE_FILE_NAME: trackereditor_linux_amd64_qt6.AppImage + LAZ_OPT: --widgetset=qt6 + QT_VERSION_CI: '6' steps: - uses: actions/checkout@v4 - - name: Install dependency - run: sudo apt-get install -y lazarus fpc fuse xvfb libqt5pas-dev qt5-qmake qtwayland5 qt5-gtk-platformtheme libqt5svg5 + - name: Install dependency for all build + run: sudo apt-get install -y fpc xvfb shell: bash - - name: Build Release version - # Build trackereditor project (Release mode) - run: ${{ env.LAZBUILD_WITH_PATH }} --build-all --build-mode=Release ${{ env.LAZ_OPT }} source/project/tracker_editor/trackereditor.lpi + - name: Install dependency for gtk2 + if: matrix.QT_VERSION_CI == '' + run: sudo apt-get install -y libgtk2.0-dev shell: bash - - name: Test OpenSSL works on Linux CI - run: xvfb-run --auto-servernum enduser/trackereditor -TEST_SSL + - name: Install dependency for qt5 + if: matrix.QT_VERSION_CI == '5' + run: sudo apt-get install -y libqt5x11extras5-dev shell: bash - - name: Download linuxdeploy - # Use static versions of AppImage builder. So it won't depend on some specific OS image or library version. - run: | + - name: Install dependency for qt6 + if: matrix.QT_VERSION_CI == '6' + run: sudo apt-get install -y qt6-base-dev + shell: bash + + - name: Install dependency for AppImage + if: matrix.BUILD_TARGET == 'AppImage_amd64' + run: | + # Add wayland plugin and platform theme + sudo apt-get install -y fuse qt6-wayland qt6-xdgdesktopportal-platformtheme qt6-gtk-platformtheme + # Use static versions of AppImage builder. So it won't depend on some specific OS image or library version. curl -L -O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-static-x86_64.AppImage curl -L -O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-static-x86_64.AppImage curl -L -O https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage chmod +x linuxdeploy-*.AppImage shell: bash + - name: Download Lazarus source code + run: | + #Download lazarus source code. Directory 'lazarus' will be created in the project folder. + curl -L -O ${{ env.LAZARUS_URL_TAR_GZ }} + tar -xzf *.tar.gz + shell: bash + + - name: Build libQTpas.so + if: matrix.QT_VERSION_CI != '' + run: | + cd "${{ github.workspace }}/lazarus/lcl/interfaces/qt${{ matrix.QT_VERSION_CI }}/cbindings/" + /usr/lib/qt${{ matrix.QT_VERSION_CI }}/bin/qmake + make -j$(nproc) + sudo make install + shell: bash + + - name: Build lazbuild + env: + LAZARUS_DIR: "${{ github.workspace }}/lazarus" + run: | + # make lazbuild and put the link with extra parameter in project folder + cd "$LAZARUS_DIR" + make lazbuild + echo "$LAZARUS_DIR/lazbuild --primary-config-path=$LAZARUS_DIR --lazarusdir=$LAZARUS_DIR \$*" > ${{ github.workspace }}/lazbuild + chmod +x ${{ github.workspace }}/lazbuild + shell: bash + + - name: Build trackereditor + # Build trackereditor project (Release mode) + run: ./lazbuild --build-all --build-mode=Release ${{ matrix.LAZ_OPT }} source/project/tracker_editor/trackereditor.lpi + shell: bash + + - name: Test if OpenSSL works on Linux CI + run: xvfb-run --auto-servernum enduser/trackereditor -TEST_SSL + shell: bash + + - name: Copy libQtpas.so before releasing the Qt5/Qt6 zip file format. + if: matrix.BUILD_TARGET == 'qt5_amd64' || matrix.BUILD_TARGET == 'qt6_amd64' + run: | + cp -av /usr/lib/*/libQt?Pas.* ${{ github.workspace }}/enduser + cat < ${{ github.workspace }}/enduser/missing_libQtPas.so.txt + Start program with: + env LD_LIBRARY_PATH=. ./trackereditor + + This uses libQtpas.so, which is necessary for this program. + Some Linux OS stores may also offer this libQtpas.so + https://archlinux.org/packages/extra/x86_64/qt5pas/ + https://archlinux.org/packages/extra/x86_64/qt6pas/ + EOF + shell: bash + + - name: Create a gtk2 or Qt5/Qt6 release in zip file format. + if: matrix.BUILD_TARGET != 'AppImage_amd64' + run: zip -j ${{ matrix.RELEASE_FILE_NAME }} enduser/*.txt enduser/libQt* enduser/trackereditor + shell: bash + - name: Create AppImage + if: matrix.BUILD_TARGET == 'AppImage_amd64' # NO_STRIP: true => or else warning: qt.qpa.plugin: Could not find the Qt platform plugin "wayland" in "" # LDAI_NO_APPSTREAM=1: skip checking AppStream metadata for issues env: - NO_STRIP: true + # NO_STRIP: true LDAI_NO_APPSTREAM: 1 - LDAI_OUTPUT: ${{ env.RELEASE_FILE_NAME }} + LDAI_OUTPUT: ${{ matrix.RELEASE_FILE_NAME }} + QMAKE: /usr/lib/qt${{ matrix.QT_VERSION_CI }}/bin/qmake + EXTRA_QT_MODULES: waylandcompositor + EXTRA_PLATFORM_PLUGINS: libqwayland-generic.so;libqwayland-egl.so + DEPLOY_PLATFORM_THEMES: true run: | ./linuxdeploy-static-x86_64.AppImage \ --output appimage \ @@ -64,8 +159,8 @@ jobs: - name: Upload Artifact uses: actions/upload-artifact@v4 with: - name: artifact-${{ runner.os }} - path: ${{ env.RELEASE_FILE_NAME }} + name: artifact-${{ matrix.RELEASE_FILE_NAME }} + path: ${{ matrix.RELEASE_FILE_NAME }} compression-level: 0 # no compression. Content is already a compress file if-no-files-found: error @@ -73,4 +168,4 @@ jobs: uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/') with: - files: ${{ env.RELEASE_FILE_NAME }} + files: ${{ matrix.RELEASE_FILE_NAME }} From edff55c68ce3669685bc91287b6c3cd68dd11dde Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Sun, 17 Nov 2024 18:31:01 +0100 Subject: [PATCH 11/46] Building without macOS certificate should be possible GitHub fork should be able to build without macOS certificate If no certificate is detected, generate a macOS application without a certificate --- .github/workflows/cicd_macos.yaml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cicd_macos.yaml b/.github/workflows/cicd_macos.yaml index 97a9a70..68abf70 100644 --- a/.github/workflows/cicd_macos.yaml +++ b/.github/workflows/cicd_macos.yaml @@ -17,7 +17,9 @@ jobs: env: LAZBUILD_WITH_PATH: /Applications/Lazarus/lazbuild RELEASE_ZIP_FILE: trackereditor_macOS_amd64.zip + MACOS_APP: enduser/trackereditor.app LAZ_OPT: --widgetset=cocoa + BUILD_WITH_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} steps: - uses: actions/checkout@v4 @@ -67,6 +69,7 @@ jobs: shell: bash - name: Codesign macOS app bundle + if: ${{ env.BUILD_WITH_CERTIFICATE != '' }} # This macOS Codesign step is copied from: # https://federicoterzi.com/blog/automatic-code-signing-and-notarization-for-macos-apps-using-github-actions/ # This is a bit different from the previous version for Travis-CI build system to build bittorrent tracker editor @@ -75,7 +78,6 @@ jobs: MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} - MACOS_APP: enduser/trackereditor.app run: | # Turn our base64-encoded certificate back to a regular .p12 file echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12 @@ -91,19 +93,19 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain # We finally codesign our app bundle, specifying the Hardened runtime option. - #/usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime "$MACOS_APP" -v + #/usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime "${{ env.MACOS_APP }}" -v # sign the app. -sign is the developer cetificate ID # Must use --deep to sign all internal content - /usr/bin/codesign --timestamp --force --options runtime --deep --sign "$MACOS_CERTIFICATE_NAME" "$MACOS_APP" + /usr/bin/codesign --timestamp --force --options runtime --deep --sign "$MACOS_CERTIFICATE_NAME" "${{ env.MACOS_APP }}" shell: bash - name: Notarize macOS app bundle + if: ${{ env.BUILD_WITH_CERTIFICATE != '' }} env: PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} - MACOS_APP: enduser/trackereditor.app run: | # Store the notarization credentials so that we can prevent a UI password dialog # from blocking the CI @@ -116,7 +118,7 @@ jobs: # notarization service echo "Creating temp notarization archive" - ditto -c -k --keepParent "$MACOS_APP" "notarization.zip" + ditto -c -k --keepParent "${{ env.MACOS_APP }}" "notarization.zip" # Here we send the notarization request to the Apple's Notarization service, waiting for the result. # This typically takes a few seconds inside a CI environment, but it might take more depending on the App @@ -129,14 +131,13 @@ jobs: # Finally, we need to "attach the staple" to our executable, which will allow our app to be # validated by macOS even when an internet connection is not available. echo "Attach staple" - xcrun stapler staple "$MACOS_APP" - - # Remove notarization.zip, otherwise it will also be 'released' to the end user - rm -f "notarization.zip" + xcrun stapler staple "${{ env.MACOS_APP }}" + shell: bash - # zip only the app folder. + - name: Zip only the app folder. + run: | echo "Zip macOS app file" - /usr/bin/ditto -c -k --keepParent "$MACOS_APP" "${{ env.RELEASE_ZIP_FILE }}" + /usr/bin/ditto -c -k --keepParent "${{ env.MACOS_APP }}" "${{ env.RELEASE_ZIP_FILE }}" shell: bash - name: Upload Artifact From 98064de880887b546011f81703791f7e0b7edb84 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Tue, 19 Nov 2024 20:00:22 +0100 Subject: [PATCH 12/46] add: Upload Artifact for snap file --- .github/workflows/snap.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index f6edf1f..59aa629 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -14,3 +14,11 @@ jobs: steps: - uses: actions/checkout@v4 - uses: snapcore/action-build@v1 + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: artifact-snap + path: "*.snap" + compression-level: 0 # no compression. Content is already a zip file + if-no-files-found: error From 34c4d02b28ee2f860c34374b72646f815c07e26f Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Tue, 19 Nov 2024 20:18:13 +0100 Subject: [PATCH 13/46] Download Lazarus from static GitHub URL link. This will: - Make Ubuntu.yaml and Snapcraft.yaml use the same Lazarus version. - No need to update two yaml files URL links anymore. --- .github/workflows/cicd_ubuntu.yaml | 2 +- snap/snapcraft.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cicd_ubuntu.yaml b/.github/workflows/cicd_ubuntu.yaml index a6673df..a348ebc 100644 --- a/.github/workflows/cicd_ubuntu.yaml +++ b/.github/workflows/cicd_ubuntu.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-24.04 #ubuntu-latest env: # Use the latest Lazarus source code. - LAZARUS_URL_TAR_GZ: "https://sourceforge.net/projects/lazarus/files/Lazarus%20Zip%20_%20GZip/Lazarus%203.6/lazarus-3.6-0.tar.gz" + LAZARUS_URL_TAR_GZ: "https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases/download/V1.32.0/lazarus.tar.gz" strategy: # Set fail-fast to false to ensure that feedback is delivered for all matrix combinations. diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index fc9cb15..9a88fe0 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -34,7 +34,7 @@ parts: - fpc - libqt5x11extras5-dev build-environment: - - LAZARUS_URL_TAR_GZ: "https://sourceforge.net/projects/lazarus/files/Lazarus%20Zip%20_%20GZip/Lazarus%203.4/lazarus-3.4-0.tar.gz" + - LAZARUS_URL_TAR_GZ: "https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases/download/V1.32.0/lazarus.tar.gz" - LAZARUS_QT_VERSION: "5" - LIB_DIR: "/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR" - LAZARUS_DIR: "$PWD/lazarus" From 850722b610374d75724050be8a8fcc7162feafd1 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Wed, 4 Dec 2024 21:06:31 +0100 Subject: [PATCH 14/46] Add: submodule dcpcrypt This is needed for sha256 used in torrent V2 file format See issue #51 --- .github/workflows/cicd_macos.yaml | 3 +++ .github/workflows/cicd_ubuntu.yaml | 10 ++++++++-- .github/workflows/cicd_windows.yaml | 3 +++ .github/workflows/snap.yml | 4 ++++ .gitmodules | 3 +++ submodule/dcpcrypt | 1 + 6 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 .gitmodules create mode 160000 submodule/dcpcrypt diff --git a/.github/workflows/cicd_macos.yaml b/.github/workflows/cicd_macos.yaml index 68abf70..8841d85 100644 --- a/.github/workflows/cicd_macos.yaml +++ b/.github/workflows/cicd_macos.yaml @@ -14,6 +14,7 @@ on: jobs: build: runs-on: macos-latest + timeout-minutes: 60 env: LAZBUILD_WITH_PATH: /Applications/Lazarus/lazbuild RELEASE_ZIP_FILE: trackereditor_macOS_amd64.zip @@ -23,6 +24,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: true - name: Install Lazarus IDE run: brew install --cask lazarus diff --git a/.github/workflows/cicd_ubuntu.yaml b/.github/workflows/cicd_ubuntu.yaml index a348ebc..f53f818 100644 --- a/.github/workflows/cicd_ubuntu.yaml +++ b/.github/workflows/cicd_ubuntu.yaml @@ -14,6 +14,7 @@ on: jobs: build: runs-on: ubuntu-24.04 #ubuntu-latest + timeout-minutes: 60 env: # Use the latest Lazarus source code. LAZARUS_URL_TAR_GZ: "https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases/download/V1.32.0/lazarus.tar.gz" @@ -46,6 +47,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: true - name: Install dependency for all build run: sudo apt-get install -y fpc xvfb @@ -136,10 +139,8 @@ jobs: - name: Create AppImage if: matrix.BUILD_TARGET == 'AppImage_amd64' - # NO_STRIP: true => or else warning: qt.qpa.plugin: Could not find the Qt platform plugin "wayland" in "" # LDAI_NO_APPSTREAM=1: skip checking AppStream metadata for issues env: - # NO_STRIP: true LDAI_NO_APPSTREAM: 1 LDAI_OUTPUT: ${{ matrix.RELEASE_FILE_NAME }} QMAKE: /usr/lib/qt${{ matrix.QT_VERSION_CI }}/bin/qmake @@ -156,6 +157,11 @@ jobs: --icon-file metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.png shell: bash + - name: Test AppImage + if: matrix.BUILD_TARGET == 'AppImage_amd64' + run: xvfb-run --auto-servernum ./${{ matrix.RELEASE_FILE_NAME }} -TEST_SSL + shell: bash + - name: Upload Artifact uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/cicd_windows.yaml b/.github/workflows/cicd_windows.yaml index 7465dd4..c9dca02 100644 --- a/.github/workflows/cicd_windows.yaml +++ b/.github/workflows/cicd_windows.yaml @@ -14,6 +14,7 @@ on: jobs: build: runs-on: windows-2022 + timeout-minutes: 60 env: LAZBUILD_WITH_PATH: c:/lazarus/lazbuild RELEASE_ZIP_FILE: trackereditor_windows_amd64.zip @@ -21,6 +22,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: true - name: Install winget # winget will be included in windows server 2025 diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index 59aa629..005920a 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -11,8 +11,12 @@ on: jobs: build: runs-on: ubuntu-latest + timeout-minutes: 60 steps: - uses: actions/checkout@v4 + with: + submodules: true + - uses: snapcore/action-build@v1 - name: Upload Artifact diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8688a44 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "submodule/dcpcrypt"] + path = submodule/dcpcrypt + url = git://lazarus-ccr.git.sourceforge.net/gitroot/lazarus-ccr/dcpcrypt diff --git a/submodule/dcpcrypt b/submodule/dcpcrypt new file mode 160000 index 0000000..14586ed --- /dev/null +++ b/submodule/dcpcrypt @@ -0,0 +1 @@ +Subproject commit 14586ed66d15fd91530ed5dfaab8a8e4bb8959ff From ccaf40acc25086cf0745f1cfd58a4b3cfb3540f8 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Wed, 4 Dec 2024 21:13:34 +0100 Subject: [PATCH 15/46] Update decodetorrent for V2 format decodetorent.pas can now decode torrent V2 format See issue #51 --- source/code/decodetorrent.pas | 506 ++++++++++++++---- .../project/tracker_editor/trackereditor.lpi | 23 +- .../project/tracker_editor/trackereditor.lpr | 6 +- 3 files changed, 417 insertions(+), 118 deletions(-) diff --git a/source/code/decodetorrent.pas b/source/code/decodetorrent.pas index 92cd81f..56b3b70 100644 --- a/source/code/decodetorrent.pas +++ b/source/code/decodetorrent.pas @@ -21,11 +21,12 @@ interface type - //Every torrent file have one or more files. - TDecodeTorrentFile = record - Filename: utf8string; - FileLength: int64; - end; + TTorrentVersion = ( + tv_V1, //< V1 version + tv_V2, //< V2 version + tv_Hybrid, //< V1 + V2 hybrid + tv_unknown //< Can not decode it + ); TDecodeTorrentFileNameAndLength = class Filename: utf8string; @@ -36,30 +37,42 @@ TDecodeTorrentFileNameAndLength = class { TDecodeTorrent } TDecodeTorrent = class private - FFilenameTorrent: UTF8String; + FFilenameTorrent: utf8string; FMemoryStream: TMemoryStream; FBEncoded: TBEncoded; FObjectListFileNameAndLength: TObjectList; FTotalFileSize: int64; + FTorrentVersion: TTorrentVersion; //Torrent file must have 'info' item. FBEncoded_Info: TBEncoded; FBEncoded_Comment: TBEncoded; - FInfoHash: utf8string; + FInfoHash_V1: utf8string; + FInfoHash_V2: utf8string; FCreatedBy: utf8string; FCreatedDate: TDateTime; FComment: utf8string; FInfoSource: utf8string; FName: utf8string; FPieceLenght: int64; + FMetaVersion: int64; FPrivateTorrent: boolean; + FPaddingPresent_V1: boolean; + FPaddingPresent_V2: boolean; + FInfoFilesVersion: integer; function DecodeTorrent: boolean; overload; + function isPresentInfoFiles_V1: boolean; + function isPresentInfoFileTree_V2: boolean; + + function GetSha256(const Source: utf8string): utf8string; + function GetMetaVersion: int64; function GetAnnounceList: boolean; - function GetFileList: boolean; - function GetInfoHash: utf8string; + function GetFileList_V1: boolean; + function GetFileList_V2: boolean; + function GetOneFileTorrent: boolean; function GetCreatedBy: utf8string; function GetCreatedDate: TDateTime; function GetComment: utf8string; @@ -72,14 +85,18 @@ TDecodeTorrent = class //All the trackers inside this torrent file TrackerList: TStringList; - property FilenameTorrent: UTF8String read FFilenameTorrent; + property MetaVersion: int64 read FMetaVersion; + property TorrentVersion: TTorrentVersion read FTorrentVersion; + property PaddingPresent_V1: boolean read FPaddingPresent_V1; + property PaddingPresent_V2: boolean read FPaddingPresent_V2; + + property FilenameTorrent: utf8string read FFilenameTorrent; - //Every torrent file have one or more files. - // TorrentFilesArray: Array of TDecodeTorrentFile; property TotalFileSize: int64 read FTotalFileSize; //Info hash - property InfoHash: utf8string read FInfoHash; + property InfoHash_V1: utf8string read FInfoHash_V1; + property InfoHash_V2: utf8string read FInfoHash_V2; //Created by property CreatedBy: utf8string read FCreatedBy; @@ -98,24 +115,28 @@ TDecodeTorrent = class //public/private flag property PrivateTorrent: boolean read FPrivateTorrent; - procedure RemovePrivateTorrentFlag; - procedure AddPrivateTorrentFlag; + function RemovePrivateTorrentFlag: boolean; + function AddPrivateTorrentFlag: boolean; //info.source property InfoSource: utf8string read FInfoSource; - procedure InfoSourceRemove; - procedure InfoSourceAdd(const Value: utf8string); + function InfoSourceRemove: boolean; + function InfoSourceAdd(const Value: utf8string): boolean; - //info.files + //info.files and info. file tree + property InfoFilesVersion: integer read FInfoFilesVersion; function InfoFilesCount: integer; function InfoFilesNameIndex(index: integer): utf8string; function InfoFilesLengthIndex(index: integer): int64; //Announce list - procedure RemoveAnnounce; - procedure RemoveAnnounceList; - procedure ChangeAnnounce(const TrackerURL: utf8string); - procedure ChangeAnnounceList(StringList: TStringList); + function RemoveAnnounce: boolean; + function RemoveAnnounceList: boolean; + function ChangeAnnounce(const TrackerURL: utf8string): boolean; + function ChangeAnnounceList(StringList: TStringList): boolean; + + function TorrentVersionToString: utf8string; + function PaddingToString: utf8string; //Load torrent file function DecodeTorrent(const Filename: utf8string): boolean; overload; @@ -131,7 +152,7 @@ TDecodeTorrent = class implementation -uses dateutils, SHA1, FileUtil, LazUTF8; +uses dateutils, SHA1, DCPsha256, FileUtil, LazUTF8; function SortFileName(Item1, Item2: Pointer): integer; begin @@ -149,7 +170,7 @@ function Sort_(Item1, Item2: Pointer): integer; constructor TDecodeTorrent.Create; begin inherited; - //Every torrent have + //Every torrent have file list FObjectListFileNameAndLength := TObjectList.Create; //List for all the trackers. @@ -212,7 +233,7 @@ function TDecodeTorrent.GetAnnounceList: boolean; var TempBEncoded: TBEncoded; i, Count: integer; - TrackerStr: UTF8String; + TrackerStr: utf8string; begin //return false, if crash at decoding. Announce is optional in torrent file. TrackerList.Clear; @@ -254,90 +275,176 @@ function TDecodeTorrent.GetAnnounceList: boolean; end; end; -function TDecodeTorrent.GetFileList: boolean; +function TDecodeTorrent.GetFileList_V1: boolean; var - //TempBEncodedInfo, - TempBEncodedInfoFiles, TempBEncodedInfoFilesPath: TBEncoded; - TempBEncodedInfoFilesData: TBEncodedData; - - i, x, countFiles, countPath: integer; - FilenameWithPathStr, Filename: utf8string; - - DecodeTorrentFileName: TDecodeTorrentFileNameAndLength; + P_File: Pointer; + P_Item: Pointer; + P_Path: Pointer; FileLength: int64; + InfoFiles: TBEncoded; + NodeData: TBEncodedData; + ThisIsFileWithPadding: boolean; + FilenameWithPathStr: utf8string; + DecodeTorrentFileName: TDecodeTorrentFileNameAndLength; begin -{ info/files/path -> all the files names - or - info/name -> one file name only -} - //return false if there is no file at all. This must not be posible. - + // return false if there is no file at all. This must not be posible. + // info/files/path -> all the files names + FInfoFilesVersion := 1; FObjectListFileNameAndLength.Clear; FTotalFileSize := 0; - try + try {find 'info.files' } - TempBEncodedInfoFiles := FBEncoded_Info.ListData.FindElement('files'); + InfoFiles := FBEncoded_Info.ListData.FindElement('files'); - if assigned(TempBEncodedInfoFiles) then + if assigned(InfoFiles) then begin //'info.files' found - countFiles := TempBEncodedInfoFiles.ListData.Count; - if countFiles > 0 then - begin - for i := 0 to countFiles - 1 do - begin - //Get the info.files node. - TempBEncodedInfoFilesData := TempBEncodedInfoFiles.ListData.Items[i]; - - //Get the file name with path - FilenameWithPathStr := ''; - TempBEncodedInfoFilesPath := - TempBEncodedInfoFilesData.Data.ListData.FindElement('path'); - countPath := TempBEncodedInfoFilesPath.ListData.Count; - for x := 0 to countPath - 1 do + + for P_File in InfoFiles.ListData do + begin // Every file need to be process one by one + + ThisIsFileWithPadding := False; + for P_Item in TBEncodedData(P_File).Data.ListData do + begin // Every item inside the file must be process one by one + NodeData := TBEncodedData(P_Item); + + // Get the file name with path + if NodeData.Header = 'path' then + begin + FilenameWithPathStr := ''; + for P_Path in NodeData.Data.ListData do + begin + FilenameWithPathStr := + FilenameWithPathStr + DirectorySeparator + TBEncodedData( + P_Path).Data.StringData; + end; + Continue; + end; + + // Get the file length + if NodeData.Header = 'length' then begin - FilenameWithPathStr := - FilenameWithPathStr + DirectorySeparator + - TempBEncodedInfoFilesPath.ListData.Items[x].Data.StringData; + FileLength := NodeData.Data.IntegerData; + Continue; end; + // check if this is padding + if NodeData.Header = 'attr' then + begin + ThisIsFileWithPadding := UTF8Pos('p', NodeData.Data.StringData) > 0; + if ThisIsFileWithPadding and not FPaddingPresent_V1 then + begin + FPaddingPresent_V1 := True; + end; + end; + end; // Every item inside the file - //Get the file length - FileLength := TempBEncodedInfoFilesData.Data.ListData.FindElement( - 'length').IntegerData; + // one file is decoded. Now add it to the list + DecodeTorrentFileName := TDecodeTorrentFileNameAndLength.Create; + DecodeTorrentFileName.Filename := FilenameWithPathStr; + DecodeTorrentFileName.FileLength := FileLength; + FObjectListFileNameAndLength.Add(DecodeTorrentFileName); - DecodeTorrentFileName := TDecodeTorrentFileNameAndLength.Create; - DecodeTorrentFileName.Filename := FilenameWithPathStr; - DecodeTorrentFileName.FileLength := FileLength; - FObjectListFileNameAndLength.Add(DecodeTorrentFileName); - //add it to the total sum of all files inside the torrent. + //add FileLength to the total sum of all files inside the torrent. + if not ThisIsFileWithPadding then + begin Inc(FTotalFileSize, FileLength); end; + end; // Every file need to be process one by one + end; //'info.files' found + + //There is file found inside the torrent? + Result := FObjectListFileNameAndLength.Count > 0; + + except + //Can not found items that should be present. + Result := False; + end; + +end; + +function TDecodeTorrent.GetFileList_V2: boolean; +var + BEncodedFileTree: TBEncoded; + + procedure ProcessPathOrFile(const Node: TBEncoded; const path: string); + var + P_Item: Pointer; + FileLength: int64; + NodeData: TBEncodedData; + ThisIsFileWithPadding: boolean; + DecodeTorrentFileName: TDecodeTorrentFileNameAndLength; + begin + // Everyting in the tree is befDictionary + ThisIsFileWithPadding := False; + FileLength := -1; // -1 is no file length found yet. + + + for P_Item in node.ListData do + begin // read all the befDictionary list items one by one + NodeData := TBEncodedData(P_Item); + + // Found a new path node + if NodeData.Data.Format = befDictionary then + begin + ProcessPathOrFile(NodeData.Data, path + DirectorySeparator + NodeData.Header); + Continue; end; - end - else //there is no 'info.files' found. This is an 'one file' torrent. - begin// Look for'info.name' and 'info.length' - //Get the file name - Filename := FBEncoded_Info.ListData.FindElement('name').StringData; - FileLength := FBEncoded_Info.ListData.FindElement('length').IntegerData; + // check if this present dictionary is a padding node + if (NodeData.Data.Format = befString) and (NodeData.Header = 'attr') then + begin + ThisIsFileWithPadding := (UTF8Pos('p', NodeData.Data.StringData) > 0); + if ThisIsFileWithPadding and not FPaddingPresent_V2 then + begin + FPaddingPresent_V2 := True; + end; + Continue; + end; + // Found a file node? This is at the end of the tree node. + if (NodeData.Data.Format = befInteger) and (NodeData.Header = 'length') then + begin + FileLength := NodeData.Data.IntegerData; + end; + + end; // read all the befDictionary list items one by one + + // One dictionary list have been process + // Is this a dictionary list with a file inside? + if FileLength >= 0 then + begin DecodeTorrentFileName := TDecodeTorrentFileNameAndLength.Create; - DecodeTorrentFileName.Filename := Filename; + DecodeTorrentFileName.Filename := ExtractFileDir(path); DecodeTorrentFileName.FileLength := FileLength; FObjectListFileNameAndLength.Add(DecodeTorrentFileName); - - Inc(FTotalFileSize, FileLength); + //add FileLength to the total sum of all files inside the torrent. + if not ThisIsFileWithPadding then + begin + Inc(FTotalFileSize, DecodeTorrentFileName.FileLength); + end; end; + end; +begin + Result := False; + FInfoFilesVersion := 2; + FObjectListFileNameAndLength.Clear; + FTotalFileSize := 0; + try + {find 'info.file tree' } + BEncodedFileTree := FBEncoded_Info.ListData.FindElement('file tree'); - //There is file found inside the torrent. + if assigned(BEncodedFileTree) then + begin //'info.file tree' found + ProcessPathOrFile(BEncodedFileTree, ''); + end; + + //There is file found inside the torrent? Result := FObjectListFileNameAndLength.Count > 0; - //We prefer that the file name are in sorted order. - FObjectListFileNameAndLength.Sort(@SortFileName); except //Can not found items that should be present. @@ -346,6 +453,32 @@ function TDecodeTorrent.GetFileList: boolean; end; +function TDecodeTorrent.GetOneFileTorrent: boolean; +var + Filename: utf8string; + DecodeTorrentFileName: TDecodeTorrentFileNameAndLength; + TempBEncoded: TBEncoded; +begin + try + // This is a torrent without file list/tree + TempBEncoded := FBEncoded_Info.ListData.FindElement('length'); + Result := assigned(TempBEncoded); + if Result then + begin + FInfoFilesVersion := 1; + FObjectListFileNameAndLength.Clear; + Filename := FBEncoded_Info.ListData.FindElement('name').StringData; + DecodeTorrentFileName := TDecodeTorrentFileNameAndLength.Create; + DecodeTorrentFileName.Filename := Filename; + DecodeTorrentFileName.FileLength := TempBEncoded.IntegerData; + FObjectListFileNameAndLength.Add(DecodeTorrentFileName); + FTotalFileSize := TempBEncoded.IntegerData; + end; + except + Result := False; + end; +end; + function TDecodeTorrent.DecodeTorrent: boolean; begin Result := False; @@ -356,20 +489,19 @@ function TDecodeTorrent.DecodeTorrent: boolean; FreeAndNil(FBEncoded); end; - //the torrent file inside FMemoryStream -> BEnencode it - FMemoryStream.Position := 0; - FBEncoded := TBEncoded.Create(FMemoryStream); - - - - - //Read the tracker list and file list inside the torrent file. - FTotalFileSize := 0; + // Clear List that will be filed later. TrackerList.Clear; - FObjectListFileNameAndLength.Clear; + + // Reset to default value FTotalFileSize := 0; + FTorrentVersion := tv_unknown; + FPaddingPresent_V1 := False; + FPaddingPresent_V2 := False; + //the torrent file inside FMemoryStream -> BEnencode it + FMemoryStream.Position := 0; + FBEncoded := TBEncoded.Create(FMemoryStream); //torrent file MUST begin with befDictionary. if FBEncoded.Format <> befDictionary then @@ -380,14 +512,39 @@ function TDecodeTorrent.DecodeTorrent: boolean; if not assigned(FBEncoded_Info) then exit; //error + // Is this V1,V2 or hybrid torrent type? + if isPresentInfoFiles_V1 then FTorrentVersion := tv_V1; + if isPresentInfoFileTree_V2 then + begin + if FTorrentVersion = tv_V1 then + FTorrentVersion := tv_Hybrid + else + FTorrentVersion := tv_V2; + end; //Accept torrent only when there is no issue in reading AnnounceList and file list - if GetAnnounceList and GetFileList then + if GetAnnounceList then begin - Result := True; + case FTorrentVersion of + tv_V1: Result := GetFileList_V1; + tv_V2: Result := GetFileList_V2; + tv_Hybrid: + begin // Only V2 is actualy used. V1 need to be read to look for padding. + Result := GetFileList_V1; + if Result then GetFileList_V2; + end; + else + Assert(False, 'Missing torrent version'); + end; + end; + + if not Result then + begin // There is nothing found in tree list. Maybe this is a Torrent with one file? + Result := GetOneFileTorrent; + if Result then FTorrentVersion := tv_V1; end; - FInfoHash := GetInfoHash; + // FInfoHash_V1 := GetInfoHash; FCreatedBy := GetCreatedBy; FCreatedDate := GetCreatedDate; FComment := GetComment; @@ -395,7 +552,91 @@ function TDecodeTorrent.DecodeTorrent: boolean; FPieceLenght := GetPieceLenght; FPrivateTorrent := GetPrivateTorrent; FInfoSource := GetInfoSource; + FMetaVersion := GetMetaVersion; + except + Result := False; + end; +end; + +function TDecodeTorrent.isPresentInfoFiles_V1: boolean; +var + Info: TBEncoded; + str: utf8string; +begin + try + {find 'info.files' } + Info := FBEncoded_Info.ListData.FindElement('files'); + Result := assigned(info); + if Result then + begin + str := ''; + TBEncoded.Encode(FBEncoded_Info, str); + FInfoHash_V1 := UpperCase(SHA1Print(SHA1String(str))); + end + else + begin + FInfoHash_V1 := 'N/A'; + end; + except + Result := False; + end; +end; + +function TDecodeTorrent.isPresentInfoFileTree_V2: boolean; +var + Info: TBEncoded; + str: utf8string; +begin + try + {find 'info.file tree' } + Info := FBEncoded_Info.ListData.FindElement('file tree'); + Result := assigned(info); + if Result then + begin + str := ''; + TBEncoded.Encode(FBEncoded_Info, str); + FInfoHash_V2 := UpperCase(GetSha256(str)); + end + else + begin + FInfoHash_V2 := 'N/A'; + end; except + Result := False; + end; +end; + +function TDecodeTorrent.GetSha256(const Source: utf8string): utf8string; +var + Hash: TDCP_sha256; + Digest: array[0..31] of byte; + i: integer; +begin + Digest[0] := 0; // suppres compiler warning. + Hash := TDCP_sha256.Create(nil); + Hash.Init; + Hash.UpdateStr(Source); + Hash.Final(Digest); + Result := ''; + for i := Low(Digest) to High(Digest) do + begin + Result := Result + IntToHex(Digest[i], 2); + end; + Hash.Free; +end; + +function TDecodeTorrent.GetMetaVersion: int64; +var + TempBEncoded: TBEncoded; +begin + Result := 0; + try + {find 'meta version' } + TempBEncoded := FBEncoded_Info.ListData.FindElement('meta version'); + if assigned(TempBEncoded) then + Result := TempBEncoded.IntegerData; + except + Result := 0; end; end; @@ -415,7 +656,6 @@ function TDecodeTorrent.InfoFilesLengthIndex(index: integer): int64; Result := TDecodeTorrentFileNameAndLength(FObjectListFileNameAndLength[index]).FileLength; end; - function TDecodeTorrent.GetPrivateTorrent: boolean; var TempBEncoded: TBEncoded; @@ -427,6 +667,7 @@ function TDecodeTorrent.GetPrivateTorrent: boolean; if assigned(TempBEncoded) then Result := TempBEncoded.IntegerData = 1; except + Result := False; end; end; @@ -441,6 +682,7 @@ function TDecodeTorrent.GetInfoSource: utf8string; if assigned(TempBEncoded) then Result := TempBEncoded.StringData; except + Result := ''; end; end; @@ -475,6 +717,7 @@ procedure TDecodeTorrent.SetComment(const AValue: utf8string); end; except + FComment := AValue; end; end; @@ -493,17 +736,19 @@ procedure TDecodeTorrent.SetComment(const AValue: utf8string); } -procedure TDecodeTorrent.RemovePrivateTorrentFlag; +function TDecodeTorrent.RemovePrivateTorrentFlag: boolean; begin try FBEncoded_Info.ListData.RemoveElement('private'); + Result := True; except + Result := False; end; //read databack again FPrivateTorrent := GetPrivateTorrent; end; -procedure TDecodeTorrent.AddPrivateTorrentFlag; +function TDecodeTorrent.AddPrivateTorrentFlag: boolean; var Encoded: TBEncoded; Data: TBEncodedData; @@ -517,23 +762,27 @@ procedure TDecodeTorrent.AddPrivateTorrentFlag; Data.Header := 'private'; FBEncoded_Info.ListData.Add(Data); FBEncoded_Info.ListData.Sort(@sort_);//text must be in alfabetical order. + Result := True; except + Result := False; end; //read databack again FPrivateTorrent := GetPrivateTorrent; end; -procedure TDecodeTorrent.InfoSourceRemove; +function TDecodeTorrent.InfoSourceRemove: boolean; begin try FBEncoded_Info.ListData.RemoveElement('source'); + Result := True; except + Result := False; end; //read databack again FInfoSource := GetInfoSource; end; -procedure TDecodeTorrent.InfoSourceAdd(const Value: utf8string); +function TDecodeTorrent.InfoSourceAdd(const Value: utf8string): boolean; var Encoded: TBEncoded; Data: TBEncodedData; @@ -547,24 +796,30 @@ procedure TDecodeTorrent.InfoSourceAdd(const Value: utf8string); Data.Header := 'source'; FBEncoded_Info.ListData.Add(Data); FBEncoded_Info.ListData.Sort(@sort_);//text must be in alfabetical order. + Result := True; except + Result := False; end; FInfoSource := GetInfoSource; end; -procedure TDecodeTorrent.RemoveAnnounce; +function TDecodeTorrent.RemoveAnnounce: boolean; begin try FBEncoded.ListData.RemoveElement('announce'); + Result := True; except + Result := False; end; end; -procedure TDecodeTorrent.RemoveAnnounceList; +function TDecodeTorrent.RemoveAnnounceList: boolean; begin try FBEncoded.ListData.RemoveElement('announce-list'); + Result := True; except + Result := False; end; end; @@ -591,7 +846,7 @@ function TDecodeTorrent.SaveTorrent(const Filename: utf8string): boolean; -procedure TDecodeTorrent.ChangeAnnounce(const TrackerURL: utf8string); +function TDecodeTorrent.ChangeAnnounce(const TrackerURL: utf8string): boolean; var Encoded: TBEncoded; Data: TBEncodedData; @@ -605,19 +860,21 @@ procedure TDecodeTorrent.ChangeAnnounce(const TrackerURL: utf8string); Data.Header := 'announce'; FBEncoded.ListData.Add(Data); FBEncoded.ListData.Sort(@sort_);//text must be in alfabetical order. + Result := True; except + Result := False; end; end; -procedure TDecodeTorrent.ChangeAnnounceList(StringList: TStringList); +function TDecodeTorrent.ChangeAnnounceList(StringList: TStringList): boolean; var EncodedListRoot, EncodedList, EncodedString: TBEncoded; DataRootBEncodedData: TBEncodedData; i: integer; begin + Result := True; //remove the present one. RemoveAnnounceList; - //if there is nothing in the list then exit. if StringList.Count = 0 then Exit; @@ -652,17 +909,40 @@ procedure TDecodeTorrent.ChangeAnnounceList(StringList: TStringList); FBEncoded.ListData.Sort(@sort_);//text must be in alfabetical order. except + Result := False; end; end; -function TDecodeTorrent.GetInfoHash: utf8string; +function TDecodeTorrent.TorrentVersionToString: utf8string; begin - Result := ''; - try - //The info.value will be hash with SHA1 - TBEncoded.Encode(FBEncoded_Info, Result); - Result := UpperCase(SHA1Print(SHA1String(Result))); - except + case FTorrentVersion of + tv_V1: Result := 'V1'; + tv_V2: Result := 'V2'; + tv_Hybrid: Result := 'Hybrid (V1&V2)'; + tv_unknown: Result := 'unknown'; + else + Result := 'TorrentVersionToString: unkown value'; + end; +end; + +function TDecodeTorrent.PaddingToString: utf8string; +begin + case FTorrentVersion of + tv_V1: + begin + Result := BoolToStr(FPaddingPresent_V1, 'Yes', 'No'); + end; + tv_V2: + begin + Result := BoolToStr(FPaddingPresent_V2, 'Yes', 'No'); + end; + tv_Hybrid: + begin // Show only V2 hash. No space for both V1 and V2 + Result := 'V1:' + BoolToStr(FPaddingPresent_V1, 'Yes', 'No') + + ' V2:' + BoolToStr(FPaddingPresent_V2, 'Yes', 'No'); + end; + else + Result := 'N/A' end; end; @@ -675,8 +955,8 @@ function TDecodeTorrent.GetCreatedBy: utf8string; TempBEncoded := FBEncoded.ListData.FindElement('created by'); if assigned(TempBEncoded) then Result := TempBEncoded.StringData; - except + Result := ''; end; end; @@ -690,6 +970,7 @@ function TDecodeTorrent.GetCreatedDate: TDateTime; if assigned(TempBEncoded) then Result := UnixToDateTime(TempBEncoded.IntegerData); except + Result := 0; end; end; @@ -701,6 +982,7 @@ function TDecodeTorrent.GetComment: utf8string; if assigned(FBEncoded_Comment) then Result := UTF8Trim(FBEncoded_Comment.StringData); except + Result := ''; end; end; @@ -715,6 +997,7 @@ function TDecodeTorrent.GetName: utf8string; if assigned(TempBEncoded) then Result := TempBEncoded.StringData; except + Result := ''; end; end; @@ -729,6 +1012,7 @@ function TDecodeTorrent.GetPieceLenght: int64; if assigned(TempBEncoded) then Result := TempBEncoded.IntegerData; except + Result := 0; end; end; diff --git a/source/project/tracker_editor/trackereditor.lpi b/source/project/tracker_editor/trackereditor.lpi index 011ff9f..a91b00d 100644 --- a/source/project/tracker_editor/trackereditor.lpi +++ b/source/project/tracker_editor/trackereditor.lpi @@ -28,7 +28,7 @@ - + @@ -69,7 +69,7 @@ - + - + @@ -160,6 +160,21 @@ end;"/> + + + + + + + + + + + + + + + @@ -170,7 +185,7 @@ end;"/> - + diff --git a/source/project/tracker_editor/trackereditor.lpr b/source/project/tracker_editor/trackereditor.lpr index 808047a..4b3a3e1 100644 --- a/source/project/tracker_editor/trackereditor.lpr +++ b/source/project/tracker_editor/trackereditor.lpr @@ -7,9 +7,9 @@ cthreads, {$ENDIF}{$ENDIF} Interfaces, // this includes the LCL widgetset - Forms, main, bencode, decodetorrent, controlergridtorrentdata, -controler_trackerlist_online, trackerlist_online, -controler_treeview_torrent_data; + Forms, DCPsha256, DCPconst, DCPcrypt2, main, bencode, decodetorrent, + controlergridtorrentdata, controler_trackerlist_online, trackerlist_online, + controler_treeview_torrent_data; {$R *.res} From 65b79c6dc9eb9b67203af2be97e9bb53cccb5c38 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Wed, 4 Dec 2024 21:18:51 +0100 Subject: [PATCH 16/46] Update the tree view for additional information Add values: - Info Hash V1: - Info Hash V2 - Meta Version: - Padding (beb 47): --- .../code/controler_treeview_torrent_data.pas | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/source/code/controler_treeview_torrent_data.pas b/source/code/controler_treeview_torrent_data.pas index 99ab014..93fa146 100644 --- a/source/code/controler_treeview_torrent_data.pas +++ b/source/code/controler_treeview_torrent_data.pas @@ -70,7 +70,7 @@ implementation TORRENT_FILES_CONTENTS_FORM_CAPTION = 'Show all the files inside the torrents. (Use right mouse for popup menu.)'; -{ Tcontroler_treeview_torrent_data } + { Tcontroler_treeview_torrent_data } procedure Tcontroler_treeview_torrent_data.FillThePopupMenu; begin @@ -285,16 +285,16 @@ procedure Tcontroler_treeview_torrent_data.AddOneTorrentFileDecoded( DecodeTorrent: TDecodeTorrent); var CountFiles: integer; - TorrentFileNameStr, TrackerStr: UTF8String; + TorrentFileNameStr, TrackerStr: utf8string; TreeNodeTorrent, TreeNodeFiles, TreeNodeTrackers, TreeNodeInfo: TTreeNode; - begin //--------------------- Fill the treeview with torrent files TorrentFileNameStr := ExtractFileName(DecodeTorrent.FilenameTorrent); //Add the torrent file name + size of all the files combined. - TorrentFileNameStr := TorrentFileNameStr + ' SIZE: ' + + TorrentFileNameStr := TorrentFileNameStr + ' (Version: ' + + DecodeTorrent.TorrentVersionToString + ') SIZE: ' + ByteSizeToBiggerSizeFormatStr(DecodeTorrent.TotalFileSize) + ' Files: ' + IntToStr(DecodeTorrent.InfoFilesCount) + '' + ' Tracker: ' + IntToStr(DecodeTorrent.TrackerList.Count) + ''; @@ -305,7 +305,9 @@ procedure Tcontroler_treeview_torrent_data.AddOneTorrentFileDecoded( TorrentFileNameStr); //Without directory path //must be in this order (Files, Trackers, Info) - TreeNodeFiles := FTreeViewFileContents.Items.AddChild(TreeNodeTorrent, 'Files'); + TreeNodeFiles := FTreeViewFileContents.Items.AddChild(TreeNodeTorrent, + 'Files V' + IntToStr(DecodeTorrent.InfoFilesVersion)); + TreeNodeTrackers := FTreeViewFileContents.Items.AddChild(TreeNodeTorrent, 'Trackers'); TreeNodeInfo := FTreeViewFileContents.Items.AddChild(TreeNodeTorrent, 'Info'); @@ -340,8 +342,10 @@ procedure Tcontroler_treeview_torrent_data.AddOneTorrentFileDecoded( FTreeViewFileContents.Items.AddChild(TreeNodeInfo, 'Comment: ' + DecodeTorrent.Comment); - FTreeViewFileContents.Items.AddChild(TreeNodeInfo, 'Info Hash: ' + - DecodeTorrent.InfoHash); + FTreeViewFileContents.Items.AddChild(TreeNodeInfo, 'Info Hash V1: ' + + DecodeTorrent.InfoHash_V1); + FTreeViewFileContents.Items.AddChild(TreeNodeInfo, 'Info Hash V2: ' + + DecodeTorrent.InfoHash_V2); FTreeViewFileContents.Items.AddChild(TreeNodeInfo, 'Created On: ' + DateTimeToStr(DecodeTorrent.CreatedDate)); FTreeViewFileContents.Items.AddChild(TreeNodeInfo, 'Created By: ' + @@ -363,6 +367,15 @@ procedure Tcontroler_treeview_torrent_data.AddOneTorrentFileDecoded( DecodeTorrent.InfoSource); end; + if DecodeTorrent.MetaVersion > 0 then + begin // 'meta version'is in torrent file present + FTreeViewFileContents.Items.AddChild(TreeNodeInfo, 'Meta Version: ' + + IntToStr(DecodeTorrent.MetaVersion)); + end; + + FTreeViewFileContents.Items.AddChild(TreeNodeInfo, 'Padding (beb 47): ' + + DecodeTorrent.PaddingToString); + //All the files count inside the torrent must be added to FTotalFileInsideTorrent Inc(FTotalFileInsideTorrent, DecodeTorrent.InfoFilesCount); From 2f137bc95009c3a2ca463509d993f0a4f12f4afb Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Wed, 4 Dec 2024 21:29:30 +0100 Subject: [PATCH 17/46] add grid column with two extra values - Torrent version: Show if this torrent is V1, V2 or hybrid - Padding (beb 47) Shows if this torrent has padding beb 47 or not --- source/code/controlergridtorrentdata.pas | 62 ++++++++++++---------- source/code/main.lfm | 66 ++++++++++++++---------- source/code/main.pas | 20 ++++++- 3 files changed, 94 insertions(+), 54 deletions(-) diff --git a/source/code/controlergridtorrentdata.pas b/source/code/controlergridtorrentdata.pas index 1f81c41..a885460 100644 --- a/source/code/controlergridtorrentdata.pas +++ b/source/code/controlergridtorrentdata.pas @@ -28,15 +28,17 @@ TControlerGridTorrentData = class //The collumn must be in this design order. FTorrentFile, //0 FInfoFileName, //1 - FInfoHash, //2 - FCreatedOn, //3 - FCreatedBy, //4 - FComment, //5 - FPrivateTorrent, //6 - FInfoSource, //7 - FPieceLength, //8 - FTotaSize, //9 - FIndexOrder //10 + FTorrentVersion, //2 + FPadding, //3 + FInfoHash, //4 + FCreatedOn, //5 + FCreatedBy, //6 + FComment, //7 + FPrivateTorrent, //8 + FInfoSource, //9 + FPieceLength, //10 + FTotaSize, //11 + FIndexOrder //12 : TGridColumn; FRowIsMovedNeedUpdate: boolean; @@ -49,15 +51,17 @@ TControlerGridTorrentData = class //All the string that can be written to grid. TorrentFile, //0 InfoFileName, //1 - InfoHash, //2 - CreatedOn, //3 - CreatedBy, //4 - Comment, //5 - PrivateTorrent, //6 - InfoSource, //7 - PieceLength, //8 - TotaSize, //9 - IndexOrder //10 + TorrentVersion, //2 + Padding, //3 + InfoHash, //4 + CreatedOn, //5 + CreatedBy, //6 + Comment, //7 + PrivateTorrent, //8 + InfoSource, //9 + PieceLength, //10 + TotaSize, //11 + IndexOrder //12 : UTF8String; procedure ClearAllImageIndex; @@ -135,6 +139,8 @@ procedure TControlerGridTorrentData.AppendRow; //write all the string to the cell. WriteCell(FTorrentFile, TorrentFile); WriteCell(FInfoFileName, InfoFileName); + WriteCell(FTorrentVersion, TorrentVersion); + WriteCell(FPadding, Padding); WriteCell(FInfoHash, InfoHash); WriteCell(FCreatedOn, CreatedOn); WriteCell(FCreatedBy, CreatedBy); @@ -183,15 +189,17 @@ constructor TControlerGridTorrentData.Create(StringGridTorrentData: TStringGrid) //Track the column AddColumn(FTorrentFile, 0); AddColumn(FInfoFileName, 1); - AddColumn(FInfoHash, 2); - AddColumn(FCreatedOn, 3); - AddColumn(FCreatedBy, 4); - AddColumn(FComment, 5); - AddColumn(FPrivateTorrent, 6); - AddColumn(FInfoSource, 7); - AddColumn(FPieceLength, 8); - AddColumn(FTotaSize, 9); - AddColumn(FIndexOrder, 10); + AddColumn(FTorrentVersion, 2); + AddColumn(FPadding, 3); + AddColumn(FInfoHash, 4); + AddColumn(FCreatedOn, 5); + AddColumn(FCreatedBy, 6); + AddColumn(FComment, 7); + AddColumn(FPrivateTorrent, 8); + AddColumn(FInfoSource, 9); + AddColumn(FPieceLength, 10); + AddColumn(FTotaSize, 11); + AddColumn(FIndexOrder, 12); //Fillin the tag value UpdateColumnTag; diff --git a/source/code/main.lfm b/source/code/main.lfm index a7a2be5..21765ea 100644 --- a/source/code/main.lfm +++ b/source/code/main.lfm @@ -5,7 +5,7 @@ object FormTrackerModify: TFormTrackerModify Width = 1179 AllowDropFiles = True Caption = 'Bittorrent Tracker Editor' - ClientHeight = 587 + ClientHeight = 607 ClientWidth = 1179 Constraints.MinHeight = 500 Constraints.MinWidth = 700 @@ -16,10 +16,10 @@ object FormTrackerModify: TFormTrackerModify OnDropFiles = FormDropFiles OnShow = FormShow Position = poScreenCenter - LCLVersion = '2.2.6.0' + LCLVersion = '3.6.0.0' object PageControl: TPageControl Left = 0 - Height = 587 + Height = 607 Top = 0 Width = 1179 ActivePage = TabSheetTrackersList @@ -28,15 +28,15 @@ object FormTrackerModify: TFormTrackerModify TabOrder = 0 object TabSheetTrackersList: TTabSheet Caption = 'Trackers List' - ClientHeight = 561 + ClientHeight = 581 ClientWidth = 1171 object PanelTop: TPanel Left = 0 - Height = 561 + Height = 581 Top = 0 Width = 1171 Align = alClient - ClientHeight = 561 + ClientHeight = 581 ClientWidth = 1171 TabOrder = 0 object GroupBoxNewTracker: TGroupBox @@ -62,19 +62,19 @@ object FormTrackerModify: TFormTrackerModify end object GroupBoxPresentTracker: TGroupBox Left = 1 - Height = 353 + Height = 373 Top = 207 Width = 1169 Align = alClient Caption = 'Present trackers in all torrent files. Select the one that you want to keep.' - ClientHeight = 335 + ClientHeight = 355 ClientWidth = 1165 Constraints.MinHeight = 100 ParentBidiMode = False TabOrder = 1 object StringGridTrackerOnline: TStringGrid Left = 0 - Height = 335 + Height = 355 Top = 0 Width = 1165 Align = alClient @@ -100,30 +100,30 @@ object FormTrackerModify: TFormTrackerModify end object TabSheetPublicPrivateTorrent: TTabSheet Caption = 'Public/Private' - ClientHeight = 561 + ClientHeight = 581 ClientWidth = 1171 object PanelTopPublicTorrent: TPanel Left = 0 - Height = 561 + Height = 581 Top = 0 Width = 1171 Align = alClient - ClientHeight = 561 + ClientHeight = 581 ClientWidth = 1171 TabOrder = 0 object GroupBoxPublicPrivateTorrent: TGroupBox Left = 1 - Height = 559 + Height = 579 Top = 1 Width = 1169 Align = alClient Caption = 'Checked items are public torrent. WARNING: change public/private setting will change torrent Hash info.' - ClientHeight = 541 + ClientHeight = 561 ClientWidth = 1165 TabOrder = 0 object CheckListBoxPublicPrivateTorrent: TCheckListBox Left = 0 - Height = 541 + Height = 561 Top = 0 Width = 1165 Align = alClient @@ -135,15 +135,15 @@ object FormTrackerModify: TFormTrackerModify end object TabSheetTorrentData: TTabSheet Caption = 'Data/Info' - ClientHeight = 561 + ClientHeight = 581 ClientWidth = 1171 object StringGridTorrentData: TStringGrid Left = 0 - Height = 561 + Height = 581 Top = 0 Width = 1171 Align = alClient - ColCount = 11 + ColCount = 13 ColumnClickSorts = True Columns = < item @@ -156,11 +156,21 @@ object FormTrackerModify: TFormTrackerModify Title.Caption = 'Info Filename' Width = 250 end + item + ReadOnly = True + Title.Caption = 'Torrent Version' + Width = 100 + end + item + ReadOnly = True + Title.Caption = 'Padding (beb 47)' + Width = 100 + end item MaxSize = 250 ReadOnly = True Title.Caption = 'Info Hash' - Width = 280 + Width = 439 end item ReadOnly = True @@ -201,8 +211,10 @@ object FormTrackerModify: TFormTrackerModify end item ReadOnly = True + SizePriority = 0 Title.Alignment = taRightJustify Title.Caption = 'IndexOrder (internal used)' + Width = 0 Visible = False end> FixedCols = 0 @@ -213,7 +225,9 @@ object FormTrackerModify: TFormTrackerModify ColWidths = ( 250 250 - 280 + 100 + 100 + 439 145 145 160 @@ -230,7 +244,7 @@ object FormTrackerModify: TFormTrackerModify end object TabSheetPrivateTrackers: TTabSheet Caption = 'Private Trackers' - ClientHeight = 561 + ClientHeight = 581 ClientWidth = 1171 object GroupBoxItemsForPrivateTrackers: TGroupBox Left = 0 @@ -244,15 +258,15 @@ object FormTrackerModify: TFormTrackerModify TabOrder = 0 object CheckBoxSkipAnnounceCheck: TCheckBox Left = 0 - Height = 19 + Height = 17 Top = 0 Width = 284 Align = alTop Caption = 'Skip Announce Check in the URL (-SAC)' - OnChange = CheckBoxSkipAnnounceCheckChange ParentShowHint = False ShowHint = True TabOrder = 0 + OnChange = CheckBoxSkipAnnounceCheckChange end object GroupBoxInfoSource: TGroupBox Left = 0 @@ -266,12 +280,12 @@ object FormTrackerModify: TFormTrackerModify TabOrder = 1 object CheckBoxRemoveAllSourceTag: TCheckBox Left = 8 - Height = 19 + Height = 17 Top = 8 - Width = 132 + Width = 130 Caption = 'Remove all source tag' - OnChange = CheckBoxRemoveAllSourceTagChange TabOrder = 0 + OnChange = CheckBoxRemoveAllSourceTagChange end object LabeledEditInfoSource: TLabeledEdit Left = 8 diff --git a/source/code/main.pas b/source/code/main.pas index 32e6096..4dcd6cd 100644 --- a/source/code/main.pas +++ b/source/code/main.pas @@ -1748,7 +1748,25 @@ procedure TFormTrackerModify.ViewUpdateOneTorrentFileDecoded; //Copy all the torrent info to the grid column. FControlerGridTorrentData.TorrentFile := TorrentFileNameStr; FControlerGridTorrentData.InfoFileName := FDecodePresentTorrent.Name; - FControlerGridTorrentData.InfoHash := FDecodePresentTorrent.InfoHash; + FControlerGridTorrentData.TorrentVersion := + FDecodePresentTorrent.TorrentVersionToString; + case FDecodePresentTorrent.TorrentVersion of + tv_V1: + begin + FControlerGridTorrentData.InfoHash := 'V1: ' + FDecodePresentTorrent.InfoHash_V1; + end; + tv_V2: + begin + FControlerGridTorrentData.InfoHash := 'V2: ' + FDecodePresentTorrent.InfoHash_V2; + end; + tv_Hybrid: + begin // Show only V2 hash. No space for both V1 and V2 + FControlerGridTorrentData.InfoHash := 'V2: ' + FDecodePresentTorrent.InfoHash_V2; + end; + else + FControlerGridTorrentData.InfoHash := 'N/A' + end; + FControlerGridTorrentData.Padding := FDecodePresentTorrent.PaddingToString; FControlerGridTorrentData.CreatedOn := DateTimeStr; FControlerGridTorrentData.CreatedBy := FDecodePresentTorrent.CreatedBy; FControlerGridTorrentData.Comment := FDecodePresentTorrent.Comment; From 403d76d9bcb50d1daf4faeb85dd6c82a8d8caafc Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Wed, 4 Dec 2024 21:33:41 +0100 Subject: [PATCH 18/46] fix some compiler warning example that variable Result is not defined or nothing inside except..end --- source/code/main.pas | 1 + source/code/ngosang_trackerslist.pas | 1 + source/code/torrent_miscellaneous.pas | 9 ++++++--- source/code/trackerlist_online.pas | 5 ++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/source/code/main.pas b/source/code/main.pas index 4dcd6cd..860a5bb 100644 --- a/source/code/main.pas +++ b/source/code/main.pas @@ -1528,6 +1528,7 @@ procedure TFormTrackerModify.FormDropFiles(Sender: TObject; MemoNewTrackers.Append(UTF8Trim(TrackerFileNameStringList.Text)); except //supress any error in loading the file + FileNameOrDirStr := FileNameOrDirStr; end; end; diff --git a/source/code/ngosang_trackerslist.pas b/source/code/ngosang_trackerslist.pas index e900ba3..2bfebec 100644 --- a/source/code/ngosang_trackerslist.pas +++ b/source/code/ngosang_trackerslist.pas @@ -95,6 +95,7 @@ function TngosangTrackerList.DownloadTracker(ngosang_List: Tngosang_List): TStri except //No OpenSSL or web server is down + FTRackerList[ngosang_List].Clear; end; Result := FTrackerList[ngosang_List]; diff --git a/source/code/torrent_miscellaneous.pas b/source/code/torrent_miscellaneous.pas index a2abc47..525dbe1 100644 --- a/source/code/torrent_miscellaneous.pas +++ b/source/code/torrent_miscellaneous.pas @@ -246,8 +246,11 @@ function ByteSizeToBiggerSizeFormatStr(ByteSize: int64): string; Result := Format('%0.2f MiB', [ByteSize / (1024 * 1024)]) else if ByteSize >= (1024) then - Result := Format('%0.2f KiB', [ByteSize / 1024]); - Result := Result + Format(' (%d Bytes)', [ByteSize]); + Result := Format('%0.2f KiB', [ByteSize / 1024]) + else + Result := ''; + + Result := Result + Format(' (%d Bytes)', [ByteSize]); end; @@ -629,11 +632,11 @@ function ConsoleModeDecodeParameter(out FileNameOrDirStr: UTF8String; -U3 "path_to_folder" -SAC -SOURCE "ABC" } + Result := False; case Paramcount of 0: begin TrackerList.LogStringList.Add('ERROR: There are no parameter detected.'); - Result := False; exit; end; 1: diff --git a/source/code/trackerlist_online.pas b/source/code/trackerlist_online.pas index 5eb00ac..65c09d7 100644 --- a/source/code/trackerlist_online.pas +++ b/source/code/trackerlist_online.pas @@ -90,7 +90,10 @@ function TTrackerListOnline.TrackerListOnlineStatusToString( tos_dead: Result := 'Dead'; tos_unknown: Result := 'Unknown'; else - assert(True, 'Unknown TTrackerListOnlineStatus') + begin + Result := ''; + assert(True, 'Unknown TTrackerListOnlineStatus') + end; end; end; From aac8786b75bb8a75238311f751ad2db1307b6219 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Thu, 5 Dec 2024 00:41:38 +0100 Subject: [PATCH 19/46] Change submodule URL to https --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 8688a44..adcb0d7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "submodule/dcpcrypt"] path = submodule/dcpcrypt - url = git://lazarus-ccr.git.sourceforge.net/gitroot/lazarus-ccr/dcpcrypt + url = https://git.code.sf.net/p/lazarus-ccr/dcpcrypt From 11f60c4f3e255ee9a72020f6448f38e7d17266ff Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Thu, 5 Dec 2024 22:52:33 +0100 Subject: [PATCH 20/46] Fix: Grid view 'source' is read only In the grid data/info tap only the comment row can be edited. All other rows are read only --- source/code/main.lfm | 1 + 1 file changed, 1 insertion(+) diff --git a/source/code/main.lfm b/source/code/main.lfm index 21765ea..3837612 100644 --- a/source/code/main.lfm +++ b/source/code/main.lfm @@ -192,6 +192,7 @@ object FormTrackerModify: TFormTrackerModify Width = 50 end item + ReadOnly = True Title.Caption = 'Source' Width = 64 end From f461072e04af5fcd0da70cfc7801a077fa08c61d Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Thu, 5 Dec 2024 22:59:40 +0100 Subject: [PATCH 21/46] Add two more test torrent file Download from: https://libtorrent.org/bittorrent-v2-hybrid-test.torrent https://libtorrent.org/bittorrent-v2-test.torrent See issue #51 --- source/test/test_start_up_parameter.pas | 4 ++-- test_torrent/bittorrent-v2-hybrid-test.torrent | Bin 0 -> 91581 bytes test_torrent/bittorrent-v2-test.torrent | Bin 0 -> 13592 bytes 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 test_torrent/bittorrent-v2-hybrid-test.torrent create mode 100644 test_torrent/bittorrent-v2-test.torrent diff --git a/source/test/test_start_up_parameter.pas b/source/test/test_start_up_parameter.pas index 60f2807..5285a7e 100644 --- a/source/test/test_start_up_parameter.pas +++ b/source/test/test_start_up_parameter.pas @@ -104,8 +104,8 @@ implementation TORRENT_FOLDER = 'test_torrent'; END_USER_FOLDER = 'enduser'; - //there are 3 test torrent files in 'test_torrent' folder. - TEST_TORRENT_FILES_COUNT = 3; + //there are 5 test torrent files in 'test_torrent' folder. + TEST_TORRENT_FILES_COUNT = 5; procedure TTestStartUpParameter.Test_Paramater_U0; begin diff --git a/test_torrent/bittorrent-v2-hybrid-test.torrent b/test_torrent/bittorrent-v2-hybrid-test.torrent new file mode 100644 index 0000000000000000000000000000000000000000..9a5c8762d6cfd0c2ebf61bace1498871aab7a648 GIT binary patch literal 91581 zcmbTdW0Yjw(k)!oWp!0`*|u%lwvlDqUAAr8wrv|-w(YLl=RMDPb)NBk zOdRyA0A@~e8xvb&4o(wuD*(Qe0{~#m%*ZKh;9%)sYio^9uH*!;vNCX>#%JK*V5Y_w zur@a}z!woxqQDndptZJRHm2t^X63X3*qAz*nX|Gmu`tuK02mlJ?aTp207v{kAWlq- zoY4#m@Yy88Or~b1ZlpgjM$4`uO7XB>a=jx0O@zrOhXDXac1|Tb10#T;ft8gbjgr%! zH04Z`%>V)pPP7It=Kr8!<}`3}a$w+e`%gYbdKP+CRz~K3C%jG#Bpw?)C*@Ju3cRHL zV71jROFSKgfm=?=^ru}^s0IL77&wh=jEwb649p#@%uUUlXp8^`jsO#L2Y?>=pCtZV z_>+mAv8}5O1q}l|JH6e%mCMA;$jC&`%0&O~{N~;nN&{F1LBT#cw>s^VK7OC;R_T{# zPx&BPYPhn8w)I(TUdD(&c~dBLgcFJ1hO))HPCuXwe7N$rx}KgJLj< zcC7y8%h^zb)rX9iKF^QR^aq!Pp7TFT()&k+GzO*s8)E}KBXb8MD{~uDJri38C$oQ+ z#6)jp|8He6vazwVbFi_p{(D&v`%sqqtbOb(Oda{Nc+g%&??ZiL@1{8L-4P$`7ioXg z^G7_^0Du#|lNkWt7+_%hhY^5JgRkUdZe@k9hcDt{YYD(-q-S9MH(F+97PdbxrhjLh zDU%b!bceJ!#h7j#ut^aYk{5~xk8hVEd}UjqQF;9Mvzhdqf24104WM;$bNYL&urmBR zFc-Ic9xYjMgCLfl$EU=0?vk8&ZtzSRGOA>bf{hdE!5?4-PA5Yv8dFsP=R~{86&ySx6%4cl6ge*^4#+RB|sKKXVh$g^{D@La9AM7J@lNm0Y2Gf+|gAqWllql5o&w4>F( zyOWvI&cMmc>i^2-|7Z(;+0X93?8i>e#KiHZh5Qqblh)4Q4;$k@qyKfo-}v>fTmFAc z|F@*ftjz2j|L9x)gY>T`;J>Q^k4P;zZm~-Q(&QIV_;?c-zoeR{ckD!r(^&1F#d16 z|M$!c^c);4|Hk~!=-(;okE{Q+xBtBb{5?41pMK5uul3A-$(7}w(Z2&{{Fkx+Me#qH z!QXN)Gcz)=vj1<6zoP$b>;B8z|H5Yck4E#aM)1eLf5c)9a5BJm0XR7RncU|8cpx?g z*8iB4hUQLxjdmIr1{xPe8Z&o82XkW@CxD~VpWFUp_Tv9FlmAg4J3D~&uZhgW%tTMm zN%YQpPFJhhz>;G{Hcn$>LVu5JewUf0x0@OuJ?xP*YJse1;p%dVYCBd$FY^ujaynk9 zQnJ-bn2vq%0BF$H?9h5D)P8Rc%m`o>)ot3(JN|^lVnlulE=Ew9CCxu6-1;Gu8Cv)0 zSW0u~&0M4*(aQtXSGha6nCqPi{lyPFRUi!>esq-95fa30B@&$b#9&)a^M2k(h$oq! zx?Pg$)yETJ6#6dP{_$9l7m5bc7w{j8A}&@U3u_&b)HZyLy~@= z&oJB?*n6A1mztF|i$n5^FV9gMV@$_q_+FJv^Ii}>y!X_wN-V`ZG_K+3Ykfr;V?&-M z^C0%08WkatW}xMj&{z_*N0}&sZlThgc)8=#;rC7H%H9XXc9?wG8(?HnKPHAwb&yo z?en<+*?1&1_>AO`-`>}^Fe{{(X06=4`&x#UP=P*>t9Vk}ijB~ar6KixhZngys z^*AVijgmbg`{OU2z_uX7N64a_HisUno8F+d6N04% zQQF-)p>(?Qq+gX4XzCgd5#{`;1~k8MIuBolk=c`uAK9 zSK@c;(nyQnZENlZ2}SI(7rYc?J(txy58F5h#-A5teirKIlPL`Vxto2K7mvN2SuYA^RO%=QEY(LrA zoxPTQinorDJKK>cJ87MaM8Z!N=~^(%w>qk{?b3c1^I0O-S{olPuz4IMHuc1x(;XK+ z{+x`%YY44po zP0s|?!TMf^Ma1s=Mb$jv2tzWPUj7)JV*NTYr@9PRJRr3YcXQBa6NR+1$1);_Nw<^5MRECPY{d zgv4U2qUpCPG$45=)J3S3^Bf1cC{4a%9_+hU*7*kBZ_3Vx2Pso#N<7wL;Y#+OZ*bRJ z1Ux=ks(A=;bpw*akhmvJtGSL#T<2D*06n86ZWLXK%kr*eB%Hw#O9gxlJifqAlu38% z2||Xbl*#mpxHG>aTwE17Yy{UC}#9KO$-4m?cHj>1=Y8 zJHb&E-+=xgf+1dA#;9B)Tw(j4L5~brcmSILdyCbj4Mt5;6bx<&``dKFcZGeZyuS8$uk3?X)bWS31~2&EcRcW<>4lKNvaq%-YtwEW+%9ZS z3ylMwugz$B_xzD>AI5d1!e8av8yGfZRgXdRn%^RD)J&W2j5Lv_DzcULV3{L;E}IgT z$eu)rfRV$dV#Gk;lQ+XFvo^09o8=sy(c>h^KF{k+qY2_8ZblEwp9Dhs1myCSBXgh!+7kv!6OhW}JZkpPuuykA}V>as_DW@mQ&d^nQ zp7#T4yLOET$_Ag$b7v!gpT1gvEa@(6MN6!@6Zy)!woaUwO_;Y7?rRb<%N3TzY?%8NGq|zEL^* zPF6lH{Ay&Qr(PUwYpge9@%wW8sd9)Z*<_)RPcRW_E{KSl%;p^m8t@3xFq@G_o<5h$ z*IZ~7oR`+3&ri_?`Nqs+p?Bfbhvbg1K+g6IM_{&>5Rw2Y`jhIX3N+=~Z)*2zD(sKt za&>)D-z?dG0`N<`2?i6RP$P%j!$c9(6|x^klG4)SFqF;8q!u%puZWH$xG{KYadI0c z54q^6)J8_du=48XopRBcq3U)ciLe5M%f<#Ma+Fv0zjc~c=-Adk^e+L^VGm@C8;l>c$W<}JRQLug_R>i7rV9j zK^t)VUG_ac+8o7UZ*+r=o9!xBhPPHQTusVnw|H#!JGLv?x<1Z~LutneeXqdP8FcaG zyIw_U_gjkPupRiu+=3RRUwN$^C|`1^7e}26gZu9N?qaww)mNrH96hP$*IlWrMi9qA zu`|WqXV-K?m*{(1f?YoDbHZ9|*t&o>!Mms9&vlntA zQA#s)-XA>Ex>4M~jgcCCN=*3@zh%9Xm2mI&F32iAyP9gBX)vg&`0bPv{^y z_4(*J`abhZYmm>K`4}0;Xqos=tjCtoO8uPp`38O|d~{rOO}%eb$4S?QwLJ+0j2!l@bh@x3RxP5OAh`CW}WtGUP5Mbcu=I(EDJ~$J+~q3 z9<4qeT0m~O?EFYif0+gGc+9Oy(P;;K(#K^m%V77G)h03TCByZ16k@HOPWNV?p@ZsX zj|Oacui|e41|qzV6-~*wpj}!#XX>Kn1b(#aquknY(T+bUtX4IQjsyC0@zl*{W|r6F<@@M+im=aH0k-V`~$p$5$z4lRs{BR||~wLvoa`1iYU1bGI}$T-6u=MW)NEb{lj@JBap(06IjE0yep{ zJaeR{!gS3GyhW-)30GzUh+0oq>SX{2tDWg+#~+&>@U+&pjFm0#?l*WuL|}vcl&nnJ zPgH>;66=vb*y#}uKYw;Iuu=pwnMoFl$@jJ*R2WV6tn-@xebdQZ6(QNrjQi3}$W-VT z`0Hi3E;HdO2FTt$@yadgy0XioOAU!)q^U(jB5(WZ@4tmXMzqHxPR2_KkeHnkB zh?|`yz=dn{)XzYWfUQwhB{Z})_Rz<2SaH_#TTwkUv0HeZV4}9H2Bc<6e%lI`75NqaoKziLFq)o35_<+M!7 z0p1ucN^TPf5h;)VVwpaT+em6~P&1ryq~=cZeU2CAjhy1GvB?*QD#HYgktxB(hk&m^ z((dHDm48**520|rT~hOL4LUygS;XSP0Dg^E2`>lw?3m58qvu`VKMNFX&IT|5eQReXr>h#pVV(jfLiYg+t=v($b!49 zY3-`i;ZI&+2pMphkA4yRlI=7Fsm&t9)w_TUqF*7hy1VI&p_>}lnH=zyuJW)t=N4VA zTT1+wOM%g^oP)YGl5K65Wtd+ZeE=`&DJToU# zU}SX0F~RW^SwUt*9*4Jo|K6hc(0_ESKhfp0#8xx(He^2?#BMw4QuWrY8 z&Z^Ez?u*UJMTxgv65sXb zuL)kab^-{g4mLBad9S#Tdft}|;fdeba(>Ys;!rQGAP1Sqs9k9#@rVoRY8FMEo_p*! zAsZ~$>VXFoq+EQ0G(k~JWg$=4 zn{&s4cgD$gk~A6}pm>R;vJiTIoFQKF=IM+HLxGSYO6+&klS`mDw4D3)#R$-e}TuV|Iilk4b+!yo|lP%Exx#2e57 zYDxtvF4M1k{ABkO@ZECpnECWZ9Ok(X6kPLQWyuPd9+JUT>J#O4@f)h-tv6IiOz{3> zA<2A^n#d~?)ERt(YKv>*@=wMXq+ei}*zs?0XYTe$z|xlBs##%c;ML4a@L?vM!&bmx zlvtw8T4EecAxQK;DiKvv511N0&QvVMRBiF)azPvVa?yWzP9m`cF92wM=qq*mE**OJz8V>AkwSrL}w{JV9N7uVtuogJeQ$O)-$cy5TlnC-?>&5g|nUEXj|HuUXkvMV|8`B)M$+ry%yv zfS0JI4quP2Z?f1+=))muD0fE87yyLekFvnEyegRAgd|0=U%GePUHfO?J#`dfR5?_@ z;6{Oz!cnK@-vVbeFV)^%-;r$w1Sf3aHSHRKyGS?Ag1@ED7G-}@= zCY#-IyrG=f6ST9@3hl-@yu}lm^TR1=p!NzpUdLuW`8Y%c&35w+v}-w^=LQOQgkCY> zpCd0qs=c=!lqa(pfsd-kx9>$uLap!PfNaakumt#Y?TbkVDJ&H8i<$`CkMptlgUiJM zwi>o~7(c<1kB*QnY}KjOtuxWVuN*0IMi!T=^$xQ~)X~nfK}_5{K8}$i*Q#v==aS}} zDDs^KIG#@DzP=(A>k5jkoq#@tyQ$8^az@`Vt>gFA65=XOXRntg@n8rI@44^PTNl0` z(4tf$3^0|Y<@&1@p=d6VH|&*o2k}R}69cEjY1mP+B=!Asb6)x=d)O9}gw3n9VN3ET zEhUb{g8g9C4^%Fx_&noT`Ujc`B}kY(h_rF_77IhNE*Y1C`d%w-dvhWj$@;O8yiYnrWmrs-K94Os7m*EF9Wm@deRk z&T>U}TO;&V;_ck5r@dr9qA>&1ZEE zF4&5idB`j}R@rK!VF{Z)fpNDyV}qFmzA^R6Ws_8m=b$VJxtOa<5rT(E4n-~{XkqLL zN*O@6<#l|}MPezOE?KR<_V$?}I)A?^HxT|{<;-)whc=~*EC%RY_8;H+Uq~l7pKBsW zG=EeqdfVIZ3?(2xYT+{pr zBuql-e4k>GEk8%VKadXzlM1jlq3lm`rM>grIA4CgOTlReeb<#Tob8z$f*1ropwt!2 zX%f^0r8t2WblP&tqWbwjj2mnCtBj|vG0f-R?|AcqWv_Q^Wl%cZP{-t)nHSiM7040r#RmvytR5-_(gEx6I~bf z`z5gGdFV)$mcgA=^NDjKsTX>dqBZ;WY+dttn+Nlj@+}$OH<*g>4qn`1&%ZUA9I$$) z(#r|>QbPwBX6?RrIIQe%TQ0%5Qme*t^fc@25DKd3Xjl_h2wJ?>A4F|Tx^kHDGNsgU zJcRcNh9%aXT~Rc9dA1~ff*g&L6XM8SmX8(tVRdABhQ3kj{VHwS!D`A8{?WT!=6Sq9 z5LB7{(Q7pO=1zg3=;9w~6d^9l-A)6@Vm`0WFsBkK)?_oA2&#*a^FN-4T7L_+F^EKp z`B2WMkQu;$C0gbeIj-a@MZ`acvqa172HFI{96a?y_~9DUgSuq6Nn4~E0;a(KmPWZx z+B?>ldmy7;qT`nOrDtiA!G$cv-W&0dHP22`Q7{ri3LdIfUSBqO6EeD=_z=TN zYAsyxs{L$DSDWdI&Z^*bLYce6@ivTV4vPXgv7v;NRAk|2j>}R0Gc#&Tc3@GFYafu1 z0*dp$uQixk>L^<4y<6mw#kVm)DiwgFM-w=5xHy+2_tYxeYWClYAe8Q5o9w+s{~9UK znxOifn>>gCdVqxC||h-VG@f<`Vx7kU4nR9P-n3p`cyok2V` zoFk#LEQ6aK@y<;mkf<)R?;ZUn#Dme`JM9`G8{rP)5}UWmCW+!=^q!|5#M+kdU9IFE zKmH>g3{Z!6&im+iBSjc=7mR0B9f1@V1hg8qY)Ji>Gr2=u~uJ&PY57gU1IEMxwCc_^c| z@C>}@=v9p(p9aAVL zrPaV?D3+Et*vu@Bu|NQ$jVNXsN4}4>Nj@M>J$K68FM=9yvDFRMmcAFJt>63R3;|&)^X*Y@v=HBYUW15W*e9GxJ#N zuAtm;0a-lg!~WBpJ%(SMC_JT%1l#9fiUdF|mE`qa0NZ!45(aw3Z%CX@iaL4BdHe`v zk3FU~PI+`$B6f)vuBqNczvdLX$f|tfZ~e4sP|*jQs2S~(9KBQ;KaN7Lg{UHQFbE=8_8e1!GKsYRa!YwnCk;Xkb-l=ZsC+beYIh<=djD8T}hWymxw2XY_L7Z!COlp}2a?+_Y(nDIHdKP3$Mc zI%mSElglXB;8|wS3v`^6)*&N$rlYwxiL8euKJlYx#_egv^uy@Tpwv2>TTHgbganG+ z>7=;9U@71~SCKdc;FbS7npWjo!QQ*ym`CdW7sQu7LH9>BwwaEl?a%}g}h}-l) zapu5G2ADOKo^!>zM9R9R*SLy}Xk=UOrMw=aE9_i@V&G}pM2&4B84a_(mB?mn6Z>8G z7TQpzjez4iJXke$dQD$^Uw$x28@tv=-SP3FM0sOykkk_^#Kvkj}=xZCMWNufT$bkzdHh;<*a1D#ad?$)l2e&2Q-wyk%$m=mHG zPsGr#XSs0fQxBPu(LxqrKjO3?@EQD{8uEmP3Ul3^3jw#!4nqUg13$8md#JkN6Vy>O zQN=93RU&||q=dd6cjak|Fzku9DE)@;GKlj+hS+I60U+(`60PIeTD6vLJI#C z(Dd#T%owl(-XHTU60cUZps6zf%g(kZ-=eA`1lB%KQo-ZEfVK@VC-dmp$8g z&>gNkK%*d;smVqFX^{oU<^=ff*uNIJab~okG|=hpd&u_p)%GBau}#w9FtE#MhvNC7 z(c##QCZFl<0A<3a9|I9`E`VY^;1GYj1;Ul`cl%MgP3TM;%8(M@BehVdEN}l{XxeIT zns?5ZciDq`M5}E#v|@{1@wYK3cZH%&r+iX>B7_LQF|_29WpBQ8M|z<+@kOf!Tp-vx z*dj+5}7h9=POp2i#nXPwDi+R}}W!+1?=`3?6rFpx$ML@ixaxI@OW^iuFHP%*6 zmCA1~A>4{neU1q;`EiR=dzPwuPNcMws>j@`dBR`~F;#)}K)fs~gEy9|;RJt|*vm#4 z6lgj|EIk9xFhy0Y2a}nLOP>RN&J0X|#+VF^HD3D)8<2n07DIV75Yu_xxvSi;rIO89 zQ7A$+(J+=Y@B*1dx6d+Qec^fOdp1-2mHZt--xichKCNcS=g3JaJeUw(`Af-wXc2m< z5?zfF&HkNoAA#H5&=!lpkxnV@afkpog>`W%Y0=FzTAd+#tC_k$`D;gNcqN(dn))E_WBV@D%kvV}6x%{eV+s z?~Q8&A(r2c*AR2QTd5>@U1~uNZ?`M(h&SjNDZD69eUg_VS1Wn)TMVyCw_YXRg{iu% zZSYVvw?pt+D+i&s0Pe~6nj{T9Z@6LeQ={4MopRz~45XjlPxENo!U-mRK@wK#e5IV>` zD4R(AQX#voMgo(@rp+kzEPM;Lh<0g<+4!E0;*<&wA7a8?bVIr)twSJ1tW;;PMrvSw zK=am7S|%9rgR2+6{Om&|x-fg$`%!SGgT^r7cd3-Yj{t$I6-A2|DdIss(qtj-4S;wK zjD7gjv*`QtNpf7_Xo}iBvB4cPE4VdHpG%6u?}FeZX*%&s#R+tA6=Uoi6iLJ=Fw$^) zB)TYZljifDU<^_c9tEL$1BXPQsFkhB1hseTFiIKjU&eIQWH$!u?8}NLF90cun!zzK z!U7n6TGz}L;^8@$7EB}^WvQb`e25sg_t0~p8f?$yKR==9Oom6;GZ8gXQx-gf63vw5 z#vC(GOjQ~_9uJhQJu>j9@@&*2mit}7%;(a9vWo{$)x8HB$2l<%A@PEWEQZ?pQY0}) zLsN)c>5PUwq*HeH$Gd9uV|UL(HOMfos z!k4AsC&J9M;5{QJX^LG*01rEQ`kx+o4#V9r^@gH`mMu^%3@2y46I z9&~bgEnBF$*GMhgG{lvo>2uGjgI}^YR(Pphn#gw!SPYI!6gP8CYC)p}q=@f!5??xv z2qP$9wKMPs*9%8tIli^;E0$OXI!j5;)Jm0=I#HX_!7lCCj1Tmt*K`qg=uTS#Pm zuyL(!D9{N_3^fo1adFD#j+EC}p4xKjPK#I?O_CEWmRUCF2pW!peS*I&cjKYhTw`WI z5$ES_Hh%re!?2`cX4(b02>L)}90uc%n2qYm$(~lEjAb*b;}AijC2#mpdFe0wk@l(U z*yVg&)R%nW)(=L#`q|{Y5pNW~ccR#O2XE~7RYt2`I7Y)s3L0H zVoT&k0=$-0Ks}z77vlKz2H@gsczR%+Z9VqT7AB$C`&!dd35M9k=G)I_YNV^ru1^%9 zlr(xTM!3B#Azp594SnyOB_k)FGbuD%fFc=;$+*Svb9Gn+@ecP&LQuu?%t=o`Z^+KO z2o=jX# z&l+J{Bw_nTw?x`DE{Ha>YKmPF0&tnBU+LC0SZr(2wdrB2l3*f|sEQh4ux9Zb9! zv6r=#6s=DXSaa*7v30y^L|NU76#cEv%wIFlM&AAwo`g&k_10?hlm0RVa?~1i>%eY9 z!zP9>n?ks7v<30xI)UrZwY>q$P}Bqs*D3^f$1;CFk)T72<|nCiB&?1rU~lM zw+I_V-2zk%@lQRh%;`0%p$2{PK@zrFW%dpF^%k*|nZ_j;w@(>IB8+L8n-H!dFO54l zv#jE>n3k32EZ9yg>az3bZkD^pHrjj3&%~6Ksz)+IRF-+UWpY196Xd87Pnwjc^t%=m z4wAZlB|Jk5mYe&~Nr>f>>qXt(OUN~&=;$+}_RKLmZ?NU?iZ!LuZ1flX&dKu7hgneT z-0{2m*s^R8IG)-#J3dK5yhw5JgBEzNxVsIF^)avsh8os~Kj}g|@(Dzq*BK)Dq3z+l$vfS#)g45la!uY{OYKXb+a-0k>V)e#+>2%TyW?M+TRHR<<`h-bA0);^C2_RB6s8nS| z{qpoq(aN;%A)dm}7+|DLftYA0HP`@h=%%voqc9)hoN1hE>ZtXxhP!|730BslNL`cm zH-Uh^z3rFwBjBRPVA}BVc}1#%kd~0pNRcNy$P{cWl0YNo!le5>dodfLG%Xt#a(vz! zZ17P8g=0p<61Adw4)ZiLYI}M4o}t>-v1GbMbO9N#%Q|CJfveq*oLF5dk^^X>JvJew?3^V3n`3~&V0w9|F2S0>u17J7tF%u5e zvj9EAY~(UtXU38pRE3u6403j1Tld+YeYbq`Tl9oXplOf`Iy{fwU6TwvvU%Hs38qm417C&+O3x4xb)enM^x=+G-?;=} z>d@wznWue!;~Y*}VRO)di8(E0&O;r=6>Z0uVYsAM$!=wSw_5cF$O}^*Z5QgNl?los zCtd#l8pJrBP|C`8+zjzhs;TTxA3l)#dR$}j4dDwEmdIE5_RhLzb}yeg(el9V9t zI!Ab2q+s9R`WsW<*&?i zDHsNoQf_)h>);#`DNDF?TDbgyCZVMk>_nq5JP-)E!o!Tt_eZ4hG-UeWusg-%I$|np zHWH>}FL8*CK)YS@UJmPaYch&1eTrEcOOS4b&D$5Z#s};b1ulbxV>L(QgMCO^m9}$z zMtD-~x8$p7l8D6~bz-&+!$1!zMi$$`lts4(JB{QyKhLVv>#gx_KVvvE7nZi2M@O{gf% zaaCvmy>m%-yBf=MLpx5j7%O>y5ZRu2)x%whkOJd0XR2*iMc{3&g%nC!l~x6YR9tHn z1TW%6V?=lEKJ1DH!-L-$QhwXh{tm&>7VdVl99$P(pk;JdIq@!I z{#?6@V{1-5TKFOOfcp!*#_tEg$u;hpbREqD{eE1;*o=y|xVCFe7SbxPWXdunk3B1( z1TfYfySyG|VmSZ(WSP(SgM>D8o*BmzBiY1$^GQfz`n(XF8z&oTr|tT?bjUAevVr#K zxeAt`n{c)=H@x8yBn#*Si~)=UxN}f@Ml#({QbzCmz|;nXk7lfxB8BkAd(5Au9H*V) z_K1g5C0qOr3tf}_K5rM7H@#E>Y;f7zAao*i5Qi{f@bB#0xCq}&NY`P49}ayHw+sR# z8QtkIkOog37uNK1fYU?klPP*R(07fFGHW!Gzggc}W~m%zc^|9ES!!fJT*=+^Tzu@M zGNUJ|Tqstub>gz5IFqIk{hS@}1Z7kf4Fp!B+LDi}NpCofmR(1~2T@TOhQe|-59)_^ zsEN3avjJS(>Ut6=g6~klAZGKKmH7Tj2JHFg!X%GxVM6$X#8NL~NXowet>|v))e(-M zi{ZQfR7CJ&1WoIY0UkU*5K%}T+=8%aR9Lg=B@9kgZzn^H@QQK2|iMwE!Dg-Mb68wyzDolR2!e+ zN)pIQ?yyZi()eVfBv*uca`%WWw@;Ab54VFcksLT`rXZ!VVL(K3J!3Jw#ZKl*vU?{) z*WyEH4GX2WpL}RZ?Jy18WLB<1A+Vw@9R~5F3OEdedyOX#E)X+3AM~Wic{0oF+;x2( zn`PLW%jU_SM``1t@nQ;w$Kz>36CHIZd>!GYAxV`74rX9nDi1Dq49|x=bdi`9gqbLU z8@bQJrwqml&Q7t5X6RF`*F`COK8xL9&mdDG)L5k1#E6U?`Up$8^PXsg7m+7E=zl`q zYERK>h9%XC4f5z4+cQu*e;c^}qCA}{W!67xT+EOjQ+?D`av(mux zsLRE#w|S&jFm6EWGt%trD%#`4gr=3n7ZQ3#vpgDlCpx*H=Dk9GFQw!1X4uH0$X@#M z!Pye;doT5UhOYbPh%#k^e|wv|!idY`T1b>DHEc%rA=##4@;mmG9Gy)kMVO<1-jgb) zHaqf{NCik_E4?Gd0f!8$DBPuL7yd`s`q&N#i5j%=Z63$VAx(IiUYpSj>P3-6uV1OtqC*Aaj`AOK*8Wrb>EB+3#*lHRH>!ZuUdK9pC64( z$7DKQ@B9uom0@JtZhklz;c%nZ!lWN)%K=Nb?M{c)hcrGJ(e)hP zO-QJEenoEFB1FHd=ydc9>T-DxE#8jQAfEOxOd8S>H^%sFw3)=y;{yqN=x zle@mXhssT`C+A#m)2RoqWiUQ{NSQv ze^qQVUtWrfh~Iq60hisKxRh{R)EaYhGjl4tbL^o4f%GwTn=M+Z9=u;LLkJCCL-mdD zjo%T<4_s*l&b$}fisbZCyrh8Ur5?kWVH8sgjk;>iz!)N}YAk$3L!%uhs=UHIdJBxR z;Y&9`9!sCfn}Yip9e^W;7=#)QoxbpmD0-Zov6rFfUQ#eF@ z;?rkF)L6n){B~gccOOX6Q3i?YQUMWXmP=iyLUtjy__-F-aq^(zILoy^or8FFo0 zM>y@^mc=&`zkyfcbt1Jv*t<%ah2dg0(eJ71d5diI)8kxZglfZ^!O-CAf(bfk*8=>` z(!Vpo4NAiYHC=&3xns-$j5s===IMTAG!w!-RGo|xK{8&s7Hz<`Pi_X8$AN`hQ?0J3 z`12V6Ef}Db@RHUCaBpQ!uf4xUtQ?6Ys6)zG|u5e2)6jfcRVAuo|wc1Q&I5E?lpXw3Ku<=ASd6 z-_?8&*HnmYSU<_R*5x!#p7%vy+IJYkZB`f`C^3@Z?#u!8+8+VozSGMN!1%1qg#!+( z;hP-=`OmhQhT?Ni!5;!$`|COhFn5k+2uW?o5VdsjEzK$D8`TRx>^+#BPmdE6k}Qxv zA$~E!W*{iH2$qpu6d|EM&5>g;JPH9_kMpGb#Pci}Am>lV|85aPTQjW0t!c%Wx$(h> zNN#|z=q}RMsx4QKA*oG4>B37b!QT9ez@2Osk3pei;=x39cgXavO2)ZUHDpvIHD0U56kIhpP$0-Aqee@$0VO2Dvv<)C-XA5{SM46mX(rB~B(k}jdxwl?!mvXbX7MWQ;31{tO)C%+7;aI?= z3ONO$Ol~0Nc&31WSH1-FW}k(~3w-Sw9K}{^$XRktHY%ku;P7g`AdzUf+ro-_zV3S` z-@PF&nzJc2mcoK?gXV=8@TQbqM+Tf?^LHZbL+n|KF$sD_)mw!?yk4F+8@yFgA+=xmFmv*CfU%_2Ne(A^_V zv@%CX@wGG56bcdsAgUUgB1&k!aE1jc&)>OF#Y5(Gms!il{rLX$G8QCu7x#rIX^_k< zQ2}#4Q+33qy&(_0{snhgpma7G$`lmRT6@hx6PIIS?Q?JECHfw9fVo28BmCV5Ry;I2 z+n6O*7Fnjg0cE7MaFr71>hQn_vyyYh)o$3t&sdMwelE^laO-2&{av~FOWw_vo*w3q zR2+Y`CT4FxE$P@+w^x)eXL9H5cOUd|3X!skqtN>iQ!Bdb+$lE=AQp?H~PDO2NR^UyIMd%vB)0s#I5(|#IXCbdAHug_fl~7F+3b``C^O>ZD5ARD+lf3@aeoT-mV|hgp%3ag%Rk z6Ns^)LsKUVTjA0zxTl;{W-=(Q3|eL>ebH;d0b{$rRO0(Q0O9RsW6r05X^OrbA(N>n zj24#fu57k1Fu4n_OdybBQ?)cD%+4c_BBw^6CQP(f@fU;47AZJ_K&h+gLeR2TSNj^A zF^2i)oo+Vx)~4Z!Q}uw~Y{@PEr|k#L$Hmh;-ngAMd`271cz88ILz&?F$xTPTQAbsY z@0L1-o^oC(0J0q3B-ommR@pMFJQhYNZ7^kH$)ZpRX0Pig)O($3#j@w=+%_LbvY8{c z*(^E7C+Z!(jy%Bz7EQ&9jpGn1yt!S4hA7ol9U2{7S|8C`svFcN(Mx0+o~>rI^+%(I z>CEG8WWPcqXVQtkBycTUSj|X_utmlBuMjyj&+;7wOxzV*;!(A1Pasx}l77Um9WNG| zxE7nPX*0$XvgU9DyEsZ=aM#pGcYp7?P)+|hE%y~QjG>-=Zb%Y4IYA-B+U#&5-!o*_ zdAayz73EM*j~*uod*ul_PTZj{X;>a6;*i)d3;U6# z2O+}i)$3L~W@ZN_s?M|m!t#XB6BSO)Y?+l1^%GPwatYUoA9O}C7k_oU0soR(FMZbB zskKBRN@|u_WIBqNakmg@3QJP?^A9M3m6*YWi(f8AgMQqJL$NW4S83YWHUx=4?D}V; z9NO^8Dndt`l!s0RHme!Y-t~$Fd8%&dN0r**9~8^h1N08iD~Hvx_ntedVL!XF$jHl4 z`K2qX0?s^$Z}oW=4awowUkFJ6Rq`{Cv(!J740H7be)^(^{eFiMm8@O{gA^xJ?L~X-@i1J9 zqCpNN^F;Y0%`4(tD5F8|fhIzStYsM9RNo2fU;XtuN23cQ;UH>e7H$wDgw$@xX1~EW z+!nk@I-X1=86@}?u8=_hlU4laOwMxx?mW$<1)gmAeGfjK=sQW%NwVbaNgF|na)8gC z!r9}++Dgs(g|*2_x;IxZB~fzu4&4ylZF@gENp#>D0W6mS3LD9T17w%01V9XjDyAK? z+j646Ym2y#Tlf6k!#Kr4duf8M4KI@0p?lNbw?>99Gc}~;-B>o>Y1bq$rb=&ec(TQN z#~qlWY$fNw&A_43a?Fp3jI(0;)|sE7Ib=xidTB~|n?~nGcz_97V4alfJ{L1_yRdN+ zbv`@jjX@EhxBO#(+gLFpDTzKoAZRg`->y|i&xh%;BHOsXm+gFXMTJj?| z^hpZhviEn%T!7kr&>9g$SnmO&1v|oibBN_$T{NKp2-tU4X0-8Na*6ja@HfGVMSxMH zJ~{Q^+kCW^&i-ZCoYyNzA&~Vc8c=}IFx}!R<%Pn(eDOFOMaej{@qOqQ;e=HV!HZ)I zW5YuO0pBR`evSau73j}p;O;Dnd})Uf9k0#8@9m3wha{!Q=ZZ*c&lWnwN#s#4YGnh5|r6gw-_rhmYlgx*drR4c<`2Flu!LS%ZZD~ z#;1+KMR+D?fWdQ|*aUO5Gn4CaWxhRKtuNOGjKRUjBxO_=epxv1r_*u40RaPv30t>5 zt;G|gWXn(Ov%>sX+yiRPiCYc&9LoP+M9!`^s!e71F{ z4e>{TRenHken-QdWmdvqIV1~GRC}r2+L6;aP*3I^EXPhk8S5^LzTM_buHJqQ!Oy?j zBj9ZxMo%ic@w9~W=mi(b#V%qg+Yn1X+(6eswr|OMt$uVRr-dv7@A(9#n@xRpdEM)1 z&2gXtE3h4r)0c5s#)xaW@*3QBRT`8`>=lYM>SwcV2K zk)iiqsXHB@YnyHEO^3V}P`v`ez+|FP(LemFqd(Fi)&aIIP8tn7rTluhaA#zYMA^z& z^ox%5DJ-d?3p}5`ZWqPlJZ7}SwaefQeJKM?-@Kkc&R^~d9TC(jjVgBO2~9H=tDm$F zL&$K9)3&=-+EbiM%1hzuCOVgG*8`IwVyACtFZT_V7hiJ{9Sj4#LVp{1TayrGb$e0= z-OT1wr}`niHuaH#t67Ak%5n|awvn;oE;nGr!pO{?tQimCd7z7HGe$Yo4*(cMx!tUdY_HR&23&1r0&@1=h3O?3xZy;glt1efN3t= za||$L7IeYBm`>6Zg|2Rk?odqgm*pL|fQu=ZrTw^1_-b$wzvm8K)gpvkuCZ;$V6Dl{ z22fjK zOJ6T$!23FVtM$-ods(}lGdw-rVr1r`16NpIN5Lr%Y)+xukHJBO{l*oq(3Tei(D9$d z3Y2jr1cfv(&-epS_o+I}Fq&yAz9VF4X5>U+*oG|Ao|CN@vxZTFDM%p19R--lY}DkHu#e{5`t4wSt3Hgi3|CCtxX zOE{FY`&nA?6$pm(=yVF zc`(d36+Z*n2rL4?VUqN51jNh6Fgdk&-S*g=Ro})Z(@%k@!G`qqayj2Z-y}H{57wn_ z<$V>gYqH)68{zTr!_BxHrkYu8Hf$u>()V` zWHgkI5S#LcUH-HqU!Ft;&L~~eM05`>zIPQYW#w4b$B&HyS$f2vRx*P1^*`YQ(8JjW zId59F9I~Gs$aO6}HGFeC_@ZH5vcfSO@`%1(PE5#o7oQKC{`~|L;;@GO(QE&}=i5UO zB=4RGw-)?SRT_u2T60~kX>Q}_{v%Nn9NSnYLMB8-i+SK(=`Ubj*YLp3#uoV=h#G9q zF$sep@7n^;uV+261YUO>@MXH(p#6?-e+~wcWN_bYhR0!ARVWjllX^itkvLO|{FZ1} zRTTl}q}1C-^*N2<-km8(f-_qL!X7p4f1Ha{4V|Z*=Hz*mlrHFv0TMam zF+)|%b-47Rp%_PobaU;*E|H|^Jp?`wp}h~Sp;ItV()fmhlT?5*#QMXHlkUYqF^2Dq z8J|n=Y-7e@N2y!35a=aAS2U;;y~Y17ID)DQ!GIDti<2_59nP~KXxSrknM>gH8`dt* z)4<1ZuMq(%hu+(9?M_wBj}Dpj$=h?INupSfvw+xZaUO4B9U`_6%!IVh5FFb}<12F7tv%sU#`?8U`u;_{8T1xt#zoIy~94>)x?%ts`#}E~p zcTufdy!cG&TH{E9!wqkD3JpyC@6+N6&t%JB1qwYfZhBtqQz27UZYl+Lm|E>j#D zR#KPf0l4iwjug)L(N<420EqI*C+$nF@_e)sSWnI2Ic=!~#JEu2T=zH|mj5gFqcfz} zafZEzX&iH@r{m|0L|`GjcVo?pcIYjysUQln32|#7dbK!?F`j*Y_@Nlq%3>83(99Ps zCne?$SKW0>0h&Z&404a8+7gwmiXmiUNqCzQ5ROoL&CY|F8~>MESvm2^4~Q!MZvf&# z@S1!rZs>v8xt-klKWpZWZ{&ajdl;0Nh2iDZ`V6LbnCFs^=J?o8UDqDIyuFBV{I4MK zW%&>S&8HLveg-(T&j%{9@tm+omS8@_*Dm+%=^PAH%EH`hgVF6>f3lnzW6O#`avn75 zwnTL3a>?QRI^k_SB1i}x01^rUI!5VRTcfP^u_AnNA;!f&CU1AFt`62n64GX=ajxtx}PVu6@%w3X3F6i0K75$){osqY=}p5wZa`6GJ9t%RzRN zdT2)Q+VR7KBa&+X-k|Ge-HHkOE)el)F#%0qtktRmKgea5Ueqo@=;M1!6G8JVxnA8Z%mO421Aq%4leC*iSKFE z*yhoqpyYyi{jkleAaT#bjab0+boE&Itguay*dqUW^B7TbX{jkc&+GmPL zYhfCrN5U&msw=eio1p$m+VoKNu!=*DGM(^=NIV%-hjUsw!xRjWFu~p?Pj`XDpvxGh z<{^$AqqMZ1VCaq6ckQ#k7R*3|TV`j(kF&aYI|uKstPis8^jWyiq!Rt&VSr3-X4!e5ugr4({je%1aOAnR3a{kru0ktkHrNN@>oN$VlQ2n3#)IG$hpWj zT(Kh*TUZ;bogqwip*D*J@ygDOtQT&jFBv2*;xo{)8`Z}5y7Lai|9&JY80tKQO-q1+ zR8MlWqL39#e?JEC`f&OK_;vp%H)?4+s~jq7jFdDarG0KrdUVkG&)g43G-u zpYo*V-!qV&A4)sljN3u#D{%_wEI9k(W*znq)TOvDb$b8FfqT>w2h%-%uOf^f5z1ez z{xGIc@(~->gLwgA8qY%kAQ_2*KWvR6ZyKfKE!t-Uawz+&F^$54sPEy9kL7BWidp4X z3;jjAeIC&R+vG8x>?YgCGHF^-@gK%4AJ^ zZGs!AoT=Bh5K5-;f@5Y1Pm8M?#57WKeBI2my#P(aca&XvEtP2SK2z1AXko!rneyVT zCeCDli153WVUEgfngJHbp8Rstu!14nf&AF1Q2M`yuGcWT1dPY%n`k^AqSGc|*QUqB zLZF|4iCqH8Nc*q9e$yTea;^9jId3`YDqDXCD$1uEm#6gn1f7+k9D)MaDJK-iS2R6dC{8>xsO#A>Uj~{E=F+nVaXB&nhjyDq4vYo7as$y+ z(C_%66U6iw*cKoPow8j?=&?KdZTNmYwmW>y4U9mqy!Zp3Cnhp+gwDLA+GSRV<^ziq z81zk;8&`?3j5@0H0iFdfsbE7AG)H_G$Mp{O!3kJcYZWE-Sb=+TS?K}uV&%?A^*Txd zvN#~NP?VaDzJ$K$V}iR`i8`U8tcdsw`3hpl0_*L!f}A!^B}ywfN9_TH7cyagOqBZ| z3+X0;YVGoxSc?F+Mjd^eeYNC}-=L|J3qS)Ad*)v`cD8Bg0~+*Q>D(Wp>68Kgzx~O9 zmy3_v4;bnfa3rZuig*;ctsbdID=L<`J!yxc{8})(F^c`wh6{nBGNq7QbSAuUZ)$OO zCXu+CJwHO4%P;z&C>%|)3B?c&{}rUAW3w&9is$7=dL9?*|pG{>aoq2VkRbkn&cKfY|iRvEwxF0P3Sl_OR8_6DnP3} z@b1;BbJl68rf0dn&MkWHp3vib^8MTVtOJ*jqLNDXk>#azR&G+&vSXGkC2L9TuYC*8sHknvX^7D3Tj@y(`>S{q>G|E^>$pnmaAb@6uWLw~8yr0- z-;^PH!^MOV<}m@NHSu3>PzGhUBJwsxF8g_l`k^s4#YLJEhz$zRtrg4A8kT|QH!C&% z#&$MCf@NfxDJSzfzT#0TV_Gg?br~tEh(KWD{T$ z5(RugLkiZrSF$#lB>jZy)kNY25zhcuR2=m|6Iu}cUaEnC(3}_`b5AS&63Sh+l#&_l z7{#R@EK$r>O9qr8_A@)!zZb)`v79D>s*jWyCCFx+-J9`F-ngC1%@JvtmhcJCn#P}o zts_-Jx0UYH(H(~GjN=X{mBh~^Q2G5973{(4qAHMY-!fUf0jYlLU}L9NQujAUL`Kww zYJ@zU%>94un44f52jUE-bKDKQThu-G?9@k_uri7z^tla?kg&qMQ96R2taxd{AdNbJ z6maT{1LrE*1xPk+k}lX_BU-M0+^8K8uQoK=IFOQ1u)vuckre8}lNGXJuJ5SzP0KI< z!A0Q!uQ3Q_iS91G1{(H#a;O?E9&zDD-<{+Yg%WN4b#lN`16EVT*v#`n;H6K>aR!3i z0iosh7VLD&T;z9|g^FIg9T5}6TWtPHovUnV(Q_Na9ak+Ymt>@;(8}MUhTS`$L#|6I ziWpvu54k6MrAtzhUe3og#siKJAxTxfsoJQkwk{mb#EO~w?JEJuuqn~NUc0sJ=WWIn z)YJ(DorU9fOYxMDA+B+JM?#Tetw;9!ms|^-s0ZSb)f~u>*-o!KxlP$p5rC08(6I3W zE~SU9atCF&JFp|AB>r0plsyXmYO{QF&p20QW_t=C0p|$A5TcqpgnDoRu?23?7Bz`0 z5BjXxSUy9vPws%*qw!9$%RA%>vPhFWCcGS?ar%ZL4M6eDpJ)2k&CnrN?&`$2x{z^) zM}kBos81hMn+F2G!uB2b%?EK%Pd4l?gL>ZB5T(STi!B?qzyOKh*mMh(#E!eP&y%JEAtKWe*&PK`r~G#km`kX-GJH@3(s{hNns=3txUM2HHT*nl}+9(>$zZD{Y0 zf4YZ`ZG_UEqqN~-`@0;K`%p>nuuPWzMh3UOuB4f3?LMRZcQXNRnT|Nc^+oS%7hQ8 zs@O1Heem-l0@yy%{rcf;5(|?7~papni@&`$KmFUV0hj zQ=(yv7Xd(@Uh?d2J9r2wB#Hl_Mj$K>IfUl*4J_F}BVQkIP`YD+#NstVt z8x2J>(Rtk|hyH*w*?VW(!lKFa0YaUe2kZ~$q%xo`LTTLDr^_9Zhees5K~1AD$`cl;B7UbG#q@?q+ZzwiDP#1 z9kPKtAA1m2%V@0vy~h(Moz!z8jYH0@k^YnY5r_S+5i(4VasCVAbjB>{rQhrv(r_}; z%!gj0$A=DpBcJAfKIslTgQXOAm38f=3-gJX+#?|SXSca~mu2Q;-{5J5!`u|Qks zfgy*DjSsIkZ1FDC^Goe9c~=QAlxZ(fb9@rz{tx+`J{v^b682}_`0Zp zC7(oocvI0nVYLgWnqf`Go#_nT8BzAZA+`Sc0*a75``8mGvAqb%XBh6!PiYQSx75P; zQyQctl1oC_Yl|+d{Q425Q^rd-05~^p#bqd%91eIZcTg1VjjpHQEe5L5L(BC=Z~Esg z#RnHu?{iIY?e9|)U}zU{`3tjFP-9h!-p(yt?AVq44gU9ARakvwUqZ~>W1PKvXP}LFfLZxiuoI5@AVK0 zU{Y=0`x0j}6{0qR;=D{b$P;hZRBAsJncY`IF;K#J%N@aNl2s8NI8bK3zl*F3{pwW_ zR>V>9;kxRWhPV2Qa>{M-6CI7t6d99Iv{Q$HYDC%?<;Bgu*U!Va$+SPqZGHQc@v^Yl zQId(6OG8j@W%(Ob6VM-Iy^^W*Go@Us`j#N=4=|jSAu>6|J0q07pg@Z3v-q`^*Ic}V zseIVSJZXR@)(BL^1ob#=R`AcUPqQo0|0A@9JLRO5t*Pd~6L0*Bx8Fh+2xi3N83f-a zYTeEtRNfhS;5sCv*#W(X*?zw#iN&-1b6b$Bo&WZ4sEe(+V_9ff^bYt(GAfYd0wmvz zdkDiVW>^tiYL#b&mkp;UIy>!zxBDWo<3ndaOp?`H&96!9e$1MRb(sJ7fN>H~q)~Ek zCF2orYwGg~U+{7GoZVXr8C41z;|FvEWlE30dhGfSCnHBRU&m<~WgoE3BvC`28GX

&TcGDsb5?F52JE{OndGvo54-Ny?=6Gy@QtSL43qpTH8qVbC zameXjCPJ2U#v8Jckk}GZ*F@j+&Q{ehcqem`)Ss^xAUU4Q26{GkkM8JxU78HuVsmuZ z^foqLP-JL~-Fuin_ie4AhK$Hj$;z(;=kLQWS@$oG@&M|#LkXi+;zT!dY zOvNnP>Z=a(!&H*I$j6@b0X-*BsrWXK|0)xIy#>s=Pm1Hj0ONIFOzMS5U3ae00tQn9 z^ym{^{RmN*T@gNepnlix4k=imhbC}Z&hulSjAv!Sv)St56ogxv@9V|mh)<&^IU zq0JgVDAQL?$;{O}gsrT!WI@MG6l_9fOvFYN)-xwI#S9d+RQr{9JcNuiC@;diGUyx?=#M>0OqeM!l1ZH^w*hUyI*{Z@ zTjqH#g4Rg6emCu3)Em4!0)yIO1@9l+Qq46Hi47E`XSF3onrJzbnVe}E3rJwpiU1l~B5`+<2B#(nyBx0Bw>=JzN~mQ2D(L-eC{wiIp0 z_SZ?BM*tdL_R_Jr`*a&Kbc+5+FH26@BOF$e^iIjnW##p#i7(SiV#*rOKi|5RKxGR~ z_qOwvp~)6f?Ne9jEMDjr;@%!^eUPIUE&4Uz4?W@R52= z9*bNzU}0T^Cz)}rv1jK}Mm^VCX$oR-UP1WmnOWw+_mS-8&lz`o$Hrn|5Dv1^glrL! zw4>)8wT#)8h2;bdpVrx+q-^OIJN1=LyZV@V-xpAN?8qGC%qH#0MfGy6U9&xSt>|`c z3V=#l+f~sFKEb!t^X{f7ioUZc>~IxnTdcpFQ+~Z0>}V-=r8>0|{0&9vAa%HH0{CT9 zRtg+qs?P1wYPH$5*<=zmK+^+-V8L~31_4L*1$%4zj8=3m3(ljrp7loFaqvd}*56mdJ=iUn;X|w$Nm?oMeCfESCVTo{?2t!P^Z*=Ft3I z&b20N=Z!nVlTWQD2vf+41YMQ2b4ZQK_o}w}p(b*&eOY{4h*eYR3tZL+m}bW7@7B=R zekgE96+atgQEM+2lmlZHBA=v_e@GrhDRi$YJd(8`5&Pj%bm7{>wby&%+e?-WpmJ}! zi9DoKL@UoTP=H<(@=d^kGU&A4yTCUf0doxfiqxVV_Jyc_B;y0HQ_SC7{_(CFYdb%f2-d{n;AFBg}tWjS%0BDSePiHKvgDI!4yeBjg`Y$+pl z8z4F|jsipl_FLHJRb9UB0zb^6(u+}mLKHlTipcV%K_C~$|KOCwVK3o`h8acKQ#FoT zbY1i2+YXHg|6{@hU1B#c_VQfZLQ5ohH%xP(@H;VKJ8;-MznuzS;?KLzhJq!z(BSKt zhu|Q>KOQuRL8tyB(#=_rbv0s+{qZbL+jFVsJ^{nULCTP5wm_0spk!l^{~3{RM#WI~ zTg#pAqNyRJFVbik?9}HJS-1SpWTEcYlT)0}2>KV$JNbMZ{7c!&=QG{ECx#&TW^P-r z(c8e*1oyo-6!4IU{M)#IB}zOMZUU(2`;@>{jqn>UxE5HGr0OcsGvhVva4jNU*XA8QN5rm!l zDkyignqRtc!fqw8YjHdsu({k{b%!6$??=t)F?rP6QG5%giqT%4CHr}rQBhHW; zR#91gm1s)hGc~nT-`JMf{h6}83C?#Bd-p0%G=hhIz_TWLx_POTX3k zNr;o<+}?ZCIj_@WFAjpejFb=_n=S;nsuVCujpo1o&VK?z0>I9R%|B83tf=8X{_55l z>lP|k`@}W9X#`+nOU=5H|3aAx9me((A@{mNy^zTUTSrEjougl3;RUrSM~gpD>>HM6~hzI`fm+# za%+6@`y5_nq6~JLJ8GwiHJCu{dPw|!OwYnXQr|tLPA~4ZZL_PIXelH7biK_B(Y-^g zm$N-WrnFpB77=O%Jf0LPHxayy+Qy%5r~+8jfSjf_a!?Bs7rgJo5YD8KHA7eVcu%N# zF3`%_`6zKA<5>rvixEd}7d3oN+n75$h8F&|MOf`c{$?#TwWxkTTaZd6QBqa z4KuJBSZiK2HZBjHvR$K0?+dSqHk*SH$#k<2fb%vD6km|mBUu5z?`sQvP=R$$@_!=x zgBfTR@Sazrtcq17uU6{7kd9R-d*BEFyTL>XnYhqk2-g|zZcSYAAs|sn@VF^V3aK>- zil}0FnquS|YpINI8C@x0U7*Wr->dl;Pw62l4ipuS1*qg@?B+C;!@gAotXzJvTw^+puYPU14K6Fommr zng=i^#gWZ%S5@w~O?G_+W59HUKBE!G;fma88GF61XW1_U)s!VW*I(He2%qPWL1Io# zKh!mPeklk?EN|k?y^H4>aRNIaLDOj_+dSqTR{q77cEqA=RrR$T@1MyFG_oj1{X$xey= z!`iWiFM5hxD>4ruGEWGdHzY#f(NdffIYVMBTC2>88?3<|v z5^fW9t6*YQ+9c`mz84V5ZmRg0nX=<2>+`op^Mn~d&X1NExsY{OoG+`|#!D-jE8IPd ze8qT805~SE^+Jz5kIP>OZZ=A0o36c}x%Wg~JF`4#11XkJDyHd@*sKh*+o5fjjY*3!SP+F$&(RRLIevx0K%rspfTQw zb|2-qZ6$nx-*#r!H6BBUBySE_gY{o&_|C-1c*8aL?6ZXp4TLH;NV~M|P$g|DBlk6z zEmIc?fS!iBxS2f1DoMX^a26X~SZRAWD@`G4*r{$dc>?3OL!BoWlW=?*6}ae;Xs9I7 zHph9Cp+7$CF8q%DRM4Izup9jYf~%CYXAzeIPgK!848#PA@5+2*>t$;I34%4q#;HZ1 zljPtkfZkgKc=A#&byiRI*2y-^8IS|lMI`i%8oHy!P_(B|vi4|$ME-om^?^bJAfxM6 z9Aapr?tY4QvcaJ#*w-r|bcePBYwSn>uu3taGTRwAl{>Exf5HE3e}k}`hhyp|8<&Qb zNSp4VE4GD*aNx`MaQ?0lG_U0QzO9`!cGFBYJ|QjZH`mLnLoK3;z+~v;L2cw9z9csX z*4|kq8df)SG`}sgQS|XI9Mj2JveL@4vNzSRnwwYl6lX1KN9zs*W)6;xxiAhn8+z6c zTpy#0n{)>k*3xsET}df`ms@VL1_eLxFScTxO$Z_pKAtc#fE$GQzJkz#NH zgp}{%hJ@|;$9fYhl~TZ-o^zNzM~Tc?`H&$x5@y>g@aFm!W&F*8?PX4346M$XCl?E? zoxIUt;;SfK)`xBCMuymNK8)c$mV{+rC)V~~rH&7W|6^7iwaCYZ8S>Rh|D=?@_NpyG z!?gLm(_6XCOx3fa_ax-U=HD^Cp%ply&-mYsVQeMLgr=^>QI{mZGK+#G^8q%Y#Gq$Q z1Ih@Rb*^2%gjdWGll&}{n}J|(^MI*-jB4%tci$;|dIX4@{t&)(^$x_P;#*$_Msc~C z1}nw>gC%V~n8UoBANIa6g-NbhDYSEmey@`PN9}{om;TKl&mJ6<^IfammTvgY&@15t zbibm>n!gHyH=^t%qL5YW0@fq-5}t5od6ud9u4W-Jo9WF7d+7JSDv$o6qXd8ok@b?q zf&&r3dBWxyx+PMqKx1rireWTLYWsDsD%*FTKyOTp0;8&0TZJH398*fL6Fyfv#$qM` z;nx@UW7r1i6>mN+EnF$k+bc>t_qo)`4~`^AZdOrxp-q70!ZMhNxT8YAv`9~(QE^bi zfRR$M*6fRfX0A_AbyGgB=Zh#?uVqwnjl3acy7x~;r^;1rh0w>{ALJ6Oz%xf?NiZE@ zR5j)wZUyF0vT~*+SDk#iXnowrkkK5b^x0MaQJ~lcfTbTq$^*lAph@M=Wo@X`$`XOI zlf9t*VH9}_hZr~g4mrf`HeG=sP`Iuw+UwCCYoJ^V1xpQG7k=a7TVveX)VA5@PE7>& zA}Z*7EF|6>FY@?lz(WwE95yny9&|azt16z8>&=SuE)~FYQhL1b_|+#ZOBFRPzPZJD zJ=N6`!s51OLLRjO7|ea0^8m`DPJ7o%(}M2l6BDpnK@vXZDtnvHAGt6d;OqC&Uo|$4xuc9vL-^vCvR-*@=@ophTC-l3J@Q?kHH8YG z?C8W9J7#mn0FDUG!UpRJShO~T$>qcXxH#uy&sjHcAtz=2c@;HM`&J2|Y-{y*9arIm zzLmQ@W@CwGku^QXMvS9C#NcNI0kmW*5%`-9We|@`McCny=Lg%%lg&O|+N1g;B@zV` zR4ubNTn6%11Z<~zwU_Ff(5~--uM-5r4pSes@I&RYy?{*Or@jwz*@?cSSm6;@0^(+J z6>xHvqnF0`i3&rf|HvhQ**UFYA{mAV4KIn;36456ZUQP5V7Zm9PVnV9S12`!LD-S+ zsF^24T%b|;kdMeEyHHL>+7lyT&Onx8Vlgf|XEgZH1OUpUIKYu9h7uV)$BD2C=MpCm zw_CRC-4UlyB4D9dU3WBR58tor)puODV5W%SiDLXmzn@T1iMJznxTTYCwXH536e>5B zC4rr(4A}K@n6tBwQ#K89KTW1y!78_Fl-G`Yz?Peb^n16#fLEfl?-h2rDjfp)Lku zqqymsp9Ny-PU)m15(^GNUTyj$Y@SFiVeH{vcdlT=7XyfoCXWmvQ7Lv8sRh@fY=DJ; zvJUFD*Y-E~oHrjP`1~Na)72auR(Pm|wJ1^W@ATSn=?O8UF`PJHT#%h@NXz_=MtecD zWkXUF(cd-fu9OVj_W%+_o%t?TPc(*`(A<;5gCWUoaiHwicm zsDiN23^ZsI5I3XOCG)tCMa-&CgJ~8ge@Tzw+^fY7ovhBfsJ$V&(k##q>HAeoTaOn} z8zJCNuiO!qKjiU9^`zAc3%h4yLU@ha|w5yi2Hq)#@MZuQOfbvJa4$VuBJ}ypY56XOFmC^lea_kVC(Oh&iw)fM<7zzF_*0q78(*ER)uwPl7Zy zHJ0F=m4JkjlO{Tm7dd$LJv)|L*y|eyp;WGnQm9vp-#r$ZHmMf#00%XR z506!#r41!*8=Q?Jr&aH90`^QJ$g8;~z#j+aIYuGtJ)+;n1AZ^IzM^JV2SeQBXm$+1F7 zRe^pWTlPV7+4ga@m3@n!VeX~L9=?~pa0EN+ryK)PR!jyFpYL}kll{QC|FGhKw`>@J z&SYH!yQVAXp5O%c|IT2QvKK`GpF#z@v>@7VJN2U>Fh+5kI3@+U=?uM@W0F6h=#C0f+WQ>b)+*OBnF&;7zHnnyfUr&jIsw4aAQ994r=l)mqkdq zeoFS6W&S)6Aq@hgYmF#?%NeWo^$4b)oaG^e));s{U+Olbp(z;>B3psTHJ<*?O=b}z zA=;*tYo%kvwK|3l97bq|2D3{Ajm`_YpC5c~x=#VlN)pHm$q&L7$7;zqelj>1R|nH7 zqR5^rRFX(9L=+8&zR;gAuq5i2=LYqhP!5M;TZE7cK;yOeT_~ZfipS zO{UaF#+w3NPCqF)NyAK|It?3efk_O=z6U&C8O6PMJ6NmPm~)_@oj5`zq;UrE zgaQZhUb<9r=i~eImH4{q9DSL6AnAA8mt+IF|cm^s208jE}kIR4pmb`bS= zQ(xuht;-?QWlNBE7NI4F4*Dz2fRbloDM7y;gI%$L|AnSGXzUEF~zc6%-&zoc!|UyU?`hIc4UhgX)@2=B=MYV|E>#^Kg#j@*uNn z$6Mwqjv(&8a~!^G0}y_O_)Y?}w{lvxw!5AQ(^Dv$Op4xGT3-WXQK>b0lBy4PbF(c1 zmfAw!Oscy=avZggZKLKWA5B6G(S57vI%|++q}wTJqv5>H(wkw;bmboEbQJGr(Oh?` z?H!JcLk>$uO#`A-5@srr=0cU@jv>I$l+Zc|<_ADK%1;e|!xVr3dKU(ie|n2z4V?EM zlDs~>x&%z~R%-hU%+(x-$4QeZ=DQV9Sea~Ihj;`=e}7Uhk?Z zZAiCd*d&d#o&>qyH)kA_hBk5Uelj=6RE0hog2Rc+t7}?Sji1_Q+h~G88Ln;+4fiHH zQt1-KCeX9eR_{+SDfjelN=fxBc?GRaaDAvG#+D;`7;fg1aVm*C*0Ta2BgYaC%QbAv%bkfw-V|P{o>*UQ0Z3^;cTn!aO=%=vA@EK z9!zW$RDt(cm0RJb7E>V%1(b4>wTN#v|W`QpW#CYYGZJLNWTJ-0!)_ zGPQHSGo8ye9(VDKQiOZm7fe}k9h1tg$u3A;mkBJIYk1C8rmHLyr1Wbx$vB2`IZYKt zJBVEv=%_w4(}R>S6s%JGa@iWOg1HMGS;G~ZU2)|~lRlBQf01;@?F}ZTGxlb54fyH+ zy5iKdoAOf;(x1o0Ut)Gqmb-w`>v5rzT{#PY)?5va3+E-^)A&yG@}R{M?k{AKX0?VM zg}Rwv817j)nuYtUq|i};4*un0uIWT=9uID@KCGd!vsU>72cc9Kf=cgvyXfRU1+&Q# zf`OK~P^;(rC$^`rhy*&*qdC^u{SKNQ7jL>z7E>-sG}}F|-1Jfz(qX$kr}=Y6M^7Cl zuiFSF8_uXbDZ}uqH3M}poqe-sIMw(}pfOj!JtQTX!ytz;MZMw}D{6F!vY#5sYN7Uc zG&kOO)do)c8e@t}PW#z(p*kZ?z`mv0WYx430{YNoR?E04t~mH$z3;p4rUx3=4YOD| zpXmoE)(yn)jw+PG(*vYYU@zj2Ko~UZF}I(_^>0JLN*hDGHD1b;y}7Ja|e%~<^%+BSogCQF(P?$oy@g6%9ZxSs%1K&-#>Wu0`JkQx`3Mh{FU zUA?9!}(pW0X)r&@I9X1epc2Or|c0FEU$gxG`!Tpw9#~;;itKtp<}?~BElhb zCYBswltiMp%=-?}Jv?mKZFp$)g|c@TZ+rOo@EWJR^L;j%KG&6ECO&-3#4My3&5!|5da3hko0}+Pf=;srABy8({iO`sHSld@kwqWehbgNyQZOV^BFrO}y?NUpW>fXl0_%08vipBF zmhW9wUOC-@%$EkH+O$}xz^@UE*l7-Le1l(2gk1PXf4!so(@uK>k?Petn=kgtSOq(_ z{WF=7HVnh`Q*!lm>4GP}BdfwbVOg%pNMpKyvg)Mz=C3 z!{-2puU~#L#ZPkRnxvmS#)Zh7J%ZAf`V{j_<9P5yIxh^Usu35nZE@?RDt6yq=;4S+iM1b9?N=seUyi+Vg zR4Jb~cCuy|#v9)S-~Hk>xeqi^q-USXHEq39zi%Pj+`nSdem|+KZ~`{yoaxxv(nH!G z+P;V@7}o|EJGH((?WC32ZBVa#oZOk5dS9?$&z0oF+(-4{_xw;58eQ zoNK&J*H>`w3F|Z^jmA%@6E14zI$uVb>Ifq9yT7324crVKSMnxp=7JMSr`_1{Ja@!` z0JSc=@QWHk5&D)~XcQrjo^IDmH}(Sd4rVyzk`qRF7FKXg;8U}iZ9nR3Tv>wt z80kyZExi6J{o^YGf zRL8fH(jnTvzZ~DtC3LQEW1p6?`4~Zzl+Rt2!B82FBYblXOY-?O^!lcaQk`IfkCFcG zcn8!JV0+Bk<+UlF2!PKV`{n?qjTV7Z2kahXy%rbFnmbw!o}b=B;txn3=e% zlPtYP+PQ$*nGEu!*#|4(8%msQG$tI;ff(RC_O|>W??ha2$E*M*ZMw$v`-Tz!c?7(0HD4~?;QR$$Vp}QSa_Nt5~HxPvkE939 zLVf#5BZ`+4qA){iEY`#4Uqb12!G_zNFQZ&~gg{<%<`U@KYGY&Y7O$(HUwFi4*$6=#83dw zoG0l$TRCDD)zNt1)b84f9Kp5;9s@Ux``@1tg_PJC5lM${r+GACfnsu;qrI;R4CG8k za0~P{+?>d?R?H(-P4r@+e@bB82e`O@GD&jF^Kv`Ddd*n?4c9n=Tn@(=W!Q^!Aa`B^R_FAt>u=iyae!uh zFdv>mwX7ET?g4h~Y^$tOW&X>eWDl5%#EMkV@(8Z#vc9Kl}1Q)-LJR zYMh^Ei*`4mDfa$o(3}3FdV=N2sMv+1)6=#VX9U;TNg~c&7^FNe4|I2?_2(d~5Qsk+Am*_eU*4q3ps=6Sd6A;ws>; ztH>&)`>CdB)@h9kOj2@3F7<7kq2~}>FORrS;NKX;7<+eVZCs=!`S!`RSxrc&!hqKL zZeb;5k|5Z{fHJp2yMIWTW+8FQRzWc-r^&kd=^Q@Uda}NkJ&U>^ z4|JmxRp=-9i$USI1kHV`5rXD%XDi9XQs6Ysq(-#UfH{09xbXFev!e!@+e*7n`GKd8 zETX=>{AMR2xkGbkWAj4FJY_L5I&f)aV`U(0VR>b8b7V6zItSwkYvVS-I5ji7qR<}- ze5L!v!6*A?9S^>X{5b5HaxpeJIWjs6DCP*H?`GxNDU+RRIoK+d=8uZW7ZE%xS9z83 zQ|jhq7G(kHbJRN5D)&ALA)M|!?x!z%W-#0JKo&yf?JJ0>9Idi@P8yf?1ER*ZVL_y5 z#h9*W&h(aZs2pnOh-OlPOphPQ_cup{SCR1Rp>eR|_(!vAQWn`$B-cqR-A*KNu%v~a z<6KvjUEku`LjxY7*0-!Hs?kg@KbvY2v1>;M70e){_-+|m|*j=HFHbarGXg1SP#%pn6EC9cBK9TBHqiWwiG!f>_~hPgDJFPBsCKD zfALO$r?{Z1gEBX|r*qv4?4=AE4Z~9Ju(KC%Bl#Fk8YM& zBPb6?vo-|x$HYAgR1_e08}9w|K!KW%2}-lp^7z}@LZJ5LCxC@s1LoVwQo5B$BhX>h zU2BL2k(VF}9<#Lvv{oxx2TtbI?5|RM)8qV6uP4l0v1x<~12{$ox-(N(TZ`QawpP}% z%f(J>mdEW;o+q90B^K$jRt&?V6unV~$JiACjppHhVbHXedO%6;;*OQWIZ7|=BRpa{ zC^L)g4&C8rBU}IcAPaqKDL6eawdVUjz7FaT)@nA96gp*$pCONSmTYaqO`!Wu93i;_ zIY$msw4gcT&wOT3Z+K7X^VpD9IvRzNvjfstcd?W9EJBxK+^v;>9qx)2)om}?(d31u zjccxDXDQ?J==DLiEPjDqHU&B-sG_sx(;oVv0B^}Lgp-!)d22G&g1bETJ0gI`bL@F1M+r4 z7ts;|CI`Il@JZ1@?}F0vMrgV03NmH8g)eo0U8UgGzo3-oh8EG;?yF(u4+1 zqA5V-lBi7oS25`2dwMxaMA(G{sL%gk`jH>_VSW`!G@=>+O|gIo{b5(c!V=87Dx(&_mru%+ zSxpE#Cxq}EeS+-W1!Cb)BDgKrk#%w^FlS6OBU+5V|GaM6MS$_}S$2-b!ei+UXS@-2 zIdpD7Fw$|ta@Z3#JuPCcJQ?oaa_JQaitGJ#ARBdyK@C3rTgzCA8c^6FZ3;DOxb!4Y zIO+z#siS(bX3r`IFyTNA01P@2`HtR9E6uEX_at}o?VHv3QhLJy<$ghIm<=^nKFXh( z4?o2iWyYbiA1-DQ%iZu5Mh_tV_knibYyxA1<2sa+$<;xZ1StOU0~D&HZ+ia+2KoLw zTPir#%SJffe1^x>c6I!&gjVYuKZgkMRH>?#Z}ydvxF;KYethg;=M=2<`y){QW_SKD zLbHCh8bMud&+=jeUah@z++;FtlhD)-C7R2vymF8*O&x)Mp}Ywy)izFZ6w{p$+HDet z4$RD&xa7`Wy&db~11lr{>fk@Q+SmETOa{=6izBSMv#DU?fJ z*uPPlOk8fEj>LGLfcisclD?ZIdSoYhRdAvSY)^)*xX$?W(@#>6Vmd$ ziffKWJ+MtmYD%XbA0Lhv5gag1aeB*9PoUE#m@fFopoLD7>8Ec~i-O+W#y*~xt z5DxH$)U0%S&?ukoNHXrxcX<;e=l2^!OzRO-5dZ_>0nO!br-+r@^w%n|p}#==6@Gf} zmG}ts<)%~KZ}Yu)!?I1EeI~Ptp+1*w2$Cq@Y0eDU8C#eVu25~C5J0Xw;<8(TEgHkb zBpOwbJO$hbBWIr$PsZi-5OI5GcBzyF_7Nn&pj5$VX}sO(otT+R8amX0EdwkR>!#E- z2z((GL1FnhaAix0uP+(i^SN*k{oxlC4Ro({u(OPdu0aiWPkA&=|SY?ca^}y%J*zj-kb^$FeNxw&~yjdO^AdUWSyvVkR$X>|3gKCF9b4l~iVpAAz#BhG?2k87qKI-kfyCX&XJJSfp8N*!y-zGajHIJq!Ef*&o43pZ; zg$Tv!?>`eSgVTx2Bkzv|B#A#eDEwBkG})F?1oat-1dY##MIUc^8L&gzKerT7|1Y$Y zwNx_MWlSe4H2mowbt;Tug`@}3L%2QjE_1h6kU(y8dgSrT-_m?^8ewEtErO1T*XCH3OAS9O{7HM?%OQ?Ii0= zm^afp!A{QnWefq}C>mLM68{W}uHD%ciEA4jtn!{A&=<@&oB(!iO)x{#J!m&ttC(wA z)1B)>schq56(|Wx>5!+~-NXhFXRF>x`FvY%2nHy(*`}WWwT64Wt%FMgt!Pej=R3ul;Rd4{~A=t{{K_~PWHDsYIv}41rKjt(J3}-t$E^^T+$@)S7eoH*320o zU5PgkaUQwY9{(J5A2H5kwL0pyatp>l-5cLta+Fj^=mm}%weUFE0;bo=YXk-%BR5e&PQ6KOPWs0@YvT4+wb&b6239@+r(yqlv$E70&kBe6R&Oez7yDPe1ypZ%{l!A{% zhyfH4U^mAY213Ex53Iq$UBd}}O?`98?NaC~^ zHI5Vk?e+N1HCG{9+F}jBS=@SRy{LLsnz;*`_`JUC@F_B~>htGoS`w_J)H_%shO+6B z-I1UBxIAR4JdTL)wQF&#^DEhwiipC`pXq`exNGq7JE4>#!`?kJ14%)^B*X{E81mA> zjbpgKaiw0ACenHL`;1;bbtk{wA0})G=?Od0=$uH8UM&8p`>GcC`%Dx{NjsNoFEdA# z=UVETe03=gycqzyUbPqYMZ`C@WB4ZLf9Z0#a20&)>JY6oP54X;h&JdnqghCiz7Wp7 za5Q-%Pc!Ko0TB5c7PgyafuNe02*K{YIWT*>@KE&K*g~YKd^@Un@ICO}$c?~JeGCGd z7(xvWjjI37ZrCob2#7l#>HZ|sCtKxh^0By<8W2<{>vq6@ z#uisZpiJw%wcch(ks5;)Q$aJDV@+AFoX1rue++0oh_-0F1D#a9L$farMKs(}>sHc1 z@LU&M5dp6LwiymVe3Ri@a0umCRI3amZAhfwbMFvSZ^);Uzl9`C(jD5KGr%=&?Vy=F zUz)OCe2y=p4+;Af~%7L{Rmk#Cx~?1w}1^Vl8ZUe7x{h@5CQZ28OT>-uqg;) zrTc)i%AU2HYcL zq?NBAY`y#!{>Y0<4{e0#k;Q0;7R$GypcKVQhIJ3htp*)!n&I+(Kz^hB(LGp`_yCp$ z89Rw>v$4`HL?sGP?^^voVv3QM0+~A#o$Xui{OPfST>1tVgN6tO7sLhQA)|33U~U4L zDI;01Pu#38V2W0KvQ1=II^K`TfUa{7Y&~<$Igb~E!Qgb<_OB4yZ#xjFt@9Xfrf_r$ zjFQnY$}xOz7flF&vsxzh>v4f^UKoujH2M>^VPS67@cyi!2lfJ8z<_};2?2SDIDCdJ z#ZpA?%I!VUGI{iy;AT2HHf&u)D?WA?<8wN zj^m*e!&g?K*$9|8qR}2!%EuoUut3&AAc+gVa<2^xrmRT5(EzA~>-qECF0qt4ls6h| zO!0NJUiJp1*3_K3jF_2i+{n0!9VRX00Ye93(VKj-9}Y((kUs63-^Q0I418U2GS!UL z!mkW-1fS|T^0Ivjn^J87ZA>nd&h+g%@W-iMv6mz2B+>~D+nd|i?g{S1Fz4Fv8_amS zgOi!j#_~8)C5Feafllo;4mF*rZUQ0pH%@@)l(Md?HI^?dCJTL^MG6QNqF7%+oyqTQ zc6tUBJ9}FHAbs*{i8xb3o}Bu?8)W$cL|X_in{}>tTF!1nCl*EovV^~KvkF)9M~=AT zPUF<&RlO$z{c0;vF=dir>7jGjv8_GkmI=NzXw`Ky?B4?K9?Dl4U}4jBf{^$iJ*m5D zk@ceiZVa{~a?@y(I8^m#8A#|K^P!2bH7IWtmAa!rXd?Pftog-rwBa+`n2&FT!s2@A}p`#{r;)dXD{MXM0RuZ)fBu z+=5h?MGY|re|4wF)!_}}T?RCT@wT4=>MPepN`Td`LGlf#{#R>% z$cT7O=b&Ee#CVQNZcJ_s7ATk2bYb$++TYpH3)KZu1x)-Q8fs+rk>|7TzPDN2g8JgN zaVAFUuV<1vaS0gz3o(~xU7e7gTHiGbK)nVt&ETENEDkGq#p?TaY+xO ztl>U(0F+qzq@Bvqj72t$pDe6kLTO0aerpz`tByk~A-LT$m zE;IprS`@;dVlucaQKSkR6p_`|lgb}H-Hr+sOR z(I(f%1Gu?%N(*6vP4$>u^1#uux{tDzZBecCU!1%z4cZf zTUBIgFBZ%_(;01XNiAvDh0+>T6pR$X7igYd0kQxJyrAxX;G)VzD1nrQz6V#6 zO2g*|s{&H96&tY}{OzqIwR%9Wh+f&f_lfzC=~;MIAh7Y zu~&+n_McifiXIKsEX2cr-F=x9OTMGX%d(oY(2YH>9co!U^OV=f>Q7hUjp#m+8fI-I z_$XjU88lH&a1Sz(6*h=d1j|O5PJxIiU?DK(v#e;yYY;R^newlJ_n2t*1BXdMvi#zG zEoz5X5YDRA+b}IijK+CfyX!zZ1fqz%T8vi?fCv3$SW2w_(EL`8O4}r8JgN!kA&{y%U-hKora&Z`{h@v8*Lh8p%F-mUq9piAXw$?5O{`gfUuL;G%Qf9o~9gf<5Pfe4SY8O`+Z?fw{gfB5PFggS7jZ#@+Me!*& z@MTQZS}>&rrv8oLDXNn`$xW-`4sv;HEl8ty^W2+Eg7cf^DK_My4LW990v!(vrdR2o zzYVnZ{uu*qWq$HOGh|R!9Q7VXlwxOgBwH88xe$=DDLZD$`-WxGpJ9Z^n-$JDwmTR! z?GE!=p=6D^)bG75H}X0Pmdh7Xd8LyU3E_duJ*TI;c{ zJ*0PKzA*8vJ-WYR+blaQAS0E6;<+R&yuN0Zoz39c!AThqEOh!R(J zv>jFPhZy7~^>gIcIsly#PIj8>+G$fs#N=ppS#h*zyfT@O_4AYbFg@`0+qE&tYpq}# zP?4brz%H#q{x4h^3I4ih7lAmsdwgv=orVAoZIlP!J$c^kpAG}JNCti&a>KGWjYO~lS; zixk{hzBK9R6Lui=%iER_K=>T;oVwSDc)x!dLG7owk){jsD4K0Fbv}cyV>I%U%-!QK z8SWZd{R{JU(XZs!mOpXIo8}jx#BJA&L{q*&Y-KD)zFXxE>y5OQfa47a zpDY}aEF4K3)KcRJPcO=c)J$^K>o%WOWShNUt{^uczEST(a~9tzbgiA!(N3cU-gV08 z`|SP2SpAN9SV2m}$n*?}xvV5E91*T@zewpd>%s6tamxx}Qug#uaD8oE8)JU6pGJ$N z=ch`BeLvDoKin+{Sbk?S=mln)$-d70=ca5}EgH1{RrX_A)di@2-IgQlhrs<0qf{m= z*7B3y|H>zap9VSp2FQ*hrCDPMZ z9}Ig7v1g^WX6G*#{pFteceLqm-JVB-x59%A!IN(J$$%jEK7o7nZLuF&!xx*zPaEyc&N)S@|&WyQQxPc}q} zBNOdrSEi}IYjgQ99v2ZLa%D)u0SI+c15hsd;**0)kK0T!T;RF9VK5RwSm#7f)>oQo zuVvqGBPF!dEY7v=>e(`UkI}6 zd-z4qC|6(zf$^eSDMXK{`|@a7fHwG@v!VyfxQa}Zo5msBDFhbVQ0>yP>Xz+~TjK>0 z9sOjG&|BVhm6&|)Nf8sK(f2~8CX-tAt0bwnXDMn4!{Zvbcw7B=`KA_D@f!?c4PhEG zmvfMi5K}|5OQ=gdbo(uVbAE?s$lHtM$Dhb;(}(3dmRWi7og_5qtMX*cPLF5pyp_0JI7pASY*BoXiGTj8hZx<>7ofJ^A}OXSF;#S)eUZ01X&w}-1) z@r2{}=fKlByv`>v9Y*=cp}cEhcx2{7H~sC3g7e*Wf09TvY*BQre|C#axX46X+Y}F( zB(FdTWYRJgL305Vc!!ydGgHoJp1Ot#iWlDV7f0BH0&9(crvVa06vYnqch*qlSZrSK zl!xbE=ytv&1X4-Kr&n}ykf*cVLT1}Oq%o;@hRe}FOm>}$?FXM8iW}FOVZ@aw-D=Gc z=iSd(gP6)oUm>g5cC~%N=D-kEcx8>IlF8ta^c3H*Xoiy|fj2Y*C#%n?L-uF1 z3qZ}ss2a=5gp;{=9l%_yuCS;kHb3{kOOr@FNd6X18fhL3zpCDsFM9?(<og*H5l==hw(geZI;EVkb*yjuKhUopx=d!NiUeQ0rCAHHe6#VG zhfngrFnyTeNmS8~G)y>pl9(lsfGxfMYxTx@eJ$4_!--yH4D%iS4&ude`_FEIBZLI0$4C%&ZaBSDfCrVI1$>H&o zMRvEPf#N}54vGwJjT8=L`2YWBJ##(G+W`EC4s!U;ro{$s`=>7f&2qgMo4w!$(SAKg zeEIu8f%`P>1G`9!N7?K|0??N>g$=(se_?Z+VZYaZhS_)+H|Fwh3f)=Lk$*Le_N3Y|Hle`SqXu}AtQDMdQB5UP25{%V1%;koAS zT>=HDQEC79m+2PVhPYwFiXz@h7Uw_eJ+@{9e?vcM3_IKxI}c@x5t^MRQ#B4=*VDLn zKGvY_DWOYF-30|md+2*r)&m}#6ZCT(c!SX5PFe#F73QU_+@Z_t;Gy_ZuSqUn>03I2 zw59FK{7rff4@o>QZ5v92)eKG>=#*sv25H{T<8J?dt#Qh9-Uu0opFdCweEBmZG9wC; z@Z%(+drc95sYUFpAUy6Z>v426#|VgFEV+q>V3gSV=Y;#zfa%Z8u-ZLd9CRiG_Q<}2 zvgH(qYiJ31ITm$t4^2*D=dpi~MZ?Z@y%C8Z4h{3dKBnl({kx5L>IqXA)CM93NJeg%zwsR{Qgo5XQ zOJPJ?fu3t`qObz(Nyh*}Z&QSlBh3bw76X=Z^ngQ&2hCW`=<4pY?+TQ()ote@BPhh8 zs8KS^zz;|#VM9p(&O{1r6YD@tu4Xa`la$#gn5LkKil#xz#`|u9HE-Kbe>Iz~K5BD) zdvuk{?Y5*Fk0s?Vk|R^SJjb6rtye&l@Fvp&>132oJqFeKV1fw&!#r)t0?SK)JiSX2 zM-^PV`59PHx0l5Icp2MA;m9qUzkzE-?60;*&NJ@qs_~ZWFg8igVGc;ruxoUrukhM} zm4Q9Mh!kUQ07Gp3wb+#`r&ZL-ab(?cV+zb;Jx84YK%#EKk_ZD zbT1$62O3YL6o~mYaXEppU1x=0iGR(z558hsi9HO)L_ z0ByYyp>Nb;(zx{n6RFOl4PgO!=ZC%Wpa# zY-UQ+uuzVO2l-taQ5CwQj*k3a+H#vbrGfH-7jyVVIk3Rb_5n2F<*scNmT^9Cfj=^| z&Eh!VX~il-#gFkY&P%rqt0rSNd_@#l!uYAm24bESMRFF-h{lhM68HFl}r9iZCP$OhWtxnN2LE+BnWIV@yVR*DR1f;dLu z6mUJ;q^!i42epm;>8&+u+}$m7T#=>Rg|jZH3_H^B8wP-dnubw{r|uAh%Wd#Mi(1e) zr^k!WEekv5!XOw3VRAs*d1KF0|9$+|m*^gNFDGw#6}rA$l|`Q}8O2YS{2{ur!wyf6 z=B=r$2K*XcM6Boer40P(jb->c2H)Ay;eU3UpDI?%y*#tE%0EghCQRNs#M>MGvFBf> z$^}0c!JL|%5@wh(nz*^~w?L6w^m~L5e*us^@1j^SX$SEX9M$gYQnZb5vuJv_al{fM!Fw6UDK6Zwr;C2ZTN0 zQG!@NO#0TZ1cjGBS{ELuc$@@i{FYAO_sBZ$#?=EYqHH@4LGN{2ZKYuRl@}Oxy~m8+ z(n!RYzmkW`saeXKXj zbz>!761F&cR&V|j;Q9fc@z_+hpX|6DV_OtQ6)AFdN_2x@`U#medX?2QhRowFa}H}u z-Pbkt2Ruy#hwk+s@y4S>ey{dg+WhIzLk@slxu(0ZsNzcWc1xrL#|s*Cy++mSS|LPk zEgR~}=rR~zL_S;cYQWAEgScl|%&}Jjy3N?` z`5o)y?m7Mqf+I=50V01MecA?INF?P~BQ8BRK*NyXpk(Glfo4|yKQ%MFFclkcCy4C0 zJ4AL?QHzyD-{rR+U5CZ|wO9nRgA5zjHXnyV5Ktw6_(mVju(YXO!hCp^csvk}xfw-r zZgyu;owzNsuxK6xd`Mq4cH`P9RQ=CwZ^C*ZUpZA@e{S^|>e2S35q>3EyA1C+1R=Ux z^WWsp0VG;J+)3&JEkPYVCb}$!OUQSv1qF#)%-Fl{6ckGwSc#OMxFtbfhHHJfKGXyI zTMZY{6AolctJ8A-2TUWS4YHw=G-X3WX_??+y2{kvI}n;?X*B{1GaaA}z!n%h$m9n) zVkHJHmwzXeVY5JK+RP)|xB_ZUL8^rMmsN#LkkUoX>#;|*6CH^+K(VE-?lE(xHaX6K zy3O~&$4?Krqu$uC-eoIA^H%VJ9c{jZ>_m4biQw-j8TzXP`26Xy*{JAeSr5eSmSC3#0jDk+R630Be!FSRp5U0;0_hrf z3wFK)a6Qt+kE=vc_HnRrwE0;@OKh^tsaCL?t}n?FO#5~@fL_&z&7!aLU4qPk7%yi}TMcSzT1 zL#tci>AX=D&?#q^C{ptMfswgAdBNA0X2T7O`9@t6C@?}FH0+i{ zR}iYml-jSSCAcp7gRx=3T^;B?{vuG2@A~2Z!0|g`GyB`}^EW!ZIvmX;F@z~+dYhm* z>xyZMA06 zrylj^Kk7&sD-=Go!LIU6U%C`Lh_P>QbaNeNDlH=1h}nD>zSIQI@AgAq6a7Kf_a2Qs z)HtDU|nygcfTE%7dg4&NaX<#IPqc8mx&XLwDkIz#8f-l%`@pWqwMisZQ1lE$(T zb6%s*9^Qs;j%K)As^Jb0V;nXC{%tp0@+!JDy*!po z7S!*|1F-OLlo5TB8wEJCO%rJe=y#w`4Bb#$A|}69ceu7L|C?Z%xZjx=V zvv50cT1ONbP0|#iosLQsxy1HU7rAI}-uj`I=W;~|Rj#pVvk^8R|6UV<{}Y}{8AK*O zjM9e8g+wKyrXK?+Mmt?ii<{+L;fwBl3_+^*tS344S{!GZ7@B8A%00k`aG#ngqq2D5 zpWX%w6F^h}EF#<~ zBjecDW@ckg#>dspYf{oF?{S+jykASmS#mok^_`bvahLho6x;X2rTCX?p56I^fgtm; zT_T`|>EY^i8b^Qko@SitU!^tiN0Rt)r<-rwp}J1*S&opc%3E?SGSacP>~jQmangw~ ztQdb+dh%`LrR^_6DVp>zR03heyqpVbAz#r=Rzyt#2eH`OQTpu67Ay~}P@g0R1%p6J zSI=TCO*iDZMv3@d4=fE;PH(MC>I+lv!JXNtP_`WH~mlLB1QT6;!)pa3baQkKWf+TF&6 zUwLGce2b1zwTnM!1=CY$=Q72!nn^fyxeUBQX}pgp3-moy`j8FC7aeLhpkVsf!-GVq zUT83pEw`dpT&pLHi4y^La_zesTgUhswXv`7=}X8UPK|cLrbd6e`8D)w>>%r>qzMoY z?1)|~ch4wgDYCb@pPTG?1j8*@)R*h*jfO9-z&jmcYoH?!{jClGtnBhARx84nGrwz` zmFsrE@yPkI9{Et<$QaY3e#W}TU*zaOhR1EbYBNS*7~WB?jNiwpiu%M;h zt~Y@3`pIS2DpLyht*rI6!3sW7y+oBBUF05QHX$e)I^4XnkgdD{y#-aFen`dLp+dCj zd5|*bB^dAqW%XLW6o@(Zd~|@!2?^l>v=9&H9Ph$X0rwIljPJ6GtHJJ^*?e){U;%pk zRnW{(Q#bNk_rpOv1%;gJOyc)2F8A9T`e%<1wSZG5OD*UxZB{O>0fuEwoV=uj1Ok8W zqhl(2rr7YZwe`b+8E38dq;p=?T`&d8F-3JK$RwP5|2(2twIi69j84Ks=)rU>PcmCx zk3FdR?(T`TuhSx0f!IHX-D`Ni`r!}I-XI-W(TsFfva?gj={)mx&e$TFoGdxMHjPcE!B%7utjdJ@WQPy6c+8- zkMxHO{IcfekRfuV`VkDHnn|CVE*|dg{XhS5+a;q?C~U`RU9Ycww`|;w-(){Q7TMX# z>Mp|$Y;AWZfjkk~PKC*U&kTe^HcE!&5c9YK83OzyOD9;c6;Q;O{Gxkxe?kpCz>Omp94!5+%KSz78Vo2H6WzQE%4(mCS8#Oki@x7Gk->h$bdq ztA|ZvY#q2B(i3>rYIQnj$Ydvxv3W5}LQiRVQSzSct{eJJqj!4<<=Jt5P(0YEu)4;n@q4_pqK{T zhrbKU5k~WAE+g-nq(_+8n*X(bl2AWaqg4^r5%*w#7vWvsBUPBCV-+fi*ahn4Ny9Y% z^7F3N_%7)zn5vl5N0aF~IvN2xd=B=D7*p4IQZ+u{M1fL(h%TvHHUp zP522GYdlVV!97g)yDC&AQ#kpk67%<($`~DaFDA$Gyi%`93vw{u!TV) z6+t3_S1XR6Qtz^EV6;YSY+(MFAz-NR9nZuUpp@x`s#{CVfGe7EEn%K@W|bfch}lh))vJ$CF{=G59g%$bCnmKD_Ko; z?3 zPvij%o^WlGWg2V`dP%$*QcGeN0s#cnF*8j#Sga)@RP~`4s|X<_H!qbn$zw@a-D^_M z;!I+lY^vr0{5u%Lm1TBJCX*8Ga~F~LZTRh;L^ctpzrqd>ahs7P3H$!B{K@PNU6YfM z(YrV5T-73+85L4LHVD1S)H}yOgaqqQg6=q>rYQj@vqv`k?4o6ts{TizXLbG6#!nFT zWP~-B9SWIz%+7x8p&z(4e`)%&=o@Emq=2vw|9ZpjC~-RtdIWSs9{u8vOu;0!EnGUX zu?Iux-H`2YH0Q~YOqQ9m9W(iIvvMf9o`>hybB-5XVc=euw6sdsj2m46=SNQkeK6K% zSgM4g1{$5x`+sJmwRu20Ig9WaXmwakap}?Sy-o@T1wB1pd1kCh znQmW*=WQoEumCfSTC>``2X#l8pE;psA(F^x%lr(JNBfV-9i;U(^i;dHGNHcdAj_- z7{jWuwNbo)7xy{jxr*eH10#n18d|3>cHq96G&vn)=Wt*lvh(dkT;(uy5bP5IFmgEb znM{;55h5Nsx%5MCNjfV&nI3>!UE%;KK-Rw{yzI+LfG%#&0@b$|f?UGdjzYVzFWkhbq#k#T>tZ=*xhjZCl~U2ulG^|ESz$kgEp23ALEs{dO=QnHSktKa*--Ut(@ z>PtbKYEH3C`fZ6VGzZn4+YNy@y9pw`UTSy4jGv}6E4npZ8rjHvQzm@?*q;nRXAtkRKfl4tVXXMx2Xkz6<>CfA zmoeq(AE<(fig>b%P7IlXx$95M-;5eEckK!Au~&UOi{%bpT73?!80+`C+tB6+bpZ8g2~^9LU-1mL(OWGCX%O8gN2c6(SDVbakJz z6&G!5;F@M~*b&TTW=6>O?+g6ETFdSqgy9;4*gRW<+Y3EGAgNkY`#D9mR@al%?^N`D zXx7|lWzPjCqgj(^OJe0$AA#G6pp&SsMWGrQ$Mf`Gjebh-x3R{~nr5H-?jxRp@iC<- zjn`#XdZ?&%f(~xoY)Bvr4f1%Hm>(BL4RW_gz# zfjGW}H*DA$u>zdSm9U6px`M4CN{x>)Wi{@s&=fAi1?>(ywcpg@6_Cz;jYvvkb6T2Q z#3acX0Il4xBDV;H{Lm_uZi;qK(5k?F_Mxe07efS0bw(STrvj`&^a$c!lC!g_b9hFK zk@d=4bk|)J1&p0S;z*>!@Nft1w8qT~QODQQ9=0bNser&&i99IoAMW#V zfj?dnXMrU(gLzi+8JzG?wuu!>zS&6GYU?i7roIEVfmVB&TkLn0rv{>xn1ci zkJAY@&12rEkeCDSg2TLoe@S`jyIJx+D8y3_%1jeVdq+XDYft>`CuWy#Gu+~0A({mC zB27|sf>fx_4hAKllS-w>0eq7zBxir9`yQ6gmHR?Tj^QgTs=V@6!8Iy=T_A!cphXx-~?pYBPS^3V^NY@bAyTB;-fA^w*yb{@oY{ECQ@L9yXR!_ zw3Y-tZspPi;V~H1z81-~NG@eRk-QoQxQ^sU&}FDmDL*2|+C`rAF_D%|#NA<5_nYq5lKdam{LLF#Ca-A3 zepsrN;tc^?Ubv4VdWJy~wmuW>4}^NbcmUl1MtYu|H(rjNX-4B{X9<54!ZVl-ET1qH z{*r7qLGJSj!_sQ?1xR!~$zgyC7;|ux@apt31JRoco!Km{C0RtG5N-zA!2}C2*vA*# z6riqa$>{4prYxuv5qCwQjwQ+Bgc!IUUKVoP^xHK%76~um?-WChEj$YGAk9tLW)ten zZUr(Ka=o48?taJ8qc)UW6GfpUK+MK+H>V<^t_d%{Z->~z^(d(_%K=8t-xPZ2a2f|R z!I<&Y%r+w4!-oysZ&tL)q5pkQ4}U*@|M7N3HP!rtKMjOC95zL626>kB_iYQQbP5P+ zFwiFaH{qd7MfU?)>Ujshe~RQ0jhWzpWW|6L5kVMP zcv6G(*0h$Y4M6nKzZ9+Vd}BQ9+B}Y(w<7+!_*fi9fB(zw9tG5LWox~w*^fKKPyBWi}18U$B z4LysQH+Z5Oa8YR8{LX$fZaa|-oxPBa0Lc~IOJH59kRxqLgrMKG7PmY)9osFu_XedX z5(3DBi-Ot@LU1^hh(6(94YN6C|y8)~i{oP}}? z*Cq#o+U(n{+T6|23>{Ttd?r;4fy-nNzOz?Hv;Dye;m(Or4~D*#yb@{SV$Tb3N7U0u zs*z=Q+I@I#{io8WX?51SC<{?2V$3j>ohE6}3C7^|d zFniYp*S-zr3p4{v$mxrq=&Z>P1cWdjI>C!K?LG_I;mRm8S>eR z29Wzw2Uku3k;itfxd)I;AXHbpFvxdIh@Q)PDV_|}^59j-@h`lQWnH`rS8l@SGB86x zmIjU`v7y9F+*1HUwj%2B@O4UpKfI%D>~k!4p(80mnH9!)%I<@XD^!>7TNNU@+Xt8t zt%GoD9(BwE;=c-$>d? z?&Fx(8|lA;7g7MvAoz~w2|(V{IUS~PU*J-RByM|kM~T~*VjqhN!9SvB(O9kbunu{W zjV|aC0ZB{IO7D)nf}1cR<*zvacc^j4E)XO(w|=4PTx5qaX0_iltBiO_etVkYZ>^tM zVa?}+4rsgs3Gf#obgiE?mgRRDx9aX=DMt_zUz~}v-D0TWQyB18N^b{Gt^;c2{7?Hh z<>sso*P!G()GuHq`suoonM(S|Ti3^@bWa$!<+OHc0wpv*iAcOu`J3cpyHEe)!*Vk- zbGa|!4@^xU+jfCoZ$A*7(^F;wkBl2tlgOG$hX15ialzag#f@W!nAF$1bB3M!l>mLb ziCJf7tba^?ASbj^EqR<~{HhcoolxcCb0)z`IgCVyTRqXX7)5!2tGShRGJGq$>@?TF z>sU1w?Di;nqeMtZvXzrQ57UwZQxm!lb#-M3f;q#Z3USsldy5(Gxru=UloMl{AT(iC zHS;2}yZV{62}R#jNvKPEK`s4QpSjAaMp##oVs7J53%3*3XBKe0Lw_B zkd_+ln+_7PG^qcbeUL9V|KDUp;@`L^GDPfpk;Xh_J=YdTL@*T5lZhcH5uifUuapCt z4WC#b4oHvM@CGD5Ne(PvHjpd%J_J(-f&!dm6#)2pJ$$(hSI|h(p6&(9!u~DY z2O9JLObK=jA;8n|%56qL%a%xTC+Cks#ZUI=C#SdMoIZwtl?a1u$LNH$e}r$9cC=P)Aq9u;y=dFK@GA*OeqQ?alyMIgT)7&$9)dnncXLu zrv2b+HH$drKYoW5!RdPS)wp@#Er^F9F!ehh8wP8Bt6+($9v&|i#W(o1jk@jI-b5!eee?H$u(p(0mpv$5rx3TcK zlfx231H(etuulzIolWf`|fd4uMTx{IpK$uKRlSm*N*H(UK^|Axii!2N5TU-83 z72|>S45-d6GX6Dq|C@V5m7nCqfu@z{W1IwAWxBjuBRVg%o_ys_(P!LUNn#bOFczTb zH;aI2L@@%DtQcVo`+4D$K{5bfGAvwDMS{m3H6(>j1BWIN zmEJocRynSg%trg~;+%u1bkPh*wZu$wL=!)}dP)NE=94u6Vu?u_VskbK{ z$iWD>?At2bU%A6X_J>`{Sfr|<|0jKit4{2mO%CDEU+#E42JW?wr7B;|Ee|9kV`ZSm%Fe;9CG>C&jKqYp4!Kjhq z&9OgPgwQr|)?;5Vf_8knMmADSu*Qm%-;v!+@m6^rf(F#NX}^2$%X&rpzLNKl&c$OQ z?3-06J6Yz7g#Q*H@A~J*q!%Y089FCxgD9sqPx|jPn%a07AUhQRz{>`IxE(&TUx$Yo- zhU9mwGG)da7f9cUaZ~mG>$7NnN8TY9kwuQVG-bTntI$z~2x&=X67p2|G)M;gv|8smAc>lld$Rf5A_rie^3E5(u((Ek0u3l8i_EvQ$`K z*Z{=tN;~t#I+HA>&i#dd7!6TQ8+}e9O*m_&w^zx+Ilwvno6hkeL{K&OkRD#{Koa+q zwvwTc1orIWPxdf!#BgRd0DYLkNBU!2~pCzzyb=HgwXe+_CQ|> zqjOTYrg$3;)9`)0-ZOgG)WV$BX1z=}Bh`OJpZ{da*TTXGV9aXlC*tiM*e| z1SE%itH%N2;BM(ATC{o5{va^ACc`4S40qku2(?B_0POc@S*Y%=Fq6nZgr^qMvQ9BjC zWJxJo3X*~70V^bU`?QMRodG5QjXp!5wmU^rB3PqkqTf!T*&_gaIVl&n4qvLta9cc zQ}N_%W^v?lo$MoYB+LzPc`!C1YF7juWIP{l<;HUAQN!1jBKq4a@owRtXQz+ZKvZ@P*CW7?@?>J|Yo7JC-WE zu{V4^1I7^tTz_|XIyyFmnKG|y>oI6-$+>~e5*8+V?D!V#?nUpHRJc}hZ4;ofl~`1* zsuflH&BJ>IM>4QrTO510IT zfYof-UE61~0^yr#ah^IqjgAOKwnDtP{t#ZOReK8dma^pi*2sSWr=OTc$5=ryZLC8F zuE*-vbMMb^D^;ieSkvHuW*vg*+h)yqqpRYp8I{x9pv}Lxv zuUUe|TJV-g8OrS}w}b4EzplzBWxj+ilc(7Im(ggFP`ohqWWH`fsxj@iBl2I%HIl(` z8Q-JxCPui}e~P3%gZh-qvPSrh7XdfGK@e}1$Hg?7CewB_L9hqrlQw|T{mdbTZ~KHd)zVv>Q*E`~NnMQz zIXVR<^K)o~M*MiuM5g%e-QPqbC=^W=r85sHfqL)te*4JaNnZ1a_4e|GidW(cq>Y)O+n2pQV7F|SP zt_I7R+%g4cmrNPD2It_3K6?UwlY|B`aOIXK21opsD%`dr#%C>)YG!q)8*`?7 zdxMl!XV7FY;4k>RDvqCGSRx_Y;?`(##-;P!YXKPu;UwD${+iWIyobspN*9~HSH>I3y!T)42kN`-)9D$MO>|q^< zI&|oR-T_F+qj%}1*05~s=?-!pSbMtkJl x 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 literal 0 HcmV?d00001 diff --git a/test_torrent/bittorrent-v2-test.torrent b/test_torrent/bittorrent-v2-test.torrent new file mode 100644 index 0000000000000000000000000000000000000000..8ad4c7eab07617e0c2b5c53df29c58d629cf3566 GIT binary patch literal 13592 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 literal 0 HcmV?d00001 From 008147f8cdfc6fb5d0bdaeef54fb19287146e721 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Sun, 8 Dec 2024 12:49:25 +0100 Subject: [PATCH 22/46] Fix: text doesn't fit inside MessageBox The caption can only have limitted length Better to avoid using caption in MessageBox only using the inside text --- source/code/main.pas | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/source/code/main.pas b/source/code/main.pas index 860a5bb..8b13ee6 100644 --- a/source/code/main.pas +++ b/source/code/main.pas @@ -168,7 +168,7 @@ TFormTrackerModify = class(TForm) FControlerGridTorrentData: TControlerGridTorrentData; function CheckForAnnounce(const TrackerURL: utf8string): boolean; procedure AppendTrackersToMemoNewTrackers(TrackerList: TStringList); - procedure ShowUserErrorMessage(const ErrorText: string; const FormText: string = ''); + procedure ShowUserErrorMessage(ErrorText: string; const FormText: string = ''); function TrackerWithURLAndAnnounce(const TrackerURL: utf8string): boolean; procedure UpdateTorrent; procedure ShowHourGlassCursor(HourGlass: boolean); @@ -538,7 +538,7 @@ function TFormTrackerModify.CheckForAnnounce(const TrackerURL: utf8string): bool (not WebTorrentTrackerURL(TrackerURL)) and (not FDragAndDropStartUp); end; -procedure TFormTrackerModify.ShowUserErrorMessage(const ErrorText: string; +procedure TFormTrackerModify.ShowUserErrorMessage(ErrorText: string; const FormText: string); begin if FConsoleMode then @@ -550,10 +550,9 @@ procedure TFormTrackerModify.ShowUserErrorMessage(const ErrorText: string; end else begin - if FormText = '' then - Application.MessageBox(PChar(@ErrorText[1]), '', MB_ICONERROR) - else - Application.MessageBox(PChar(@ErrorText[1]), PChar(@FormText[1]), MB_ICONERROR); + if FormText <> '' then + ErrorText := FormText + sLineBreak + ErrorText; + Application.MessageBox(PChar(@ErrorText[1]), '', MB_ICONERROR); end; end; @@ -621,7 +620,6 @@ procedure TFormTrackerModify.UpdateTorrent; Reply, BoxStyle, i, CountTrackers: integer; PopUpMenuStr: string; SomeFilesCannotBeWriten, SomeFilesAreReadOnly, AllFilesAreReadBackCorrectly: boolean; - begin //Update all the torrent files. @@ -639,8 +637,8 @@ procedure TFormTrackerModify.UpdateTorrent; begin //Warn user before updating the torrent BoxStyle := MB_ICONWARNING + MB_OKCANCEL; - Reply := Application.MessageBox('Warning: There is no undo.', - 'Torrent files will be change!', BoxStyle); + Reply := Application.MessageBox('Torrent files will be change!' + + sLineBreak + 'Warning: There is no undo.', '', BoxStyle); if Reply <> idOk then begin ShowHourGlassCursor(True); @@ -679,9 +677,9 @@ procedure TFormTrackerModify.UpdateTorrent; if not FConsoleMode and (CountTrackers = 0) then begin //Torrent without a tracker is posible. But is this what the user realy want? a DHT torrent. BoxStyle := MB_ICONWARNING + MB_OKCANCEL; - Reply := Application.MessageBox( - 'Warning: Create torrent file without any URL of the tracker?', - 'There are no Trackers selected!', BoxStyle); + Reply := Application.MessageBox('There are no Trackers selected!' + + sLineBreak + 'Warning: Create torrent file without any URL of the tracker?', + '', BoxStyle); if Reply <> idOk then begin ShowHourGlassCursor(False); @@ -1317,9 +1315,9 @@ procedure TFormTrackerModify.MenuTrackersAllTorrentArePublicPrivateClick( i: integer; begin //Warn user about torrent Hash. - if Application.MessageBox( + if Application.MessageBox('Are you sure!' + sLineBreak + 'Warning: Changing the public/private torrent flag will change the info hash.', - 'Are you sure!', MB_ICONWARNING + MB_OKCANCEL) <> idOk then + '', MB_ICONWARNING + MB_OKCANCEL) <> idOk then exit; //Set all the trackers publick/private CheckBoxRemoveAllSourceTag ON or OFF @@ -1422,7 +1420,6 @@ procedure TFormTrackerModify. end; procedure TFormTrackerModify.MenuUpdateTorrentAddBeforeRemoveNewClick(Sender: TObject); - begin //User have selected to add new tracker. FTrackerList.TrackerListOrderForUpdatedTorrent := From 4c029af359741b6cd1ce3ce007335fa9e824d4ef Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Sun, 8 Dec 2024 12:54:57 +0100 Subject: [PATCH 23/46] Update version (1.33.0) -> (1.33.1) close #51 --- enduser/version.txt | 4 ++++ ...erryferdinandus.bittorrent-tracker-editor.metainfo.xml | 8 ++++++++ source/code/main.pas | 6 ++++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/enduser/version.txt b/enduser/version.txt index 512f784..5b91cbf 100644 --- a/enduser/version.txt +++ b/enduser/version.txt @@ -1,3 +1,7 @@ +------ Version 1.33.1 +FIX: Cannot open torrent file V2 format (Issue 51) +Compiler Lazarus: v3.6 + ------ Version 1.33 ADD: Verify the working status of public trackers. (Issue 21) ADD: Wrong tracker URL format from torrent files should be unselected by default (Issue 22) diff --git a/metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.metainfo.xml b/metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.metainfo.xml index 3e61786..e2aa6d8 100644 --- a/metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.metainfo.xml +++ b/metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.metainfo.xml @@ -46,6 +46,14 @@ + + +

This release fixes the following bugs:

+
    +
  • Cannot open torrent file V2 format
  • +
+ +

diff --git a/source/code/main.pas b/source/code/main.pas index 8b13ee6..60f47be 100644 --- a/source/code/main.pas +++ b/source/code/main.pas @@ -215,8 +215,10 @@ implementation ); //program name and version (http://semver.org/) - FORM_CAPTION = 'Bittorrent tracker editor (1.33.0/LCL ' + - lcl_version + '/FPC ' + {$I %FPCVERSION%} + ')'; + PROGRAM_VERSION = '1.33.1'; + + FORM_CAPTION = 'Bittorrent tracker editor (' + PROGRAM_VERSION + + '/LCL ' + lcl_version + '/FPC ' + {$I %FPCVERSION%} + ')'; GROUPBOX_PRESENT_TRACKERS_CAPTION = 'Present trackers in all torrent files. Select the one that you want to keep. And added to all torrent files.'; From 8ce1069801a83d0afa5e61c070843c78b15af18b Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Sun, 8 Dec 2024 16:10:35 +0100 Subject: [PATCH 24/46] V1.33.1 [skip ci] --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4b619d9..d4385d9 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ There is no backup function in this software. Use it at your own risk. Bittorren --- ## Software history: ## +### 1.33.1 ### + * FIX: Cannot open torrent file V2 format. ([Issue 51](https://github.com/GerryFerdinandus/bittorrent-tracker-editor/issues/51)) ### 1.33.0 ### * ADD: Support for OpenSSL 3 From 40b875548dcca0f0fef95d9758d70ffaf8e9f027 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Sat, 14 Dec 2024 12:45:02 +0100 Subject: [PATCH 25/46] Create apple dmg file Use dmg file as installer file. Don't use zip file anymore for macOS. --- .github/workflows/cicd_macos.yaml | 119 ++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 31 deletions(-) diff --git a/.github/workflows/cicd_macos.yaml b/.github/workflows/cicd_macos.yaml index 8841d85..b196bd3 100644 --- a/.github/workflows/cicd_macos.yaml +++ b/.github/workflows/cicd_macos.yaml @@ -17,10 +17,11 @@ jobs: timeout-minutes: 60 env: LAZBUILD_WITH_PATH: /Applications/Lazarus/lazbuild - RELEASE_ZIP_FILE: trackereditor_macOS_amd64.zip MACOS_APP: enduser/trackereditor.app - LAZ_OPT: --widgetset=cocoa + PROGRAM_NAME_WITH_PATH: 'enduser/trackereditor' BUILD_WITH_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} + PROJECT_LPI: source/project/tracker_editor/trackereditor.lpi + RELEASE_DMG_FILE: trackereditor_macOS_universal.dmg steps: - uses: actions/checkout@v4 @@ -30,24 +31,54 @@ jobs: - name: Install Lazarus IDE run: brew install --cask lazarus - - name: Build Release version - # Build trackereditor project (Release mode) - run: ${{ env.LAZBUILD_WITH_PATH }} --build-all --build-mode=Release ${{ env.LAZ_OPT }} source/project/tracker_editor/trackereditor.lpi + - name: Install Create dmg + run: brew install create-dmg + + - name: Build trackereditor app for Apple silicon (aarch64) + run: | + ${{ env.LAZBUILD_WITH_PATH }} --build-all --build-mode=Release --widgetset=cocoa --cpu=aarch64 ${{ env.PROJECT_LPI }} + cp -a ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64 + shell: bash + + - name: Build trackereditor app for Intel Mac version (x86_64) + run: | + ${{ env.LAZBUILD_WITH_PATH }} --build-all --build-mode=Release --widgetset=cocoa --cpu=x86_64 ${{ env.PROJECT_LPI }} + cp -a ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64 + shell: bash + + - name: Create a Universal macOS binary from aarch64 and x86_64 + run: | + # remove the previeus binary build + rm -f ${{ env.PROGRAM_NAME_WITH_PATH }} + + # Create a new Universal macOS binary + lipo -create -output ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64 ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64 + + # Remove this single binary build. Not needed any more. + rm -f ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64 + rm -f ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64 + shell: bash + + - name: Extract latest program version from metainfo and update the Info.plist with it + env: + METAINFO_FILE: metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.metainfo.xml + run: | + TRACKER_EDITOR_VERSION=$(xmllint --xpath "string(/component/releases/release[1]/@version)" $METAINFO_FILE) + echo Program version: $TRACKER_EDITOR_VERSION + plutil -replace CFBundleShortVersionString -string $TRACKER_EDITOR_VERSION ${{ env.MACOS_APP }}/Contents/Info.plist shell: bash - name: Move program and icon into macOS .app env: ICON_FILE: 'metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.png' - PROGRAM_NAME_WITH_PATH: 'enduser/trackereditor' run: | - # remove the path - PROGRAM_NAME_ONLY=$(basename -- "$PROGRAM_NAME_WITH_PATH") + PROGRAM_NAME_ONLY=$(basename -- "${{ env.PROGRAM_NAME_WITH_PATH }}") # ------ Move program to app - # remove symbolic link in app. Need real program here. - rm -f "${PROGRAM_NAME_WITH_PATH}.app/Contents/MacOS/${PROGRAM_NAME_ONLY}" + # remove the previeus app version + rm -f "${{ env.MACOS_APP }}/Contents/MacOS/${PROGRAM_NAME_ONLY}" # copy the program to the app version. - mv -f "${PROGRAM_NAME_WITH_PATH}" "${PROGRAM_NAME_WITH_PATH}.app/Contents/MacOS" + mv -f "${{ env.PROGRAM_NAME_WITH_PATH }}" "${{ env.MACOS_APP }}/Contents/MacOS" # ------ Create icon set and move it into the app iconset_folder="temp_folder.iconset" @@ -65,17 +96,29 @@ jobs: rm -r "${iconset_folder}" # move icon file to the app - mv -f "iconfile.icns" "${PROGRAM_NAME_WITH_PATH}.app/Contents/Resources" + mv -f "iconfile.icns" "${{ env.MACOS_APP }}/Contents/Resources" # add icon to plist xml file CFBundleIconFile = "iconfile" - plutil -insert CFBundleIconFile -string "iconfile" "${PROGRAM_NAME_WITH_PATH}.app/Contents/Info.plist" + plutil -insert CFBundleIconFile -string "iconfile" "${{ env.MACOS_APP }}/Contents/Info.plist" + shell: bash + + - name: Check CPU type generated by Lazbuild + run: | + lipo -archs "${{ env.MACOS_APP }}"/Contents/MacOS/trackereditor + lipo -archs "${{ env.MACOS_APP }}"/Contents/MacOS/trackereditor | grep -Fq x86_64 + lipo -archs "${{ env.MACOS_APP }}"/Contents/MacOS/trackereditor | grep -Fq arm64 + shell: bash + + - name: Test App SSL connection + run: open "${{ env.MACOS_APP }}" --args -TEST_SSL shell: bash - - name: Codesign macOS app bundle + - name: Codesign macOS app bundle. If certificate is present. if: ${{ env.BUILD_WITH_CERTIFICATE != '' }} # This macOS Codesign step is copied from: # https://federicoterzi.com/blog/automatic-code-signing-and-notarization-for-macos-apps-using-github-actions/ # This is a bit different from the previous version for Travis-CI build system to build bittorrent tracker editor + # More info https://developer.apple.com/forums/thread/128166 env: MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} @@ -95,15 +138,35 @@ jobs: security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain - # We finally codesign our app bundle, specifying the Hardened runtime option. - #/usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime "${{ env.MACOS_APP }}" -v - # sign the app. -sign is the developer cetificate ID - # Must use --deep to sign all internal content - /usr/bin/codesign --timestamp --force --options runtime --deep --sign "$MACOS_CERTIFICATE_NAME" "${{ env.MACOS_APP }}" + /usr/bin/codesign --timestamp --force --options runtime --sign "$MACOS_CERTIFICATE_NAME" "${{ env.MACOS_APP }}" + shell: bash + + - name: Create dmg file from the enduser/ folder + run: | + # Remove all txt file. There are not needed. + rm -f enduser/*.txt + + # Build dmg image. https://github.com/create-dmg/create-dmg + create-dmg \ + --volname "bittorrent-tracker-editor" \ + --window-pos 200 120 \ + --window-size 800 400 \ + --icon "trackereditor.app" 200 190 \ + --app-drop-link 600 185 \ + ${{ env.RELEASE_DMG_FILE }} \ + "./enduser" + shell: bash + + - name: Codesign dmg file. If certificate is present. + if: ${{ env.BUILD_WITH_CERTIFICATE != '' }} + env: + MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} + run: | + /usr/bin/codesign --timestamp --force --options runtime --sign "$MACOS_CERTIFICATE_NAME" "${{ env.RELEASE_DMG_FILE }}" shell: bash - - name: Notarize macOS app bundle + - name: Notarize macOS DMG bundle. If certificate is present. if: ${{ env.BUILD_WITH_CERTIFICATE != '' }} env: PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} @@ -121,7 +184,7 @@ jobs: # notarization service echo "Creating temp notarization archive" - ditto -c -k --keepParent "${{ env.MACOS_APP }}" "notarization.zip" + ditto -c -k --keepParent "${{ env.RELEASE_DMG_FILE }}" "notarization.zip" # Here we send the notarization request to the Apple's Notarization service, waiting for the result. # This typically takes a few seconds inside a CI environment, but it might take more depending on the App @@ -134,20 +197,14 @@ jobs: # Finally, we need to "attach the staple" to our executable, which will allow our app to be # validated by macOS even when an internet connection is not available. echo "Attach staple" - xcrun stapler staple "${{ env.MACOS_APP }}" - shell: bash - - - name: Zip only the app folder. - run: | - echo "Zip macOS app file" - /usr/bin/ditto -c -k --keepParent "${{ env.MACOS_APP }}" "${{ env.RELEASE_ZIP_FILE }}" + xcrun stapler staple "${{ env.RELEASE_DMG_FILE }}" shell: bash - name: Upload Artifact uses: actions/upload-artifact@v4 with: - name: artifact-${{ runner.os }} - path: ${{ env.RELEASE_ZIP_FILE }} + name: artifact-${{ env.RELEASE_DMG_FILE }} + path: ${{ env.RELEASE_DMG_FILE }} compression-level: 0 # no compression. Content is already a zip file if-no-files-found: error @@ -155,4 +212,4 @@ jobs: uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/') with: - files: ${{ env.RELEASE_ZIP_FILE }} + files: ${{ env.RELEASE_DMG_FILE }} From 9e21a1bdbbc201bba4464aaf3cfdae5e424176dd Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Wed, 18 Dec 2024 01:40:58 +0100 Subject: [PATCH 26/46] add macOS(universal) macOS build now generate intel + apple silicon as one binary [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d4385d9..98779fe 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This software works on Windows 7+, macOS and Linux. ## Build Status: ## Continuous integration|Status| Generate an executable file for the operating system| Download link ------------|---------|---------|---------- -GitHub Actions |[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/gerryferdinandus/bittorrent-tracker-editor/cicd_ubuntu.yaml?label=Ubuntu)](https://github.com/GerryFerdinandus/bittorrent-tracker-editor/actions/workflows/cicd_ubuntu.yaml)[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/gerryferdinandus/bittorrent-tracker-editor/cicd_windows.yaml?label=Windows)](https://github.com/GerryFerdinandus/bittorrent-tracker-editor/actions/workflows/cicd_windows.yaml)[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/gerryferdinandus/bittorrent-tracker-editor/cicd_macos.yaml?label=macOS)](https://github.com/GerryFerdinandus/bittorrent-tracker-editor/actions/workflows/cicd_macos.yaml)|Linux(amd64), macOS(Intel processors) and Windows|[![GitHub Latest release](https://img.shields.io/github/release/GerryFerdinandus/bittorrent-tracker-editor/all.svg)](https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases) +GitHub Actions |[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/gerryferdinandus/bittorrent-tracker-editor/cicd_ubuntu.yaml?label=Ubuntu)](https://github.com/GerryFerdinandus/bittorrent-tracker-editor/actions/workflows/cicd_ubuntu.yaml)[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/gerryferdinandus/bittorrent-tracker-editor/cicd_windows.yaml?label=Windows)](https://github.com/GerryFerdinandus/bittorrent-tracker-editor/actions/workflows/cicd_windows.yaml)[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/gerryferdinandus/bittorrent-tracker-editor/cicd_macos.yaml?label=macOS)](https://github.com/GerryFerdinandus/bittorrent-tracker-editor/actions/workflows/cicd_macos.yaml)|Linux(amd64), Windows(amd64) and macOS(Universal)|[![GitHub Latest release](https://img.shields.io/github/release/GerryFerdinandus/bittorrent-tracker-editor/all.svg)](https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases) GitHub Actions (Ubuntu snap) |[![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/gerryferdinandus/bittorrent-tracker-editor/snap.yml)](https://github.com/GerryFerdinandus/bittorrent-tracker-editor/actions/workflows/snap.yml)|Linux (amd64 and arm64)|[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-white.svg)](https://snapcraft.io/bittorrent-tracker-editor) Flathub build server||Linux (amd64 and arm64)|Download on Flathub --- From e6c1479874b8f344b632729837312bc26d135e20 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Sun, 22 Dec 2024 09:43:40 +0100 Subject: [PATCH 27/46] macOS: Rename DMG file - Rename DMG file to indicate whether it is notarized or not - Unsigned DMG is still only released as artifact - Don't accidentally release unsigned binaries to end users --- .github/workflows/cicd_macos.yaml | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/.github/workflows/cicd_macos.yaml b/.github/workflows/cicd_macos.yaml index b196bd3..81cf110 100644 --- a/.github/workflows/cicd_macos.yaml +++ b/.github/workflows/cicd_macos.yaml @@ -21,7 +21,7 @@ jobs: PROGRAM_NAME_WITH_PATH: 'enduser/trackereditor' BUILD_WITH_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} PROJECT_LPI: source/project/tracker_editor/trackereditor.lpi - RELEASE_DMG_FILE: trackereditor_macOS_universal.dmg + RELEASE_DMG_FILE: trackereditor_macOS_notarized_universal_binary.dmg steps: - uses: actions/checkout@v4 @@ -37,26 +37,22 @@ jobs: - name: Build trackereditor app for Apple silicon (aarch64) run: | ${{ env.LAZBUILD_WITH_PATH }} --build-all --build-mode=Release --widgetset=cocoa --cpu=aarch64 ${{ env.PROJECT_LPI }} - cp -a ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64 + mv ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64 shell: bash - name: Build trackereditor app for Intel Mac version (x86_64) run: | ${{ env.LAZBUILD_WITH_PATH }} --build-all --build-mode=Release --widgetset=cocoa --cpu=x86_64 ${{ env.PROJECT_LPI }} - cp -a ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64 + mv ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64 shell: bash - name: Create a Universal macOS binary from aarch64 and x86_64 run: | - # remove the previeus binary build - rm -f ${{ env.PROGRAM_NAME_WITH_PATH }} - # Create a new Universal macOS binary lipo -create -output ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64 ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64 - # Remove this single binary build. Not needed any more. - rm -f ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64 - rm -f ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64 + # Remove these extra binary build. Not needed any more. + rm -f ${{ env.PROGRAM_NAME_WITH_PATH }}-* shell: bash - name: Extract latest program version from metainfo and update the Info.plist with it @@ -200,16 +196,21 @@ jobs: xcrun stapler staple "${{ env.RELEASE_DMG_FILE }}" shell: bash - - name: Upload Artifact + - name: Use diferent .dmg file name for non signed/notarize version + if: ${{ env.BUILD_WITH_CERTIFICATE == '' }} + run: mv ${{ env.RELEASE_DMG_FILE }} trackereditor_macOS_UNSIGNED_universal_binary.dmg + shell: bash + + - name: Upload Artifact. Signed/Notarize is optional. uses: actions/upload-artifact@v4 with: - name: artifact-${{ env.RELEASE_DMG_FILE }} - path: ${{ env.RELEASE_DMG_FILE }} + name: artifact-${{ runner.os }} + path: "*.dmg" compression-level: 0 # no compression. Content is already a zip file if-no-files-found: error - - name: File release to end user + - name: Notarize file release to end user. If certificate is present. uses: softprops/action-gh-release@v2 - if: startsWith(github.ref, 'refs/tags/') + if: startsWith(github.ref, 'refs/tags/') && (env.BUILD_WITH_CERTIFICATE != '') with: files: ${{ env.RELEASE_DMG_FILE }} From be013ed74ca8273677e35cbb9960c4ab28bab896 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Mon, 21 Apr 2025 17:26:36 +0200 Subject: [PATCH 28/46] Update CI/CD workflows for Ubuntu and Windows: - Change Ubuntu runner to use specific versions for AppImage builds. - Refactor AppImage installation steps for amd64 and arm64 - Update Windows runner to use Windows Server 2025. --- .github/workflows/cicd_ubuntu.yaml | 36 +++++++++++++++++++++-------- .github/workflows/cicd_windows.yaml | 8 +------ 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/.github/workflows/cicd_ubuntu.yaml b/.github/workflows/cicd_ubuntu.yaml index f53f818..61107a4 100644 --- a/.github/workflows/cicd_ubuntu.yaml +++ b/.github/workflows/cicd_ubuntu.yaml @@ -13,10 +13,9 @@ on: jobs: build: - runs-on: ubuntu-24.04 #ubuntu-latest timeout-minutes: 60 - env: # Use the latest Lazarus source code. + env: # Use the latest Lazarus source code. Copied from: https://sourceforge.net/projects/lazarus/files/Lazarus%20Zip%20_%20GZip/ LAZARUS_URL_TAR_GZ: "https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases/download/V1.32.0/lazarus.tar.gz" strategy: @@ -29,21 +28,36 @@ jobs: - BUILD_TARGET: gtk2_amd64 RELEASE_FILE_NAME: trackereditor_linux_amd64_gtk2.zip LAZ_OPT: --widgetset=gtk2 + RUNS_ON: ubuntu-24.04 - BUILD_TARGET: qt5_amd64 RELEASE_FILE_NAME: trackereditor_linux_amd64_qt5.zip LAZ_OPT: --widgetset=qt5 QT_VERSION_CI: '5' + RUNS_ON: ubuntu-24.04 - BUILD_TARGET: qt6_amd64 RELEASE_FILE_NAME: trackereditor_linux_amd64_qt6.zip LAZ_OPT: --widgetset=qt6 QT_VERSION_CI: '6' + RUNS_ON: ubuntu-24.04 - BUILD_TARGET: AppImage_amd64 RELEASE_FILE_NAME: trackereditor_linux_amd64_qt6.AppImage LAZ_OPT: --widgetset=qt6 QT_VERSION_CI: '6' + LINUX_DEPLOY_FILE_CPU: x86_64 + RUNS_ON: ubuntu-22.04 + + - BUILD_TARGET: AppImage_arm64 + RELEASE_FILE_NAME: trackereditor_linux_arm64_qt6.AppImage + LAZ_OPT: --widgetset=qt6 + QT_VERSION_CI: '6' + LINUX_DEPLOY_FILE_CPU: aarch64 + RUNS_ON: ubuntu-22.04-arm + + name: ${{ matrix.BUILD_TARGET }} + runs-on: ${{ matrix.RUNS_ON }} steps: - uses: actions/checkout@v4 @@ -51,7 +65,9 @@ jobs: submodules: true - name: Install dependency for all build - run: sudo apt-get install -y fpc xvfb + run: | + sudo apt-get update + sudo apt-get install -y fpc xvfb shell: bash - name: Install dependency for gtk2 @@ -70,14 +86,14 @@ jobs: shell: bash - name: Install dependency for AppImage - if: matrix.BUILD_TARGET == 'AppImage_amd64' + if: matrix.BUILD_TARGET == 'AppImage_amd64' || matrix.BUILD_TARGET == 'AppImage_arm64' run: | # Add wayland plugin and platform theme sudo apt-get install -y fuse qt6-wayland qt6-xdgdesktopportal-platformtheme qt6-gtk-platformtheme - # Use static versions of AppImage builder. So it won't depend on some specific OS image or library version. - curl -L -O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-static-x86_64.AppImage - curl -L -O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-static-x86_64.AppImage - curl -L -O https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage + # Download/Install AppImage tools + curl -L -O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${{ matrix.LINUX_DEPLOY_FILE_CPU }}.AppImage + curl -L -O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-${{ matrix.LINUX_DEPLOY_FILE_CPU }}.AppImage + curl -L -O https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-${{ matrix.LINUX_DEPLOY_FILE_CPU }}.AppImage chmod +x linuxdeploy-*.AppImage shell: bash @@ -138,7 +154,7 @@ jobs: shell: bash - name: Create AppImage - if: matrix.BUILD_TARGET == 'AppImage_amd64' + if: matrix.BUILD_TARGET == 'AppImage_amd64' || matrix.BUILD_TARGET == 'AppImage_arm64' # LDAI_NO_APPSTREAM=1: skip checking AppStream metadata for issues env: LDAI_NO_APPSTREAM: 1 @@ -148,7 +164,7 @@ jobs: EXTRA_PLATFORM_PLUGINS: libqwayland-generic.so;libqwayland-egl.so DEPLOY_PLATFORM_THEMES: true run: | - ./linuxdeploy-static-x86_64.AppImage \ + ./linuxdeploy-${{ matrix.LINUX_DEPLOY_FILE_CPU }}.AppImage \ --output appimage \ --appdir temp_appdir \ --plugin qt \ diff --git a/.github/workflows/cicd_windows.yaml b/.github/workflows/cicd_windows.yaml index c9dca02..425b96e 100644 --- a/.github/workflows/cicd_windows.yaml +++ b/.github/workflows/cicd_windows.yaml @@ -13,7 +13,7 @@ on: jobs: build: - runs-on: windows-2022 + runs-on: windows-2025 timeout-minutes: 60 env: LAZBUILD_WITH_PATH: c:/lazarus/lazbuild @@ -25,12 +25,6 @@ jobs: with: submodules: true - - name: Install winget - # winget will be included in windows server 2025 - uses: Cyberboss/install-winget@v1 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Install Lazarus IDE run: winget install lazarus --disable-interactivity --accept-source-agreements --silent From 805b123887daed3aefb5f2ce5e1910d8fb52169c Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Wed, 23 Apr 2025 19:24:49 +0200 Subject: [PATCH 29/46] Refactor CI/CD workflows for macOS and Ubuntu - Download Lazarus source code into temp folder - Add lazbuild to the PATH variable. --- .github/workflows/cicd_macos.yaml | 32 +++++++++++++++++++++++++----- .github/workflows/cicd_ubuntu.yaml | 22 ++++++++++---------- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/.github/workflows/cicd_macos.yaml b/.github/workflows/cicd_macos.yaml index 81cf110..5133a1b 100644 --- a/.github/workflows/cicd_macos.yaml +++ b/.github/workflows/cicd_macos.yaml @@ -16,33 +16,55 @@ jobs: runs-on: macos-latest timeout-minutes: 60 env: - LAZBUILD_WITH_PATH: /Applications/Lazarus/lazbuild MACOS_APP: enduser/trackereditor.app PROGRAM_NAME_WITH_PATH: 'enduser/trackereditor' BUILD_WITH_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} PROJECT_LPI: source/project/tracker_editor/trackereditor.lpi RELEASE_DMG_FILE: trackereditor_macOS_notarized_universal_binary.dmg + # Copied the latest Lazarus source code from: https://sourceforge.net/projects/lazarus/files/Lazarus%20Zip%20_%20GZip/ + LAZARUS_URL_TAR_GZ: "https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases/download/V1.32.0/lazarus.tar.gz" steps: - uses: actions/checkout@v4 with: submodules: true - - name: Install Lazarus IDE - run: brew install --cask lazarus + - name: Install Free Pascal Compiler (FPC) multi arch version for macOS x86_64 and aarch64 + run: brew install --cask fpc-laz + shell: bash - name: Install Create dmg run: brew install create-dmg + - name: Download Lazarus source code into temp folder + run: | + #Download lazarus source code. Directory 'lazarus' will be created in the temp folder. + cd ${RUNNER_TEMP} + curl -L -O ${{ env.LAZARUS_URL_TAR_GZ }} + tar -xzf *.tar.gz + shell: bash + + - name: Build lazbuild from Lazarus source code + run: | + # make lazbuild and put the link with extra parameter in the temp folder. + LAZARUS_DIR=${RUNNER_TEMP}/lazarus + cd "$LAZARUS_DIR" + make lazbuild + echo "$LAZARUS_DIR/lazbuild --primary-config-path=$LAZARUS_DIR --lazarusdir=$LAZARUS_DIR \$*" > ${RUNNER_TEMP}/lazbuild + chmod +x ${RUNNER_TEMP}/lazbuild + # Add lazbuild to the PATH variable. So it can be used in the next steps. + echo ${RUNNER_TEMP} >> $GITHUB_PATH + shell: bash + - name: Build trackereditor app for Apple silicon (aarch64) run: | - ${{ env.LAZBUILD_WITH_PATH }} --build-all --build-mode=Release --widgetset=cocoa --cpu=aarch64 ${{ env.PROJECT_LPI }} + lazbuild --build-all --build-mode=Release --widgetset=cocoa --cpu=aarch64 ${{ env.PROJECT_LPI }} mv ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64 shell: bash - name: Build trackereditor app for Intel Mac version (x86_64) run: | - ${{ env.LAZBUILD_WITH_PATH }} --build-all --build-mode=Release --widgetset=cocoa --cpu=x86_64 ${{ env.PROJECT_LPI }} + lazbuild --build-all --build-mode=Release --widgetset=cocoa --cpu=x86_64 ${{ env.PROJECT_LPI }} mv ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64 shell: bash diff --git a/.github/workflows/cicd_ubuntu.yaml b/.github/workflows/cicd_ubuntu.yaml index 61107a4..4d1b826 100644 --- a/.github/workflows/cicd_ubuntu.yaml +++ b/.github/workflows/cicd_ubuntu.yaml @@ -97,9 +97,10 @@ jobs: chmod +x linuxdeploy-*.AppImage shell: bash - - name: Download Lazarus source code + - name: Download Lazarus source code into temp folder run: | - #Download lazarus source code. Directory 'lazarus' will be created in the project folder. + #Download lazarus source code. Directory 'lazarus' will be created in the temp folder. + cd ${RUNNER_TEMP} curl -L -O ${{ env.LAZARUS_URL_TAR_GZ }} tar -xzf *.tar.gz shell: bash @@ -107,26 +108,27 @@ jobs: - name: Build libQTpas.so if: matrix.QT_VERSION_CI != '' run: | - cd "${{ github.workspace }}/lazarus/lcl/interfaces/qt${{ matrix.QT_VERSION_CI }}/cbindings/" + cd "${RUNNER_TEMP}/lazarus/lcl/interfaces/qt${{ matrix.QT_VERSION_CI }}/cbindings/" /usr/lib/qt${{ matrix.QT_VERSION_CI }}/bin/qmake make -j$(nproc) sudo make install shell: bash - - name: Build lazbuild - env: - LAZARUS_DIR: "${{ github.workspace }}/lazarus" + - name: Build lazbuild from Lazarus source code run: | - # make lazbuild and put the link with extra parameter in project folder + # make lazbuild and put the link with extra parameter in the temp folder. + LAZARUS_DIR=${RUNNER_TEMP}/lazarus cd "$LAZARUS_DIR" make lazbuild - echo "$LAZARUS_DIR/lazbuild --primary-config-path=$LAZARUS_DIR --lazarusdir=$LAZARUS_DIR \$*" > ${{ github.workspace }}/lazbuild - chmod +x ${{ github.workspace }}/lazbuild + echo "$LAZARUS_DIR/lazbuild --primary-config-path=$LAZARUS_DIR --lazarusdir=$LAZARUS_DIR \$*" > ${RUNNER_TEMP}/lazbuild + chmod +x ${RUNNER_TEMP}/lazbuild + # Add lazbuild to the PATH variable. So it can be used in the next steps. + echo ${RUNNER_TEMP} >> $GITHUB_PATH shell: bash - name: Build trackereditor # Build trackereditor project (Release mode) - run: ./lazbuild --build-all --build-mode=Release ${{ matrix.LAZ_OPT }} source/project/tracker_editor/trackereditor.lpi + run: lazbuild --build-all --build-mode=Release ${{ matrix.LAZ_OPT }} source/project/tracker_editor/trackereditor.lpi shell: bash - name: Test if OpenSSL works on Linux CI From b76cff28eaeb3d427ef3c429f73aaa104e6e45ab Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Wed, 23 Apr 2025 19:30:28 +0200 Subject: [PATCH 30/46] Update conditional custom options for macOS aarch64 architecture - aarch64 -> macOS: 11.0 (first macOS version for aarch64) - intel -> macOS: 10.14 (dark theme is supported since macOS 10.14) the aarch64 linker give warning that the minimum version should be 11.0 --- source/project/tracker_editor/trackereditor.lpi | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/source/project/tracker_editor/trackereditor.lpi b/source/project/tracker_editor/trackereditor.lpi index a91b00d..338ca85 100644 --- a/source/project/tracker_editor/trackereditor.lpi +++ b/source/project/tracker_editor/trackereditor.lpi @@ -74,7 +74,14 @@ From aa9e751d03c9d4fa35806a786422bf951aeac85b Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Tue, 21 Oct 2025 21:35:09 +0200 Subject: [PATCH 31/46] macOS: Update custom options for aarch64 architecture to use -WM10.15 Reason: The newer xCode linker is no longer compatible with the current FPC compiler. Await a future FPC fix. This is a temporary workaround. --- source/project/tracker_editor/trackereditor.lpi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/project/tracker_editor/trackereditor.lpi b/source/project/tracker_editor/trackereditor.lpi index 338ca85..9b49c62 100644 --- a/source/project/tracker_editor/trackereditor.lpi +++ b/source/project/tracker_editor/trackereditor.lpi @@ -76,7 +76,7 @@ begin If TargetCPU = 'aarch64' then begin - CustomOptions += '-WM11.0'; + CustomOptions += '-WM10.15'; end else begin From 8b2d945565183ed7cad38d3a8920a9eeabe02d9f Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Tue, 21 Oct 2025 21:38:11 +0200 Subject: [PATCH 32/46] snap: Update snapcraft.yaml to support QT6 and core24 base Upgrade: core22 -> core24 QT5 -> QT6 --- snap/snapcraft.yaml | 50 +++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 9a88fe0..23803bc 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -2,23 +2,22 @@ name: bittorrent-tracker-editor adopt-info: mainprogram icon: metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.png -base: core22 +base: core24 grade: stable confinement: strict -architectures: - - build-on: amd64 - - build-on: arm64 +platforms: + amd64: + build-on: [amd64] + arm64: + build-on: [arm64] apps: bittorrent-tracker-editor: desktop: io.github.gerryferdinandus.bittorrent-tracker-editor.desktop extensions: - - kde-neon + - kde-neon-6 command: trackereditor - environment: - # Fallback to XWayland if running in a Wayland session. - DISABLE_WAYLAND: 1 plugs: - home - network @@ -32,28 +31,29 @@ parts: - curl - build-essential - fpc - - libqt5x11extras5-dev + build-environment: - LAZARUS_URL_TAR_GZ: "https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases/download/V1.32.0/lazarus.tar.gz" - - LAZARUS_QT_VERSION: "5" + - LAZARUS_QT_VERSION: "6" - LIB_DIR: "/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR" - LAZARUS_DIR: "$PWD/lazarus" override-build: | - # Remove the older libQTpas.so - rm -f $LIB_DIR/libQt${LAZARUS_QT_VERSION}Pas.* - #Download lazarus source code. Directory 'lazarus' will be created in the root. curl -L -O $LAZARUS_URL_TAR_GZ tar -xzf *.tar.gz - # Create libQTpas.so and put it in snap $CRAFT_PART_INSTALL + #--- Create libQTpas.so and put it in /usr/lib/ and $CRAFT_PART_INSTALL cd "$LAZARUS_DIR/lcl/interfaces/qt${LAZARUS_QT_VERSION}/cbindings/" - qmake + /snap/kde-qt6-core24-sdk/current/usr/bin/qt6/qmake6 make -j$(nproc) - make install + + # copy the libQTpas.so to /usr/lib/ (needed for compile-time linking) + cp -av libQt${LAZARUS_QT_VERSION}Pas.* $LIB_DIR/ + + # copy the libQTpas.so to snap install directory (needed for run-time linking) cp -av --parents $LIB_DIR/libQt${LAZARUS_QT_VERSION}Pas.* $CRAFT_PART_INSTALL - # make lazbuild and put the link with extra parameter in /usr/bin/ + #--- Make lazbuild and put the link with extra parameter in /usr/bin/ cd "$LAZARUS_DIR" make lazbuild echo "$LAZARUS_DIR/lazbuild --primary-config-path=$LAZARUS_DIR --lazarusdir=$LAZARUS_DIR \$*" > /usr/bin/lazbuild @@ -65,19 +65,20 @@ parts: plugin: nil parse-info: [metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.metainfo.xml] override-build: | - lazbuild --build-mode=Release --widgetset=qt5 source/project/tracker_editor/trackereditor.lpi + lazbuild --build-mode=Release --widgetset=qt6 source/project/tracker_editor/trackereditor.lpi install enduser/trackereditor $CRAFT_PART_INSTALL/ install metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.desktop $CRAFT_PART_INSTALL/ # -------------------------------------------------------------- -# Only 2 files are explicitly added in this snap +# Only 3 files are explicitly added in this snap # - main program: enduser/trackereditor -# - Lazarus QT suport library: libQt5Pas.so +# - desktop file: metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.desktop +# - libQt6Pas.so created during the build_lazarus part # # Create snap. Run from the project root folder: -# snapcraft --verbosity verbose +# snapcraft pack --verbosity verbose # -# he snapTo look what is inside t file. Directory 'squashfs-root' will be created in the root folder: +# To look what is inside the snap file. Directory 'squashfs-root' will be created in the root folder: # unsquashfs *.snap # # Install the snap: @@ -86,8 +87,3 @@ parts: # Run the snap # snap run bittorrent-tracker-editor # -------------------------------------------------------------- -# Todo: building for QT6 is still not working with snap -# https://askubuntu.com/questions/1460242/ubuntu-22-04-with-qt6-qmake-could-not-find-a-qt-installation-of -# qtchooser -install qt{LAZARUS_QT_VERSION} $(which qmake6) -# export QT_SELECT=qt{LAZARUS_QT_VERSION} -# -------------------------------------------------------------- From ce456b60d37b20df9fc127542604a0a0e2f6b469 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Fri, 24 Oct 2025 22:50:56 +0200 Subject: [PATCH 33/46] macOS: CI/CD adds macOS 15 Intel build server Two important changes: - Use two build servers: Intel and ARM instead of a single ARM build server. Reason: Intel processor stuck on the 'macos-15-intel' runner as the last macOS runner that supported Intel architecture. Arm processor still uses the 'macos-latest' runner. Brew install --cask Replace fpc-laz with fpc Reason: fpc-laz will be disabled/removed on September 1, 2026. https://github.com/Homebrew/homebrew-cask/commit/3648170a09b66772291c27ea5d4a2774fe84cb61 Fpc should now be used as a replacement. Note: Brew 'Lazarus' has not been updated due to an issue. The solution is to use 'fpc' to build Lazarus from source code. And to build the tracker editor. --- .github/workflows/cicd_macos.yaml | 215 ++++++++++++++++++++++++------ 1 file changed, 173 insertions(+), 42 deletions(-) diff --git a/.github/workflows/cicd_macos.yaml b/.github/workflows/cicd_macos.yaml index 5133a1b..2cca2b7 100644 --- a/.github/workflows/cicd_macos.yaml +++ b/.github/workflows/cicd_macos.yaml @@ -1,3 +1,23 @@ +# CI/CD workflow for macOS systems to build bittorrent tracker editor app +# Uses GitHub Actions macOS runners to build the app for both Apple silicon (aarch64) and Intel Mac (x86_64) +# Then create a Universal binary using lipo tool. +# Finally create a dmg file for end user distribution. +# The build also codesign and notarize the dmg file if the required Apple developer certificate +# and notarization credentials are present in the GitHub secrets. +# The build is triggered on push, pull request, manual workflow dispatch and every 6 months cron job. +# +# macos-15-intel runner is used to build the x86_64 version. +# macos-latest runner is used to build the aarch64 version and also to create the Universal binary dmg file. +# The build uses Free Pascal Compiler (FPC) and Lazarus IDE to build the app. +# +# macos-15-intel runner is the last macOS runner that supports Intel architecture. +# Newer macOS runners only support Apple silicon (aarch64) architecture. +# Keep supporting Intel architecture build while there runners are still available. +# +# Must use macOS ditto tool to create zip files. +# Using zip command creates zip files that missing some metadata required for macOS apps. + + name: CI/CD on macOS systems. permissions: @@ -11,31 +31,28 @@ on: schedule: - cron: "0 0 1 1/6 *" +env: + MACOS_APP: enduser/trackereditor.app + PROGRAM_NAME_WITH_PATH: 'enduser/trackereditor' + BUILD_WITH_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} + PROJECT_LPI: source/project/tracker_editor/trackereditor.lpi + RELEASE_DMG_FILE: trackereditor_macOS_notarized_universal_binary.dmg + # Copied the latest Lazarus source code from: https://sourceforge.net/projects/lazarus/files/Lazarus%20Zip%20_%20GZip/ + LAZARUS_URL_TAR_GZ: "https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases/download/V1.32.0/lazarus.tar.gz" + jobs: - build: + build_aarch64: # Build for Apple silicon Mac architecture runs-on: macos-latest timeout-minutes: 60 - env: - MACOS_APP: enduser/trackereditor.app - PROGRAM_NAME_WITH_PATH: 'enduser/trackereditor' - BUILD_WITH_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} - PROJECT_LPI: source/project/tracker_editor/trackereditor.lpi - RELEASE_DMG_FILE: trackereditor_macOS_notarized_universal_binary.dmg - # Copied the latest Lazarus source code from: https://sourceforge.net/projects/lazarus/files/Lazarus%20Zip%20_%20GZip/ - LAZARUS_URL_TAR_GZ: "https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases/download/V1.32.0/lazarus.tar.gz" - steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true - name: Install Free Pascal Compiler (FPC) multi arch version for macOS x86_64 and aarch64 - run: brew install --cask fpc-laz + run: brew install fpc shell: bash - - name: Install Create dmg - run: brew install create-dmg - - name: Download Lazarus source code into temp folder run: | #Download lazarus source code. Directory 'lazarus' will be created in the temp folder. @@ -59,25 +76,13 @@ jobs: - name: Build trackereditor app for Apple silicon (aarch64) run: | lazbuild --build-all --build-mode=Release --widgetset=cocoa --cpu=aarch64 ${{ env.PROJECT_LPI }} - mv ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64 shell: bash - - name: Build trackereditor app for Intel Mac version (x86_64) - run: | - lazbuild --build-all --build-mode=Release --widgetset=cocoa --cpu=x86_64 ${{ env.PROJECT_LPI }} - mv ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64 - shell: bash - - - name: Create a Universal macOS binary from aarch64 and x86_64 - run: | - # Create a new Universal macOS binary - lipo -create -output ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64 ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64 - - # Remove these extra binary build. Not needed any more. - rm -f ${{ env.PROGRAM_NAME_WITH_PATH }}-* + - name: Test App SSL connection + run: open "${{ env.PROGRAM_NAME_WITH_PATH }}" --args -TEST_SSL shell: bash - - name: Extract latest program version from metainfo and update the Info.plist with it + - name: Set correct version number in macOS .app bundle Info.plist from metainfo xml file env: METAINFO_FILE: metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.metainfo.xml run: | @@ -86,17 +91,10 @@ jobs: plutil -replace CFBundleShortVersionString -string $TRACKER_EDITOR_VERSION ${{ env.MACOS_APP }}/Contents/Info.plist shell: bash - - name: Move program and icon into macOS .app + - name: Create and set app icon in macOS .app bundle env: ICON_FILE: 'metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.png' run: | - PROGRAM_NAME_ONLY=$(basename -- "${{ env.PROGRAM_NAME_WITH_PATH }}") - - # ------ Move program to app - # remove the previeus app version - rm -f "${{ env.MACOS_APP }}/Contents/MacOS/${PROGRAM_NAME_ONLY}" - # copy the program to the app version. - mv -f "${{ env.PROGRAM_NAME_WITH_PATH }}" "${{ env.MACOS_APP }}/Contents/MacOS" # ------ Create icon set and move it into the app iconset_folder="temp_folder.iconset" @@ -120,7 +118,139 @@ jobs: plutil -insert CFBundleIconFile -string "iconfile" "${{ env.MACOS_APP }}/Contents/Info.plist" shell: bash - - name: Check CPU type generated by Lazbuild + - name: Zip the macOS .app bundle for artifact upload + run: ditto -c -k enduser/trackereditor.app enduser/trackereditor_app.zip + shell: bash + + - name: Rename built -aarch64 binary for artifact upload + run: | + mv ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64 + ditto -c -k ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64 ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64.zip + shell: bash + + - name: Upload Artifact. + uses: actions/upload-artifact@v4 + with: + name: artifact-aarch64 + # Include both the zipped universal app bundle and the aarch64 binary zip + path: enduser/*.zip + if-no-files-found: error + + build_x86_64: # Build for Intel Mac architecture + runs-on: macos-15-intel + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + with: + submodules: true + + - name: Install Free Pascal Compiler (FPC) multi arch version for macOS x86_64 and aarch64 + run: brew install fpc + shell: bash + + - name: Download Lazarus source code into temp folder + run: | + #Download lazarus source code. Directory 'lazarus' will be created in the temp folder. + cd ${RUNNER_TEMP} + curl -L -O ${{ env.LAZARUS_URL_TAR_GZ }} + tar -xzf *.tar.gz + shell: bash + + - name: Build lazbuild from Lazarus source code + run: | + # make lazbuild and put the link with extra parameter in the temp folder. + LAZARUS_DIR=${RUNNER_TEMP}/lazarus + cd "$LAZARUS_DIR" + make lazbuild + echo "$LAZARUS_DIR/lazbuild --primary-config-path=$LAZARUS_DIR --lazarusdir=$LAZARUS_DIR \$*" > ${RUNNER_TEMP}/lazbuild + chmod +x ${RUNNER_TEMP}/lazbuild + # Add lazbuild to the PATH variable. So it can be used in the next steps. + echo ${RUNNER_TEMP} >> $GITHUB_PATH + shell: bash + + - name: Build trackereditor app for Intel Mac version (x86_64) + run: | + lazbuild --build-all --build-mode=Release --widgetset=cocoa --cpu=x86_64 ${{ env.PROJECT_LPI }} + shell: bash + + - name: Test App SSL connection + run: open "${{ env.PROGRAM_NAME_WITH_PATH }}" --args -TEST_SSL + shell: bash + + - name: Rename built -x86_64 binary for artifact upload + run: | + mv ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64 + ditto -c -k ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64 ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64.zip + shell: bash + + - name: Upload Artifact. + uses: actions/upload-artifact@v4 + with: + name: artifact-x86_64 + path: enduser/*.zip + if-no-files-found: error + + + create_universal_macOS_binary: # Create Universal binary from aarch64 and x86_64 builds + runs-on: macos-latest + timeout-minutes: 60 + + needs: + - build_aarch64 + - build_x86_64 + + steps: + - name: Install create-dmg tool + run: brew install create-dmg + shell: bash + + - name: Download build artifacts from previous jobs + uses: actions/download-artifact@v5 + with: + path: enduser/ + merge-multiple: true + + - name: Display the downloaded artifact files + run: ls -R enduser/ + shell: bash + + - name: Unzip all the artifact files + run: | + ditto -xk enduser/trackereditor_app.zip enduser/trackereditor.app + ditto -xk ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64.zip enduser/ + ditto -xk ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64.zip enduser/ + # Remove the zip files after extraction + rm -f enduser/*.zip + shell: bash + + - name: Display the downloaded artifact files after unzip + run: ls -R enduser/ + shell: bash + + - name: Create a Universal macOS binary from aarch64 and x86_64 + run: | + # Create Universal binary using lipo tool + lipo -create -output ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64 ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64 + + # Remove the previous architecture specific binaries + rm -f ${{ env.PROGRAM_NAME_WITH_PATH }}-* + shell: bash + + - name: Replace the macOS .app bundle binary with the Universal binary + run: | + PROGRAM_NAME_ONLY=$(basename -- "${{ env.PROGRAM_NAME_WITH_PATH }}") + # remove the previous app (symbolic link) + rm -f "${{ env.MACOS_APP }}/Contents/MacOS/${PROGRAM_NAME_ONLY}" + # copy the program to the app version. + mv -f "${{ env.PROGRAM_NAME_WITH_PATH }}" "${{ env.MACOS_APP }}/Contents/MacOS" + ls -l "${{ env.MACOS_APP }}/Contents/MacOS/" + shell: bash + + - name: Display the enduser/ folder contents + run: ls -R enduser/ + shell: bash + + - name: Verify the Universal binary architectures run: | lipo -archs "${{ env.MACOS_APP }}"/Contents/MacOS/trackereditor lipo -archs "${{ env.MACOS_APP }}"/Contents/MacOS/trackereditor | grep -Fq x86_64 @@ -160,11 +290,12 @@ jobs: /usr/bin/codesign --timestamp --force --options runtime --sign "$MACOS_CERTIFICATE_NAME" "${{ env.MACOS_APP }}" shell: bash + - name: Display the enduser/ folder contents + run: ls -R enduser/ + shell: bash + - name: Create dmg file from the enduser/ folder run: | - # Remove all txt file. There are not needed. - rm -f enduser/*.txt - # Build dmg image. https://github.com/create-dmg/create-dmg create-dmg \ --volname "bittorrent-tracker-editor" \ From 838ababff271f2c5dc85b3a972a2aea87deb0492 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Sat, 25 Oct 2025 19:55:25 +0200 Subject: [PATCH 34/46] chore: add dependabot configuration for GitHub Actions updates [skip ci] --- .github/dependabot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..7a5f2a3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# Enable version updates for GitHub Actions with daily checks +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "chore" + include: "scope" + open-pull-requests-limit: 5 From 3043ca4ed096df1b1d40d765005001adb9ba13c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 20:28:06 +0200 Subject: [PATCH 35/46] chore(deps): bump actions/checkout from 4 to 5 (#55) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cicd_ubuntu.yaml | 2 +- .github/workflows/cicd_windows.yaml | 2 +- .github/workflows/snap.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cicd_ubuntu.yaml b/.github/workflows/cicd_ubuntu.yaml index 4d1b826..02a1997 100644 --- a/.github/workflows/cicd_ubuntu.yaml +++ b/.github/workflows/cicd_ubuntu.yaml @@ -60,7 +60,7 @@ jobs: runs-on: ${{ matrix.RUNS_ON }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true diff --git a/.github/workflows/cicd_windows.yaml b/.github/workflows/cicd_windows.yaml index 425b96e..d246f72 100644 --- a/.github/workflows/cicd_windows.yaml +++ b/.github/workflows/cicd_windows.yaml @@ -21,7 +21,7 @@ jobs: LAZ_OPT: steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index 005920a..8e38937 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: true From 4f17c0273eb4b36a532ee0143dda3af18336912d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 20:28:25 +0200 Subject: [PATCH 36/46] chore(deps): bump actions/upload-artifact from 4 to 5 (#54) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cicd_macos.yaml | 6 +++--- .github/workflows/cicd_ubuntu.yaml | 2 +- .github/workflows/cicd_windows.yaml | 2 +- .github/workflows/snap.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cicd_macos.yaml b/.github/workflows/cicd_macos.yaml index 2cca2b7..2a3ff70 100644 --- a/.github/workflows/cicd_macos.yaml +++ b/.github/workflows/cicd_macos.yaml @@ -129,7 +129,7 @@ jobs: shell: bash - name: Upload Artifact. - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: artifact-aarch64 # Include both the zipped universal app bundle and the aarch64 binary zip @@ -184,7 +184,7 @@ jobs: shell: bash - name: Upload Artifact. - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: artifact-x86_64 path: enduser/*.zip @@ -355,7 +355,7 @@ jobs: shell: bash - name: Upload Artifact. Signed/Notarize is optional. - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: artifact-${{ runner.os }} path: "*.dmg" diff --git a/.github/workflows/cicd_ubuntu.yaml b/.github/workflows/cicd_ubuntu.yaml index 02a1997..b8ce958 100644 --- a/.github/workflows/cicd_ubuntu.yaml +++ b/.github/workflows/cicd_ubuntu.yaml @@ -181,7 +181,7 @@ jobs: shell: bash - name: Upload Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: artifact-${{ matrix.RELEASE_FILE_NAME }} path: ${{ matrix.RELEASE_FILE_NAME }} diff --git a/.github/workflows/cicd_windows.yaml b/.github/workflows/cicd_windows.yaml index d246f72..475e926 100644 --- a/.github/workflows/cicd_windows.yaml +++ b/.github/workflows/cicd_windows.yaml @@ -67,7 +67,7 @@ jobs: shell: pwsh - name: Upload Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: artifact-${{ runner.os }} path: ${{ env.RELEASE_ZIP_FILE }} diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index 8e38937..5fc51a3 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -20,7 +20,7 @@ jobs: - uses: snapcore/action-build@v1 - name: Upload Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: artifact-snap path: "*.snap" From 53fdaf90a7d1e7224c62ed0031cedaec20edf21a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 20:28:43 +0200 Subject: [PATCH 37/46] chore(deps): bump actions/download-artifact from 5 to 6 (#53) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cicd_macos.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cicd_macos.yaml b/.github/workflows/cicd_macos.yaml index 2a3ff70..f5cd72f 100644 --- a/.github/workflows/cicd_macos.yaml +++ b/.github/workflows/cicd_macos.yaml @@ -205,7 +205,7 @@ jobs: shell: bash - name: Download build artifacts from previous jobs - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: path: enduser/ merge-multiple: true From e55f7b00620b01542d62b6c211f9f0a58c3abf81 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Sat, 25 Oct 2025 22:30:09 +0200 Subject: [PATCH 38/46] chore: update dependabot to use cron schedule every 6 months Use the same cron schedule time as the other Github Action [skip ci] --- .github/dependabot.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7a5f2a3..5b52009 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,10 +1,13 @@ -# Enable version updates for GitHub Actions with daily checks +# Enable version updates for GitHub Actions at cron schedule every 6 months +# Can be manually triggered via menu: Insights > Dependency graph > "Recent update jobs" -> "Check for updates" + version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" + interval: "cron" + cronjob: "0 0 1 1/6 *" commit-message: prefix: "chore" include: "scope" From d21c2f8ea94aada1fc0a20dc014a8d379b068d22 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Fri, 7 Nov 2025 19:55:10 +0100 Subject: [PATCH 39/46] snap: add armhf and riscv64 build [skip ci] --- snap/snapcraft.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 23803bc..a5a0572 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -8,9 +8,9 @@ confinement: strict platforms: amd64: - build-on: [amd64] arm64: - build-on: [arm64] + armhf: + riscv64: apps: bittorrent-tracker-editor: From bbb9ed0f68440efd8e430befb058c34bebcc7116 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:41:54 +0100 Subject: [PATCH 40/46] chore(deps): bump actions/checkout from 5 to 6 (#56) Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cicd_macos.yaml | 4 ++-- .github/workflows/cicd_ubuntu.yaml | 2 +- .github/workflows/cicd_windows.yaml | 2 +- .github/workflows/snap.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cicd_macos.yaml b/.github/workflows/cicd_macos.yaml index f5cd72f..33d4c71 100644 --- a/.github/workflows/cicd_macos.yaml +++ b/.github/workflows/cicd_macos.yaml @@ -45,7 +45,7 @@ jobs: runs-on: macos-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: true @@ -140,7 +140,7 @@ jobs: runs-on: macos-15-intel timeout-minutes: 60 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: true diff --git a/.github/workflows/cicd_ubuntu.yaml b/.github/workflows/cicd_ubuntu.yaml index b8ce958..6218991 100644 --- a/.github/workflows/cicd_ubuntu.yaml +++ b/.github/workflows/cicd_ubuntu.yaml @@ -60,7 +60,7 @@ jobs: runs-on: ${{ matrix.RUNS_ON }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: true diff --git a/.github/workflows/cicd_windows.yaml b/.github/workflows/cicd_windows.yaml index 475e926..65032fb 100644 --- a/.github/workflows/cicd_windows.yaml +++ b/.github/workflows/cicd_windows.yaml @@ -21,7 +21,7 @@ jobs: LAZ_OPT: steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: true diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index 5fc51a3..7a916dc 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: true From 79a8a232f6003619bec59532ba4d9ca7046bcc97 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Tue, 16 Dec 2025 13:55:14 +0100 Subject: [PATCH 41/46] Add missing libxkbcommon-dev - New build/linker suddenly failed due to missing libxkbcommon.so.0 - Remove broken targets: armhf and riscv64 --- snap/snapcraft.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index a5a0572..bc6fe21 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -9,8 +9,6 @@ confinement: strict platforms: amd64: arm64: - armhf: - riscv64: apps: bittorrent-tracker-editor: @@ -31,6 +29,7 @@ parts: - curl - build-essential - fpc + - libxkbcommon-dev build-environment: - LAZARUS_URL_TAR_GZ: "https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases/download/V1.32.0/lazarus.tar.gz" From b07a2fad38d30bc63d513bfa0452b5d2d423ddfa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 23:04:50 +0100 Subject: [PATCH 42/46] chore(deps): bump actions/upload-artifact from 5 to 6 (#57) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cicd_macos.yaml | 6 +++--- .github/workflows/cicd_ubuntu.yaml | 2 +- .github/workflows/cicd_windows.yaml | 2 +- .github/workflows/snap.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cicd_macos.yaml b/.github/workflows/cicd_macos.yaml index 33d4c71..5008f45 100644 --- a/.github/workflows/cicd_macos.yaml +++ b/.github/workflows/cicd_macos.yaml @@ -129,7 +129,7 @@ jobs: shell: bash - name: Upload Artifact. - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: artifact-aarch64 # Include both the zipped universal app bundle and the aarch64 binary zip @@ -184,7 +184,7 @@ jobs: shell: bash - name: Upload Artifact. - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: artifact-x86_64 path: enduser/*.zip @@ -355,7 +355,7 @@ jobs: shell: bash - name: Upload Artifact. Signed/Notarize is optional. - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: artifact-${{ runner.os }} path: "*.dmg" diff --git a/.github/workflows/cicd_ubuntu.yaml b/.github/workflows/cicd_ubuntu.yaml index 6218991..56fc41c 100644 --- a/.github/workflows/cicd_ubuntu.yaml +++ b/.github/workflows/cicd_ubuntu.yaml @@ -181,7 +181,7 @@ jobs: shell: bash - name: Upload Artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: artifact-${{ matrix.RELEASE_FILE_NAME }} path: ${{ matrix.RELEASE_FILE_NAME }} diff --git a/.github/workflows/cicd_windows.yaml b/.github/workflows/cicd_windows.yaml index 65032fb..09763b4 100644 --- a/.github/workflows/cicd_windows.yaml +++ b/.github/workflows/cicd_windows.yaml @@ -67,7 +67,7 @@ jobs: shell: pwsh - name: Upload Artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: artifact-${{ runner.os }} path: ${{ env.RELEASE_ZIP_FILE }} diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index 7a916dc..2a77953 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -20,7 +20,7 @@ jobs: - uses: snapcore/action-build@v1 - name: Upload Artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: artifact-snap path: "*.snap" From 41eed2caf1695822f2bb1226e899e2eddc961099 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 23:05:29 +0100 Subject: [PATCH 43/46] chore(deps): bump actions/download-artifact from 6 to 7 (#58) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cicd_macos.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cicd_macos.yaml b/.github/workflows/cicd_macos.yaml index 5008f45..6d06386 100644 --- a/.github/workflows/cicd_macos.yaml +++ b/.github/workflows/cicd_macos.yaml @@ -205,7 +205,7 @@ jobs: shell: bash - name: Download build artifacts from previous jobs - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: path: enduser/ merge-multiple: true From 771550bc3bf8076c77b8ca950ff7b0bee04d44be Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Sat, 27 Dec 2025 17:04:37 +0100 Subject: [PATCH 44/46] feat: add composite action to install Lazarus IDE with dependencies for Windows, Linux, and macOS --- .github/actions/install_lazarus/action.yaml | 93 +++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 .github/actions/install_lazarus/action.yaml diff --git a/.github/actions/install_lazarus/action.yaml b/.github/actions/install_lazarus/action.yaml new file mode 100644 index 0000000..6063a9b --- /dev/null +++ b/.github/actions/install_lazarus/action.yaml @@ -0,0 +1,93 @@ +# include this file in the cicd_windows.yaml, cicd_ubuntu.yaml, and cicd_macos.yaml like this: +# - name: Install Lazarus IDE +# uses: ./.github/actions/install_lazarus +# with: +# qt_version_ci: 6 # optional, only needed when building with qt5 or qt6 + + +# https://docs.github.com/en/actions/tutorials/create-actions/create-a-composite-action +name: 'Install Lazaurus IDE' +description: 'download and build lazbuild' + +inputs: + qt_version_ci: # id of input + description: 'QT version' + required: false + +runs: + using: "composite" + steps: + - name: Install Lazarus IDE (Windows) + if: runner.os == 'Windows' + run: winget install lazarus --disable-interactivity --accept-source-agreements --silent + shell: powershell + + - name: Install fpc (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y fpc + shell: bash + + - name: Install dependency for gtk2 + if: runner.os == 'Linux' && inputs.qt_version_ci == '' + run: sudo apt-get install -y libgtk2.0-dev + shell: bash + + - name: Install dependency for qt5 + if: runner.os == 'Linux' && inputs.qt_version_ci == '5' + run: sudo apt-get install -y libqt5x11extras5-dev + shell: bash + + - name: Install dependency for qt6 + if: runner.os == 'Linux' && inputs.qt_version_ci == '6' + run: sudo apt-get install -y qt6-base-dev + shell: bash + + - name: Install Free Pascal Compiler (FPC) multi arch version for macOS x86_64 and aarch64 (macOS) + if: runner.os == 'macOS' + run: brew install fpc + shell: bash + + - name: Download Lazarus source code into temp folder (Linux and macOS) + if: runner.os != 'Windows' + env: + # Linux and macOS need to build lazbuild from Lazarus source code. + # Copied the latest Lazarus source code from: https://sourceforge.net/projects/lazarus/files/Lazarus%20Zip%20_%20GZip/ + LAZARUS_URL_TAR_GZ: "https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases/download/V1.32.0/lazarus.tar.gz" + RUNNER_TEMP: ${{ runner.temp }} + run: | + #Download lazarus source code. Directory 'lazarus' will be created in the temp folder. + echo ${RUNNER_TEMP} + cd ${RUNNER_TEMP} + curl -L -O ${{ env.LAZARUS_URL_TAR_GZ }} + tar -xzf *.tar.gz + shell: bash + + - name: Build lazbuild from Lazarus source code (Linux and macOS) + if: runner.os != 'Windows' + env: + RUNNER_TEMP: ${{ runner.temp }} + run: | + # make lazbuild and put the link with extra parameter in the temp folder. + LAZARUS_DIR=${RUNNER_TEMP}/lazarus + cd "$LAZARUS_DIR" + make lazbuild + echo "$LAZARUS_DIR/lazbuild --primary-config-path=$LAZARUS_DIR --lazarusdir=$LAZARUS_DIR \$*" > ${RUNNER_TEMP}/lazbuild + chmod +x ${RUNNER_TEMP}/lazbuild + # Add lazbuild to the PATH variable. + echo ${RUNNER_TEMP} >> $GITHUB_PATH + shell: bash + + - name: Build libQTpas.so (qt5 or qt6) + if: runner.os == 'Linux' && inputs.qt_version_ci != '' + env: + RUNNER_TEMP: ${{ runner.temp }} + run: | + cd "${RUNNER_TEMP}/lazarus/lcl/interfaces/qt${{ inputs.qt_version_ci }}/cbindings/" + /usr/lib/qt${{ inputs.qt_version_ci }}/bin/qmake + make -j$(nproc) + sudo make install + shell: bash + + From 982629eef9deeafa0604bc066988391021978a3a Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Sat, 27 Dec 2025 17:07:35 +0100 Subject: [PATCH 45/46] feat: refactor CI/CD workflows To use the composite action to install Lazarus --- .github/workflows/cicd_macos.yaml | 196 ++++++++++------------------ .github/workflows/cicd_ubuntu.yaml | 54 +------- .github/workflows/cicd_windows.yaml | 4 +- 3 files changed, 77 insertions(+), 177 deletions(-) diff --git a/.github/workflows/cicd_macos.yaml b/.github/workflows/cicd_macos.yaml index 6d06386..92b4ed2 100644 --- a/.github/workflows/cicd_macos.yaml +++ b/.github/workflows/cicd_macos.yaml @@ -37,169 +37,75 @@ env: BUILD_WITH_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} PROJECT_LPI: source/project/tracker_editor/trackereditor.lpi RELEASE_DMG_FILE: trackereditor_macOS_notarized_universal_binary.dmg - # Copied the latest Lazarus source code from: https://sourceforge.net/projects/lazarus/files/Lazarus%20Zip%20_%20GZip/ - LAZARUS_URL_TAR_GZ: "https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases/download/V1.32.0/lazarus.tar.gz" jobs: - build_aarch64: # Build for Apple silicon Mac architecture - runs-on: macos-latest + build: timeout-minutes: 60 + + strategy: + # Set fail-fast to false to ensure that feedback is delivered for all matrix combinations. + fail-fast: false + + # Set up an include to perform the following build configurations. + matrix: + include: + - BUILD_TARGET: build_aarch64 + ARCH: aarch64 + RUNS_ON: macos-latest + + - BUILD_TARGET: build_x86_64 + ARCH: x86_64 + RUNS_ON: macos-15-intel + + name: ${{ matrix.BUILD_TARGET }} + runs-on: ${{ matrix.RUNS_ON }} + steps: - uses: actions/checkout@v6 with: submodules: true - - name: Install Free Pascal Compiler (FPC) multi arch version for macOS x86_64 and aarch64 - run: brew install fpc - shell: bash + - name: Install Lazarus IDE + uses: ./.github/actions/install_lazarus - - name: Download Lazarus source code into temp folder + - name: Build trackereditor app for Apple (${{ matrix.ARCH }}) run: | - #Download lazarus source code. Directory 'lazarus' will be created in the temp folder. - cd ${RUNNER_TEMP} - curl -L -O ${{ env.LAZARUS_URL_TAR_GZ }} - tar -xzf *.tar.gz - shell: bash - - - name: Build lazbuild from Lazarus source code - run: | - # make lazbuild and put the link with extra parameter in the temp folder. - LAZARUS_DIR=${RUNNER_TEMP}/lazarus - cd "$LAZARUS_DIR" - make lazbuild - echo "$LAZARUS_DIR/lazbuild --primary-config-path=$LAZARUS_DIR --lazarusdir=$LAZARUS_DIR \$*" > ${RUNNER_TEMP}/lazbuild - chmod +x ${RUNNER_TEMP}/lazbuild - # Add lazbuild to the PATH variable. So it can be used in the next steps. - echo ${RUNNER_TEMP} >> $GITHUB_PATH - shell: bash - - - name: Build trackereditor app for Apple silicon (aarch64) - run: | - lazbuild --build-all --build-mode=Release --widgetset=cocoa --cpu=aarch64 ${{ env.PROJECT_LPI }} + lazbuild --build-all --build-mode=Release --widgetset=cocoa --cpu=${{ matrix.ARCH }} ${{ env.PROJECT_LPI }} shell: bash - name: Test App SSL connection run: open "${{ env.PROGRAM_NAME_WITH_PATH }}" --args -TEST_SSL shell: bash - - name: Set correct version number in macOS .app bundle Info.plist from metainfo xml file - env: - METAINFO_FILE: metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.metainfo.xml - run: | - TRACKER_EDITOR_VERSION=$(xmllint --xpath "string(/component/releases/release[1]/@version)" $METAINFO_FILE) - echo Program version: $TRACKER_EDITOR_VERSION - plutil -replace CFBundleShortVersionString -string $TRACKER_EDITOR_VERSION ${{ env.MACOS_APP }}/Contents/Info.plist - shell: bash - - - name: Create and set app icon in macOS .app bundle - env: - ICON_FILE: 'metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.png' - run: | - - # ------ Create icon set and move it into the app - iconset_folder="temp_folder.iconset" - rm -rf "${iconset_folder}" - mkdir -p "${iconset_folder}" - - for s in 16 32 128 256 512; do - d=$(($s*2)) - sips -Z $s $ICON_FILE --out "${iconset_folder}/icon_${s}x$s.png" - sips -Z $d $ICON_FILE --out "${iconset_folder}/icon_${s}x$s@2x.png" - done - - # create .icns icon file - iconutil -c icns "${iconset_folder}" -o "iconfile.icns" - rm -r "${iconset_folder}" - - # move icon file to the app - mv -f "iconfile.icns" "${{ env.MACOS_APP }}/Contents/Resources" - - # add icon to plist xml file CFBundleIconFile = "iconfile" - plutil -insert CFBundleIconFile -string "iconfile" "${{ env.MACOS_APP }}/Contents/Info.plist" - shell: bash - - name: Zip the macOS .app bundle for artifact upload + if: matrix.ARCH == 'aarch64' run: ditto -c -k enduser/trackereditor.app enduser/trackereditor_app.zip shell: bash - name: Rename built -aarch64 binary for artifact upload run: | - mv ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64 - ditto -c -k ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64 ${{ env.PROGRAM_NAME_WITH_PATH }}-aarch64.zip + mv ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-${{ matrix.ARCH }} + ditto -c -k ${{ env.PROGRAM_NAME_WITH_PATH }}-${{ matrix.ARCH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-${{ matrix.ARCH }}.zip shell: bash - name: Upload Artifact. uses: actions/upload-artifact@v6 with: - name: artifact-aarch64 + name: artifact-${{ matrix.ARCH }} # Include both the zipped universal app bundle and the aarch64 binary zip path: enduser/*.zip if-no-files-found: error - build_x86_64: # Build for Intel Mac architecture - runs-on: macos-15-intel - timeout-minutes: 60 - steps: - - uses: actions/checkout@v6 - with: - submodules: true - - - name: Install Free Pascal Compiler (FPC) multi arch version for macOS x86_64 and aarch64 - run: brew install fpc - shell: bash - - - name: Download Lazarus source code into temp folder - run: | - #Download lazarus source code. Directory 'lazarus' will be created in the temp folder. - cd ${RUNNER_TEMP} - curl -L -O ${{ env.LAZARUS_URL_TAR_GZ }} - tar -xzf *.tar.gz - shell: bash - - - name: Build lazbuild from Lazarus source code - run: | - # make lazbuild and put the link with extra parameter in the temp folder. - LAZARUS_DIR=${RUNNER_TEMP}/lazarus - cd "$LAZARUS_DIR" - make lazbuild - echo "$LAZARUS_DIR/lazbuild --primary-config-path=$LAZARUS_DIR --lazarusdir=$LAZARUS_DIR \$*" > ${RUNNER_TEMP}/lazbuild - chmod +x ${RUNNER_TEMP}/lazbuild - # Add lazbuild to the PATH variable. So it can be used in the next steps. - echo ${RUNNER_TEMP} >> $GITHUB_PATH - shell: bash - - - name: Build trackereditor app for Intel Mac version (x86_64) - run: | - lazbuild --build-all --build-mode=Release --widgetset=cocoa --cpu=x86_64 ${{ env.PROJECT_LPI }} - shell: bash - - - name: Test App SSL connection - run: open "${{ env.PROGRAM_NAME_WITH_PATH }}" --args -TEST_SSL - shell: bash - - - name: Rename built -x86_64 binary for artifact upload - run: | - mv ${{ env.PROGRAM_NAME_WITH_PATH }} ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64 - ditto -c -k ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64 ${{ env.PROGRAM_NAME_WITH_PATH }}-x86_64.zip - shell: bash - - - name: Upload Artifact. - uses: actions/upload-artifact@v6 - with: - name: artifact-x86_64 - path: enduser/*.zip - if-no-files-found: error - - create_universal_macOS_binary: # Create Universal binary from aarch64 and x86_64 builds + needs: build runs-on: macos-latest timeout-minutes: 60 - needs: - - build_aarch64 - - build_x86_64 - steps: + - uses: actions/checkout@v6 + with: + sparse-checkout: metainfo + - name: Install create-dmg tool run: brew install create-dmg shell: bash @@ -227,6 +133,42 @@ jobs: run: ls -R enduser/ shell: bash + - name: Set correct version number in macOS .app bundle Info.plist from metainfo xml file + env: + METAINFO_FILE: metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.metainfo.xml + run: | + TRACKER_EDITOR_VERSION=$(xmllint --xpath "string(/component/releases/release[1]/@version)" $METAINFO_FILE) + echo Program version: $TRACKER_EDITOR_VERSION + plutil -replace CFBundleShortVersionString -string $TRACKER_EDITOR_VERSION ${{ env.MACOS_APP }}/Contents/Info.plist + shell: bash + + - name: Create and set app icon in macOS .app bundle + env: + ICON_FILE: 'metainfo/io.github.gerryferdinandus.bittorrent-tracker-editor.png' + run: | + + # ------ Create icon set and move it into the app + iconset_folder="temp_folder.iconset" + rm -rf "${iconset_folder}" + mkdir -p "${iconset_folder}" + + for s in 16 32 128 256 512; do + d=$(($s*2)) + sips -Z $s $ICON_FILE --out "${iconset_folder}/icon_${s}x$s.png" + sips -Z $d $ICON_FILE --out "${iconset_folder}/icon_${s}x$s@2x.png" + done + + # create .icns icon file + iconutil -c icns "${iconset_folder}" -o "iconfile.icns" + rm -r "${iconset_folder}" + + # move icon file to the app + mv -f "iconfile.icns" "${{ env.MACOS_APP }}/Contents/Resources" + + # add icon to plist xml file CFBundleIconFile = "iconfile" + plutil -insert CFBundleIconFile -string "iconfile" "${{ env.MACOS_APP }}/Contents/Info.plist" + shell: bash + - name: Create a Universal macOS binary from aarch64 and x86_64 run: | # Create Universal binary using lipo tool diff --git a/.github/workflows/cicd_ubuntu.yaml b/.github/workflows/cicd_ubuntu.yaml index 56fc41c..82de8e1 100644 --- a/.github/workflows/cicd_ubuntu.yaml +++ b/.github/workflows/cicd_ubuntu.yaml @@ -15,9 +15,6 @@ jobs: build: timeout-minutes: 60 - env: # Use the latest Lazarus source code. Copied from: https://sourceforge.net/projects/lazarus/files/Lazarus%20Zip%20_%20GZip/ - LAZARUS_URL_TAR_GZ: "https://github.com/GerryFerdinandus/bittorrent-tracker-editor/releases/download/V1.32.0/lazarus.tar.gz" - strategy: # Set fail-fast to false to ensure that feedback is delivered for all matrix combinations. fail-fast: false @@ -64,25 +61,15 @@ jobs: with: submodules: true + - name: Install Lazarus IDE + uses: ./.github/actions/install_lazarus + with: + qt_version_ci: ${{ matrix.QT_VERSION_CI }} + - name: Install dependency for all build run: | sudo apt-get update - sudo apt-get install -y fpc xvfb - shell: bash - - - name: Install dependency for gtk2 - if: matrix.QT_VERSION_CI == '' - run: sudo apt-get install -y libgtk2.0-dev - shell: bash - - - name: Install dependency for qt5 - if: matrix.QT_VERSION_CI == '5' - run: sudo apt-get install -y libqt5x11extras5-dev - shell: bash - - - name: Install dependency for qt6 - if: matrix.QT_VERSION_CI == '6' - run: sudo apt-get install -y qt6-base-dev + sudo apt-get install -y xvfb shell: bash - name: Install dependency for AppImage @@ -97,35 +84,6 @@ jobs: chmod +x linuxdeploy-*.AppImage shell: bash - - name: Download Lazarus source code into temp folder - run: | - #Download lazarus source code. Directory 'lazarus' will be created in the temp folder. - cd ${RUNNER_TEMP} - curl -L -O ${{ env.LAZARUS_URL_TAR_GZ }} - tar -xzf *.tar.gz - shell: bash - - - name: Build libQTpas.so - if: matrix.QT_VERSION_CI != '' - run: | - cd "${RUNNER_TEMP}/lazarus/lcl/interfaces/qt${{ matrix.QT_VERSION_CI }}/cbindings/" - /usr/lib/qt${{ matrix.QT_VERSION_CI }}/bin/qmake - make -j$(nproc) - sudo make install - shell: bash - - - name: Build lazbuild from Lazarus source code - run: | - # make lazbuild and put the link with extra parameter in the temp folder. - LAZARUS_DIR=${RUNNER_TEMP}/lazarus - cd "$LAZARUS_DIR" - make lazbuild - echo "$LAZARUS_DIR/lazbuild --primary-config-path=$LAZARUS_DIR --lazarusdir=$LAZARUS_DIR \$*" > ${RUNNER_TEMP}/lazbuild - chmod +x ${RUNNER_TEMP}/lazbuild - # Add lazbuild to the PATH variable. So it can be used in the next steps. - echo ${RUNNER_TEMP} >> $GITHUB_PATH - shell: bash - - name: Build trackereditor # Build trackereditor project (Release mode) run: lazbuild --build-all --build-mode=Release ${{ matrix.LAZ_OPT }} source/project/tracker_editor/trackereditor.lpi diff --git a/.github/workflows/cicd_windows.yaml b/.github/workflows/cicd_windows.yaml index 09763b4..8de17cd 100644 --- a/.github/workflows/cicd_windows.yaml +++ b/.github/workflows/cicd_windows.yaml @@ -13,7 +13,7 @@ on: jobs: build: - runs-on: windows-2025 + runs-on: windows-latest timeout-minutes: 60 env: LAZBUILD_WITH_PATH: c:/lazarus/lazbuild @@ -26,7 +26,7 @@ jobs: submodules: true - name: Install Lazarus IDE - run: winget install lazarus --disable-interactivity --accept-source-agreements --silent + uses: ./.github/actions/install_lazarus - name: Download OpenSSL *.dll run: | From 2901803d6901600ddff5f5d2964859d2ad1848d7 Mon Sep 17 00:00:00 2001 From: Gerry Ferdinandus Date: Sat, 27 Dec 2025 22:18:30 +0100 Subject: [PATCH 46/46] add lazbuild in windows PATH lazbuild is already in PATH for Linux and macOS --- .github/actions/install_lazarus/action.yaml | 12 ++++++++++-- .github/workflows/cicd_windows.yaml | 5 ++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/actions/install_lazarus/action.yaml b/.github/actions/install_lazarus/action.yaml index 6063a9b..f62e0c5 100644 --- a/.github/actions/install_lazarus/action.yaml +++ b/.github/actions/install_lazarus/action.yaml @@ -19,8 +19,16 @@ runs: steps: - name: Install Lazarus IDE (Windows) if: runner.os == 'Windows' - run: winget install lazarus --disable-interactivity --accept-source-agreements --silent - shell: powershell + run: | + winget install lazarus --disable-interactivity --accept-source-agreements --silent + echo 'c:/lazarus' >> $GITHUB_PATH + shell: pwsh + + - name: Make PATH also in bash available (Windows) + if: runner.os == 'Windows' + run: | + echo 'c:/lazarus' >> $GITHUB_PATH + shell: bash - name: Install fpc (Linux) if: runner.os == 'Linux' diff --git a/.github/workflows/cicd_windows.yaml b/.github/workflows/cicd_windows.yaml index 8de17cd..05a2fba 100644 --- a/.github/workflows/cicd_windows.yaml +++ b/.github/workflows/cicd_windows.yaml @@ -16,7 +16,6 @@ jobs: runs-on: windows-latest timeout-minutes: 60 env: - LAZBUILD_WITH_PATH: c:/lazarus/lazbuild RELEASE_ZIP_FILE: trackereditor_windows_amd64.zip LAZ_OPT: @@ -38,12 +37,12 @@ jobs: - name: Build Release version # Build trackereditor project (Release mode) - run: ${{ env.LAZBUILD_WITH_PATH }} --build-all --build-mode=Release ${{ env.LAZ_OPT }} source/project/tracker_editor/trackereditor.lpi + run: lazbuild --build-all --build-mode=Release ${{ env.LAZ_OPT }} source/project/tracker_editor/trackereditor.lpi shell: bash - name: Build Unit Test on Windows # Build unit test project (Debug mode) - run: ${{ env.LAZBUILD_WITH_PATH }} --build-all --build-mode=Debug ${{ env.LAZ_OPT }} source/project/unit_test/tracker_editor_test.lpi + run: lazbuild --build-all --build-mode=Debug ${{ env.LAZ_OPT }} source/project/unit_test/tracker_editor_test.lpi shell: bash - name: Run Unit Test on Windows