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