diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index bc0cd2b..c5b561a 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -379,6 +379,51 @@ jobs: cp -a /usr/share/postgresql/${PG_VERSION}/. ~/opt/postgresql/${PG_FULL}/share/ ls ~/opt/postgresql/${PG_FULL}/bin/ + - name: Test init --provider=postgresql + run: | + # Run init against isolated dirs, verify it creates them and + # prints the PostgreSQL setup instructions. + OUT=$(./dbdeployer init --provider=postgresql \ + --sandbox-binary=/tmp/init-pg-bin \ + --sandbox-home=/tmp/init-pg-home \ + --skip-shell-completion 2>&1) + echo "$OUT" + + # Verify directories were created + [ -d /tmp/init-pg-bin ] || { echo "FAIL: sandbox-binary not created"; exit 1; } + [ -d /tmp/init-pg-home ] || { echo "FAIL: sandbox-home not created"; exit 1; } + + # Verify the Linux-specific PostgreSQL setup instructions were printed + echo "$OUT" | grep -q "PostgreSQL binaries are not auto-downloaded on Linux" || { + echo "FAIL: expected Linux PostgreSQL setup instructions in output" + exit 1 + } + echo "$OUT" | grep -q "dbdeployer unpack --provider=postgresql" || { + echo "FAIL: expected unpack instructions in output" + exit 1 + } + + # Verify it didn't try to download a MySQL tarball + echo "$OUT" | grep -q "mysql-" && { + echo "FAIL: output references mysql tarball" + exit 1 + } + + # Sanity: invalid provider should fail + ./dbdeployer init --provider=oracle --dry-run \ + --sandbox-binary=/tmp/init-bad \ + --sandbox-home=/tmp/init-bad-home 2>&1 | grep -q "unknown provider" || { + echo "FAIL: invalid provider did not error as expected" + exit 1 + } + + # Cleanup init side-effects so the rest of the job uses its own dirs + rm -rf /tmp/init-pg-bin /tmp/init-pg-home /tmp/init-bad /tmp/init-bad-home + # Restore sandbox defaults so subsequent steps find ~/opt/postgresql + ./dbdeployer defaults update sandbox-binary "$HOME/opt/mysql" + ./dbdeployer defaults update sandbox-home "$HOME/sandboxes" + echo "OK: dbdeployer init --provider=postgresql works" + - name: Test deploy postgresql (single) run: | PG_FULL=$(ls ~/opt/postgresql/ | head -1) @@ -483,6 +528,86 @@ jobs: done 2>/dev/null || true pkill -9 -u "$USER" postgres 2>/dev/null || true + # Test PostgreSQL support on macOS via Postgres.app (#85). + # `dbdeployer init --provider=postgresql` on macOS downloads a Postgres.app + # single-version .dmg, mounts it with hdiutil, and extracts bin/lib/share + # into ~/opt/postgresql//. This job verifies the full happy path + # end-to-end: init → directory layout → deploy single → SELECT. + postgresql-macos-test: + name: PostgreSQL on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-14, macos-15] + env: + GO111MODULE: on + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Build dbdeployer + run: go build -o dbdeployer . + + - name: init --provider=postgresql (downloads Postgres.app) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Clean slate — no ~/opt/postgresql yet. + rm -rf "$HOME/opt/postgresql" "$HOME/sandboxes-pg-mac" + ./dbdeployer init --provider=postgresql \ + --sandbox-home="$HOME/sandboxes-pg-mac" \ + --skip-shell-completion + + # Verify binaries landed under ~/opt/postgresql//bin + INSTALL_DIR=$(ls -d "$HOME/opt/postgresql"/*/ 2>/dev/null | head -1) + [ -n "$INSTALL_DIR" ] || { echo "FAIL: no version directory under ~/opt/postgresql"; exit 1; } + echo "Install dir: $INSTALL_DIR" + for bin in postgres initdb pg_ctl psql pg_basebackup; do + [ -x "${INSTALL_DIR}bin/$bin" ] || { echo "FAIL: missing $bin at ${INSTALL_DIR}bin/"; exit 1; } + done + "${INSTALL_DIR}bin/postgres" --version + "${INSTALL_DIR}bin/psql" --version + + # Check the directory name is in X.Y format + PG_VERSION=$(basename "${INSTALL_DIR%/}") + echo "$PG_VERSION" | grep -Eq '^[0-9]+\.[0-9]+$' || { + echo "FAIL: expected X.Y version dir, got: $PG_VERSION" + exit 1 + } + + - name: Deploy PostgreSQL single sandbox + run: | + # Discover the X.Y version that init installed + PG_VERSION=$(basename "$(ls -d "$HOME/opt/postgresql"/*/ | head -1 | sed 's:/$::')") + echo "Deploying PostgreSQL $PG_VERSION" + ./dbdeployer deploy postgresql "$PG_VERSION" \ + --sandbox-home="$HOME/sandboxes-pg-mac" + # Discover the sandbox directory + SBDIR=$(ls -d "$HOME/sandboxes-pg-mac"/pg_sandbox_* | head -1) + echo "Sandbox: $SBDIR" + "$SBDIR/use" -c "SELECT version();" + "$SBDIR/status" + + - name: Write + read smoke test + run: | + SBDIR=$(ls -d "$HOME/sandboxes-pg-mac"/pg_sandbox_* | head -1) + "$SBDIR/use" -c "CREATE TABLE mac_smoke(id serial, val text); INSERT INTO mac_smoke(val) VALUES ('hello_from_macos');" + RESULT=$("$SBDIR/use" -c "SELECT val FROM mac_smoke;" 2>&1) + echo "$RESULT" + echo "$RESULT" | grep -q "hello_from_macos" || { echo "FAIL: select did not return inserted row"; exit 1; } + + - name: Cleanup + if: always() + run: | + for dir in "$HOME"/sandboxes-pg-mac/pg_sandbox_*; do + [ -d "$dir" ] && bash "$dir/stop" 2>/dev/null || true + done 2>/dev/null || true + pkill -9 -u "$USER" postgres 2>/dev/null || true + # Test InnoDB Cluster topology with MySQL Shell + MySQL Router innodb-cluster-test: name: InnoDB Cluster (${{ matrix.mysql-version }}) diff --git a/cmd/init.go b/cmd/init.go index 50121b2..2bf3070 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -31,6 +31,7 @@ func initEnvironment(cmd *cobra.Command, args []string) error { skipDownloads, _ := flags.GetBool(globals.SkipAllDownloadsLabel) skipTarballDownload, _ := flags.GetBool(globals.SkipTarballDownloadLabel) skipCompletion, _ := flags.GetBool(globals.SkipShellCompletionLabel) + provider, _ := flags.GetString(globals.ProviderLabel) return ops.InitEnvironment(ops.InitOptions{ SandboxBinary: sandboxBinary, @@ -39,23 +40,26 @@ func initEnvironment(cmd *cobra.Command, args []string) error { SkipDownloads: skipDownloads, SkipTarballDownload: skipTarballDownload, SkipCompletion: skipCompletion, + Provider: provider, }) } var initCmd = &cobra.Command{ Use: "init", Short: "initializes dbdeployer environment", - Long: `Initializes dbdeployer environment: + Long: `Initializes dbdeployer environment: * creates $SANDBOX_HOME and $SANDBOX_BINARY directories -* downloads and expands the latest MySQL tarball +* downloads and expands the latest MySQL tarball (default provider) + or prints PostgreSQL setup instructions (--provider=postgresql) * installs shell completion file`, RunE: initEnvironment, } func init() { rootCmd.AddCommand(initCmd) - initCmd.PersistentFlags().Bool(globals.SkipAllDownloadsLabel, false, "Do not download any file (skip both MySQL tarball and shell completion file)") - initCmd.PersistentFlags().Bool(globals.SkipTarballDownloadLabel, false, "Do not download MySQL tarball") + initCmd.PersistentFlags().Bool(globals.SkipAllDownloadsLabel, false, "Do not download any file (skip both tarball and shell completion file)") + initCmd.PersistentFlags().Bool(globals.SkipTarballDownloadLabel, false, "Do not download the database tarball") initCmd.PersistentFlags().Bool(globals.SkipShellCompletionLabel, false, "Do not download shell completion file") initCmd.PersistentFlags().Bool(globals.DryRunLabel, false, "Show operations but don't run them") + initCmd.PersistentFlags().String(globals.ProviderLabel, globals.ProviderValue, "Database provider (mysql, postgresql)") } diff --git a/downloads/remote_registry.go b/downloads/remote_registry.go index e7be0d1..179dda1 100644 --- a/downloads/remote_registry.go +++ b/downloads/remote_registry.go @@ -187,6 +187,9 @@ func DeleteTarball(tarballs []TarballDescription, tarballName string) ([]Tarball func CompareTarballChecksum(tarball TarballDescription, fileName string) error { if tarball.Checksum == "" { + fmt.Fprintf(os.Stderr, + "WARNING: no checksum available for %s — download integrity cannot be verified\n", + tarball.Name) return nil } reCRC := regexp.MustCompile(`(MD5|SHA1|SHA256|SHA512)\s*:\s*(\S+)`) diff --git a/downloads/tarball_list.json b/downloads/tarball_list.json index 12ae1b3..6b21716 100644 --- a/downloads/tarball_list.json +++ b/downloads/tarball_list.json @@ -2222,7 +2222,8 @@ "short_version": "8.4", "version": "8.4.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:243d84f7eceef8564d49cbfa8b1d09de" }, { "name": "mysql-8.4.0-linux-glibc2.17-x86_64-minimal.tar.xz", @@ -2235,7 +2236,8 @@ "short_version": "8.4", "version": "8.4.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:9c59dfc42f6998b94b4d03ad7a1afad8" }, { "name": "mysql-8.4.0-linux-glibc2.28-aarch64.tar.xz", @@ -2248,7 +2250,8 @@ "short_version": "8.4", "version": "8.4.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:d2f528255f380ae9b1c088b12939e609" }, { "name": "mysql-8.4.0-macos14-arm64.tar.gz", @@ -2261,7 +2264,8 @@ "short_version": "8.4", "version": "8.4.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:a1e2b046b03812884a0ef0bbfd628f58" }, { "name": "mysql-8.4.0-macos14-x86_64.tar.gz", @@ -2274,7 +2278,8 @@ "short_version": "8.4", "version": "8.4.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:a1db269018bd9fa816ae68cd3aa39750" }, { "name": "mysql-8.4.2-linux-glibc2.17-x86_64.tar.xz", @@ -2287,7 +2292,8 @@ "short_version": "8.4", "version": "8.4.2", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:cc7a578851feb8f05f7cbcaff94a5254" }, { "name": "mysql-8.4.2-linux-glibc2.17-x86_64-minimal.tar.xz", @@ -2300,7 +2306,8 @@ "short_version": "8.4", "version": "8.4.2", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:ad70726bfb4089620d03772e36238e0c" }, { "name": "mysql-8.4.2-linux-glibc2.28-aarch64.tar.xz", @@ -2313,7 +2320,8 @@ "short_version": "8.4", "version": "8.4.2", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:7539453bc941f194d5e85eda31fd7c45" }, { "name": "mysql-8.4.2-macos14-arm64.tar.gz", @@ -2326,7 +2334,8 @@ "short_version": "8.4", "version": "8.4.2", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:503babfaceedf024c99f722824ab054c" }, { "name": "mysql-8.4.2-macos14-x86_64.tar.gz", @@ -2339,7 +2348,8 @@ "short_version": "8.4", "version": "8.4.2", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:f8226460536f4b4f21ceff20cd419944" }, { "name": "mysql-8.4.3-linux-glibc2.17-x86_64.tar.xz", @@ -2352,7 +2362,8 @@ "short_version": "8.4", "version": "8.4.3", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:bb4832299aba5ce4a4f40f34870bdbff" }, { "name": "mysql-8.4.3-linux-glibc2.17-x86_64-minimal.tar.xz", @@ -2365,7 +2376,8 @@ "short_version": "8.4", "version": "8.4.3", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:465fa52c59b1cee6e15849e97fa51607" }, { "name": "mysql-8.4.3-linux-glibc2.28-aarch64.tar.xz", @@ -2378,7 +2390,8 @@ "short_version": "8.4", "version": "8.4.3", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:2a6d55fdfacc5f945ebe5af52a974abe" }, { "name": "mysql-8.4.3-macos14-arm64.tar.gz", @@ -2391,7 +2404,8 @@ "short_version": "8.4", "version": "8.4.3", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:1f02612a8ec99e8da8520232919efa6b" }, { "name": "mysql-8.4.3-macos14-x86_64.tar.gz", @@ -2404,7 +2418,8 @@ "short_version": "8.4", "version": "8.4.3", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:516cdd560af2715da00189aeef4804cd" }, { "name": "mysql-8.4.4-linux-glibc2.17-x86_64.tar.xz", @@ -2417,7 +2432,8 @@ "short_version": "8.4", "version": "8.4.4", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:683db607b34406af7fc9080690776df1" }, { "name": "mysql-8.4.4-linux-glibc2.17-x86_64-minimal.tar.xz", @@ -2430,7 +2446,8 @@ "short_version": "8.4", "version": "8.4.4", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:7269a8e3a6ba247265f6c6cebdfe1f93" }, { "name": "mysql-8.4.4-linux-glibc2.28-aarch64.tar.xz", @@ -2443,7 +2460,8 @@ "short_version": "8.4", "version": "8.4.4", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:6eb39e595f4d17da299bc04af891643f" }, { "name": "mysql-8.4.4-macos15-arm64.tar.gz", @@ -2456,7 +2474,8 @@ "short_version": "8.4", "version": "8.4.4", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:364611026f551df1fa5e1696f9902796" }, { "name": "mysql-8.4.4-macos15-x86_64.tar.gz", @@ -2469,7 +2488,8 @@ "short_version": "8.4", "version": "8.4.4", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:58aaf915e17b7095ae8ce9ca6d6d38a7" }, { "name": "mysql-8.4.5-linux-glibc2.17-x86_64.tar.xz", @@ -2482,7 +2502,8 @@ "short_version": "8.4", "version": "8.4.5", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:8c7573c055a38821f8e65946f48a07ae" }, { "name": "mysql-8.4.5-linux-glibc2.17-x86_64-minimal.tar.xz", @@ -2495,7 +2516,8 @@ "short_version": "8.4", "version": "8.4.5", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:802284d974bbb7709ec10ea29229c489" }, { "name": "mysql-8.4.5-linux-glibc2.28-aarch64.tar.xz", @@ -2508,7 +2530,8 @@ "short_version": "8.4", "version": "8.4.5", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:d62f0051b47d298c506a92806d70c2f3" }, { "name": "mysql-8.4.5-macos15-arm64.tar.gz", @@ -2521,7 +2544,8 @@ "short_version": "8.4", "version": "8.4.5", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:22f91512e1f5c5a78ac1f67722f2da68" }, { "name": "mysql-8.4.5-macos15-x86_64.tar.gz", @@ -2534,7 +2558,8 @@ "short_version": "8.4", "version": "8.4.5", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:57936471cb813b20f32b6ddca61c2756" }, { "name": "mysql-8.4.6-linux-glibc2.17-x86_64.tar.xz", @@ -2547,7 +2572,8 @@ "short_version": "8.4", "version": "8.4.6", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:51b6576dedc974e255461cbefa246785" }, { "name": "mysql-8.4.6-linux-glibc2.17-x86_64-minimal.tar.xz", @@ -2560,7 +2586,8 @@ "short_version": "8.4", "version": "8.4.6", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:a02644e50bd395dc8b6b3174ed060e98" }, { "name": "mysql-8.4.6-linux-glibc2.28-aarch64.tar.xz", @@ -2573,7 +2600,8 @@ "short_version": "8.4", "version": "8.4.6", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:2a5c6e6b77a3e43674b36c0e4c4e9cab" }, { "name": "mysql-8.4.6-macos15-arm64.tar.gz", @@ -2586,7 +2614,8 @@ "short_version": "8.4", "version": "8.4.6", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:f297e6f92c9c2d8df7e6ee437639286a" }, { "name": "mysql-8.4.6-macos15-x86_64.tar.gz", @@ -2599,7 +2628,8 @@ "short_version": "8.4", "version": "8.4.6", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:e48f65c6d56e5b42efd1867d3097a58d" }, { "name": "mysql-8.4.7-linux-glibc2.17-x86_64.tar.xz", @@ -2612,7 +2642,8 @@ "short_version": "8.4", "version": "8.4.7", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:186fd13eac6e12391eb63c78d735d3f6" }, { "name": "mysql-8.4.7-linux-glibc2.17-x86_64-minimal.tar.xz", @@ -2625,7 +2656,8 @@ "short_version": "8.4", "version": "8.4.7", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:6442e8c50350e97bce838abeb56df731" }, { "name": "mysql-8.4.7-linux-glibc2.28-aarch64.tar.xz", @@ -2638,7 +2670,8 @@ "short_version": "8.4", "version": "8.4.7", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:3c27d74b07571bb3d4c2bc959901e806" }, { "name": "mysql-8.4.7-macos15-arm64.tar.gz", @@ -2651,7 +2684,8 @@ "short_version": "8.4", "version": "8.4.7", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:1382f2d2c54c1a7c8389b61d1241fea9" }, { "name": "mysql-8.4.7-macos15-x86_64.tar.gz", @@ -2664,7 +2698,8 @@ "short_version": "8.4", "version": "8.4.7", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:ae5c826e3f55e52c8e3cdbd9a4aa5312" }, { "name": "mysql-9.0.1-linux-glibc2.17-x86_64.tar.xz", @@ -2677,7 +2712,8 @@ "short_version": "9.0", "version": "9.0.1", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:7e2bb87060bd8a989ca9115780ef3c88" }, { "name": "mysql-9.0.1-linux-glibc2.17-x86_64-minimal.tar.xz", @@ -2690,7 +2726,8 @@ "short_version": "9.0", "version": "9.0.1", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:6d1b40d3f39f0cc098fcfe846c52008c" }, { "name": "mysql-9.0.1-linux-glibc2.28-aarch64.tar.xz", @@ -2703,7 +2740,8 @@ "short_version": "9.0", "version": "9.0.1", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:8e55202205dacd0a9ce578d17fb474ea" }, { "name": "mysql-9.0.1-macos14-arm64.tar.gz", @@ -2716,7 +2754,8 @@ "short_version": "9.0", "version": "9.0.1", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:1d447e95e748b2d8d7f07986aa2318e9" }, { "name": "mysql-9.0.1-macos14-x86_64.tar.gz", @@ -2729,7 +2768,8 @@ "short_version": "9.0", "version": "9.0.1", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:8b578fc73c854a2bef1dd8ee1cc50ffd" }, { "name": "mysql-9.1.0-linux-glibc2.17-x86_64.tar.xz", @@ -2742,7 +2782,8 @@ "short_version": "9.1", "version": "9.1.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:73a8ccb3dd8ce8be0d327356cc728598" }, { "name": "mysql-9.1.0-linux-glibc2.17-x86_64-minimal.tar.xz", @@ -2755,7 +2796,8 @@ "short_version": "9.1", "version": "9.1.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:ae94e2660381b52a39808eba43350cde" }, { "name": "mysql-9.1.0-linux-glibc2.28-aarch64.tar.xz", @@ -2768,7 +2810,8 @@ "short_version": "9.1", "version": "9.1.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:487436b5777870b5a42ecce40d465ba3" }, { "name": "mysql-9.1.0-macos14-arm64.tar.gz", @@ -2781,7 +2824,8 @@ "short_version": "9.1", "version": "9.1.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:829b9ba32331d5e4477815fe449d5105" }, { "name": "mysql-9.1.0-macos14-x86_64.tar.gz", @@ -2794,7 +2838,8 @@ "short_version": "9.1", "version": "9.1.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:85f4b284e5f98ed76ce7c34ca4d46663" }, { "name": "mysql-9.2.0-linux-glibc2.17-x86_64.tar.xz", @@ -2807,7 +2852,8 @@ "short_version": "9.2", "version": "9.2.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:3240cf4f1864e4a8f28634e3a52f7871" }, { "name": "mysql-9.2.0-linux-glibc2.17-x86_64-minimal.tar.xz", @@ -2820,7 +2866,8 @@ "short_version": "9.2", "version": "9.2.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:37e2cf3d385d0b5f02fa090b495615de" }, { "name": "mysql-9.2.0-linux-glibc2.28-aarch64.tar.xz", @@ -2833,7 +2880,8 @@ "short_version": "9.2", "version": "9.2.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:ae16f8ee0290d9d98015104287f129f8" }, { "name": "mysql-9.2.0-macos15-arm64.tar.gz", @@ -2846,7 +2894,8 @@ "short_version": "9.2", "version": "9.2.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:d284fb682bcb76aa4083b0c9b930123c" }, { "name": "mysql-9.2.0-macos15-x86_64.tar.gz", @@ -2859,7 +2908,8 @@ "short_version": "9.2", "version": "9.2.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:3370cf67a346415da567619c25f30b16" }, { "name": "mysql-9.3.0-linux-glibc2.17-x86_64.tar.xz", @@ -2872,7 +2922,8 @@ "short_version": "9.3", "version": "9.3.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:e585a7e718c542bf0fe000df708573ab" }, { "name": "mysql-9.3.0-linux-glibc2.17-x86_64-minimal.tar.xz", @@ -2885,7 +2936,8 @@ "short_version": "9.3", "version": "9.3.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:c24ef84be1f80790d7a81419b1728191" }, { "name": "mysql-9.3.0-linux-glibc2.28-aarch64.tar.xz", @@ -2898,7 +2950,8 @@ "short_version": "9.3", "version": "9.3.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:f6fc416fdbbb76c79ad5398e9e517b17" }, { "name": "mysql-9.3.0-macos15-arm64.tar.gz", @@ -2911,7 +2964,8 @@ "short_version": "9.3", "version": "9.3.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:cbcb56c277a4220b2d953d042e7d97d6" }, { "name": "mysql-9.3.0-macos15-x86_64.tar.gz", @@ -2924,7 +2978,8 @@ "short_version": "9.3", "version": "9.3.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:7685098d3eb1ea6b9cde439eb805d009" }, { "name": "mysql-9.4.0-linux-glibc2.17-x86_64.tar.xz", @@ -2937,7 +2992,8 @@ "short_version": "9.4", "version": "9.4.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:b5b178b82657498a8020ff80cf3c5a63" }, { "name": "mysql-9.4.0-linux-glibc2.17-x86_64-minimal.tar.xz", @@ -2950,7 +3006,8 @@ "short_version": "9.4", "version": "9.4.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:3bd75f440f0ecb518b89aa4cabd728fe" }, { "name": "mysql-9.4.0-linux-glibc2.28-aarch64.tar.xz", @@ -2963,7 +3020,8 @@ "short_version": "9.4", "version": "9.4.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:5d740da908019bfadf5d1b5f3df6f80c" }, { "name": "mysql-9.4.0-macos15-arm64.tar.gz", @@ -2976,7 +3034,8 @@ "short_version": "9.4", "version": "9.4.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:09ceb4b5238470b8fe4b76e5919e483b" }, { "name": "mysql-9.4.0-macos15-x86_64.tar.gz", @@ -2989,7 +3048,8 @@ "short_version": "9.4", "version": "9.4.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:99b09d78a26cf0d83aef76d4698e4171" }, { "name": "mysql-9.5.0-linux-glibc2.17-x86_64.tar.xz", @@ -3002,7 +3062,8 @@ "short_version": "9.5", "version": "9.5.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:c49ad0f418f3e64f576c3286037b37c0" }, { "name": "mysql-9.5.0-linux-glibc2.17-x86_64-minimal.tar.xz", @@ -3015,7 +3076,8 @@ "short_version": "9.5", "version": "9.5.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:8f003e3ca95a03a8bac8dfec2b2ab724" }, { "name": "mysql-9.5.0-linux-glibc2.28-aarch64.tar.xz", @@ -3028,7 +3090,8 @@ "short_version": "9.5", "version": "9.5.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:a5d2dfe6827243c2c98b3d7bf2b55377" }, { "name": "mysql-9.5.0-macos15-arm64.tar.gz", @@ -3041,7 +3104,8 @@ "short_version": "9.5", "version": "9.5.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:15d626a071c5d7d888c7ec9545b813a7" }, { "name": "mysql-9.5.0-macos15-x86_64.tar.gz", @@ -3054,7 +3118,8 @@ "short_version": "9.5", "version": "9.5.0", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:1e991181f51da2194d684ee27a343172" }, { "name": "mysql-8.4.8-linux-glibc2.17-x86_64.tar.xz", @@ -3067,7 +3132,8 @@ "short_version": "8.4", "version": "8.4.8", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:43cd4e14b688e9364ec4dbd58dd37437" }, { "name": "mysql-8.4.8-linux-glibc2.17-x86_64-minimal.tar.xz", @@ -3080,7 +3146,8 @@ "short_version": "8.4", "version": "8.4.8", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:bbf8084d8be89b5a5d65ba14f476dc08" }, { "name": "mysql-8.4.8-linux-glibc2.28-aarch64.tar.xz", @@ -3093,7 +3160,8 @@ "short_version": "8.4", "version": "8.4.8", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:e3c492b66f39e981ace5794a9a1300ee" }, { "name": "mysql-8.4.8-macos15-arm64.tar.gz", @@ -3106,7 +3174,8 @@ "short_version": "8.4", "version": "8.4.8", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:5e278c89e880b9f11c6797bbad8baeee" }, { "name": "mysql-8.4.8-macos15-x86_64.tar.gz", @@ -3119,7 +3188,8 @@ "short_version": "8.4", "version": "8.4.8", "notes": "added with version 2.0.0", - "date_added": "2026-03-24 00:00" + "date_added": "2026-03-24 00:00", + "checksum": "MD5:221c146bac3838bca1c1e1628599f50e" }, { "name": "Percona-Server-5.7.41-44-Linux.x86_64.glibc2.17-minimal.tar.gz", @@ -3132,7 +3202,8 @@ "short_version": "5.7", "version": "5.7.41", "notes": "added with version 2.1.1", - "date_added": "2026-04-05 00:00" + "date_added": "2026-04-05 00:00", + "checksum": "SHA256:6278946df96972588b6b50e706ed2cbc5e2dba0e60e05bf78a8a9cfb2ad3df99" }, { "name": "Percona-Server-5.7.42-46-Linux.x86_64.glibc2.17-minimal.tar.gz", @@ -3145,7 +3216,8 @@ "short_version": "5.7", "version": "5.7.42", "notes": "added with version 2.1.1", - "date_added": "2026-04-05 00:00" + "date_added": "2026-04-05 00:00", + "checksum": "SHA256:2812e9b3bb87a5e716259aaefa913e49486f3a4efd049dc5aa55de3b8c1d34b5" }, { "name": "Percona-Server-5.7.43-47-Linux.x86_64.glibc2.17-minimal.tar.gz", @@ -3158,7 +3230,8 @@ "short_version": "5.7", "version": "5.7.43", "notes": "added with version 2.1.1", - "date_added": "2026-04-05 00:00" + "date_added": "2026-04-05 00:00", + "checksum": "SHA256:d22af9fda2b0bb3cfa066d333f5d774be9193b90d44ff5b70aa31262c3218e51" }, { "name": "Percona-Server-5.7.44-48-Linux.x86_64.glibc2.17-minimal.tar.gz", @@ -3171,7 +3244,8 @@ "short_version": "5.7", "version": "5.7.44", "notes": "added with version 2.1.1", - "date_added": "2026-04-05 00:00" + "date_added": "2026-04-05 00:00", + "checksum": "SHA256:2aec67e7d23cf0cf54ebc2603d38359982967536bec5750842665641feec10d0" }, { "name": "Percona-Server-8.0.33-25-Linux.x86_64.glibc2.17-minimal.tar.gz", @@ -3184,7 +3258,8 @@ "short_version": "8.0", "version": "8.0.33", "notes": "added with version 2.1.1", - "date_added": "2026-04-05 00:00" + "date_added": "2026-04-05 00:00", + "checksum": "SHA256:9af96d33b4472f7bdd2ca63fa95b5f93eebe084d76afecfd4721f6c45a61fa4a" }, { "name": "Percona-Server-8.0.34-26-Linux.x86_64.glibc2.17-minimal.tar.gz", @@ -3197,7 +3272,8 @@ "short_version": "8.0", "version": "8.0.34", "notes": "added with version 2.1.1", - "date_added": "2026-04-05 00:00" + "date_added": "2026-04-05 00:00", + "checksum": "SHA256:a7135886018276abe49813cf45fd1f0caa6b67151028314fbfe37550e1061598" }, { "name": "Percona-Server-8.0.35-27-Linux.x86_64.glibc2.17-minimal.tar.gz", @@ -3210,7 +3286,8 @@ "short_version": "8.0", "version": "8.0.35", "notes": "added with version 2.1.1", - "date_added": "2026-04-05 00:00" + "date_added": "2026-04-05 00:00", + "checksum": "SHA256:94b40d59d26373880c7288bacf5259afa3d276247d5b0855e597277ee9cba20f" }, { "name": "Percona-Server-8.0.36-28-Linux.x86_64.glibc2.17-minimal.tar.gz", @@ -3223,7 +3300,8 @@ "short_version": "8.0", "version": "8.0.36", "notes": "added with version 2.1.1", - "date_added": "2026-04-05 00:00" + "date_added": "2026-04-05 00:00", + "checksum": "SHA256:11d87ec3620befa14f08be6ee4161968081c61104ac9df89d49f577f449bc204" }, { "name": "Percona-Server-8.0.39-30-Linux.x86_64.glibc2.35-minimal.tar.gz", @@ -3236,7 +3314,8 @@ "short_version": "8.0", "version": "8.0.39", "notes": "added with version 2.1.1", - "date_added": "2026-04-05 00:00" + "date_added": "2026-04-05 00:00", + "checksum": "SHA256:8ea8fa21d3dc58040692bece5c0ccbf4e2c30195577361cb4d4dba0ef835281c" }, { "name": "Percona-Server-8.0.40-31-Linux.x86_64.glibc2.35-minimal.tar.gz", @@ -3249,7 +3328,8 @@ "short_version": "8.0", "version": "8.0.40", "notes": "added with version 2.1.1", - "date_added": "2026-04-05 00:00" + "date_added": "2026-04-05 00:00", + "checksum": "SHA256:173c74e4e5551f2e1037dabc7b2d54e0a7d1c64740bc097ea07aefbb3d838feb" }, { "name": "Percona-Server-8.4.0-1-Linux.x86_64.glibc2.17-minimal.tar.gz", @@ -3262,7 +3342,8 @@ "short_version": "8.4", "version": "8.4.0", "notes": "added with version 2.1.1", - "date_added": "2026-04-05 00:00" + "date_added": "2026-04-05 00:00", + "checksum": "SHA256:521687713f78bb72ec91cac8082b98a89f4a2a95ff1ffdb60a2914bb7920992d" }, { "name": "Percona-Server-8.4.2-2-Linux.x86_64.glibc2.17-minimal.tar.gz", @@ -3275,7 +3356,8 @@ "short_version": "8.4", "version": "8.4.2", "notes": "added with version 2.1.1", - "date_added": "2026-04-05 00:00" + "date_added": "2026-04-05 00:00", + "checksum": "SHA256:c7f3b985bf4f0488ef2db0ac730a1eabaaab0b82fd0c28965b8d51121895f1b9" }, { "name": "Percona-Server-8.4.3-3-Linux.x86_64.glibc2.35-minimal.tar.gz", @@ -3288,7 +3370,8 @@ "short_version": "8.4", "version": "8.4.3", "notes": "added with version 2.1.1", - "date_added": "2026-04-05 00:00" + "date_added": "2026-04-05 00:00", + "checksum": "SHA256:54ee4ae2e8ad4161abef10737ee4d9b4a57714df7e6f9f6ba29d3ec23d5e15b0" }, { "name": "Percona-Server-8.4.4-4-Linux.x86_64.glibc2.35-minimal.tar.gz", @@ -3301,7 +3384,8 @@ "short_version": "8.4", "version": "8.4.4", "notes": "added with version 2.1.1", - "date_added": "2026-04-05 00:00" + "date_added": "2026-04-05 00:00", + "checksum": "SHA256:3af319cb8f1d0ee2c0cea1751ef939035cd39f6596ce048de5de0173a48d4f44" }, { "name": "mariadb-10.6.9-linux-systemd-x86_64.tar.gz", @@ -3314,7 +3398,8 @@ "short_version": "10.6", "version": "10.6.9", "notes": "archive.mariadb.org LTS", - "date_added": "2026-04-02 00:00" + "date_added": "2026-04-02 00:00", + "checksum": "SHA256:7c0ab150f8ea4114f6a488e03b73fbb7a9ee1d973749f7f567028e42535564fd" }, { "name": "mariadb-10.11.9-linux-systemd-x86_64.tar.gz", @@ -3327,7 +3412,8 @@ "short_version": "10.11", "version": "10.11.9", "notes": "archive.mariadb.org LTS", - "date_added": "2026-04-02 00:00" + "date_added": "2026-04-02 00:00", + "checksum": "SHA256:c45becf9f8e9e90d9e32d5f2b4378cac971ef7a54c2ba4b668625e203285b5cb" }, { "name": "mariadb-11.4.5-linux-systemd-x86_64.tar.gz", @@ -3340,7 +3426,8 @@ "short_version": "11.4", "version": "11.4.5", "notes": "archive.mariadb.org LTS", - "date_added": "2026-04-02 00:00" + "date_added": "2026-04-02 00:00", + "checksum": "SHA256:2a44cb70a87dba7eb2cab3b5af2c0416a0204d93a8fda387b4b70c9f1bab7bd6" }, { "name": "mariadb-11.4.9-linux-systemd-x86_64.tar.gz", @@ -3353,7 +3440,8 @@ "short_version": "11.4", "version": "11.4.9", "notes": "archive.mariadb.org LTS", - "date_added": "2026-04-02 00:00" + "date_added": "2026-04-02 00:00", + "checksum": "SHA256:c079403239fa74900c18ae0f2d99806625b3ae936c8983dd39a96c8b237072da" }, { "name": "mariadb-11.8.6-linux-systemd-x86_64.tar.gz", @@ -3366,7 +3454,8 @@ "short_version": "11.8", "version": "11.8.6", "notes": "archive.mariadb.org short-term", - "date_added": "2026-04-02 00:00" + "date_added": "2026-04-02 00:00", + "checksum": "SHA256:9367b26f63a7f4936acf914a987d15f37e2449ef568552b70d3c4ffa48896f8b" } ] } diff --git a/ops/init.go b/ops/init.go index b2586ce..5fd191f 100644 --- a/ops/init.go +++ b/ops/init.go @@ -26,6 +26,7 @@ import ( "github.com/ProxySQL/dbdeployer/defaults" "github.com/ProxySQL/dbdeployer/downloads" "github.com/ProxySQL/dbdeployer/globals" + "github.com/ProxySQL/dbdeployer/providers/postgresql" "github.com/ProxySQL/dbdeployer/rest" ) @@ -36,6 +37,7 @@ type InitOptions struct { SkipDownloads bool SkipTarballDownload bool SkipCompletion bool + Provider string } func verifyInitOptions(options InitOptions) error { @@ -58,6 +60,24 @@ func InitEnvironment(options InitOptions) error { dryRun := options.DryRun skipDownloads := options.SkipDownloads skipCompletion := options.SkipCompletion + provider := options.Provider + if provider == "" { + provider = "mysql" + } + if provider != "mysql" && provider != "postgresql" { + return fmt.Errorf("unknown provider %q: supported values are 'mysql' and 'postgresql'", provider) + } + + // For PostgreSQL, point sandbox-binary at ~/opt/postgresql instead of + // the MySQL default, unless the user explicitly overrode it via + // --sandbox-binary. + if provider == "postgresql" && sandboxBinary == defaults.Defaults().SandboxBinary { + home, herr := os.UserHomeDir() + if herr != nil { + return fmt.Errorf("cannot determine home directory: %s", herr) + } + sandboxBinary = path.Join(home, "opt", "postgresql") + } sandboxHome, err = common.AbsolutePath(sandboxHome) if err != nil { @@ -164,9 +184,16 @@ func InitEnvironment(options InitOptions) error { fmt.Println() if needDownload { - err = initDownloadTarball(options) - if err != nil { - return err + if provider == "postgresql" { + err = initInstallPostgreSQL(options, sandboxBinary) + if err != nil { + return err + } + } else { + err = initDownloadTarball(options) + if err != nil { + return err + } } } if !common.DirExists(defaults.ConfigurationDir) { @@ -231,3 +258,80 @@ func initDownloadTarball(options InitOptions) error { } return nil } + +// initInstallPostgreSQL is the PostgreSQL equivalent of initDownloadTarball. +// +// On macOS, it downloads the latest Postgres.app .dmg (single-major-version +// asset) and extracts bin/lib/share/include into sandboxBinary//. +// No sudo, no GUI, no Homebrew. +// +// On Linux, dbdeployer has no automatic PostgreSQL download yet (the .deb +// packaging is messy to resolve programmatically — architecture-specific, +// Ubuntu-version-specific, and the PGDG repo has multiple build variants +// per release). Instead, print clear instructions telling the user how +// to obtain the .deb files and run `dbdeployer unpack --provider=postgresql`. +func initInstallPostgreSQL(options InitOptions, sandboxBinary string) error { + if runtime.GOOS == "darwin" { + return initInstallPostgresApp(options, sandboxBinary) + } + printPostgreSQLSetupInstructions(sandboxBinary) + return nil +} + +// initInstallPostgresApp picks the latest Postgres.app release, downloads +// the most recent single-major-version .dmg, and extracts it under +// sandboxBinary// where is the exact PostgreSQL version that +// `postgres --version` reports. Honors --dry-run / --skip-tarball-download +// / --skip-all-downloads. +func initInstallPostgresApp(options InitOptions, sandboxBinary string) error { + fmt.Println(globals.DashLine) + fmt.Println("# Fetching latest Postgres.app release from GitHub") + assets, err := postgresql.LatestPostgresAppAssets() + if err != nil { + return fmt.Errorf("cannot discover Postgres.app release: %s", err) + } + // Pick the newest major version available. + asset := assets[0] + fmt.Printf("Postgres.app %s bundles PostgreSQL major %s\n", asset.AppVersion, asset.Major) + fmt.Printf(" URL: %s\n", asset.URL) + fmt.Printf(" Size: ~%d MB\n", asset.Size/(1024*1024)) + fmt.Printf(" Target dir: %s//\n", sandboxBinary) + + if options.DryRun || options.SkipDownloads || options.SkipTarballDownload { + fmt.Println("(skipped: --dry-run / --skip-downloads / --skip-tarball-download)") + return nil + } + + fullVersion, err := postgresql.InstallFromPostgresAppDMG(asset, sandboxBinary) + if err != nil { + return fmt.Errorf("installing Postgres.app: %s", err) + } + fmt.Println(globals.DashLine) + fmt.Printf("PostgreSQL %s binaries installed under %s/%s/\n", fullVersion, sandboxBinary, fullVersion) + fmt.Println("You can now deploy PostgreSQL sandboxes with:") + fmt.Printf(" dbdeployer deploy postgresql %s\n", fullVersion) + fmt.Printf(" dbdeployer deploy replication %s --provider=postgresql\n", fullVersion) + return nil +} + +// printPostgreSQLSetupInstructions explains how to populate the PostgreSQL +// sandbox-binary directory on Linux, where dbdeployer does not (yet) +// auto-download PostgreSQL packages. The user provides the .deb files and +// runs `dbdeployer unpack --provider=postgresql`. +func printPostgreSQLSetupInstructions(sandboxBinary string) { + fmt.Println(globals.DashLine) + fmt.Println("PostgreSQL binaries are not auto-downloaded on Linux.") + fmt.Println("Fetch the server + client .deb packages from either:") + fmt.Println(" - https://apt.postgresql.org/pub/repos/apt/pool/main/p/postgresql-16/") + fmt.Println(" - your distribution's PostgreSQL package mirror") + fmt.Println() + fmt.Println("Then unpack them into the sandbox-binary directory:") + fmt.Printf(" dbdeployer unpack --provider=postgresql \\\n") + fmt.Printf(" postgresql-16_*.deb postgresql-client-16_*.deb\n") + fmt.Println() + fmt.Printf("Binaries will be installed under %s//\n", sandboxBinary) + fmt.Println() + fmt.Println("Once unpacked, you can deploy PostgreSQL sandboxes with:") + fmt.Println(" dbdeployer deploy postgresql ") + fmt.Println(" dbdeployer deploy replication --provider=postgresql") +} diff --git a/providers/postgresql/macos_install.go b/providers/postgresql/macos_install.go new file mode 100644 index 0000000..ba69618 --- /dev/null +++ b/providers/postgresql/macos_install.go @@ -0,0 +1,309 @@ +// Package postgresql — macOS Postgres.app installer. +// +// Unlike Linux, where PostgreSQL is distributed as two .deb files that we +// extract with dpkg-deb, macOS has no official PostgreSQL binary tarball. +// The closest to a "drop-in" binary distribution is Postgres.app, which +// bundles multiple PostgreSQL major versions inside a .dmg, each under +// Postgres.app/Contents/Versions//{bin,lib,share,include}. +// +// This file provides a helper that downloads a single-version Postgres.app +// .dmg from GitHub, mounts it headlessly with hdiutil, copies the +// bin/lib/share/include tree into the dbdeployer sandbox-binary directory, +// and detaches the image. No sudo, no GUI, no Homebrew required. +package postgresql + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" + "time" +) + +// PostgresAppReleasesAPI is the GitHub API endpoint for the Postgres.app +// project. We read the latest release to discover the current version +// number and the .dmg asset URLs. +const PostgresAppReleasesAPI = "https://api.github.com/repos/PostgresApp/PostgresApp/releases/latest" + +type githubAsset struct { + Name string `json:"name"` + BrowserDownloadURL string `json:"browser_download_url"` + Size int64 `json:"size"` +} + +type githubRelease struct { + TagName string `json:"tag_name"` + Name string `json:"name"` + Assets []githubAsset `json:"assets"` +} + +// PostgresAppAsset describes a single-major-version .dmg from Postgres.app. +type PostgresAppAsset struct { + // AppVersion is the Postgres.app release tag, e.g. "2.9.4". + AppVersion string + // Major is the PostgreSQL major version, e.g. "16". + Major string + // URL is the direct download URL for the .dmg. + URL string + // Size is the .dmg size in bytes. + Size int64 +} + +// LatestPostgresAppAssets fetches the latest Postgres.app release and +// returns one asset per PostgreSQL major version (the small single-version +// .dmg files, not the bundle). Assets are ordered by major version +// descending (newest first). +func LatestPostgresAppAssets() ([]PostgresAppAsset, error) { + return parseAssetsFromURL(PostgresAppReleasesAPI) +} + +// parseAssetsFromURL is factored out from LatestPostgresAppAssets so tests +// can point it at a local httptest server. +func parseAssetsFromURL(apiURL string) ([]PostgresAppAsset, error) { + req, err := http.NewRequest("GET", apiURL, nil) + if err != nil { + return nil, fmt.Errorf("building GitHub request: %w", err) + } + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("User-Agent", "dbdeployer") + // Unauthenticated GitHub API is rate-limited to 60 req/hour per IP. + // Shared CI runners routinely exhaust this. If GITHUB_TOKEN is set + // in the environment (as it is on every GitHub Actions runner by + // default), send it to get a 5000 req/hour limit. + if token := os.Getenv("GITHUB_TOKEN"); token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("fetching Postgres.app releases: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GitHub returned %d for %s", resp.StatusCode, apiURL) + } + + var rel githubRelease + if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil { + return nil, fmt.Errorf("decoding release JSON: %w", err) + } + appVersion := strings.TrimPrefix(rel.TagName, "v") + + // Single-version assets are named Postgres--.dmg, + // distinguished from the multi-version bundle Postgres--14-15-16-...dmg + // by having exactly one numeric segment after the app version. + var assets []PostgresAppAsset + for _, a := range rel.Assets { + if !strings.HasSuffix(a.Name, ".dmg") { + continue + } + base := strings.TrimSuffix(a.Name, ".dmg") + // e.g. "Postgres-2.9.4-16" + prefix := "Postgres-" + appVersion + "-" + if !strings.HasPrefix(base, prefix) { + continue + } + tail := strings.TrimPrefix(base, prefix) + if strings.Contains(tail, "-") { + // multi-version bundle like "14-15-16-17-18" — skip + continue + } + assets = append(assets, PostgresAppAsset{ + AppVersion: appVersion, + Major: tail, + URL: a.BrowserDownloadURL, + Size: a.Size, + }) + } + if len(assets) == 0 { + return nil, fmt.Errorf("no single-version Postgres.app .dmg assets found in release %s", rel.TagName) + } + // Sort by major version descending (string sort works because all + // majors are 2 digits). + for i := 0; i < len(assets)-1; i++ { + for j := i + 1; j < len(assets); j++ { + if assets[j].Major > assets[i].Major { + assets[i], assets[j] = assets[j], assets[i] + } + } + } + return assets, nil +} + +// InstallFromPostgresAppDMG downloads the given Postgres.app .dmg, mounts +// it with hdiutil, queries the bundled postgres binary for its exact +// X.Y version, copies bin/lib/share/include into +// sandboxBinaryDir//, and detaches. +// +// Returns the detected full version (e.g. "16.4") on success so callers +// can feed it to `dbdeployer deploy postgresql `. +// +// Only supported on darwin. On other platforms, returns an error. +func InstallFromPostgresAppDMG(asset PostgresAppAsset, sandboxBinaryDir string) (string, error) { + if runtime.GOOS != "darwin" { + return "", fmt.Errorf("Postgres.app installation is only supported on macOS (current GOOS=%s)", runtime.GOOS) + } + if asset.URL == "" || asset.Major == "" { + return "", fmt.Errorf("invalid Postgres.app asset: %+v", asset) + } + if _, err := exec.LookPath("hdiutil"); err != nil { + return "", fmt.Errorf("hdiutil not found in PATH (this should ship with macOS): %w", err) + } + + tmpDir, err := os.MkdirTemp("", "dbdeployer-pgapp-*") + if err != nil { + return "", fmt.Errorf("creating temp directory: %w", err) + } + defer os.RemoveAll(tmpDir) + + dmgPath := filepath.Join(tmpDir, "Postgres.dmg") + if err := downloadFile(asset.URL, dmgPath); err != nil { + return "", fmt.Errorf("downloading %s: %w", asset.URL, err) + } + + mountPoint := filepath.Join(tmpDir, "mnt") + if err := os.MkdirAll(mountPoint, 0755); err != nil { + return "", fmt.Errorf("creating mount point: %w", err) + } + + // Mount headlessly (-nobrowse keeps it out of Finder, -readonly avoids + // any write attempts, -noverify skips the checksum dance). + mountCmd := exec.Command("hdiutil", "attach", + "-nobrowse", "-readonly", "-noverify", + "-mountpoint", mountPoint, + dmgPath) + if out, err := mountCmd.CombinedOutput(); err != nil { + return "", fmt.Errorf("hdiutil attach failed: %s: %w", string(out), err) + } + + // Regardless of whether the copy succeeds, detach on the way out. + detach := func() { + detachCmd := exec.Command("hdiutil", "detach", mountPoint, "-quiet") + _ = detachCmd.Run() + } + defer detach() + + versionDir := filepath.Join(mountPoint, "Postgres.app", "Contents", "Versions", asset.Major) + binDir := filepath.Join(versionDir, "bin") + if _, err := os.Stat(binDir); err != nil { + return "", fmt.Errorf("expected %s in mounted DMG but it is missing: %w", binDir, err) + } + + // Query the bundled postgres binary for the exact X.Y version. + // dbdeployer's VersionToPort expects X.Y format (e.g. "16.4"), and + // deploy postgresql looks up the binary at ~/opt/postgresql//. + fullVersion, err := detectPostgresFullVersion(filepath.Join(binDir, "postgres")) + if err != nil { + return "", fmt.Errorf("detecting bundled PostgreSQL version: %w", err) + } + + targetDir := filepath.Join(sandboxBinaryDir, fullVersion) + if err := os.MkdirAll(targetDir, 0755); err != nil { + return "", fmt.Errorf("creating target directory %s: %w", targetDir, err) + } + + // Copy bin/, lib/, share/, include/ preserving symlinks and modes. + // Postgres.app's `share/postgresql/` contains the datadir contents + // (postgres.bki, timezonesets, ...) — this is where the `postgres` + // binary looks for them at runtime after compiled-in path relocation. + for _, sub := range []string{"bin", "lib", "share", "include"} { + src := filepath.Join(versionDir, sub) + dst := filepath.Join(targetDir, sub) + if _, err := os.Stat(src); os.IsNotExist(err) { + continue + } + if err := os.MkdirAll(dst, 0755); err != nil { + return "", fmt.Errorf("creating %s: %w", dst, err) + } + cpCmd := exec.Command("cp", "-a", src+"/.", dst+"/") //nolint:gosec // paths come from mounted DMG + if out, err := cpCmd.CombinedOutput(); err != nil { + return "", fmt.Errorf("copying %s to %s: %s: %w", src, dst, string(out), err) + } + } + + // dbdeployer invokes `initdb -L /share` (see + // providers/postgresql/sandbox.go). `initdb` then looks for + // postgres.bki directly under that path, but Postgres.app keeps + // datadir files at `share/postgresql/`. Flatten them into + // `/share/` alongside the nested copy so both initdb (flat) + // and the postgres runtime (nested, via compiled-in sharedir + // relocation) find what they need. + srcDataDir := filepath.Join(versionDir, "share", "postgresql") + dstShare := filepath.Join(targetDir, "share") + if _, err := os.Stat(srcDataDir); err == nil { + flatCmd := exec.Command("cp", "-a", srcDataDir+"/.", dstShare+"/") //nolint:gosec // paths come from mounted DMG + if out, err := flatCmd.CombinedOutput(); err != nil { + return "", fmt.Errorf("flattening share/postgresql/ into share/: %s: %w", string(out), err) + } + } + + // Sanity check that the required binaries are present. + for _, bin := range RequiredBinaries() { + if _, err := os.Stat(filepath.Join(targetDir, "bin", bin)); err != nil { + return "", fmt.Errorf("required binary %q not found at %s after extraction", bin, targetDir) + } + } + return fullVersion, nil +} + +// detectPostgresFullVersion runs `postgres --version` and parses out an +// X.Y version string. Postgres prints something like: +// +// postgres (PostgreSQL) 16.4 (Postgres.app) +// +// We extract "16.4". +func detectPostgresFullVersion(postgresBinary string) (string, error) { + out, err := exec.Command(postgresBinary, "--version").Output() //nolint:gosec // binary path constructed from mounted DMG + if err != nil { + return "", fmt.Errorf("running %s --version: %w", postgresBinary, err) + } + // Regex matches the first X.Y (or X.Y.Z) number in the output. + re := regexp.MustCompile(`(\d+\.\d+(?:\.\d+)?)`) + m := re.FindStringSubmatch(string(out)) + if len(m) < 2 { + return "", fmt.Errorf("could not parse version from %q", strings.TrimSpace(string(out))) + } + // Normalize to X.Y (drop any patch component — dbdeployer's + // VersionToPort only accepts two segments). + parts := strings.SplitN(m[1], ".", 3) + if len(parts) < 2 { + return "", fmt.Errorf("unexpected version format %q", m[1]) + } + return parts[0] + "." + parts[1], nil +} + +// downloadFile streams a URL to a local path. +func downloadFile(url, dst string) error { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + req.Header.Set("User-Agent", "dbdeployer") + + // Long timeout: DMGs are ~100 MB and GitHub Releases occasionally throttles. + client := &http.Client{Timeout: 10 * time.Minute} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("download returned status %d", resp.StatusCode) + } + + out, err := os.Create(dst) //nolint:gosec // dst is a path we constructed in a tempdir + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err +} diff --git a/providers/postgresql/macos_install_test.go b/providers/postgresql/macos_install_test.go new file mode 100644 index 0000000..a391001 --- /dev/null +++ b/providers/postgresql/macos_install_test.go @@ -0,0 +1,85 @@ +package postgresql + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +// fakeGithubRelease mirrors the February 2026 Postgres.app release (v2.9.4) +// which has five single-version .dmg files plus one multi-version bundle. +// Using a fake server keeps the test offline and deterministic. +var fakeGithubRelease = githubRelease{ + TagName: "v2.9.4", + Name: "February 2026 Bugfix Updates (Feb 26)", + Assets: []githubAsset{ + { + Name: "Postgres-2.9.4-14-15-16-17-18.dmg", + BrowserDownloadURL: "https://example.invalid/Postgres-2.9.4-14-15-16-17-18.dmg", + Size: 532 * 1024 * 1024, + }, + { + Name: "Postgres-2.9.4-14.dmg", + BrowserDownloadURL: "https://example.invalid/Postgres-2.9.4-14.dmg", + Size: 84 * 1024 * 1024, + }, + { + Name: "Postgres-2.9.4-15.dmg", + BrowserDownloadURL: "https://example.invalid/Postgres-2.9.4-15.dmg", + Size: 99 * 1024 * 1024, + }, + { + Name: "Postgres-2.9.4-16.dmg", + BrowserDownloadURL: "https://example.invalid/Postgres-2.9.4-16.dmg", + Size: 107 * 1024 * 1024, + }, + { + Name: "Postgres-2.9.4-17.dmg", + BrowserDownloadURL: "https://example.invalid/Postgres-2.9.4-17.dmg", + Size: 113 * 1024 * 1024, + }, + { + Name: "Postgres-2.9.4-18.dmg", + BrowserDownloadURL: "https://example.invalid/Postgres-2.9.4-18.dmg", + Size: 116 * 1024 * 1024, + }, + }, +} + +// TestLatestPostgresAppAssetsParsing verifies that the asset filter picks +// up only single-version .dmg files, drops the multi-version bundle, and +// returns them sorted newest-major-first. +func TestLatestPostgresAppAssetsParsing(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(fakeGithubRelease) + })) + defer server.Close() + + assets, err := parseAssetsFromURL(server.URL) + if err != nil { + t.Fatalf("parseAssetsFromURL: %v", err) + } + + wantMajors := []string{"18", "17", "16", "15", "14"} + if len(assets) != len(wantMajors) { + t.Fatalf("expected %d single-version assets, got %d: %+v", + len(wantMajors), len(assets), assets) + } + for i, want := range wantMajors { + if assets[i].Major != want { + t.Errorf("assets[%d].Major = %q, want %q", i, assets[i].Major, want) + } + if assets[i].AppVersion != "2.9.4" { + t.Errorf("assets[%d].AppVersion = %q, want %q", i, assets[i].AppVersion, "2.9.4") + } + } + + // The multi-version bundle must not appear. + for _, a := range assets { + if a.Major == "" || len(a.Major) > 2 { + t.Errorf("unexpected asset slipped through filter: %+v", a) + } + } +} diff --git a/scripts/checksums/README.md b/scripts/checksums/README.md new file mode 100644 index 0000000..5e316c8 --- /dev/null +++ b/scripts/checksums/README.md @@ -0,0 +1,40 @@ +# Tarball checksum scrapers + +These scripts populate the `checksum` field in `downloads/tarball_list.json` +by scraping the official publisher pages. + +## Usage + +```bash +# MySQL (MD5, scraped from dev.mysql.com download pages) +python3 scripts/checksums/scrape_mysql_checksums.py downloads/tarball_list.json + +# MariaDB + Percona Server (SHA256, from upstream sidecar files) +python3 scripts/checksums/scrape_mariadb_percona_checksums.py downloads/tarball_list.json +``` + +Both scripts are idempotent: they skip entries that already have a checksum. +After running, verify there are no missing checksums other than TiDB (whose +"master" rolling tarballs are legitimately unversioned): + +```bash +jq -r '.Tarballs[] | select(.checksum == null or .checksum == "") | "\(.flavor) \(.version) \(.name)"' \ + downloads/tarball_list.json +``` + +## When to re-run + +Re-run these scripts after adding new MySQL / MariaDB / Percona versions +to `tarball_list.json`. A CI lint (tracked in [#84](https://github.com/ProxySQL/dbdeployer/issues/84)) +will fail if new non-TiDB entries are added without a checksum. + +## Upstream sources + +- **MySQL**: MD5 scraped from + `https://downloads.mysql.com/archives/community/?tpl=version&os=&version=` + (fallback to `https://dev.mysql.com/downloads/mysql/.html` for the + current-GA release). Requires a primed cookie jar (`/tmp/mysql-scrape-cookies.txt`). +- **MariaDB**: SHA256 read from the `sha256sums.txt` file in each + `archive.mariadb.org/mariadb-X.Y.Z/bintar-.../` directory. +- **Percona Server**: SHA256 read from the per-tarball `.sha256sum` sidecar + file on `downloads.percona.com`. diff --git a/scripts/checksums/scrape_mariadb_percona_checksums.py b/scripts/checksums/scrape_mariadb_percona_checksums.py new file mode 100755 index 0000000..8503cbd --- /dev/null +++ b/scripts/checksums/scrape_mariadb_percona_checksums.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +""" +Populate SHA256 checksums for MariaDB and Percona Server entries in +tarball_list.json. + +MariaDB: archive.mariadb.org publishes sha256sums.txt in each directory. +Percona: downloads.percona.com publishes .sha256sum sidecar per file. + +Usage: scrape_mariadb_percona.py +""" +import json +import re +import subprocess +import sys +from pathlib import Path +from urllib.parse import urlparse + + +def fetch(url: str) -> tuple[int, str]: + """Return (http_status, body) for a URL.""" + try: + out = subprocess.run( + ["curl", "-sL", "-w", "\n%{http_code}", url], + capture_output=True, text=True, check=True, + ).stdout + except subprocess.CalledProcessError as e: + return (0, "") + # Last line is the HTTP code + body, _, code = out.rpartition("\n") + try: + return (int(code), body) + except ValueError: + return (0, out) + + +def fetch_mariadb_dir(url: str) -> dict[str, str]: + """For a MariaDB bintar URL, fetch the directory's sha256sums.txt and + return {filename: sha256}.""" + # url: https://archive.mariadb.org/mariadb-10.11.9/bintar-linux-systemd-x86_64/mariadb-10.11.9-linux-systemd-x86_64.tar.gz + parent = url.rsplit("/", 1)[0] + "/sha256sums.txt" + code, body = fetch(parent) + if code != 200: + return {} + result: dict[str, str] = {} + for line in body.splitlines(): + # lines look like: ./ + m = re.match(r"([0-9a-f]{64})\s+\.?/?(.+)$", line.strip()) + if m: + result[m.group(2)] = m.group(1) + return result + + +def fetch_percona_sum(url: str) -> str | None: + """Fetch .sha256sum sidecar for a single tarball.""" + sum_url = url + ".sha256sum" + code, body = fetch(sum_url) + if code != 200: + return None + m = re.match(r"([0-9a-f]{64})\s+", body.strip()) + return m.group(1) if m else None + + +def main(): + if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + + json_path = Path(sys.argv[1]) + data = json.loads(json_path.read_text()) + tarballs = data["Tarballs"] + + mariadb_dir_cache: dict[str, dict[str, str]] = {} + + updated = 0 + missing = [] + + for tb in tarballs: + if tb.get("checksum"): + continue + flavor = tb.get("flavor") + name = tb["name"] + url = tb.get("url", "") + + if flavor == "mariadb": + # Cache directory listings + dir_url = url.rsplit("/", 1)[0] + if dir_url not in mariadb_dir_cache: + mariadb_dir_cache[dir_url] = fetch_mariadb_dir(url) + sums = mariadb_dir_cache[dir_url] + if name in sums: + tb["checksum"] = f"SHA256:{sums[name]}" + updated += 1 + else: + missing.append((flavor, tb["version"], name)) + + elif flavor == "percona": + sha = fetch_percona_sum(url) + if sha: + tb["checksum"] = f"SHA256:{sha}" + updated += 1 + else: + missing.append((flavor, tb["version"], name)) + + json_path.write_text(json.dumps(data, indent=2) + "\n") + print(f"Updated {updated} entries") + if missing: + print(f"\nStill missing: {len(missing)}") + for row in missing[:20]: + print(f" {row}") + + +if __name__ == "__main__": + main() diff --git a/scripts/checksums/scrape_mysql_checksums.py b/scripts/checksums/scrape_mysql_checksums.py new file mode 100755 index 0000000..2da948a --- /dev/null +++ b/scripts/checksums/scrape_mysql_checksums.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +""" +Scrape MD5 checksums from MySQL download archive pages and update +tarball_list.json in-place. + +Usage: scrape_checksums.py +""" +import json +import re +import sys +import subprocess +from pathlib import Path + +ARCHIVE_URL = "https://downloads.mysql.com/archives/community/" +CURRENT_URL_TMPL = "https://dev.mysql.com/downloads/mysql/{major}.html" + +# OS id in the form +OS_LINUX = "2" # Linux - Generic +OS_MACOS = "33" # macOS + +# rowspan pattern: +# () +# MD5: +ROW_RE = re.compile( + r'\((?P[^)]+\.tar\.(?:gz|xz))\).*?md5">(?P[0-9a-f]{32})', + re.DOTALL, +) + + +def fetch(url: str, cookies: str | None = None) -> str: + """Fetch a URL via curl and return the body.""" + cmd = ["curl", "-sL"] + if cookies: + cmd += ["-b", cookies] + cmd.append(url) + return subprocess.check_output(cmd, text=True) + + +def prime_cookies() -> str: + """Prime cookies by fetching the archives page once.""" + jar = "/tmp/mysql-scrape-cookies.txt" + subprocess.check_call( + ["curl", "-sL", "-c", jar, ARCHIVE_URL, "-o", "/dev/null"], + stderr=subprocess.DEVNULL, + ) + return jar + + +def scrape_page(url: str, cookies: str | None = None) -> dict[str, str]: + """Extract {filename: md5} from a download page.""" + html = fetch(url, cookies) + result: dict[str, str] = {} + for m in ROW_RE.finditer(html): + result[m.group("file")] = m.group("md5") + return result + + +def scrape_archived(version: str, os_id: str, cookies: str) -> dict[str, str]: + url = f"{ARCHIVE_URL}?tpl=version&os={os_id}&version={version}" + return scrape_page(url, cookies) + + +def scrape_current(major: str, os_id: str) -> dict[str, str]: + url = f"{CURRENT_URL_TMPL.format(major=major)}?os={os_id}" + return scrape_page(url) + + +def main(): + if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + + json_path = Path(sys.argv[1]) + data = json.loads(json_path.read_text()) + tarballs = data["Tarballs"] + + # Find MySQL entries needing checksums, grouped by version + by_version: dict[str, list[dict]] = {} + for tb in tarballs: + if tb.get("flavor") != "mysql": + continue + if tb.get("checksum"): + continue + by_version.setdefault(tb["version"], []).append(tb) + + if not by_version: + print("No MySQL entries need updating") + return + + cookies = prime_cookies() + updated = 0 + missing = [] + + for version, entries in sorted(by_version.items()): + major = ".".join(version.split(".")[:2]) # e.g. 8.4 + # Scrape both Linux and macOS pages — start with archived URL, + # fall back to the current-GA page if the archived page serves + # a different version (happens for the latest release). + for os_id in (OS_LINUX, OS_MACOS): + files = scrape_archived(version, os_id, cookies) + # If we didn't find entries for this version, try current-GA page + has_version = any(version in f for f in files) + if not has_version: + files = scrape_current(major, os_id) + for tb in entries: + name = tb["name"] + if name in files: + tb["checksum"] = f"MD5:{files[name]}" + updated += 1 + + # Second pass to collect still-missing entries + for version, entries in by_version.items(): + for tb in entries: + if not tb.get("checksum"): + missing.append((version, tb["name"])) + + json_path.write_text(json.dumps(data, indent=2) + "\n") + print(f"Updated {updated} entries") + if missing: + print(f"\nStill missing checksums for {len(missing)} files:") + for v, name in sorted(missing)[:30]: + print(f" {v} {name}") + if len(missing) > 30: + print(f" ... and {len(missing) - 30} more") + + +if __name__ == "__main__": + main()