From 6f322ea16025c282676bea7f0a426f67c22ff793 Mon Sep 17 00:00:00 2001 From: Dave Page Date: Mon, 21 Sep 2026 09:30:46 +0100 Subject: [PATCH] Address the pre-merge review Ten fixes from a review pass with fresh eyes. The ones that mattered: rebuild-yum-repo.sh signed repomd.xml with no -u, so it used whatever the keyring's default key resolved to. That is the packaging key today only because there is one secret key in it. A repository whose metadata is signed by the wrong key is one every client with gpgcheck=1 rejects, produced by a run that exits 0. The snapshot publish gate required only deb and rpm, whilst its comment justified exactly one exception. A failed docs, tarball or wheel job published a snapshot missing those files, and a failed repo-rpm job was worse: install-repo-readme.py then fails after the packages are uploaded and signed, leaving half a snapshot on the download server. The location input was interpolated into shell on the runner holding both publishing keys. It now travels through the environment, and is validated against the shape of a datestamp before anything downstream sees it, so a bad one fails in five seconds rather than after an hour of building. Make.bat tested %ERRORLEVEL% inside a parenthesised block, where it is expanded at parse time, so a failed Authenticode signature on pgAdmin4.exe was undetectable; the handler was PAUSE, which on a headless signing host either returns at once or blocks until the job times out. Both sites now fail the build. A called workflow inherits the caller's ref, so once this is on master an ordinary push would have shared a concurrency group with the nightly snapshot's builds and cancelled them. And PULL_KEY named a file that does not exist on the download server, which would have failed the first release promotion. The rest: if-no-files-found on every build artefact, so a build that produces nothing fails rather than silently shipping a snapshot without it; pinned syft and helm installers, which were piping a default branch into a shell as root in the container that builds signed packages; the Helm chart no longer built and discarded; and the comments that had stopped describing the code. The self-test grows from 56 cases to 70, covering the verbs that delete things, arguments that look like options, and the SSH_ORIGINAL_COMMAND entry point rather than only the dry-run path. --- .github/workflows/check-container-build.yml | 13 +- .github/workflows/check-deb-build.yml | 10 +- .github/workflows/check-doc-builds.yml | 10 +- .github/workflows/check-helm-build.yml | 10 +- .github/workflows/check-macos-build.yml | 1 + .github/workflows/check-python-build.yml | 8 +- .github/workflows/check-rpm-build.yml | 10 +- .github/workflows/check-tarball-build.yml | 8 +- .github/workflows/check-windows-build.yml | 1 + .github/workflows/snapshot-macos.yml | 1 + .github/workflows/snapshot-windows.yml | 8 +- .github/workflows/snapshot.yml | 55 +- Make.bat | 1005 ++++++++++--------- pkg/publish/README.md | 5 +- pkg/publish/pga-publish | 2 +- pkg/publish/rebuild-yum-repo.sh | 8 +- pkg/publish/selftest.sh | 46 + 17 files changed, 676 insertions(+), 525 deletions(-) diff --git a/.github/workflows/check-container-build.yml b/.github/workflows/check-container-build.yml index 766adb117..139fe4c08 100644 --- a/.github/workflows/check-container-build.yml +++ b/.github/workflows/check-container-build.yml @@ -1,9 +1,9 @@ name: Check container build on: - # Callable from snapshot.yml, so that what gets published is the - # same build that runs on every push rather than a second - # definition of it that can drift. + # Callable, so that a workflow which needs a container build gets this one + # rather than a second definition of it that can drift. Snapshot does not + # call it today, the image not being part of the download tree. workflow_call: push: @@ -36,7 +36,12 @@ concurrency: # github.workflow, because in a reusable workflow that expression is # the CALLER's name. Every workflow Snapshot calls would otherwise # share one group and, with cancel-in-progress, cancel each other. - group: 'check-container-build @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + # github.event_name is in the key because a called workflow inherits the + # caller's ref: once this is on master, the nightly Snapshot's deb build + # and an ordinary push's deb build would otherwise share a group and, + # with cancel-in-progress, kill each other. A push landing mid-nightly + # would cancel the snapshot's build and skip the publish entirely. + group: 'check-container-build @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }} @ ${{ github.event_name }}' cancel-in-progress: true jobs: diff --git a/.github/workflows/check-deb-build.yml b/.github/workflows/check-deb-build.yml index 43e0a2a53..2d5acb6fa 100644 --- a/.github/workflows/check-deb-build.yml +++ b/.github/workflows/check-deb-build.yml @@ -34,7 +34,12 @@ concurrency: # github.workflow, because in a reusable workflow that expression is # the CALLER's name. Every workflow Snapshot calls would otherwise # share one group and, with cancel-in-progress, cancel each other. - group: 'check-deb-build @ ${{ github.ref }}' + # github.event_name is in the key because a called workflow inherits the + # caller's ref: once this is on master, the nightly Snapshot's deb build + # and an ordinary push's deb build would otherwise share a group and, + # with cancel-in-progress, kill each other. A push landing mid-nightly + # would cancel the snapshot's build and skip the publish entirely. + group: 'check-deb-build @ ${{ github.ref }} @ ${{ github.event_name }}' cancel-in-progress: true permissions: @@ -98,7 +103,7 @@ jobs: || python3 -m pip install sphinxcontrib-youtube - name: Install syft - run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin + run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/v1.52.0/install.sh | sh -s -- -b /usr/local/bin v1.52.0 - uses: actions/setup-node@v7 with: @@ -127,4 +132,5 @@ jobs: uses: actions/upload-artifact@v7 with: name: pgadmin4-deb-build-output-${{ matrix.name }} + if-no-files-found: error path: dist/*.deb diff --git a/.github/workflows/check-doc-builds.yml b/.github/workflows/check-doc-builds.yml index 0714d38c4..1e0dc2598 100644 --- a/.github/workflows/check-doc-builds.yml +++ b/.github/workflows/check-doc-builds.yml @@ -36,7 +36,12 @@ concurrency: # github.workflow, because in a reusable workflow that expression is # the CALLER's name. Every workflow Snapshot calls would otherwise # share one group and, with cancel-in-progress, cancel each other. - group: 'check-doc-builds @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + # github.event_name is in the key because a called workflow inherits the + # caller's ref: once this is on master, the nightly Snapshot's deb build + # and an ordinary push's deb build would otherwise share a group and, + # with cancel-in-progress, kill each other. A push landing mid-nightly + # would cancel the snapshot's build and skip the publish entirely. + group: 'check-doc-builds @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }} @ ${{ github.event_name }}' cancel-in-progress: true jobs: @@ -63,6 +68,7 @@ jobs: uses: actions/upload-artifact@v7 with: name: pgadmin4-html-build-output + if-no-files-found: error path: docs/en_US/_build/html/* - name: Check the ePub build @@ -74,6 +80,7 @@ jobs: uses: actions/upload-artifact@v7 with: name: pgadmin4-epub-build-output + if-no-files-found: error path: docs/en_US/_build/epub/pgAdmin4.epub - name: Check the PDF build @@ -85,4 +92,5 @@ jobs: uses: actions/upload-artifact@v7 with: name: pgadmin4-pdf-build-output + if-no-files-found: error path: docs/en_US/_build/latex/pgadmin4.pdf diff --git a/.github/workflows/check-helm-build.yml b/.github/workflows/check-helm-build.yml index 98d047eb5..9cb533923 100644 --- a/.github/workflows/check-helm-build.yml +++ b/.github/workflows/check-helm-build.yml @@ -24,7 +24,12 @@ concurrency: # github.workflow, because in a reusable workflow that expression is # the CALLER's name. Every workflow Snapshot calls would otherwise # share one group and, with cancel-in-progress, cancel each other. - group: 'check-helm-build @ ${{ github.ref }}' + # github.event_name is in the key because a called workflow inherits the + # caller's ref: once this is on master, the nightly Snapshot's deb build + # and an ordinary push's deb build would otherwise share a group and, + # with cancel-in-progress, kill each other. A push landing mid-nightly + # would cancel the snapshot's build and skip the publish entirely. + group: 'check-helm-build @ ${{ github.ref }} @ ${{ github.event_name }}' cancel-in-progress: true permissions: @@ -39,7 +44,7 @@ jobs: - name: Install Helm run: | - curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 -o get-helm-3 + curl -fsSL https://raw.githubusercontent.com/helm/helm/v4.3.0/scripts/get-helm-3 -o get-helm-3 chmod 700 get-helm-3 ./get-helm-3 @@ -50,4 +55,5 @@ jobs: uses: actions/upload-artifact@v7 with: name: pgadmin4-helm-build-output + if-no-files-found: error path: pgadmin4-helm-*.tgz diff --git a/.github/workflows/check-macos-build.yml b/.github/workflows/check-macos-build.yml index b41a0efda..82691021a 100644 --- a/.github/workflows/check-macos-build.yml +++ b/.github/workflows/check-macos-build.yml @@ -115,4 +115,5 @@ jobs: uses: actions/upload-artifact@v7 with: name: pgadmin4-macos-build-output-${{ matrix.name }} + if-no-files-found: error path: dist/*.dmg diff --git a/.github/workflows/check-python-build.yml b/.github/workflows/check-python-build.yml index 9c2b10024..e67231049 100644 --- a/.github/workflows/check-python-build.yml +++ b/.github/workflows/check-python-build.yml @@ -38,7 +38,12 @@ concurrency: # github.workflow, because in a reusable workflow that expression is # the CALLER's name. Every workflow Snapshot calls would otherwise # share one group and, with cancel-in-progress, cancel each other. - group: 'check-python-build @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + # github.event_name is in the key because a called workflow inherits the + # caller's ref: once this is on master, the nightly Snapshot's deb build + # and an ordinary push's deb build would otherwise share a group and, + # with cancel-in-progress, kill each other. A push landing mid-nightly + # would cancel the snapshot's build and skip the publish entirely. + group: 'check-python-build @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }} @ ${{ github.event_name }}' cancel-in-progress: true jobs: @@ -65,4 +70,5 @@ jobs: uses: actions/upload-artifact@v7 with: name: pgadmin4-python-build-output + if-no-files-found: error path: dist/* diff --git a/.github/workflows/check-rpm-build.yml b/.github/workflows/check-rpm-build.yml index 7a5df4a2a..e39e90a25 100644 --- a/.github/workflows/check-rpm-build.yml +++ b/.github/workflows/check-rpm-build.yml @@ -37,7 +37,12 @@ concurrency: # github.workflow, because in a reusable workflow that expression is # the CALLER's name. Every workflow Snapshot calls would otherwise # share one group and, with cancel-in-progress, cancel each other. - group: 'check-rpm-build @ ${{ github.ref }}' + # github.event_name is in the key because a called workflow inherits the + # caller's ref: once this is on master, the nightly Snapshot's deb build + # and an ordinary push's deb build would otherwise share a group and, + # with cancel-in-progress, kill each other. A push landing mid-nightly + # would cancel the snapshot's build and skip the publish entirely. + group: 'check-rpm-build @ ${{ github.ref }} @ ${{ github.event_name }}' cancel-in-progress: true permissions: @@ -135,7 +140,7 @@ jobs: run: python3 -m pip install --break-system-packages sphinxcontrib-youtube || python3 -m pip install sphinxcontrib-youtube - name: Install syft - run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin + run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/v1.52.0/install.sh | sh -s -- -b /usr/local/bin v1.52.0 - uses: actions/setup-node@v7 with: @@ -160,4 +165,5 @@ jobs: uses: actions/upload-artifact@v7 with: name: pgadmin4-rpm-build-output-${{ matrix.name }} + if-no-files-found: error path: dist/*.rpm diff --git a/.github/workflows/check-tarball-build.yml b/.github/workflows/check-tarball-build.yml index cf53c63d0..8fc5162af 100644 --- a/.github/workflows/check-tarball-build.yml +++ b/.github/workflows/check-tarball-build.yml @@ -38,7 +38,12 @@ concurrency: # github.workflow, because in a reusable workflow that expression is # the CALLER's name. Every workflow Snapshot calls would otherwise # share one group and, with cancel-in-progress, cancel each other. - group: 'check-tarball-build @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + # github.event_name is in the key because a called workflow inherits the + # caller's ref: once this is on master, the nightly Snapshot's deb build + # and an ordinary push's deb build would otherwise share a group and, + # with cancel-in-progress, kill each other. A push landing mid-nightly + # would cancel the snapshot's build and skip the publish entirely. + group: 'check-tarball-build @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }} @ ${{ github.event_name }}' cancel-in-progress: true jobs: @@ -65,4 +70,5 @@ jobs: uses: actions/upload-artifact@v7 with: name: pgadmin4-tarball-build-output + if-no-files-found: error path: dist/* diff --git a/.github/workflows/check-windows-build.yml b/.github/workflows/check-windows-build.yml index 86426bb1d..df74c3d40 100644 --- a/.github/workflows/check-windows-build.yml +++ b/.github/workflows/check-windows-build.yml @@ -137,4 +137,5 @@ jobs: uses: actions/upload-artifact@v7 with: name: pgadmin4-windows-build-output + if-no-files-found: error path: dist/*.exe diff --git a/.github/workflows/snapshot-macos.yml b/.github/workflows/snapshot-macos.yml index eb284fec1..f4afdcfdb 100644 --- a/.github/workflows/snapshot-macos.yml +++ b/.github/workflows/snapshot-macos.yml @@ -151,6 +151,7 @@ jobs: uses: actions/upload-artifact@v7 with: name: pgadmin4-macos-snapshot-${{ matrix.name }} + if-no-files-found: error path: | dist/*.dmg dist/*.zip diff --git a/.github/workflows/snapshot-windows.yml b/.github/workflows/snapshot-windows.yml index b3e9591be..5e5e6bc3c 100644 --- a/.github/workflows/snapshot-windows.yml +++ b/.github/workflows/snapshot-windows.yml @@ -158,9 +158,10 @@ jobs: CALL "%VS_INSTALL_DIR%\VC\Auxiliary\Build\vcvars64.bat" || EXIT /b 1 CALL Make.bat || EXIT /b 1 - # Make.bat verifies the installer's own signature, but says nothing about - # the executable inside it, which is signed in a separate step and is the - # thing users actually run. + # Make.bat now fails the build if either signature fails, the executable + # inside the installer included, so this is a second opinion rather than + # the only check. It verifies what is in dist/, which is the installer; + # the executable within it is covered by Make.bat refusing to continue. - name: Verify the signatures shell: powershell run: | @@ -175,4 +176,5 @@ jobs: uses: actions/upload-artifact@v7 with: name: pgadmin4-windows-snapshot + if-no-files-found: error path: dist/*.exe diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 4aef82679..0956c4670 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -60,9 +60,25 @@ jobs: name: ${{ steps.pick.outputs.name }} steps: - id: pick + env: + REQUESTED: ${{ inputs.location }} run: | - NAME="${{ inputs.location || '' }}" + set -euo pipefail + NAME="${REQUESTED:-}" [ -n "${NAME}" ] || NAME=$(date +%Y-%m-%d) + + # Validated here so that everything downstream, including a shell on + # the machine holding the publishing keys, is working with a name + # that cannot be anything but a date and an optional suffix. The + # wrapper on the server applies its own rule to the same value; this + # one exists so a bad input fails in five seconds rather than after + # an hour of building. + if ! printf '%s' "${NAME}" | grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2}(-[1-9][0-9]?)?$'; then + echo "Not a valid snapshot location: ${NAME}" >&2 + echo "Expected YYYY-MM-DD, optionally with a -N suffix." >&2 + exit 1 + fi + echo "Snapshot location: ${NAME}" echo "name=${NAME}" >> "${GITHUB_OUTPUT}" @@ -76,8 +92,11 @@ jobs: uses: ./.github/workflows/check-python-build.yml docs: uses: ./.github/workflows/check-doc-builds.yml - helm: - uses: ./.github/workflows/check-helm-build.yml + # No helm job here. The chart is not part of the download tree: the + # buildfarm pushed it to an OCI registry, which nothing in this workflow + # does yet. Calling the build and then discarding its artefact, which is what + # happened before, cost a runner and proved nothing that check-helm-build + # does not already prove on every push. # The repository configuration RPMs. These are not built by the QA # workflows, because they are not a build of pgAdmin: they are a one-file @@ -98,8 +117,12 @@ jobs: - name: Build the repository RPMs env: - PGADMIN_REPO_DIR: https://ftp.postgresql.org/pub/pgadmin/pgadmin4/snapshots/${{ needs.location.outputs.name }}/yum + LOCATION: ${{ needs.location.outputs.name }} run: | + # Through the environment rather than interpolated into the script. + # The location is a dispatch input, so it is text somebody chose, and + # ${{ }} substitution happens before the shell ever sees the line. + export PGADMIN_REPO_DIR="https://ftp.postgresql.org/pub/pgadmin/pgadmin4/snapshots/${LOCATION}/yum" echo "Repositories will point at ${PGADMIN_REPO_DIR}" pkg/redhat/repo-rpm.sh @@ -107,6 +130,7 @@ jobs: uses: actions/upload-artifact@v7 with: name: pgadmin4-repo-rpm-build-output + if-no-files-found: error path: dist/*.noarch.rpm macos: @@ -132,9 +156,22 @@ jobs: # is not gated by it. environment: publishing - # The Windows signing host is the one runner that can be offline, and a - # snapshot without an installer is still worth having. - if: always() && needs.deb.result == 'success' && needs.rpm.result == 'success' + # Everything must have built except Windows. That is the one runner which + # can legitimately be offline, and a snapshot without the installer is + # still worth having; a snapshot missing its documentation, source tarball, + # wheel or repository RPMs is not, and the repository RPMs are worse than + # missing, since install-repo-readme.py then fails after the packages have + # been uploaded and signed, leaving half a snapshot on the download server. + if: >- + always() + && needs.location.result == 'success' + && needs.deb.result == 'success' + && needs.rpm.result == 'success' + && needs.tarball.result == 'success' + && needs.wheel.result == 'success' + && needs.docs.result == 'success' + && needs.repo-rpm.result == 'success' + && needs.macos.result == 'success' steps: - uses: actions/checkout@v7 @@ -150,6 +187,8 @@ jobs: # table, since dpkg already encodes it there; the codename does not, so # that mapping is explicit. - name: Assemble the staging tree + env: + LOCATION: ${{ needs.location.outputs.name }} run: | set -euo pipefail # Flat, beside apt/ and yum/, which is how both the staging server @@ -163,7 +202,7 @@ jobs: # them. rm -rf stage - STAGE="stage/${{ needs.location.outputs.name }}" + STAGE="stage/${LOCATION}" mkdir -p "${STAGE}/yum" # An artefact can be legitimately absent: the Windows signing host is diff --git a/Make.bat b/Make.bat index 3da3fec04..1cffc50f7 100644 --- a/Make.bat +++ b/Make.bat @@ -1,498 +1,507 @@ -@ECHO off -SETLOCAL - -SET WD=%CD% -SET "BUILDROOT=%WD%\win-build" -SET "TMPDIR=%WD%\win-temp" -SET "DISTROOT=%WD%\dist" - -SET CMDOPTIONS="" -IF "%1" == "clean" SET CMDOPTIONS="VALID" -IF "%1" == "" SET CMDOPTIONS="VALID" - -IF NOT %CMDOPTIONS%=="VALID" ( - GOTO USAGE -) - -IF "%1" == "clean" ( - CALL :CLEAN - EXIT /B %ERRORLEVEL% -) - -REM Main build sequence -REM -REM The clean runs first because :FETCH_PYTHON unpacks into the temp directory -REM that it removes, and the fetch runs before :SET_ENVIRONMENT because the -REM latter has no interpreter of its own to ask about the build. -CALL :CLEAN || EXIT /B 1 -CALL :FETCH_PYTHON || EXIT /B 1 -CALL :SET_ENVIRONMENT -CALL :VALIDATE_ENVIRONMENT || EXIT /B 1 -CALL :CREATE_VIRTUAL_ENV || EXIT /B 1 -CALL :CREATE_PYTHON_ENV || EXIT /B 1 -CALL :CREATE_RUNTIME_ENV || EXIT /B 1 -CALL :GENERATE_SBOM || EXIT /B 1 -CALL :CREATE_INSTALLER || EXIT /B 1 -CALL :VERIFY_SIGNATURE || EXIT /B 1 - -EXIT /B %ERRORLEVEL% -REM Main build sequence Ends - - -:CLEAN - ECHO Removing build directory... - IF EXIST "%BUILDROOT%" RD "%BUILDROOT%" /S /Q > nul || EXIT /B 1 - - ECHO Removing tmp directory... - IF EXIST "%TMPDIR%" RD "%TMPDIR%" /S /Q > nul || EXIT /B 1 - - ECHO Removing installer build directory... - IF EXIST "%WD%\pkg\win32\Output" rd "%WD%\pkg\win32\Output" /S /Q > nul || EXIT /B 1 - - ECHO Removing installer configuration script... - IF EXIST DEL /q "%WD%\pkg\win32\installer.iss" > nul || EXIT /B 1 - - EXIT /B 0 - - -:FETCH_PYTHON - REM Fetch the Python this build uses, rather than using whatever happens to - REM be installed on the machine running it. Which interpreter pgAdmin ships - REM is a decision recorded in pkg\python-version.txt, not a property of the - REM build agent, and this is the only place on Windows that reads it. - REM - REM The distribution comes from astral-sh/python-build-standalone: it is - REM relocatable, published per exact CPython version, and is a plain tarball - REM rather than an installer, so unpacking it raises no UAC prompt. That - REM matters on the signing host, where the desktop session is logged in - REM automatically and an elevation dialog waits forever for somebody who is - REM not there. - REM - REM curl.exe and tar.exe have shipped in Windows since 1803, so this needs - REM nothing installed to bootstrap itself, not even a Python. - ECHO Reading the Python version... - SET "PYTHON_VERSION=" - SET "PYTHON_BUILD_STANDALONE_TAG=" - FOR /F "eol=# usebackq tokens=1,2 delims==" %%a IN ("%WD%\pkg\python-version.txt") DO ( - IF "%%a" == "PYTHON_VERSION" SET "PYTHON_VERSION=%%b" - IF "%%a" == "PYTHON_BUILD_STANDALONE_TAG" SET "PYTHON_BUILD_STANDALONE_TAG=%%b" - ) - - IF "%PYTHON_VERSION%" == "" ( - ECHO PYTHON_VERSION is not set in pkg\python-version.txt. - EXIT /B 1 - ) - IF "%PYTHON_BUILD_STANDALONE_TAG%" == "" ( - ECHO PYTHON_BUILD_STANDALONE_TAG is not set in pkg\python-version.txt. - EXIT /B 1 - ) - - REM Split for the runtime build, where e.g. 3.13.15 becomes 313 - FOR /F "tokens=1,2,3 delims=." %%a IN ("%PYTHON_VERSION%") DO ( - SET "PYTHON_MAJOR=%%a" - SET "PYTHON_MINOR=%%b" - SET "PYTHON_REVISION=%%c" - ) - - SET "PGADMIN_PYTHON_DIR=%TMPDIR%\python" - SET "PYTHON_ARCHIVE=cpython-%PYTHON_VERSION%+%PYTHON_BUILD_STANDALONE_TAG%-x86_64-pc-windows-msvc-install_only.tar.gz" - - IF NOT EXIST "%TMPDIR%" MKDIR "%TMPDIR%" - - ECHO Downloading Python %PYTHON_VERSION%... - curl.exe --fail --location --silent --show-error --output "%TMPDIR%\python.tar.gz" "https://github.com/astral-sh/python-build-standalone/releases/download/%PYTHON_BUILD_STANDALONE_TAG%/%PYTHON_ARCHIVE%" || EXIT /B 1 - - ECHO Unpacking Python... - REM The archive unpacks to a single python\ directory. - tar -x -f "%TMPDIR%\python.tar.gz" -C "%TMPDIR%" || EXIT /B 1 - DEL /q "%TMPDIR%\python.tar.gz" > nul 2>&1 - - "%PGADMIN_PYTHON_DIR%\python.exe" --version || EXIT /B 1 - - REM :CREATE_VIRTUAL_ENV needs virtualenv rather than the venv module, as it - REM relocates the result. pip is bundled with the distribution, but ask - REM ensurepip for it anyway so this does not quietly depend on that. - ECHO Installing virtualenv... - "%PGADMIN_PYTHON_DIR%\python.exe" -m ensurepip --upgrade || EXIT /B 1 - "%PGADMIN_PYTHON_DIR%\python.exe" -m pip install --upgrade pip virtualenv || EXIT /B 1 - - EXIT /B 0 - - -:SET_ENVIRONMENT - ECHO Configuring the environment... - IF "%PGADMIN_KRB5_DIR%" == "" SET "PGADMIN_KRB5_DIR=C:\Program Files\MIT\Kerberos" - IF "%PGADMIN_POSTGRES_DIR%" == "" SET "PGADMIN_POSTGRES_DIR=C:\Program Files\PostgreSQL\17" - IF "%PGADMIN_INNOTOOL_DIR%" == "" SET "PGADMIN_INNOTOOL_DIR=C:\Program Files (x86)\Inno Setup 6" - IF "%PGADMIN_VCREDIST_DIR%" == "" SET "PGADMIN_VCREDIST_DIR=C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.40.33807" - IF "%PGADMIN_VCREDIST_FILE%" == "" SET "PGADMIN_VCREDIST_FILE=vc_redist.x64.exe" - IF "%PGADMIN_SIGNTOOL_DIR%" == "" SET "PGADMIN_SIGNTOOL_DIR=C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64" - IF "%PGADMIN_WINDOWS_CSC%" == "" SET "PGADMIN_WINDOWS_CSC=" - - REM Set additional variables we need - FOR /F "tokens=3" %%a IN ('findstr /C:"APP_RELEASE =" %WD%\web\version.py') DO SET APP_MAJOR=%%a - FOR /F "tokens=3" %%a IN ('findstr /C:"APP_REVISION =" %WD%\web\version.py') DO SET APP_MINOR=%%a - FOR /F "tokens=3" %%a IN ('findstr /C:"APP_SUFFIX =" %WD%\web\version.py') DO SET APP_VERSION_SUFFIX=%%a - REM remove single quote from the string - SET APP_VERSION_SUFFIX=%APP_VERSION_SUFFIX:'=% - SET APP_NAME="" - FOR /F "tokens=2* DELims='" %%a IN ('findstr /C:"APP_NAME =" web\branding.py') DO SET APP_NAME=%%a - FOR /f "tokens=1 DELims=." %%G IN ('%PGADMIN_PYTHON_DIR%/python.exe -c "print('%APP_NAME%'.lower().replace(' ', ''))"') DO SET APP_SHORTNAME=%%G - SET APP_VERSION=%APP_MAJOR%.%APP_MINOR% - SET INSTALLERNAME=%APP_SHORTNAME%-%APP_MAJOR%.%APP_MINOR%-%APP_VERSION_SUFFIX%-x64.exe - IF "%APP_VERSION_SUFFIX%" == "" SET INSTALLERNAME=%APP_SHORTNAME%-%APP_MAJOR%.%APP_MINOR%-x64.exe - - EXIT /B 0 - - -:VALIDATE_ENVIRONMENT - ECHO **************************************************************** - ECHO * Build summary - ECHO **************************************************************** - ECHO Build path: %BUILDROOT% - ECHO Output directory: %DISTROOT% - ECHO Installer name: %INSTALLERNAME% - ECHO. - ECHO Python version: %PYTHON_MAJOR%.%PYTHON_MINOR%.%PYTHON_REVISION% - ECHO Python directory: %PGADMIN_PYTHON_DIR% - ECHO. - ECHO KRB5 directory: %PGADMIN_KRB5_DIR% - ECHO PostgreSQL directory: %PGADMIN_POSTGRES_DIR% - ECHO. - ECHO VC++ redist directory: %PGADMIN_VCREDIST_DIR% - ECHO VC++ redist file: %PGADMIN_VCREDIST_FILE% - ECHO InnoTool directory: %PGADMIN_INNOTOOL_DIR% - ECHO signtool directory: %PGADMIN_SIGNTOOL_DIR% - IF "%PGADMIN_WINDOWS_CSC%" == "" ( - ECHO Code signing certificate: [NONE - Signing disabled] - ) ELSE ( - ECHO Code signing certificate: %PGADMIN_WINDOWS_CSC% - ) - ECHO. - ECHO App version: %APP_VERSION% - ECHO App version suffix: %APP_VERSION_SUFFIX% - ECHO App short name: %APP_SHORTNAME% - ECHO App name: %APP_NAME% - ECHO **************************************************************** - - ECHO Checking the environment... - IF NOT EXIST "%PGADMIN_INNOTOOL_DIR%" ( - ECHO !PGADMIN_INNOTOOL_DIR! does not exist - ECHO Please install InnoTool and set the PGADMIN_INNOTOOL_DIR environment variable. - EXIT /B 1 - ) - - IF NOT EXIST "%PGADMIN_VCREDIST_DIR%" ( - ECHO !PGADMIN_VCREDIST_DIR! does not exist - ECHO Please install Microsoft Visual studio and set the PGADMIN_VCREDIST_DIR environment variable. - EXIT /B 1 - ) - - IF NOT EXIST "%PGADMIN_KRB5_DIR%" ( - ECHO !PGADMIN_KRB5_DIR! does not exist. - ECHO Please install MIT Kerberos for Windows and set the PGADMIN_KRB5_DIR environment variable. - EXIT /B 1 - ) - - IF NOT EXIST "%PGADMIN_POSTGRES_DIR%" ( - ECHO !PGADMIN_POSTGRES_DIR! does not exist. - ECHO Please install PostgreSQL and set the PGADMIN_POSTGRES_DIR environment variable. - EXIT /B 1 - ) - - SET "PATH=%PGADMIN_POSTGRES_DIR%\bin;%PATH%" - - EXIT /B 0 - - -:CREATE_VIRTUAL_ENV - ECHO Creating virtual environment... - IF NOT EXIST "%TMPDIR%" MKDIR "%TMPDIR%" - - CD "%TMPDIR%" - - REM Note that we must use virtualenv.exe here, as the venv module doesn't allow python.exe to relocate. - "%PGADMIN_PYTHON_DIR%\Scripts\virtualenv.exe" venv || EXIT /B 1 - - XCOPY /S /I /E /H /Y "%PGADMIN_PYTHON_DIR%\DLLs" "%TMPDIR%\venv\DLLs" > nul || EXIT /B 1 - REM Copy the standard library, but NOT site-packages: the venv already has its - REM own seeded pip there, and overwriting only the files the system Python also - REM has leaves a mix of two pip versions behind. - ROBOCOPY /E /R:3 /W:5 /NFL /NDL /NP "%PGADMIN_PYTHON_DIR%\Lib" "%TMPDIR%\venv\Lib" /XD site-packages - CALL :CHECK_ROBOCOPY_ERROR || EXIT /B 1 - - ECHO Activating virtual environment - %TMPDIR%\venv... - CALL "%TMPDIR%\venv\Scripts\activate" || EXIT /B 1 - - ECHO Installing dependencies... - CALL python -m pip install --upgrade pip || EXIT /B 1 - CALL pip install --only-binary=cryptography -r "%WD%\requirements.txt" || EXIT /B 1 - - CD %WD% - EXIT /B 0 - - -:CREATE_PYTHON_ENV - ECHO Staging Python... - MKDIR "%BUILDROOT%\python\Lib" || EXIT /B 1 - - ECHO Downloading embedded Python %PYTHON_VERSION%... - REM What ships is the embeddable distribution from python.org, a different - REM build from the one :FETCH_PYTHON downloaded, and it has to be the same - REM CPython version: the extension modules in site-packages were compiled - REM against the build Python and are about to be run under this one. Both - REM therefore read pkg\python-version.txt, rather than either asking the - REM other what it is. - curl.exe --fail --location --silent --show-error --output "%TMPDIR%\python-embedded.zip" "https://www.python.org/ftp/python/%PYTHON_VERSION%/python-%PYTHON_VERSION%-embed-amd64.zip" || EXIT /B 1 - tar -x -f "%TMPDIR%\python-embedded.zip" -C "%BUILDROOT%\python" || EXIT /B 1 - - ECHO Copying site-packages... - XCOPY /S /I /E /H /Y "%TMPDIR%\venv\Lib\site-packages" "%BUILDROOT%\python\Lib\site-packages" > nul || EXIT /B 1 - - REM NOTE: There is intentionally no space after "site" in the line below, to prevent Python barfing if there's one in the file - ECHO import site>> "%BUILDROOT%\python\python%PYTHON_MAJOR%%PYTHON_MINOR%._pth" - - ECHO Staging Kerberos components... - COPY "%PGADMIN_KRB5_DIR%\bin\kinit.exe" "%BUILDROOT%\python" > nul || EXIT /B 1 - COPY "%PGADMIN_KRB5_DIR%\bin\krb5_64.dll" "%BUILDROOT%\python" > nul || EXIT /B 1 - COPY "%PGADMIN_KRB5_DIR%\bin\comerr64.dll" "%BUILDROOT%\python" > nul || EXIT /B 1 - COPY "%PGADMIN_KRB5_DIR%\bin\k5sprt64.dll" "%BUILDROOT%\python" > nul || EXIT /B 1 - COPY "%PGADMIN_KRB5_DIR%\bin\gssapi64.dll" "%BUILDROOT%\python" > nul || EXIT /B 1 - - ECHO Cleaning up unnecessary .pyc and .pyo files... - FOR /R "%BUILDROOT%\python" %%f in (*.pyc *.pyo) do DEL /q "%%f" 1> nul 2>&1 - ECHO Removing tests... - FOR /R "%BUILDROOT%\python\Lib" %%f in (test tests) do RD /Q /S "%%f" 1> nul 2>&1 - - EXIT /B 0 - - -:CREATE_RUNTIME_ENV - IF NOT EXIST "%BUILDROOT%" MKDIR "%BUILDROOT%" - MKDIR "%BUILDROOT%\runtime" - - ECHO Removing webpack caches... - RD /Q /S "%WD%\web\pgadmin\static\js\generated\.cache" 1> nul 2>&1 - - ECHO Copying web directory... - ROBOCOPY /S /NFL /NDL /NP "%WD%\web" "%BUILDROOT%\web" - CALL :CHECK_ROBOCOPY_ERROR || EXIT /B 1 - - ECHO Installing javascript dependencies... - CD "%BUILDROOT%\web" - SET "YARN_VERSION=" - FOR /f "delims=" %%v IN ('node -p "require('./package.json').packageManager.split('@')[1]"') DO SET "YARN_VERSION=%%v" - IF "%YARN_VERSION%"=="" ( - ECHO ERROR: Could not determine Yarn version from package.json packageManager field. - EXIT /B 1 - ) - CALL yarn set version %YARN_VERSION% || EXIT /B 1 - CALL yarn install || EXIT /B 1 - CALL npm rebuild || EXIT /B 1 - - ECHO Bundling javascript... - CALL yarn run bundle || EXIT /B 1 - - ECHO Cleaning up unnecessary .pyc and .pyo files... - FOR /R "%BUILDROOT%\web" %%f in (*.pyc *.pyo) do DEL /q "%%f" 1> nul 2>&1 - ECHO Removing tests, Python caches and node modules... - FOR /R "%BUILDROOT%\web" %%f in (tests feature_tests __pycache__ node_modules) do RD /Q /S "%%f" 1> nul 2>&1 - ECHO Removing the test framework... - RD /Q /S "%BUILDROOT%\web\regression" 1> nul 2>&1 - ECHO Removing tools... - RD /Q /S "%BUILDROOT%\web\tools" 1> nul 2>&1 - ECHO Removing the JavaScript build configuration... - RD /Q /S "%BUILDROOT%\web\.yarn" 1> nul 2>&1 - DEL /q "%BUILDROOT%\web\yarn.lock" 1> nul 2>&1 - DEL /q "%BUILDROOT%\web\.yarnrc.yml" 1> nul 2>&1 - DEL /q "%BUILDROOT%\web\package.json" 1> nul 2>&1 - DEL /q "%BUILDROOT%\web\jest.config.js" 1> nul 2>&1 - DEL /q "%BUILDROOT%\web\babel.cfg" 1> nul 2>&1 - DEL /q "%BUILDROOT%\web\babel.config.json" 1> nul 2>&1 - DEL /q "%BUILDROOT%\web\webpack.config.js" 1> nul 2>&1 - DEL /q "%BUILDROOT%\web\webpack.shim.js" 1> nul 2>&1 - DEL /q "%BUILDROOT%\web\.eslintrc.js" 1> nul 2>&1 - DEL /q "%BUILDROOT%\web\.editorconfig" 1> nul 2>&1 - ECHO Removing any existing configurations... - DEL /q "%BUILDROOT%\web\pgadmin4.db" 1> nul 2>&1 - DEL /q "%BUILDROOT%\web\config_local.py" 1> nul 2>&1 - - ECHO Staging license files... - COPY "%WD%\LICENSE" "%BUILDROOT%\" > nul || EXIT /B 1 - - ECHO Creating config_distro.py - ECHO SERVER_MODE = False > "%BUILDROOT%\web\config_distro.py" - ECHO HELP_PATH = '../../../docs/en_US/html/' >> "%BUILDROOT%\web\config_distro.py" - ECHO DEFAULT_BINARY_PATHS = { >> "%BUILDROOT%\web\config_distro.py" - ECHO 'pg': '$DIR/../runtime', >> "%BUILDROOT%\web\config_distro.py" - ECHO 'ppas': '' >> "%BUILDROOT%\web\config_distro.py" - ECHO } >> "%BUILDROOT%\web\config_distro.py" - - ECHO Building docs... - CALL pip install sphinx || EXIT /B 1 - CALL pip install sphinxcontrib-youtube || EXIT /B 1 - MKDIR "%BUILDROOT%\docs\en_US\html" - CD "%WD%\docs\en_US" - CALL "%TMPDIR%\venv\Scripts\python.exe" build_code_snippet.py || EXIT /B 1 - CALL "%TMPDIR%\venv\Scripts\sphinx-build.exe" "%WD%\docs\en_US" "%BUILDROOT%\docs\en_US\html" || EXIT /B 1 - - REM Remove unnecessary doc files - DEL /q "%BUILDROOT%\docs\en_US\html\_static\*.png" 1> nul 2>&1 - RD /Q /S "%BUILDROOT%\docs\en_US\html\_sources" 1> nul 2>&1 - - ECHO Staging runtime components... - MKDIR "%BUILDROOT%\runtime\resources\app" - XCOPY /S /I /E /H /Y "%WD%\runtime\assets" "%BUILDROOT%\runtime\resources\app\assets" > nul || EXIT /B 1 - XCOPY /S /I /E /H /Y "%WD%\runtime\src" "%BUILDROOT%\runtime\resources\app\src" > nul || EXIT /B 1 - - COPY "%WD%\runtime\package.json" "%BUILDROOT%\runtime\resources\app\" > nul || EXIT /B 1 - COPY "%WD%\runtime\.yarnrc.yml" "%BUILDROOT%\runtime\resources\app\" > nul || EXIT /B 1 - - CD "%BUILDROOT%\runtime\resources\app\" - - SET "YARN_VERSION=" - FOR /f "delims=" %%v IN ('node -p "require('./package.json').packageManager.split('@')[1]"') DO SET "YARN_VERSION=%%v" - IF "%YARN_VERSION%"=="" ( - ECHO ERROR: Could not determine Yarn version from package.json packageManager field. - EXIT /B 1 - ) - CALL yarn set version %YARN_VERSION% || EXIT /B 1 - CALL yarn workspaces focus --production || EXIT /B 1 - - ECHO Removing yarn cache... - RD /Q /S "%BUILDROOT%\runtime\resources\app\.yarn" 1> nul 2>&1 - - ECHO Downloading Electron to %TMPDIR%... - REM Get a fresh copy of electron. - - REM WGET - FOR /f "tokens=*" %%i IN ('npm info electron version') DO SET "ELECTRON_VERSION=%%i" - - :GET_NW - wget https://github.com/electron/electron/releases/download/v%ELECTRON_VERSION%/electron-v%ELECTRON_VERSION%-win32-x64.zip -O "%TMPDIR%\electron-v%ELECTRON_VERSION%-win32-x64.zip" - IF %ERRORLEVEL% NEQ 0 GOTO GET_NW - - MKDIR "%TMPDIR%\electron-v%ELECTRON_VERSION%-win32-x64" || EXIT /B 1 - tar -C "%TMPDIR%\electron-v%ELECTRON_VERSION%-win32-x64" -xvf "%TMPDIR%\electron-v%ELECTRON_VERSION%-win32-x64.zip" || EXIT /B 1 - REM WGET END - - REM XCOPY - XCOPY /S /I /E /H /Y "%TMPDIR%\electron-v%ELECTRON_VERSION%-win32-x64\*" "%BUILDROOT%\runtime" > nul || EXIT /B 1 - REM XCOPY END - - MOVE "%BUILDROOT%\runtime\electron.exe" "%BUILDROOT%\runtime\pgAdmin4.exe" - - ECHO Downloading rcedit.exe... - wget https://github.com/electron/rcedit/releases/download/v2.0.0/rcedit-x64.exe -O "%TMPDIR%\rcedit-x64.exe" - - ECHO Replacing executable icon, description, version... - %TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-icon "%WD%\pkg\win32\Resources\pgAdmin4.ico" - %TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-version-string "FileDescription" "%APP_NAME%" - %TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-version-string "ProductName" "%APP_NAME%" - %TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-product-version "%APP_VERSION%" - - IF NOT "%PGADMIN_WINDOWS_CSC%" == "" ( - ECHO Attempting to sign the pgAdmin4.exe... - CALL "%PGADMIN_SIGNTOOL_DIR%\signtool.exe" sign /sm /n "%PGADMIN_WINDOWS_CSC%" /tr http://timestamp.digicert.com /td sha256 /fd sha1 /v "%BUILDROOT%\runtime\pgAdmin4.exe" - IF %ERRORLEVEL% NEQ 0 ( - ECHO. - ECHO ************************************************************ - ECHO * Failed to sign the pgAdmin4.exe - ECHO ************************************************************ - PAUSE - ) - ) ELSE ( - ECHO Skipping code signing ^(PGADMIN_WINDOWS_CSC is not set^)... - ) - - ECHO Staging PostgreSQL components... - COPY "%PGADMIN_POSTGRES_DIR%\bin\libpq.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1 - COPY "%PGADMIN_POSTGRES_DIR%\bin\libcrypto-*-x64.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1 - COPY "%PGADMIN_POSTGRES_DIR%\bin\libssl-*-x64.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1 - IF EXIST "%PGADMIN_POSTGRES_DIR%\bin\libintl-*.dll" COPY "%PGADMIN_POSTGRES_DIR%\bin\libintl-*.dll" "%BUILDROOT%\runtime" > nul - IF EXIST "%PGADMIN_POSTGRES_DIR%\bin\libiconv-*.dll" COPY "%PGADMIN_POSTGRES_DIR%\bin\libiconv-*.dll" "%BUILDROOT%\runtime" > nul - IF EXIST "%PGADMIN_POSTGRES_DIR%\bin\liblz4.dll" COPY "%PGADMIN_POSTGRES_DIR%\bin\liblz4.dll" "%BUILDROOT%\runtime" > nul - IF EXIST "%PGADMIN_POSTGRES_DIR%\bin\libzstd.dll" COPY "%PGADMIN_POSTGRES_DIR%\bin\libzstd.dll" "%BUILDROOT%\runtime" > nul - COPY "%PGADMIN_POSTGRES_DIR%\bin\zlib1.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1 - COPY "%PGADMIN_POSTGRES_DIR%\bin\pg_dump.exe" "%BUILDROOT%\runtime" > nul || EXIT /B 1 - COPY "%PGADMIN_POSTGRES_DIR%\bin\pg_dumpall.exe" "%BUILDROOT%\runtime" > nul || EXIT /B 1L% - COPY "%PGADMIN_POSTGRES_DIR%\bin\pg_restore.exe" "%BUILDROOT%\runtime" > nul || EXIT /B 1 - COPY "%PGADMIN_POSTGRES_DIR%\bin\psql.exe" "%BUILDROOT%\runtime" > nul || EXIT /B 1 - - ECHO Staging VC++ runtime... - MKDIR "%BUILDROOT%\installer" || EXIT /B 1 - COPY "%PGADMIN_VCREDIST_DIR%\%PGADMIN_VCREDIST_FILE%" "%BUILDROOT%\installer" > nul || EXIT /B 1 - - CD %WD% - EXIT /B 0 - - -:CREATE_INSTALLER - ECHO Preparing for creation of windows installer... - IF NOT EXIST "%DISTROOT%" MKDIR "%DISTROOT%" - - ECHO Copying icon file... - COPY "%WD%\pkg\win32\Resources\pgAdmin4.ico" "%BUILDROOT%" > nul || EXIT /B 1 - - CD "%WD%\pkg\win32" - - ECHO Processing installer configuration script... - CALL "%PGADMIN_PYTHON_DIR%\python" "%WD%\pkg\win32\replace.py" "-i" "%WD%\pkg\win32\installer.iss.in" "-o" "%WD%\pkg\win32\installer.iss.in_stage1" "-s" MYAPP_FULLVERSION -r """%APP_VERSION%""" - CALL "%PGADMIN_PYTHON_DIR%\python" "%WD%\pkg\win32\replace.py" "-i" "%WD%\pkg\win32\installer.iss.in_stage1" "-o" "%WD%\pkg\win32\installer.iss.in_stage2" "-s" MYAPP_VERSION -r """v%APP_MAJOR%""" - CALL "%PGADMIN_PYTHON_DIR%\python" "%WD%\pkg\win32\replace.py" "-i" "%WD%\pkg\win32\installer.iss.in_stage2" "-o" "%WD%\pkg\win32\installer.iss" "-s" MYAPP_VCDIST -r """%PGADMIN_VCREDIST_DIRNAME%\%PGADMIN_VCREDIST_FILE%""" - - ECHO Cleaning up... - DEL /s "%WD%\pkg\win32\installer.iss.in_stage*" > nul - - ECHO Creating windows installer using INNO tool... - IF NOT "%PGADMIN_WINDOWS_CSC%" == "" ( - CALL "%PGADMIN_INNOTOOL_DIR%\ISCC.exe" "%WD%\pkg\win32\installer.iss" "/DSIGNED" "/SpgAdminSigntool=%PGADMIN_SIGNTOOL_DIR%\signtool.exe sign /sm /n $q%PGADMIN_WINDOWS_CSC%$q /tr http://timestamp.digicert.com /td sha256 /fd sha1 /v $f" || EXIT /B 1 - ) ELSE ( - CALL "%PGADMIN_INNOTOOL_DIR%\ISCC.exe" "%WD%\pkg\win32\installer.iss" || EXIT /B 1 - ) - - ECHO Renaming installer... - MOVE "%WD%\pkg\win32\Output\pgadmin4-setup.exe" "%DISTROOT%\%INSTALLERNAME%" > nul || EXIT /B 1 - - ECHO Location - %DISTROOT%\%INSTALLERNAME% - ECHO Installer generated successfully. - - CD %WD% - EXIT /B 0 - -:GENERATE_SBOM - ECHO Generating SBOM... - CALL syft "%BUILDROOT%" -o cyclonedx-json > "%BUILDROOT%\sbom.json" - - EXIT /B 0 - -:VERIFY_SIGNATURE - IF "%PGADMIN_WINDOWS_CSC%" == "" ( - ECHO Skipping signature verification ^(PGADMIN_WINDOWS_CSC is not set^)... - EXIT /B 0 - ) - - ECHO Verifying the installer signature... - - CALL "%PGADMIN_SIGNTOOL_DIR%\signtool.exe" verify /pa /v "%DISTROOT%\%INSTALLERNAME%" - IF %ERRORLEVEL% NEQ 0 ( - ECHO. - ECHO ************************************************************ - ECHO * Failed to verify signature of the installer - ECHO ************************************************************ - PAUSE - ) - - EXIT /B 0 - - -:USAGE - ECHO Invalid command line options. - ECHO Usage: "Make.bat [clean]" - ECHO. - - EXIT /B 1 - -:CHECK_ROBOCOPY_ERROR - IF %ERRORLEVEL% GEQ 8 EXIT /B %ERRORLEVEL% - EXIT /B 0 +@ECHO off +SETLOCAL + +SET WD=%CD% +SET "BUILDROOT=%WD%\win-build" +SET "TMPDIR=%WD%\win-temp" +SET "DISTROOT=%WD%\dist" + +SET CMDOPTIONS="" +IF "%1" == "clean" SET CMDOPTIONS="VALID" +IF "%1" == "" SET CMDOPTIONS="VALID" + +IF NOT %CMDOPTIONS%=="VALID" ( + GOTO USAGE +) + +IF "%1" == "clean" ( + CALL :CLEAN + EXIT /B %ERRORLEVEL% +) + +REM Main build sequence +REM +REM The clean runs first because :FETCH_PYTHON unpacks into the temp directory +REM that it removes, and the fetch runs before :SET_ENVIRONMENT because the +REM latter has no interpreter of its own to ask about the build. +CALL :CLEAN || EXIT /B 1 +CALL :FETCH_PYTHON || EXIT /B 1 +CALL :SET_ENVIRONMENT +CALL :VALIDATE_ENVIRONMENT || EXIT /B 1 +CALL :CREATE_VIRTUAL_ENV || EXIT /B 1 +CALL :CREATE_PYTHON_ENV || EXIT /B 1 +CALL :CREATE_RUNTIME_ENV || EXIT /B 1 +CALL :GENERATE_SBOM || EXIT /B 1 +CALL :CREATE_INSTALLER || EXIT /B 1 +CALL :VERIFY_SIGNATURE || EXIT /B 1 + +EXIT /B %ERRORLEVEL% +REM Main build sequence Ends + + +:CLEAN + ECHO Removing build directory... + IF EXIST "%BUILDROOT%" RD "%BUILDROOT%" /S /Q > nul || EXIT /B 1 + + ECHO Removing tmp directory... + IF EXIST "%TMPDIR%" RD "%TMPDIR%" /S /Q > nul || EXIT /B 1 + + ECHO Removing installer build directory... + IF EXIST "%WD%\pkg\win32\Output" rd "%WD%\pkg\win32\Output" /S /Q > nul || EXIT /B 1 + + ECHO Removing installer configuration script... + IF EXIST DEL /q "%WD%\pkg\win32\installer.iss" > nul || EXIT /B 1 + + EXIT /B 0 + + +:FETCH_PYTHON + REM Fetch the Python this build uses, rather than using whatever happens to + REM be installed on the machine running it. Which interpreter pgAdmin ships + REM is a decision recorded in pkg\python-version.txt, not a property of the + REM build agent, and this is the only place on Windows that reads it. + REM + REM The distribution comes from astral-sh/python-build-standalone: it is + REM relocatable, published per exact CPython version, and is a plain tarball + REM rather than an installer, so unpacking it raises no UAC prompt. That + REM matters on the signing host, where the desktop session is logged in + REM automatically and an elevation dialog waits forever for somebody who is + REM not there. + REM + REM curl.exe and tar.exe have shipped in Windows since 1803, so this needs + REM nothing installed to bootstrap itself, not even a Python. + ECHO Reading the Python version... + SET "PYTHON_VERSION=" + SET "PYTHON_BUILD_STANDALONE_TAG=" + FOR /F "eol=# usebackq tokens=1,2 delims==" %%a IN ("%WD%\pkg\python-version.txt") DO ( + IF "%%a" == "PYTHON_VERSION" SET "PYTHON_VERSION=%%b" + IF "%%a" == "PYTHON_BUILD_STANDALONE_TAG" SET "PYTHON_BUILD_STANDALONE_TAG=%%b" + ) + + IF "%PYTHON_VERSION%" == "" ( + ECHO PYTHON_VERSION is not set in pkg\python-version.txt. + EXIT /B 1 + ) + IF "%PYTHON_BUILD_STANDALONE_TAG%" == "" ( + ECHO PYTHON_BUILD_STANDALONE_TAG is not set in pkg\python-version.txt. + EXIT /B 1 + ) + + REM Split for the runtime build, where e.g. 3.13.15 becomes 313 + FOR /F "tokens=1,2,3 delims=." %%a IN ("%PYTHON_VERSION%") DO ( + SET "PYTHON_MAJOR=%%a" + SET "PYTHON_MINOR=%%b" + SET "PYTHON_REVISION=%%c" + ) + + SET "PGADMIN_PYTHON_DIR=%TMPDIR%\python" + SET "PYTHON_ARCHIVE=cpython-%PYTHON_VERSION%+%PYTHON_BUILD_STANDALONE_TAG%-x86_64-pc-windows-msvc-install_only.tar.gz" + + IF NOT EXIST "%TMPDIR%" MKDIR "%TMPDIR%" + + ECHO Downloading Python %PYTHON_VERSION%... + curl.exe --fail --location --silent --show-error --output "%TMPDIR%\python.tar.gz" "https://github.com/astral-sh/python-build-standalone/releases/download/%PYTHON_BUILD_STANDALONE_TAG%/%PYTHON_ARCHIVE%" || EXIT /B 1 + + ECHO Unpacking Python... + REM The archive unpacks to a single python\ directory. + tar -x -f "%TMPDIR%\python.tar.gz" -C "%TMPDIR%" || EXIT /B 1 + DEL /q "%TMPDIR%\python.tar.gz" > nul 2>&1 + + "%PGADMIN_PYTHON_DIR%\python.exe" --version || EXIT /B 1 + + REM :CREATE_VIRTUAL_ENV needs virtualenv rather than the venv module, as it + REM relocates the result. pip is bundled with the distribution, but ask + REM ensurepip for it anyway so this does not quietly depend on that. + ECHO Installing virtualenv... + "%PGADMIN_PYTHON_DIR%\python.exe" -m ensurepip --upgrade || EXIT /B 1 + "%PGADMIN_PYTHON_DIR%\python.exe" -m pip install --upgrade pip virtualenv || EXIT /B 1 + + EXIT /B 0 + + +:SET_ENVIRONMENT + ECHO Configuring the environment... + IF "%PGADMIN_KRB5_DIR%" == "" SET "PGADMIN_KRB5_DIR=C:\Program Files\MIT\Kerberos" + IF "%PGADMIN_POSTGRES_DIR%" == "" SET "PGADMIN_POSTGRES_DIR=C:\Program Files\PostgreSQL\17" + IF "%PGADMIN_INNOTOOL_DIR%" == "" SET "PGADMIN_INNOTOOL_DIR=C:\Program Files (x86)\Inno Setup 6" + IF "%PGADMIN_VCREDIST_DIR%" == "" SET "PGADMIN_VCREDIST_DIR=C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.40.33807" + IF "%PGADMIN_VCREDIST_FILE%" == "" SET "PGADMIN_VCREDIST_FILE=vc_redist.x64.exe" + IF "%PGADMIN_SIGNTOOL_DIR%" == "" SET "PGADMIN_SIGNTOOL_DIR=C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64" + IF "%PGADMIN_WINDOWS_CSC%" == "" SET "PGADMIN_WINDOWS_CSC=" + + REM Set additional variables we need + FOR /F "tokens=3" %%a IN ('findstr /C:"APP_RELEASE =" %WD%\web\version.py') DO SET APP_MAJOR=%%a + FOR /F "tokens=3" %%a IN ('findstr /C:"APP_REVISION =" %WD%\web\version.py') DO SET APP_MINOR=%%a + FOR /F "tokens=3" %%a IN ('findstr /C:"APP_SUFFIX =" %WD%\web\version.py') DO SET APP_VERSION_SUFFIX=%%a + REM remove single quote from the string + SET APP_VERSION_SUFFIX=%APP_VERSION_SUFFIX:'=% + SET APP_NAME="" + FOR /F "tokens=2* DELims='" %%a IN ('findstr /C:"APP_NAME =" web\branding.py') DO SET APP_NAME=%%a + FOR /f "tokens=1 DELims=." %%G IN ('%PGADMIN_PYTHON_DIR%/python.exe -c "print('%APP_NAME%'.lower().replace(' ', ''))"') DO SET APP_SHORTNAME=%%G + SET APP_VERSION=%APP_MAJOR%.%APP_MINOR% + SET INSTALLERNAME=%APP_SHORTNAME%-%APP_MAJOR%.%APP_MINOR%-%APP_VERSION_SUFFIX%-x64.exe + IF "%APP_VERSION_SUFFIX%" == "" SET INSTALLERNAME=%APP_SHORTNAME%-%APP_MAJOR%.%APP_MINOR%-x64.exe + + EXIT /B 0 + + +:VALIDATE_ENVIRONMENT + ECHO **************************************************************** + ECHO * Build summary + ECHO **************************************************************** + ECHO Build path: %BUILDROOT% + ECHO Output directory: %DISTROOT% + ECHO Installer name: %INSTALLERNAME% + ECHO. + ECHO Python version: %PYTHON_MAJOR%.%PYTHON_MINOR%.%PYTHON_REVISION% + ECHO Python directory: %PGADMIN_PYTHON_DIR% + ECHO. + ECHO KRB5 directory: %PGADMIN_KRB5_DIR% + ECHO PostgreSQL directory: %PGADMIN_POSTGRES_DIR% + ECHO. + ECHO VC++ redist directory: %PGADMIN_VCREDIST_DIR% + ECHO VC++ redist file: %PGADMIN_VCREDIST_FILE% + ECHO InnoTool directory: %PGADMIN_INNOTOOL_DIR% + ECHO signtool directory: %PGADMIN_SIGNTOOL_DIR% + IF "%PGADMIN_WINDOWS_CSC%" == "" ( + ECHO Code signing certificate: [NONE - Signing disabled] + ) ELSE ( + ECHO Code signing certificate: %PGADMIN_WINDOWS_CSC% + ) + ECHO. + ECHO App version: %APP_VERSION% + ECHO App version suffix: %APP_VERSION_SUFFIX% + ECHO App short name: %APP_SHORTNAME% + ECHO App name: %APP_NAME% + ECHO **************************************************************** + + ECHO Checking the environment... + IF NOT EXIST "%PGADMIN_INNOTOOL_DIR%" ( + ECHO !PGADMIN_INNOTOOL_DIR! does not exist + ECHO Please install InnoTool and set the PGADMIN_INNOTOOL_DIR environment variable. + EXIT /B 1 + ) + + IF NOT EXIST "%PGADMIN_VCREDIST_DIR%" ( + ECHO !PGADMIN_VCREDIST_DIR! does not exist + ECHO Please install Microsoft Visual studio and set the PGADMIN_VCREDIST_DIR environment variable. + EXIT /B 1 + ) + + IF NOT EXIST "%PGADMIN_KRB5_DIR%" ( + ECHO !PGADMIN_KRB5_DIR! does not exist. + ECHO Please install MIT Kerberos for Windows and set the PGADMIN_KRB5_DIR environment variable. + EXIT /B 1 + ) + + IF NOT EXIST "%PGADMIN_POSTGRES_DIR%" ( + ECHO !PGADMIN_POSTGRES_DIR! does not exist. + ECHO Please install PostgreSQL and set the PGADMIN_POSTGRES_DIR environment variable. + EXIT /B 1 + ) + + SET "PATH=%PGADMIN_POSTGRES_DIR%\bin;%PATH%" + + EXIT /B 0 + + +:CREATE_VIRTUAL_ENV + ECHO Creating virtual environment... + IF NOT EXIST "%TMPDIR%" MKDIR "%TMPDIR%" + + CD "%TMPDIR%" + + REM Note that we must use virtualenv.exe here, as the venv module doesn't allow python.exe to relocate. + "%PGADMIN_PYTHON_DIR%\Scripts\virtualenv.exe" venv || EXIT /B 1 + + XCOPY /S /I /E /H /Y "%PGADMIN_PYTHON_DIR%\DLLs" "%TMPDIR%\venv\DLLs" > nul || EXIT /B 1 + REM Copy the standard library, but NOT site-packages: the venv already has its + REM own seeded pip there, and overwriting only the files the system Python also + REM has leaves a mix of two pip versions behind. + ROBOCOPY /E /R:3 /W:5 /NFL /NDL /NP "%PGADMIN_PYTHON_DIR%\Lib" "%TMPDIR%\venv\Lib" /XD site-packages + CALL :CHECK_ROBOCOPY_ERROR || EXIT /B 1 + + ECHO Activating virtual environment - %TMPDIR%\venv... + CALL "%TMPDIR%\venv\Scripts\activate" || EXIT /B 1 + + ECHO Installing dependencies... + CALL python -m pip install --upgrade pip || EXIT /B 1 + CALL pip install --only-binary=cryptography -r "%WD%\requirements.txt" || EXIT /B 1 + + CD %WD% + EXIT /B 0 + + +:CREATE_PYTHON_ENV + ECHO Staging Python... + MKDIR "%BUILDROOT%\python\Lib" || EXIT /B 1 + + ECHO Downloading embedded Python %PYTHON_VERSION%... + REM What ships is the embeddable distribution from python.org, a different + REM build from the one :FETCH_PYTHON downloaded, and it has to be the same + REM CPython version: the extension modules in site-packages were compiled + REM against the build Python and are about to be run under this one. Both + REM therefore read pkg\python-version.txt, rather than either asking the + REM other what it is. + curl.exe --fail --location --silent --show-error --output "%TMPDIR%\python-embedded.zip" "https://www.python.org/ftp/python/%PYTHON_VERSION%/python-%PYTHON_VERSION%-embed-amd64.zip" || EXIT /B 1 + tar -x -f "%TMPDIR%\python-embedded.zip" -C "%BUILDROOT%\python" || EXIT /B 1 + + ECHO Copying site-packages... + XCOPY /S /I /E /H /Y "%TMPDIR%\venv\Lib\site-packages" "%BUILDROOT%\python\Lib\site-packages" > nul || EXIT /B 1 + + REM NOTE: There is intentionally no space after "site" in the line below, to prevent Python barfing if there's one in the file + ECHO import site>> "%BUILDROOT%\python\python%PYTHON_MAJOR%%PYTHON_MINOR%._pth" + + ECHO Staging Kerberos components... + COPY "%PGADMIN_KRB5_DIR%\bin\kinit.exe" "%BUILDROOT%\python" > nul || EXIT /B 1 + COPY "%PGADMIN_KRB5_DIR%\bin\krb5_64.dll" "%BUILDROOT%\python" > nul || EXIT /B 1 + COPY "%PGADMIN_KRB5_DIR%\bin\comerr64.dll" "%BUILDROOT%\python" > nul || EXIT /B 1 + COPY "%PGADMIN_KRB5_DIR%\bin\k5sprt64.dll" "%BUILDROOT%\python" > nul || EXIT /B 1 + COPY "%PGADMIN_KRB5_DIR%\bin\gssapi64.dll" "%BUILDROOT%\python" > nul || EXIT /B 1 + + ECHO Cleaning up unnecessary .pyc and .pyo files... + FOR /R "%BUILDROOT%\python" %%f in (*.pyc *.pyo) do DEL /q "%%f" 1> nul 2>&1 + ECHO Removing tests... + FOR /R "%BUILDROOT%\python\Lib" %%f in (test tests) do RD /Q /S "%%f" 1> nul 2>&1 + + EXIT /B 0 + + +:CREATE_RUNTIME_ENV + IF NOT EXIST "%BUILDROOT%" MKDIR "%BUILDROOT%" + MKDIR "%BUILDROOT%\runtime" + + ECHO Removing webpack caches... + RD /Q /S "%WD%\web\pgadmin\static\js\generated\.cache" 1> nul 2>&1 + + ECHO Copying web directory... + ROBOCOPY /S /NFL /NDL /NP "%WD%\web" "%BUILDROOT%\web" + CALL :CHECK_ROBOCOPY_ERROR || EXIT /B 1 + + ECHO Installing javascript dependencies... + CD "%BUILDROOT%\web" + SET "YARN_VERSION=" + FOR /f "delims=" %%v IN ('node -p "require('./package.json').packageManager.split('@')[1]"') DO SET "YARN_VERSION=%%v" + IF "%YARN_VERSION%"=="" ( + ECHO ERROR: Could not determine Yarn version from package.json packageManager field. + EXIT /B 1 + ) + CALL yarn set version %YARN_VERSION% || EXIT /B 1 + CALL yarn install || EXIT /B 1 + CALL npm rebuild || EXIT /B 1 + + ECHO Bundling javascript... + CALL yarn run bundle || EXIT /B 1 + + ECHO Cleaning up unnecessary .pyc and .pyo files... + FOR /R "%BUILDROOT%\web" %%f in (*.pyc *.pyo) do DEL /q "%%f" 1> nul 2>&1 + ECHO Removing tests, Python caches and node modules... + FOR /R "%BUILDROOT%\web" %%f in (tests feature_tests __pycache__ node_modules) do RD /Q /S "%%f" 1> nul 2>&1 + ECHO Removing the test framework... + RD /Q /S "%BUILDROOT%\web\regression" 1> nul 2>&1 + ECHO Removing tools... + RD /Q /S "%BUILDROOT%\web\tools" 1> nul 2>&1 + ECHO Removing the JavaScript build configuration... + RD /Q /S "%BUILDROOT%\web\.yarn" 1> nul 2>&1 + DEL /q "%BUILDROOT%\web\yarn.lock" 1> nul 2>&1 + DEL /q "%BUILDROOT%\web\.yarnrc.yml" 1> nul 2>&1 + DEL /q "%BUILDROOT%\web\package.json" 1> nul 2>&1 + DEL /q "%BUILDROOT%\web\jest.config.js" 1> nul 2>&1 + DEL /q "%BUILDROOT%\web\babel.cfg" 1> nul 2>&1 + DEL /q "%BUILDROOT%\web\babel.config.json" 1> nul 2>&1 + DEL /q "%BUILDROOT%\web\webpack.config.js" 1> nul 2>&1 + DEL /q "%BUILDROOT%\web\webpack.shim.js" 1> nul 2>&1 + DEL /q "%BUILDROOT%\web\.eslintrc.js" 1> nul 2>&1 + DEL /q "%BUILDROOT%\web\.editorconfig" 1> nul 2>&1 + ECHO Removing any existing configurations... + DEL /q "%BUILDROOT%\web\pgadmin4.db" 1> nul 2>&1 + DEL /q "%BUILDROOT%\web\config_local.py" 1> nul 2>&1 + + ECHO Staging license files... + COPY "%WD%\LICENSE" "%BUILDROOT%\" > nul || EXIT /B 1 + + ECHO Creating config_distro.py + ECHO SERVER_MODE = False > "%BUILDROOT%\web\config_distro.py" + ECHO HELP_PATH = '../../../docs/en_US/html/' >> "%BUILDROOT%\web\config_distro.py" + ECHO DEFAULT_BINARY_PATHS = { >> "%BUILDROOT%\web\config_distro.py" + ECHO 'pg': '$DIR/../runtime', >> "%BUILDROOT%\web\config_distro.py" + ECHO 'ppas': '' >> "%BUILDROOT%\web\config_distro.py" + ECHO } >> "%BUILDROOT%\web\config_distro.py" + + ECHO Building docs... + CALL pip install sphinx || EXIT /B 1 + CALL pip install sphinxcontrib-youtube || EXIT /B 1 + MKDIR "%BUILDROOT%\docs\en_US\html" + CD "%WD%\docs\en_US" + CALL "%TMPDIR%\venv\Scripts\python.exe" build_code_snippet.py || EXIT /B 1 + CALL "%TMPDIR%\venv\Scripts\sphinx-build.exe" "%WD%\docs\en_US" "%BUILDROOT%\docs\en_US\html" || EXIT /B 1 + + REM Remove unnecessary doc files + DEL /q "%BUILDROOT%\docs\en_US\html\_static\*.png" 1> nul 2>&1 + RD /Q /S "%BUILDROOT%\docs\en_US\html\_sources" 1> nul 2>&1 + + ECHO Staging runtime components... + MKDIR "%BUILDROOT%\runtime\resources\app" + XCOPY /S /I /E /H /Y "%WD%\runtime\assets" "%BUILDROOT%\runtime\resources\app\assets" > nul || EXIT /B 1 + XCOPY /S /I /E /H /Y "%WD%\runtime\src" "%BUILDROOT%\runtime\resources\app\src" > nul || EXIT /B 1 + + COPY "%WD%\runtime\package.json" "%BUILDROOT%\runtime\resources\app\" > nul || EXIT /B 1 + COPY "%WD%\runtime\.yarnrc.yml" "%BUILDROOT%\runtime\resources\app\" > nul || EXIT /B 1 + + CD "%BUILDROOT%\runtime\resources\app\" + + SET "YARN_VERSION=" + FOR /f "delims=" %%v IN ('node -p "require('./package.json').packageManager.split('@')[1]"') DO SET "YARN_VERSION=%%v" + IF "%YARN_VERSION%"=="" ( + ECHO ERROR: Could not determine Yarn version from package.json packageManager field. + EXIT /B 1 + ) + CALL yarn set version %YARN_VERSION% || EXIT /B 1 + CALL yarn workspaces focus --production || EXIT /B 1 + + ECHO Removing yarn cache... + RD /Q /S "%BUILDROOT%\runtime\resources\app\.yarn" 1> nul 2>&1 + + ECHO Downloading Electron to %TMPDIR%... + REM Get a fresh copy of electron. + + REM WGET + FOR /f "tokens=*" %%i IN ('npm info electron version') DO SET "ELECTRON_VERSION=%%i" + + :GET_NW + wget https://github.com/electron/electron/releases/download/v%ELECTRON_VERSION%/electron-v%ELECTRON_VERSION%-win32-x64.zip -O "%TMPDIR%\electron-v%ELECTRON_VERSION%-win32-x64.zip" + IF %ERRORLEVEL% NEQ 0 GOTO GET_NW + + MKDIR "%TMPDIR%\electron-v%ELECTRON_VERSION%-win32-x64" || EXIT /B 1 + tar -C "%TMPDIR%\electron-v%ELECTRON_VERSION%-win32-x64" -xvf "%TMPDIR%\electron-v%ELECTRON_VERSION%-win32-x64.zip" || EXIT /B 1 + REM WGET END + + REM XCOPY + XCOPY /S /I /E /H /Y "%TMPDIR%\electron-v%ELECTRON_VERSION%-win32-x64\*" "%BUILDROOT%\runtime" > nul || EXIT /B 1 + REM XCOPY END + + MOVE "%BUILDROOT%\runtime\electron.exe" "%BUILDROOT%\runtime\pgAdmin4.exe" + + ECHO Downloading rcedit.exe... + wget https://github.com/electron/rcedit/releases/download/v2.0.0/rcedit-x64.exe -O "%TMPDIR%\rcedit-x64.exe" + + ECHO Replacing executable icon, description, version... + %TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-icon "%WD%\pkg\win32\Resources\pgAdmin4.ico" + %TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-version-string "FileDescription" "%APP_NAME%" + %TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-version-string "ProductName" "%APP_NAME%" + %TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-product-version "%APP_VERSION%" + + IF NOT "%PGADMIN_WINDOWS_CSC%" == "" ( + ECHO Attempting to sign the pgAdmin4.exe... + CALL "%PGADMIN_SIGNTOOL_DIR%\signtool.exe" sign /sm /n "%PGADMIN_WINDOWS_CSC%" /tr http://timestamp.digicert.com /td sha256 /fd sha1 /v "%BUILDROOT%\runtime\pgAdmin4.exe" + REM IF ERRORLEVEL, not IF %ERRORLEVEL%: inside a parenthesised block the + REM latter is expanded when the block is parsed, before signtool has run, + REM so it tested a stale value and this branch never fired. A failed + REM signature on the executable users actually run was undetectable, and + REM the installer around it was signed and verified regardless. + IF ERRORLEVEL 1 ( + ECHO. + ECHO ************************************************************ + ECHO * Failed to sign the pgAdmin4.exe + ECHO ************************************************************ + EXIT /B 1 + ) + ) ELSE ( + ECHO Skipping code signing ^(PGADMIN_WINDOWS_CSC is not set^)... + ) + + ECHO Staging PostgreSQL components... + COPY "%PGADMIN_POSTGRES_DIR%\bin\libpq.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1 + COPY "%PGADMIN_POSTGRES_DIR%\bin\libcrypto-*-x64.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1 + COPY "%PGADMIN_POSTGRES_DIR%\bin\libssl-*-x64.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1 + IF EXIST "%PGADMIN_POSTGRES_DIR%\bin\libintl-*.dll" COPY "%PGADMIN_POSTGRES_DIR%\bin\libintl-*.dll" "%BUILDROOT%\runtime" > nul + IF EXIST "%PGADMIN_POSTGRES_DIR%\bin\libiconv-*.dll" COPY "%PGADMIN_POSTGRES_DIR%\bin\libiconv-*.dll" "%BUILDROOT%\runtime" > nul + IF EXIST "%PGADMIN_POSTGRES_DIR%\bin\liblz4.dll" COPY "%PGADMIN_POSTGRES_DIR%\bin\liblz4.dll" "%BUILDROOT%\runtime" > nul + IF EXIST "%PGADMIN_POSTGRES_DIR%\bin\libzstd.dll" COPY "%PGADMIN_POSTGRES_DIR%\bin\libzstd.dll" "%BUILDROOT%\runtime" > nul + COPY "%PGADMIN_POSTGRES_DIR%\bin\zlib1.dll" "%BUILDROOT%\runtime" > nul || EXIT /B 1 + COPY "%PGADMIN_POSTGRES_DIR%\bin\pg_dump.exe" "%BUILDROOT%\runtime" > nul || EXIT /B 1 + COPY "%PGADMIN_POSTGRES_DIR%\bin\pg_dumpall.exe" "%BUILDROOT%\runtime" > nul || EXIT /B 1L% + COPY "%PGADMIN_POSTGRES_DIR%\bin\pg_restore.exe" "%BUILDROOT%\runtime" > nul || EXIT /B 1 + COPY "%PGADMIN_POSTGRES_DIR%\bin\psql.exe" "%BUILDROOT%\runtime" > nul || EXIT /B 1 + + ECHO Staging VC++ runtime... + MKDIR "%BUILDROOT%\installer" || EXIT /B 1 + COPY "%PGADMIN_VCREDIST_DIR%\%PGADMIN_VCREDIST_FILE%" "%BUILDROOT%\installer" > nul || EXIT /B 1 + + CD %WD% + EXIT /B 0 + + +:CREATE_INSTALLER + ECHO Preparing for creation of windows installer... + IF NOT EXIST "%DISTROOT%" MKDIR "%DISTROOT%" + + ECHO Copying icon file... + COPY "%WD%\pkg\win32\Resources\pgAdmin4.ico" "%BUILDROOT%" > nul || EXIT /B 1 + + CD "%WD%\pkg\win32" + + ECHO Processing installer configuration script... + CALL "%PGADMIN_PYTHON_DIR%\python" "%WD%\pkg\win32\replace.py" "-i" "%WD%\pkg\win32\installer.iss.in" "-o" "%WD%\pkg\win32\installer.iss.in_stage1" "-s" MYAPP_FULLVERSION -r """%APP_VERSION%""" + CALL "%PGADMIN_PYTHON_DIR%\python" "%WD%\pkg\win32\replace.py" "-i" "%WD%\pkg\win32\installer.iss.in_stage1" "-o" "%WD%\pkg\win32\installer.iss.in_stage2" "-s" MYAPP_VERSION -r """v%APP_MAJOR%""" + CALL "%PGADMIN_PYTHON_DIR%\python" "%WD%\pkg\win32\replace.py" "-i" "%WD%\pkg\win32\installer.iss.in_stage2" "-o" "%WD%\pkg\win32\installer.iss" "-s" MYAPP_VCDIST -r """%PGADMIN_VCREDIST_DIRNAME%\%PGADMIN_VCREDIST_FILE%""" + + ECHO Cleaning up... + DEL /s "%WD%\pkg\win32\installer.iss.in_stage*" > nul + + ECHO Creating windows installer using INNO tool... + IF NOT "%PGADMIN_WINDOWS_CSC%" == "" ( + CALL "%PGADMIN_INNOTOOL_DIR%\ISCC.exe" "%WD%\pkg\win32\installer.iss" "/DSIGNED" "/SpgAdminSigntool=%PGADMIN_SIGNTOOL_DIR%\signtool.exe sign /sm /n $q%PGADMIN_WINDOWS_CSC%$q /tr http://timestamp.digicert.com /td sha256 /fd sha1 /v $f" || EXIT /B 1 + ) ELSE ( + CALL "%PGADMIN_INNOTOOL_DIR%\ISCC.exe" "%WD%\pkg\win32\installer.iss" || EXIT /B 1 + ) + + ECHO Renaming installer... + MOVE "%WD%\pkg\win32\Output\pgadmin4-setup.exe" "%DISTROOT%\%INSTALLERNAME%" > nul || EXIT /B 1 + + ECHO Location - %DISTROOT%\%INSTALLERNAME% + ECHO Installer generated successfully. + + CD %WD% + EXIT /B 0 + +:GENERATE_SBOM + ECHO Generating SBOM... + CALL syft "%BUILDROOT%" -o cyclonedx-json > "%BUILDROOT%\sbom.json" + + EXIT /B 0 + +:VERIFY_SIGNATURE + IF "%PGADMIN_WINDOWS_CSC%" == "" ( + ECHO Skipping signature verification ^(PGADMIN_WINDOWS_CSC is not set^)... + EXIT /B 0 + ) + + ECHO Verifying the installer signature... + + CALL "%PGADMIN_SIGNTOOL_DIR%\signtool.exe" verify /pa /v "%DISTROOT%\%INSTALLERNAME%" + REM PAUSE was the old handler here, which on the signing host either returns + REM immediately, leaving the build to carry on with an unverified installer, + REM or blocks until the job times out. There is nobody at that machine to + REM press a key. + IF ERRORLEVEL 1 ( + ECHO. + ECHO ************************************************************ + ECHO * Failed to verify signature of the installer + ECHO ************************************************************ + EXIT /B 1 + ) + + EXIT /B 0 + + +:USAGE + ECHO Invalid command line options. + ECHO Usage: "Make.bat [clean]" + ECHO. + + EXIT /B 1 + +:CHECK_ROBOCOPY_ERROR + IF %ERRORLEVEL% GEQ 8 EXIT /B %ERRORLEVEL% + EXIT /B 0 diff --git a/pkg/publish/README.md b/pkg/publish/README.md index a1e5bef00..b5f8656b2 100644 --- a/pkg/publish/README.md +++ b/pkg/publish/README.md @@ -75,7 +75,10 @@ needs the publishing account to be able to write it. Three key pairs. `roadie-publish` and `roadie-upload` belong to the runner; `paxsor-pull` belongs to the download server, which uses it to fetch staged -content from the staging server during publication. +content from the staging server during publication. Its private half must be at +`~pgaupload/.ssh/id_ed25519_paxsor_pull` on the download server, which is +where `pga-publish` looks for it; the name in the key management system is +only a label, and the path is what matters. `authorized_keys.procyon` and `authorized_keys.paxsor` are annotated templates: substitute the addresses and the public keys, and read the annotations before diff --git a/pkg/publish/pga-publish b/pkg/publish/pga-publish index beae3528e..5c5debf26 100755 --- a/pkg/publish/pga-publish +++ b/pkg/publish/pga-publish @@ -169,7 +169,7 @@ CLOUDFRONT_CONF = "/etc/pga-publish.cloudfront" # SITE: use the internal name or address that paxsor actually resolves. PULL_HOST = "procyon" PULL_USER = "pgaupload" -PULL_KEY = os.path.expanduser("~/.ssh/id_staging_pull") +PULL_KEY = os.path.expanduser("~/.ssh/id_ed25519_paxsor_pull") # One publishing operation at a time. Two concurrent workflow runs indexing the # same repository would produce a Release file describing packages that are half diff --git a/pkg/publish/rebuild-yum-repo.sh b/pkg/publish/rebuild-yum-repo.sh index a5871b52c..b974c25a1 100755 --- a/pkg/publish/rebuild-yum-repo.sh +++ b/pkg/publish/rebuild-yum-repo.sh @@ -55,7 +55,13 @@ VERSION=$3 TREE=${ROOT}/yum/${FAMILY}/${NAME}-${VERSION}-${ARCH} /usr/bin/createrepo_c "${TREE}" -/usr/bin/gpg --yes --detach-sign --armor "${TREE}/repodata/repomd.xml" +# -u, because without it gpg signs with whatever the keyring's default +# key resolves to. That is the packaging key today only because there is +# one secret key in it, and it stops being true the moment a second one +# lands. A repository whose metadata is signed by the wrong key is one +# every client with gpgcheck=1 rejects, produced by a run that exits 0. +/usr/bin/gpg --yes -u packages@pgadmin.org --detach-sign --armor \ + "${TREE}/repodata/repomd.xml" # Some EL variants report $releasever with a variant suffix, so the repository # has to answer to each of the resulting directory names. diff --git a/pkg/publish/selftest.sh b/pkg/publish/selftest.sh index 12cbef51f..0504a555e 100755 --- a/pkg/publish/selftest.sh +++ b/pkg/publish/selftest.sh @@ -39,6 +39,32 @@ check() { fi } +# check_ssh +# +# The other helper drives --dry-run, which takes its request from argv and so +# joins it with single spaces. Production takes the raw string sshd puts in +# SSH_ORIGINAL_COMMAND, which keeps runs of whitespace, tabs, and whatever else +# the client sent. Only rejections are tested this way: an accepted request +# would run for real, and the point of these cases is that none of them gets +# that far. +check_ssh() { + local expected="$1"; shift + local description="$1"; shift + + local output + output="$(SSH_ORIGINAL_COMMAND="$1" "$WRAPPER" 2>&1)" + local rc=$? + + if [ "$rc" = "$expected" ]; then + pass=$((pass + 1)) + printf 'ok %-3s %s\n' "$rc" "$description" + else + fail=$((fail + 1)) + printf 'FAIL %-3s (wanted %s) %s\n' "$rc" "$expected" "$description" + printf ' %s\n' "$output" + fi +} + echo "== accepted requests: staging role (procyon) ==" check 0 staging "hello" hello check 0 staging "stage-create" stage-create 2026-09-17 @@ -50,6 +76,11 @@ check 0 staging "stage-index-yum" stage-index-yum 2026-09-17 echo echo "== accepted requests: download role (paxsor) ==" +check 0 download "snapshot-create" snapshot-create 2026-09-17 +check 0 download "snapshot-sign" snapshot-sign 2026-09-17 +check 0 download "snapshot-index-apt" snapshot-index-apt 2026-09-17 +check 0 download "snapshot-index-yum" snapshot-index-yum 2026-09-17 +check 0 download "snapshot-purge" snapshot-purge check 0 download "release-exists" release-exists 9.18 check 0 download "release-create" release-create 9.18 check 0 download "release-create with patch" release-create 9.18.1 @@ -111,6 +142,21 @@ check 64 download "sync-s3 takes no argument" "sync-s3 release" check 77 download "unpublished yum arch" "rebuild-yum redhat rhel 9 aarch64" check 77 download "unpublished apt codename" "rebuild-apt plucky" +echo +echo "== through the real entry point, SSH_ORIGINAL_COMMAND ==" +check_ssh 64 "tab instead of a space" "$(printf 'stage-create\t2026-09-17')" +check_ssh 64 "embedded newline" "$(printf 'hello\nstage-create 2026-09-17')" +check_ssh 64 "trailing carriage return" "$(printf 'hello\r')" +check_ssh 64 "Cyrillic homoglyph in the verb" "ѕtage-create 2026-09-17" +check_ssh 64 "non-ASCII in an argument" "rebuild-apt bookwоrm" +check_ssh 64 "NUL-ish escape in the request" "$(printf 'hello\\x00')" + +echo +echo "== an argument may not look like an option ==" +check 64 download "leading hyphen as a codename" "rebuild-apt -r" +check 64 download "leading hyphen as a datestamp" "snapshot-sign -rf" +check 64 staging "lone hyphen" "stage-exists -" + echo echo "== the dry-run flag is not reachable from a client ==" # With SSH_ORIGINAL_COMMAND set, our own argv is ignored entirely, so a client